mark-gerarts / auto-mapper-plus
PHP 的 AutoMapper
Requires
- php: >=7.1.0
Requires (Dev)
- phpunit/phpunit: ^6.4
- symfony/debug: ^3.3
- symfony/var-dumper: ^3.3
This package is auto-updated.
Last update: 2024-08-29 04:48:25 UTC
README
受 .NET 的 automapper 启发的 PHP AutoMapper。可以在对象之间转移数据,允许自定义映射操作。
目录
安装
此库可在 packagist 上获取
$ composer require "mark-gerarts/auto-mapper-plus"
如果您使用的是 Symfony,请查看 AutoMapper+ 包。
为什么?
当您需要将数据从一个对象转移到另一个对象时,您可能需要编写大量模板代码。例如,当使用视图模型、CommandBus 命令、处理 API 响应等时。
Automapper+ 通过自动从对象之间转移属性来帮助您,包括私有属性。默认情况下,同名属性将被转移。这可以按需覆盖。
示例用法
假设您有一个名为 Employee 的类和一个关联的 DTO。
<?php class Employee { private $id; private $firstName; private $lastName; private $birthYear; public function __construct($id, $firstName, $lastName, $birthYear) { $this->id = $id; $this->firstName = $firstName; $this->lastName = $lastName; $this->birthYear = $birthYear; } public function getId() { return $this->id; } // And so on... } class EmployeeDto { // While the properties are public for this example, we can map to private // or protected properties just the same. public $firstName; public $lastName; public $age; }
以下代码片段提供了如何配置和使用映射器的一个快速概述
<?php use AutoMapperPlus\Configuration\AutoMapperConfig; use AutoMapperPlus\AutoMapper; $config = new AutoMapperConfig(); // Simply registering the mapping is enough to convert properties with the same // name. Custom actions can be registered for each individual property. $config ->registerMapping(Employee::class, EmployeeDto::class) ->forMember('age', function (Employee $source) { return date('Y') - $source->getBirthYear(); }) ->reverseMap(); // Register the reverse mapping as well. $mapper = new AutoMapper($config); // With this configuration we can start converting our objects. $john = new Employee(10, "John", "Doe", 1980); $dto = $mapper->map($john, EmployeeDto::class); echo $dto->firstName; // => "John" echo $dto->lastName; // => "Doe" echo $dto->age; // => 37
深入了解
实例化 AutoMapper
AutoMapper 需要一个 AutoMapperConfig
,它包含已注册的映射。这可以通过两种方式完成
将其传递给构造函数
<?php use AutoMapperPlus\Configuration\AutoMapperConfig; use AutoMapperPlus\AutoMapper; $config = new AutoMapperConfig(); $config->registerMapping(Source::class, Destination::class); $mapper = new AutoMapper($config);
使用静态构造函数
<?php $mapper = AutoMapper::initialize(function (AutoMapperConfig $config) { $config->registerMapping(Source::class, Destination::class); $config->registerMapping(AnotherSource::class, Destination::class); // ... });
使用 AutoMapper
一旦配置完成,使用 AutoMapper 就很简单了
<?php $john = new Employee("John", "Doe", 1980); // Map the source object to a new instance of the destination class. $mapper->map($john, EmployeeDto::class); // Mapping to an existing object is possible as well. $mapper->mapToObject($john, new EmployeeDto()); // Map a collection using mapMultiple $mapper->mapMultiple($employees, EmployeeDto::class);
注册映射
映射是通过使用 AutoMapperConfig
的 registerMapping()
方法定义的。默认情况下,您必须显式定义每个映射才能使用它。
映射是通过提供源类和目标类来定义的。最基本的定义如下
<?php $config->registerMapping(Employee::class, EmployeeDto::class);
这将允许 Employee
类的对象映射到 EmployeeDto
实例。由于没有提供额外配置,映射将仅转移具有相同名称的属性。
自定义回调
使用 forMember()
方法,您可以指定目标类给定属性应该发生什么。当您向此方法传递回调时,返回值将用于设置属性。
回调接收源对象作为参数。
<?php $config->registerMapping(Employee::class, EmployeeDto::class) ->forMember('fullName', function (Employee $source) { return $source->getFirstName() . ' ' . $source->getLastName(); });
操作
在幕后,前一个示例中的回调被包装在一个 MapFrom
操作中。操作代表针对给定属性应执行的操作。
以下操作提供
您可以使用它们与相同的 forMember()
方法。可以使用 Operation
类来提高清晰度。
<?php $getName = function ($source, AutoMapperInterface $mapper) { return 'John'; }; $mapping->forMember('name', $getName); // The above is a shortcut for the following: $mapping->forMember('name', Operation::mapFrom($getName)); // Which in turn is equivalent to: $mapping->forMember('name', new MapFrom($getName)); // Other examples: // Ignore this property. $mapping->forMember('id', Operation::ignore()); // Map this property to the given class. $mapping->forMember('employee', Operation::mapTo(EmployeeDto::class)); // Explicitly state what the property name is of the source object. $mapping->forMember('name', Operation::fromProperty('unconventially_named_property')); // The `FromProperty` operation can be chained with `MapTo`, allowing a // differently named property to be mapped to a class. $mapping->forMember( 'address', Operation::fromProperty('adres')->mapTo(Address::class) ); // SetTo sets the property to the given value. $mapping->forMember('type', Operation::setTo('employee')); // An extended example showing you can access the mapper in `MapFrom`. $getColorPalette = function(SimpleXMLElement $XMLElement, AutoMapperInterface $mapper) { /** @var SimpleXMLElement $palette */ $palette = $XMLElement->xpath('/product/specification/palette/colour'); return $mapper->mapMultiple($palette, Color::class); }; $mapping->forMember('palette', $getColorPalette);
MapTo
需要一些额外的说明。由于列表和映射在 PHP 中是相同的数据结构(数组),我们无法可靠地区分这两者。因此,MapTo
接受第二个参数,$sourceIsObjectArray
,这是一个布尔值,表示源值应该被解释为集合,还是表示对象的关联数组。默认情况下,我们假设是一个集合或单个非数组值。
<?php // This assumes address is an object, or a collection of mappable // objects if the source is an array/iterable. $mapping->forMember('address', Operation::mapTo(Address::class)); // This is equivalent to: $mapping->forMember('address', Operation::mapTo(Address::class, false)); // If you want to be very specific about the source being a collection, you // can use `mapCollectionTo`. This is purely syntactic sugar; it is equivalent // to the declarations above as well. $mapping->forMember('addresses', Operation::mapCollectionTo(Address::class)); // On the other hand, if the source is an array that represents an object, you // can use the following: $mapping->forMember('address', Operation::mapTo(Address::class, true)); // Or nicer $mapping->forMember('address', Operation::mapArrayTo(Address::class));
你可以通过实现 MappingOperationInterface
来创建自己的操作。看看提供的实现以获得一些灵感:实现示例。
如果你需要在操作中使用自动映射,你可以实现 MapperAwareInterface
,并使用 MapperAwareTrait
。默认的 MapTo
和 MapFrom
操作都使用这些。
处理多态属性
有时你有一些属性包含不同类型的对象列表,例如当使用 单表继承 加载所有实体时。你可以使用 MapToAnyOf
将每个对象映射到可能的不同对象。请注意,子类的映射也必须注册。源属性可以是单个值或集合。
<?php // This iterates over every property of the source property // 'polymorphicChildren'. Each value will be mapped to the first existing // mapping from the value to one of the given classes. $config->createMapping(ChildA::class, ChildADto::class); $config->createMapping(ChildB::class, ChildBDto::class); $config->createMapping(Parent::class, ParentDto::class) ->forMember( 'polymorphicChildren', Operation::mapToAnyOf([ChildADto::class, ChildBDto::class] ));
处理嵌套映射
可以使用 MapTo
操作注册嵌套映射。请注意,子类的映射也必须注册。
MapTo
支持单个实体和集合。
<?php $config->registerMapping(Child::class, ChildDto::class); $config->registerMapping(Parent::class, ParentDto::class) ->forMember('child', Operation::mapTo(ChildDto::class));
处理对象构造
你可以指定新目标对象的构造方式(如果你使用 mapToObject
,则这不相关)。你可以通过注册一个 工厂回调 来做到这一点。这个回调将传递源对象和 AutoMapper 的实例。
<?php $config->registerMapping(Source::class, Destination::class) ->beConstructedUsing(function (Source $source, AutoMapperInterface $mapper): Destination { return new Destination($source->getProperty()); });
另一个选项是完全跳过构造函数。这可以通过选项来设置。
<?php // Either set it in the options: $config->getOptions()->skipConstructor(); $mapper = new AutoMapper($config); // Or set it on the mapping directly: $config->registerMapping(Source::class, Destination::class)->skipConstructor();
ReverseMap
由于双向映射是一个常见用例,因此提供了 reverseMap()
方法。这将在相反方向创建一个新的映射。
reverseMap
将考虑已注册的命名约定,如果有的话。
<?php // reverseMap() returns the new mapping, allowing to continue configuring the // new mapping. $config->registerMapping(Employee::class, EmployeeDto::class) ->reverseMap() ->forMember('id', Operation::ignore()); $config->hasMappingFor(Employee::class, EmployeeDto::class); // => True $config->hasMappingFor(EmployeeDto::class, Employee::class); // => True
注意:reverseMap()
简单地使用默认选项在反向方向创建了一个全新的映射。然而,你使用 forMember
定义的每个实现 Reversible
接口的操作,也会在新的映射中定义。目前,只有 fromProperty
支持反转。
为了更清晰地说明,请看以下示例
<?php // Source class properties: Destination class properties: // - 'some_property', - 'some_property' // - 'some_alternative_property' - 'some_other_property' // - 'the_last_property' - 'the_last_property' // $config->registerMapping(Source::class, Destination::class) ->forMember('some_property', Operation::ignore()) ->forMember('some_other_property', Operation::fromProperty('some_alternative_property')) ->reverseMap(); // When mapping from Source to Destination, the following will happen: // - some_property gets ignored // - some_other_property gets mapped by using the value form some_alternative_property // - the_last_property gets mapped because the names are equal. // // Now, when we go in the reverse direction things are different: // - some_property gets mapped, because Ignore is not reversible // - some_alternative_property gets mapped because FromProperty is reversible // - the_last_property gets mapped as well
复制映射
当定义不同的视图模型时,可能会出现很多相似的属性。例如,ListViewModel 和 DetailViewModel。这意味着映射配置也会相似。
因此,可以复制映射。在实践中,这意味着将复制所有选项以及所有显式定义的映射操作。
在复制映射后,你可以自由地覆盖新映射上的操作或选项。
<?php $detailMapping = $config->registerMapping(Employee::class, EmployeeDetailView::class) // Define operations and options ... ->forMember('age', function () { return 20; }); // You can copy a mapping by passing source and destination class. This will // search the config for the relevant mapping. $listMapping = $config->registerMapping(Employee::class, EmployeeListView::class) ->copyFrom(Employee::class, EmployeeDetailView::class) // Alternatively, copy a mapping by passing it directly. // ->copyFromMapping($detailMapping) // // You can now go ahead and define new operations, or override existing // ones. ->forMember('name', Operation::ignore()) ->skipConstructor();
自动创建映射
当你处理不需要任何配置的非常简单的映射时,为每个映射注册映射可能相当繁琐。对于这些情况,可以启用映射的自动创建。
<?php $config->getOptions()->createUnregisteredMappings();
使用此配置,映射器将动态生成一个非常基本的映射,而不是在映射未配置时抛出异常。
解析属性名称
除非你定义了获取值的具体方式(例如 mapFrom
),否则映射器必须有一种方式知道映射哪个源属性。默认情况下,它将尝试在同名属性之间传输数据。然而,有几个方法可以改变这种行为。
如果源属性被特别定义(例如,FromProperty
),则将在所有情况下使用它。
命名约定
您可以指定源类和目标类遵循的命名约定。映射器在解析名称时将考虑这些约定。
例如
<?php use AutoMapperPlus\NameConverter\NamingConvention\CamelCaseNamingConvention; use AutoMapperPlus\NameConverter\NamingConvention\SnakeCaseNamingConvention; $config->registerMapping(CamelCaseSource::class, SnakeCaseDestination::class) ->withNamingConventions( new CamelCaseNamingConvention(), // The naming convention of the source class. new SnakeCaseNamingConvention() // The naming convention of the destination class. ); $source = new CamelCaseSource(); $source->propertyName = 'camel'; $result = $mapper->map($source, SnakeCaseDestination::class); echo $result->property_name; // => "camel"
以下提供了以下约定(更多即将到来)
- CamelCaseNamingConvention
- PascalCaseNamingConvention
- SnakeCaseNamingConvention
您可以使用 NamingConventionInterface
实现自己的命名约定。
显式指定源属性
如前所述,操作 FromProperty
允许您明确指定源对象应使用哪个属性。
<?php $config->registerMapping(Source::class, Destination::class) ->forMember('id', Operation::fromProperty('identifier'));
您应该这样读取前面的代码片段:“对于目标对象上名为 'id' 的属性,使用源对象 'identifier' 属性的值”。
FromProperty
是 Reversible
,这意味着当您应用 reverseMap()
时,AutoMapper 将知道如何映射这两个属性。有关更多信息,请参阅reverseMap
的部分。
使用回调解析名称
如果命名约定和明确指定属性名称不足以解决问题,您可以求助于 CallbackNameResolver
(或实现自己的 NameResolverInterface
)。
此 CallbackNameResolver
接收一个回调作为参数,并使用该回调来转换属性名称。
<?php class Uppercase { public $IMAPROPERTY; } class Lowercase { public $imaproperty; } $uppercaseResolver = new CallbackNameResolver(function ($targetProperty) { return strtolower($targetProperty); }); $config->registerMapping(Uppercase::class; Lowercase::class) ->withNameResolver($uppercaseResolver); $uc = new Uppercase(); $uc->IMAPROPERTY = 'value'; $lc = $mapper->map($uc, Lowercase::class); echo $lc->imaproperty; // => "value"
Options 对象
Options
对象是一个值对象,包含 AutoMapperConfig
和 Mapping
实例的可能选项。
为 AutoMapperConfig
设置的 Options
将作为您注册的每个 Mapping
的默认选项。这些选项可以针对每个映射进行覆盖。
例如
<?php $config = new AutoMapperConfig(); $config->getOptions()->setDefaultMappingOperation(Operation::ignore()); $defaultMapping = $config->registerMapping(Source::class, Destination::class); $overriddenMapping = $config->registerMapping(AnotherSource::class, Destination::class) ->withDefaultOperation(new DefaultMappingOperation()); $defaultMapping->getOptions()->getDefaultMappingOperation(); // => Ignore $overriddenMapping->getOptions()->getDefaultMappingOperation(); // => DefaultMappingOperation
可以设置的可用选项如下
设置选项
对于 AutoMapperConfig
您可以通过检索对象来设置 AutoMapperConfig
的选项
<?php $config = new AutoMapperConfig(); $config->getOptions()->dontSkipConstructor();
或者,您可以通过向构造函数提供一个回调来设置选项。该回调将传递一个默认 Options
的实例
<?php // This will set the options for this specific mapping. $config = new AutoMapperConfig(function (Options $options) { $options->dontSkipConstructor(); $options->setDefaultMappingOperation(Operation::ignore()); // ... });
对于映射
映射还具有可用的 getOptions
方法。但是,存在可链的辅助方法,可以更方便地覆盖选项
<?php $config->registerMapping(Source::class, Destination::class) ->skipConstructor() ->withDefaultOperation(Operation::ignore());
映射也提供了通过可调用设置的选项,使用 setDefaults()
方法
<?php $config->registerMapping(Source::class, Destination::class) ->setDefaults(function (Options $options) { $options->dontSkipConstructor(); // ... });
与 stdClass 映射
作为一个附加说明,值得指出的是,可以从和到 stdClass
进行映射。从 stdClass
映射的方式与预期相同,将属性复制到新对象中。
<?php // Register the mapping. $config->registerMapping(\stdClass::class, Employee::class); $mapper = new AutoMapper($config); $employee = new \stdClass(); $employee->firstName = 'John'; $employee->lastName = 'Doe'; $result = $mapper->map($employee, Employee::class); echo $result->firstName; // => "John" echo $result->lastName; // => "Doe"
将 to \stdClass
需要一些解释。提供的源对象上所有可用的属性都将作为公共属性复制到 \stdClass
中。仍然可以定义针对单个属性的操作(例如,忽略属性)。
<?php // Operations can still be registered. $config->registerMapping(Employee::class, \stdClass::class) ->forMember('id', Operation::ignore()); $mapper = new AutoMapper($config); $employee = new Employee(5, 'John', 'Doe', 1978); $result = $mapper->map($employee, \stdClass::class); echo $result->firstName; // => "John" echo $result->lastName; // => "Doe" var_dump(isset($result->id)); // => bool(false)
命名约定将被考虑,因此在定义操作时请记住这一点。属性名称必须与目标的一致。
<?php $config->registerMapping(CamelCaseSource::class, \stdClass::class) ->withNamingConventions( new CamelCaseNamingConvention(), new SnakeCaseNamingConvention() ) // Operations have to be defined using the target property name. ->forMember('some_property', function () { return 'new value'; }); $mapper = new AutoMapper($config); $source = new CamelCaseSource(); $source->someProperty = 'original value'; $source->anotherProperty = 'Another value'; $result = $mapper->map($employee, \stdClass::class); var_dump(isset($result->someProperty)); // => bool(false) echo $result->some_property; // => "new value" echo $result->another_property; // => "Another value"
对象创建的概念
如此问题中建议和解释的那样,AutoMapper+ 使用 对象箱 来允许映射到 \stdClass
。这意味着您也可以注册自己的类作为对象箱。这使得映射器将其处理为 \stdClass
,将所有源属性写入目标上的公共属性。
可以使用 Options
进行注册对象箱。
<?php class YourObjectCrate { } $config = new AutoMapperConfig(); // (Or pass a callable to the constructor) $config->getOptions()->registerObjectCrate(YourObjectCrate::class); $config->registerMapping(Employee::class, YourObjectCrate::class); $mapper = new AutoMapper($config); $employee = new Employee(5, 'John', 'Doe', 1978); $result = $mapper->map($employee, YourObjectCrate::class); echo $result->firstName; // => "John" echo $result->lastName; // => "Doe" echo get_class($result); // => "YourObjectCrate"
与数组映射
将关联数组映射到对象是可能的(向 @slava-v 致敬)。这可以像声明常规映射一样完成
<?php $config->registerMapping('array', Employee::class); // Alternatively, use the enum DataType::ARRAY // Adding operations works just as you would expect. $config->registerMapping(DataType::ARRAY, Employee::class) ->forMember('id', Operation::ignore()) ->forMember('type', Operation::setTo('employee')) // Since arrays are oftentimes snake_case'd. ->withNamingConventions( new SnakeCaseNamingConvention(), new CamelCaseNamingConvention() ); // It is now possible to map an array to an employee: $employee = [ 'id' => 5, 'first_name' => 'John', 'last_name' => 'Doe' ]; $result = $mapper->map($employee, Employee::class); echo $result->firstName; // => "John" echo $result->id; // => null echo $result->type; // => "employee"
有关此操作与数组结合使用的细节,请参阅 操作 下的 MapTo
部分。
到目前为止,尚不支持将映射 到 数组。虽然这相对容易实现,但它将引入破坏性的变化。它是版本 2.x 的一部分,因此如果需要此功能,请检查那里。
使用自定义映射器
这个库试图让注册映射变得尽可能简单,配置尽可能少。然而,确实存在一些情况,映射需要大量的自定义代码。如果将这些代码放入自己的类中,将使代码看起来更干净。另一个需要自定义映射器的原因是性能。
因此,可以为映射指定自定义映射器类。这个映射器必须实现MapperInterface
接口。为了您的方便,已经提供了一个实现了该接口的CustomMapper
类。
<?php // You can either extend the CustomMapper, or just implement the MapperInterface // directly. class EmployeeMapper extends CustomMapper { /** * @param Employee $source * @param EmployeeDto $destination * @return EmployeeDto */ public function mapToObject($source, $destination) { $destination->id = $source->getId(); $destination->firstName = $source->getFirstName(); $destination->lastName = $source->getLastName(); $destination->age = date('Y') - $source->getBirthYear(); return $destination; } } $config->registerMapping(Employee::class, EmployeeDto::class) ->useCustomMapper(new EmployeeMapper()); $mapper = new AutoMapper($config); // The AutoMapper can now be used as usual, but your custom mapper class will be // called to do the actual mapping. $employee = new Employee(10, 'John', 'Doe', 1980); $result = $mapper->map($employee, EmployeeDto::class);
添加上下文
有时映射的行为应根据上下文而有所不同。因此,可以将第三个参数传递给映射方法,以描述当前上下文。MapFrom
和MapTo
操作都可以利用这个上下文来改变它们的行为。
上下文参数是一个可以包含任何任意值的数组。请注意,这个参数还不是AutoMapperInterface
的一部分,因为它会破坏向后兼容性。它将在下一个主要版本中添加。
<?php // This example shows how for example the current locale can be passed to alter // the mapping behaviour. $config->registerMapping(Employee::class, EmployeeDto::class) ->forMember( 'honorific', function ($source, AutoMapperInterface $mapper, array $context): string { $translationKey = "honorific.{$source->getGender()}"; return $this->translator->trans($translationKey, $context['locale']); } ); // Usage: $mapper->map($employee, EmployeeDto::class, ['locale' => $request->getLocale()]);
当使用mapToObject
方法时,上下文默认包含目标对象。可以通过$context[AutoMapper::DESTINATION_CONTEXT]
访问它。这在需要从目标对象获取数据来填充映射的对象的情况下非常有用。
当实现自定义构造函数时,上下文默认包含目标类。可以通过$context[AutoMapper::DESTINATION_CLASS_CONTEXT]
访问它。
当映射对象图时,上下文还将包含属性名称路径、祖先源对象和祖先目标对象的数组。这些数组可以通过$context[AutoMapper::PROPERTY_STACK_CONTEXT]
、$context[AutoMapper::SOURCE_STACK_CONTEXT]
和$context[AutoMapper::DESTINATION_STACK_CONTEXT]
访问。它们可以用来根据层次级别和对象图中的当前位置实现自定义映射函数。
杂项
- 将
NULL
作为源对象传递给map
方法将返回NULL
。
类似库
选择库时,重要的是要看到有哪些选项可用。没有库是完美的,它们都有它们的优点和缺点。
PHP还有一些其他对象映射器。这里列出了它们的简要描述,并且绝对值得一看!
- Jane automapper:
- 相似的API
- 编译映射,实现接近原生性能
- Nylle/PHP-AutoMapper:
- 仅映射公共属性
- 需要满足一些约定
- 对类型做一些有趣的操作
- Papper:
- 基于约定的
- 高性能
- 缺乏文档
- BCCAutoMapperBundle:
- 仅作为Symfony bundle可用 (<3.0)
- 与本项目非常相似
- 在图映射方面做一些酷的事情
性能基准(归功于 idr0id)
运行时:PHP 7.2.9-1
主机:Linux 4.18.0-2-amd64 #1 SMP Debian 4.18.10-2 (2018-11-02) x86_64
收集大小:100000
最新的基准测试可以在这里找到。
请注意,使用自定义映射器非常快。所以当性能在您的应用程序中变得真正重要时,您可以在需要的地方轻松地实现自定义映射器,而无需更改使用映射器的代码。
另请参阅
关于 PHPStan 的说明
由于这里描述的问题,如果使用$context
参数,PHPStan会报告以下错误
Method AutoMapperPlus\MapperInterface::map() invoked with 3 parameters, 2 required.
如果您看到此错误,您应该启用AutoMapper+ 扩展。请注意,这是一个临时解决方案。问题将在2.0版本中修复。
路线图
- 提供更详细的教程
- 创建一个演示自动映射的示例应用程序
- 允许从
stdClass
映射 - 或者甚至是一个关联数组(可能也有)
- 允许映射到
stdClass
- 提供复制映射的选项
- 允许设置名称解析器的前缀(见automapper)
- 创建从属性复制值的操作
- 允许传递构造函数
- 允许在AutoMapperConfig中配置选项 -> 尝试使用已注册的映射时出错
- 考虑:将选项传递给单个映射操作
- MapTo: 允许映射集合
- 清理Mapping::forMember()方法中的属性检查。
- 重构测试
- 允许设置最大深度,见#14
- 提供接受数组映射的NameResolver,作为多个
FromProperty
的替代 - 使用装饰过的Symfony的
PropertyAccessor
(见#16) - 允许向映射器添加中间件
- 允许映射到数组
2.0版本正在开发中,请检查那里以获取新功能