-
-
Notifications
You must be signed in to change notification settings - Fork 0
Entities
An entity is a typed row container. The reference implementation is InitORM\ORM\Entity — values live in an internal $attributes array, with optional per-column accessor and mutator hooks.
Every column lives in $attributes, an associative array column → value. The magic accessors expose it through property syntax:
$entity = new \InitORM\ORM\Entity(['title' => 'Hello', 'body' => 'World']);
$entity->toArray(); // ['title' => 'Hello', 'body' => 'World']
$entity->getAttributes(); // same
$entity->title; // 'Hello' — via __get
$entity->title = 'New'; // sets via __set
$entity->title; // 'New'
isset($entity->title); // true — via __isset
unset($entity->title); // via __unset
isset($entity->title); // falseThree of the four magic methods route through the attribute bag:
-
__get($name)checks for an accessor method, otherwise returns$attributes[$name] ?? null. -
__set($name, $value)checks for a mutator method, otherwise sets$attributes[$name] = $value. -
__isset($name)returnsisset($attributes[$name]). -
__unset($name)removes the entry from$attributes.
An accessor is a method named get{Column}Attribute(mixed $value). The column name is the PascalCase form of the snake_case column. $value is the current stored value (or null if the attribute is absent).
class PostEntity extends \InitORM\ORM\Entity
{
public function getTitleAttribute(mixed $value): mixed
{
return is_string($value) ? ucwords($value) : $value;
}
}
$entity = new PostEntity(['title' => 'hello world']);
$entity->title; // 'Hello World' — transformed by the accessor
$entity->getAttribute('title'); // 'hello world' — bypasses the accessorUse accessors for presentation transformations:
- Capitalising names.
- Formatting dates / numbers.
- Deserialising JSON / CSV columns.
- Computing derived properties from siblings (use
$this->getAttribute('other_col')inside the accessor — never$this->other_col, which would invoke that column's accessor recursively if defined).
A mutator is a method named set{Column}Attribute(mixed $value). It runs on every property write — including assignments made by PDO during hydration.
class PostEntity extends \InitORM\ORM\Entity
{
public function setTitleAttribute(mixed $value): void
{
$this->setAttribute('title', is_string($value) ? trim($value) : $value);
}
}
$entity = new PostEntity();
$entity->title = ' hello ';
$entity->getAttribute('title'); // 'hello' — mutator stripped whitespaceA mutator body MUST write back via $this->setAttribute('col', $value). Plain $this->title = $value from inside a class method bypasses __set and creates a dynamic property on the object — the value never reaches $attributes, and:
- PHP 8.2+ raises a
"Creation of dynamic property"deprecation. - A future PHP version (likely 9.0) will make it fatal.
- Subsequent reads of
$entity->titlefind the dynamic property and return it directly, skipping__getand therefore the accessor.
// ❌ WRONG — creates a dynamic property, never reaches $attributes
public function setTitleAttribute(mixed $value): void
{
$this->title = trim($value);
}
// ✅ RIGHT — explicit, future-proof, no surprises
public function setTitleAttribute(mixed $value): void
{
$this->setAttribute('title', trim($value));
}This is the single biggest pitfall in the entity API. The exception class hierarchy will not save you here — PHP just silently puts the value in the wrong place.
The conventional pattern: mutator normalises on write, accessor presents on read.
class UserEntity extends \InitORM\ORM\Entity
{
public function setEmailAttribute(mixed $value): void
{
$this->setAttribute(
'email',
is_string($value) ? strtolower(trim($value)) : $value,
);
}
public function getEmailAttribute(mixed $value): mixed
{
// Stored lower-cased; presented as-is.
return $value;
}
}
$user = new UserEntity();
$user->email = ' Foo@Example.COM ';
$user->email; // 'foo@example.com'
$user->getAttribute('email'); // 'foo@example.com'The helper methods read and write $attributes directly, skipping the magic accessors entirely. Use them:
- Inside a mutator body, to write back without re-entering the mutator.
- Inside an accessor body, to read a sibling column without re-entering that sibling's accessor.
- In tests, to assert what was actually stored vs. what the accessor presents.
$entity->setAttribute('email', 'me@example.com');
$entity->getAttribute('email'); // 'me@example.com' — no accessor involvedEach entity captures the construction-time attribute bag as the "original" snapshot. Mutations after construction do not change it.
$entity = new \InitORM\ORM\Entity(['title' => 'Hello']);
$entity->title = 'Edited';
$entity->getOriginal(); // ['title' => 'Hello']
$entity->getAttributes(); // ['title' => 'Edited']Call syncOriginal() to overwrite the snapshot with the current values — for example, after persisting via Model::save():
$model = new \App\Model\Posts();
$model->save($entity);
$entity->syncOriginal(); // entity is "clean" againThe package intentionally does not ship an isDirty() / getChanges() helper — diffing the two arrays in user code lets each project pick the semantics it needs:
function isDirty(\InitORM\ORM\Entity $e): bool
{
return $e->getAttributes() !== $e->getOriginal();
}
function getDirtyColumns(\InitORM\ORM\Entity $e): array
{
return array_diff_assoc($e->getAttributes(), $e->getOriginal());
}If a subclass does not declare a get{Column}Attribute or set{Column}Attribute method, calling it on the entity still works — Entity::__call provides a default implementation that routes directly to the attribute bag:
$entity = new \InitORM\ORM\Entity();
$entity->setPostTitleAttribute('Hello'); // sets $attributes['post_title']
$entity->getPostTitleAttribute(); // returns 'Hello'This makes $entity->post_title and $entity->setPostTitleAttribute('Hello') round-trip identically when no custom transform is needed.
__call raises EntityException if the method name does not match the get{*}Attribute / set{*}Attribute pattern:
try {
$entity->doSomething();
} catch (\InitORM\ORM\Exceptions\EntityException $e) {
// Unknown entity method "doSomething".
}When a model read()s, the underlying DataMapper uses PDO::FETCH_CLASS to instantiate the entity. PDO:
- Creates the entity object (without calling
__constructyet). - Sets each column as a property. The properties are not declared, and
__setis defined, so__setfires for every column — running mutators if any. - Calls
__construct()with no arguments. The default?array $data = []parameter is[], sofill([])is a no-op;syncOriginal()then captures the PDO-set values as the original snapshot.
This order matters: setting properties before the constructor means that mutators run on raw fetched values, and syncOriginal() captures the post-mutator state.
class SettingEntity extends \InitORM\ORM\Entity
{
public function getValueAttribute(mixed $value): mixed
{
return is_string($value) ? json_decode($value, true) : $value;
}
public function setValueAttribute(mixed $value): void
{
$this->setAttribute('value', is_string($value) ? $value : json_encode($value));
}
}
$setting = new SettingEntity();
$setting->value = ['theme' => 'dark', 'lang' => 'en'];
$setting->getAttribute('value'); // '{"theme":"dark","lang":"en"}' — stored as string
$setting->value; // ['theme' => 'dark', 'lang' => 'en'] — accessor decodesclass UserEntity extends \InitORM\ORM\Entity
{
public function getFullNameAttribute(mixed $value): mixed
{
// $value is null — full_name is not actually stored
return trim(($this->getAttribute('first_name') ?? '') . ' ' . ($this->getAttribute('last_name') ?? ''));
}
}
$user = new UserEntity(['first_name' => 'Ada', 'last_name' => 'Lovelace']);
$user->full_name; // 'Ada Lovelace'(Note: full_name is not in $attributes, so toArray() won't include it. If you want the computed value to round-trip through toArray(), copy it into the bag via setAttribute in the constructor, or override toArray() to merge it in.)
class PostEntity extends \InitORM\ORM\Entity
{
public function getIsPublishedAttribute(mixed $value): bool
{
return (bool) $value;
}
public function setIsPublishedAttribute(mixed $value): void
{
$this->setAttribute('is_published', $value ? 1 : 0);
}
}-
Defining Models — wiring an entity class via
$entity. -
CRUD Operations — how
read()hydrates entities andsave()persists them. - Architecture — the hydration order and the dynamic-property gotcha.
InitORM ORM · MIT · maintained by Muhammet ŞAFAK · part of the InitORM stack
Getting Started
Models
Entities
Cross-Cutting
Reference
Upgrading
Project