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

Dto Laravel Package

cerbero/dto

Cerbero DTO is a lightweight Laravel/PHP package for creating type-safe data transfer objects from arrays and requests. It supports property casting, validation, nested DTOs, and easy serialization, helping keep application data structured and predictable.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require cerbero/dto

No service provider, no facade, no published config required — it's a plain library that plugs into the framework only through Illuminate\Support\ contracts (Arrayable, JsonSerializable, Jsonable). That means the install step really is the whole setup.

Where to look first

  1. The README sections Instantiate DTOs → Read/Write properties → Cast properties → Serialize. Read them in that order; everything else in the package is a variation on those four.
  2. Cerbero\Dto\DtoFlags — the flag constants are the real API surface. Skim the list before you design anything, because flags decide how hydration behaves (strict vs. lenient, partial vs. complete).
  3. The Casters/ directory in vendor/cerbero/dto/src. The built-in casters are the best documentation for writing your own.

Minimal first use case

The canonical win is replacing array-shuffling at the HTTP boundary.

namespace App\Data;

use Cerbero\Dto\Dto;

class AddressDto extends Dto
{
    public string $street;
    public string $city;
    public string $postcode;
}

class UserDto extends Dto
{
    public int $id;
    public string $name;
    public string $email;
    public AddressDto $address;      // nested DTO
    public array $roles = [];        // default value
}
// Controller
public function store(CreateUserRequest $request): JsonResponse
{
    $user = UserDto::make($request->validated());

    $this->createUser->handle($user);   // action receives a typed object

    return response()->json($user->toArray());
}

Once you've done that once, the pattern generalises: hydrate from anywhere untrusted (request, queue payload, HTTP client response, webhook), pass the DTO inward, serialise on the way out. Services and actions stop accepting array $data and start accepting UserDto $user, and static analysis starts earning its keep.


Implementation Patterns

1. Controller → Action boundary

The highest-value pattern. Keep FormRequest for validation (it's good at that) and use the DTO for shape.

$dto = OrderDto::make($request->validated());

// instead of: $this->service->place($request->validated());
$this->service->place($dto);

Services then declare place(OrderDto $order) and PHP enforces the contract. Bonus: the DTO is constructible in a test without touching HTTP.

Rule of thumb: use FormRequest for rules, DTOs for transport, Eloquent models for persistence. Don't collapse the DTO into the model — you'll immediately regret it when the API shape and the table shape diverge.

2. Nested DTOs and collections

Declare the nested type on the property and let the package hydrate recursively:

class CartDto extends Dto
{
    public UserDto $customer;
    public DtoCollection $items;     // collection of CartItemDto
}

For collections, either declare a DtoCollection and configure the item type, or cast explicitly:

protected function castItems(): DtoCollection
{
    return DtoCollection::make($this->raw('items'), CartItemDto::class);
}

This is where the package buys you the most over hand-rolled array_map — deeply nested JSON from a third-party API becomes a real object graph in one call.

3. Casting

Handle type coercion and value objects in one place rather than sprinkling (int) and Carbon::parse() across the codebase. Typical candidates:

  • scalars coming back as strings from JSON/DB
  • Carbon / CarbonImmutable dates
  • backed enums (Status::from(...))
  • value objects (Money, Email)
  • DtoCollection for arrays of children

Custom casters implement the package's caster contract and are registered per-property. Write one as soon as you have a cast used in three DTOs — a MoneyCaster beats a Money::fromCents((int) $this->amount) repeated everywhere.

4. Mapping foreign payloads

Integrations rarely speak your naming conventions. Map on the way in instead of leaking snake_case_from_vendor into your domain:

protected function map(): array
{
    return [
        'user_id'    => 'userId',
        'created_at' => 'createdAt',
        'line_items' => 'items',
    ];
}

This is the single cleanest place to absorb a webhook provider renaming a field — one line, one test.

5. Immutable vs. mutable

Default to immutable DTOs. Construct, hand off, never mutate. When you genuinely need staged mutation (builder-style assembly, form wizards), use the mutable base and explicitly freeze() at the end so downstream code can rely on it not changing.

Value objects (AddressDto, MoneyDto, CoordinatesDto) should always be frozen — they get passed around and cached, and a mutation bug there is miserable to find.

6. Partial DTOs for PATCH semantics

A full DTO cannot represent {"name": "new"} because every other property is "missing", which is not the same as null. Use the partial flag so the DTO tracks which properties were supplied:

$patch = UserPatchDto::make($request->validated(), DtoFlags::PARTIAL);

if ($patch->has('name')) { ... }

Wire this to your update action and skip array_key_exists gymnastics. This is the pattern that most repays the cost of adopting the package at all.

7. Lenient ingestion

For third-party payloads you don't control, the strict default (throw on unknown property) is a liability — a vendor adding a field breaks your consumer. Use DtoFlags::IGNORE_UNKNOWN_PROPERTIES on integration DTOs and keep strict mode on internal ones. Being able to make that choice per-DTO, rather than per-project, is the correct granularity.

8. Serialisation

DTOs are Arrayable/Jsonable, so return $dto; from a controller route works, and $dto->toArray() composes well inside JsonResource when you need envelope metadata. Prefer serialising the DTO over serialising the Eloquent model — you get a stable, greppable public shape and you lose the accidental $hidden leak class of bug.

9. Testing

DTOs are trivially testable and make excellent fixtures:

$order = OrderDto::make([
    'id' => 1,
    'total' => 1999,
    'items' => [['sku' => 'A', 'qty' => 2]],
]);

$this->assertSame(1999, $order->total);

For every DTO with casts or mappings, write one test asserting make($payload)->toArray() equals a literal array. That test is your contract with the integration and catches vendor drift the day it happens.

10. Layer discipline

Resist adding methods to DTOs beyond casts/maps. As soon as a DTO method hits the database or calls a service, it has become a half-model and you've reintroduced the coupling you adopted DTOs to escape. Compute derived fields in the consumer, or in a dedicated presenter.


Gotchas and Tips

Strict-by-default will surprise you. Hydrating a payload with extra keys throws by default. This is a feature on internal boundaries and a foot-gun on external ones. Decide the flag per DTO deliberately, not by copy-pasting the first example you find.

Missing ≠ null. Without the partial flag, an absent property and a null property are indistinguishable after hydration. Every PATCH endpoint that "mysteriously nulls out fields" is this bug. Reach for PARTIAL early.

Verify method names against the README. At 17 stars and a single maintainer, the package's public surface is not stabilised by mass adoption. The concepts (flags, casters, mapping, collections) are stable; exact helper names (raw(), has(), freeze(), DtoCollection::make()) are worth a five-minute check against the version you installed before writing 40 DTOs around them. Pin the version in composer.json and bump deliberately.

Magic property access hides typos from some tooling. $dto->nmae may fail at runtime rather than in your IDE or PHPStan run. Add the package's extension/stub if one exists, or declare real typed properties on each DTO so your analyser can see them. This alone prevents most "why is this null" afternoons.

Reflection cost in hot loops. Hydration and casting go through reflection and per-property resolution. For 10 DTOs per request it is irrelevant; for 50,000 rows in an import job it is not. In bulk paths, hydrate lazily or map to plain arrays and only build DTOs at the boundary where you actually need behaviour. Measure before optimising, but know where the cost lives.

Nested type resolution needs an explicit hint. If a nested property is typed as bare array or left untyped, you'll silently get an array of arrays instead of child DTOs, and the failure surfaces much later as a null-property error. Always declare the concrete DTO or collection type.

Deep graphs and recursion. A DTO that references a parent which references the DTO will blow up toArray(). Keep the graph acyclic — DTOs point down the tree, never back up. If you need both directions, hold an ID, not the object.

Caster/flag ordering. When a value is both mapped and cast, the mapping has to resolve before the cast runs. If a cast is receiving the wrong key, it's almost always a mapping bug — dump the post-map array before blaming the caster.

Debugging toolkit. In order: $dto->toArray() (what actually got in), $dto->has('x') (was it supplied at all), the flag value (strict/partial/ignore), then the caster. Nearly every issue is one of the first three.

Extension points worth knowing. Custom casters, the map() hook, and the flags argument are the three places to make the package behave the way your domain needs. Everything else is better handled by composition — wrap the package in your own App\Data\AbstractDto base class that sets your team's default flags, adds your common casters, and centralises serialisation policy. That one indirection means a future swap (or a maintenance stall on this package) is a one-file change instead of a repository-wide refactor.

Maintenance risk is real. One maintainer, 17 stars, zero dependents. Laravel's own spatie/laravel-data covers a superset of this ground with a much larger install base and far more Stack Overflow coverage. cerbero/dto is lighter and less opinionated, which is a legitimate reason to pick it — but treat it as a library you own: pin it, wrap it behind App\Data\AbstractDto, keep your own tests on the behaviours you rely on, and you'll be insulated if it goes quiet.

Alternatives, honestly. If you only need array → object with casts and nesting, the base class here is genuinely pleasant and small. If you need validation rules on DTOs, lazy/partial construction as a first-class concept, or Eloquent-aware magic, look at spatie/laravel-data before committing — the migration cost grows with every DTO you write.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package