gpalyan/dto-forge

Advanced DTO system with casting, validation, masks, nested DTOs

Maintainers

Package info

github.com/GaiPalyan/dtoforge

pkg:composer/gpalyan/dto-forge

Statistics

Installs: 65

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 1

v2.0.0 2026-05-31 09:19 UTC

This package is auto-updated.

Last update: 2026-05-31 12:42:41 UTC


README

Tests codecov Latest Stable Version License

A flexible and powerful Data Transfer Object (DTO) library for PHP that provides validation, type casting, default value generation, and convenient data manipulation methods.

Contents

Features

  • Easy DTO Creation - Create DTOs from arrays, JSON, or use fluent setters
  • Built-in Validation - Validate data with custom rules (required fields, INN validation, etc.)
  • Type Casting - Automatic type conversion for scalar values
  • Data Manipulation - Merge, diff, fill, and clone DTOs
  • Nested DTOs - Full support for nested DTO structures
  • Default Values - Generate default values automatically when needed
  • Serialization - Convert to arrays and JSON easily
  • Masking - Redact sensitive fields on serialization without mutating the DTO

Installation

composer require gpalyan/dto-forge

Basic Usage

Creating DTOs

All DTOs must extend BaseDto:

use Forge\Dto\Support\BaseDto;

final class SimpleDto extends BaseDto
{
    public ?string $name = null;
    public ?string $age = null;
    public ?string $address = null;
}

// From array
$dto = new SimpleDto([
    'name' => 'John Doe',
    'age' => '25',
    'address' => 'Some address'
]);

// From JSON
$dto = new SimpleDto(json_encode($data));

// Using setters
$dto = new SimpleDto()
    ->setName('John Doe')
    ->setAge('25')
    ->setAddress('Some address');

// Access via properties or getters
echo $dto->name;           // John Doe
echo $dto->getName();      // John Doe

if ($dto->hasName()) {
    echo "Name is set";
}

Magic Methods

DTOs automatically provide getters, setters, and checkers for all properties without explicit method definitions. You don't need to manually create getter/setter methods - they are generated automatically based on property names using camelCase convention.

Filling Data

$dto = new SimpleDto();
$dto->fill([
    'name' => 'John Doe',
    'age' => '25'
]);

// Overwrite specific fields
$dto->fill(['name' => 'Jane Doe']);
echo $dto->name; // Jane Doe

Merging DTOs

$dto1 = new SimpleDto()->setName('John Doe');
$dto2 = new SimpleDto()
    ->setAge('25')
    ->setAddress('Some address');

$dto3 = $dto1->merge($dto2);

// $dto3 now contains all fields from both DTOs
echo $dto3->getName();    // John Doe
echo $dto3->getAge();     // 25
echo $dto3->getAddress(); // Some address

Comparing DTOs

$dto1 = new SimpleDto([
    'name' => 'John Doe',
    'age' => '25',
    'address' => 'Some address'
]);

$dto2 = new SimpleDto(['name' => 'Jane Doe']);

$diff = $dto1->diff($dto2);

/*
[
    'name' => ['old' => 'John Doe', 'new' => 'Jane Doe'],
    'age' => ['old' => '25', 'new' => null],
    'address' => ['old' => 'Some address', 'new' => null]
]
*/

Cloning DTOs

$dto = new SimpleDto(['name' => 'John Doe']);
$clone = $dto->clone();

$diff = $dto->diff($clone); // Empty array - perfect copy

Serialization

$dto = new SimpleDto([
    'name' => 'John Doe',
    'age' => '25'
]);

// To array
$array = $dto->toArray();

// To JSON
$json = $dto->toJson();

Nested DTOs

use YourVendor\Dto\NestedDto;
use YourVendor\Dto\SimpleDto;

$dto = new NestedDto([
    'children' => new SimpleDto([
        'name' => 'John Doe',
        'age' => '25',
        'address' => 'Some address'
    ]),
    'companyAddress' => 'Some address',
    'companyName' => 'google'
]);

// Access nested properties
$children = $dto->getChildren(); // Returns SimpleDto instance
echo $children->getName(); // John Doe

// Serialize nested DTOs
$array = $dto->toArray();
/*
[
    'children' => [
        'name' => 'John Doe',
        'age' => '25',
        'address' => 'Some address'
    ],
    'companyAddress' => 'Some address',
    'companyName' => 'google'
]
*/

// Merge nested DTOs
$dto1 = new NestedDto($data);
$dto2 = new NestedDto();
$dto2->setChildren(
    (new SimpleDto())
        ->setName('Changed name')
        ->setAge(null)
);

$merged = $dto1->merge($dto2);
echo $merged->getChildren()->getName(); // Changed name

Validation

Validation is applied via PHP attributes on DTO properties.

Using Built-in Validators

use Forge\Dto\Support\BaseDto;
use Forge\Dto\Support\Validation\Uuid;
use Forge\Dto\Support\Validation\Required;
use Forge\Dto\Support\Validation\Inn;

final class UserDto extends BaseDto
{
    #[Uuid]
    public ?string $id = null;
    
    #[Required]
    public ?string $name = null;
    
    #[Inn]
    public ?string $inn = null;
}

// Validation happens automatically on property assignment
$dto = new UserDto();
$dto->setId('invalid-uuid'); // Throws DtoValidationException
$dto->setId('550e8400-e29b-41d4-a716-446655440000'); // ✅

$dto->setInn('627708638650'); // ✅ Valid individual INN
$dto->setInn('4404380820');   // ✅ Valid legal entity INN

Validation Errors

Get all validation errors that occurred during DTO population:

$dto = new UserDto();

try {
    $dto->setId('invalid-uuid');
} catch (DtoValidationException $e) {
    // Exception thrown
}

// Validation errors are used by default value generators
if (empty($dto->getValidationErrors())) {
    // Safe to generate defaults
}

Creating Custom Validators

Implement PropertyValidatorInterface to create your own validators:

use Attribute;
use Forge\Dto\Contracts\PropertyValidatorInterface;
use Forge\Dto\Support\Validation\Traits\HasLaravelValidation;

#[Attribute(Attribute::TARGET_PROPERTY)]
readonly class Email implements PropertyValidatorInterface
{
    use HasLaravelValidation;

    public function validate(mixed $value, string $propertyName): void
    {
        $this->performValidation(
            value: $value,
            rules: ['nullable', 'email'],
            field: $propertyName
        );
    }
}

// Usage
final class ContactDto extends BaseDto
{
    #[Email]
    public ?string $email = null;
}

Requirements for custom validators:

  • Must be a PHP 8 Attribute with Attribute::TARGET_PROPERTY
  • Must implement PropertyValidatorInterface
  • Throw DtoValidationException on validation failure

Default Value Generation

Generate default values for properties using attributes.

Creating Custom Generators

Implement DefaultValueGeneratorInterface to create your own generators:

use Attribute;
use Forge\Dto\Contracts\DefaultValueGeneratorInterface;
use Forge\Dto\Support\BaseDto;

#[Attribute(Attribute::TARGET_PROPERTY)]
class UuidGenerator implements DefaultValueGeneratorInterface
{
    public function generate(BaseDto $dto): mixed
    {
        // return the generated value for the property
    }

    public function supports(BaseDto $dto, string $propertyName): bool
    {
        // return true if generation should occur (e.g. property is empty and DTO has no errors)
    }
}

// Usage
final class EntityDto extends BaseDto
{
    #[UuidGenerator]
    public ?string $id = null;
}

Requirements for custom generators:

  • Must be a PHP 8 Attribute with Attribute::TARGET_PROPERTY
  • Must implement DefaultValueGeneratorInterface
  • Implement generate(BaseDto $dto): mixed - returns the generated value
  • Implement supports(BaseDto $dto, string $propertyName): bool - determines if generation should occur

Type Casting

The package automatically casts values to appropriate types:

final class UserDto extends BaseDto
{
    public ?string $age = null; // declared as string
}

$dto = new UserDto()->setAge(25); // passing int
echo gettype($dto->getAge()); // "string" — automatically cast to match property type

Casting Collections

An array property does not know what its items are. To cast each element into a DTO, declare the item type explicitly with the #[CastEachTo] attribute:

use Forge\Dto\Support\BaseDto;
use Forge\Dto\Support\Casting\CastEachTo;

final class OrderDto extends BaseDto
{
    #[CastEachTo(LineItemDto::class)]
    public ?array $items = null;
}

$order = new OrderDto([
    'items' => [
        ['sku' => 'A-1', 'qty' => 2],
        ['sku' => 'B-7', 'qty' => 1],
    ],
]);

$order->items[0]; // LineItemDto instance

Each raw array (or JSON object) is constructed into the given class; values that are already instances of that class are passed through unchanged. Casting is opt-in via this attribute only — docblock @var Item[] annotations are treated as documentation and never drive casting.

Note: #[CastEachTo] and #[ArrayOf] are not meant to be combined on the same property. CastEachTo constructs items from raw data; #[ArrayOf] validates a collection of already-built (e.g. polymorphic) objects without constructing them. In strict mode #[ArrayOf] runs before casting and will reject raw input — pick the one that matches your intent.

Masking

Masking redacts property values on serialization only — the stored DTO value is never changed. It is opt-in per call via the masking flag on toArray() / toJson().

A mask is a PHP attribute implementing PropertyMaskInterface:

use Attribute;
use Forge\Dto\Contracts\PropertyMaskInterface;

#[Attribute(Attribute::TARGET_PROPERTY)]
readonly class MaskCard implements PropertyMaskInterface
{
    public function apply(string $value): string
    {
        return str_repeat('*', max(0, strlen($value) - 4)) . substr($value, -4);
    }
}

final class PaymentDto extends BaseDto
{
    #[MaskCard]
    public ?string $cardNumber = null;
}

$dto = new PaymentDto(['cardNumber' => '4111111111111111']);

$dto->toArray();              // ['cardNumber' => '4111111111111111']
$dto->toArray(masking: true); // ['cardNumber' => '************1111']
echo $dto->cardNumber;        // '4111111111111111' — untouched

Requirements / behavior:

  • Must be a PHP 8 Attribute with Attribute::TARGET_PROPERTY implementing PropertyMaskInterface.
  • apply(string $value): string is called only for string values, and only when masking is enabled.
  • One mask per property — the first attribute wins.
  • Nested DTOs are masked recursively; masking never mutates the DTO, only its serialized output.

API Reference

Core Methods

  • fill(array $data): self - Fill DTO with data from array
  • merge(DtoInterface $dto): self - Merge another DTO into this one
  • diff(DtoInterface $dto): array - Get differences between DTOs
  • clone(): self - Create a deep copy of the DTO
  • toArray(): array - Convert DTO to array
  • toJson(): string - Convert DTO to JSON string
  • generateDefaultsIfAllowed(): void - Generate default values for null fields

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.