Skip to content

Repository files navigation

CIcodecov

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

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: stringrequired: truelength:
min: 3max: 20email:
type: emailrequired: trueage:
type: integerrange:
min: 18max: 120

2. Create and Use the DTO

useNeuron\Dto\Factory;
// Create DTO from configuration$factory = newFactory('user.yaml');
$dto = $factory->create();
// Set values$dto->username = 'johndoe';
$dto->email = 'john@example.com';
$dto->age = 25;
// Validateif (!$dto->validate()) {
$errors = $dto->getErrors();
// Handle validation errors
}
// Export as JSONecho$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|etcrequired: true|false# Additional validation rules

Complete Example

dto:
# Simple string propertyfirstName:
type: stringrequired: truelength:
min: 2max: 50# Email with validationemail:
type: emailrequired: true# Integer with rangeage:
type: integerrange:
min: 0max: 150# Date with patternbirthDate:
type: datepattern: '/^\d{4}-\d{2}-\d{2}$/'# YYYY-MM-DD# Nested objectaddress:
type: objectrequired: trueproperties:
street:
type: stringrequired: truelength:
min: 5max: 100city:
type: stringrequired: truestate:
type: stringlength:
min: 2max: 2zipCode:
type: stringpattern: '/^\d{5}(-\d{4})?$/'# US ZIP code# Array of objectsphoneNumbers:
type: arrayitems:
type: objectproperties:
type:
type: stringenum: ['home', 'work', 'mobile']required: truenumber:
type: stringpattern: '/^\+?[\d\s\-\(\)]+$/'required: true# Array of primitivestags:
type: arrayitems:
type: stringlength:
min: 1max: 20

Creating DTOs

From YAML Configuration

useNeuron\Dto\Factory;
// Load from file$factory = newFactory('path/to/neuron.yaml');
$dto = $factory->create();
// Set properties$dto->firstName = 'John';
$dto->email = 'john@example.com';
$dto->age = 30;

Programmatic Creation

useNeuron\Dto\Dto;
useNeuron\Dto\Property;
$dto = newDto();
// Create string property$username = newProperty();
$username->setName('username');
$username->setType('string');
$username->setRequired(true);
$username->addLengthValidator(3, 20);
$dto->addProperty($username);
// Create email property$email = newProperty();
$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_timerequired: trueupdatedAt:
type: date_timerequired: false

common/address.yaml

dto:
street:
type: stringrequired: truelength:
min: 3max: 100city:
type: stringrequired: truestate:
type: stringrequired: truelength:
min: 2max: 2zipCode:
type: stringrequired: truepattern: '/^\d{5}(-\d{4})?$/'

common/user.yaml

dto:
id:
type: uuidrequired: trueusername:
type: stringrequired: truelength:
min: 3max: 20email:
type: emailrequired: truefirstName:
type: stringrequired: truelastName:
type: stringrequired: true

Using Referenced DTOs

Reference these DTOs in your main DTO definition using type: dto with a ref parameter:

dto:
id:
type: uuidrequired: truetitle:
type: stringrequired: true# Reference to reusable timestamps DTOtimestamps:
type: dtoref: 'common/timestamps.yaml'required: true# Reference to reusable user DTOauthor:
type: dtoref: 'common/user.yaml'required: true# Reference to reusable address DTOshippingAddress:
type: dtoref: 'common/address.yaml'required: false

Working with Composed DTOs

useNeuron\Dto\Factory;
// Create DTO with referenced DTOs$factory = newFactory('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 JSONecho$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: dtoref: '/absolute/path/to/timestamps.yaml'

Validation

Built-in Validators

The DTO component includes comprehensive validation for each property type:

// Validate entire DTOif( !$dto->validate() ) {
$errors = $dto->getErrors();
foreach( $errorsas$property => $propertyErrors ) {
echo"Property '$property' has errors:\n";
foreach( $propertyErrorsas$error ) {
echo" - $error\n";
}
}
}
// Validate specific property$usernameProperty = $dto->getProperty('username');
if (!$usernameProperty->validate()) {
$errors = $usernameProperty->getErrors();
}

Validation Rules

Length Validation

username:
type: stringlength:
min: 3max: 20

Range Validation

age:
type: integerrange:
min: 18max: 65

Pattern Validation

phoneNumber:
type: stringpattern: '/^\+?[1-9]\d{1,14}$/'# E.164 format

Enum Validation

status:
type: stringenum: ['active', 'inactive', 'pending']

Custom Validation

useNeuron\Validation\IValidator;
class CustomValidator implements IValidator
{
publicfunctionvalidate($value): bool
{
// Custom validation logicreturn$value !== 'forbidden';
}
publicfunctiongetError(): string
{
return'Value cannot be "forbidden"';
}
}
// Add to property$property->addValidator(newCustomValidator());

Data Mapping

Mapper Configuration

Create a mapping configuration (mapping.yaml):

map:
# Simple mappingexternal.username: dto.usernameexternal.user_email: dto.email# Nested mappingexternal.user.profile.age: dto.ageexternal.user.contact.street: dto.address.streetexternal.user.contact.city: dto.address.city# Array mappingexternal.phones: dto.phoneNumbersexternal.phones.type: dto.phoneNumbers.typeexternal.phones.value: dto.phoneNumbers.number

Using the Mapper

useNeuron\Dto\Mapper\FactoryasMapperFactory;
// Create mapper$mapperFactory = newMapperFactory('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 dataecho$dto->username; // 'johndoe'echo$dto->address->street; // '123 Main St'echo$dto->phoneNumbers[0]->number; // '+1234567890'

Dynamic Mapping

useNeuron\Dto\Mapper\Dynamic;
$mapper = newDynamic();
// 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

TypeDescriptionValidation
stringText valuesLength, pattern
integerWhole numbersRange, min, max
floatDecimal numbersRange, precision
booleanTrue/false valuesType checking
arrayLists of itemsItem validation
objectNested objectsProperty validation
dtoReferenced DTOFull DTO validation
emailEmail addressesRFC compliance
urlURLsURL format
dateDate valuesDate format
date_timeDate and timeDateTime format
timeTime valuesTime format
currencyMoney amountsCurrency format
uuidUUIDsUUID v4 format
ip_addressIP addressesIPv4/IPv6
phone_numberPhone numbersInternational format
namePerson namesName validation
einEIN numbersUS EIN format
upcUPC codesUPC-A format
numericAny numberNumeric validation
imageImage dataBase64/data URI, MIME type
base64Base64 encoded dataBase64 format

Type Examples

dto:
# String with constraintsusername:
type: stringlength:
min: 3max: 20pattern: '/^[a-zA-Z0-9_]+$/'# Email validationemail:
type: emailrequired: true# URL validationwebsite:
type: urlrequired: false# Date with formatbirthDate:
type: dateformat: 'Y-m-d'# Currencyprice:
type: currencyrange:
min: 0.01max: 999999.99# UUIDuserId:
type: uuidrequired: true# IP AddressclientIp:
type: ip_addressversion: 4# IPv4 only# Phone numberphone:
type: phone_numberformat: international# Image data (base64 or data URI)profilePicture:
type: imagerequired: falsedescription: "User profile picture as base64 or data:image URI"# Multiple imagesgallery:
type: arrayitems:
type: imagedescription: "Array of base64 encoded images"

Collections

Array of Objects

dto:
users:
type: arrayitems:
type: objectproperties:
id:
type: integerrequired: truename:
type: stringrequired: trueemail:
type: emailrequired: true
// Adding items to collection$dto->users[] = (object)[
'id' => 1,
'name' => 'John Doe',
'email' => 'john@example.com'
];
// Accessing collection itemsforeach ($dto->usersas$user) {
echo$user->name;
}
// Collection validation$collection = newCollection($dto->users);
if (!$collection->validate()) {
$errors = $collection->getErrors();
}

Array of Primitives

dto:
tags:
type: arrayitems:
type: stringlength:
min: 1max: 20scores:
type: arrayitems:
type: integerrange:
min: 0max: 100

Advanced Usage

Working with Images

The image type provides validation for base64-encoded image data and data URIs. It supports common image formats including JPEG, PNG, GIF, WebP, and SVG.

Image Property Configuration

dto:
# Simple image propertyavatar:
type: imagerequired: false# Image with data URI supportlogo:
type: imagerequired: truedescription: "Company logo as base64 or data:image/png;base64,..."

Using Image Properties in Code

useNeuron\Dto\Factory;
// Create DTO with image property$factory = newFactory([
'profile_pic' => [
'type' => 'image',
'required' => true
]
]);
$dto = $factory->create();
// Set image as base64$imageData = base64_encode(file_get_contents('photo.jpg'));
$dto->profile_pic = $imageData;
// Or use data URI format$dto->profile_pic = 'data:image/jpeg;base64,' . $imageData;
// Validate$dto->validate();
// Get JSON output (image remains as base64 string)$json = $dto->getAsJson();

Supported Image Formats

The image validator automatically detects and validates the following formats:

  • JPEG/JPG - Detected by JPEG file signature
  • PNG - Detected by PNG file signature
  • GIF - Supports both GIF87a and GIF89a
  • WebP - Modern image format
  • SVG - XML-based vector graphics (disabled by default for security - see below)

Image Validation Features

  • Base64 Encoding: Validates proper base64 encoding
  • Data URI Support: Accepts data:image/type;base64, format
  • MIME Type Detection: Automatically detects image type from file signatures
  • Format Validation: Ensures the data actually contains valid image content
  • Size Constraints: Can be configured with maximum file size limits (via custom validator)
  • SVG Security: SVG images are disabled by default as they can contain embedded scripts (XSS risk)

Security Considerations for SVG

SVG images are disabled by default because they are XML-based and can contain:

  • JavaScript code via <script> tags
  • Event handlers that execute JavaScript
  • External resource references
  • CSS that could be used for attacks

If you need to accept SVG images, you must:

  1. Explicitly enable SVG support in your validator configuration
  2. Sanitize SVG content before storage or display
  3. Serve SVG files with appropriate Content Security Policy headers
  4. Consider using a dedicated SVG sanitization library

To enable SVG support (use with caution):

// Create a custom validator with SVG enabled$imageValidator = new \Neuron\Validation\IsImage(
[], // allowed MIME types (empty = all)null, // max sizetrue, // check image datatrue// ALLOW SVG (security risk!)
);

Complex DTO Example

dto:
# User profile DTOprofile:
type: objectproperties:
personalInfo:
type: objectrequired: trueproperties:
firstName:
type: stringrequired: truelength:
min: 2max: 50lastName:
type: stringrequired: truelength:
min: 2max: 50dateOfBirth:
type: daterequired: truegender:
type: stringenum: ['male', 'female', 'other', 'prefer_not_to_say']contactInfo:
type: objectrequired: trueproperties:
emails:
type: arrayitems:
type: objectproperties:
type:
type: stringenum: ['personal', 'work']required: trueaddress:
type: emailrequired: trueverified:
type: booleandefault: falsephones:
type: arrayitems:
type: objectproperties:
type:
type: stringenum: ['mobile', 'home', 'work']number:
type: phone_numberrequired: trueprimary:
type: booleandefault: falsepreferences:
type: objectproperties:
newsletter:
type: booleandefault: truenotifications:
type: objectproperties:
email:
type: booleandefault: truesms:
type: booleandefault: falsepush:
type: booleandefault: truelanguage:
type: stringenum: ['en', 'es', 'fr', 'de']default: 'en'

Custom DTO Class

useNeuron\Dto\Dto;
class UserDto extends Dto
{
publicfunction__construct()
{
parent::__construct();
$this->loadConfiguration('user.yaml');
}
publicfunctiongetFullName(): string
{
return$this->firstName . '' . $this->lastName;
}
publicfunctionisAdult(): bool
{
return$this->age >= 18;
}
publicfunctiontoArray(): array
{
return [
'username' => $this->username,
'email' => $this->email,
'fullName' => $this->getFullName(),
'isAdult' => $this->isAdult()
];
}
}

DTO Factory with Caching

useNeuron\Dto\Factory;
class CachedDtoFactory extends Factory
{
privatestaticarray$cache = [];
publicfunctioncreate(): Dto
{
$cacheKey = md5($this->configPath);
if (!isset(self::$cache[$cacheKey])) {
self::$cache[$cacheKey] = parent::create();
}
// Return deep clone to prevent shared statereturncloneself::$cache[$cacheKey];
}
}

Testing

Unit Testing DTOs

usePHPUnit\Framework\TestCase;
useNeuron\Dto\Factory;
class DtoTest extends TestCase
{
private$dto;
protectedfunctionsetUp(): void
{
$factory = newFactory('test-dto.yaml');
$this->dto = $factory->create();
}
publicfunctiontestValidation(): 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);
}
publicfunctiontestValidData(): 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());
}
publicfunctiontestNestedObjects(): 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);
}
publicfunctiontestJsonExport(): 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
{
publicfunctiontestDataMapping(): void
{
$factory = newFactory('dto.yaml');
$dto = $factory->create();
$mapperFactory = newMapperFactory('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 namingdto:
firstName:
type: stringrequired: truelastName:
type: stringrequired: trueemailAddress:
type: emailrequired: true# Avoid: Inconsistent or unclear namesdto:
fname: # Too abbreviatedlast_name: # Inconsistent stylemail: # Ambiguous

Validation Strategy

// Always validate before processingif( !$dto->validate() ) {
// Log errors
Log::error('DTO validation failed', $dto->getErrors());
// Return early with error responsereturnnewValidationErrorResponse($dto->getErrors());
}
// Process valid data$result = $service->process($dto);

Error Handling

try {
$dto->username = $input['username'];
$dto->email = $input['email'];
if( !$dto->validate() ) {
thrownewValidationException($dto->getErrors());
}
$user = $userService->create($dto);
} catch( ValidationException$e ) {
// Handle validation errorsreturnresponse()->json([
'error' => 'Validation failed',
'details' => $e->getErrors()
], 422);
} catch (PropertyNotFound$e) {
// Handle missing propertyreturnresponse()->json([
'error' => 'Invalid property: ' . $e->getMessage()
], 400);
}

Reusable DTOs

// Base DTO for common propertiesabstractclass BaseDto extends Dto
{
protectedfunctionaddTimestamps(): void
{
$createdAt = newProperty();
$createdAt->setName('createdAt');
$createdAt->setType('date_time');
$this->addProperty($createdAt);
$updatedAt = newProperty();
$updatedAt->setName('updatedAt');
$updatedAt->setType('date_time');
$this->addProperty($updatedAt);
}
}
// Specific DTO extending baseclass UserDto extends BaseDto
{
publicfunction__construct()
{
parent::__construct();
$this->loadConfiguration('user.yaml');
$this->addTimestamps();
}
}

Performance Optimization

// Cache DTO definitionsclass DtoCache
{
privatestaticarray$definitions = [];
publicstaticfunctiongetDefinition(string$config): array
{
if (!isset(self::$definitions[$config])) {
self::$definitions[$config] = Yaml::parseFile($config);
}
returnself::$definitions[$config];
}
}
// Use lazy loading for nested objectsclass LazyDto extends Dto
{
privatearray$lazyProperties = [];
publicfunction__get(string$name)
{
if( isset( $this->lazyProperties[ $name ] ) ) {
// Load only when accessed$this->loadProperty($name);
}
returnparent::__get($name);
}
}

Integration Examples

API Request Validation

class ApiController
{
privateFactory$dtoFactory;
publicfunctioncreateUser(Request$request): Response
{
$dto = $this->dtoFactory->create('user');
// Map request data to DTO$mapper = newRequestMapper();
$mapper->map($dto, $request->all());
// Validateif( !$dto->validate() ) {
returnresponse()->json([
'errors' => $dto->getErrors()
], 422);
}
// Process valid data$user = $this->userService->create($dto);
returnresponse()->json($user, 201);
}
}

Database Integration

class UserRepository
{
publicfunctionsave(UserDto$dto): User
{
$user = newUser();
$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;
}
publicfunctiontoDto(User$user): UserDto
{
$factory = newFactory('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

License

MIT License - see LICENSE file for details

About

Easy DTO creation, validation and mapping.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages