cmath10/mapper
A lightweight Laravel/PHP mapping utility to transform data between arrays and objects using configurable field mappings. Helps normalize payloads, rename keys, cast values, and build DTO-style structures with minimal boilerplate.
Installation
composer require cmath10/mapper
Add the service provider to config/app.php:
'providers' => [
// ...
Cmath10\Mapper\MapperServiceProvider::class,
],
Basic Usage
Define a mapper class (e.g., UserMapper):
namespace App\Mappers;
use Cmath10\Mapper\Mapper;
class UserMapper extends Mapper
{
protected $source = 'App\Models\User';
protected $destination = 'App\DTO\UserDTO';
public function map($sourceData)
{
return $this->destination::create([
'id' => $sourceData->id,
'name' => $sourceData->name,
'email' => $sourceData->email,
]);
}
}
First Use Case Map a model to a DTO in a controller:
use App\Mappers\UserMapper;
public function show(User $user)
{
$mapper = app(UserMapper::class);
$dto = $mapper->map($user);
return response()->json($dto);
}
Collection Mapping
Use mapCollection() for bulk operations:
$users = User::all();
$mapped = app(UserMapper::class)->mapCollection($users);
Nested Mappings Chain mappers for complex structures:
class PostMapper extends Mapper
{
protected $source = 'App\Models\Post';
protected $destination = 'App\DTO\PostDTO';
public function map($sourceData)
{
$dto = $this->destination::create([
'id' => $sourceData->id,
'title' => $sourceData->title,
'author' => app(UserMapper::class)->map($sourceData->user),
]);
return $dto;
}
}
Conditional Mapping
Override map() to handle edge cases:
public function map($sourceData)
{
if (!$sourceData->isActive()) {
return null; // Skip inactive records
}
return $this->destination::create([...]);
}
public function handle(PostCreated $event)
{
$mapper = app(PostMapper::class);
$dto = $mapper->map($event->post);
// Process DTO...
}
JsonResource with mapped data:
public function toArray($request)
{
return [
'data' => app(UserMapper::class)->map($this->resource),
];
}
public function rules()
{
return [
'name' => 'required',
'email' => 'required|email',
];
}
public function prepareForValidation()
{
$this->merge([
'user' => app(UserMapper::class)->map($this->all()),
]);
}
Circular References
Avoid infinite loops in nested mappings (e.g., User → Post → User).
Fix: Use ignore() in mapper:
public function map($sourceData)
{
return $this->destination::create([
'posts' => $sourceData->posts->map(fn($post) => $post->id), // Avoid nested mapping
]);
}
Type Safety
Ensure $destination class exists and implements Arrayable/Jsonable.
Fix: Add a validateDestination() method to your mapper:
protected function validateDestination()
{
if (!class_exists($this->destination)) {
throw new \RuntimeException("Destination class {$this->destination} not found.");
}
}
Performance
mapCollection() loads all records into memory. For large datasets:
User::chunk(100, function ($users) {
app(UserMapper::class)->mapCollection($users);
});
map():
\Log::debug('Mapping source:', [$sourceData->toArray()]);
dd() or dump() to inspect $sourceData before mapping.Custom Mapper Traits Reuse logic across mappers:
trait SoftDeletesMapper
{
public function map($sourceData)
{
return $this->destination::create([
'deleted_at' => $sourceData->deleted_at,
// ...
]);
}
}
Dynamic Destinations
Set $destination dynamically:
protected $destination = 'App\DTO\\' . Str::studly($this->source->getTable()) . 'DTO';
Caching Mapped Results Cache DTOs to avoid redundant mapping:
public function map($sourceData)
{
return Cache::remember(
"mapper_{$this->destination}_{$sourceData->id}",
now()->addHours(1),
fn() => $this->destination::create([...])
);
}
How can I help you explore Laravel packages today?