Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A tiny, underwhelming data mapper for Symfony to map one object to another!

License

NotificationsYou must be signed in to change notification settings

SymfonyCasts/micro-mapper

Repository files navigation

CI

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!

FeatureMicroMapperJane's Automapper
Some of the mapping is automatic
Extensible
Handles nested objects
Small & Dead-simple(not SO simple)

Support us & Symfony

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!

Installation

composer require symfonycasts/micro-mapper

If you're using Symfony, you're done! If not, seeStand-alone Library Setup.

Usage

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).

Step 1: Create the Mapper Class

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:

  1. #[AsMapper] attribute: defines the "from" and "to" classes (needed forSymfony usage only).
  2. load() method: creates/loads the "to" object - e.g. load it from thedatabase or create it and populate just the identifier.
  3. populate() method: populates the "to" object with data from the "from"object.

Step 2: Use the MicroMapper Service

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);    }}

Reverse Transforming

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.

Handling Nested Objects

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.

MAX_DEPTH & Circular References

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, theDragon entitywill be mapped to aDragonApi object by calling theload() method onDragonEntityToApiMapper. But thepopulate() method willnot be called.This means that the finalDragonApi object will have anid but no other data.

Result:

DragonApi:id:1name:nullfirePower:nulltreasures:[]
  • MAX_DEPTH = 1: TheDragon entity will befully mapped to aDragonApi object: both theload() andpopulate() methods will becalled on its mapper like normal. However, when eachTreasure inDragon.treasures is mapped to aTreasureApi object, this will be"shallow": theTreasureApi object will have anid property 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.

Settable Collection Relations on Entities

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;    }}

Standalone Library Setup

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.

Credits

License

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

Stars

Watchers

Forks

Packages

No packages published

Contributors7

Languages


[8]ページ先頭

©2009-2025 Movatter.jp