boshurik/mapper
Lightweight Laravel/PHP object mapper for converting between arrays and DTOs/entities. Helps map input data to typed objects and back with minimal boilerplate, supporting custom mapping rules and nested structures for clean data transformations.
Installation
composer require boshurik/mapper
Add the service provider to config/app.php under providers:
Boshurik\Mapper\MapperServiceProvider::class,
Basic Mapping Define a mapper class for your entities/DTOs:
use Boshurik\Mapper\Mapper;
class UserMapper extends Mapper
{
public function map($entity, $dtoClass)
{
$dto = new $dtoClass();
$dto->name = $entity->name;
$dto->email = $entity->email;
return $dto;
}
}
First Use Case Register the mapper in a service container or manually:
$mapper = new UserMapper();
$userDto = $mapper->map($userEntity, UserDto::class);
DTO Creation Use mappers to transform Eloquent models into DTOs for API responses:
$userDto = $mapper->map($user, UserDto::class);
return response()->json($userDto);
Bidirectional Mapping
Extend Mapper to handle reverse mapping (DTO → Entity):
public function reverseMap($dto, $entityClass)
{
$entity = new $entityClass();
$entity->name = $dto->name;
$entity->email = $dto->email;
return $entity;
}
Collection Mapping
Leverage mapCollection for bulk operations:
$userDtos = $mapper->mapCollection($users, UserDto::class);
Service Integration Bind mappers to the container for dependency injection:
$this->app->bind(UserMapper::class, function ($app) {
return new UserMapper();
});
Conditional Mapping Use logic to map fields conditionally:
if ($entity->isActive()) {
$dto->status = 'active';
}
Nested Object Mapping Handle relationships recursively:
$dto->posts = $mapper->mapCollection($entity->posts, PostDto::class);
Custom Type Handling
Override mapProperty for complex types (e.g., dates, enums):
protected function mapProperty($source, $target, $property)
{
if ($property === 'created_at') {
return $source->created_at->format('Y-m-d');
}
return parent::mapProperty($source, $target, $property);
}
Circular References
Avoid infinite loops when mapping nested objects with bidirectional relationships. Use ignore or break cycles manually:
$mapper->ignore('posts.user'); // Skip mapping posts.user to prevent cycles
Property Overrides
Ensure property names match exactly (case-sensitive). Use mapProperty to handle mismatches:
$dto->userName = $entity->name; // Custom mapping
Performance with Large Collections Batch processing or lazy loading may be needed for large datasets. Consider:
$mapper->mapCollection($users->take(100), UserDto::class);
Type Safety Validate DTO classes exist before mapping:
if (!class_exists($dtoClass)) {
throw new \InvalidArgumentException("DTO class {$dtoClass} not found.");
}
Enable Logging
Extend Mapper to log mappings for debugging:
protected function mapProperty($source, $target, $property)
{
\Log::debug("Mapping {$property}: " . print_r($source, true));
return parent::mapProperty($source, $target, $property);
}
Check for Nulls
Handle null values explicitly to avoid errors:
$dto->email = $entity->email ?? null;
Custom Mappers Create reusable mappers for shared logic:
class BaseMapper extends Mapper {
protected function mapTimestamps($source, $target) {
$target->created_at = $source->created_at->toDateTimeString();
$target->updated_at = $source->updated_at->toDateTimeString();
}
}
Hooks
Override beforeMap/afterMap for pre/post-processing:
protected function beforeMap($source, $target)
{
$this->sanitizeInput($source);
}
Configuration Use constructor injection for dynamic behavior:
class UserMapper extends Mapper {
public function __construct(private bool $includeSoftDeletes = false) {}
public function map($entity, $dtoClass) {
if ($this->includeSoftDeletes) {
$dto->deleted_at = $entity->deleted_at;
}
}
}
Testing Mock mappers in unit tests:
$mapper = $this->createMock(UserMapper::class);
$mapper->method('map')->willReturn(new UserDto());
How can I help you explore Laravel packages today?