ufo-tech / dto-transformer
The library provides tools for two-way transformation of DTO objects ⇄ arrays, respecting typing, contracts, and flexible transformation logic.
Requires
- php: >=8.3
- ext-intl: *
- phpdocumentor/reflection-docblock: ^5.6
- phpdocumentor/type-resolver: ^1.10
- symfony/cache-contracts: ^3.5
- symfony/serializer: ^7.2
- symfony/validator: ^7.2
Requires (Dev)
- phpunit/phpunit: ^11.0
README
ufo-tech/dto-transformer is a PHP 8.3+ library for converting DTO objects to arrays and hydrating DTO objects from arrays.
The current implementation is built around replaceable services:
DTOTransformeris the main facade/service.DTOFromArrayTransformerhydrates arrays into DTO objects.DTOToArrayTransformernormalizes DTO objects into arrays.ParamHydratorInterfaceimplementations resolve typed values during hydration.PropertyNormalizerInterfaceimplementations normalize values during serialization.- Reflection metadata, DocBlocks, strict mode and type schemas are centralized and cacheable.
Installation
composer require ufo-tech/dto-transformer
Requirements:
- PHP
>=8.3 ext-intlsymfony/serializersymfony/validatorsymfony/cache-contractsphpdocumentor/reflection-docblockphpdocumentor/type-resolver
Quick Start
use Ufo\DTO\Factory\DefaultDTOTransformerFactory; final class UserDto { public function __construct( public string $name, public string $email, ) {} } $transformer = DefaultDTOTransformerFactory::default()->create(); $user = $transformer->transformFromArray(UserDto::class, [ 'name' => 'Alex', 'email' => 'alex@example.com', ]); $array = $transformer->transformToArray($user);
Static calls are still supported, but the transformer must be booted first:
use Ufo\DTO\DTOTransformer; use Ufo\DTO\Factory\DefaultDTOTransformerFactory; DTOTransformer::boot(DefaultDTOTransformerFactory::default()->create()); $dto = DTOTransformer::fromArray(UserDto::class, $payload); $array = DTOTransformer::toArray($dto);
If the static facade is used before DTOTransformer::boot(), NotInitializeException is thrown.
Default Factories
Use DefaultDTOTransformerFactory::default() for the standard composition:
use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Ufo\DTO\Factory\DefaultDTOTransformerFactory; $cache = new FilesystemAdapter(namespace: 'dto_transformer'); $transformer = DefaultDTOTransformerFactory::default( persistentCache: $cache, )->create();
The default factory wires:
RuntimeReflectionCacheReflectionMetadataProviderStrictModeResolverReflectionTypeSchemaResolverDefaultDTOTransformerFromArrayFactoryDefaultDTOTransformerToArrayFactoryDefaultPropertyNormalizerFactory
For custom wiring, instantiate DefaultDTOTransformerFactory with your own DTOTransformerFromArrayFactoryInterface and DTOTransformerToArrayFactoryInterface implementations.
Object To Array
$array = $transformer->transformToArray( dto: $dto, renameKey: ['name' => 'full_name'], asSmartArray: false, publicOnly: true, context: [], );
The default normalizer chain is created by DefaultPropertyNormalizerFactory:
new PropertyNormalizer([ new ScalarValueConverter(), new EnumNormalizer(), new DateTimeNormalizer($dateTimeValueConverter), new ArrayNormalizer(), new DtoNormalizer($metadataProvider, $serializationContextProvider), ]);
Normalizers implement:
use Ufo\DTO\Interfaces\Normalizer\PropertyNormalizerInterface; use Ufo\DTO\VO\NormalizationContext; interface PropertyNormalizerInterface { public function supports(mixed $data, NormalizationContext $context): bool; public function normalize(mixed $data, NormalizationContext $context): mixed; }
DtoNormalizer reads DTO properties through metadata, applies renameKey, publicOnly, SerializationContext, smart-array output and recursively delegates nested values back to the chain.
Serialization Context
use DateTimeInterface; use Ufo\DTO\Attributes\SerializationContext; use Ufo\DTO\VO\NormalizationContext; final class EventDto { public function __construct( #[SerializationContext([ NormalizationContext::DATE_FORMAT => DateTimeInterface::ATOM, NormalizationContext::DATE_TIMEZONE => 'UTC', ])] public DateTimeImmutable $createdAt, ) {} }
Default enum output:
- backed enums are normalized to their backing value;
- unit enums are normalized to the case name.
Default DateTime output format is Y-m-d H:i:s. Use SerializationContext or call-level context to change date format, timezone or timestamp output.
Array To Object
$dto = $transformer->transformFromArray(UserDto::class, [ 'name' => 'Alex', 'email' => 'alex@example.com', ]);
The default hydration chain is created by DefaultDTOTransformerFromArrayFactory and DefaultParamHydratorFactory:
new UnionParamHydrator(); new EnumParamHydrator(); new ScalarParamHydrator(); new ReflectionClassHydrator(); new ReflectionParameterHydrator(); new ReflectionPropertyHydrator(); new DateTimeHydrator(); new DtoHydrator($transformer, $customTransformers); new ArrayItemsHydrator(); new AdditionalHydrator(); new MixedHydrator();
Hydrators implement:
use Ufo\DTO\Interfaces\Hydrator\ParamHydratorInterface; use Ufo\DTO\VO\TransformationContext; interface ParamHydratorInterface { public function supports(array $schema): bool; public function resolve( array $schema, mixed $value, TransformationContext $context, ): mixed; }
ReflectionTypeSchemaResolver builds schemas from native PHP types, DocBlocks, AttrDTO, namespaces and strict-mode metadata. If no schema is available, the original value is kept.
Native Types
final class ProfileDto { public function __construct( public string $name, public int $age, public bool $active, ) {} }
Nested DTOs
final class OrderDto { public function __construct( public UserDto $user, ) {} } $order = DTOTransformer::fromArray(OrderDto::class, [ 'user' => [ 'name' => 'Alex', 'email' => 'alex@example.com', ], ]);
Collections
Collections can be described with DocBlocks:
final class TeamDto { /** * @param UserDto[] $users */ public function __construct( public array $users, ) {} }
Or with AttrDTO:
use Ufo\DTO\Attributes\AttrDTO; final class TeamDto { public function __construct( #[AttrDTO(UserDto::class, context: [ AttrDTO::C_COLLECTION => true, ])] public array $users, ) {} }
Smart Arrays
Smart arrays include the DTO class name in the payload:
$array = DTOTransformer::toArray($dto, asSmartArray: true); // [ // 'name' => 'Alex', // '$className' => App\Dto\UserDto::class, // ]
They can be restored with namespace aliases:
$dto = DTOTransformer::fromSmartArray($array, namespaces: [ DTOTransformer::DTO_NS_KEY => App\Dto::class, ]);
DateTime
Hydration supports DateTimeImmutable, DateTime, DateTimeInterface, strings, integer timestamps and float timestamps with microseconds.
final class EventDto { public function __construct( public DateTimeImmutable $createdAt, ) {} } $dto = DTOTransformer::fromArray(EventDto::class, [ 'createdAt' => '2026-05-08 14:30:00', ]);
Enums
Backed enums use tryFrom(). Integer-backed enums also accept numeric strings. Unit enums are resolved by case name, case-insensitively.
enum Status: string { case Active = 'active'; } final class UserDto { public function __construct( public Status $status, ) {} }
Attributes
AttrDTO
AttrDTO gives explicit denormalization hints:
use Ufo\DTO\Attributes\AttrDTO; final class WrapperDto { public function __construct( #[AttrDTO(UserDto::class, context: [ AttrDTO::C_COLLECTION => true, AttrDTO::C_STRICT => true, ])] public array $users, ) {} }
Supported context keys:
AttrDTO::C_COLLECTIONAttrDTO::C_STRICTAttrDTO::C_NSAttrDTO::C_RENAME_KEYSAttrDTO::C_TRANSFORMERAttrDTO::C_IS_ENUMAttrDTO::C_PROPERTY
StrictMode
Strict mode turns hydration failures into BadParamException. Without strict mode, invalid nested values may be kept as-is.
use Ufo\DTO\Attributes\StrictMode; final class UserDto { public function __construct( #[StrictMode] public int $age, ) {} }
DocBlock strict mode is also supported:
/** * @strictMode true */ final class UserDto { }
Extensibility
Add custom object-to-array behavior with PropertyNormalizerInterface:
use Ufo\DTO\Interfaces\Normalizer\PropertyNormalizerInterface; use Ufo\DTO\VO\NormalizationContext; final class MoneyNormalizer implements PropertyNormalizerInterface { public function supports(mixed $data, NormalizationContext $context): bool { return $data instanceof Money; } public function normalize(mixed $data, NormalizationContext $context): string { return $data->currency . ' ' . number_format($data->amount / 100, 2); } }
Add custom array-to-object behavior with ParamHydratorInterface:
use Ufo\DTO\Interfaces\Hydrator\ParamHydratorInterface; use Ufo\DTO\VO\TransformationContext; final class UuidHydrator implements ParamHydratorInterface { public function supports(array $schema): bool { return ($schema['format'] ?? null) === 'uuid'; } public function resolve(array $schema, mixed $value, TransformationContext $context): Uuid { return Uuid::fromString((string) $value); } }
For full control, provide your own factory implementations or construct DTOFromArrayTransformer / DTOToArrayTransformer with custom chains.
Symfony Integration
Install dependencies:
composer require ufo-tech/dto-transformer composer require phpdocumentor/reflection-docblock
Configure services:
services: _defaults: autowire: true autoconfigure: true _instanceof: Ufo\DTO\Interfaces\Hydrator\ParamHydratorInterface: tags: - dto.param_hydrator Ufo\DTO\Interfaces\Normalizer\PropertyNormalizerInterface: tags: - dto.param_normalizer Ufo\DTO\: resource: '../vendor/ufo-tech/dto-transformer/src' exclude: - '../vendor/ufo-tech/dto-transformer/src/Annotations/' - '../vendor/ufo-tech/dto-transformer/src/Attributes/' phpDocumentor\Reflection\DocBlockFactoryInterface: class: phpDocumentor\Reflection\DocBlockFactory factory: [ 'phpDocumentor\Reflection\DocBlockFactory', 'createInstance' ] Ufo\DTO\Interfaces\Normalizer\PropertyNormalizerChainInterface: class: Ufo\DTO\Transformer\Normalizer\PropertyNormalizer arguments: $converters: !tagged_iterator dto.param_normalizer Ufo\DTO\Interfaces\DTOFromArrayTransformerInterface: factory: [ '@Ufo\DTO\Factory\DefaultDTOTransformerFromArrayFactory', 'create' ] Ufo\DTO\Interfaces\DTOToArrayTransformerInterface: factory: [ '@Ufo\DTO\Factory\DefaultDTOTransformerToArrayFactory', 'create' ] Ufo\DTO\DTOTransformer: public: true arguments: $fromArrayTransformer: '@Ufo\DTO\Interfaces\DTOFromArrayTransformerInterface' $toArrayTransformer: '@Ufo\DTO\Interfaces\DTOToArrayTransformerInterface'
public: true is required if the transformer is accessed through:
$container->get(DTOTransformer::class)
or inside Bundle::boot().
Cache adapters are optional.
ufo.dto.cache: public: true class: Symfony\Component\Cache\Adapter\FilesystemAdapter arguments: $namespace: 'meta' $directory: '%rpc_cache_dirrectory%/dto-transformer' $defaultLifetime: '%rpc_filecache_ttl%'
Example:
use Ufo\DTO\DTOTransformer; final class UserService { public function __construct( private readonly DTOTransformer $transformer, ) { } }
$user = $this->transformer->fromArray(UserDTO::class, [ 'name' => 'Alex', ]); $array = $this->transformer->toArray($user);
Performance
Recommendations:
- reuse one
DTOTransformerservice instance; - pass a Symfony cache adapter to
DefaultDTOTransformerFactory::default(); - prefer native types where possible;
- use DocBlocks for collections and unions where native types are not enough;
- avoid rebuilding custom hydrator or normalizer chains per request.
Testing
docker exec php_dto php vendor/bin/phpunit
Benchmarks:
composer bench
License
MIT