---
title: Why Nibiru, not Laravel
description: Five things Nibiru does differently from Laravel and Symfony — each backed by real code from production.
---

Most PHP framework comparisons are vibes. This one is grep-able.

The five differentiators below are pulled from one real revenue-generating site: **[maschinen-stockert.de](/en/showcase/projects/)** — a multilingual industrial machinery marketplace. **36 controllers, 18 modules, 348 templates, 161 SQL migrations, 37,369 lines of PHP** across two repos. Every claim links to a file:line reference inside the codebases, so you can verify it.

If you've shipped on Laravel or Symfony, you'll recognise the patterns Nibiru *doesn't* have. That's the point.

---

## 1. CMS layer baked into the framework

In Laravel, page copy lives in Blade files (`{{ __('page.title') }}`) or translation JSON. To change it, a developer edits a file and deploys. In Nibiru, page copy lives in the database, keyed by `<controller>/<action>` + language, and editors update it from the admin UI **without touching code**.

```php
// data.maschinen-stockert.de/application/controller/maschineController.php
public function pageAction()
{
    $controllerPath = $this->getController() . '/' . $this->getRequest('_action', 'page');

    foreach (Cms::init($this->getController())
                 ->loadCmsTemplateTextsByControllerPath($controllerPath, $this->language)
             as $t) {
        View::assign([
            $t['cms_template_texts_text_identifier']
                => $t['cms_template_texts_text_content']
        ]);
    }
}
```

**Net effect**: marketing changes a headline in the editor, refreshes the page, ships. No PR, no deploy, no developer in the loop. That's not a CMS plugin — the framework's `Cms` module is built in.

→ See [Modules](/en/core/modules/) for how the CMS module composes itself.

---

## 2. Modules compose via traits, not via a service container

Laravel's answer to a 5,000-line class is service providers and dependency injection. Nibiru's answer is **traits**.

The CMS module on `maschinen-stockert.de` is built from **13 traits**, each a single responsibility — `CmsStore`, `TextsForm`, `PageBuilderForm`, `CmsPageStructureModifier`, `FormElements`, … The main `Cms` class is 30 lines that pull them together. Adding a new feature is *"create a trait, include it, done."*

```php
// application/module/cms/cms.php
class Cms implements Interfaces\Cms, SplSubject
{
    use Traits\CmsStore;
    use Traits\TextsForm;
    use Traits\PageBuilderForm;
    use Traits\CmsPageStructureModifier;
    use Traits\FormElements;
    // …8 more traits
}
```

**Net effect**: zero constructor injection, zero service-provider registration, zero `bind()` / `singleton()` calls in a config file. A new developer can grep for the trait name and see all callers.

---

## 3. Direct SQL with JSON aggregation, not an ORM

Eloquent and Doctrine ship with a tax: every relationship loaded is potentially an N+1 query, and every aggregation pushed into PHP is wasted memory. Nibiru leans on the database where it's strong.

```sql
-- application/module/machineryscout/traits/machinesElasticSearch.php
SELECT
    m.ms_machines_id AS machine_id,
    m.ms_machines_name AS machine_name,
    JSON_ARRAYAGG(
        DISTINCT JSON_OBJECT(
            'attribute_name',  ma.ms_machine_attributes_attribute_name,
            'attribute_value', mav.ms_machine_attribute_values_value,
            'attribute_type',  ma.ms_machine_attributes_attribute_type
        )
    ) AS attributes,
    JSON_ARRAYAGG(DISTINCT mi.ms_machine_images_filename
        ORDER BY mi.ms_machine_images_sort_order ASC) AS images
FROM ms_machines m
LEFT JOIN ms_machine_attribute_values mav ON mav.ms_machine_attribute_values_machine_id = m.ms_machines_id
LEFT JOIN ms_machine_attributes ma ON mav.ms_machine_attribute_values_attribute_id = ma.ms_machine_attributes_id
LEFT JOIN ms_machine_images mi ON mi.ms_machines_id = m.ms_machines_id
WHERE m.ms_active = 1
GROUP BY m.ms_machines_id;
```

One query. JSON-aggregated attributes and images, indexed-from-the-database. The result feeds straight into Elasticsearch, ready to power range queries on dimensions like *"2500 × 1200 mm"*.

**Net effect**: N+1 problems don't exist. Auditable, profileable, fast.

→ See [Database & Migrations](/en/core/database/) for the `Pdo` adapter that runs queries like this.

---

## 4. Role-based ACL without middleware chains

In Laravel, authorization is a stack: `auth` middleware → policy class → gate → controller. In Nibiru, it's three lines in the constructor:

```php
// data.maschinen-stockert.de/application/controller/adminController.php
public function __construct() {
    parent::__construct();
    $this->user = new User();
    $this->acl  = new Acl();
    $this->acl->init();
    $this->user->validate();
}
```

`$this->acl->init()` loads the role / permission set for the current session. `$this->user->validate()` checks the session is good. If either fails, the request returns the login page. Per-action checks (`$this->acl->can('edit', 'pages')`) live in actions that need them.

The API controller takes the inverse approach: a whitelist of public endpoints in the constructor, auth required for everything else.

```php
// public endpoints can be listed up-front, auth wraps the rest.
if (in_array($action, ['category', 'machines', 'ollama', 'team'])) {
    // public, skip auth
} else {
    $this->user = new User();
    $this->acl  = new Acl();
    $this->acl->init();
    $this->user->validate();
}
```

**Net effect**: authorization is one greppable expression. No middleware ordering bugs. No "why is this endpoint public?" mysteries.

→ See [Auth](/en/core/auth/) for session and ACL details.

---

## 5. Observer pattern, not event dispatcher

Laravel's events fan out via a dispatcher with closures, listeners, and queued job records. Nibiru uses PHP's standard-library `SplSubject` / `SplObserver` interfaces. Same pattern, two interfaces, no abstraction layer.

```php
class Machineryscout implements IModule, \SplSubject
{
    private \SplObjectStorage $observers;

    public function attach(\SplObserver $o): void { $this->observers->attach($o); }
    public function detach(\SplObserver $o): void { $this->observers->detach($o); }
    public function notify(): void {
        foreach ($this->observers as $o) $o->update($this);
    }

    public function indexMachines(): void {
        // …do the work…
        $this->notify();   // analytics, cache invalidator, audit log all see it.
    }
}
```

**Net effect**: state changes are synchronous, deterministic, debuggable. No queue worker. No "did the listener fire?" mystery. Add an observer in the controller and it's wired.

→ See [Modules](/en/core/modules/#the-observer-pattern) for the pattern.

---

## What this gets you, in two sentences

A solo developer or a small team can build and *operate* a real production app — like one selling industrial machinery in 12 countries — with **less framework to learn**, **fewer abstractions to debug**, and **less ceremony per feature**. The trade-off is that if you'd rather have the framework hide every database query and orchestrate every event, you'll be happier somewhere else.

If you'd rather see your code, take it.

→ [Read the showcase →](/en/showcase/projects/)
