yorcreative/argonaut-dto

A framework-agnostic PHP DTO package that provides nested casting, recursive serialization, validation, assemblers, and immutable DTO support out of the box.

Maintainers

Package info

github.com/YorCreative/Argonaut-DTO

pkg:composer/yorcreative/argonaut-dto

Transparency log

Fund package maintenance!

YorCreative

Statistics

Installs: 111

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-30 17:58 UTC

This package is auto-updated.

Last update: 2026-07-30 18:22:08 UTC


README



Logo

Argonaut DTO

GitHub license GitHub stars GitHub Org's stars GitHub issues GitHub forks Packagist Downloads Tests Security

Framework-agnostic Data Transfer Objects for PHP 8.3+. Argonaut DTO provides the useful parts of Laravel Argonaut DTO without requiring Laravel: nested DTO and enum casting, recursive serialization, mutable and immutable DTOs, convention-based assemblers, and lightweight validation.

Requirements

  • PHP 8.3, 8.4, or 8.5

Installation

Install via Composer:

composer require yorcreative/argonaut-dto

Basic DTO

use YorCreative\ArgonautDTO\ArgonautDTO;

final class UserDTO extends ArgonautDTO
{
    public string $name;
    public string $email;

    protected array $casts = [
        'name' => 'string',
        'email' => 'string',
    ];

    public function rules(): array
    {
        return [
            'name' => ['required', 'string'],
            'email' => ['required', 'email'],
        ];
    }
}

$user = new UserDTO(['name' => 'Jane', 'email' => 'jane@example.com']);
$user->merge(['name' => 'Jane Doe']);

$user->toArray();
$user->toJson();
$user->isValid();

Unknown input keys are ignored. A setter named set<FieldName> takes precedence over direct property assignment. Declare $prioritizedAttributes when setters must run before the remaining input attributes.

Nested casts

use YorCreative\ArgonautDTO\ArgonautDTO;
use YorCreative\ArgonautDTO\Collection;

final class OrderDTO extends ArgonautDTO
{
    public array $items;
    public Collection $history;
    public ?UserDTO $customer = null;

    protected array $casts = [
        'items' => [OrderItemDTO::class],
        'history' => Collection::class.':'.OrderEventDTO::class,
        'customer' => UserDTO::class,
    ];
}

Array casts use [SomeDTO::class]. Collection casts use Collection::class . ':' . SomeDTO::class (the shorthand collection:SomeDTO is also supported). Arrays, traversables, and the package Collection can be used as input.

Backed enums and DateTimeInterface implementations can be cast directly:

protected array $casts = [
    'status' => OrderStatus::class,
    'createdAt' => DateTimeImmutable::class,
];

Nested DTOs and backed enums serialize recursively; enums serialize to their backing values.

Serialization depth

toArray(), toJson(), and jsonSerialize() walk the whole DTO graph:

$dto->toArray();          // full graph
$dto->toArray(2);         // two levels of nested DTOs
$dto->toJson(depth: 2);   // same limit, JSON encoded

$depth counts DTO nesting levels and defaults to ArgonautDTO::DEFAULT_MAX_DEPTH (512). Exceeding it throws a RuntimeException rather than silently emitting an empty array, so truncation can never be mistaken for missing data.

Circular references are detected directly, not inferred from the depth limit, and raise YorCreative\ArgonautDTO\CircularReferenceException (a RuntimeException) naming the instance involved. The same DTO appearing in two sibling branches is a shared reference rather than a cycle and serializes normally.

toJson() also accounts for PHP's own json_encode() depth limit. Because each array or collection of DTOs adds a level of its own, a graph well inside $depth can still exceed the encoder's 512 levels; toJson() raises the encoder limit to fit whatever the walk produced. Note that json_decode() has the same 512 default, so decoding very deep payloads needs an explicit depth.

Immutable DTOs

Declare DTO properties as readonly and extend ArgonautImmutableDTO:

final class UserSnapshotDTO extends ArgonautImmutableDTO
{
    public readonly string $id;
    public readonly string $name;
}

Readonly properties are initialized once during construction. Missing required properties remain uninitialized, allowing PHP's normal typed-property error to identify an incomplete snapshot.

Assemblers

Assemblers resolve to<ClassName> first, then from<ClassName>:

final class UserAssembler extends ArgonautAssembler
{
    public static function toUserDTO(object $input): UserDTO
    {
        return new UserDTO([
            'name' => $input->display_name,
            'email' => $input->email,
        ]);
    }
}

$user = UserAssembler::assemble($payload, UserDTO::class);
$users = UserAssembler::fromArray($payloads, UserDTO::class);

fromArray() and fromCollection() return the package's dependency-free Collection, which supports iteration, array access, count(), first(), all(), map(), and filter().

Instance assembler methods are supported through assembleInstance() or by passing an assembler instance to assemble().

Validation

rules() uses YorCreative DataValidation, including its complete rule set, nested paths, wildcards, and custom closures. Argonaut preserves the convenient int, bool, collection, and sometimes aliases: collections are arrays after serialization, and sometimes omits the rule set when the field is absent.

Custom closures use DataValidation's signature: function (string $field, mixed $value, callable $fail, array $data): bool.

$errors = $user->validate(throw: false);
// ['email' => ['The email field failed the email rule.']]

Calling validate() without throw: false raises YorCreative\ArgonautDTO\ValidationException.

isValid() returns false only for validation failure. A missing or broken rules() method is a programming error and surfaces as an exception rather than being reported as invalid data. Pass isValid(throw: true) to raise ValidationException instead of returning false.

Relationship to the Laravel package

yorcreative/argonaut-dto contains no Illuminate dependency. The Laravel package can provide Laravel-specific adapters and continue to expose its existing API while sharing this framework-neutral behavior. Laravel applications that need Laravel's validator or collection implementation can remain on yorcreative/laravel-argonaut-dto.

Testing

Run the test suite:

composer test

Run tests with coverage report:

composer coverage

Run static analysis (PHPStan):

composer phpstan

Run code style fixer (Pint):

composer lint

Continuous integration runs the suite on PHP 8.3, 8.4, and 8.5 against locked, lowest, and highest dependency sets, alongside PHPStan, Pint, Composer validation, dependency audits, and scheduled Composer security audits.

Credits

License

This package is open-sourced software licensed under the MIT license.