symfonycasts/micro-mapper

一个微小、不起眼的映射器,用于将一个对象映射到另一个对象!

v0.2.0 2024-06-27 12:06 UTC

This package is auto-updated.

Last update: 2024-08-31 19:22:18 UTC


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

映射器类有三个部分

  1. #[AsMapper] 属性:定义“从”和“到”类(仅用于 Symfony 使用)。
  2. load() 方法:创建/加载“到”对象 - 例如,从数据库中加载或创建它并仅填充标识符。
  3. 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);
    }
}

反向转换

要进行反向转换 - DragonApiDragon - 过程相同:创建一个映射器类

映射器

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 中,使用 MicroMapperInterfaceTreasure 对象映射到 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):有关更多详细信息,请参阅 许可文件