0

Object hydration patterns — from array/DB row to object

Intermediate5 min read·php-08-016
interviewsql

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:

  1. Constructor hydration: Pass all fields to __construct. Clean, immutable, but requires all fields upfront.
  2. Static factory from array: User::fromArray($row) — encapsulates the mapping logic in the model.
  3. Property assignment loop: Iterate over the array and use $obj->$key = $value or Reflection.
  4. Reflection-based hydration: Use ReflectionClass to 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
<?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