Models
Models
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.
Static query entry points
Model::query(): ModelQuery
Get a fresh query builder bound to the model.
$users = User::query()->where('active', 1)->get();Model::with(array|string $relations): ModelQuery
Eager-load relations.
$posts = Post::with('author')->get();
$posts = Post::with(['author', 'comments'])->get();Model::withCount(array|string $relations): ModelQuery
Add a <relation>_count attribute.
$users = User::withCount('posts')->get();
$users[0]->posts_count; // intModel::all(): array
Fetch every row.
$users = User::all();Model::find(mixed $id): ?static
Find by primary key.
$user = User::find(42);Model::create(array $attributes): static
Insert a new row.
$user = User::create([
'name' => 'Tofik',
'email' => 'tofik@example.test',
]);Model::updateOrCreate(array $attributes, array $values = []): static
$user = User::updateOrCreate(
['email' => 'tofik@example.test'],
['name' => 'Tofik H.']
);Model::findOrCreate(array $attributes, array $values = []): static
Same as updateOrCreate but only insert when not found (no update on the existing row).
$user = User::findOrCreate(['email' => 'tofik@example.test']);Model::paginate(int $perPage = 15, int $page = 1): Paginator
$users = User::paginate(20, page: (int) request('page', 1));
foreach ($users as $user) { /* ... */ }
$users->total(); // total countModel::simplePaginate(int $perPage = 15, int $page = 1): Paginator
Cheaper than paginate() — no total count.
$users = User::simplePaginate(15);Instance methods
->save(): bool
Persist a new or modified instance.
$user = new User(['name' => 'Tofik']);
$user->email = 'tofik@example.test';
$user->save();->update(?array $attributes = null): bool
Mass-assign and save.
$user->update(['name' => 'New Name']);->delete(): bool
Soft delete when configured; hard delete otherwise.
$user->delete();->forceDelete(): bool
Hard delete even when soft deletes are enabled.
$user->forceDelete();->restore(): bool
Restore a soft-deleted record.
$user->restore();->trashed(): bool
if ($user->trashed()) { /* ... */ }->usesSoftDeletes(): bool
True when the model has a deleted_at column / config.
->getDeletedAtColumn(): string
Defaults to deleted_at.
->refresh(): static
Reload the row from the database.
$user->refresh();->exists(): bool
True after save() / find().
$user->exists(); // true->getKey(): mixed
The primary key value.
$user->getKey(); // 42->fill(array $attributes): static
Mass-assign respecting $fillable.
$user->fill(['name' => 'Tofik', 'email' => 'a@b'])->save();->forceFill(array $attributes): static
Bypass $fillable.
$user->forceFill(['admin' => true])->save();->getAttribute(string $key): mixed / ->hasAttribute(string $key): bool
$user->getAttribute('name');
$user->hasAttribute('email'); // true->getAttributes(): array / ->getOriginal(): array
$user->getAttributes(); // current
$user->getOriginal(); // attributes when loaded->isDirty(): bool
$user->name = 'X';
$user->isDirty(); // true->getPrimaryKey(): string
Default 'id'.
->toArray(): array / ->jsonSerialize(): array
Convert to array (respects $hidden).
$user->toArray();
json_encode($user); // jsonSerialize()Property access (magic)
__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 attributeRelationships
Define on the model. The framework infers foreign keys from snake-case class names; override when needed.
hasOne(string $related, ?string $foreignKey = null, ?string $localKey = null): HasOne
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}
$user->profile; // Profile|nullhasMany(string $related, ?string $foreignKey = null, ?string $localKey = null): HasMany
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
$user->posts; // array<Post>belongsTo(string $related, ?string $foreignKey = null, ?string $ownerKey = null): BelongsTo
class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class, 'author_id');
}
}
$post->author; // User|nullbelongsToMany(...)
Many-to-many through a pivot table.
class User extends Model
{
public function roles()
{
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
}
}->relationLoaded(string $key): bool
$user->relationLoaded('posts');->getRelation(string $key): mixed / ->setRelation($key, $value): static / ->getRelations(): array
Manually inspect or seed loaded relations.
$user->setRelation('posts', $cachedPosts);
$user->getRelation('posts');ModelQuery
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.
->with(array|string $relations): self
$posts = Post::query()->with(['author', 'comments'])->get();->withCount(array|string $relations): self
$users = User::query()->withCount('posts')->get();->whereHas(string $relation, ?Closure $callback = null): self
$activeAuthors = User::query()
->whereHas('posts', fn ($q) => $q->where('published', 1))
->get();->orWhereHas(string $relation, ?Closure $callback = null): self
$query->whereHas('posts')->orWhereHas('comments');->whereDoesntHave(string $relation, ?Closure $callback = null): self
$inactive = User::query()->whereDoesntHave('posts')->get();->orWhereDoesntHave(string $relation, ?Closure $callback = null): self
Soft-delete scopes
->withTrashed(): self
Include soft-deleted rows.
$all = User::query()->withTrashed()->get();->onlyTrashed(): self
$trash = User::query()->onlyTrashed()->get();->withoutTrashed(): self
The default; useful when overriding a previous scope.
$query->withTrashed()->where('active', 1)->withoutTrashed();Terminal methods
->get(array|string|DBMLExpression $columns = []): array
$users = User::query()->where('active', 1)->get(['id', 'email']);->first(array|string|DBMLExpression $columns = []): ?Model
$user = User::query()->where('email', $email)->first();->find(mixed $id, $columns = []): ?Model
$user = User::query()->find(42, ['id', 'email']);->updateOrCreate(array $attributes, array $values = []): Model
User::query()->updateOrCreate(
['email' => $email],
['name' => $name]
);->findOrCreate(array $attributes, array $values = []): Model
User::query()->findOrCreate(['email' => $email]);->count(string $column = '*'): int
$active = User::query()->where('active', 1)->count();->exists(): bool
if (User::query()->where('email', $email)->exists()) { /* ... */ }->pluck(string $column, ?string $key = null): array
$emails = User::query()->pluck('email'); // ['a@b', 'c@d']
$emails = User::query()->pluck('email', 'id'); // [1 => 'a@b', 2 => 'c@d']->value(string $column): mixed
$email = User::query()->where('id', 42)->value('email');->paginate(int $perPage = 15, int $page = 1): Paginator / ->simplePaginate(...)
$users = User::query()->where('active', 1)->paginate(20);->delete(): int / ->forceDelete(): int
User::query()->where('active', 0)->delete(); // soft if enabled
User::query()->where('active', 0)->forceDelete(); // always hardInspection
->toBase(): DBML
Drop down to the raw DBML builder.
$builder = User::query()->where('active', 1)->toBase();->toSql(): string / ->getBindings(): array
Useful for logging/debugging.
$sql = User::query()->where('active', 1)->toSql();
$bindings = User::query()->where('active', 1)->getBindings();Conventions
- 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.