neuron-php / dto
Easy, dynamic DTO creation, validation and mapping.
Installs: 278
Dependents: 2
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/neuron-php/dto
Requires
- php: ^8.4
- myclabs/deep-copy: ^1.12
- neuron-php/logging: 0.9.*
- neuron-php/validation: ^0.7.0
- symfony/yaml: ^6.4
Requires (Dev)
- phpmd/phpmd: ^2.15
- phpunit/phpunit: 9.*
README
Neuron-PHP DTO
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.
Table of Contents
- Installation
- Quick Start
- Core Features
- DTO Configuration
- Creating DTOs
- Validation
- Data Mapping
- Property Types
- Collections
- Advanced Usage
- Testing
- Best Practices
- More Information
Installation
Requirements
- PHP 8.4 or higher
- Composer
- symfony/yaml (^6.4)
- neuron-php/validation (^0.7.0)
Install via Composer
composer require neuron-php/dto
Quick Start
1. Define Your DTO Structure
Create 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: 120
2. Create and Use the DTO
use 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();
Core Features
- 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
- DTO Composition: Reuse DTOs by referencing existing DTO definitions
- 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
DTO Configuration
Basic Structure
DTOs are configured using YAML files with property definitions:
dto: propertyName: type: string|integer|boolean|array|object|etc required: true|false # Additional validation rules
Complete Example
dto: # 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: 20
Creating DTOs
From YAML Configuration
use 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;
Programmatic Creation
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);
Nested Objects
// 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;
DTO Composition (Reusable DTOs)
You can create reusable DTO definitions and reference them in other DTOs, making it easy to share common structures like timestamps, addresses, or user records across multiple DTOs.
Creating Reusable DTOs
First, create standalone DTO definition files:
common/timestamps.yaml
dto: createdAt: type: date_time required: true updatedAt: type: date_time required: false
common/address.yaml
dto: street: type: string required: true length: min: 3 max: 100 city: type: string required: true state: type: string required: true length: min: 2 max: 2 zipCode: type: string required: true pattern: '/^\d{5}(-\d{4})?$/'
common/user.yaml
dto: id: type: uuid required: true username: type: string required: true length: min: 3 max: 20 email: type: email required: true firstName: type: string required: true lastName: type: string required: true
Using Referenced DTOs
Reference these DTOs in your main DTO definition using type: dto with a ref parameter:
dto: id: type: uuid required: true title: type: string required: true # Reference to reusable timestamps DTO timestamps: type: dto ref: 'common/timestamps.yaml' required: true # Reference to reusable user DTO author: type: dto ref: 'common/user.yaml' required: true # Reference to reusable address DTO shippingAddress: type: dto ref: 'common/address.yaml' required: false
Working with Composed DTOs
use Neuron\Dto\Factory; // Create DTO with referenced DTOs $factory = new Factory('article.yaml'); $dto = $factory->create(); // Set values on the main DTO $dto->id = '550e8400-e29b-41d4-a716-446655440000'; $dto->title = 'My Article'; // Set values on referenced DTOs $dto->timestamps->createdAt = '2024-01-01 10:00:00'; $dto->timestamps->updatedAt = '2024-01-02 12:00:00'; $dto->author->id = '550e8400-e29b-41d4-a716-446655440001'; $dto->author->username = 'johndoe'; $dto->author->email = 'john@example.com'; $dto->author->firstName = 'John'; $dto->author->lastName = 'Doe'; $dto->shippingAddress->street = '123 Main St'; $dto->shippingAddress->city = 'New York'; $dto->shippingAddress->state = 'NY'; $dto->shippingAddress->zipCode = '10001'; // Validate entire structure including referenced DTOs $dto->validate(); // Export to JSON echo $dto->getAsJson();
Benefits of DTO Composition
- Reusability: Define common structures once, use them everywhere
- Consistency: Ensure the same validation rules across all uses
- Maintainability: Update the definition in one place
- Performance: Referenced DTOs are cached automatically
- Type Safety: Full validation support for nested structures
Path Resolution
Referenced paths are resolved relative to the parent DTO file:
# If this file is at: project/dtos/article.yaml # And you reference: 'common/timestamps.yaml' # The system will look for: project/dtos/common/timestamps.yaml # You can also use absolute paths: timestamps: type: dto ref: '/absolute/path/to/timestamps.yaml'
Validation
Built-in Validators
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(); }
Validation Rules
Length Validation
username: type: string length: min: 3 max: 20
Range Validation
age: type: integer range: min: 18 max: 65
Pattern Validation
phoneNumber: type: string pattern: '/^\+?[1-9]\d{1,14}$/' # E.164 format
Enum Validation
status: type: string enum: ['active', 'inactive', 'pending']
Custom Validation
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());
Data Mapping
Mapper Configuration
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.number
Using the Mapper
use 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'
Dynamic Mapping
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);
Property Types
Supported Types
| 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 |
dto |
Referenced DTO | Full DTO 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 |
Type Examples
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: international
Collections
Array of Objects
dto: 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(); }
Array of Primitives
dto: tags: type: array items: type: string length: min: 1 max: 20 scores: type: array items: type: integer range: min: 0 max: 100
Advanced Usage
Complex DTO Example
dto: # 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'
Custom DTO Class
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() ]; } }
DTO Factory with Caching
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]; } }
Testing
Unit Testing DTOs
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']); } }
Testing Mappers
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); } }
Best Practices
DTO Design
# 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
Validation Strategy
// 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);
Error Handling
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); }
Reusable DTOs
// 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(); } }
Performance Optimization
// 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); } }
Integration Examples
API Request Validation
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); } }
Database Integration
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; } }
More Information
- Neuron Framework: neuronphp.com
- GitHub: github.com/neuron-php/dto
- Packagist: packagist.org/packages/neuron-php/dto
License
MIT License - see LICENSE file for details