symfonycasts / micro-mapper
一个微小、不起眼的映射器,用于将一个对象映射到另一个对象!
Requires
- php: >=8.1
Requires (Dev)
- phpstan/phpstan: ^1.10.39
- symfony/filesystem: ^6.3|^7.0
- symfony/framework-bundle: ^6.3|^7.0
- symfony/phpunit-bridge: ^6.3|^7.0
README
需要将一个对象(例如 Doctrine 实体)映射到另一个对象(例如 DTO)并 喜欢 手动编写映射代码?那么这个库就是为您准备的!
定义一个“映射器”类
use App\Entity\Dragon; use App\DTO\DragonDTO; #[AsMapper(from: Dragon::class, to: DragonDTO::class)] class DragonEntityToDtoMapper implements MapperInterface { public function load(object $from, string $toClass, array $context): object { $entity = $from; return new DragonDTO($entity->getId()); } public function populate(object $from, object $to, array $context): object { $entity = $from; $dto = $to; $dto->name = $entity->getName(); $dto->firePower = $entity->getFirePower(); return $dto; } }
然后...映射!
$dragon = $dragonRepository->find(1); $dragonDTO = $microMapper->map($dragon, DragonDTO::class);
MicroMapper 与其他数据映射器类似,如 jane-php/automapper,但...更不引人注目!Jane 的 Automapper 很棒,可以处理很多繁重的工作。使用 MicroMapper,您 做繁重的工作。让我们用表格来回顾一下!
支持我们 & Symfony
这个包有用!我们 非常兴奋 😍!
Symfonycasts 团队和 Symfony 社区投入了大量的时间和精力来创建和维护这些包。您可以订阅 SymfonyCasts 来支持我们 + Symfony(并学习大量知识)!
安装
composer require symfonycasts/micro-mapper
如果您正在使用 Symfony,那么您已经完成了!如果您不使用 Symfony,请参阅 独立库设置。
用法
假设您有一个 Dragon
实体,并且您想将其映射到 DragonApi
对象(可能用于 API Platform,就像我们在 Api Platform EP3 教程 中做的那样)。
步骤 1:创建映射器类
为此,创建一个定义如何映射的“映射器”类
namespace App\Mapper; use App\Entity\Dragon; use App\ApiResource\DragonApi; use Symfonycasts\MicroMapper\AsMapper; use Symfonycasts\MicroMapper\MapperInterface; #[AsMapper(from: Dragon::class, to: DragonApi::class)] class DragonEntityToApiMapper implements MapperInterface { public function load(object $from, string $toClass, array $context): object { $entity = $from; assert($entity instanceof Dragon); // helps your editor know the type return new DragonApi($entity->getId()); } public function populate(object $from, object $to, array $context): object { $entity = $from; $dto = $to; // helps your editor know the types assert($entity instanceof Dragon); assert($dto instanceof DragonApi); $dto->name = $entity->getName(); $dto->firePower = $entity->getFirePower(); return $dto; } }
映射器类有三个部分
#[AsMapper]
属性:定义“从”和“到”类(仅用于 Symfony 使用)。load()
方法:创建/加载“到”对象 - 例如,从数据库中加载或创建它并仅填充标识符。populate()
方法:使用来自“从”对象的数据填充“到”对象。
步骤 2:使用 MicroMapper 服务
要使用映射器,您可以获取 MicroMapperInterface
服务。例如,从控制器中
<?php namespace App\Controller; use App\Entity\Dragon; use App\ApiResource\DragonApi; use Symfonycasts\MicroMapper\MicroMapperInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; class DragonController extends AbstractController { #[Route('/dragons/{id}', name: 'api_dragon_get_collection')] public function index(Dragon $dragon, MicroMapperInterface $microMapper) { $dragonApi = $microMapper->map($dragon, DragonApi::class); return $this->json($dragonApi); } }
反向转换
要进行反向转换 - DragonApi
到 Dragon
- 过程相同:创建一个映射器类
映射器
namespace App\Mapper; use App\ApiResource\DragonApi; use App\Entity\Dragon; use App\Repository\DragonRepository; use Symfonycasts\MicroMapper\AsMapper; use Symfonycasts\MicroMapper\MapperInterface; #[AsMapper(from: DragonApi::class, to: Dragon::class)] class DragonApiToEntityMapper implements MapperInterface { public function __construct(private DragonRepository $dragonRepository) { } public function load(object $from, string $toClass, array $context): object { $dto = $from; assert($dto instanceof DragonApi); return $dto->id ? $this->dragonRepository->find($dto->id) : new Dragon(); } public function populate(object $from, object $to, array $context): object { $dto = $from; $entity = $to; assert($dto instanceof DragonApi); assert($entity instanceof Dragon); $entity->setName($dto->name); $entity->setFirePower($dto->firePower); return $entity; } }
在这种情况下,load()
方法如果 Dragon
实体有一个 id
属性,则会从数据库中获取 Dragon
实体。
处理嵌套对象
如果您有嵌套对象,您可以使用 MicroMapperInterface
来映射这些对象。假设 Dragon
实体有一个 treasures
属性,它是与 Treasure
实体的 OneToMany
关系。在 DragonApi
中,我们有一个应该包含 TreasureApi
对象数组的 treasures
属性。
首先,为 Treasure
-> TreasureApi
映射创建映射器
// ... #[AsMapper(from: Treasure::class, to: TreasureApi::class)] class TreasureEntityToApiMapper implements MapperInterface { public function load(object $from, string $toClass, array $context): object { return new TreasureApi($from->getId()); } public function populate(object $from, object $to, array $context): object { $entity = $from; $dto = $to; // ... map all the properties return $dto; } }
然后,在 DragonEntityToApiMapper
中,使用 MicroMapperInterface
将 Treasure
对象映射到 TreasureApi
对象
namespace App\Mapper; // ... use App\ApiResource\TreasureApi; use Symfonycasts\MicroMapper\MicroMapperInterface; #[AsMapper(from: Dragon::class, to: DragonApi::class)] class DragonEntityToApiMapper implements MapperInterface { public function __construct(private MicroMapperInterface $microMapper) { } // load() is the same public function populate(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; } }
这就完成了!结果将是一个具有包含 TreasureApi
对象数组的 treasures
属性的 DragonApi
对象。
MAX_DEPTH & 循环引用
现在想象一下 TreasureEntityToApiMapper
也会映射 TreasureApi
对象上的 dragon
属性
// ... #[AsMapper(from: Treasure::class, to: TreasureApi::class)] class TreasureEntityToApiMapper implements MapperInterface { public function __construct(private MicroMapperInterface $microMapper) { } // load() public function populate(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; } }
这创建了一个循环引用:将 Dragon
实体映射到 DragonApi
对象... 然后,该对象将它的 treasures
属性映射到一个 TreasureApi
对象数组... 然后,每个对象又将其 dragon
属性映射到一个 DragonApi
对象... 永无止境...
MAX_DEPTH
选项告诉 MicroMapper 映射时要深入多少层,并且在映射关系时你通常想将其设置为 0 或 1。
当达到最大深度时,该层的映射器的 load()
方法将被调用,但 populate()
方法 不会 被调用。这会导致最终层的对象进行“浅层”映射。
让我们看看几个使用此代码的深度示例
$dto->dragon = $this->microMapper->map($dragonEntity, DragonApi::class, [ MicroMapperInterface::MAX_DEPTH => ???, ]);
MAX_DEPTH = 0
:由于深度立即达到,将通过在DragonEntityToApiMapper
上调用load()
方法将Dragon
实体映射到DragonApi
对象。但不会调用populate()
方法。这意味着最终的DragonApi
对象将有一个id
但没有其他数据。
结果
DragonApi: id: 1 name: null firePower: null treasures: []
MAX_DEPTH = 1
:将Dragon
实体完整地映射到DragonApi
对象:其映射器的load()
和populate()
方法都会像平常一样被调用。然而,当将Dragon.treasures
中的每个Treasure
映射到TreasureApi
对象时,这将是“浅层”的:TreasureApi
对象将有一个id
属性但没有其他数据(因为最大深度已达到,所以仅在TreasureEntityToApiMapper
上调用load()
)。
结果
DragonApi: id: 1 name: 'Sizzley Pete' firePower: 100 treasures: TreasureApi: id: 1 name: null value: null dragon: null TreasureApi: id: 2 name: null value: null dragon: null
在像 API Platform 这样的东西中,你还可以使用 MAX_DEPTH
来限制序列化的深度以提高性能。例如,如果 TreasureApi
对象有一个表示为 IRI 字符串的 dragon
属性(例如,/api/dragons/1
),则将 MAX_DEPTH
设置为 0
就足够了,这样可以防止额外的映射工作。
实体上的可设置集合关系
在我们的示例中,Dragon
实体有一个 treasures
属性,它与 Treasure
实体有一个 OneToMany
关系。我们的 DTO 类有相同的关系:DragonApi
包含一个 TreasureApi
对象数组。那些贪婪的龙啊!
如果你要将 DragonApi
对象映射到 Dragon
实体,并且 DragonApi.treasures
属性可能已更改,你需要小心地正确更新 Dragon.treasures
。
例如,这样做将 不起作用
// ... #[AsMapper(from: DragonApi::class, to: Dragon::class)] class DragonApiToEntityMapper implements MapperInterface { // ... public function populate(object $from, object $to, array $context): object { $dto = $from; $entity = $to; // ... $treasureEntities = new ArrayCollection(); foreach ($dto->treasures as $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; } }
问题是 $entity->setTreasures()
调用。实际上,这个方法可能甚至不存在于 Dragon
实体上!相反,它可能具有 addTreasure()
和 removeTreasure()
方法,并且必须调用这些方法,以便正确设置 Doctrine 关系的“拥有”侧(否则更改不会保存)。
使用 PropertyAccessorInterface
服务这是一种简单的方法
// ... use Symfony\Component\PropertyAccess\PropertyAccessorInterface; #[AsMapper(from: DragonApi::class, to: Dragon::class)] class DragonApiToEntityMapper implements MapperInterface { public function __construct( private MicroMapperInterface $microMapper, private PropertyAccessorInterface $propertyAccessor ) { } // ... public function populate(object $from, object $to, array $context): object { $dto = $from; $entity = $to; // ... $treasureEntities = []; foreach ($dto->treasures as $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; } }
独立库设置
如果你没有使用 Symfony,你仍然可以使用 MicroMapper!你需要实例化 MicroMapper
类,并将所有映射传递给它
$microMapper = new MicroMapper([]); $microMapper->addMapperConfig(new MapperConfig( from: Dragon::class, to: DragonApi::class, fn() => new DragonEntityToApiMapper($microMapper) )); $microMapper->addMapperConfig(new MapperConfig( from: DragonApi::class, to: Dragon::class, fn() => new DragonApiToEntityMapper($microMapper) )); // now it's ready to use!
在这种情况下,不需要 #[AsMapper]
属性。
致谢
许可协议
MIT 许可协议 (MIT):有关更多详细信息,请参阅 许可文件。