- Notifications
You must be signed in to change notification settings - Fork6
A tiny, underwhelming data mapper for Symfony to map one object to another!
License
SymfonyCasts/micro-mapper
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Need to map one object (e.g. a Doctrine entity) to anotherobject (e.g. a DTO) andlove writing the mapping code manually?Then this library is for you!
Define a "mapper" class:
useApp\Entity\Dragon;useApp\DTO\DragonDTO;#[AsMapper(from: Dragon::class, to: DragonDTO::class)]class DragonEntityToDtoMapperimplements MapperInterface{publicfunctionload(object$from,string$toClass,array$context):object {$entity =$from;returnnewDragonDTO($entity->getId()); }publicfunctionpopulate(object$from,object$to,array$context):object {$entity =$from;$dto =$to;$dto->name =$entity->getName();$dto->firePower =$entity->getFirePower();return$dto; }}
Then... map!
$dragon =$dragonRepository->find(1);$dragonDTO =$microMapper->map($dragon, DragonDTO::class);
MicroMapper is similar to other data mappers, likejolicode/automapper, except... lessimpressive! Jane's Automapper is awesome and handles a lot of heavy lifting.With MicroMapper,you do the heavy lifting. Let's review with a table!
| Feature | MicroMapper | Jane's Automapper |
|---|---|---|
| Some of the mapping is automatic | ❌ | ✅ |
| Extensible | ✅ | ✅ |
| Handles nested objects | ✅ | ✅ |
| Small & Dead-simple | ✅ | (not SO simple) |
Is this package useful! We'rethrilled 😍!
A lot of time & effort from the Symfonycasts team & the Symfony communitygoes into creating and maintaining these packages. You can support us +Symfony (and learn a bucket-load) by grabbing a subscription toSymfonyCasts!
composer require symfonycasts/micro-mapper
If you're using Symfony, you're done! If not, seeStand-alone Library Setup.
Suppose you have aDragon entity, and you want to map it to aDragonApi object (perhaps to use with API Platform, like we doin ourApi Platform EP3 Tutorial).
To do this, create a "mapper" class that defines how to map:
namespaceApp\Mapper;useApp\Entity\Dragon;useApp\ApiResource\DragonApi;useSymfonycasts\MicroMapper\AsMapper;useSymfonycasts\MicroMapper\MapperInterface;#[AsMapper(from: Dragon::class, to: DragonApi::class)]class DragonEntityToApiMapperimplements MapperInterface{publicfunctionload(object$from,string$toClass,array$context):object {$entity =$from;assert($entityinstanceof Dragon);// helps your editor know the typereturnnewDragonApi($entity->getId()); }publicfunctionpopulate(object$from,object$to,array$context):object {$entity =$from;$dto =$to;// helps your editor know the typesassert($entityinstanceof Dragon);assert($dtoinstanceof DragonApi);$dto->name =$entity->getName();$dto->firePower =$entity->getFirePower();return$dto; }}
The mapper class has three parts:
#[AsMapper]attribute: defines the "from" and "to" classes (needed forSymfony usage only).load()method: creates/loads the "to" object - e.g. load it from thedatabase or create it and populate just the identifier.populate()method: populates the "to" object with data from the "from"object.
To use the mapper, you can fetch theMicroMapperInterface service. Forexample, from a controller:
<?phpnamespaceApp\Controller;useApp\Entity\Dragon;useApp\ApiResource\DragonApi;useSymfonycasts\MicroMapper\MicroMapperInterface;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\Routing\Annotation\Route;class DragonControllerextends AbstractController{ #[Route('/dragons/{id}', name:'api_dragon_get_collection')]publicfunctionindex(Dragon$dragon,MicroMapperInterface$microMapper) {$dragonApi =$microMapper->map($dragon, DragonApi::class);return$this->json($dragonApi); }}
To do the reverse transformation -DragonApi toDragon - it'sthe same process: create a mapper class:
The mapper:
namespaceApp\Mapper;useApp\ApiResource\DragonApi;useApp\Entity\Dragon;useApp\Repository\DragonRepository;useSymfonycasts\MicroMapper\AsMapper;useSymfonycasts\MicroMapper\MapperInterface;#[AsMapper(from: DragonApi::class, to: Dragon::class)]class DragonApiToEntityMapperimplements MapperInterface{publicfunction__construct(privateDragonRepository$dragonRepository) { }publicfunctionload(object$from,string$toClass,array$context):object {$dto =$from;assert($dtoinstanceof DragonApi);return$dto->id ?$this->dragonRepository->find($dto->id) :newDragon(); }publicfunctionpopulate(object$from,object$to,array$context):object {$dto =$from;$entity =$to;assert($dtoinstanceof DragonApi);assert($entityinstanceof Dragon);$entity->setName($dto->name);$entity->setFirePower($dto->firePower);return$entity; }}
In this case, theload() method fetches theDragon entity from thedatabase if it has anid property.
If you have nested objects, you can use theMicroMapperInterface to mapthose too. Suppose theDragon entity has atreasures propertythat is aOneToMany relation toTreasure entity. And inDragonApi, we haveatreasures property that should hold an array ofTreasureApi objects.
First, create a mapper for theTreasure ->TreasureApi mapping:
// ...#[AsMapper(from: Treasure::class, to: TreasureApi::class)]class TreasureEntityToApiMapperimplements MapperInterface{publicfunctionload(object$from,string$toClass,array$context):object {returnnewTreasureApi($from->getId()); }publicfunctionpopulate(object$from,object$to,array$context):object {$entity =$from;$dto =$to;// ... map all the propertiesreturn$dto; }}
Next, in theDragonEntityToApiMapper, use theMicroMapperInterface to map theTreasure objects toTreasureApi objects:
namespaceApp\Mapper;// ...useApp\ApiResource\TreasureApi;useSymfonycasts\MicroMapper\MicroMapperInterface;#[AsMapper(from: Dragon::class, to: DragonApi::class)]class DragonEntityToApiMapperimplements MapperInterface{publicfunction__construct(privateMicroMapperInterface$microMapper) { }// load() is the samepublicfunctionpopulate(object$from,object$to,array$context):object {$entity =$from;$dto =$to;// ... other properties$treasuresApis = [];foreach ($entity->getTreasures()as$treasureEntity) {$treasuresApis[] =$this->microMapper->map($treasureEntity, TreasureApi::class, [ MicroMapperInterface::MAX_DEPTH =>1, ]); }$dto->treasures =$treasuresApis;return$dto; }}
That's it! The result will be aDragonApi object with atreasures propertythat holds an array ofTreasureApi objects.
Imagine now thatTreasureEntityToApiMapperalso maps adragonproperty on theTreasureApi object:
// ...#[AsMapper(from: Treasure::class, to: TreasureApi::class)]class TreasureEntityToApiMapperimplements MapperInterface{publicfunction__construct(privateMicroMapperInterface$microMapper) { }// load()publicfunctionpopulate(object$from,object$to,array$context):object {$entity =$from;$dto =$to;// ... map all the properties$dto->dragon =$this->microMapper->map($entity->getDragon(), DragonApi::class, [ MicroMapperInterface::MAX_DEPTH =>1, ]);return$dto; }}
This creates a circular reference: theDragon entity is mapped to aDragonApi object... which then maps itstreasures property to an arrayofTreasureApi objects... which then each map theirdragon property to aDragonApi object... forever... and ever... and ever...
TheMAX_DEPTH option tells MicroMapper how many levels deep togo when mapping, and youusually want to set this to 0 or 1 when mapping arelation.
When the max depth is hit, theload() method will be called on the mapperfor that level butpopulate() willnot be called. This results in a"shallow" mapping of the final level object.
Let's look at a few depth examples using this code:
$dto->dragon =$this->microMapper->map($dragonEntity, DragonApi::class, [ MicroMapperInterface::MAX_DEPTH => ???,]);
MAX_DEPTH = 0: Because the depth is immediately hit, theDragonentitywill be mapped to aDragonApiobject by calling theload()method onDragonEntityToApiMapper. But thepopulate()method willnot be called.This means that the finalDragonApiobject will have anidbut no other data.
Result:
DragonApi:id:1name:nullfirePower:nulltreasures:[]
MAX_DEPTH = 1: TheDragonentity will befully mapped to aDragonApiobject: both theload()andpopulate()methods will becalled on its mapper like normal. However, when eachTreasureinDragon.treasuresis mapped to aTreasureApiobject, this will be"shallow": theTreasureApiobject will have anidproperty butno other data (because the max depth was hit and so onlyload()is calledonTreasureEntityToApiMapper).
Result:
DragonApi:id:1name:'Sizzley Pete'firePower:100treasures:TreasureApi:id:1name:nullvalue:nulldragon:nullTreasureApi:id:2name:nullvalue:nulldragon:null
In something like API Platform, you can also useMAX_DEPTH to limit thedepth of the serialization for performance. For example, if theTreasureApiobject has adragon property that is expressed as the IRI string (e.g./api/dragons/1), then settingMAX_DEPTH to0 is enough and preventsextra mapping work.
In our example, theDragon entity has atreasures property that is aOneToMany relation to theTreasure entity. Our DTO classes havethe same relation:DragonApi holds an array ofTreasureApi objects.Those greedy dragons!
If you want to map aDragonApi object to theDragon entity andtheDragonApi.treasures property may have changed, you need toupdate theDragon.treasures properly carefully.
For example, this willnot work:
// ...#[AsMapper(from: DragonApi::class, to: Dragon::class)]class DragonApiToEntityMapperimplements MapperInterface{// ...publicfunctionpopulate(object$from,object$to,array$context):object {$dto =$from;$entity =$to;// ...$treasureEntities =newArrayCollection();foreach ($dto->treasuresas$treasureApi) {$treasureEntities[] =$this->microMapper->map($treasureApi, Treasure::class, [// depth=0 because we really just need to load/query each Treasure entity MicroMapperInterface::MAX_DEPTH =>0, ]); }// !!!!! THIS WILL NOT WORK !!!!!$entity->setTreasures($treasureEntities);return$entity; }}
The problem is with the$entity->setTreasures() call. In fact, this method probablydoesn't even exist on theDragon entity! Instead, it likely hasaddTreasure() andremoveTreasure() methods andthese must be called instead so that the "owning"side of the Doctrine relationship is correctly set (otherwise the changes won't save).
An easy way to do this is with thePropertyAccessorInterface service:
// ...useSymfony\Component\PropertyAccess\PropertyAccessorInterface;#[AsMapper(from: DragonApi::class, to: Dragon::class)]class DragonApiToEntityMapperimplements MapperInterface{publicfunction__construct(privateMicroMapperInterface$microMapper,privatePropertyAccessorInterface$propertyAccessor ) { }// ...publicfunctionpopulate(object$from,object$to,array$context):object {$dto =$from;$entity =$to;// ...$treasureEntities = [];foreach ($dto->treasuresas$treasureApi) {$treasureEntities[] =$this->microMapper->map($treasureApi, Treasure::class, [ MicroMapperInterface::MAX_DEPTH =>0, ]); }// this will call the addTreasure() and removeTreasure() methods$this->propertyAccessor->setValue($entity,'treasures',$treasureEntities);return$entity; }}
If you're not using Symfony, you can still use MicroMapper! You'll need toinstantiate theMicroMapper class and pass it all of your mappings:
$microMapper =newMicroMapper([]);$microMapper->addMapperConfig(newMapperConfig( from: Dragon::class, to: DragonApi::class,fn() =>newDragonEntityToApiMapper($microMapper)));$microMapper->addMapperConfig(newMapperConfig( from: DragonApi::class, to: Dragon::class,fn() =>newDragonApiToEntityMapper($microMapper)));// now it's ready to use!
In this case, the#[AsMapper] attribute is not needed.
MIT License (MIT): see theLicense File for more details.
About
A tiny, underwhelming data mapper for Symfony to map one object to another!
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors7
Uh oh!
There was an error while loading.Please reload this page.