dto-core is a lightweight, typed, and flexible base layer for working with Data Transfer Objects (DTOs) in PHP 8.1+.
It supports strict/lenient modes, nested DTOs, lazy evaluation via Closure, and full debug output.
- Strict or lenient validation modes
- Full support for nested DTOs (recursive)
- Property type validation and class-level error tracking
make()method (recommended) for flexible and different modes using, ornew DTOClass()for strict usingtoArray()method converts the current object instance into an array representationClosuresupport for lazy-loaded propertiesdebug()method with full structural output- Framework-agnostic β works with pure PHP classes
- Optimized for PHP 8.1+
- Compatible with Symfony 6.4, Laravel 10+, and modern typed PHP codebases.
- PHP 8.1 or higher
- symfony/var-dumper
Add to composer.json
"repositories": [ { "type": "vcs", "url": "https://github.com/technoquill/dto-core" } ]composer require technoquill/dto-coredto-core supports two primary approaches for defining DTO properties, depending on the desired level of strictness, static typing, and compatibility with constructor-based instantiation.
Properties are declared directly in the constructor using PHP 8.1+ promoted parameters.
final class UserDTO extends AbstractDTO { public function __construct( public int $id, public string $email, public bool $blocked, public string $created_at ) {} }- Ideal for strict, fully defined DTOs
- Perfect compatibility with
make()and nativenew DTO(...) - Properties are readonly by default and ideal for domain logic
Properties are declared as public class-level variables (optionally with defaults), typically without a constructor.
final class UserAddressDTO extends AbstractDTO { public string $street; public string $city; public string $postalCode; public string $country; public ?string $state = null; public ?string $houseNumber = null; public ?string $apartment = null; }- Useful for flexible, input-bound structures
- Well-suited for use with
make(array $data)where construction is data-driven - Allows partial initialization (
strict: false)
Mixing constructor-promoted properties and public properties in the same DTO is not allowed.
// β This is not valid: final class InvalidDTO extends AbstractDTO { public string $name; public function __construct( public int $id ) {} }This ensures predictable property population, reliable type enforcement, and consistent validation.
- Use constructor DTOs for domain-level, immutable structures
- Use dynamic DTOs for input mapping, transformation, or API data
Both styles are fully supported by make(), debug(), toArray(), and nested DTO handling.
final class UserDTO extends AbstractDTO { public function __construct( public int $id, public string $type, public string $first_name, public string $last_name, public string $email, public string $phone, public UserAddressDTO|array $address = [], // for ex. relation public string $annotation, public bool $blocked, public string $created_at ) { } } final class UserAddressDTO extends AbstractDTO { public function __construct( public string $street, public string $city, public string $postalCode, public string $country, public ?string $state = null, public ?string $houseNumber = null, public ?string $apartment = null ) {} } // Create via array $user = UserDTO::make([ 'id' => 435, 'type' => 'manager', 'first_name' => 'John', 'last_name' => 'Smith', 'email' => 'john@example.com', 'phone' => '123456789', 'address' => UserAddressDTO::make([ 'street' => '123 Main St', 'city' => 'New York', 'postalCode' => '10001', 'country' => 'USA', //'state' => 'NY', // optional; dto makes it nullable, because the property may be NULL by default 'houseNumber' => '123', 'apartment' => '123A', ])->toArray(), 'annotation' => static fn() => strip_tags('<p>Some annotation</p>'), 'blocked' => false, 'created_at' => '2022-10-03 22:59:52' ])->toArray(); dd($user); // or $dtoData = PaymentDTO::make($data, false); dd($dtoData->getErrors()); // Data output to an array is supported $payment = PaymentDTO::make($data)->toArray();final class OrderDTO extends AbstractDTO { public function __construct( public int $id, public PaymentDTO $payment ) {} } final class PaymentDTO extends AbstractDTO { public function __construct( public int $id, public float $amount ) {} } $order = OrderDTO::make([ 'id' => 10, 'payment' => PaymentDTO::make([ 'id' => 435, 'amount' => 765.35, ]) ]);DTOs can also be instantiated directly via the constructor (including nested DTOs), but all values must be fully typed and pre-resolved.
Note:
Always works in strict mode
$payment = (new PaymentDTO(435, 765.35))->toArray();src/ βββ AbstractDTO.php βββ Contracts/ β βββ DTOInterface.php βββ Support/ β βββ LoggerContext.php βββ Traits/ β βββ DebuggableTrait.php β βββ DTOTrait.php This package is released under the MIT license.
Β© technoquill