Install the Bundle
composer require antonchernik/dto-bundle
Register the bundle in config/bundles.php:
return [
// ...
AntonChernik\DtoBundle\AntonChernikDtoBundle::class => ['all' => true],
];
Configure AutoMapper+
Update config/packages/automapper_plus.yaml (or create it):
automapper_plus:
sources:
- '%kernel.project_dir%/config/automapper'
mapping_paths:
- '%kernel.project_dir%/config/automapper'
Define a DTO
Create a DTO class (e.g., src/Dto/UserDto.php):
namespace App\Dto;
class UserDto
{
public string $name;
public int $age;
}
Map an Entity to DTO
Create a mapping file (config/automapper/UserMapping.php):
use App\Entity\User;
use App\Dto\UserDto;
use AutoMapperPlus\AutoMapper\Configuration\MappingConfiguration;
return function (MappingConfiguration $config) {
$config->newMapping()
->forSource(User::class)
->forDestination(UserDto::class)
->constructBy('name', 'age');
};
Use in Controller
use App\Dto\UserDto;
use App\Entity\User;
use AutoMapperPlus\AutoMapper\AutoMapperInterface;
class UserController
{
public function __construct(private AutoMapperInterface $mapper) {}
public function show(User $user): UserDto
{
return $this->mapper->map(UserDto::class, $user);
}
}
DTO Creation
User entity to UserDto for JSON responses.Nested Mappings
User → UserDto with embedded AddressDto):
$config->newMapping()
->forSource(User::class)
->forDestination(UserDto::class)
->constructBy('name', 'age')
->forMember('address', fn($src) => $this->mapper->map(AddressDto::class, $src->address));
Conditional Logic
forMember with closures for dynamic transformations:
->forMember('fullName', fn($src) => $src->firstName . ' ' . $src->lastName)
Validation Integration
use Symfony\Component\Validator\Constraints as Assert;
class UserDto
{
#[Assert\NotBlank]
public string $name;
}
Service Layer Abstraction
// Controller
$dto = $this->mapper->map(UserDto::class, $request->all());
$this->userService->create($dto);
// Service
public function create(UserDto $dto): User
{
$user = new User();
$this->mapper->map($user, $dto);
$entityManager->persist($user);
}
Batch Processing
$users = $this->userRepository->findAll();
$dtos = $this->mapper->map(UserDto::class, $users, true); // true = batch mode
Circular References
User ↔ Address).->ignore() or custom logic:
->ignore('addresses') // Skip circular references
Mapping Paths
mapping_paths in automapper_plus.yaml will break mappings.Type Mismatches
string ↔ int) will throw errors.->forMember('age', fn($src) => (int) $src->age)
Caching Issues
php bin/console cache:clear
Overwriting Default Mappings
->override() to explicitly replace mappings.Enable AutoMapper+ Debugging
Add to config/packages/dev/automapper_plus.yaml:
automapper_plus:
debug: true
Check logs for mapping execution details.
Inspect Mappings Dump registered mappings:
$this->mapper->getConfiguration()->getMappings();
Use constructBy Carefully
constructBy requires exact property names. Typos will cause silent failures.Custom Value Resolvers
Extend AutoMapperPlus\AutoMapper\ValueResolver\ValueResolverInterface for complex logic:
class CustomResolver implements ValueResolverInterface
{
public function resolve($source, $destination, $propertyPath, MappingContext $context)
{
return strtoupper($source->name);
}
}
Register in a mapping:
->forMember('name', fn($src) => $this->mapper->resolve('CustomResolver', $src))
Event Listeners Use Symfony events to pre/post-process mappings:
// src/EventListener/MappingListener.php
public function onKernelRequest(GetResponseEvent $event)
{
$this->mapper->addValueResolver(new CustomResolver());
}
Custom Naming Conventions Override AutoMapper+'s default naming strategies (e.g., for snake_case ↔ camelCase):
automapper_plus:
naming_convention: snake_case
How can I help you explore Laravel packages today?