Zero\Lib\Model is a lightweight active-record layer on top of DBML. Extend it under App\Models to map a table, hydrate records, and declare relationships.
namespace App\Models;
use Zero\Lib\Model;
class User extends Model
{
protected static string $table = 'users';
protected static array $fillable = ['name', 'email', 'password'];
protected static array $hidden = ['password'];
protected static bool $timestamps = true;
}Implementation: Model.php, ModelQuery.php, Concerns/InteractsWithRelations.php.
__call / __callStatic forward unknown methods to a fresh ModelQuery, so User::where(...), User::orderBy(...), etc., all work.
By default a model uses the application's default database connection. To pin a model to another connection defined in config/database.php, set the instance property $connection:
class Event extends Model
{
protected ?string $connection = 'analytics';
}All reads, writes, and relation queries for that model then run on the named connection. See Database Connections for the full multi-connection guide.
Get a fresh query builder bound to the model.
$users = User::query()->where('active', 1)->get();Eager-load relations.
$posts = Post::with('author')->get();
$posts = Post::with(['author', 'comments'])->get();Add a <relation>_count attribute.
$users = User::withCount('posts')->get();
$users[0]->posts_count; // intFetch every row.
$users = User::all();Find by primary key.
$user = User::find(42);Insert a new row.
$user = User::create([
'name' => 'Tofik',
'email' => 'tofik@example.test',
]);$user = User::updateOrCreate(
['email' => 'tofik@example.test'],
['name' => 'Tofik H.']
);Same as updateOrCreate but only insert when not found (no update on the existing row).
$user = User::findOrCreate(['email' => 'tofik@example.test']);$users = User::paginate(20, page: (int) request('page', 1));
foreach ($users as $user) { /* ... */ }
$users->total(); // total countCheaper than paginate() — no total count.
$users = User::simplePaginate(15);Persist a new or modified instance.
$user = new User(['name' => 'Tofik']);
$user->email = 'tofik@example.test';
$user->save();Mass-assign and save.
$user->update(['name' => 'New Name']);Soft delete when configured; hard delete otherwise.
$user->delete();Hard delete even when soft deletes are enabled.
$user->forceDelete();Restore a soft-deleted record.
$user->restore();if ($user->trashed()) { /* ... */ }True when the model has a deleted_at column / config.
Defaults to deleted_at.
Reload the row from the database.
$user->refresh();True after save() / find().
$user->exists(); // trueThe primary key value.
$user->getKey(); // 42Mass-assign respecting $fillable.
$user->fill(['name' => 'Tofik', 'email' => 'a@b'])->save();Bypass $fillable.
$user->forceFill(['admin' => true])->save();$user->getAttribute('name');
$user->hasAttribute('email'); // true$user->getAttributes(); // current
$user->getOriginal(); // attributes when loaded$user->name = 'X';
$user->isDirty(); // trueDefault 'id'.
Convert to array (respects $hidden).
$user->toArray();
json_encode($user); // jsonSerialize()__get, __set, __isset, __unset proxy to attributes/relations.
$user->name; // attribute
$user->posts; // loaded relation (HasMany → array)
isset($user->email); // bool
unset($user->cached); // remove an attributeDefine on the model. The framework infers foreign keys from snake-case class names; override when needed.
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}
$user->profile; // Profile|nullclass User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
$user->posts; // array<Post>class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class, 'author_id');
}
}
$post->author; // User|nullMany-to-many through a pivot table.
class User extends Model
{
public function roles()
{
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
}
}$user->relationLoaded('posts');Manually inspect or seed loaded relations.
$user->setRelation('posts', $cachedPosts);
$user->getRelation('posts');ModelQuery is the chainable builder. __call forwards any DBML method (where, whereIn, orderBy, select, join, limit, …) to the underlying DBML query — see that doc for the full set.
$posts = Post::query()->with(['author', 'comments'])->get();$users = User::query()->withCount('posts')->get();$activeAuthors = User::query()
->whereHas('posts', fn ($q) => $q->where('published', 1))
->get();$query->whereHas('posts')->orWhereHas('comments');$inactive = User::query()->whereDoesntHave('posts')->get();Include soft-deleted rows.
$all = User::query()->withTrashed()->get();$trash = User::query()->onlyTrashed()->get();The default; useful when overriding a previous scope.
$query->withTrashed()->where('active', 1)->withoutTrashed();$users = User::query()->where('active', 1)->get(['id', 'email']);$user = User::query()->where('email', $email)->first();$user = User::query()->find(42, ['id', 'email']);User::query()->updateOrCreate(
['email' => $email],
['name' => $name]
);User::query()->findOrCreate(['email' => $email]);$active = User::query()->where('active', 1)->count();if (User::query()->where('email', $email)->exists()) { /* ... */ }$emails = User::query()->pluck('email'); // ['a@b', 'c@d']
$emails = User::query()->pluck('email', 'id'); // [1 => 'a@b', 2 => 'c@d']$email = User::query()->where('id', 42)->value('email');$users = User::query()->where('active', 1)->paginate(20);User::query()->where('active', 0)->delete(); // soft if enabled
User::query()->where('active', 0)->forceDelete(); // always hardDrop down to the raw DBML builder.
$builder = User::query()->where('active', 1)->toBase();Useful for logging/debugging.
$sql = User::query()->where('active', 1)->toSql();
$bindings = User::query()->where('active', 1)->getBindings();- Table name:
static $table(defaults to plural snake-case of the class). - Primary key:
static $primaryKey = 'id'. - Mass assignment:
static $fillable = [...]. - Hidden attributes:
static $hidden = [...](omitted fromtoArray/JSON). - Casts:
static $casts = ['payload' => 'array']. - Timestamps:
static $timestamps = trueenablescreated_at/updated_atauto-fill. - Soft deletes: add a
deleted_atcolumn and setstatic $softDeletes = true.
See dbml.md for the full query builder surface that ModelQuery delegates to.