pixelshaped / flat-mapper-bundle
一个用于轻松从反规范化数组结果(如数据库查询结果)创建非标量 DTO 的包。
Requires
- php: ^8.2
- symfony/cache-contracts: ^2.5 || ^3.3
- symfony/config: ^5.4 || ^6.0 || ^7.0
- symfony/dependency-injection: ^5.4 || ^6.0 || ^7.0
- symfony/http-kernel: ^5.4 || ^6.0 || ^7.0
Requires (Dev)
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^11.1
README
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 实例并创建它们一次。您可以使用它#[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" ) ) ) )
填充列数组
使用以下结果集调用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))。
不要犹豫,提出替代方案或做出贡献。