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

Models

A Model binds one database table and exposes the same CRUD surface as DB::, with a few model-only conveniences layered on top:

  • Configurable primary key column.
  • Auto-derived schema name (PostCommentpost_comment) when you don't set one.
  • Soft deletes (deleted_at column).
  • Timestamps — auto-managed created_at / updated_at.
  • Per-operation access gates ($readable, $writable, $updatable, $deletable).
  • An Entity class used to hydrate read() results.

A minimal model

namespaceApp\Model;
useInitPHP\Database\Model;
finalclass Posts extends Model
{
protectedstring$schema = 'posts';
protectedstring$schemaId = 'id';
}

The base constructor binds the model to the shared DB::getDatabase() instance — so as long as DB::createImmutable([...]) ran during bootstrap, no extra wiring is needed:

$posts = newApp\Model\Posts();
$posts->create(['title' => 'Hello', 'content' => 'World']);

Auto-derived schema name

If you omit $schema, the constructor derives it from the class' short name using Helper::camelCaseToSnakeCase():

ClassDerived schema
Postsposts
BlogPostblog_post
UserProfileuser_profile
APITokenapi_token
finalclass BlogPost extends \InitPHP\Database\Model {}
(newBlogPost())->getSchema(); // 'blog_post'

Full configuration surface

namespaceApp\Model;
useApp\Entity\PostEntity;
useInitPHP\Database\Model;
finalclass Posts extends Model
{
// Optional. Defaults to the snake_case form of the class' short name.protectedstring$schema = 'posts';
// Primary key column. Set to '' to disable the PK lift-out in update().protectedstring$schemaId = 'id';
// Entity class used by read() to hydrate rows.// Defaults to InitPHP\Database\Entity.protectedstring$entity = PostEntity::class;
// Soft deletes — see Soft-Deletes page.protectedbool$useSoftDeletes = true;
protected ?string$deletedField = 'deleted_at';
// Timestamp columns — see Timestamps page.protected ?string$createdField = 'created_at';
protected ?string$updatedField = 'updated_at';
protectedstring$timestampFormat = 'Y-m-d H:i:s';
// Access gates. Setting any of these to false throws on the matching call.protectedbool$readable = true;
protectedbool$writable = true;
protectedbool$updatable = true;
protectedbool$deletable = true;
// Use a non-default connection — see Multiple-Connections.protected ?array$credentials = null;
}

CRUD on a model

$posts = newApp\Model\Posts();
// Insert; $createdField is filled in automatically when set.$posts->create(['title' => 'Hello', 'content' => 'World']);
// Select; soft-deleted rows are excluded automatically.$rows = $posts->read(['id', 'title'], ['status' => 1])
->asAssoc()
->rows();
// Update; if the primary key sits in $set, it is lifted into a WHERE// clause and removed from the SET map.$posts->update(['id' => 13, 'title' => 'New title']);
// Or pass conditions explicitly:$posts->update(['title' => 'New title'], ['id' => 13]);
// Delete; soft-deletes when $useSoftDeletes = true, hard-deletes otherwise.$posts->delete(['id' => 13]);
$posts->delete(['id' => 13], purge: true); // force a real DELETE

Fluent chains work too

Every method that does not live on Model itself falls through to the underlying Database, and chainable builder calls re-wrap to return the Model so the chain stays type-consistent:

$rows = $posts
->select('id', 'title', 'created_at')
->where('status', 1)
->orderBy('created_at', 'DESC')
->limit(20)
->read()
->asAssoc()
->rows();

Read with an entity class

When $entity points at a subclass of InitPHP\Database\Entity, read() hydrates rows into instances of that class:

foreach ($posts->read() as$post) {
echo$post->title, PHP_EOL;
}

->read() returns a DataMapperInterface, which is iterable — each iteration gives back one entity. For the full attribute / accessor / mutator story, see Entities.

save() — insert-or-update via an entity

$entity = newPostEntity(['title' => 'New post']);
$posts->save($entity); // no id ⇒ create()$entity = newPostEntity(['id' => 13, 'title' => 'Edited']);
$posts->save($entity); // id present ⇒ update()

save() checks the entity for a value at $schemaId: present (non-null, non-empty-string) ⇒ update; absent ⇒ create. Useful in a controller that handles both flows with the same method body.

Access gates

Each gate is a boolean on the subclass; flipping any to false makes the matching call throw:

protected bool $readable = true;
protected bool $writable = true;
protected bool $updatable = true;
protected bool $deletable = true;
GateDisabled call throws
$readable = falseread()InitORM\ORM\Exceptions\ReadableException
$writable = falsecreate() / createBatch()WritableException
$updatable = falseupdate() / updateBatch()UpdatableException
$deletable = falsedelete()DeletableException

Handy for read-only views ($writable = $updatable = $deletable = false) or audit-log tables that must never be updated after insert ($updatable = $deletable = false).

Accessing the underlying Database

$db = $posts->getDatabase(); // InitORM\Database\Interfaces\DatabaseInterface$db->transaction(function () use ($posts) {
$posts->create([...]);
});

Useful for transaction boundaries and for sharing the same connection across two models.

What model state survives between calls

Each Model instance is bound to one Database. Builder state on that Database resets after every read() / create() / update() / delete() — so two model instances sharing the same underlying Database will not bleed into each other's queries, as long as one method runs at a time.

If you really need parallel chains, request a fresh builder:

$model->getDatabase()->withFreshBuilder();

Where to go next

Clone this wiki locally