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 Data Object Tools Laravel Package

juststeveking/laravel-data-object-tools

Handy tools and helpers for working with data objects in Laravel. Simplifies creating, validating, transforming, and casting DTOs, helping you keep application data flow clean, type-safe, and consistent across your codebase.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm what you actually got. On a Laravel 11 app:

composer require juststeveking/laravel-data-object-tools:^2.0

Before anything else, close the one open question in this package's documentation: read the installed composer.json at vendor/juststeveking/laravel-data-object-tools/composer.json and record the php and illuminate/* constraints. The 2.0.0 changelog says only "Support for Laravel 11", which almost certainly means Laravel 9/10 and possibly older PHP were dropped. Do not trust the release note as the compatibility statement — the constraint block is the source of truth. On apps that can't take 2.0.0, pin ^1.1.0 explicitly so the upgrade fails loudly rather than drifting.

Find the generator before writing a class by hand. The entire value proposition is scaffolding; if you hand-roll your first DTO you'll never adopt the convention. Run:

php artisan list | grep -i data

and read the package's stubs (vendor/juststeveking/laravel-data-object-tools/stubs/ or similar) before generating. This tells you three things at once: the command signature, the default namespace generated classes land in, and — most importantly — whether generated classes extend the package's base class or stand alone. That single fact determines your blast radius if upstream goes quiet: stand-alone classes make abandonment a composer remove plus a template copy; inheriting classes mean a future major can invalidate every DTO in your repo.

Also check for namespace collisions if any team already uses spatie/laravel-data. Two Data suffixes in one codebase with different hydration semantics is the most common way this adoption goes wrong. Pick one per app, deliberately, and write it down.

First use case: one write-path DTO. Don't start with the domain model. Convert a single validated request into a typed action argument:

// app/Data/CreateUserData.php  (generated, then trimmed)
final class CreateUserData
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly ?string $referrerCode = null,
    ) {}
}
// app/Http/Controllers/UsersController.php
public function store(StoreUserRequest $request, CreateUserAction $action)
{
    $user = $action->execute(CreateUserData::fromArray($request->validated()));

    return new UserResource($user);
}

Note the boundaries: the FormRequest still validates, the API Resource still serialises, and the DTO only carries typed data across one seam. If your first DTO does anything else, you've misread the package's role.

Once that compiles and one test passes, generate a second DTO and diff it against the first. That's your cheapest breakage detector after the version bump, and it takes thirty seconds.

Implementation Patterns

Validate → hydrate → act. The load-bearing pattern is a three-step pipeline where each step has exactly one owner:

$data = CreateOrderData::fromArray($request->validated());  // FormRequest owns validation
$order = $createOrder->execute($data);                      // Action owns behaviour
return new OrderResource($order);                           // Resource owns serialisation

Hydrate only from $request->validated(), never from $request->all(). The whole point of a typed constructor is that unvalidated input can't reach it; passing all() silently reopens the hole the DTO was meant to close.

Nest for real payloads, flatten for shallow ones. Line items, addresses and metadata are where arrays hurt most and DTOs earn their keep:

final class CreateOrderData
{
    /** @param list<OrderLineData> $lines */
    public function __construct(
        public readonly string $customerId,
        public readonly AddressData $shippingAddress,
        public readonly array $lines,
    ) {}

    public static function fromArray(array $payload): self
    {
        return new self(
            customerId: $payload['customer_id'],
            shippingAddress: AddressData::fromArray($payload['shipping_address']),
            lines: array_map(
                fn (array $line) => OrderLineData::fromArray($line),
                $payload['lines'],
            ),
        );
    }
}

If the base class provides fromArray/toArray, it will handle flat payloads but will not automatically recurse into your nested DTOs — write the fromArray override explicitly and type the array as list<OrderLineData> in the docblock so static analysis catches a missed mapping. Keep nested hydration one level per method; deeply recursive magic is where this kind of toolkit becomes unreadable.

Keep persistence explicit. Do not let a toModel() shorthand hide business rules. Where a DTO maps to an Eloquent model, prefer an explicit mapping in the action or a named toAttributes() you can read in a code review:

User::create($data->toAttributes());
// not: User::create($data->toArray())  -- field names drift, columns break silently

If the package's mapping helper produces array keys that don't match your column names, don't patch the helper — pass an explicit array. A DTO is a boundary object, not an ORM.

Typed fixtures in tests. This is the highest-ROI, lowest-risk adoption path and the one to push first across a team:

$data = new CreateUserData('Ada', '[email protected]');
$action->execute($data);

instead of $action->execute(['name' => 'Ada', 'email' => ...]). Raw fixture arrays don't fail when a required field is renamed; constructor calls do, at the test that constructs them, which is exactly where you want the failure.

Convert opportunistically, not in a big bang. Pick the seams with the most traffic (create/update actions, third-party API responses, queue job payloads), convert those, and leave the long tail alone. A codebase with four well-chosen DTOs is in better shape than one with forty mechanical ones — and the forty will be the ones nobody dares change.

Queue jobs. DTOs serialise fine if they're plain readonly value objects, but if you're passing one to a job, verify it round-trips through the queue driver's serialiser in a test before shipping. Custom base-class state or closures will not survive it.

Gotchas and Tips

The version bump is the biggest real risk, and it's cheap to close. Before rolling out across more than one app: read the constraints, run composer why-not juststeveking/laravel-data-object-tools 2.0.0 per app to surface transitive blockers early, bump, regenerate one representative DTO, and diff it against an existing one. That's the whole verification plan and it takes an afternoon. Doing it after a framework upgrade fails is the expensive version.

Check inheritance before you generate fifty classes. If generated DTOs extend a package base class, a future major version is a repo-wide refactor, not a Composer line change. If they stand alone, upstream abandonment costs you a template. Decide which world you're in on day one, and if it's the inheriting one, keep your own fromArray/toArray conventions thin so swapping the parent later is mechanical.

Config publishing is usually a mistake. The service provider may offer vendor:publish for the config. Resist it unless you need to override the default namespace or stub path — a published config file is a file you maintain forever, for a package that runs nothing at runtime. If you do publish, add a comment saying why, because in eighteen months nobody will know.

Stale or hand-edited stubs silently fork your convention. If you publish and edit the stub, every DTO generated before that edit is now off-convention, and nothing tells you. Add a CI check — a golden-file diff of one freshly generated DTO, or a lint rule asserting readonly on promoted properties — so drift is caught mechanically rather than by review vigilance. A convention that cannot be checked will be ignored once more than two teams are involved.

Don't overload it. It is not a validation engine (FormRequests), not a serialisation layer (API Resources), and not an Eloquent cast replacement (Eloquent). The characteristic failure is a DTO slowly acquiring validation rules, formatting helpers, and a toJson(), at which point you've built a worse spatie/laravel-data without the community. If you find yourself wanting casts or rule inference, that's the signal to switch packages rather than extend this one.

Debugging hydration failures. fromArray on a strict constructor throws on missing keys with a message that names the parameter but not the payload field — the mapping from snake_case array keys to camelCase constructor args is where you'll lose time. When a hydration error appears, dump the input array's keys next to the constructor signature before touching anything else. If you hit this more than twice in a week, add explicit named-argument mapping in every fromArray override rather than relying on the base class's guess.

Abandonment posture. Single maintainer, 66 stars, zero dependents, last release September 2024. None of that blocks adoption — there's no runtime, on-call, or data surface — but it means you own the convention, not the vendor. Keep the DTO convention documented in your repo, not linked from the package's README, and assume you could fork the base class if upstream goes quiet. The documentation you write is worth more than the package is.

Splitting majors across apps is how dialects start. With more than one or two apps involved, upgrade the framework estate first and move to 2.0.0 everywhere in one pass. A code-generation tool at two major versions in one estate produces two DTO shapes that reviewers will approve without noticing, and reconciling that costs more than the sequencing discipline.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo