yorcreative/laravel-argonaut-dto
Lightweight, composable Laravel DTO package to transform arrays/objects/collections into typed, validated data objects. Supports deep nested casting, type-safe conversion, Laravel Validator rules, explicit attribute priority, clean toArray/toJson serialization, and immutable readonly DTOs.
Laravel Argonaut DTO is a lightweight, highly composable package for transforming arrays, objects, or collections into structured DTOs (Data Transfer Objects), with built-in support for:
toArray, toJson)Install via Composer:
composer require yorcreative/laravel-argonaut-dto
DTOs extend ArgonautDTO, and define your expected structure via public properties, casting rules, and validation.
class UserDTO extends ArgonautDTO
{
public string $username;
public string $email;
protected array $casts = [
'username' => 'string',
'email' => 'string',
];
public function rules(): array
{
return [
'username' => ['required', 'string'],
'email' => ['required', 'email'],
];
}
}
This defines a strongly typed DTO with both validation rules and simple type casting.
Assemblers are responsible for mapping raw inputs (arrays or objects) into your DTOs.
// static usage example
class UserDTOAssembler extends ArgonautAssembler
{
public static function toUserDTO(object $input): UserDTO
{
return new UserDTO([
'username' => $input->display_name,
'email' => $input->email,
]);
}
}
// instance usage example
class UserDTOAssembler extends ArgonautAssembler
{
public function __construct(protected UserFormattingService $formattingService)
{
//
}
public function toUserDTO(object $input): UserDTO
{
return new UserDTO([
'username' => $this->formattingService->userName($input->display_name),
'email' => $this->formattingService->email($input->email),
]);
}
}
Assembler method names must follow the format
to<ClassName>orfrom<ClassName>, and are resolved automatically usingclass_basename.
Use the assembler to transform raw data into structured, casted DTO instances.
// static usage example
$dto = UserDTOAssembler::assemble([
'display_name' => 'jdoe',
'email' => 'jdoe@example.com',
], UserDTO::class);
// instance usage example
$dto = $userDTOAssemblerInstance->assembleInstance([
'display_name' => 'jdoe',
'email' => 'jdoe@example.com',
], UserDTO::class);
You can also batch transform arrays or collections:
// static usage
UserDTOAssembler::fromArray($userArray, UserDTO::class);
UserDTOAssembler::fromCollection($userCollection, UserDTO::class);
// instance usage
UserDTOAssembler::fromArray($userArray, UserDTO::class, $userDTOAssemblerInstance);
UserDTOAssembler::fromCollection($userCollection, UserDTO::class, $userDTOAssemblerInstance);
// or using the assembler instance's static methods
$userDTOAssemblerInstance::fromArray($userArray, UserDTO::class, $userDTOAssemblerInstance);
$userDTOAssemblerInstance::fromCollection($userCollection, UserDTO::class, $userDTOAssemblerInstance);
This example demonstrates nested relationships and complex type casting in action.
class ProductDTO extends ArgonautDTO
{
public string $title;
public array $features;
public Collection $reviews;
public ?UserDTO $user = null;
protected array $casts = [
'features' => [ProductFeatureDTO::class],
'reviews' => Collection::class . ':' . ProductReviewDTO::class,
'user' => UserDTO::class,
];
public function rules(): array
{
return [
'title' => ['required', 'string'],
'reviews' => ['sometimes', 'required', 'collection', 'min:1'],
];
}
}
class ProductDTOAssembler extends ArgonautAssembler
{
public static function toProductDTO(object $input): ProductDTO
{
return new ProductDTO([
'title' => $input->product_name,
'user' => $input->user,
'features' => $input->features ?? [],
'reviews' => $input->reviews ?? [],
]);
}
public static function toProductFeatureDTO(object $input): ProductFeatureDTO
{
return new ProductFeatureDTO([
'name' => $input->name ?? 'Unnamed Feature',
'description' => $input->description ?? null,
]);
}
public static function toProductReviewDTO(object $input): ProductReviewDTO
{
return new ProductReviewDTO([
'rating' => (int) ($input->rating ?? 0),
'comment' => $input->comment ?? '',
]);
}
}
ArgonautAssembler offers enhanced flexibility for your Assembler logic by supporting dependency injection. This allows you to leverage services or custom logic, whether defined in static or non-static methods, during the DTO assembly process. This is particularly powerful when integrating with Laravel's service container.
This feature enables you to:
ArgonautAssembler supports dependency injection in non-static transformation methods (e.g., toUserDTO or
fromUserDTO) by leveraging Laravelβs service container. When you call ArgonautAssembler::assemble(),
fromCollection(), fromArray(), or assembleInstance() with an instance of the assembler, the transformation method
is invoked on that instance. Laravelβs container automatically resolves and injects any dependencies declared in the
methodβs signature.
public static function toUserDTO($input)) do not support
dependency injection, as they are called statically without an instance.public function toUserDTO($input)) are called on an
assembler instance, allowing Laravel to inject dependencies into the method.Below is an example of an assembler with a non-static transformation method that uses dependency injection to format a userβs name via an injected service.
<?php
namespace App\Assemblers;
use App\DTOs\UserDTO;
use App\Services\UserFormattingService;
use YorCreative\LaravelArgonautDTO\ArgonautAssembler;
class UserAssembler extends ArgonautAssembler
{
public function __construct(protected UserFormattingService $formattingService)
{
//
}
/**
* Transform input data into a UserDTO with dependency injection.
*
* @param object $input Input data (e.g., from a model or array cast to object).
* @param UserFormattingService $formatter Injected service for formatting user data.
* @return UserDTO
*/
public function toUserDTO(object $input): UserDTO
{
return new UserDTO([
'full_name' => $this->formattingService->formatName($input->first_name, $input->last_name),
'email' => $input->email,
'created_at' => $input->created_at,
]);
}
}
// ServiceProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class YourServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(FormattingServiceInterface::class, function ($app) {
return new FormattingService();
});
$this->app->bind(YourArgonautAssembler::class, function ($app) {
return new YourArgonautAssembler($app->get(FormattingServiceInterface::class));
});
}
public function provides()
{
return [
YourArgonautAssembler::class,
FormattingServiceInterface::class,
];
}
}
To use the assembler with dependency injection, you need to provide an instance of the assembler to the assemble
method or related methods (fromCollection, fromArray, or assembleInstance). Laravelβs container will resolve the
dependencies when the method is invoked.
<?php
use App\Assemblers\UserAssembler;
use App\DTOs\UserDTO;
// Example input (e.g., a model or object)
$input = (object) [
'first_name' => 'John',
'last_name' => 'Doe',
'email' => 'john.doe@example.com',
'created_at' => now(),
];
// Creating an assembler instance
$formattingService = new UserFormattingService();
$assembler = new UserAssembler($formattingService);
// or using the container instance
$assembler = resolve(YourArgonautAssembler::class);
// Pass the $assembler instance
$userDTO = UserAssembler::assemble($input, UserDTO::class, $assembler);
// Or use the instance method
$userDTO = $assembler->assembleInstance($input, UserDTO::class);
// Transform a collection passing the $assembler instance
$array = [$input, $input];
$collection = collect($array);
$userDTOs = UserAssembler::fromCollection($collection, UserDTO::class, $assembler);
$userDTOs = $assembler::fromArray($array, UserDTO::class, $assembler);
In this example:
toUserDTO method requires a UserFormattingService dependency.$assembler) is passed to assemble, fromArray or fromCollection, ensuring the
non-static toUserDTO method is invoked on the instance.Nested assemblers enhance the casting process by allowing you to specify an assembler class for individual fields in your DTO. When a value is assigned to that field during construction or attribute setting, the raw input is first passed through the assembler's transformation method (e.g., toUserDTO) before the cast is applied. This is ideal for handling complex, nested data structures where raw inputs need preprocessing or mapping.
Nested assemblers integrate seamlessly with casting:
UserDTO::class), the assembler transforms the input value directly.[ProductFeatureDTO::class] or Collection::class . ':' . ProductReviewDTO::class), the assembler is applied to each item in the iterable value.Add a protected array $nestedAssemblers property to your DTO, mapping field names to assembler classes:
class ProductDTO extends ArgonautDTO
{
public string $title;
public array $features;
public Collection $reviews;
public ?UserDTO $user = null;
protected array $casts = [
'features' => [ProductFeatureDTO::class],
'reviews' => Collection::class . ':' . ProductReviewDTO::class,
'user' => UserDTO::class,
];
protected array $nestedAssemblers = [
'user' => UserDTOAssembler::class
];
public function rules(): array
{
return [
'title' => ['required', 'string'],
'reviews' => ['sometimes', 'required', 'collection', 'min:1'],
];
}
}
In this example:
user field will use UserDTOAssembler::toUserDTO() (or fromUserDTO()) to transform raw input (e.g., an array or object with display_name and email) before casting it to a UserDTO instance.features or reviews would apply the assembler to each item in the array/collection.When setting attributes (via constructor or setAttributes):
$casts (e.g., into a DTO, collection, etc.).This ensures deep, automatic transformations while maintaining type safety and structure.
With the above ProductDTO and a raw input:
$rawProduct = [
'title' => 'Standing Desk',
'user' => ['display_name' => 'jdoe', 'email' => 'jdoe@example.com'],
'features' => [['name' => 'Height Adjustable']],
'reviews' => [['rating' => 5, 'comment' => 'Great!']],
];
$productDTO = ProductDTOAssembler::assemble($rawProduct, ProductDTO::class);
// $productDTO->user is now a fully assembled UserDTO instance
$this->assertInstanceOf(UserDTO::class, $productDTO->user);
$this->assertSame('jdoe', $productDTO->user->username);
Nested assemblers promote composability, making it easier to handle multi-layered data in APIs, services, or complex domain logic.
For scenarios where data integrity is critical, ArgonautImmutableDTO provides a base class for creating DTOs with readonly properties that cannot be modified after construction.
use YorCreative\LaravelArgonautDTO\ArgonautImmutableDTO;
use Illuminate\Support\Carbon;
class UserDTO extends ArgonautImmutableDTO
{
public readonly string $username;
public readonly string $email;
public readonly ?string $firstName;
public readonly ?string $lastName;
public readonly ?Carbon $registeredAt;
protected array $casts = [
'registeredAt' => Carbon::class,
];
public function rules(): array
{
return [
'username' => ['required', 'string', 'max:64'],
'email' => ['required', 'email', 'max:255'],
];
}
}
// Create an immutable DTO - works exactly like ArgonautDTO
$user = new UserDTO([
'username' => 'jdoe',
'email' => 'jdoe@example.com',
'firstName' => 'John',
'lastName' => 'Doe',
'registeredAt' => '2024-01-15 10:30:00',
]);
// Access properties normally
echo $user->username; // 'jdoe'
echo $user->registeredAt; // Carbon instance
// Serialization works the same
$array = $user->toArray();
$json = $user->toJson();
// Validation works the same
$user->validate();
$user->isValid();
// Attempting to modify throws an Error
$user->username = 'other'; // β Error: Cannot modify readonly property
Immutable DTOs work seamlessly with assemblers:
class UserDTOAssembler extends ArgonautAssembler
{
public static function toUserDTO(object $input): UserDTO
{
return new UserDTO([
'username' => $input->display_name,
'email' => $input->email,
'firstName' => $input->first_name ?? null,
'lastName' => $input->last_name ?? null,
]);
}
}
// Works exactly the same as mutable DTOs
$user = UserDTOAssembler::assemble($input, UserDTO::class);
$users = UserDTOAssembler::fromArray($userArray, UserDTO::class);
$users = UserDTOAssembler::fromCollection($userCollection, UserDTO::class);
Immutable DTOs support all casting features including nested DTOs:
class OrderDTO extends ArgonautImmutableDTO
{
public readonly string $orderId;
public readonly UserDTO $customer; // Single nested DTO
public readonly array $items; // Array of DTOs
public readonly Collection $payments; // Collection of DTOs
protected array $casts = [
'customer' => UserDTO::class,
'items' => [OrderItemDTO::class],
'payments' => Collection::class . ':' . PaymentDTO::class,
];
}
| Feature | ArgonautDTO |
ArgonautImmutableDTO |
|---|---|---|
| Property modification | β Allowed | β Blocked (readonly) |
| Custom setters | β
setPropertyName() |
β Not supported |
setAttributes() / merge() |
β Available | β Not available |
| Casting (DTOs, enums, dates) | β Full support | β Full support |
| Validation | β Full support | β Full support |
Serialization (toArray, toJson, only, except) |
β Full support | β Full support |
| Assembler integration | β Full support | β Full support |
| Nested assemblers | β Full support | β Full support |
| PHP requirement | 8.2+ | 8.2+ (readonly properties) |
Note: Choose
ArgonautDTOwhen you need mutable objects with custom setters. ChooseArgonautImmutableDTOwhen you want guaranteed immutability and don't need post-construction modifications.
ArgonautDTO allows you to prioritize the assignment of specific fields using $prioritizedAttributes, which is critical
for cases where one field influences others.
class UserDTO extends ArgonautDTO
{
public ?string $firstName = null;
public ?string $lastName = null;
public string $username;
public string $email;
public ?string $fullName = null;
protected array $prioritizedAttributes = ['firstName', 'lastName'];
protected array $casts = [
'firstName' => 'string',
'lastName' => 'string',
'username' => 'string',
'email' => 'string',
'fullName' => 'string',
];
public function setFirstName($value)
{
$this->firstName = $value;
$this->fullName = $this->firstName . ' ' . $this->lastName;
}
public function setLastName($value)
{
$this->lastName = $value;
$this->fullName = $this->firstName . ' ' . $this->lastName;
}
public function rules(): array
{
return [
'firstName' => ['nullable', 'string', 'max:32'],
'lastName' => ['nullable', 'string', 'max:32'],
'username' => ['required', 'string', 'max:64'],
'email' => ['required', 'email', 'max:255'],
];
}
}
Casting allows you to automatically transform values into other DTOs, Laravel Collections, arrays, dates, enums, and more.
protected array $casts = [
'registeredAt' => \Illuminate\Support\Carbon::class,
'profile' => ProfileDTO::class,
'roles' => [RoleDTO::class],
'permissions' => Collection::class . ':' . PermissionDTO::class,
'status' => StatusEnum::class,
'tags' => [TagEnum::class],
'priorities' => Collection::class . ':' . PriorityEnum::class,
];
| Cast Type | Example | Description |
|---|---|---|
| Single DTO | ProfileDTO::class |
Cast an array to a DTO instance |
| Array of DTOs | [RoleDTO::class] |
Cast to array of DTOs |
| Collection of DTOs | Collection::class . ':' . CommentDTO::class |
Cast to a Laravel Collection |
| Date casting | Carbon::class |
Cast to Carbon/DateTime instance |
| BackedEnum | StatusEnum::class |
Cast a raw value to a BackedEnum |
| Array of Enums | [TagEnum::class] |
Cast to array of BackedEnum instances |
| Collection of Enums | Collection::class . ':' . PriorityEnum::class |
Cast to a Collection of BackedEnums |
PHP BackedEnum types are automatically detected and cast using Enum::from(). Existing enum instances are passed through unchanged. On serialization, enums are converted back to their backing value.
use App\Enums\StatusEnum; // enum StatusEnum: string { case Active = 'active'; ... }
class TaskDTO extends ArgonautDTO
{
public ?StatusEnum $status = null;
protected array $casts = [
'status' => StatusEnum::class,
];
}
$task = new TaskDTO(['status' => 'active']);
$task->status; // StatusEnum::Active
$task->toArray()['status']; // 'active'
Validate DTOs with Laravelβs validator:
$userDTO->validate(); // Throws ValidationException
$userDTO->validate(false); // Returns array of errors (non-throwing)
$userDTO->isValid(); // Returns true/false
Serialize DTOs for output, API responses, etc.
$userDTO->toArray(); // Recursively converts nested DTOs
$userDTO->toJson(); // JSON output (throws on encoding errors)
Use only() and except() to serialize a subset of properties:
$user = new UserDTO([
'username' => 'jdoe',
'email' => 'jdoe@example.com',
'firstName' => 'John',
'lastName' => 'Doe',
]);
$user->only('username', 'email');
// ['username' => 'jdoe', 'email' => 'jdoe@example.com']
$user->except('email');
// ['username' => 'jdoe', 'firstName' => 'John', 'lastName' => 'Doe', ...]
Mutable DTOs support merging additional attributes after construction:
$user = new UserDTO(['email' => 'old@example.com', 'firstName' => 'John']);
$user->merge(['email' => 'new@example.com']);
$user->email; // 'new@example.com'
$user->firstName; // 'John' (unchanged)
merge() returns the same instance, so it can be chained:
$user->merge(['firstName' => 'Jane'])->merge(['lastName' => 'Doe']);
Note:
merge()is only available onArgonautDTO. Immutable DTOs do not support post-construction modification.
Under the hood, both ArgonautDTO and ArgonautImmutableDTO compose shared behavior from three traits:
| Trait | Provides |
|---|---|
HasCasting |
castInputValue(), enum/DTO/collection/date casting |
HasSerialization |
toArray(), toJson(), only(), except(), collection() |
HasValidation |
validate(), isValid() |
This is transparent to most users, but if you are extending internal behavior (e.g., overriding castInputValue() in a subclass), note that these methods now live in traits rather than directly on the base class. Method resolution is identical for inheritance purposes.
Create DTO collections directly:
UserDTO::collection([
['username' => 'john', 'email' => 'john@example.com'],
]);
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
This package is open-sourced software licensed under the MIT license.
How can I help you explore Laravel packages today?