Installation:
composer require boshurik/mapper-bundle
Register the bundle in config/bundles.php (Symfony 5+ auto-discovers it).
First Use Case:
Define a simple mapping between two entities (e.g., User and UserDto).
Create a mapping class implementing MappingInterface:
namespace App\Mapper;
use BoShurik\MapperBundle\Mapper\Mapping\MappingInterface;
use App\Entity\User;
use App\Dto\UserDto;
class UserToUserDtoMapper implements MappingInterface
{
public function map(User $source): UserDto
{
return new UserDto($source->getId(), $source->getName());
}
}
Register the Mapper:
Bind the mapper in a service (e.g., services.yaml):
services:
App\Mapper\UserToUserDtoMapper:
tags: ['mapper.mapping']
Autowire and Use: Inject the mapper into a service/controller:
use App\Mapper\UserToUserDtoMapper;
class UserController
{
public function __construct(private UserToUserDtoMapper $mapper) {}
public function show(User $user): UserDto
{
return $this->mapper->map($user);
}
}
One-Way Mappings:
Use MappingInterface for unidirectional transformations (e.g., Entity → DTO).
Example:
$dto = $mapper->map($entity);
Two-Way Mappings:
Implement ReverseMappingInterface for bidirectional transformations (e.g., DTO ↔ Entity).
Example:
class UserDtoToUserMapper implements ReverseMappingInterface
{
public function map(UserDto $source): User
{
$user = new User();
$user->setName($source->getName());
return $user;
}
public function reverseMap(User $source): UserDto
{
return new UserDto($source->getId(), $source->getName());
}
}
Dependency Injection:
Tag mappers with mapper.mapping or mapper.reverse_mapping to auto-register them.
Access via:
$this->container->get('mapper.mapping.user_to_dto');
Batch Processing: Loop through collections:
$dtos = array_map(
fn ($entity) => $mapper->map($entity),
$entities
);
Conditional Logic:
Use if checks or strategy patterns inside map() for dynamic transformations.
Symfony Forms:
Combine with Symfony\Component\Form for DTO-to-Entity binding:
$form = $this->createForm(UserDtoType::class, $dto);
if ($form->isSubmitted() && $form->isValid()) {
$entity = $reverseMapper->map($dto);
$entityManager->persist($entity);
}
API Resources:
Use mappers in ApiPlatform serializers or Symfony Serializer for consistent responses:
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
class UserDtoNormalizer implements ContextAwareNormalizerInterface
{
public function normalize($object, $format = null, array $context = [])
{
return $this->mapper->map($object);
}
}
Event Listeners:
Trigger mappings on entity events (e.g., prePersist):
class UserListener
{
public function __construct(private UserToUserDtoMapper $mapper) {}
public function prePersist(User $user)
{
$dto = $this->mapper->map($user);
// Log or validate DTO
}
}
Custom Generators: Extend the CLI generator to scaffold mappers with default logic:
bin/console make:mapper User UserDto
Circular Dependencies:
Avoid bidirectional mappings between the same classes (e.g., A ↔ B where A and B reference each other).
Solution: Use DTOs as intermediaries or lazy-load properties.
Performance with Large Collections:
Mapping collections eagerly (e.g., array_map) can be memory-intensive.
Solution: Use generators or chunk processing:
$generator = function () use ($entities, $mapper) {
foreach ($entities as $entity) {
yield $mapper->map($entity);
}
};
Type Safety: The package lacks runtime type validation. Ensure return types match expectations. Solution: Use PHP 8 attributes or runtime assertions:
assert($dto instanceof UserDto);
Generator Dependencies:
Forgetting to install nikic/php-parser will break CLI generation:
composer require nikic/php-parser
Service Overrides: Custom mapper services must be properly tagged or manually bound to avoid conflicts.
Mapper Not Found:
Verify the service is tagged (mapper.mapping) and the bundle is loaded.
Debug: Run bin/console debug:container | grep mapper to check registration.
Generation Errors:
Ensure generated files are writable and class names are unique.
Debug: Check var/log/dev.log for PHP-Parser exceptions.
Reverse Mapping Issues:
Test reverseMap() separately to isolate bidirectional logic:
$entity = $reverseMapper->map($dto);
$dtoRoundTrip = $reverseMapper->reverseMap($entity);
assert($dto->equals($dtoRoundTrip));
Custom Mappers: Extend the base interfaces for domain-specific logic:
interface CustomMappingInterface extends MappingInterface
{
public function mapWithContext($source, array $context);
}
Generator Templates:
Override the Twig templates for code generation in templates/mapper/.
Example: Add default imports or annotations.
Event Dispatching:
Trigger events during mapping (e.g., MapperEvents::PRE_MAP):
use BoShurik\MapperBundle\Event\MapperEvents;
$dispatcher->dispatch(new PreMapEvent($source, $mapper));
Caching: Cache generated mappers or results for performance:
$cache = new FilesystemCache();
$cachedMapper = $cache->get('mapper.user_to_dto', fn() => $mapper);
Testing: Mock mappers in tests to isolate logic:
$this->mockBuilder(UserToUserDtoMapper::class)
->shouldAllowMocking()
->getMock()
->method('map')
->willReturn(new UserDto(1, 'Test'));
How can I help you explore Laravel packages today?