Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Argonaut Dto Laravel Package

yorcreative/laravel-argonaut-dto

Laravel Argonaut DTO transforms arrays, objects, and collections into structured, immutable DTOs with deep nested casting, type-safe conversion, Laravel validation, attribute prioritization, and clean toArray/toJson serialization.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and orient (5 minutes)

composer require yorcreative/laravel-argonaut-dto

There is no config file to publish, no service provider to register, no migration. The package is two classes plus traits. Before writing a single DTO, open these two files in vendor/yorcreative/laravel-argonaut-dto/src/:

  1. ArgonautDTO.php — the constructor, setAttributes, the cast resolution logic, and how rules() is invoked.
  2. ArgonautAssembler.php — how assemble/fromArray/fromCollection resolve the to<ClassName>/from<ClassName> method via class_basename.

That reading is worth more than the README, because the whole package's behavior hinges on convention resolution you can't see from the docs alone.

First use case: normalize an external payload.

The highest-value first application is a boundary you don't control — a webhook body, a third-party API response, or a CSV import row. Define the shape you want, and let an assembler map the shape you got:

class CustomerDTO extends ArgonautDTO
{
    public string $name;
    public string $email;

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

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

class CustomerDTOAssembler extends ArgonautAssembler
{
    public static function toCustomerDTO(object $input): CustomerDTO
    {
        return new CustomerDTO([
            'name'  => $input->full_name ?? $input->name,
            'email' => $input->email_address,
        ]);
    }
}

$dto = CustomerDTOAssembler::assemble($payload, CustomerDTO::class);

If that throws, the payload was wrong — and it throws at the edge, not three service layers deep. That is the whole pitch.

Implementation Patterns

One DTO + one assembler per external source. Don't build a universal UserDTO consumed by every integration. Build StripeCustomerDTO, CsvCustomerDTO, and map each into your domain object. Assemblers are cheap; untangling a DTO that serves five callers is not. The naming convention (to<ClassName>) means the assembler method name is fixed by the DTO basename, so keeping DTOs distinct also keeps assembler methods unambiguous.

FormRequest → DTO → Action. Drop validation from the FormRequest into the DTO's rules() so the same guarantee holds whether input arrives via HTTP, a queued job, or a console command:

public function store(StoreOrderRequest $request)
{
    $dto = OrderDTOAssembler::assemble($request->validated(), OrderDTO::class);

    return new OrderResource($this->createOrder->handle($dto));
}

Your action's signature becomes handle(OrderDTO $order) — explicitly typed, no array $data bags, and trivially testable without a request.

Aggregate payloads with nested casts. The cast syntax is the part worth memorizing:

Need Cast
Scalar 'string', 'int', 'bool'
Single nested DTO UserDTO::class
Array of DTOs [ProductFeatureDTO::class]
Collection of DTOs Collection::class . ':' . ProductReviewDTO::class

For aggregates (order → lines → product), define one assembler method per node type — toOrderDTO, toOrderLineDTO, toProductDTO — and the recursion resolves itself as long as each method exists on the same assembler.

Nested assemblers when keys don't match your DTO. If a nested field arrives with a different internal shape (display_name vs name), a plain cast will fail or produce garbage. Add:

protected array $nestedAssemblers = [
    'user' => UserDTOAssembler::class,
];

The assembler runs before the cast, so raw input is preprocessed first. This is the correct tool for "third-party nested object → my DTO."

Use DI assemblers for normalization services. When mapping needs a formatter, a slugifier, or a repository lookup, make the assembler method non-static and let the container inject:

class UserAssembler extends ArgonautAssembler
{
    public function __construct(protected UserFormattingService $formatter) {}

    public function toUserDTO(object $input): UserDTO
    {
        return new UserDTO([
            'name'  => $this->formatter->formatName($input->first_name, $input->last_name),
            'email' => strtolower($input->email),
        ]);
    }
}

// bind in a provider, then:
$dto = resolve(UserAssembler::class)->assembleInstance($input, UserDTO::class);

The rule: static assembler methods get no DI, instance methods do. Pick per-assembler and stay consistent — mixing the two in one class works but produces confusing call sites (assemble(..., $instance) in some places, not others).

Serialize at the exit boundary. toArray() / toJson() give you a stable contract for API resources, cache payloads, and queued job arguments. Because the DTO enforces shape on entry, your serialized output is predictably shaped on exit — that symmetry is what makes DTOs worth the class count.

Test the assembler, not the framework. Your unit tests should assert $dto->email === '[email protected]' and that a missing required field throws. Mock the injected formatting service so the test is about mapping, not about Laravel's container.

Gotchas and Tips

The naming convention is magic and it will bite you. class_basename strips the namespace, so two DTOs named UserDTO in different namespaces (App\DTOs\Admin\UserDTO and App\DTOs\UserDTO) both map to toUserDTO. The assembler resolves by basename only — it cannot distinguish them. Keep DTO basenames globally unique, or you'll get silent mis-mapping rather than an error.

Nested assemblers must be static. The README is explicit: assemblers used in $nestedAssemblers are invoked statically, so they cannot receive container injection. If your nested mapping genuinely needs a service, assemble that node manually inside the parent to<Parent>DTO method and pass it through — don't wait for a stack trace to discover the limitation.

Validation is per-DTO, not recursive. rules() on ProductDTO validates reviews as collection, not each review's fields. Each nested DTO validates itself when constructed, so a bad review inside a valid product still throws — but the exception message won't tell you which index failed. For batch imports, catch and log the index in your loop; you'll otherwise be debugging blind.

ValidationException behaves differently outside HTTP. In a controller it's a clean 422. In a queued job or a console command it's an unhandled exception that fails the job. If DTOs are assembled outside the request lifecycle, wrap assembly in a try/catch and translate to your own domain exception.

Casts and typed properties are not redundant, and strict types matters. public int $qty with input "5" coerces in weak-typing files but throws in files declaring declare(strict_types=1). The 'int' cast makes conversion explicit and file-independent. Always declare both — the type documents the shape, the cast guarantees it.

Check null handling before relying on it. For public ?UserDTO $user = null with a UserDTO::class cast, confirm how a null input is treated (skip-cast vs. attempted cast) by reading the cast resolver in ArgonautDTO.php. This is exactly the edge case you want to unit-test yourself rather than assume.

Keep behavior out of DTOs. No save(), no repository calls, no business rules. The moment a DTO knows about persistence you lose the ability to pass it across a queue boundary and you've rebuilt a badly-behaved Eloquent model. Assemblers map, services do.

Watch the cost on large collections. fromCollection assembles item by item with cast resolution per field. For a 50k-row import this is measurably slower than a plain array_map. Use DTOs at the boundary and batch the persistence layer separately; don't DTO-transform every row in a tight loop just for symmetry.

Treat the package as small and pin it. 106 stars, zero dependents — you are effectively on the source of truth. Pin the minor version, and keep assemblers as your only touchpoint so swapping in hand-written factories (or spatie/laravel-data) stays an afternoon's work rather than a refactor. The assembler layer is your insurance policy; violating it to call new UserDTO(...) all over the codebase throws that away.

When not to use it: a single endpoint with a three-field form does not need a DTO, an assembler, and a validation class. Reach for it when the payload is nested, comes from a source you don't control, or crosses a boundary (queue, cache, API). Below that threshold, $request->validated() is the honest answer.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode