drabek-digital / hydra
๐ Modern PHP serialization, deserialization and validation library using PHP 8.4 attributes
Requires
- php: >=8.5
- nikic/php-parser: ^5.0
- psr/simple-cache: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.0.1
- dev-claude/dazzling-dijkstra-1q6j2x
- dev-claude/bold-volta-ldylsy
- dev-docs/readme-php-85
- dev-claude/great-bohr-75z3r1
- dev-claude/optimistic-cannon-p7uwlw
- dev-claude/elegant-dirac-wb4s86
- dev-claude/adoring-knuth-yjdzas
- dev-claude/wonderful-ritchie-qubr02
- dev-claude/exciting-faraday-ctjb7h
- dev-feat/accept-int-for-float
This package is auto-updated.
Last update: 2026-10-23 08:56:09 UTC
README
This is a serialization & deserialization & validation library with minimum dependencies to allow you easily guard the app's valuable asset - data integrity. All that in PHP using top-notch PHP 8.5 language features.
Requires PHP 8.5 or newer.
The main principles of this approach are:
- Specification of serialization & deserialization & validation via PHP attributes in the DTOs
- First pass generates a DTOs hierarchy with hydration/dehydration logic in place through normal PHP code (
isset,array_key_exists,is_string,is_int, ...) and caches generated runtime for subsequent runs. - Validation is a separate runtime flow (
Validator) and is not code-generated. - Attributes should be specified above properties only when needed (in other words native typing should be used when possible)
- There should be sane defaults for most of the situations.
- Validation & dehydration & hydration are recursive.
Limitations:
- Circular references are not supported.
- Type coercion is prevented by design. The only exception is
intโfloatwidening (the same one PHP allows understrict_types; integers above 2^53 lose precision just like in PHP), so JSON5hydrates into afloatproperty (orfloatlist/map/array item) as5.0. Numeric strings and bools are still rejected. readonlyDTOs without constructor parameters are not currently supported for hydration. Use constructor-basedreadonlyDTOs (promoted params), or non-readonlypublic properties for property-only DTOs.
Naming strategy
Bidirectional way how the fields are mapped during serialization & deserialization.
- Default (1:1 mapping)
UnderscoreCase: use when in serialized format you haveunderscore_casewhile members are named incamelCase
Also there is possibility to rename a particular field (for legacy named fields) by using Rename attribute.
Rename is direction-aware: use from for hydration (input) and to for dehydration (output).
Mappers
- Scalars
boolfloatintmixedstring
Special types:
DateTimeInterfaceandDateTimeImmutableBackedEnum- Array and lists
array(of anything)Listof particular value type (with discriminator)Mapof particular key & value type (with discriminator)
- Shapes
Objectof particular type (with discriminator)
- Custom mappers
CustomMapperof particular type (or your own attribute implementingMapperAttributeinterface)
Validators
- Integer & float
- Range (min, max, inclusive, exclusive) -
NumberRange
- Range (min, max, inclusive, exclusive) -
- Strings
LengthRange(min, max) with one particular aliasNonEmptyString- Regex -
StringMatches - URL
- Array
- Count (min, max)
- Unique items (including custom comparator)
- Date & time
DateTimeRange(min, max, inclusive, exclusive)
- Custom
Usage
<?php declare(strict_types=1); use DrabekDigital\Hydra\Caching\FileCache; use DrabekDigital\Hydra\Caching\Psr16CacheAdapter; use DrabekDigital\Hydra\Dehydrator; use DrabekDigital\Hydra\Hydrator; use DrabekDigital\Hydra\Exceptions\HydrationFailure; use DrabekDigital\Hydra\Exceptions\DehydrationFailure; use DrabekDigital\Hydra\Exceptions\ValidationFailure; use DrabekDigital\Hydra\Validator; use Psr\SimpleCache\CacheInterface as Psr16CacheInterface; // Option A: built-in file cache (use a dedicated/private directory, not a shared world-writable temp path) $cache = new FileCache(sys_get_temp_dir()); // Option B: any PSR-16 cache implementation from your DI container/framework // (ideally a pool dedicated to Hydra; a shared pool is safe too, see "Runtime code loading" below) /** @var Psr16CacheInterface $psr16 */ $psr16 = $container->get(Psr16CacheInterface::class); $cache = new Psr16CacheAdapter($psr16); // optional: custom key prefix and TTL of generated code entries $cache = new Psr16CacheAdapter($psr16, prefix: 'hydra', ttl: 86400); // Hydrating from array to object $hydrator = new Hydrator($cache); try { $object = $hydrator->hydrate($input, MyDTO::class); $object = $hydrator->hydrateArray($input, discriminator: 'type', mapping: [ 'type1' => Type1::class, 'type2' => Type2::class, ]); } catch (HydrationFailure $e) { // handling } // Dehydration (excluding validation) $dehydrator = new Dehydrator($cache); try { $output = $dehydrator->dehydrate($object); $output = $dehydrator->dehydrateArray($arrayOfObjects); } catch (DehydrationFailure $e) { // handling } // Validation $validator = new Validator(); try { $validator->validate($object); } catch (ValidationFailure $e) { // handling }
Runtime code loading and temp directory safety
Hydra executes generated PHP runtime code loaded from cache.
FileCachestores generated code in files and loads it withrequire.Psr16CacheAdapterloads generated code from cache storage and executes it viaeval().
Because generated code is executable, treat cache storage as trusted runtime surface:
- keep cache backend private to your application process,
- do not share writable cache/temp directories with untrusted actors,
- prefer app-scoped directories over generic system temp locations in production.
Cache invalidation
Cache keys are versioned, so you do not need to clear the cache manually after a deployment:
- DTO changes โ every key contains a structure hash of the class (property types, defaults, attributes), so changing a DTO generates fresh code under a new key.
- Hydra upgrades โ every key also contains
CodeGen\GeneratorVersion::GENERATOR_VERSION, which is bumped whenever a release changes the generated code. Aftercomposer updatethe new code is generated automatically instead of executing stale code from a persistent cache (FileCachedirectory, Redis behindPsr16CacheAdapter, ...).
Entries written under old keys are no longer read but are not deleted either. Call clearCache() (or clear the backing cache) after an upgrade if you want to reclaim the space.
Clearing a PSR-16 cache
Psr16CacheAdapter::clear() (called by Hydrator::clearCache() / Dehydrator::clearCache() without a class name)
never calls clear() on the wrapped PSR-16 pool, so it is safe to pass your application's shared pool โ sessions,
rate limits or other cached data stay untouched.
Entries are stored under {prefix}.{generation}.{hash} keys, where the generation is a random token kept under
{prefix}.__generation (stored without TTL). Clearing only writes a new random token, which makes all previously
stored Hydra entries unreachable; the backend drops them once they expire or get evicted. If the token itself is
evicted or deleted, a new random one is generated, so old entries never become reachable again.
The token is read from the backend once per adapter instance. A clear made by another process (e.g. a deploy script) is picked up by adapter instances created afterwards, so restart long-running workers after clearing. Therefore:
- prefer a dedicated pool (or namespace) for Hydra where your framework allows it,
- when using a shared pool, pass a
ttlso orphaned entries do not occupy storage forever, - use a distinct
prefixfor each Hydra instance that should be cleared independently within one pool.
Errors handling
Hydration
Use getErrors on Failure exception to get particular instance of HydrationError which will contain
code- any backed enumprogrammerMessage- which will describe the error in programmer understandable way for debugging & logging purposeskey- property, array key etc... where the error is, can bestring,int,bool, ornullfullPath- full path from root towards the property, array key etc... where the error is. For exampleperson.interests.0.name
Dehydration
Use getErrors to get particular instance of DehydrationError which will contain
code- any backed enumprogrammerMessage- which will describe the error in programmer understandable way for debugging & logging purposeskey- property, array key etc... where the error is, can bestring,int,bool, ornullfullPath- full path from root towards the property, array key etc... where the error is. For exampleperson.interests.0.name
Validation
Use getErrors to get particular instance of ValidationError which will contain
code- any backed enumprogrammerMessage- which will describe the error in programmer understandable way for debugging & logging purposeskey- property, array key etc... where the error is, can bestring,int,bool, ornullfullPath- full path from root towards the property, array key etc... where the error is. For exampleperson.interests.0.name
Detailed docs
Potential future scope
- Serialization groups
- Lifecycle hooks
- Conditional Inclusion (
SkipWhen,IncludeIf) - Prefixed fields (
PrefixedWith)