-
Notifications
You must be signed in to change notification settings - Fork 0
Entities
An entity is a typed-ish row container. Each instance carries:
- An attribute bag (
protected array $attributes) — the actual column → value map. - A snapshot (
protected array $attributesOriginal) taken at construction time, useful for dirty tracking.
Reading or writing a column through $entity->column dispatches through __get / __set, which gives subclasses a clean place to hang accessor / mutator hooks.
namespace App\Entity;
use InitPHP\Database\Entity;
final class PostEntity extends Entity
{
}Wire it into a model:
final class Posts extends \InitPHP\Database\Model
{
protected string $entity = \App\Entity\PostEntity::class;
}Now $posts->read() hydrates rows into instances of your subclass.
$post = new PostEntity(['id' => 1, 'title' => 'Hello']);
echo $post->title; // 'Hello' — routed through __get
$post->title = 'Edited'; // routed through __set
$post->toArray(); // ['id' => 1, 'title' => 'Edited']
$post->getAttributes(); // same as toArray()
$post->getOriginal(); // ['id' => 1, 'title' => 'Hello'] — snapshot at constructisset($post->title) and unset($post->title) work too (via __isset / __unset).
$post = new PostEntity(['title' => 'First']);
$post->title = 'Second';
$post->getOriginal()['title']; // 'First' — still the construct-time value
$post->syncOriginal();
$post->getOriginal()['title']; // 'Second' — fresh snapshotCall this after a successful save() when you want subsequent dirty-tracking to be relative to the just-persisted state.
Define get{Column}Attribute($value) and the entity routes property reads through it:
final class PostEntity extends Entity
{
public function getTitleAttribute(?string $value): string
{
return (string) ($value ?? '(untitled)');
}
}
$post = new PostEntity(['title' => null]);
echo $post->title; // '(untitled)'The method receives whatever sits in the attribute bag (or null when nothing has been stored) and may return anything.
A common pattern — derived columns:
public function getFullNameAttribute(): string
{
return trim(($this->attributes['first_name'] ?? '') . ' ' . ($this->attributes['last_name'] ?? ''));
}$post->full_name then "just works" even though there is no full_name column.
Define set{Column}Attribute($value) and the entity routes property writes through it. This is where almost every entity bug in the wild comes from, so read this section carefully.
final class PostEntity extends Entity
{
public function setTitleAttribute(string $value): void
{
$this->setAttribute('title', mb_strtolower($value));
}
}
$post = new PostEntity();
$post->title = 'Hello';
echo $post->title; // 'hello'final class PostEntity extends Entity
{
public function setTitleAttribute(string $value): void
{
$this->title = mb_strtolower($value); // ⚠️ does NOT go through __set
}
}Inside a class method, PHP resolves $this->column = $value directly on the object, bypassing __set. That means:
- The transformed value never reaches
$attributes. - PHP creates a dynamic property instead — deprecated since 8.2, fatal in a future PHP release.
Always use $this->setAttribute($name, $value) inside a mutator.
The translation is snake_case → PascalCase:
| Column | Accessor | Mutator |
|---|---|---|
title |
getTitleAttribute |
setTitleAttribute |
author_id |
getAuthorIdAttribute |
setAuthorIdAttribute |
post_meta_data |
getPostMetaDataAttribute |
setPostMetaDataAttribute |
Entity::__call() also handles the *Attribute family when you don't define a real method — the fallback simply forwards to the attribute bag. So calls like $entity->getTitleAttribute() always work, defined or not.
There are two ways an entity gets populated:
| Path | Mutators run? |
|---|---|
new PostEntity(['title' => 'X']) — direct construction |
✅ yes — the constructor dispatches every key through __set. |
$model->read()->asClass(PostEntity::class) — PDO hydration |
❌ no — PDO writes properties directly on the object via FETCH_CLASS. |
If you need transformation on read, do it via a get{Column}Attribute() accessor — accessors always run, even after PDO hydration.
var_dump($post);__debugInfo() returns the attribute bag, so var_dump shows the row data rather than the internal $attributes / $attributesOriginal fields. Useful in test output.
namespace App\Entity;
use DateTimeImmutable;
use InitPHP\Database\Entity;
final class UserEntity extends Entity
{
/** Lower-case + trimmed email on write. */
public function setEmailAttribute(string $value): void
{
$this->setAttribute('email', mb_strtolower(trim($value)));
}
/** Always return a typed object on read. */
public function getCreatedAtAttribute(?string $value): ?DateTimeImmutable
{
return $value === null ? null : new DateTimeImmutable($value);
}
/** Derived column — never persisted. */
public function getDisplayNameAttribute(): string
{
$name = $this->attributes['name'] ?? '';
return $name !== '' ? $name : ($this->attributes['email'] ?? '(no name)');
}
}$user = new UserEntity(['email' => 'ADA@example.com', 'created_at' => '2026-05-25 10:00:00']);
$user->email; // 'ada@example.com' — mutator on construct
$user->created_at; // DateTimeImmutable — accessor on read
$user->display_name; // 'ada@example.com' — derived from the email- Models — the table side of the pair.
-
CRUD Operations —
save()(insert-or-update) semantics.
initphp/database · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Core API
ORM
Advanced
DataTables Helper
Recipes
- Index
- — Pagination
- — Search & Filters
- — Upsert / REPLACE INTO
- — Audit Log
- — DataTables Bootstrap
- — Repository Pattern
Reference
Migration & Help