PHPackages                             jakubboucek/hydrator - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. jakubboucek/hydrator

ActiveLibrary[Database &amp; ORM](/categories/database)

jakubboucek/hydrator
====================

Fast bidirectional hydrator between typed PHP entities and database rows or other data formats, built for modern PHP with property hooks support

v0.4.1(today)00MITPHPPHP ~8.4CI passing

Since Jul 29Pushed todayCompare

[ Source](https://github.com/jakubboucek/hydrator)[ Packagist](https://packagist.org/packages/jakubboucek/hydrator)[ RSS](/packages/jakubboucek-hydrator/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (3)Versions (6)Used By (0)

Hydrator
========

[](#hydrator)

Fast bidirectional hydrator between typed PHP entities and database rows (or other data formats), built for modern PHP.

Entities are plain data objects: typed public properties, [property hooks](https://www.php.net/manual/en/language.oop5.property-hooks.php), no magic getters/setters and no mandatory attributes. The only contract is the empty `Entity` marker interface — it keeps the entity a plain object while letting the hydrator (and your IDE) refuse foreign objects early instead of failing later with confusing field-mismatch errors. The hydrator maps entities to and from associative data — a database row, a raw PDO result or any other representation described by a *format*.

Warning

The library is in development stage (0.x versions): the API may change between minor versions until it stabilizes.

Why another hydrator
--------------------

[](#why-another-hydrator)

General mapping libraries struggle with entities written in modern PHP style. This library is designed around them by design:

- **Property hooks aware** — a virtual get-only property is skipped in both directions, a property with a set hook is writable, `private(set)`/`protected(set)`/`readonly` properties are extracted but never written.
- **Partial updates** — extraction distinguishes *uninitialized* from *null*: only initialized properties produce fields, so a partially filled entity naturally becomes a partial `UPDATE`.
- **Pass-through of already-typed values** — layers like [nette/database](https://github.com/nette/database) return `DateTimeImmutable`, `bool` and `DateInterval` instances; the hydrator accepts them as-is instead of demanding strings.
- **Database type nuances** — `DATE` vs `DATETIME` (`#[Type\Date]`) and `TIME` columns (day-scoped `#[Type\Time]` or full-range `DateInterval`) are first-class citizens.
- **Deterministic time zones** — every hydrated date-time is normalized into the application time zone.
- **Performance** — reflection runs once per entity class; per-row work is a plain loop over precomputed metadata (hundreds of thousands of rows per second). The library never caches data or entities, only its own metadata: data sets are processed as lazy single-pass streams.

### Built for gradual modernization

[](#built-for-gradual-modernization)

The hydrator is designed to work as an intermediate step when modernizing a legacy application: it requires no change of the database-access paradigm and no changes to the database structure. Retrofitting Doctrine or another full ORM into a large existing codebase tends to be a demanding, all-or-nothing endeavor; typed entities backed by this library can instead be adopted piecemeal — one table or one query at a time — while the rest of the application keeps its existing database layer, making it a practical vehicle for refactoring database access gradually.

The only hard requirement is PHP 8.4. For an older application that is usually a far smaller obstacle than a data-access rewrite: a PHP upgrade is well supported by automated tooling (such as Rector), whereas replacing the database layer of a grown codebase rarely is.

Installation
------------

[](#installation)

```
composer require jakubboucek/hydrator
```

Requires PHP 8.4+. No runtime dependencies.

Usage
-----

[](#usage)

```
use JakubBoucek\Hydrator\HydratorFactory;
use JakubBoucek\Hydrator\Format\NetteDatabase;

$factory = new HydratorFactory(
    format: NetteDatabase::class,                 // preferred format
    timeZone: new DateTimeZone('Europe/Prague'),  // app time zone (defaults to PHP default)
);

$articles = $factory->for(Article::class);

// single row: array or Traversable (Nette Row / ActiveRow)
$article = $articles->fromData($explorer->table('article')->get(1));

// whole result: lazy stream keyed by the table primary key
foreach ($articles->fromDataSet($explorer->table('article')) as $id => $article) {
    // ...
}

// partial update: only initialized properties are extracted
$patch = new Article();
$patch->title = 'Updated title';
$explorer->table('article')->where('id', $id)->update($articles->toData($patch));
```

The entity is a plain object:

```
use JakubBoucek\Hydrator\Attribute\Type;
use JakubBoucek\Hydrator\Entity;

class Article implements Entity
{
    public int $id;
    public string $title;
    public ?string $note;
    public bool $published;
    public DateTimeImmutable $createdAt;

    #[Type\Date]                        // DATE column: no time part
    public DateTimeImmutable $publishedOn;

    public DateInterval $readingTime;   // TIME column
    public ArticleStatus $status;       // BackedEnum, mapped by backing value

    public string $label {              // virtual property: ignored by the hydrator
        get => "#{$this->id} {$this->title}";
    }
}
```

Property names map to field names by convention (camelCase ↔ snake\_case by default, defined by the format).

### Strictness

[](#strictness)

Every writable property requires its field in data: a missing field, a `null` for a non-nullable property or a value of an unexpected type throws an exception with the entity class, property and field name in the message. Extra fields in data with no matching property are silently ignored, and fields of non-writable properties (`readonly`, `private(set)`, virtual get-only) are never required. All library exceptions implement the `JakubBoucek\Hydrator\Exception\HydratorException` marker interface.

Legacy zero dates (`'0000-00-00'`, `'0000-00-00 00:00:00'`) hydrate as `null` with an `E_USER_WARNING` — matching nette/database's behavior — so a non-nullable property over such data fails loudly instead of receiving a nonsense date.

Both strictness rules have explicit, per-call switches on `fromData()`/`fromDataSet()`:

- **`allowPartial: true`** tolerates missing fields — the corresponding properties stay *uninitialized*, mirroring the partial extraction: a sparse `SELECT id, title` hydrates a sparse entity whose `toData()` produces exactly those fields back. Combined with `into:` it acts as a merge/patch — absent fields keep the target's current values. Beware that the strict default is what catches column-name typos; pair `allowPartial` with `rejectUnknown` to keep that protection.
- **`rejectUnknown: true`** tightens the opposite direction: a data key that maps to no entity property throws (known keys cover all mapped properties including the non-writable ones, so a full-row roundtrip passes).

There is deliberately no factory-wide default for either switch — tolerance is a per-call decision, not a mode.

Formats
-------

[](#formats)

A *format* describes how values are represented in data: the field naming convention and the codecs for booleans, date-times, dates and intervals. Formats are stateless and identified by their class name:

- `Format\NetteDatabase` — for nette/database, which already converts values on both sides: instances pass through, booleans stay booleans. `fromDataSet()` auto-detects the primary key of a `Selection` (duck-typed, no hard dependency).
- `Format\Mysql` — for raw PDO/mysqli: date-times as `'Y-m-d H:i:s'`, dates as `'Y-m-d'`, booleans as `0`/`1`, TIME as `'HH:MM:SS'` strings.
- `Format\Json` — for decoded JSON payloads (APIs): property names as-is (camelCase), date-times as RFC 3339 (a foreign offset is recalculated into the app time zone), dates as `'Y-m-d'`, native booleans, times as `'HH:MM:SS'` strings.

### Export values by format

[](#export-values-by-format)

What `toData()` produces for each property type:

Property typeNetteDatabaseMysqlJson`int`, `float`, `string`as-isas-isas-is`bool``bool``1` / `0``bool``BackedEnum`backing valuebacking valuebacking value`DateTimeImmutable`instance 1)`'Y-m-d H:i:s'` 2)RFC 3339 2)`#[Type\Date]`instance 1)`'Y-m-d'` 2)`'Y-m-d'` 2)`#[Type\Time]``'H:i:s'` 3)`'H:i:s'` 3)`'H:i:s'` 3)`DateInterval`instance 1)`'HH:MM:SS'` 4)`'HH:MM:SS'` 4)`Struct`JSON string 5)JSON string 5)nested arraycustom typesby native typeby native typeby native type`mixed` / untypedas-isas-isas-is1\) Instance pass-through — the database layer formats it itself.
2\) Rendered in the application time zone.
3\) Wall clock of the value, no zone conversion; fractional seconds appended when non-zero. A plain time string is used even with nette/database — Nette would write an instance as a full `'Y-m-d H:i:s'`.
4\) Full TIME domain kept: sign, hours over 24, fractional seconds.
5\) The struct's own `toJson()` rendering; an empty struct is stored as `NULL`.

The `#[Fraction]` and `#[DateFormat]` attributes override these default renderings — see [Attributes](#attributes).

### Hydration inputs by format

[](#hydration-inputs-by-format)

What `fromData()` accepts for each property type:

Property typeNetteDatabaseMysqlJson`int`, `float`, `string`scalar (cast)scalar (cast)scalar (cast)`bool``bool`, `0`/`1`, `'0'`/`'1'``bool`, `0`/`1`, `'0'`/`'1'``bool` only`BackedEnum`backing value 6)backing value 6)backing value 6)`DateTimeImmutable`instance, string 7)instance, string 7)instance, string 7)`#[Type\Date]`instance, string 7)instance, string 7)instance, string 7)`#[Type\Time]`instance, `'HH:MM:SS'`, `DateInterval` 8)instance, `'HH:MM:SS'` 8)instance, `'HH:MM:SS'` 8)`DateInterval`instance, `'HH:MM:SS'` 9)instance, `'HH:MM:SS'` 9)instance, `'HH:MM:SS'` 9)`Struct`JSON string, `NULL` 10)JSON string, `NULL` 10)array, `null` 10)custom typesby native typeby native typeby native type`mixed` / untypedanything, as-isanything, as-isanything, as-is6\) `int` or `string`, cast to the enum backing type, mapped via `::from()`.
7\) Any `DateTimeInterface` instance is converted into the application time zone; a naive string is interpreted in it, a string carrying its own offset is recalculated into it.
8\) Day range enforced (`00:00:00 fromData($row);
$member->address->city = 'Plzeň';    // writable at any time, no null-checks
$member->notes->add('Paid by wire', 'admin', new DateTimeImmutable());
```

Rules of the mechanism:

- **An instance always exists**: a `NULL` column hydrates into an empty struct, so struct properties are declared non-nullable and are writable at any time. Partial-update semantics stay untouched (an uninitialized property still produces no field).
- **An empty struct is stored as `NULL`**, never as `'{}'` or `'[]'` — emptiness is defined by the struct itself (`toJson()` returning null).
- In the Json format structs travel as **nested arrays** and emptiness is explicit (`[]`).
- Structs must be constructible without arguments, and the array representation must stay plain JSON-serializable data — no objects inside (dates as strings).

Bundled implementations: `BaseStruct` (declared fields; unknown keys are dropped and nulls filtered — documented lossy traits), `DynamicStruct` (lossless free-form, an stdClass analogy), and the list-shaped showcases `TagListStruct` and `NoteListStruct` (`add…`/`remove…`, iteration, `toText()`).

Custom types
------------

[](#custom-types)

A custom type maps a domain value object to a single column through an *intermediate native type*: the value first passes the format codec of that native type — with all its strictness — and only then reaches the custom conversion. Custom types are therefore format-blind: the same type renders as `'Y-m-d H:i:s'` in Mysql, RFC 3339 in Json and an instance pass-through with nette/database, and how a bool or a date-time is represented never becomes the custom code's business.

**Own types** implement one typed sub-interface of the `CustomValue` marker — the interface choice declares the native type: `StringValue`, `IntValue`, `FloatValue`, `BoolValue`, `DateTimeValue`, `IntervalValue`.

```
use JakubBoucek\Hydrator\Value\IntValue;

final class Money implements IntValue
{
    private function __construct(
        public readonly int $cents,
    ) {}

    public static function fromNative(int $value): static   // exact type, no unions
    {
        return new static($value);
    }

    public function toNative(): ?int
    {
        return $this->cents;
    }
}
```

**Foreign classes** — types that exist independently and cannot implement the interface (ramsey/uuid and friends) — get a registered `TypeAdapter`:

```
use JakubBoucek\Hydrator\Value\NativeType;
use JakubBoucek\Hydrator\Adapter\TypeAdapter;

final class UuidAdapter implements TypeAdapter
{
    public static function provides(): array
    {
        return [
            UuidInterface::class => NativeType::String,
            LazyUuidFromString::class => NativeType::String,
        ];
    }

    public function import(mixed $value, string $targetClass): object
    {
        return Uuid::fromString((string) $value);
    }

    public function export(object $value): string
    {
        return (string) $value;
    }
}

$factory = new HydratorFactory(
    format: NetteDatabase::class,
    adapters: [UuidAdapter::class, new GeoAdapter($resolver)],
);
```

Rules of the mechanism:

- **Registration order is binding** — the first adapter declaring a class wins. Class-strings load lazily: an adapter is instantiated only when a processed entity actually uses a declared class. Instances suit adapters with dependencies and win over a class-string registration of the same class.
- **`provides()` is a pure function of the class**: exact-match string keys that may reference classes absent from the application (optional dependencies) — matching never triggers autoload and the capability map is plain, cacheable data by contract.
- **Adapters cannot shadow natively handled classes** (`DateTimeImmutable`, `DateInterval`, `BackedEnum`, `Struct` and `CustomValue` implementations) — that fails loudly at metadata build.
- **Null policy is deliberately asymmetric**: `fromNative()`/`import()` never receive null — a `NULL` field is decided by the property's nullability — while `toNative()`/`export()` may return null for inner nullness; the field is then stored as `NULL` and the value collapses to a plain null on the next hydration.

Tests
-----

[](#tests)

```
composer install
composer run test
composer run phpstan
```

Integration tests against a real MariaDB server (common column types over PDO, mysqli and nette/database in every `convertBoolean`/`newDateTime` configuration) run when `DATABASE_DSN` (plus optional `DATABASE_USER`/`DATABASE_PASSWORD`) points to a server, and skip otherwise.

License
-------

[](#license)

MIT. See the [LICENSE](LICENSE) file.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

5

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1657322?v=4)[Jakub Bouček](/maintainers/jakubboucek)[@jakubboucek](https://github.com/jakubboucek)

---

Top Contributors

[![jakubboucek](https://avatars.githubusercontent.com/u/1657322?v=4)](https://github.com/jakubboucek "jakubboucek (44 commits)")

---

Tags

entityhydrationhydratormappingnettenettedatabaseentitymappinghydratorhydration

###  Code Quality

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/jakubboucek-hydrator/health.svg)

```
[![Health](https://phpackages.com/badges/jakubboucek-hydrator/health.svg)](https://phpackages.com/packages/jakubboucek-hydrator)
```

###  Alternatives

[tatter/relations

Entity relationships for CodeIgniter 4

8924.7k1](/packages/tatter-relations)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
