Readonly data containers with typed factory methods (`fromArray`, `fromModel`, `fromCollection`, `fromRequest`) used to pass structured data between application layers — especially for external API responses, Eloquent models, and service boundaries. Use this skill whenever creating, reviewing, or refactoring DTOs, Data Transfer Objects, value objects for inter-layer communication, or mapping payloads from APIs, models, or collections into typed PHP objects. Also trigger when the user mentions spatie/laravel-data alternatives, data mapping, or payload normalization in a Laravel context.
Installation
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Readonly data containers with typed factory methods (`fromArray`, `fromModel`, `fromCollection`, `fromRequest`) used to pass structured data between application layers — especially for external API responses, Eloquent models, and service boundaries. Use this skill whenever creating, reviewing, or refactoring DTOs, Data Transfer Objects, value objects for inter-layer communication, or mapping payloads from APIs, models, or collections into typed PHP objects. Also trigger when the user mentions spatie/laravel-data alternatives, data mapping, or payload normalization in a Laravel context.
compatible_agents
["architect","implement","refactor","review"]
DTO (Data Transfer Object)
When to Apply
Use when mapping external API payloads into typed objects before service logic runs.
Use when passing structured data between Actions, Services, Jobs, and Resources.
Use when hydrating typed objects from Eloquent models, collections, or paginators.
Use when providers return multiple key formats for the same field (ID, id, customer_id, customerId).
Use when a Form Request, Job, or Event needs a well-defined payload contract.
Do not use for single scalar wrappers, data used in only one private method, or when spatie/laravel-data is already installed and covers the use case (see Alternatives at the end).
Preconditions
The payload shape is known from API docs, Eloquent model attributes, fixtures, or tests.
The owning module exists (example: app/Services/Billing/DataObjects/).
Validation rules are defined in the caller (Form Request, Action, or Service), not the DTO.
Process
1. Create the DTO Class
Declare a readonly class with promoted, explicitly typed constructor properties.
Place it in the owning feature/module DataObjects/ directory.
Import Illuminate\Support\Arr at the top of the file.
2. Implement Factory Methods
Add one or more public static factory methods depending on the data sources the DTO serves. Every factory method that reads from an associative array must use Laravel's Arr::get() helper (or the data_get() global) instead of raw $data['key'] bracket access. This provides safe default handling and dot-notation support for nested payloads.
fromArray(array $data): static
The primary factory for raw associative arrays (API responses, decoded JSON, validated request data).
fromModel(Model $model): static
Accepts an Eloquent model and reads attributes via $model->getAttribute() or $model->{property}. Use when the DTO is frequently hydrated from a database record.
fromRequest(FormRequest $request): static
Reads from a validated Form Request. Prefer $request->validated() to ensure only validated fields are passed, then delegate to fromArray().
fromCollection(Collection $collection): Collection(collection of DTOs)
Returns a Collection (or typed array) of DTO instances. Use $collection->map(...) internally.
Not every DTO needs all four factories — add only those that match real call-sites. At minimum, provide fromArray().
3. Use Laravel Array Helpers Everywhere
Inside factory methods, never access array values with raw bracket syntax ($data['key'] ?? null). Instead:
useIlluminate\Support\Arr;
// Good — safe access with defaultArr::get($data, 'customer.name', 'Unknown');
// Good — global helper, supports dot-notation and wildcardsdata_get($data, 'customer.name', 'Unknown');
// Bad — raw bracket access, no dot-notation, verbose fallback chains$data['customer']['name'] ?? $data['Customer']['Name'] ?? 'Unknown';
When normalizing multiple key variants for the same field, combine Arr::get() calls with a ?? chain on the results — not on raw brackets:
Validate required/format rules before calling any factory method.
The DTO's job is mapping and type-safe access — not enforcing business rules.
5. Write a Dedicated DTO Test
Every DTO must have its own test class. DTO tests are pure unit tests — no HTTP calls and no database. They can run without booting the framework, but may extend a lightweight bootstrapped base test case (e.g. Tests\\TestCase via Testbench) when DTO factories depend on Eloquent models. They verify that every factory method correctly maps, normalizes, and type-casts input data.
Place test files alongside the DTO's module path: tests/Unit/Services/Billing/DataObjects/CustomerDataTest.php.
A DTO test class should cover:
Happy path — standard payload maps to correct property values.
Key variant normalization — each known alternate key (ID vs id vs customer_id) resolves correctly.
Nullable / optional fields — missing keys result in null (not exceptions).
Type casting — string IDs, float totals, bool flags are cast correctly.
Nested data — dot-notation fields and nested DTOs hydrate properly.
fromModel — model attributes (including casts and accessors) map correctly.
fromCollection — returns a collection of the correct DTO type and count.
Edge cases — empty arrays, missing keys, extra keys are handled gracefully.
6. Verify Call Sites
Replace ad-hoc array indexing in services/jobs with DTO property access.
Where a DTO is created from a model, ensure eager-loaded relationships are available before mapping.
// Validation belongs to the caller; the DTO maps only.$validated = $request->validated();
$customer = CustomerData::fromArray($validated);
echo$customer->name;
// From an Eloquent model$orderData = OrderData::fromModel(Order::findOrFail($id));
// From a collection / paginator$orders = OrderData::fromCollection(Order::where('status', 'shipped')->get());
Nested DTO access with data_get
When the source payload has deeply nested data, prefer data_get() for dot-notation traversal:
DTO is readonly and all properties are explicitly typed.
Array access inside factory methods uses Arr::get() or data_get() — no raw bracket access.
fromArray() handles all known key variants for the source payload.
Additional factory methods (fromModel, fromCollection, fromRequest) exist where needed.
Validation and business rules live outside the DTO (Form Request, Action, or Service).
File is stored in the owning module's DataObjects/ directory.
Nested DTOs are hydrated via their own factory methods, not inline array parsing.
A dedicated test class exists covering happy path, key variants, nullables, type casting, and edge cases.
fromModel tests use forceFill() on unsaved model instances — no database required.
fromCollection tests assert correct count, instance type, and empty-collection handling.
Anti-Patterns
Adding setters or mutable state to a DTO.
Putting business logic, DB queries, or HTTP calls inside a DTO.
Using DTOs as Eloquent model replacements (use models for persistence, DTOs for transport).
Accessing arrays with raw bracket syntax ($data['key']) instead of Arr::get() / data_get().
Assuming a single naming convention (e.g. only customer_id, ignoring customerId or CustomerID).
Leaving properties untyped or using mixed without narrowing.
Creating a single "god DTO" for create, update, and read — split into separate DTOs per context when validation rules diverge significantly.
Duplicating model attribute logic inside the DTO — call $model->getAttribute() and let Eloquent handle casts and accessors.
Skipping DTO tests because "it's just a data class" — factory methods contain mapping logic that breaks silently when API payloads change.
Alternatives
spatie/laravel-data
If the project already uses (or is open to) spatie/laravel-data, consider extending Spatie\LaravelData\Data or Spatie\LaravelData\Dto instead of writing a plain readonly class. The package provides:
Automatic creation from arrays, models, requests, and collections via ::from() and ::collect().
Built-in validation, casts, transformers, and TypeScript generation.
Property name mapping with #[MapInputName] for snake_case ↔ camelCase.
Eloquent casting support for storing data objects in JSON columns.
Use a manual readonly DTO when you need zero dependencies, full control over mapping logic, or when working with external API payloads that require heavy key normalization.