Installation
Add the bundle to your composer.json:
composer require 24hoursmedia/tesla-dto-bundle
Enable the bundle in config/bundles.php:
return [
// ...
Tesla\DtoBundle\TeslaDtoBundle::class => ['all' => true],
];
Define a DTO Class
Create a DTO class (e.g., src/Dto/UserDto.php) with annotations:
namespace App\Dto;
use Tesla\DtoBundle\Annotation\Dto;
use Tesla\DtoBundle\Annotation\DtoProperty;
#[Dto]
class UserDto
{
#[DtoProperty(source: 'id')]
public int $id;
#[DtoProperty(source: 'name', target: 'fullName')]
public string $fullName;
#[DtoProperty(source: 'email')]
public string $email;
}
Create an Assembler
Generate an assembler (e.g., src/Assembler/UserAssembler.php):
namespace App\Assembler;
use App\Dto\UserDto;
use App\Entity\User;
use Tesla\DtoBundle\Assembler\AssemblerInterface;
class UserAssembler implements AssemblerInterface
{
public function toDto(User $entity): UserDto
{
$dto = new UserDto();
$dto->id = $entity->getId();
$dto->fullName = $entity->getName();
$dto->email = $entity->getEmail();
return $dto;
}
public function toEntity(UserDto $dto): User
{
$entity = new User();
$entity->setId($dto->id);
$entity->setName($dto->fullName);
$entity->setEmail($dto->email);
return $entity;
}
}
Register the Assembler
Add the assembler to the container in config/services.yaml:
services:
App\Assembler\UserAssembler:
tags: ['tesla.dto.assembler']
Use the Assembler Inject the assembler and use it:
use App\Assembler\UserAssembler;
use App\Entity\User;
class UserController
{
public function __construct(private UserAssembler $assembler) {}
public function show(User $user)
{
$dto = $this->assembler->toDto($user);
return $this->json($dto);
}
}
Annotations Over Configuration
Prefer @DtoProperty annotations for mapping fields (e.g., source/target for renaming). Avoid manual property-by-property mapping where possible.
#[DtoProperty(source: 'created_at', target: 'createdAt')]
public DateTime $createdAt;
Nested DTOs Use nested DTOs for complex relationships:
#[Dto]
class OrderDto {
#[DtoProperty(source: 'customer')]
public CustomerDto $customer;
}
Collections
Map ArrayCollection or arrays to DTO arrays:
#[DtoProperty(source: 'orders')]
public array $orders;
Service-Based Assemblers For reusable logic, create service-based assemblers:
class OrderAssembler implements AssemblerInterface {
public function __construct(private OrderRepository $repo) {}
public function toDto(Order $order): OrderDto {
$dto = new OrderDto();
$dto->id = $order->getId();
$dto->customer = $this->customerAssembler->toDto($order->getCustomer());
return $dto;
}
}
Bulk Operations
Use AssemblerInterface in batch contexts (e.g., API responses):
$dtoList = array_map(
fn(User $user) => $this->assembler->toDto($user),
$this->userRepository->findAll()
);
Validation Integration Combine with Symfony Validator for DTO validation:
use Symfony\Component\Validator\Validator\ValidatorInterface;
class UserAssembler {
public function __construct(private ValidatorInterface $validator) {}
public function toDto(User $user): UserDto {
$dto = $this->mapEntityToDto($user);
$this->validator->validate($dto);
return $dto;
}
}
API Platform
Use DTOs with API Platform’s serialization_context:
# config/packages/api_platform.yaml
api_platform:
formats:
jsonld:
mime_types: ['application/ld+json']
serialization_context:
groups: ['dto']
Symfony Forms Bind DTOs to forms for type-safe input:
$form = $this->createForm(UserDtoType::class, $dto);
Doctrine Events
Assemble DTOs in prePersist/preUpdate for side effects:
$dto = $this->assembler->toDto($entity);
$this->eventDispatcher->dispatch(new DtoAssembledEvent($dto));
Circular References
Avoid infinite loops in nested DTOs/assemblers. Use lazy loading or @Ignore:
#[DtoProperty(source: 'author', ignore: true)]
public AuthorDto $author;
Annotation Caching Clear cache after adding new DTOs/assemblers:
php bin/console cache:clear
Assembler Registration
Forgetting to tag assemblers (tesla.dto.assembler) prevents autowiring. Verify with:
php bin/console debug:container App\Assembler\UserAssembler
Type Safety
Mismatched types (e.g., int vs string) in source/target cause runtime errors. Validate early:
if (!$entity->getId() instanceof int) {
throw new \InvalidArgumentException('ID must be an integer');
}
Enable Debug Mode
Set TESLA_DTO_DEBUG: true in .env to log mapping issues:
TESLA_DTO_DEBUG=true
Check Mapping Logs
Look for TeslaDtoBundle logs in var/log/dev.log:
[TeslaDtoBundle] Property 'email' not found in source entity.
Use var_dump()
Inspect intermediate DTOs:
$dto = $this->assembler->toDto($entity);
var_dump($dto); // Check for null/empty values
Custom Assembler Logic
Extend Tesla\DtoBundle\Assembler\AssemblerInterface for custom logic:
class CustomAssembler implements AssemblerInterface {
public function toDto($entity): Dto {
if ($entity instanceof SpecialEntity) {
return $this->handleSpecialCase($entity);
}
return $this->defaultMapper->toDto($entity);
}
}
Dynamic Property Mapping
Use Reflection for dynamic DTOs:
$reflection = new \ReflectionClass($dto);
foreach ($reflection->getProperties() as $property) {
$source = $property->getValue($dto);
// Custom logic here
}
Event Dispatching Trigger events during assembly:
$this->eventDispatcher->dispatch(
new DtoAssembledEvent($dto),
DtoAssembledEvent::class
);
Bundle Priority
Ensure TeslaDtoBundle loads after DoctrineBundle to avoid conflicts.
Environment-Specific Mappings
Use %kernel.environment% in annotations for environment-specific fields:
#[DtoProperty(source: 'debug_token', if: '%kernel.environment% == "dev"')]
public ?string $debugToken;
Performance For large datasets, cache assemblers:
services:
App\Assembler\UserAssembler:
tags: ['tesla.dto.assembler']
public: false
synthetic: true
How can I help you explore Laravel packages today?