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.
The library provides a utility for converting objects to each other (and arrays to objects) and transferring data between them by reusable map.
Basic example:
class ArticleInput
{
public $title;
public $text;
public $author;
}
class Article
{
public $title;
public $text;
public $author;
public function __construct(?string $title = null)
{
$this->title = $title;
}
}
$input = new ArticleInput();
$input->title = 'title';
$input->text = 'text';
$article1 = new Article();
$mapper = new cmath10\Mapper\Mapper();
$mapper->create(ArticleInput::class, Article::class);
$mapper->map($input, $article1);
// ($article1->title === 'title') === true
// ($article1->text === 'text') === true
$article2 = $mapper->map($input, Article::class)
// ($article2 instanceof Article) === true
// ($article2->title === 'title') === true
// ($article2->text === 'text') === true
The main term of the library is a map. It is an object which is containing information about how to extract values from the source and inject them into a destination object. Values can be processed before injection (to get an opportunity for sanitization and nested mapping).
Map includes:
symfony/property-access
for that purpose, but there are other options, like using callbacks or symfony/expression-language and presets (if
the extracted value is null), also there is an interface for custom accessors;By default, a map creates a set of accessors based on a class properties list, so a field from a source will be mapped on a field of a destination with the same name. You can change this behavior for specific fields if needed.
All maps must be registered in a mapper instance before using. When you call the mapper method map it
seeks the correct map for a given source and destination, then it uses accessors to extract values, filters to process
them, and, after that, injects values to a destination. If the destination is a class name, the mapper will try to
create an instance of the given class, if the class constructor is public and all possible arguments are optional.
composer require cmath10/mapper
cmath10\Mapper\MapperInterface — interface for mappers:
create(string $sourceType, string $destinationType) — creates, registers and returns default map for two
classes;register(MapInterface $map) — registers a custom map in the mapper;map($source, $destination) — performs mapping from a source to a destination object or class.cmath10\mapper\MapInterface — interface for maps:
getSourceType() — returns name of source type; used to find the correct map;getDestinationType() — returns name of destination type; used to find the correct map;getFieldAccessors() — returns all fields accessors;getFieldFilters() — returns all fields filters;getOverwriteIfSet() — returns boolean flag; if true, destination member will be overwritten even if
its value is not null;getSkipNull() — returns boolean flag; if true, the mapper will not push null source member's value;route(string $destinationMember, string $sourceMember) — sets mapping of destination route to
source route; fluent;forMember(string $destinationMember, AccessorInterface $fieldMapper) — sets custom accessor for field; fluent;filter(string $destinationMember, FilterInterface $fieldFilter) — sets a custom filter for value
processing; fluent;ignoreMember(string $destinationMember) — excludes members from accounting, so they will not be filled;
fluent.cmath10\mapper\FieldAccessor\AccessorInterface — interface for accessors:
getValue($source) — extracts value from a source.cmath10\mapper\FieldFilter\FitlerInterface — interface for filters:
filter($value) — processes the value.cmath10\Mapper\TypeFilterInterface — interface for type filters is used to process type names. Useful for
extracting class names from runtime generated classes like proxies in Doctrine. Used in default mapper which uses an
array of type filters as a constructor parameter (to instantiate cmath10\mapper\TypeGuesser which is used to
determine the correct map for supplied types in mapper's map method).
Basic class is used to create custom map classes, implements MapperInterface;
provides following protected method:
setupDefaults — creates a set of accessors based on a class properties list.Usage example:
use cmath10\Mapper\AbstractMap;
class ArticleOutputMap extends AbstractMap
{
public function __construct()
{
$this
->setupDefaults() // create default accessors set
->route('textNotMappedByDefault', 'text') // customize
;
}
public function getSourceType(): string
{
return Article::class;
}
public function getDestinationType(): string
{
return ArticleOutput::class;
}
}
Simple descendant of AbstractMap, just calls setupDefaults in
its constructor; the mapper create method creates, registers and returns instance of this class.
Uses callback to extract a value:
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldAccessor\ClosureAccessor;
$mapper = new Mapper();
$mapper
->create(Fixtures\Article::class, Fixtures\ArticleInput::class)
->forMember('author', new ClosureAccessor(fn (Fixtures\ArticleInput $a) => $a->author->name))
;
Uses symfony/expression-language to extract a value:
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldAccessor\ExpressionAccessor;
$mapper = new Mapper();
$mapper
->create(Fixtures\MagazineWithPrivateProperties::class, Fixtures\MagazineOutput::class)
->forMember('articles', new ExpressionAccessor('getArticles()'))
;
Does not extract a value, just provides:
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldAccessor\PresetAccessor;
$mapper = new Mapper();
$mapper
->create(Fixtures\MagazineWithPrivateProperties::class, Fixtures\MagazineOutput::class)
->forMember('articles', new PresetAccessor([]))
;
Uses symfony/property-access to extract a value. This accessor is used by default. Calls like:
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldAccessor\PropertyPathAccessor;
$mapper = new Mapper();
$mapper
->create(Fixtures\MagazineWithPrivateProperties::class, Fixtures\MagazineOutput::class)
->forMember('articles', new PropertyPathAccessor('someFieldWithArticles'))
;
and
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldAccessor\PropertyPathAccessor;
$mapper = new Mapper();
$mapper
->create(Fixtures\MagazineWithPrivateProperties::class, Fixtures\MagazineOutput::class)
->route('articles', 'someFieldWithArticles')
;
are equivalent. Also, if the fields in a source and destination have the same name, and you use
setupDefaults, you don't need to call route or forMember explicitly.
Uses callback to process value:
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldFilter\ClosureFilter;
// For ArticleInput title='title' we will get Article title='[[title]]'
$mapper = new Mapper();
$mapper
->create(Fixtures\ArticleInput::class, Fixtures\Article::class)
->filter('title', new ClosureFilter(static fn ($title) => '[[' . $title . ']]'))
;
Replaces the value if it is null:
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldFilter\IfNullFilter;
// For ArticleInput title=null we will get Article title='defaultTitle'
$mapper = new Mapper();
$mapper
->create(Fixtures\ArticleInput::class, Fixtures\Article::class)
->filter('title', new IfNullFilter('defaultTitle'))
;
Basic class for filters that can use mapper. Requires class name in the constructor. Used for nested mapping.
Descendant of AbstractMappingFilter, uses class name and mapper to make an
object from a source member:
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldFilter\ObjectMappingFilter;
$mapper = new Mapper();
$mapper->create(Fixtures\AuthorInput::class, Fixtures\Author::class);
$mapper
->create(Fixtures\ArticleInput::class, Fixtures\Article::class)
->route('author', 'author')
->filter('author', new ObjectMappingFilter(Fixtures\Author::class))
;
Descendant of AbstractMappingFilter, uses class name and mapper to make an
array of objects from a source member, if the source member's value is array (returns
an empty array instead):
use cmath10\Mapper\Mapper;
use cmath10\Mapper\FieldFilter\ObjectArrayMappingFilter;
$mapper = new Mapper();
$mapper->create(Fixtures\Article::class, Fixtures\ArticleOutput::class);
$mapper
->create(Fixtures\Magazine::class, Fixtures\MagazineOutput::class)
->route('articles', 'articles')
->filter('articles', new ObjectArrayMappingFilter(Fixtures\ArticleOutput::class))
;
How can I help you explore Laravel packages today?