Usage Guide
March 25, 2026 · View on GitHub
Controller Integration
From Request Data
use App\Dto\UserDto;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
class UserController extends AbstractController
{
#[Route('/users', methods: ['POST'])]
public function store(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$dto = new UserDto($data);
return $this->json($dto->toArray());
}
#[Route('/users/{id}', methods: ['PATCH'])]
public function update(Request $request, int $id): JsonResponse
{
$data = json_decode($request->getContent(), true);
// Use ignoreMissing for partial updates
$dto = UserDto::createFromArray($data, ignoreMissing: true);
return $this->json($dto->toArray());
}
}
DTO Mapping Helpers
use App\Dto\UserDto;
use PhpCollective\SymfonyDto\Mapper\DtoMapper;
$dto = DtoMapper::fromArray(['name' => 'Mark'], UserDto::class);
$dtos = DtoMapper::fromIterable($rows, UserDto::class);
$collection = DtoMapper::fromCollection($doctrineCollection, UserDto::class);
// Generic pagination wrapper
$pagination = DtoMapper::fromPaginated(
items: $pageItems,
total: $total,
perPage: $perPage,
page: $page,
dtoClass: UserDto::class,
);
JSON Response Helper
use PhpCollective\SymfonyDto\Http\DtoJsonResponse;
return DtoJsonResponse::fromDto($dto);
// or
return DtoJsonResponse::fromCollection($dtos);
Automatic DTO Resolution
Enable the value resolver in config (enable_value_resolver: true) and use #[MapRequestDto]:
use App\Dto\UserDto;
use PhpCollective\SymfonyDto\Attribute\MapRequestDto;
use Symfony\Component\HttpFoundation\JsonResponse;
class UserController extends AbstractController
{
#[Route('/users', methods: ['POST'])]
public function store(#[MapRequestDto] UserDto $dto): JsonResponse
{
return $this->json($dto->toArray());
}
}
Choose a specific source if needed:
#[MapRequestDto(source: MapRequestDto::SOURCE_QUERY)]
From Doctrine Entities
#[Route('/users/{id}', methods: ['GET'])]
public function show(User $user): JsonResponse
{
// Convert entity to DTO
$dto = new UserDto([
'id' => $user->getId(),
'name' => $user->getName(),
'email' => $user->getEmail(),
]);
return $this->json($dto->toArray());
}
Collections
The PhpCollectiveDtoBundle automatically registers Doctrine's ArrayCollection as the collection type for DTO collection fields. No manual setup is needed.
Defining Collection Fields
In your DTO config, use the [] suffix to define collection fields:
<dto name="User">
<field name="id" type="int"/>
<field name="name" type="string"/>
<field name="roles" type="Role[]"/>
<field name="tags" type="string[]"/>
</dto>
After generating, collection fields will use Doctrine's ArrayCollection class:
$user = new UserDto([
'id' => 1,
'name' => 'John',
'roles' => [
['name' => 'admin', 'active' => true],
['name' => 'editor', 'active' => false],
],
'tags' => ['vip', 'premium'],
]);
// Doctrine Collection methods are available
$activeRoles = $user->getRoles()->filter(fn (RoleDto $role) => $role->getActive());
$firstRole = $user->getRoles()->first();
$tagCount = $user->getTags()->count();
What the Adapter Does
The bundle performs two registrations on boot:
- Runtime collection factory —
Dto::setCollectionFactory(fn (array $items) => new ArrayCollection($items))ensures that collection fields are hydrated asDoctrine\Common\Collections\ArrayCollectioninstances at runtime. - Code generation adapter —
CollectionAdapterRegistry::register(new DoctrineCollectionAdapter())ensures that generated DTO code usesnew ArrayCollection([])and->add()for collection initialization and appending.
Both are required: the factory handles runtime hydration from arrays, while the adapter controls the generated PHP code.
Validation
Use Symfony's Validator component:
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Constraints as Assert;
class UserController extends AbstractController
{
#[Route('/users', methods: ['POST'])]
public function store(Request $request, ValidatorInterface $validator): JsonResponse
{
$data = json_decode($request->getContent(), true);
$dto = new UserDto($data);
$violations = $validator->validate($dto->toArray(), new Assert\Collection([
'name' => [new Assert\NotBlank(), new Assert\Length(max: 255)],
'email' => [new Assert\NotBlank(), new Assert\Email()],
]));
if (count($violations) > 0) {
$errors = [];
foreach ($violations as $violation) {
$errors[$violation->getPropertyPath()] = $violation->getMessage();
}
return $this->json(['errors' => $errors], 422);
}
return $this->json($dto->toArray());
}
}
Validation Bridge
If your DTOs use the built-in validation rules from php-collective/dto (e.g. required, minLength, maxLength, min, max, pattern), you can automatically convert them to Symfony Validator constraints:
composer require symfony/validator
use PhpCollective\SymfonyDto\Validation\DtoConstraintBuilder;
use Symfony\Component\Validator\Validation;
$dto = new UserDto();
$constraint = DtoConstraintBuilder::fromDto($dto);
$validator = Validation::createValidator();
$violations = $validator->validate($data, $constraint);
if (count($violations) > 0) {
// Handle validation errors
}
The bridge maps DTO rules to Symfony constraints:
| DTO Rule | Symfony Constraint |
|---|---|
required | Assert\NotBlank (wrapped in Assert\Required) |
minLength / maxLength | Assert\Length(min:, max:) |
min / max | Assert\Range(min:, max:) |
pattern | Assert\Regex(pattern:) |
Optional fields are wrapped in Assert\Optional, required fields in Assert\Required.
Extra fields not defined in the DTO are allowed by default.
Service Layer Pattern
// src/Service/UserService.php
use App\Dto\UserDto;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
class UserService
{
public function __construct(
private EntityManagerInterface $em,
) {}
public function createUser(UserDto $dto): User
{
$user = new User();
$user->setName($dto->getName());
$user->setEmail($dto->getEmail());
$this->em->persist($user);
$this->em->flush();
return $user;
}
}
Doctrine Integration
Query Results to DTOs
// src/Repository/UserRepository.php
use App\Dto\UserSummaryDto;
class UserRepository extends ServiceEntityRepository
{
/**
* @return UserSummaryDto[]
*/
public function findActiveSummaries(): array
{
$rows = $this->createQueryBuilder('u')
->select('u.id', 'u.name', 'u.email')
->where('u.active = true')
->getQuery()
->getArrayResult();
return array_map(fn($row) => new UserSummaryDto($row), $rows);
}
}
Nested DTOs
When your DTO has nested DTO fields:
// config/dto.xml
<dto name="Order">
<field name="id" type="int"/>
<field name="customer" type="Customer"/>
<field name="items" type="OrderItem[]"/>
</dto>
// Usage
$order = new OrderDto([
'id' => 1,
'customer' => ['name' => 'John', 'email' => 'john@example.com'],
'items' => [
['product' => 'Widget', 'quantity' => 2],
['product' => 'Gadget', 'quantity' => 1],
],
]);
// Access nested data
$customerName = $order->getCustomer()->getName();
AutoMapper Bridge
For automatic object-to-DTO mapping without manual field-by-field code, install the optional jolicode/automapper integration:
composer require jolicode/automapper
Supports AutoMapper 8.x (PHP 8.2+) and 10.x (PHP 8.4+).
The bundle automatically registers the DtoAutoMapper service when AutoMapper is available.
Entity to DTO
use App\Dto\UserDto;
use App\Entity\User;
use PhpCollective\SymfonyDto\AutoMapper\DtoAutoMapperInterface;
class UserController extends AbstractController
{
public function __construct(
private DtoAutoMapperInterface $dtoMapper,
) {}
#[Route('/users/{id}', methods: ['GET'])]
public function show(User $user): JsonResponse
{
// Automatic mapping - no manual field assignment
$dto = $this->dtoMapper->toDto($user, UserDto::class);
return $this->json($dto->toArray());
}
#[Route('/users', methods: ['GET'])]
public function index(UserRepository $repository): JsonResponse
{
$users = $repository->findAll();
// Map entire collection
$dtos = $this->dtoMapper->toDtoCollection($users, UserDto::class);
return $this->json(array_map(fn($dto) => $dto->toArray(), $dtos));
}
}
DTO to Entity (Updates)
#[Route('/users/{id}', methods: ['PUT'])]
public function update(
#[MapRequestDto] UserDto $dto,
User $user,
EntityManagerInterface $em,
): JsonResponse {
// Update existing entity from DTO
$this->dtoMapper->fromDto($dto, $user);
$em->flush();
return $this->json($this->dtoMapper->toDto($user, UserDto::class)->toArray());
}
DTO to New Entity
#[Route('/users', methods: ['POST'])]
public function store(
#[MapRequestDto] UserDto $dto,
EntityManagerInterface $em,
): JsonResponse {
// Create new entity from DTO
$user = $this->dtoMapper->fromDtoToNew($dto, User::class);
$em->persist($user);
$em->flush();
return $this->json(
$this->dtoMapper->toDto($user, UserDto::class)->toArray(),
201,
);
}
Mapping Context
Pass additional context to control mapping behavior:
$dto = $this->dtoMapper->toDto($user, UserDto::class, [
'groups' => ['read'],
]);
Configuration
The AutoMapper bridge is enabled by default when the package is installed. To disable it:
# config/packages/php_collective_dto.yaml
php_collective_dto:
enable_automapper: false
When to Use Each Approach
| Scenario | Recommended Approach |
|---|---|
| Array data (API request, DB row) | DtoMapper::fromArray() or new UserDto($data) |
| Entity → DTO | $dtoMapper->toDto($entity, UserDto::class) |
| DTO → existing Entity | $dtoMapper->fromDto($dto, $entity) |
| DTO → new Entity | $dtoMapper->fromDtoToNew($dto, User::class) |
| Multiple entities → DTOs | $dtoMapper->toDtoCollection($entities, UserDto::class) |
The static DtoMapper class handles array-based hydration (the core strength of generated DTOs), while DtoAutoMapper handles object-to-object transformations using AutoMapper's code generation.
Further Reading
See the main php-collective/dto documentation for:
- DTO configuration options
- Type support
- Custom casters
- Advanced patterns