Design Decisions
Non-Goals
These features are intentionally out of scope:
- ORM/Database integration - Keep library focused on data transfer
- Request handling - Framework-specific, use adapters
- Dependency injection - Use framework DI containers
- Runtime reflection - Defeats purpose of code generation
Validation
The library provides focused built-in validation:
- Required Fields
- Type Checking
- Common field constraints (
minLength,maxLength,min,max,pattern)
For more complex validation, you can use a service like Respect/Validation.
We don't want to bloat the library with a full validation DSL when dedicated validators already solve richer use cases better.
Best Practices
Keep DTOs simple - DTOs are data containers, not business logic holders. Consider using a separate validation service for complex rules.
Fail fast - Use
requiredfor fields that must always be present. This catches errors at construction time.Use type hints - PHP's type system already validates types. A
stringfield won't accept an integer.Validate at boundaries - Validate input at API/form boundaries, not deep in business logic.
Don't over-validate - Internal DTOs used between trusted services don't need the same validation as user input DTOs.
Test validation - Write unit tests for your validation rules.
Custom Casters / Transformers
Current features are sufficient:
// Input transformation
->factory('fromArray') // Calls ClassName::fromArray($value)
->factory('fromString') // Calls ClassName::fromString($value)
->factory('External::create') // Calls External::create($value)
// Output transformation
->serialize('array') // Calls $obj->toArray()
->serialize('string') // Calls $obj->__toString()
->serialize('FromArrayToArray') // Full round-tripPlus auto-detection for:
- FromArrayToArrayInterface
- JsonSerializable
- Enums (backed/unit)
Additional casters add complexity without real benefit.
Readonly Properties (PHP 8.1+)
Readonly properties are supported, but they are a deliberate tradeoff rather than the default recommendation.
Tradeoffs:
- They change the generated API shape by exposing
public readonlyproperties instead of the usual protected-property plus accessor pattern. - They are stricter than the default mutable model, so they fit best when the DTO is fully initialized up front.
- Immutable DTOs with
with*()methods remain the more flexible default for most applications.
Verdict: supported for teams that want language-level readonly semantics, but immutable DTOs are still the more ergonomic default in this library.
Best Practices
- Use immutable DTOs for readonly behavior.
- Avoid mixing mutable and immutable DTOs.
Additional Serialization Formats (XML, CSV)
Not at this point.
Reason: Out of scope. Use symfony/serializer for complex serialization needs.
Advanced Type Constraints (positive-int, non-empty-string)
Not at this point.
Reason: Validation concern, not type concern. Use validation rules instead.
Computed Properties
No need on the generated part.
Reason: Use traits instead.
// config
Dto::create('User')->traits(\App\Traits\UserComputedTrait::class)->fields(...)
// trait
trait UserComputedTrait {
public function getFullName(): string {
return $this->firstName . ' ' . $this->lastName;
}
}Lifecycle Hooks
Not needed.
Reason: Use traits instead.
class UserDto extends AbstractUserDto {
protected function setFromArray(...): static {
// pre-processing
parent::setFromArray(...);
// post-processing
return $this;
}
}Constructor promotion (PHP 8.0+)
The real value of constructor promotion:
- Reduces boilerplate in hand-written code
- Our code is generated - verbosity isn't a maintenance burden
Verdict: Not useful for this library. The flexibility of optional fields and incremental population is more valuable than the syntactic sugar of constructor promotion.