gpalyan / dto-forge
Advanced DTO system with casting, validation, masks, nested DTOs
Requires
- php: ^8.4
- ext-ctype: *
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- laravel/pint: ^1.24
- orchestra/testbench: ^v10.0
- pestphp/pest: ^3.7
- phpstan/phpstan: 2.1.46
- symfony/var-dumper: ^7.2.0
This package is auto-updated.
Last update: 2026-05-31 12:42:41 UTC
README
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
- Installation
- Basic Usage
- Nested DTOs
- Validation
- Default Value Generation
- Type Casting
- Masking
- API Reference
- Contributing
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
DtoValidationExceptionon 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.CastEachToconstructs 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_PROPERTYimplementingPropertyMaskInterface. apply(string $value): stringis called only for string values, and only whenmaskingis 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 arraymerge(DtoInterface $dto): self- Merge another DTO into this onediff(DtoInterface $dto): array- Get differences between DTOsclone(): self- Create a deep copy of the DTOtoArray(): array- Convert DTO to arraytoJson(): string- Convert DTO to JSON stringgenerateDefaultsIfAllowed(): void- Generate default values for null fields
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.