Object hydration patterns — from array/DB row to object
Concept
Object hydration is the process of populating an object's properties from raw data — typically from a database row, JSON payload, or form input. The inverse is serialization/dehydration — converting an object back to a raw data format.
Common patterns:
- Constructor hydration: Pass all fields to
__construct. Clean, immutable, but requires all fields upfront. - Static factory from array:
User::fromArray($row)— encapsulates the mapping logic in the model. - Property assignment loop: Iterate over the array and use
$obj->$key = $valueorReflection. - Reflection-based hydration: Use
ReflectionClassto set private/protected properties. Used by ORMs like Doctrine to hydrate objects without requiring public setters.
Hydration vs mass assignment in Eloquent: Eloquent's User::create($data) is hydration gated by the $fillable list. Only keys listed in $fillable are assigned. This prevents mass-assignment vulnerabilities (e.g., a user POSTing is_admin=1).
Casting and transformation during hydration: Good hydration doesn't just copy values — it applies the appropriate PHP types. A database '1' should become true for a boolean field. An ISO date string should become a Carbon/DateTimeImmutable object. Eloquent's $casts array defines these transformations.
Code Example
<?php
declare(strict_types=1);
// Constructor hydration — immutable
class UserDto
{
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $email,
public readonly bool $active,
) {}
public static function fromRow(array $row): self
{
return new self(
id: (int) $row['id'],
name: (string) $row['name'],
email: (string) $row['email'],
active: (bool) $row['active'],
);
}
}
$dbRow = ['id' => '1', 'name' => 'Alice', 'email' => 'alice@ex.com', 'active' => '1'];
$user = UserDto::fromRow($dbRow);
var_dump($user->active); // bool(true) — properly cast from "1"
// Reflection hydration — sets private properties (like Doctrine does)
class PrivateUser
{
private int $id;
private string $name;
private string $email;
public function getId(): int { return $this->id; }
public function getName(): string { return $this->name; }
}
function hydrateWithReflection(string $className, array $data): object
{
$rc = new ReflectionClass($className);
$obj = $rc->newInstanceWithoutConstructor(); // skips constructor
foreach ($data as $key => $value) {
if (!$rc->hasProperty($key)) continue;
$prop = $rc->getProperty($key);
$prop->setAccessible(true); // can set private properties
$prop->setValue($obj, $value);
}
return $obj;
}
$user = hydrateWithReflection(PrivateUser::class, ['id' => 1, 'name' => 'Bob', 'email' => 'b@ex.com']);
echo $user->getName(); // "Bob"
// Mass assignment with whitelist (Eloquent pattern)
class Model
{
protected array $fillable = [];
protected array $attributes = [];
public function fill(array $data): void
{
foreach ($data as $key => $value) {
if (in_array($key, $this->fillable, strict: true)) {
$this->attributes[$key] = $value;
}
}
}
}
class Post extends Model
{
protected array $fillable = ['title', 'body', 'published']; // NOT 'user_id'
}
$post = new Post();
$post->fill(['title' => 'Hello', 'body' => 'World', 'is_admin' => true]);
// 'is_admin' is silently ignored — not in $fillable