laika-framework

Models & Database

Laika’s data layer is The Model module of laikait/laika-engine — a PDO query builder and schema builder for MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle and Firebird. This page covers setting up models, schemas and migrations; the Query Builder page is the full method reference.

use App\Model\UsersModel;

$users = new UsersModel();

$active = $users->where(['is_active' => 'yes'])->order('id', 'DESC')->limit(20)->get();
$one    = $users->find(5);
$id     = $users->insert(['email' => 'ann@example.com', 'first_name' => 'Ann']);
$users->where(['id' => $id])->update(['first_name' => 'Anne']);

Connections

Connections are defined in lf-config/database.php, one per top-level key:

return [
    'default' => [
        'driver'   => 'mysql',
        'host'     => 'localhost',
        'port'     => 3306,
        'database' => 'test',
        'username' => 'root',
        'password' => '',
    ],
];

A connection is registered from this file the first time a model or schema asks for it. Note that creating a model opens the connection — new UsersModel() connects in its constructor.

Connection Keys

Key Drivers Notes
driver all mysql/mariadb, pgsql/postgres, sqlite/sqlite3, sqlsrv, oci/oracle, firebird/ibase
host, port all but SQLite Default host 127.0.0.1; default port per driver (3306, 5432, 1433, 1521, 3050)
database all Database name, or for SQLite the file path (or :memory:)
username, password all but SQLite  
charset mysql, oci, firebird Default utf8mb4 (mysql), AL32UTF8 (oci), UTF8 (firebird)
timezone mysql Session time zone, e.g. '+00:00'
unix_socket mysql Local socket instead of host/port
sslmode pgsql e.g. require
encrypt, trust_server_certificate sqlsrv Booleans
service_name, tns oci Instead of database
options all PDO attributes, overriding the defaults (ERRMODE_EXCEPTION, FETCH_ASSOC, EMULATE_PREPARES => false)

Rows come back as associative arrays (PDO::FETCH_ASSOC).

Multiple Connections

Add more keys to database.php and point a model at one with its $connection property or constructor argument:

// lf-config/database.php
return [
    'default'   => [ /* ... */ ],
    'analytics' => ['driver' => 'pgsql', 'host' => 'reports.internal', 'database' => 'analytics', 'username' => 'app', 'password' => 'secret'],
];
$rows = (new ReportModel('analytics'))->where(['year' => 2025])->get();

Each connection is registered under its own name the first time something uses it — a model, Schema::on('analytics'), the session Init::model('analytics') driver, a token guard with 'connection' => 'analytics', or queue.connection. A name missing from database.php throws.

Older packages (laika-core 5.1.1, laika-model 4.0.6 and earlier) registered a second connection under the name default — overwriting the default connection — and then failed. On those versions, register extra connections yourself in a hook file: Laika\Engine\Model\Connection::add(config('database', 'analytics'), 'analytics');

Defining a Model

php laika model:make UsersModel --table=users

This creates lf-app/Model/UsersModel.php and lf-app/Schema/UsersModelSchema.php.

namespace App\Model;

use Laika\Engine\Model\Model;

class UsersModel extends Model
{
    protected string $table           = 'users';
    protected string $id              = 'id';        // primary key column
    protected string $uid             = 'uid';       // public id column, used by find('abc...')
    protected string $connection      = 'default';
    protected bool   $softDelete      = false;
    protected string $deletedAtColumn = 'deleted_at';

    /** @var array<string,string> */
    protected array $casts = [
        'id'          => 'int',
        'is_admin'    => 'bool',
        'preferences' => 'json',
    ];
}
Property Meaning
$table Table name (required)
$id Primary key column. find(5) looks it up.
$uid Public identifier column. find('3f2b…') (any non-numeric value) looks it up.
$connection Connection name from database.php
$softDelete delete() sets $deletedAtColumn instead of deleting, and reads hide those rows
$casts Convert columns when reading: int, float, decimal (string), bool, json/array, serialize, string

Each model instance is a query builder. The builder resets after every query, so one instance can run many queries — but build one query at a time on it.

Basic CRUD

$users = new UsersModel();

// Read
$all   = $users->get();
$page  = $users->where(['is_active' => 'yes'])->order('id', 'DESC')->limit(20)->page(2)->get();
$user  = $users->find(5);                              // by id, or by uid for a non-numeric value
$user  = $users->where(['email' => $email])->first();  // null if not found
$user  = $users->where(['email' => $email])->firstOrFail(); // throws ModelException
$count = $users->where(['is_active' => 'yes'])->count();

// Create — returns the new id as a string
$id = $users->insert(['email' => 'ann@example.com', 'first_name' => 'Ann']);

// Several rows at once
$users->insert([
    ['email' => 'a@example.com', 'first_name' => 'A'],
    ['email' => 'b@example.com', 'first_name' => 'B'],
]);

// Update / delete — both return the affected row count
$users->where(['id' => $id])->update(['first_name' => 'Anne']);
$users->where(['id' => $id])->delete();

Safety rails — these throw Laika\Engine\Model\Exceptions\ModelException without a where():

insert() returns '' on Oracle and Firebird, which have no auto-generated id to report.

Soft Deletes

class PostsModel extends Model
{
    protected string $table      = 'posts';
    protected bool   $softDelete = true;
}

$posts = new PostsModel();
$posts->where(['id' => 3])->delete();         // sets deleted_at = now
$posts->get();                                // trashed rows hidden
$posts->withTrash()->get();                   // include them
$posts->onlyTrashed()->get();                 // only them
$posts->where(['id' => 3])->restore();        // deleted_at = NULL
$posts->soft(false)->where(['id' => 3])->delete(); // really delete this once

The deleted_at column must default to NULL. Blueprint::timestamp() adds DEFAULT CURRENT_TIMESTAMP, so $t->timestamp('deleted_at')->nullable() fills the column on every insert and every row counts as deleted. Use $t->deleted() (nullable, default null, indexed) — or add ->default(null). Schemas generated by laika-cli 3.0.9 and earlier have this problem; add ->default(null) to their deleted_at column, and clear the column in rows already inserted.

Schemas

A schema owns one table’s DDL. Schemas live in lf-app/Schema/ and extend Laika\Engine\Model\Contract\SchemaAbstract:

namespace App\Schema;

use Laika\Engine\Model\Schema\Schema;
use Laika\Engine\Model\Schema\Blueprint;
use Laika\Engine\Model\Contract\SchemaAbstract;

class UsersModelSchema extends SchemaAbstract
{
    protected string $table = 'users';

    public function up(): void
    {
        Schema::on($this->connection)->createIfNotExists($this->table, function (Blueprint $t) {
            $t->bigId('id');
            $t->uid('uid');                          // UUID column + unique index
            $t->string('email');
            $t->string('first_name');
            $t->string('last_name')->nullable()->default(null);
            $t->enum('is_active', ['yes', 'no'])->default('no');
            $t->timestamps();                        // created_at, updated_at
            $t->deleted();                           // deleted_at, nullable, indexed

            $t->unique('email');
        });
    }

    public function seed(): void
    {
        // optional — runs after every schema's up(), on every app:migrate
    }
}
Method Called by
up(): void Required. php laika app:migrate
seed(): void Optional. app:migrate, after every schema’s up()
down(): void Optional; defaults to dropping $table. No CLI command calls it.

The full list of column types, modifiers, indexes and foreign keys is in Query Builder → Schema Builder.

Running Migrations

php laika app:migrate                 # every discovered schema
php laika app:migrate --table=users   # one schema, by TABLE name
php laika schema:list                 # what app:migrate will run

app:migrate runs up() on every schema, then seed() on every schema, with foreign-key checks disabled around each call. It discovers every class in lf-app/Schema/, plus schemas that installed packages declare.

Some framework tables are not created by app:migrate — they install themselves on first use, which needs CREATE rights once:

Table Created by
options, activities The Core module, the first time Option/Activity is used
sessions Init::model('default', install: true)
auth_tokens a token guard with 'install' => true
laika_queue_jobs, laika_failed_jobs nothing — create them yourself, see Queue → Choosing a Driver

See Deployment for creating them ahead of time.

Security

CLI Reference

Command Description
php laika model:make <name> [--table=] [--id=id] [--uid=uid] [--connection=default] Create a model and its schema
php laika model:list List models, keyed by table
php laika model:remove <name> Delete a model and its schema
php laika model:rename --old=<name> --new=<name> [--table=] [--id=] [--uid=] Rename a model and its schema
php laika schema:list List schemas, keyed by table
php laika app:migrate [--table=<table>] Run schemas

See Also