Skip to content

Defining Models

Muhammet Şafak edited this page May 24, 2026 · 1 revision

Defining Models

A model is a subclass of InitORM\ORM\Model. Configuration is declarative: you set protected properties on the class, and the model constructor wires up everything else. Every property is optional with sensible defaults.

The property cheat-sheet

Property Type Default What it does
$schema string (derived) Table name. Auto-derived from class short name when unset.
$schemaId string 'id' PK column. Lifted out of update()'s $set into a WHERE; used by save().
$entity class-string Entity::class Class used to hydrate read() rows via PDO::FETCH_CLASS.
$credentials array|null null Standalone connection credentials. Null → shared DB facade.
$writable bool true False blocks create() / createBatch() with WritableException.
$readable bool true False blocks read() with ReadableException.
$updatable bool true False blocks update() / updateBatch() with UpdatableException.
$deletable bool true False blocks delete() with DeletableException.
$createdField string|null null Auto-filled with date($timestampFormat) on every create. Disabled when null.
$updatedField string|null null Auto-filled with date($timestampFormat) on every update. Disabled when null.
$useSoftDeletes bool false True → delete() sets $deletedField instead of issuing DELETE.
$deletedField string|null null Required when $useSoftDeletes is true. Soft-delete marker column.
$timestampFormat string 'Y-m-d H:i:s' date() format used for all three timestamp columns.

$schema — the table name

class Posts extends \InitORM\ORM\Model
{
    protected string $schema = 'posts';
}

If you omit $schema, the constructor derives it from the class short name using snake_case conversion:

class PostCategory extends \InitORM\ORM\Model {}

(new PostCategory())->getSchema();   // 'post_category'

Conversion rules:

Class short name Derived schema
Posts posts
Post post
PostCategory post_category
PostCategoryTag post_category_tag
XMLParser xml_parser
HTTPRequest http_request
User2Login user2_login

For anything more exotic (pluralisation, table prefixes, multi-tenancy), set $schema explicitly.

$schemaId — primary key column

class Posts extends \InitORM\ORM\Model
{
    protected string $schema   = 'posts';
    protected string $schemaId = 'id';   // default
}

Two methods use it:

  1. update() lifts the PK out of $set into a WHERE — so you never accidentally try to write the PK:

    $posts->update(['id' => 5, 'title' => 'X']);
    // UPDATE posts SET title = :title WHERE id = :id
  2. save(Entity) checks the PK to decide between create() and update():

    $entity = new PostEntity(['title' => 'New']);
    $posts->save($entity);              // → create()
    
    $entity = new PostEntity(['id' => 1, 'title' => 'Edit']);
    $posts->save($entity);              // → update()

For non-id keys, set $schemaId explicitly:

class Sessions extends \InitORM\ORM\Model
{
    protected string $schema   = 'sessions';
    protected string $schemaId = 'session_id';
}

$entity — row hydration class

class Posts extends \InitORM\ORM\Model
{
    protected string $schema = 'posts';
    protected string $entity = \App\Entity\PostEntity::class;
}

read() hands the SELECT statement to the DataMapper with asClass($this->entity). Subsequent fetches return instances of that class. The default is the bare Entity — perfectly usable for projects that don't need accessor / mutator hooks.

The class must accept a no-argument constructor (or have a ?array $data = [] parameter, which Entity does). See Entities.

$credentials — standalone connection

class ReportsEvents extends \InitORM\ORM\Model
{
    protected string $schema = 'events';

    protected ?array $credentials = [
        'driver'   => 'pgsql',
        'host'     => 'reports.internal',
        'database' => 'reports',
        'username' => 'reports_ro',
        'password' => '',
    ];
}

When $credentials is non-null, the constructor calls DB::connect($credentials) to build a fresh Database. This bypasses the shared facade. See Multiple Connections.

When $credentials is null (default), the constructor calls DB::getDatabase() — which throws if no DB::createImmutable() has been done yet.

Permission gates

class Configuration extends \InitORM\ORM\Model
{
    protected string $schema    = 'configuration';
    protected bool   $writable  = false;
    protected bool   $updatable = false;
    protected bool   $deletable = false;
}

(new Configuration())->read()->rows();  // OK
(new Configuration())->create([...]);   // WritableException

Each flag is checked at the very top of the corresponding method — before any SQL is built or sent. See Permission Gates for the full pattern.

Timestamps

class Posts extends \InitORM\ORM\Model
{
    protected string $schema = 'posts';

    protected ?string $createdField = 'created_at';
    protected ?string $updatedField = 'updated_at';

    protected bool    $useSoftDeletes = true;
    protected ?string $deletedField   = 'deleted_at';

    protected string  $timestampFormat = 'Y-m-d H:i:s';   // default
}

See Timestamps and Soft Deletes for the details — including the soft-delete invariant validation that fires at construction.

Construction lifecycle

__construct() runs three steps in order:

  1. Auto-derive $schema if it was not set on the subclass.
  2. Validate the soft-delete invariant$useSoftDeletes = true without a $deletedField raises ModelException.
  3. Acquire the DatabaseDB::getDatabase() or DB::connect($credentials).

Subclasses overriding __construct should set their properties before calling parent::__construct():

class Posts extends \InitORM\ORM\Model
{
    public function __construct(string $schema = 'posts')
    {
        $this->schema = $schema;
        parent::__construct();   // ← lifecycle runs here
    }
}

That said — overriding the constructor is rare. The conventional path is to declare properties directly on the class so PSR-4 autoloading composes cleanly with new MyModel().

Worked example

A multi-tenant Documents model that:

  • Uses a custom table name.
  • Has a non-id primary key.
  • Hydrates as a custom entity.
  • Auto-fills timestamps.
  • Supports soft delete.
  • Connects to a tenant-specific database (via $credentials that come from the parent class).
namespace App\Model;

use InitORM\ORM\Model;

abstract class TenantModel extends Model
{
    public function __construct(string $tenantHost)
    {
        $this->credentials = [
            'driver'   => 'mysql',
            'host'     => $tenantHost,
            'database' => 'tenant_app',
            'username' => 'app',
            'password' => '',
        ];
        parent::__construct();
    }
}

class Documents extends TenantModel
{
    protected string $schema    = 'documents';
    protected string $schemaId  = 'doc_id';
    protected string $entity    = \App\Entity\DocumentEntity::class;

    protected ?string $createdField = 'created_at';
    protected ?string $updatedField = 'updated_at';

    protected bool    $useSoftDeletes = true;
    protected ?string $deletedField   = 'deleted_at';
}

$docs = new Documents('tenant-a.db.internal');
$docs->create(['title' => 'Onboarding']);

Read also

Clone this wiki locally