Skip to content

Validation

This document explains what validation php-collective/dto provides and how to integrate with validation libraries for more complex rules.

Built-in Validation

Required Fields

When a field is marked as required, the DTO will throw an exception if that field is missing or null during instantiation.

xml
<dto name="User">
    <field name="id" type="int" required="true"/>
    <field name="email" type="string" required="true"/>
    <field name="nickname" type="string"/>  <!-- optional -->
</dto>
php
// This throws InvalidArgumentException:
// Required field missing in App\Dto\UserDto: email
$user = new UserDto(['id' => 1]);

// This works - nickname is optional
$user = new UserDto(['id' => 1, 'email' => 'test@example.com']);

Validation Rules

Fields support built-in validation rules for common constraints:

php
use PhpCollective\Dto\Config\Dto;
use PhpCollective\Dto\Config\Field;
use PhpCollective\Dto\Config\Schema;

return Schema::create()
    ->dto(Dto::create('User')->fields(
        Field::string('name')->required()->minLength(2)->maxLength(100),
        Field::string('email')->required()->pattern('/^[^@]+@[^@]+\.[^@]+$/'),
        Field::int('age')->min(0)->max(150),
        Field::float('score')->min(0.0)->max(100.0),
    ))
    ->toArray();

Or in XML:

xml
<dto name="User">
    <field name="name" type="string" required="true" minLength="2" maxLength="100"/>
    <field name="email" type="string" required="true" pattern="/^[^@]+@[^@]+\.[^@]+$/"/>
    <field name="age" type="int" min="0" max="150"/>
    <field name="score" type="float" min="0" max="100"/>
</dto>

Available Rules

RuleApplies ToDescription
minLengthstringMinimum string length (via mb_strlen)
maxLengthstringMaximum string length (via mb_strlen)
minint, floatMinimum numeric value (inclusive)
maxint, floatMaximum numeric value (inclusive)
patternstringRegex pattern that must match (via preg_match)

Behavior

  • Null fields skip validation — rules are only checked when a value is present
  • Required check runs before validation rules
  • On failure, an InvalidArgumentException is thrown with a descriptive message:
php
// InvalidArgumentException:
// Validation failed in App\Dto\UserDto: name must be at least 2 characters
$user = new UserDto(['name' => 'A', 'email' => 'a@b.com']);

// InvalidArgumentException:
// Validation failed in App\Dto\UserDto: email must match pattern /^[^@]+@[^@]+\.[^@]+$/
$user = new UserDto(['name' => 'Test', 'email' => 'invalid']);

Integrating with Validation Libraries

Symfony Validator

php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validation;

// Create your DTO
$userDto = new UserDto($formData);

// Define constraints separately
$constraints = new Assert\Collection([
    'email' => [new Assert\NotBlank(), new Assert\Email()],
    'age' => [new Assert\Range(['min' => 18, 'max' => 120])],
    'name' => [new Assert\Length(['min' => 2, 'max' => 100])],
]);

// Validate
$validator = Validation::createValidator();
$violations = $validator->validate($userDto->toArray(), $constraints);

if (count($violations) > 0) {
    foreach ($violations as $violation) {
        echo $violation->getPropertyPath() . ': ' . $violation->getMessage();
    }
}

Laravel Validation (Standalone)

php
use Illuminate\Validation\Factory;
use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;

$loader = new ArrayLoader();
$translator = new Translator($loader, 'en');
$factory = new Factory($translator);
$dtoRules = $userDto->validationRules();

$validator = $factory->make($userDto->toArray(), [
    'email' => 'required|email',
    'age' => 'required|integer|min:18|max:120',
    'name' => 'required|string|min:' . ($dtoRules['name']['minLength'] ?? 2) . '|max:' . ($dtoRules['name']['maxLength'] ?? 100),
]);

if ($validator->fails()) {
    $errors = $validator->errors()->all();
}

validationRules() returns framework-agnostic metadata, not Laravel-ready rule strings. Read from it and translate it into Laravel's rule format when you want to reuse DTO constraints.

Adapting DTO Rules

The practical way to reuse DTO metadata is to translate it in one place:

php
function laravelRulesFromDto(UserDto $dto): array
{
    $dtoRules = $dto->validationRules();

    return [
        'name' => array_filter([
            !empty($dtoRules['name']['required']) ? 'required' : null,
            'string',
            isset($dtoRules['name']['minLength']) ? 'min:' . $dtoRules['name']['minLength'] : null,
            isset($dtoRules['name']['maxLength']) ? 'max:' . $dtoRules['name']['maxLength'] : null,
        ]),
        'email' => ['required', 'email'],
    ];
}

For Symfony, the same idea applies with constraints:

php
use Symfony\Component\Validator\Constraints as Assert;

function symfonyConstraintsFromDto(UserDto $dto): Assert\Collection
{
    $dtoRules = $dto->validationRules();

    return new Assert\Collection([
        'name' => array_filter([
            !empty($dtoRules['name']['required']) ? new Assert\NotBlank() : null,
            isset($dtoRules['name']['minLength']) || isset($dtoRules['name']['maxLength'])
                ? new Assert\Length(
                    min: $dtoRules['name']['minLength'] ?? null,
                    max: $dtoRules['name']['maxLength'] ?? null,
                )
                : null,
        ]),
        'email' => [new Assert\NotBlank(), new Assert\Email()],
    ]);
}

Respect/Validation

php
use Respect\Validation\Validator as v;

$userValidator = v::key('email', v::email())
    ->key('age', v::intVal()->between(18, 120))
    ->key('name', v::stringType()->length(2, 100));

try {
    $userValidator->assert($userDto->toArray());
} catch (\Respect\Validation\Exceptions\NestedValidationException $e) {
    $errors = $e->getMessages();
}

Webmozart Assert (Simple Assertions)

For simple assertions without full validation framework:

php
use Webmozart\Assert\Assert;

$data = $userDto->toArray();

Assert::email($data['email'], 'Invalid email format');
Assert::range($data['age'], 18, 120, 'Age must be between 18 and 120');
Assert::lengthBetween($data['name'], 2, 100, 'Name must be 2-100 characters');

Validation in Controllers

A common pattern is to validate before creating the DTO:

php
class UserController
{
    public function create(Request $request): Response
    {
        // 1. Validate raw input first
        $validated = $this->validate($request->all(), [
            'email' => 'required|email',
            'name' => 'required|string|max:100',
        ]);

        // 2. Create DTO from validated data
        $userDto = new UserDto($validated);

        // 3. Process the DTO
        $this->userService->create($userDto);

        return new Response('Created');
    }
}

Or validate the DTO after creation:

php
class UserController
{
    public function create(Request $request): Response
    {
        // 1. Create DTO (handles type coercion, required fields)
        $userDto = new UserDto($request->all());

        // 2. Validate business rules
        $this->validator->validate($userDto->toArray(), $this->getUserRules());

        // 3. Process
        $this->userService->create($userDto);

        return new Response('Created');
    }
}

Type Coercion vs Validation

The library performs type coercion, not validation:

php
// String "123" is coerced to int 123
$dto = new UserDto(['id' => '123', 'email' => 'test@example.com']);
echo $dto->getId(); // int 123

// But invalid types throw TypeError
$dto = new UserDto(['id' => 'not-a-number', 'email' => 'test@example.com']);
// TypeError: Cannot assign string to property UserDto::$id of type int

This is PHP's native type system at work, not library validation.

Summary

Featurephp-collective/dtoValidation Library
Required fields
Type checking✅ (PHP native)
Min/max length
Numeric ranges
Regex patterns
Email/URL format✅ (via pattern)
Custom rules
Error messagesBasicRich

Recommendation: Use the built-in validation rules for simple structural constraints. For complex business logic validation (conditional rules, cross-field dependencies, custom messages), use a dedicated validation library alongside your DTOs.

Extracting Rules for Framework Validation

The validationRules() method returns a framework-agnostic array of validation rules from the DTO metadata. This is useful for bridging DTO rules to framework-native validators:

php
$dto = new UserDto();
$rules = $dto->validationRules();
// [
//     'name' => ['required' => true, 'minLength' => 2, 'maxLength' => 50],
//     'email' => ['pattern' => '/^[^@]+@[^@]+\.[^@]+$/'],
//     'age' => ['min' => 0, 'max' => 150],
// ]

The returned structure is intentionally simple. Frameworks like Laravel or Symfony still need a small adapter layer if you want to turn this metadata into native validator rules or constraints.

Only fields with at least one active rule are included. The returned keys match the config rule names: required, minLength, maxLength, min, max, pattern.

Released under the MIT License.