Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

Neuron-PHP/dto

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CI codecov

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: 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 = '[email protected]';
$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 = '[email protected]';
$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 = '[email protected]';
$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' => '[email protected]',
        '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' => '[email protected]'
];

// 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 = '[email protected]';
        $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 = '[email protected]';

        $json = $this->dto->getAsJson();
        $decoded = json_decode($json, true);

        $this->assertEquals('johndoe', $decoded['username']);
        $this->assertEquals('[email protected]', $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' => '[email protected]'
            ]
        ];

        $mapper->map($dto, $sourceData);

        $this->assertEquals('johndoe', $dto->username);
        $this->assertEquals('[email protected]', $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

License

MIT License - see LICENSE file for details

About

Easy DTO creation, validation and mapping.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages