Skip to content
Technomantus Corvi edited this page Sep 5, 2026 · 1 revision

Models

Every model extends Model and declares its table name:

<?php

class ArticleModel extends Model
{
    protected static string $table = 'articles';
    protected static bool $timestamps = true; // auto created_at / updated_at
}

Naming convention: model classes and files use the XxxModel suffix (e.g. TodoModel.phpclass TodoModel) to avoid ambiguity with the table name or with unrelated classes. The autoloader matches the file name to the class name exactly, so renaming one always requires renaming the other.

Model API

// ── Read ─────────────────────────────────────────────────────────────
ArticleModel::all()                        // All records
ArticleModel::all('title', 'DESC')         // With custom order
ArticleModel::find(5)                      // By ID → array | null
ArticleModel::where('active', 1)           // Simple filter
ArticleModel::where('views', '>', 100)     // With operator (=, !=, <, >, <=, >=, LIKE)
ArticleModel::first()                      // First record
ArticleModel::count()                      // Total records

// Pagination
$result = ArticleModel::paginate(page: 1, perPage: 10);
// $result['data']    → records for this page
// $result['total']   → total records in the table
// $result['pages']   → total number of pages
// $result['current'] → current page

// ── Write ────────────────────────────────────────────────────────────
$id = ArticleModel::create(['title' => 'Hello']);   // → int (inserted ID)
ArticleModel::update(5, ['title' => 'Updated']);    // → bool
ArticleModel::delete(5);                            // → bool

If $timestamps = true, create() fills created_at/updated_at automatically, and update() refreshes updated_at.

Identifier safety

Column and table identifiers are validated (rejected if not a simple [a-zA-Z_][a-zA-Z0-9_]* pattern) and quoted automatically according to the active driver — backticks for MySQL, double quotes for PostgreSQL and SQLite. You never need to escape them yourself, and dynamic column names (e.g. from user input) are rejected rather than interpolated unsafely.

Database drivers

Set DB_DRIVER in .env to mysql, pgsql, or sqlite. For SQLite, also set DB_PATH to the .sqlite file's location (created automatically on first write if it doesn't exist).

DB_DRIVER=mysql   # mysql | pgsql | sqlite
DB_PATH=          # only used when DB_DRIVER=sqlite

The connection is lazy — if no route ever calls a model, no connection is ever opened.

Custom queries beyond the base API

For anything the generic API doesn't cover, drop into Database::connect() directly inside your model:

class TodoModel extends Model
{
    protected static string $table = 'todo';

    public static function allOrdered(): array
    {
        $table = static::quoteIdent(static::$table);
        $pdo   = Database::connect();
        $stmt  = $pdo->query("SELECT * FROM $table ORDER BY completed ASC, created_at DESC");
        return $stmt->fetchAll();
    }
}

Use static::quoteIdent() (inherited from Model) for any identifier you interpolate, so your custom queries stay driver-agnostic and safe.

Optional connections: Redis and MongoDB

See Optional Connections.

Clone this wiki locally