Part of Jardis — the Domain-Driven Design platform for PHP. You model your domain; Jardis generates the production-ready hexagonal code (DTOs, Command/Query handlers, repositories, persistence). This package is part of the open-source foundation that generated code runs on.
Three focused services for entity hydration and DTO mapping in PHP: Hydration transforms raw database rows into typed PHP objects — filling the entities and DTOs Jardis generates — with automatic change tracking. FieldMapper provides bidirectional array-key mapping between domain and database names. Identity generates unique identifiers (UUID v4/v5/v7, NanoID).
- Two-Stage Hydration —
hydrate()for flat entities,hydrateAggregate()for nested aggregates with recursive object graphs - Programmatic Apply —
apply()sets properties likehydrate()but preserves the snapshot — changes are detected bygetChanges() - Automatic Snapshots — a snapshot is taken on every hydration call to establish the baseline for change detection. Snapshot keys are the original DB column names — used as source of truth for
toArray(),aggregateToArray(), anddiff() - Value-Based Detection —
toArray()anddiff()automatically distinguish DB columns from relations by value type — no attributes required - Change Tracking — field-level diff between current property values and the snapshot via
getChanges() - Entity Cloning —
clone()for flat entity clone (DB columns),cloneAggregate()for deep clone including nested objects - Batch Loading —
loadMultiple()hydrates an array of database rows into an array of entities efficiently - Field Mapping —
toColumns()andfromColumns()rename array keys between domain and database names using an explicit map;fromAggregate()maps hierarchical arrays with per-entity mappings and filters unmapped keys - Identity Generation — UUID v4 (random), v5 (deterministic), v7 (time-ordered with monotonic counter), and NanoID (compact, URL-safe)
- PHP Attributes —
#[Table],#[Column],#[PrimaryKey],#[Aggregate]for entity metadata - TypeCaster — automatic DB-to-PHP type conversion (int, bool, float, DateTime, DateTimeImmutable, BackedEnum); DateTime/DateTimeImmutable fall back to PHP's general date parser for anything beyond the three fixed formats, so TIMESTAMPTZ/ISO-8601 strings with offset and/or microseconds (e.g.
2026-08-28 12:34:56.123456+00,2026-08-28T12:34:56Z) parse correctly instead of silently returningnull - ProjectWire — stateless handler for the opposite direction (PHP → JSON wire):
(new ProjectWire())($payload, $typeMap)projects values per a type map ('date','date-time','time','boolean', or a nested map for child arrays/lists).'boolean'turns the flat-row form of a boolean column (1/0,'1'/'0',true/false) into a JSON boolean;nullstaysnull, any other value becomesnull(never silentlyfalse). Wire values are UTC: adate-timeDateTimeInterfaceis converted to UTC before formatting (the caller's object is not modified;date/timeobjects keep their own wall clock), strings without offset are read as UTC, strings with offset are converted to UTC;date-timeyieldsY-m-d\TH:i:s+00:00(no sub-seconds). Zero-dates, empty and unparsable strings becomenull; unmapped keys pass through unchanged
$wire = (new ProjectWire())(
['name' => 'Jane', 'active' => 1, 'created_at' => '2024-01-15 14:30:45', 'items' => [['due' => '2024-02-01 00:00:00']]],
['active' => 'boolean', 'created_at' => 'date-time', 'items' => ['due' => 'date']]
);
// ['name' => 'Jane', 'active' => true, 'created_at' => '2024-01-15T14:30:45+00:00', 'items' => [['due' => '2024-02-01']]]composer require jardissupport/datause JardisSupport\Data\Hydration;
use JardisSupport\Data\Identity;
use JardisSupport\Data\FieldMapper;
// --- Hydration ---
$hydration = new Hydration();
// Hydrate an entity from a database row
$user = new User();
$hydration->hydrate($user, [
'id' => 42,
'first_name' => 'Jane',
'last_name' => 'Doe',
'created_at' => '2024-06-01 10:00:00',
]);
// Apply programmatic changes (snapshot stays untouched)
$hydration->apply($user, ['first_name' => 'John']);
// Detect exactly what changed since hydration
$changes = $hydration->getChanges($user);
// ['first_name' => 'John']
// --- Identity ---
$identity = new Identity();
$id = $identity->generateUuid7(); // time-ordered, sortable
$id = $identity->generateUuid4(); // random
$id = $identity->generateUuid5($namespace, 'customer:12345'); // deterministic
$id = $identity->generateNanoId(); // compact, URL-safe (21 chars)
// --- FieldMapper ---
$mapper = new FieldMapper();
$map = ['customerName' => 'name', 'orderNumber' => 'order_number'];
$columns = $mapper->toColumns(['customerName' => 'Müller'], $map);
// ['name' => 'Müller']
$fields = $mapper->fromColumns(['name' => 'Müller'], $map);
// ['customerName' => 'Müller']A column that must never be overwritten with null — typically a password hash on update, where the
caller leaves the field empty to keep the stored value — is declared by the caller as a skip list.
The package does not know where the list comes from; it only skips null for the named entries.
// Column names are matched after mapping; other keys (and other nulls) pass through unchanged.
$row = $mapper->toColumns(
['name' => 'Jane', 'passwordHash' => null],
['passwordHash' => 'password_hash'],
['password_hash'],
);
// ['name' => 'Jane'] — password_hash is not written
// Same idea on the entity side: keys of $data, `null` is not written, the field stays untouched.
$hydration->apply($user, ['first_name' => 'John', 'password_hash' => null], ['password_hash']);An empty list (the default) keeps the previous behaviour byte-for-byte.
A field the caller did not deliver is JardisSupport\Contract\Data\Absent::Value, distinct from a delivered null.
toColumns() omits every key with that value — with a map, with an empty map, and with a skip list.
null stays in the result (it clears a nullable column) unless its column is in the skip list.
The check covers the top level only; Absent inside a nested array is not looked at.
use JardisSupport\Contract\Data\Absent;
$row = $mapper->toColumns(
['name' => 'Jane', 'phone' => null, 'nickname' => Absent::Value],
[],
);
// ['name' => 'Jane', 'phone' => null] — nickname is not written, phone is set to NULLHydration::apply() never writes Absent either, so the output of toColumns() goes straight into it:
an absent key leaves the property untouched.
All three services implement port interfaces from jardissupport/contracts:
| Service | Interface | Purpose |
|---|---|---|
Hydration |
JardisSupport\Contract\Data\HydrationInterface |
Entity hydration, snapshots, change tracking |
Identity |
JardisSupport\Contract\Data\IdentityInterface |
UUID v4/v5/v7, NanoID generation |
FieldMapper |
JardisSupport\Contract\Data\FieldMapperInterface |
Bidirectional array-key mapping |
Full documentation, guides, and API reference:
docs.jardis.io/en/support/data
This package is licensed under the MIT License.
Jardis · Documentation · Headgent
This package ships with a skill for Claude Code, Cursor, Continue, and Aider. Install it in your consuming project:
composer require --dev jardis/dev-skillsMore details: https://docs.jardis.io/en/skills