Entities
class Crustum\JsonField\Model\Entity.JsonEntity
While Table Objects represent and provide access to a set of rows, entities represent individual records. In this plugin an entity is a CakePHP Entity: a root entity owns one or more JSON storage columns declared with #[JsonColumn], and the nested sub-entities inside those columns are embedded entities that extend JsonEntity. Fields — including values at arbitrary JSON paths — are accessed as properties or through get/set, exactly as in core CakePHP.
The standard entity topics are documented in the CakePHP Entities cookbook and behave identically here: creating entities, get/set/has/hasValue/patch, accessors & mutators, virtual fields, isDirty/getOriginal/clean, validation errors, mass assignment, isNew, traits, and toArray/json_encode. This page covers only the differences the plugin introduces on top of that baseline.
Creating Entity Classes
By convention entity classes live in src/Model/Entity/. A root entity applies the #[JsonColumn] attribute and uses JsonFieldTrait for path-string access:
// src/Model/Entity/Article.php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Crustum\JsonField\Database\Schema\Attribute\JsonColumn;
use Crustum\JsonField\Model\Entity\JsonFieldTrait;
#[JsonColumn('data')]
class Article extends Entity
{
use JsonFieldTrait;
}NOTE
If you don't define an entity class CakePHP uses the generic Cake\ORM\Entity. The class is derived from the table alias (App\Model\Entity\<Alias>). Embedded sub-entities produced by hydration default to Crustum\JsonField\Model\Entity\JsonEntity unless an embed entity class is declared on the #[JsonEmbed].
Embedded Entity Classes
A nested entity stored inside a JSON column is a JsonEntity subclass. It carries no schema of its own and holds an embeddedParent back-pointer (JsonFieldTrait::getEmbeddedParent(), ::isEmbedded()), so save and delete calls route down to the owning JSON column:
// src/Model/Entity/ArticleProfile.php
namespace App\Model\Entity;
use Crustum\JsonField\Database\Schema\Attribute\JsonPathType;
use Crustum\JsonField\Model\Entity\JsonEntity;
#[JsonPathType('created', type: 'datetime')]
#[JsonPathType('price', type: 'decimal')]
class ArticleProfile extends JsonEntity
{
}Link the embed to the root entity with #[JsonEmbed]:
#[JsonColumn('data')]
#[JsonEmbed(property: 'profile', documentClass: ArticleProfile::class, path: 'data->profile')]
class Article extends Entity
{
use JsonFieldTrait;
}Creating Entities
Use newEntity() / newEmptyEntity() as usual (see the cookbook). The JSON storage column is just another field, passed as a plain array; nested arrays under a #[JsonColumn] are hydrated into JsonEntity subtrees when read back, and you can also attach JsonEntity instances directly in memory:
$article = $articles->newEntity([
'title' => 'New Article',
'data' => ['profile' => ['name' => 'Jane']],
]);Accessing Entity Data
Object notation, get()/set()/has()/hasValue()/patch() all work as documented in the CakePHP cookbook. The plugin adds one extension — path-string access — via JsonFieldTrait:
Path-String Access
Add JsonFieldTrait to a root entity for column->node (or node@column) sugar over the standard Entity API. Non-path calls pass through unchanged, so ordinary CakePHP usage is untouched:
$article->get('data->profile->name');
$article->set('data->profile->name', 'Patrick');
$article->isDirty('data->profile->name');Path access resolves in pure PHP against the already-fetched JSON tree — no SQL is issued. Writes initialize missing columns and build nested arrays as needed, even on a fresh entity. has() and hasValue() accept a path string too:
$article->has('data->profile->name'); // true when the path exists
$article->hasValue('data->profile->name'); // true when the nested value is non-emptyTo remove a node, unset() accepts a path as well, marking the owning embed dirty so the change routes to an atomic JSON-column write:
$article->unset('data->profile->name');Accessors & Mutators
Accessors (_get*), mutators (_set*), and virtual fields follow the standard CakePHP convention and are documented in the cookbook. The only difference: they apply at every nested layer. An accessor declared on an embedded entity (_getName() on ArticleProfile) intercepts $article->profile->name the same way one on the root entity intercepts $article->title — accessors, mutators, and virtual fields all work per embed.
WARNING
Accessors run when entities are persisted, so a mutator that formats data will persist the formatted value. Use virtual fields for derived values you do not want stored — this applies to embedded entities exactly as to root entities.
Checking if an Entity Has Been Modified
isDirty(), getOriginal(), clean(), setDirty(), getDirty(), and the markClean instantiation option behave as in core CakePHP (see the cookbook). Two plugin extensions:
- Path-string dirty check —
isDirty('data->profile->name')walks down to the owning nested entity and reports whether the leaf is dirty. - Embedded dirty propagation — because embedded entities carry an
embeddedParentback-pointer, a change deep inside a nested entity marks the root entity's embed property dirty (e.g.profile, never the whole column). That makesTable::save()dispatchModel.beforeSave, where the change is routed down to an atomic JSON-column write:
// Add a chapter and mark the embed property as changed.
$article->chapters[] = $newChapter;
$article->setDirty('chapters', true);Validation Errors
After you save an entity, validation errors are stored on the entity and read with getErrors() / getError() / hasErrors() / setErrors() (see the cookbook). The plugin-specific part: an embedded validator configured on a JsonFieldEmbed association runs during save and throws on invalid nested data before it is written (see Associations).
Mass Assignment
_accessible, setAccess(), the guard option on set(), and isNew() / setNew() follow the core rules (see the cookbook). The plugin-specific note:
NOTE
The data column itself is mass-assignable as a unit; if you need finer control over individual JSON paths, marshal them explicitly rather than trusting the request array. The standard _accessible map still governs the top-level keys.
Lazy Loading References
Embedded associations (JsonFieldHasOne / JsonFieldHasMany) are hydrated automatically on every find — the data lives inside the parent row's JSON column, so there is nothing to lazy load. Reference associations (JsonFieldBelongsTo / JsonFieldBelongsToMany), by contrast, only store a foreign key, so you resolve them on demand with loadJsonReferences() on the table:
$article = $articles->get(1);
$articles->loadJsonReferences([$article]); // author and tags are resolved
echo $article->author->username;You can also eager-load references during hydration with eagerLoadJsonReferences($query), or add a real SQL join with joinJsonReference($query, $name) (see Associations). Unlike embedded data, a reference is not present until you resolve it.
Creating Re-usable Code with Traits
PHP traits work as in core CakePHP (see the cookbook). The plugin ships two: JsonFieldTrait (path-string access for root entities) and JsonEntity (the embedded base class, which already uses JsonFieldTrait). Application traits belong in src/Model/Entity and are conventionally suffixed with Trait.
Converting to Arrays/JSON
toArray() and json_encode() follow the core rules for virtual/hidden fields (see the cookbook). The plugin-specific behavior:
- Embedded sub-entities are serialized recursively — a hydrated
profileorchaptersembed appears in the output as nested arrays. - References are included only if you resolved them first with
loadJsonReferences()/eagerLoadJsonReferences(); otherwiseauthor/tagsare absent from the export.
Storing Complex Types
Accessor & mutator methods are not intended to contain the logic for serializing and unserializing complex data. Per-path type casting is handled by the JsonTypeMap, built from #[JsonPathType] attributes on your embedded entities (or registered programmatically). Declaring a type restores the correct PHP representation when the value is read and casts it back on write:
#[JsonPathType('created', type: 'datetime')]
#[JsonPathType('price', type: 'decimal')]
class ArticleProfile extends JsonEntity
{
}Values at typed paths become real Cake\I18n\DateTime / decimal strings on read and are cast back through toDatabase on save; core JsonType still handles the whole-column encode/decode. Nested sub-entities themselves are the "complex types" — each #[JsonEmbed] declaration hydrates its slice of the JSON column into a dedicated JsonEntity subclass, so an entity can nest arbitrarily deep while keeping per-layer accessors, virtual fields, and dirty tracking intact.