---
title: Models
description: How Nibiru auto-generates model classes from your database schema, and how to extend them.
---

Nibiru's model layer is *schema-first*. You don't write models by hand — the framework reads your database and generates one PHP class per table on every boot.

## How generation works

When `[GENERATOR] database = true` in your INI, the dispatcher runs `new Model(false)` on each request, which:

1. Connects with the active driver.
2. Lists tables in the configured database (`information_schema.tables` for PG, `SHOW TABLES` for MySQL).
3. For each table, writes `application/model/<table>.php` containing a class that extends the relevant `Db` adapter and embeds a `TABLE` constant describing the columns.

Re-running is cheap: the generator overwrites only when the schema has changed, so checked-in models keep their handwritten methods *if* you turn `database = false` after the first run.

```ini
[GENERATOR]
database          = true     ; regenerate models on each request
database.overwrite = true    ; if false, generator won't touch existing files
```

In production set `database = false` so models aren't regenerated on every hit.

## A generated model

For a `users` table with columns `user_id, user_login, user_pass, user_email`:

```php
<?php
namespace Nibiru\Model;
use Nibiru\Adapter\MySQL\Db;

class users extends Db
{
    const TABLE = [
        'table' => 'users',
        'field' => [
            'user_id'    => 'user_id',
            'user_login' => 'user_login',
            'user_pass'  => 'user_pass',
            'user_email' => 'user_email',
        ],
    ];

    public function __construct() {
        self::initTable(self::TABLE);
    }
}
```

The `Db` adapter gives you simple CRUD helpers via `Pdo::` (or the active driver):

```php
$users = new \Nibiru\Model\users();

// Read by primary key
$row = \Nibiru\Pdo::fetchRowInArrayById('users', ['user_id' => 42]);

// Read all
$all = \Nibiru\Pdo::fetchAll('SELECT * FROM users WHERE user_account_active = 1');

// Insert
\Nibiru\Pdo::insert('users', [
    'user_login' => 'marduk',
    'user_email' => 'marduk@nibiru.local',
]);

// Update
\Nibiru\Pdo::update('users',
    ['user_email' => 'new@nibiru.local'],
    ['user_id' => 42]);

// Delete
\Nibiru\Pdo::delete('users', ['user_id' => 42]);
```

## Custom queries

The generated class is a plain PHP class — add methods freely:

```php
namespace Nibiru\Model;
use Nibiru\Adapter\MySQL\Db;
use Nibiru\Pdo;

class documentation extends Db
{
    const TABLE = [
        'table' => 'documentation',
        'field' => [
            'id' => 'id', 'title' => 'title', 'slug' => 'slug',
            'content' => 'content', 'category' => 'category',
            'version' => 'version',
        ],
    ];

    public function __construct() { self::initTable(self::TABLE); }

    public function getBySlug(string $slug): ?array {
        return Pdo::fetchRowInArrayById(
            self::TABLE['table'],
            [self::TABLE['field']['slug'] => $slug]
        ) ?: null;
    }

    public function search(string $query): array {
        $sql = 'SELECT * FROM ' . self::TABLE['table']
             . ' WHERE title LIKE :q OR content LIKE :q ORDER BY title';
        return Pdo::fetchAll($sql, [':q' => '%' . $query . '%']);
    }
}
```

If the generator runs again with `database.overwrite = true` it will replace this file. Keep custom methods in a child class or set `database.overwrite = false` after first generation.

## Pattern: thin model + module plugin

For complex domains (auth, billing, analytics), the showcase apps put query methods on a **module plugin** rather than the bare model. The plugin owns the business rules; the model is just a typed handle to the table:

```
application/module/users/
├── plugins/
│   └── user.php         # User::isAuthorized(), User::loginForm(), …
└── traits/
    └── userForm.php
```

```php
namespace Nibiru\Module\Users\Plugin;
use Nibiru\Model\users;

class User {
    private users $usersModel;
    public function __construct() { $this->usersModel = new users(); }

    public function isAuthorized(): bool {
        return isset($_SESSION['auth']['user_id']);
    }

    public function findByLogin(string $login): ?array {
        return \Nibiru\Pdo::fetchRow(
            'SELECT * FROM users WHERE user_login = :login',
            [':login' => $login]
        ) ?: null;
    }
}
```

This keeps controllers thin (`$this->user->isAuthorized()`) while leaving the generated model untouched.

## Multi-driver gotchas

The generated `Db` adapter is driver-specific. If you switch from MySQL to PostgreSQL, regenerate models so they extend the right base class:

- MySQL / PDO → `Nibiru\Adapter\MySQL\Db`
- PostgreSQL (libpq) → `Nibiru\Adapter\PostgreSQL\Db`
- ODBC → `Nibiru\Adapter\Odbc\Db`

Same query helpers, different adapter under the hood.

## When to skip the generator

Read-only databases, vendor systems, or any schema you don't control: set `database = false`, hand-write minimal model classes that match the columns you actually use, and check them in.
