pixelshaped/flat-mapper-bundle

一个用于轻松从反规范化数组结果(如数据库查询结果)创建非标量 DTO 的包。

安装: 703

依赖项: 0

建议者: 0

安全: 0

星标: 7

关注者: 1

分支: 0

开放问题: 0

类型:symfony-bundle

2.0.2 2024-07-30 10:09 UTC

This package is auto-updated.

Last update: 2024-08-30 10:17:10 UTC


README

Latest Stable Version CI codecov

Flat Mapper Bundle

本插件旨在解决从扁平数组(如数据库查询结果)构建嵌套 DTO 的问题。

其中一个目的是帮助您以与 Doctrine NEW 关键字相同的方式创建 DTO,只是在深度上。其他方法通常意味着将实体映射到 DTO,这在性能上较低(内存和 CPU 方面)。您可以在 Pixelshaped/flat-mapper-benchmark 中找到此包的基准测试。

您还可以将其用于将 SQL 查询映射到对象,它不依赖于特定的 ORM。

如何使用?

快速浏览

给定一个 DTO,例如 AuthorDTO

$result = $flatMapper->map(AuthorDTO::class, $authorRepository->getAuthorsAndTheirBooks());

将为您提供包含所有 BookDTO 书籍的 AuthorDTO 数组(见 完整示例)。

安装

composer require pixelshaped/flat-mapper-bundle

配置

此插件可以在不进行任何配置的情况下工作,但如果禁用映射验证并自动注入某些缓存服务,则会显示更好的性能。

如果您使用 Symfony,您可以创建一个配置文件来这样做

# config/pixelshaped_flat_mapper.yaml
pixelshaped_flat_mapper:
    validate_mapping: '%kernel.debug%' # disable on prod environment
    cache_service: cache.app

如果您不这样做,您仍然可以在实例化 FlatMapper 时从中受益,即

$flatMapper = (new FlatMapper())
    ->setCacheService($yourCacheService) // PSR-6
    ->setValidateMapping(false)
;

映射预缓存

在第一次调用函数时创建 DTO 的映射。在相同脚本执行期间的后续调用不会重新创建映射。如果配置了缓存服务,映射将在下一次脚本执行中从缓存中加载。

如果您想预先缓存所有 DTO 以避免在热点路径上执行,您可以这样做

$dtoClassNames = [CustomerDTO::class, ...];
foreach($dtoClassNames as $className) {
    $flatMapper->createMapping($className);
}

这应该是可选的。在调用时无论如何都会创建映射信息

$flatMapper->map(CustomerDTO::class, $results);

将映射添加到您的 DTO 中

此插件附带了一些属性,您可以使用它们将映射添加到您的 DTO 中

  • #[Identifier]: 任何 DTO 都必须恰好有一个标识符。此标识符用于内部跟踪 DTO 实例并创建它们一次。您可以使用它
    • 将其用作类属性,如果您不打算使用该属性(示例)。它将仅内部使用,不会映射到您的 DTO。
    • 将其用作属性属性,如果您对其有某些用途(示例)。
    • 直接在属性上指定映射的属性名称(示例)。当用作类属性时,这是必需的。
    • 使用 InboundProperty 属性分别指定映射的属性名称,类似于 Doctrine 风格(示例)。
  • #[Scalar("mapped_property_name")]:您的结果集的 mapped_property_name 列将被映射到 DTO 的标量属性(将使用第一行的值)。如果您的 DTO 的属性名称已经匹配结果集,则此操作是可选的(示例)。
  • #[ReferenceArray(NestedDTO::class)]:将使用NestedDTO中包含的映射信息创建一个NestedDTO数组。
  • #[ScalarArray("mapped_property_name")] 您的结果集的列mapped_property_name将被映射为一个标量属性数组,例如ID(查看示例)。

填充嵌套DTO

给定

使用以下结果集调用FlatMapper

$results = [
    ['author_id' => 1, 'author_name' => 'Alice Brian', 'book_id' => 1, 'book_name' => 'Travelling as a group', 'book_publisher_name' => 'TravelBooks'],
    ['author_id' => 1, 'author_name' => 'Alice Brian', 'book_id' => 2, 'book_name' => 'My journeys', 'book_publisher_name' => 'Lorem Press'],
    ['author_id' => 1, 'author_name' => 'Alice Brian', 'book_id' => 3, 'book_name' => 'Coding on the road', 'book_publisher_name' => 'Ipsum Books'],
    ['author_id' => 2, 'author_name' => 'Bob Schmo', 'book_id' => 1, 'book_name' => 'Travelling as a group', 'book_publisher_name' => 'TravelBooks'],
    ['author_id' => 2, 'author_name' => 'Bob Schmo', 'book_id' => 4, 'book_name' => 'My best recipes', 'book_publisher_name' => 'Cooking and Stuff'],
];

$flatMapper->map(AuthorDTO::class, $results);

将输出

Array
(
    [1] => AuthorDTO Object
        (
            [id] => 1
            [name] => Alice Brian
            [leafs] => Array
                (
                    [1] => BookDTO Object
                        (
                            [id] => 1
                            [name] => "Travelling as a group"
                            [publisherName] => "TravelBooks"
                        )
                    [2] => BookDTO Object
                        (
                            [id] => 2
                            [name] => "My journeys"
                            [publisherName] => "Lorem Press"
                        )
                    [3] => BookDTO Object
                        (
                            [id] => 3
                            [name] => "Coding on the road"
                            [publisherName] => "Ipsum Books"
                        )
                )
        )
    [2] => AuthorDTO Object
        (
            [id] => 2
            [name] => Bob Schmo
            [leafs] => Array
                (
                    [1] => BookDTO Object
                        (
                            [id] => 1
                            [name] => "Travelling as a group"
                            [publisherName] => "TravelBooks"
                        )
                    [4] => BookDTO Object
                        (
                            [id] => 4
                            [name] => "My best recipes"
                            [publisherName] => "Cooking and Stuff"
                        )
                )
        )
)

填充列数组

给定ScalarArrayDTO

使用以下结果集调用FlatMapper

$results = [
    ['object1_id' => 1, 'object1_name' => 'Root 1', 'object2_id' => 1],
    ['object1_id' => 1, 'object1_name' => 'Root 1', 'object2_id' => 2],
    ['object1_id' => 1, 'object1_name' => 'Root 1', 'object2_id' => 3],
    ['object1_id' => 2, 'object1_name' => 'Root 2', 'object2_id' => 1],
    ['object1_id' => 2, 'object1_name' => 'Root 2', 'object2_id' => 4],
];

将输出

Array
(
    [1] => ScalarArrayDTO Object
        (
            [id] => 1
            [name] => Root 1
            [object2s] => Array
                (
                    [0] => 1
                    [1] => 2
                    [2] => 3
                )
        )
    [2] => ScalarArrayDTO Object
        (
            [id] => 2
            [name] => Root 2
            [object2s] => Array
                (
                    [0] => 1
                    [1] => 4
                )
        )
)

使用Doctrine查询

给定以下DTO类

<?php
class CustomerDTO
{
    public function __construct(
        #[Identifier]
        #[Scalar('customer_id')]
        public int $id,
        #[Scalar('customer_name')]
        public string $name,
        #[ScalarArray('shopping_list_id')]
        public array $shoppingListIds
    )
}

查询

<?php
$result = $this->getOrCreateQueryBuilder()
    ->select('customer.id AS customer_id, customer.name AS customer_name, shopping_list.id AS shopping_list_id')
    ->leftJoin('customer.shopping_list', 'shopping_list')
    ->getQuery()->getResult()
    ;

$flatMapper = new \Pixelshaped\FlatMapperBundle\FlatMapper()

$flatMapper->map(CustomerDTO::class, $result);

将返回一个包含CustomerDTO的数组,其中$shoppingListIds属性被填充为相应的ShoppingList ID数组。

使用分页

您仍然可以使用Doctrine对您的DQL查询进行分页

$qb = $customerRepository->createQueryBuilder('customer');
$qb
    ->leftJoin('customer.addresses', 'customer_addresses')
    ->select('customer.id AS customer_id, customer.ref AS customer_ref, customer_addresses.id AS address_id')
    ->setFirstResult(0)
    ->setMaxResults(10)
    ;

$paginator = new Paginator($qb->getQuery(), fetchJoinCollection: true);
$paginator->setUseOutputWalkers(false);

$result = $flatMapper->map(CustomerWithAddressesDTO::class, $paginator);

将得到一个包含10个CustomerWithAddressesDTO的数组(假设您的数据库中有10个)。

在不使用Symfony的情况下使用

您可以在不使用Symfony的情况下使用此包。只需实例化FlatMapper类并使用其方法。

替代方案

Doctrine 提供了一个解决方案,可以直接从QueryBuilder构建DTO。

给定一个DTO类,如CustomerDTO

<?php
class CustomerDTO
{
    public function __construct($name, $email, $city, $value = null){ /* ... */ }
}

Doctrine可以执行一个查询,返回一个array<CustomerDTO>

<?php
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city) FROM Customer c JOIN c.email e JOIN c.address a');
$users = $query->getResult(); // array<CustomerDTO>

遗憾的是,如果您需要检索具有非标量属性(例如)的DTO,则Doctrine提供的解决方案不起作用

  • 一个ID数组
  • 一个嵌套DTO数组

因此,Doctrine提供的解决方案不起作用。创建此包正是出于这种情况。

当我开始编写这段代码时,我寻找替代方案,但只找到了部分替代方案

  • mark-gerarts/automapper-plus 在将对象映射到其他对象(尤其是实体到DTO及其反向映射)方面非常出色,但它无法解决映射非规范化数据(即每行包含多个对象的信息且行之间有大量冗余)到对象的问题。
  • jolicode/automapper 是前一个包的一个很好的替代方案,具有相同的限制。
  • sunrise-php/hydrator 可以将数组映射到对象,但不能映射非规范化数组
  • 其他几个包可以将JSON信息映射到对象。
  • doctrine/orm 使用ResultSetMapping在内部解决这个问题。它可以连接实体,但不能连接DTO,因为没有方法可以声明DTO的映射。这正是为什么Doctrine只处理具有标量属性的DTO的原因。
  • 技术上,您可以使用Doctrine构建PARTIAL对象,但我认为这是一种不好的做法,因为下一个开发者不知道当前的对象是否是一个完整的对象。然后,您可以将它映射到DTO并丢弃它以避免这种情况,但算法的复杂度可能会比我们包中的映射要高(O(n))。

不要犹豫,提出替代方案或做出贡献。