Skip to content

Motivation and Background

The Problem with Arrays

Working with complex and nested arrays quickly becomes problematic:

  • In templates, you don't know exactly what keys are available or what the nesting structure looks like
  • No IDE autocomplete or typehinting unless you're inside the same code that created the array
  • No verification of fields and their types with simple associative arrays
  • PHPStan and other introspection tools can't work well with untyped arrays

The Solution: DTOs

Data Transfer Objects (DTOs) are the best approach here, but creating them manually can be tedious.

Some people argue that arrays are faster and use less memory than objects. This might have been true in PHP 5.2, but modern PHP handles objects efficiently.

Other Existing Solutions

The PHP DTO ecosystem has evolved significantly with PHP 8.x features. Here's the current landscape (2025):

Active Libraries:

  • spatie/laravel-data (v4.18+): Laravel-specific, runtime reflection with PHP 8 attributes. Features validation, TypeScript generation, lazy properties, and Eloquent integration. Requires PHP 8.1+.
  • cuyz/valinor (v2.3+): Framework-agnostic runtime mapper with PHPStan/Psalm type support (generics, shaped arrays, integer ranges). Excellent error messages and normalization support. Requires PHP 8.1+.
  • symfony/serializer (v7/8): Component-based serialization with new JsonStreamer for streaming large datasets. Supports JSON, XML, YAML, CSV.
  • symfony/object-mapper (Symfony 7.3+): New lightweight ObjectMapper component for simple DTO hydration without full Serializer overhead.
  • jms/serializer (v3.32+): Mature annotation/attribute-driven serializer with versioning, Doctrine integration, and circular reference handling. Requires PHP 7.4+.

Deprecated:

Native PHP 8.2+ (No Library):

php
final readonly class UserDto
{
    public function __construct(
        public int $id,
        public string $email,
    ) {}
}

Sufficient for simple cases, but lacks collections, validation, and inflection support.

Common issues with runtime libraries:

  • Runtime reflection overhead on every instantiation
  • IDE support limited by "magic" - autocomplete depends on plugin quality
  • Static analysis requires additional annotations or plugins

Why Generated Code?

This library takes a fundamentally different approach: code generation instead of runtime reflection.

Other libraries leverage declared properties and reflection/introspection at runtime to finalize the DTO. What if we let a generator do that for us? Taking the maximum performance benefit from creating a customized object, while having all the addons we want on top for free?

We generate optimized DTOs where all inflection, reflection, validation and asserting is done at generation time. Using them is just as simple as with basic arrays, only with tons of benefits on top.

Key advantages of code generation:

  • Zero runtime reflection - no performance overhead per instantiation
  • Excellent IDE support - real methods mean perfect autocomplete and "Find Usages"
  • Perfect static analysis - PHPStan/Psalm work without plugins or annotations
  • Reviewable code - generated classes can be inspected in pull requests
  • No magic - what you see is exactly what runs

Comparison with Alternatives

Aspectphp-collective/dtolaravel-datavalinorsymfony/serializerjms/serializernative PHP
ApproachCode generationReflectionMappingReflectionReflectionManual
IDE AutocompleteExcellentGoodGoodGoodGoodExcellent
Static AnalysisExcellentGoodExcellentGoodGoodExcellent
Runtime PerformanceBestModerateModerateModerateModerateBest
ValidationRequired onlyFullGoodPartialPartialNone
TypeScript GenYesYesNoNoNoNo
CollectionsBuilt-inBuilt-inBuilt-inManualBuilt-inManual
InflectionBuilt-inManualManualManualManualManual
Immutable DTOsBuilt-inBuilt-inBuilt-inManualManualManual
Lazy PropertiesNoYesNoNoNoNo
Generics SupportPHPDoc onlyPartialExcellentPartialPartialNo
Error MessagesGoodGoodExcellentGoodGoodN/A
FrameworkAnyLaravelAnySymfonyAnyAny
PHP Requirement8.2+8.1+8.1+8.4+7.4+8.2+

When to choose php-collective/dto:

  • Performance is critical (25-60x faster than runtime libraries)
  • You want the best possible IDE and static analysis support
  • You prefer configuration files over code attributes
  • You need either mutable or immutable DTOs with explicit choice
  • You work with different key formats (camelCase, snake_case, dashed)
  • Code review of generated DTOs is valuable to your team

Summary

Strengths vs competition:

Aspectphp-collective/dtoRuntime Libraries
IDE/Static AnalysisExcellent (real methods)Good (reflection/magic)
Runtime PerformanceBest (25-60x faster)Moderate
Code ReviewGenerated code visible in PRMagic/runtime behavior
Inflection SupportBuilt-in (snake/camel/dash)Usually manual
Build-time ErrorsCatch issues at generationDiscover at runtime

Gaps compared to runtime libraries:

Featurephp-collective/dtolaravel-datavalinorjms/serializer
Validation RulesRequired onlyFullGoodPartial
Lazy PropertiesNoYesNoNo
Integer RangesNoNoYesNo
API VersioningNoNoNoYes
Eloquent IntegrationNot YetYesNoNo
Streaming/Large DataNoNoNoNo

Verdict: php-collective/dto is the only code-generation approach in the PHP DTO ecosystem, giving it unique advantages for performance (25-60x faster) and IDE support. Choose runtime libraries if you need advanced validation, lazy loading, or framework-specific integration.

Why Not Immutable by Default?

Arrays are somewhat immutable, so this is a fair point. The goal was to first make it work for easy use cases and simple usage. For most use cases, mutable objects are a good compromise - allowing easy modifications where needed.

Immutable means we either have to insert all data into the constructor or provide with...() methods. This should be a deliberate choice.

Why the Dto Suffix?

A Post or Article object will likely clash with existing entities or similar classes. Having to alias in all files is not ideal. Also consider Date and other reserved words.

So PostDto etc. is easy enough to avoid issues while not being much longer. Inside code it can also be helpful to keep prefixes in variables:

php
$postArray = [
    'title' => 'My cool Post',
];
$postDto = new PostDto($postArray);

This makes the code more readable in pull requests or when not directly inside the IDE.

Why No Interfaces?

Contracting with interfaces is important when building SOLID code. For generated classes it seems like overhead. From a stability perspective, manually modified code shouldn't extend/implement fluently changing generated code. The generated classes always have to be evaluated as a whole.

Value Objects

Value objects should work nicely with DTOs. Value objects like DateTime, Money, or custom ones are usually immutable by design.

The key difference: value objects can contain logic and "operations" between each other ($moneyOne->subtract($moneyTwo)), whereas a DTO must not contain anything beyond holding pure data and setting/getting.

Performance Benchmark

Benchmarks show php-collective/dto is 25-37x faster than spatie/laravel-data and 37-63x faster than cuyz/valinor, while being within 1.2-1.6x of hand-written plain PHP DTOs.

See Performance Guide for detailed benchmarks and optimization tips, or run them yourself:

bash
php benchmark/run.php              # Main benchmark suite
php benchmark/run-external.php     # Compare with other libraries

Developer speed, code readability and code reliability strongly increase with only a bit of speed decrease that usually doesn't matter for a normal web request.

Released under the MIT License.