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

Data Transfer Objects Laravel Package

tenantcloud/data-transfer-objects

A lightweight PHP package for defining typed data transfer objects, making it easy to move structured data between layers of your Laravel application with clear, validated, and reusable DTO classes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the surface area first

composer require tenantcloud/data-transfer-objects

Before writing a single DTO, spend ten minutes reading vendor/tenantcloud/data-transfer-objects/src/ and the package's test suite. With 4 stars and zero dependents, this is a small maintained fork (the API closely mirrors the archived spatie/data-transfer-object lineage), so upstream blog posts and docs are approximately right but not authoritative. Check the base class for:

  • the static constructor name (from(), fromArray(), make())
  • the accessor for the raw array (all(), toArray())
  • which attributes exist (#[Cast], #[MapFrom], #[ArrayOf], or docblock-only casting)
  • the exception type thrown on a missing/invalid property

Pin the version explicitly ("tenantcloud/data-transfer-objects": "1.*") — a fork moving under you is the main risk here.

First use case: request payload → DTO → action

final class CreateInvoiceData extends DataTransferObject
{
    public string $number;
    public int $customerId;
    public ?string $note;
    /** @var InvoiceLineData[] */
    public array $lines;
}
// In a controller (or a FormRequest::toDto())
$data = CreateInvoiceData::from($request->validated());

$invoice = app(CreateInvoiceAction::class)->execute($data);

This is the highest-value pattern: it gives you a typed seam between HTTP (untyped arrays) and your domain, and it makes the action signature self-documenting.

Implementation Patterns

Keep DTOs dumb. The package gives you construction/casting, not behaviour. Resist adding save(), calculate(), or Eloquent calls inside a DTO — a DTO that touches the DB becomes untestable and stops being a transfer object.

Validation lives in the FormRequest, not the DTO. Use $request->validated() and let the DTO assume shape correctness. If the package exposes a validate() helper, treat it as a secondary net for DTOs built from non-HTTP sources (queues, imports, console commands), not the primary gate.

Nested DTOs and collections are the feature you'll lean on daily. The docblock annotation is load-bearing — without it, nested arrays stay as plain arrays:

/** @var InvoiceLineData[] */
public array $lines;

For strongly-typed collections, check whether the package ships a collection base class (DataTransferObjectCollection in the Spatie lineage). If it does, extending it gives you Iterator, ArrayAccess, and toArray() recursion for free.

DTO as an action/service return type. Symmetry pays off: actions accept DTOs and return DTOs, and your HTTP layer does the final serialisation.

final class InvoiceResource extends JsonResource
{
    public function toArray($request): array
    {
        return InvoiceData::from($this->resource->toArray())->toArray();
    }
}

Be careful doing this for large collections — see gotchas.

Hydrating from Eloquent. Model::toArray() is the cheapest bridge, but it drags along created_at, updated_at, and any appended accessors. Either curate the array first ($model->only([...])) or add #[MapFrom]-style attributes if the package supports them. Curating is boring and reliably correct.

Use DTOs at queue boundaries. Serialising a DTO into a job constructor and reconstructing it in handle() means payload changes break loudly at deserialisation instead of silently returning null three layers deep. This is one of the better payoffs for a package like this.

Extension point: a project base class. Put App\Support\Data\DataTransferObject extending the package class and hang shared concerns off it (snake_case→camelCase mapping, date casting, a toJson() helper). Every DTO extends yours, not theirs — this is your escape hatch if the fork stalls and you need to swap the implementation later.

Gotchas and Tips

Missing keys throw; they don't default silently. If a payload can omit a field, declare it ?string $note = null and verify the package respects defaults over "key must exist." In the Spatie lineage, a non-nullable typed property with no default throws on missing input — which is usually what you want, but it will surprise you the first time an API client drops an optional field.

Type coercion is loose by default. A numeric string "42" will often satisfy int $customerId. Don't rely on the DTO for strict input validation; that's the FormRequest's job. Conversely, if you need strictness, declare(strict_types=1) in your DTO file does not always propagate through reflection-based assignment — test it.

Deep nesting is recursive and untracked. toArray() on a DTO with 500 nested line items runs the full reflection/casting path at every level. For large collections, map to plain arrays once at the edge rather than converting DTO→array→DTO→array repeatedly.

Dates and enums are the usual breakage points. Check explicitly how the package handles DateTimeInterface, Carbon, and backed enums. If enum casting isn't supported, you'll silently get a string where you expected an enum instance — write one test per special type on day one and pin that behaviour.

Reflection caching. Most packages in this family memoise property metadata per class. If you're constructing thousands of DTOs in a loop (importers, report generation), benchmark before assuming it's free; if there's a cache-clear/warm command, wire it into deploy.

Debugging: inspect the raw input. When casting fails, the error usually names the property but not the offending value. Catch the package's exception, Log::debug() the original array alongside it, and re-throw. Do this once in your base DTO — it will save hours.

Fork-specific risk mitigation. Zero dependents means the ecosystem won't catch regressions for you. Concretely: pin the version, keep a small test suite that exercises your DTOs against a fixed fixture payload, and read the changelog before bumping. Because your DTOs extend a project base class, replacing the package later is a one-file change rather than a codebase-wide search-and-replace.

Don't use DTOs for your API response shape by default. If your responses are stable and simple, arrays or resources are fine. Reach for a DTO on the inbound side first — that's where type safety actually prevents bugs.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
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