metglobal/dto-bundle
A Symfony bundle for working with data transfer objects (DTOs), making it easier to convert request or array data into typed PHP objects instead of loose arrays. Helps keep controllers and services cleaner by centralizing data mapping and structure.
This is a small, archived Symfony bundle (2 stars, 0 dependents) that provides a minimal base for data transfer objects. Treat it as a reference implementation rather than a maintained dependency.
Install and register
composer require metglobal/dto-bundle
app/AppKernel.php or config/bundles.php.composer.json in the repo for the supported symfony/* and php constraints — this package predates PHP 8 and will likely need a polyfill or a fork to run on modern stacks.Where to look first
src/ — find the abstract/base DTO class. That is the entire contract you will extend.DependencyInjection/ extension — confirm whether it registers any services (normalizers, hydrators) or is essentially empty.tests/ — the fastest way to learn the intended hydration API is to read how the author instantiates DTOs from arrays.First use case
Replace raw arrays crossing a boundary with a typed object:
// Before
public function register(array $data) { ... }
// After
public function register(RegisterUserDTO $dto) { ... }
The controller (or a listener) hydrates the DTO from $request->request->all(), and every downstream service gets autocompletion, type safety, and a single place to add validation.
Hydrate at the edge, pass typed objects inward. The bundle's value is at the HTTP boundary. Do the array → DTO conversion once in the controller (or a kernel request listener / argument resolver), then never pass arrays deeper into the domain.
$dto = RegisterUserDTO::fromArray($request->request->all());
// or, depending on the API the package exposes:
$dto = new RegisterUserDTO();
$dto->hydrate($request->request->all());
DTO → Entity mapping belongs in a dedicated mapper. Keep the DTO free of Doctrine. A RegisterUserMapper (or a static toEntity() on the DTO if you accept the coupling) keeps persistence concerns out of the transfer object.
DTO → Response. Pair the bundle with the Symfony Serializer to emit JSON. If the DTO is a plain PHP object with public properties or getters, $serializer->serialize($dto, 'json') just works — you rarely need the bundle for this direction.
Use DTOs to define service contracts.
interface UserRegistrarInterface
{
public function register(RegisterUserDTO $dto): User;
}
This is where the pattern pays off day-to-day: refactors become IDE-driven, and controllers stop being the only place that "knows" the shape of request data.
Nested DTOs and collections. For request payloads with arrays of objects (e.g., order lines), hydrate each element into its own DTO in a loop. The bundle itself is unlikely to do recursive hydration — handle that in a small factory method on the parent DTO.
Validation at the DTO boundary. Run the Symfony Validator against the hydrated DTO before it reaches your service. Add constraints as annotations/attributes on DTO properties and let the controller throw a 422 on violation. This keeps validation rules colocated with the data shape.
Form requests as an alternative. If you are already using Symfony Forms, the bundle overlaps significantly with form data mappers. Pick one; do not mix both at the same boundary.
Archived status is the headline risk. No security patches, no PHP 8.x / Symfony 6+ compatibility work, zero dependents means zero community testing. Do a composer audit and read every file in src/ before adding it to a production app. For most teams the honest call is to copy the ~100 lines of base-class code into your own App\DTO namespace and drop the dependency.
Do not rely on undocumented magic. If the bundle does reflection-based hydration, it will silently ignore unknown keys and leave typed properties uninitialized. Always add a test that asserts a missing or misspelled field throws rather than producing an object with null/uninitialized state.
Strict types and coercion. Older hydration code often casts loosely — "0" becomes false, "1" becomes true, numeric strings become ints. If you need strict behavior, override the hydration method or add explicit setters with type checks.
Serialization pitfalls. DTOs with circular references (parent/child relationships) will blow up the Serializer. Use @Groups / serialization groups and serialize a response DTO, not your entity graph.
Debugging. dump() / var_dump() on a freshly hydrated DTO is the fastest way to confirm property mapping. If the bundle uses reflection, uninitialized typed properties will appear as uninitialized — that is your signal that the source array key was missing.
Extension points to lean on. The base DTO class is the intended extension point: subclass it, add fromArray/toArray helpers, and add validation. Avoid patching the vendor directory; if you need behavior changes, fork the package into your own namespace so upgrades (there won't be any) are not a concern.
Compare against modern alternatives before committing. symfony/serializer with constructor argument name mapping, Symfony 6.3+'s #[MapRequestPayload], symfony/validator on DTOs, or spatie/laravel-data (if you ever leave Symfony) each cover this ground with active maintenance. Adopt this bundle only if you specifically need its historical API or are maintaining a legacy app that already depends on it.
Pin the version and document the fork. If you do adopt it, pin an exact version in composer.json and leave a note in the README about the archived status so the next developer knows why the dependency exists.
How can I help you explore Laravel packages today?