fresns/dto
Fresns DTO provides a simple, typed data transfer object layer for Laravel/PHP applications. Define structured payloads, transform and validate data, and serialize cleanly to arrays or JSON for APIs, requests, and service boundaries.
composer require fresns/dto. Check composer.json first: the package targets modern PHP (8.1+ for enums/readonly/attribute support). On a legacy app, confirm the platform constraint before installing.src/Dto.php (the base class — hydration, all(), only(), except(), toArray()/toJson()), src/Attributes/ (the constraint vocabulary you'll actually annotate with), src/Collections/ (DtoCollection), then tests/ for canonical usage the docs may lag behind.use Fresns\Dto\Dto;
class CreatePostDto extends Dto
{
public string $title;
public ?string $content = null;
public array $tagIds = [];
}
// controller
$dto = CreatePostDto::from($request->all());
$post = app(CreatePost::class)->execute($dto);
The win on day one isn't validation (FormRequests already do that) — it's that the argument passed into your Action has a name, a type, and an IDE-completable shape instead of array $data.
$request->all() past that line. This is the single highest-leverage pattern; it makes Actions testable without HTTP.Post::create($dto->toArray()) works when names align, but prefer an explicit mapping method ($dto->toModel() or a PostData::fromModel() inverse) as soon as column names diverge. Implicit toArray() → create() breaks silently when a property is renamed.CreatePostDto and PostResourceDto have different rules and different lifetimes; sharing one class is where DTO layers rot.array gets you an array of arrays, not an array of DTOs — decide deliberately which you want at each level.map/filter returning typed objects instead of degrading to arrays halfway through a pipeline.only()/except() on the way out is the clean way to hide internal fields from an API response; don't add unset() calls at the call site.toArray(). This is much faster than feature-testing through HTTP just to check a payload shape.app/DTOs/Input and app/DTOs/Output, one class per boundary crossing. Reviewers should be able to tell from the namespace which direction data flows.::from() or new. If you want injection, bind them in a controller method and let the container build from the request — but be aware you're then relying on container resolution rather than explicit hydration, which is harder to trace when it fails.^1.3 in composer.json, and read the compare diff (not just the release title) on every bump. The 1.3.3 changelog is a bare compare link with no written notes, so the diff is the only source of truth.public string $foo; with no default and no attribute marking it optional will throw the moment it's read after hydration from a payload that omitted it. Either give a default, mark it nullable, or ensure it's required at the boundary — but decide per property, not per class."12" landing in an int property, or "false" in a bool, will not always fail loudly. Use declare(strict_types=1) and validate the transport layer as well; don't treat DTO typing as input validation.$request->all(); if the FormRequest enforces them, hydrate from $request->validated() and keep the DTO permissive. Mixing both produces two different error formats for the same bad input, and the one the user sees is whichever runs second.toArray() actually emits for a DateTimeInterface or a backed enum before it reaches a JSON response. This is the most common source of "the API shape changed after a refactor" surprises; pin it with an assertion in a test.resolve() calls inside toArray(). The moment a DTO has side effects, it can't be constructed in a unit test and you've lost the main benefit.src/Attributes, and overriding toArray() on your own base DTO for project-wide normalisation (e.g. snake_case keys, null-stripping). Prefer one project-level base class over per-DTO overrides.config/dto.php existst in your install after vendor:publish, treat it as advisory and confirm in src/ that the key is actually read before relying on it.^1.3, bump deliberately, and let your DTO toArray() assertions catch any serialization drift — that test suite is what makes patch releases safe to take.How can I help you explore Laravel packages today?