A powerful Data Transfer Object (DTO) library for PHP 8.4+ that provides dynamic DTO creation, comprehensive validation, and flexible data mapping capabilities with support for nested structures and YAML configuration.
- Installation
- Quick Start
- Core Features
- DTO Configuration
- Creating DTOs
- Validation
- Data Mapping
- Property Types
- Collections
- Advanced Usage
- Testing
- Best Practices
- More Information
- PHP 8.4 or higher
- Composer
- symfony/yaml (^6.4)
- neuron-php/validation (^0.7.0)
composer require neuron-php/dtoCreate a YAML configuration file (user.yaml):
dto:
username:
type: string
required: true
length:
min: 3
max: 20
email:
type: email
required: true
age:
type: integer
range:
min: 18
max: 120use Neuron\Dto\Factory;
// Create DTO from configuration
$factory = new Factory('user.yaml');
$dto = $factory->create();
// Set values
$dto->username = 'johndoe';
$dto->email = 'john@example.com';
$dto->age = 25;
// Validate
if (!$dto->validate()) {
$errors = $dto->getErrors();
// Handle validation errors
}
// Export as JSON
echo $dto->getAsJson();- Dynamic DTO Creation: Generate DTOs from YAML configuration files
- Comprehensive Validation: Built-in validators for 20+ data types
- Nested Structures: Support for complex, hierarchical data models
- Data Mapping: Transform external data structures to DTOs
- Type Safety: Strict type checking and validation
- Collections: Handle arrays of objects with validation
- JSON Export: Easy serialization to JSON format
- Custom Validators: Extend with custom validation logic
DTOs are configured using YAML files with property definitions:
dto:
propertyName:
type: string|integer|boolean|array|object|etc
required: true|false
# Additional validation rulesdto:
# Simple string property
firstName:
type: string
required: true
length:
min: 2
max: 50
# Email with validation
email:
type: email
required: true
# Integer with range
age:
type: integer
range:
min: 0
max: 150
# Date with pattern
birthDate:
type: date
pattern: '/^\d{4}-\d{2}-\d{2}$/' # YYYY-MM-DD
# Nested object
address:
type: object
required: true
properties:
street:
type: string
required: true
length:
min: 5
max: 100
city:
type: string
required: true
state:
type: string
length:
min: 2
max: 2
zipCode:
type: string
pattern: '/^\d{5}(-\d{4})?$/' # US ZIP code
# Array of objects
phoneNumbers:
type: array
items:
type: object
properties:
type:
type: string
enum: ['home', 'work', 'mobile']
required: true
number:
type: string
pattern: '/^\+?[\d\s\-\(\)]+$/'
required: true
# Array of primitives
tags:
type: array
items:
type: string
length:
min: 1
max: 20use Neuron\Dto\Factory;
// Load from file
$factory = new Factory('path/to/neuron.yaml');
$dto = $factory->create();
// Set properties
$dto->firstName = 'John';
$dto->email = 'john@example.com';
$dto->age = 30;use Neuron\Dto\Dto;
use Neuron\Dto\Property;
$dto = new Dto();
// Create string property
$username = new Property();
$username->setName('username');
$username->setType('string');
$username->setRequired(true);
$username->addLengthValidator(3, 20);
$dto->addProperty($username);
// Create email property
$email = new Property();
$email->setName('email');
$email->setType('email');
$email->setRequired(true);
$dto->addProperty($email);// Setting nested properties
$dto->address->street = '123 Main St';
$dto->address->city = 'New York';
$dto->address->state = 'NY';
$dto->address->zipCode = '10001';
// Accessing nested properties
$street = $dto->address->street;
$city = $dto->address->city;The DTO component includes comprehensive validation for each property type:
// Validate entire DTO
if( !$dto->validate() )
{
$errors = $dto->getErrors();
foreach( $errors as $property => $propertyErrors )
{
echo "Property '$property' has errors:\n";
foreach( $propertyErrors as $error )
{
echo " - $error\n";
}
}
}
// Validate specific property
$usernameProperty = $dto->getProperty('username');
if (!$usernameProperty->validate()) {
$errors = $usernameProperty->getErrors();
}username:
type: string
length:
min: 3
max: 20age:
type: integer
range:
min: 18
max: 65phoneNumber:
type: string
pattern: '/^\+?[1-9]\d{1,14}$/' # E.164 formatstatus:
type: string
enum: ['active', 'inactive', 'pending']use Neuron\Validation\IValidator;
class CustomValidator implements IValidator
{
public function validate($value): bool
{
// Custom validation logic
return $value !== 'forbidden';
}
public function getError(): string
{
return 'Value cannot be "forbidden"';
}
}
// Add to property
$property->addValidator(new CustomValidator());Create a mapping configuration (mapping.yaml):
map:
# Simple mapping
external.username: dto.username
external.user_email: dto.email
# Nested mapping
external.user.profile.age: dto.age
external.user.contact.street: dto.address.street
external.user.contact.city: dto.address.city
# Array mapping
external.phones: dto.phoneNumbers
external.phones.type: dto.phoneNumbers.type
external.phones.value: dto.phoneNumbers.numberuse Neuron\Dto\Mapper\Factory as MapperFactory;
// Create mapper
$mapperFactory = new MapperFactory('mapping.yaml');
$mapper = $mapperFactory->create();
// External data structure
$externalData = [
'external' => [
'username' => 'johndoe',
'user_email' => 'john@example.com',
'user' => [
'profile' => [
'age' => 30
],
'contact' => [
'street' => '123 Main St',
'city' => 'New York'
]
],
'phones' => [
['type' => 'mobile', 'value' => '+1234567890'],
['type' => 'home', 'value' => '+0987654321']
]
]
];
// Map to DTO
$mapper->map($dto, $externalData);
// Now DTO contains mapped data
echo $dto->username; // 'johndoe'
echo $dto->address->street; // '123 Main St'
echo $dto->phoneNumbers[0]->number; // '+1234567890'use Neuron\Dto\Mapper\Dynamic;
$mapper = new Dynamic();
// Define mappings programmatically
$mapper->addMapping('source.field1', 'target.property1');
$mapper->addMapping('source.nested.field2', 'target.property2');
// Map data
$mapper->map($dto, $sourceData);| Type | Description | Validation |
|---|---|---|
string |
Text values | Length, pattern |
integer |
Whole numbers | Range, min, max |
float |
Decimal numbers | Range, precision |
boolean |
True/false values | Type checking |
array |
Lists of items | Item validation |
object |
Nested objects | Property validation |
email |
Email addresses | RFC compliance |
url |
URLs | URL format |
date |
Date values | Date format |
date_time |
Date and time | DateTime format |
time |
Time values | Time format |
currency |
Money amounts | Currency format |
uuid |
UUIDs | UUID v4 format |
ip_address |
IP addresses | IPv4/IPv6 |
phone_number |
Phone numbers | International format |
name |
Person names | Name validation |
ein |
EIN numbers | US EIN format |
upc |
UPC codes | UPC-A format |
numeric |
Any number | Numeric validation |
dto:
# String with constraints
username:
type: string
length:
min: 3
max: 20
pattern: '/^[a-zA-Z0-9_]+$/'
# Email validation
email:
type: email
required: true
# URL validation
website:
type: url
required: false
# Date with format
birthDate:
type: date
format: 'Y-m-d'
# Currency
price:
type: currency
range:
min: 0.01
max: 999999.99
# UUID
userId:
type: uuid
required: true
# IP Address
clientIp:
type: ip_address
version: 4 # IPv4 only
# Phone number
phone:
type: phone_number
format: internationaldto:
users:
type: array
items:
type: object
properties:
id:
type: integer
required: true
name:
type: string
required: true
email:
type: email
required: true// Adding items to collection
$dto->users[] = (object)[
'id' => 1,
'name' => 'John Doe',
'email' => 'john@example.com'
];
// Accessing collection items
foreach ($dto->users as $user) {
echo $user->name;
}
// Collection validation
$collection = new Collection($dto->users);
if (!$collection->validate()) {
$errors = $collection->getErrors();
}dto:
tags:
type: array
items:
type: string
length:
min: 1
max: 20
scores:
type: array
items:
type: integer
range:
min: 0
max: 100dto:
# User profile DTO
profile:
type: object
properties:
personalInfo:
type: object
required: true
properties:
firstName:
type: string
required: true
length:
min: 2
max: 50
lastName:
type: string
required: true
length:
min: 2
max: 50
dateOfBirth:
type: date
required: true
gender:
type: string
enum: ['male', 'female', 'other', 'prefer_not_to_say']
contactInfo:
type: object
required: true
properties:
emails:
type: array
items:
type: object
properties:
type:
type: string
enum: ['personal', 'work']
required: true
address:
type: email
required: true
verified:
type: boolean
default: false
phones:
type: array
items:
type: object
properties:
type:
type: string
enum: ['mobile', 'home', 'work']
number:
type: phone_number
required: true
primary:
type: boolean
default: false
preferences:
type: object
properties:
newsletter:
type: boolean
default: true
notifications:
type: object
properties:
email:
type: boolean
default: true
sms:
type: boolean
default: false
push:
type: boolean
default: true
language:
type: string
enum: ['en', 'es', 'fr', 'de']
default: 'en'use Neuron\Dto\Dto;
class UserDto extends Dto
{
public function __construct()
{
parent::__construct();
$this->loadConfiguration('user.yaml');
}
public function getFullName(): string
{
return $this->firstName . ' ' . $this->lastName;
}
public function isAdult(): bool
{
return $this->age >= 18;
}
public function toArray(): array
{
return [
'username' => $this->username,
'email' => $this->email,
'fullName' => $this->getFullName(),
'isAdult' => $this->isAdult()
];
}
}use Neuron\Dto\Factory;
class CachedDtoFactory extends Factory
{
private static array $cache = [];
public function create(): Dto
{
$cacheKey = md5($this->configPath);
if (!isset(self::$cache[$cacheKey])) {
self::$cache[$cacheKey] = parent::create();
}
// Return deep clone to prevent shared state
return clone self::$cache[$cacheKey];
}
}use PHPUnit\Framework\TestCase;
use Neuron\Dto\Factory;
class DtoTest extends TestCase
{
private $dto;
protected function setUp(): void
{
$factory = new Factory('test-dto.yaml');
$this->dto = $factory->create();
}
public function testValidation(): void
{
$this->dto->username = 'ab'; // Too short
$this->dto->email = 'invalid-email';
$this->assertFalse($this->dto->validate());
$errors = $this->dto->getErrors();
$this->assertArrayHasKey('username', $errors);
$this->assertArrayHasKey('email', $errors);
}
public function testValidData(): void
{
$this->dto->username = 'johndoe';
$this->dto->email = 'john@example.com';
$this->dto->age = 25;
$this->assertTrue($this->dto->validate());
$this->assertEmpty($this->dto->getErrors());
}
public function testNestedObjects(): void
{
$this->dto->address->street = '123 Main St';
$this->dto->address->city = 'New York';
$this->assertEquals('123 Main St', $this->dto->address->street);
$this->assertEquals('New York', $this->dto->address->city);
}
public function testJsonExport(): void
{
$this->dto->username = 'johndoe';
$this->dto->email = 'john@example.com';
$json = $this->dto->getAsJson();
$decoded = json_decode($json, true);
$this->assertEquals('johndoe', $decoded['username']);
$this->assertEquals('john@example.com', $decoded['email']);
}
}class MapperTest extends TestCase
{
public function testDataMapping(): void
{
$factory = new Factory('dto.yaml');
$dto = $factory->create();
$mapperFactory = new MapperFactory('mapping.yaml');
$mapper = $mapperFactory->create();
$sourceData = [
'external' => [
'user_name' => 'johndoe',
'user_email' => 'john@example.com'
]
];
$mapper->map($dto, $sourceData);
$this->assertEquals('johndoe', $dto->username);
$this->assertEquals('john@example.com', $dto->email);
}
}# Good: Clear, consistent naming
dto:
firstName:
type: string
required: true
lastName:
type: string
required: true
emailAddress:
type: email
required: true
# Avoid: Inconsistent or unclear names
dto:
fname: # Too abbreviated
last_name: # Inconsistent style
mail: # Ambiguous// Always validate before processing
if( !$dto->validate() )
{
// Log errors
Log::error('DTO validation failed', $dto->getErrors());
// Return early with error response
return new ValidationErrorResponse($dto->getErrors());
}
// Process valid data
$result = $service->process($dto);try
{
$dto->username = $input['username'];
$dto->email = $input['email'];
if( !$dto->validate() )
{
throw new ValidationException($dto->getErrors());
}
$user = $userService->create($dto);
}
catch( ValidationException $e )
{
// Handle validation errors
return response()->json([
'error' => 'Validation failed',
'details' => $e->getErrors()
], 422);
}
catch (PropertyNotFound $e)
{
// Handle missing property
return response()->json([
'error' => 'Invalid property: ' . $e->getMessage()
], 400);
}// Base DTO for common properties
abstract class BaseDto extends Dto
{
protected function addTimestamps(): void
{
$createdAt = new Property();
$createdAt->setName('createdAt');
$createdAt->setType('date_time');
$this->addProperty($createdAt);
$updatedAt = new Property();
$updatedAt->setName('updatedAt');
$updatedAt->setType('date_time');
$this->addProperty($updatedAt);
}
}
// Specific DTO extending base
class UserDto extends BaseDto
{
public function __construct()
{
parent::__construct();
$this->loadConfiguration('user.yaml');
$this->addTimestamps();
}
}// Cache DTO definitions
class DtoCache
{
private static array $definitions = [];
public static function getDefinition(string $config): array
{
if (!isset(self::$definitions[$config])) {
self::$definitions[$config] = Yaml::parseFile($config);
}
return self::$definitions[$config];
}
}
// Use lazy loading for nested objects
class LazyDto extends Dto
{
private array $lazyProperties = [];
public function __get(string $name)
{
if( isset( $this->lazyProperties[ $name ] ) )
{
// Load only when accessed
$this->loadProperty($name);
}
return parent::__get($name);
}
}class ApiController
{
private Factory $dtoFactory;
public function createUser(Request $request): Response
{
$dto = $this->dtoFactory->create('user');
// Map request data to DTO
$mapper = new RequestMapper();
$mapper->map($dto, $request->all());
// Validate
if( !$dto->validate() )
{
return response()->json([
'errors' => $dto->getErrors()
], 422);
}
// Process valid data
$user = $this->userService->create($dto);
return response()->json($user, 201);
}
}class UserRepository
{
public function save(UserDto $dto): User
{
$user = new User();
$user->username = $dto->username;
$user->email = $dto->email;
$user->profile = json_encode([
'firstName' => $dto->firstName,
'lastName' => $dto->lastName,
'address' => [
'street' => $dto->address->street,
'city' => $dto->address->city,
'state' => $dto->address->state,
'zipCode' => $dto->address->zipCode
]
]);
$user->save();
return $user;
}
public function toDto(User $user): UserDto
{
$factory = new Factory('user.yaml');
$dto = $factory->create();
$dto->username = $user->username;
$dto->email = $user->email;
$profile = json_decode($user->profile, true);
$dto->firstName = $profile['firstName'];
$dto->lastName = $profile['lastName'];
$dto->address->street = $profile['address']['street'];
$dto->address->city = $profile['address']['city'];
return $dto;
}
}- Neuron Framework: neuronphp.com
- GitHub: github.com/neuron-php/dto
- Packagist: packagist.org/packages/neuron-php/dto
MIT License - see LICENSE file for details