Skip to content
Nibiru docsv0.9.2

Why Nibiru, not Laravel

Five things Nibiru does differently from Laravel and Symfony — each backed by real code from production.

Stable Reading time ~ 4 min Edit on GitHub

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 — 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.


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.

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 for how the CMS module composes itself.


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

Section titled “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.”

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

Section titled “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.

-- 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 for the Pdo adapter that runs queries like this.


4. Role-based ACL without middleware chains

Section titled “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:

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.

// 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 for session and ACL details.


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.

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 for the pattern.


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 →