mark-gerarts/automapper-plus-bundle
Symfony bundle integrating AutoMapperPlus for fast, configurable object-to-object mapping. Provides service wiring, profiles, and easy mapping between DTOs, entities, and view models with type safety and custom transformations.
Install the Bundle
composer require mark-gerarts/automapper-plus-bundle
Ensure the bundle is enabled in config/bundles.php:
return [
MarkGerarts\AutoMapperPlusBundle\AutoMapperPlusBundle::class => ['all' => true],
];
Define a Mapping Profile
Create a YAML file (e.g., config/automapper/mappings/user_mapping.yaml):
MarkGerarts\AutoMapperPlusBundle\Mapping\Mapping:
source: App\Entity\User
destination: App\DTO\UserDTO
by_method: 'mapUserToUserDTO'
Or use annotations in your entity/DTO:
use AutoMapperPlus\Annotation as AMP;
#[AMP\Map(source: User::class, destination: UserDTO::class)]
class UserDTO {}
First Usage
Inject the AutoMapperInterface into a service/controller:
use AutoMapperPlus\AutoMapperInterface;
public function __construct(private AutoMapperInterface $mapper) {}
public function transformUser(User $user): UserDTO {
return $this->mapper->map($user, UserDTO::class);
}
Verify Configuration Run the bundle’s validation command:
php bin/console automapper:validate
Pattern: Map entities to DTOs for API responses or service boundaries.
// In a controller/service
$userDTO = $this->mapper->map($userEntity, UserDTO::class);
return $this->json($userDTO);
Best Practice: Use nested DTOs for complex objects:
# config/automapper/mappings/user_mapping.yaml
MarkGerarts\AutoMapperPlusBundle\Mapping\Mapping:
source: App\Entity\User
destination: App\DTO\UserDTO
nested:
- { source: address, destination: App\DTO\AddressDTO }
Pattern: Optimize performance for collections (e.g., paginated API responses).
$usersDTO = $this->mapper->map($usersEntities, UserDTO::class, true); // true = bulk
Tip: Use true for bulk operations to leverage AutoMapper+’s batch processing.
Pattern: Handle complex transformations with IValueResolver.
use AutoMapperPlus\ValueResolver\ValueResolverInterface;
class FullNameResolver implements ValueResolverInterface {
public function resolve($source, $destination, $propertyPath, $context) {
return $source->getFirstName() . ' ' . $source->getLastName();
}
}
Register in YAML:
MarkGerarts\AutoMapperPlusBundle\Mapping\Mapping:
source: App\Entity\User
destination: App\DTO\UserDTO
value_resolvers:
- App\Resolver\FullNameResolver
Pattern: Skip or transform properties based on conditions.
MarkGerarts\AutoMapperPlusBundle\Mapping\Mapping:
source: App\Entity\Order
destination: App\DTO\OrderDTO
ignore:
- sensitiveData
conditional:
- { property: discountApplied, condition: $source->getTotal() > 100 }
Pattern: Use mappings in Symfony services.
// src/Service/UserService.php
public function __construct(
private AutoMapperInterface $mapper,
private UserRepository $userRepo
) {}
public function getUserDTO(int $id): UserDTO {
$user = $this->userRepo->find($id);
return $this->mapper->map($user, UserDTO::class);
}
Tip: Autowire AutoMapperInterface directly—no manual configuration needed.
Use Case: Map to different DTOs based on runtime logic.
$destinationClass = $isAdmin ? AdminUserDTO::class : UserDTO::class;
$dto = $this->mapper->map($user, $destinationClass);
Use Case: Trigger mappings on Symfony events (e.g., kernel.request).
use Symfony\Component\HttpKernel\Event\ViewEvent;
public function onKernelView(ViewEvent $event) {
$data = $event->getControllerResult();
if ($data instanceof User) {
$event->setControllerResult($this->mapper->map($data, UserDTO::class));
}
}
Pattern: Unit test mappings with AutoMapperTestCase.
use AutoMapperPlus\Tests\AutoMapperTestCase;
class UserMappingTest extends AutoMapperTestCase {
public function testUserToUserDTO() {
$user = new User('John', 'Doe');
$dto = $this->mapper->map($user, UserDTO::class);
$this->assertEquals('John Doe', $dto->getFullName());
}
}
Circular References
User ↔ Address) causes infinite loops.ignore_circular_references: true in YAML or configure globally:
# config/packages/automapper_plus.yaml
automapper_plus:
ignore_circular_references: true
Property Name Mismatches
PropertyNotFoundException if property names differ (e.g., createdAt vs. created_at).property_mappings in YAML:
MarkGerarts\AutoMapperPlusBundle\Mapping\Mapping:
source: App\Entity\User
destination: App\DTO\UserDTO
property_mappings:
- { source: createdAt, destination: created_at }
Performance Overhead
automapper_plus:
cache_enabled: true
cache_dir: '%kernel.cache_dir%/automapper'
Configuration Overrides
priority in YAML to control precedence:
MarkGerarts\AutoMapperPlusBundle\Mapping\Mapping:
source: App\Entity\User
destination: App\DTO\UserDTO
priority: 100 # Higher = overrides lower-priority mappings
Doctrine Proxy Issues
LazyLoadingException for Doctrine proxies.$user->getAddress(); // Force-load proxy
$dto = $this->mapper->map($user, UserDTO::class);
Enable Debug Mode Set environment variable:
export AM_DEBUG=true
Or in config/packages/automapper_plus.yaml:
automapper_plus:
debug: true
Validate Mappings Run the validation command to catch misconfigurations early:
php bin/console automapper:validate
Log Mapping Steps
Use a custom IValueResolver to log transformations:
class DebugResolver implements ValueResolverInterface {
public function resolve($source, $destination, $propertyPath, $context) {
\Log::debug("Mapping $propertyPath: " . print_r($source, true));
return $source;
}
}
Check Cache Clear the cache if mappings behave unexpectedly:
php bin/console cache:clear
Custom Type Converters
Use Case: Convert between non-standard types (e.g., DateTime ↔ Carbon).
use AutoMapperPlus\TypeConverter\TypeConverterInterface;
class CarbonConverter implements TypeConverterInterface {
public function convert($source, $destinationType, $context) {
return Carbon::instance($source);
}
}
Register in YAML:
automapper_plus:
type_converters:
- App\Converter\CarbonConverter
Event Listeners Use Case: Trigger actions before/after mapping (e.g., sanitize data).
use AutoMapperPlus\Event\MappingEvent;
public function onPreMap(MappingEvent $event) {
if ($event->getSource() instanceof User) {
$event->setSource($this
How can I help you explore Laravel packages today?