stasa87/auto-mapper-plus
一个PHP的AutoMapper
Requires
- php: ^5.6 || ^7.2
- lstrojny/functional-php: ^1.4
Requires (Dev)
- phpunit/phpunit: ^5.7
- symfony/debug: ^3.3
- symfony/var-dumper: ^3.3
README
一个由.NET的AutoMapper启发的PHP自动映射器。它可以将数据从一个对象传输到另一个对象,允许自定义映射操作。
目录
安装
此库可在 packagist 上找到。
$ composer require "mark-gerarts/auto-mapper-plus"
如果你使用的是Symfony,请查看 AutoMapper+ 扩展包。
为什么使用?
当你需要将数据从一个对象传输到另一个对象时,你可能需要编写大量的模板代码。例如,当使用视图模型、CommandBus命令、处理API响应等时。
Automapper+通过自动将属性从对象A传输到对象B来帮助你,包括私有属性。默认情况下,同名属性将被传输。这可以按需覆盖。
示例用法
假设你有一个名为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() { 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) );
MapFromWithMapper
的示例
<?php $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', Operation::mapFromWithMapper($getColorPalette)); // Or another Example using inline function $mapping->forMember('palette', new MapFromWithMapper(function(SimpleXMLElement $XMLElement, AutoMapperInterface $mapper) { /** @var SimpleXMLElement $palette */ $palette = $XMLElement->xpath('/product/specification/palette/colour'); return $mapper->mapMultiple($palette, Color::class); }));
您可以通过实现MappingOperationInterface
来创建自己的操作。查看提供的实现以获取一些灵感。
如果您需要在操作中使用自动映射器,您可以实现 MapperAwareInterface
并使用 MapperAwareTrait
。默认的 MapTo
和 MapFromWithMapper
操作都使用这些。
处理嵌套映射
可以使用 MapTo
操作注册嵌套映射。请注意,子类的映射也必须注册。
MapTo
支持单个实体和集合。
<?php $config->createMapping(Child::class, ChildDto::class); $config->createMapping(Parent::class, ParentDto::class) ->forMember('child', Operation::mapTo(ChildDto::class));
处理对象构造
您可以指定新目标对象的构造方式(如果您使用 mapToObject
,则此内容不相关)。您可以通过注册一个 工厂回调 来实现这一点。此回调将传递源对象。
<?php $config->registerMapping(Source::class, Destination::class) ->beConstructedUsing(function (Source $source): 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();
解析属性名称
除非您定义了特定的值获取方式(例如 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"
将映射到 \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"
使用自定义映射器
这个库试图通过尽可能少的配置来使注册映射变得简单。然而,确实存在一些需要大量自定义代码的映射情况。如果将此代码放入自己的类中,它将看起来更加清晰。另一个需要使用自定义映射器的原因可能是 性能。
因此,可以为映射指定一个自定义映射器类。此映射器必须实现 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);
杂项
- 将
NULL
作为参数传递给map
方法中的源对象将返回NULL
。
类似库
在选择库时,重要的是要查看可用的选项。没有库是完美的,它们都有自己的优点和缺点。
PHP 还存在一些其他对象映射器。它们在这里列出并附带简短描述,绝对值得检查!
- Nylle/PHP-AutoMapper:
- 只映射公共属性
- 需要满足一些约定
- 对类型做了一些有趣的处理
- Papper:
- 基于约定的
- 高性能
- 文档不足
- BCCAutoMapperBundle:
- 仅作为 Symfony 扩展包可用 (<3.0)
- 与本项目非常相似
- 在图映射方面做一些有趣的事情
性能基准(归功于 idr0id)
运行时:PHP 7.1.8-1
主机:Linux 4.13.0-1-amd64 #1 SMP Debian 4.13.4-2 (2017-10-15) x86_64
集合大小:10000
最新的基准测试可以在 这里 找到。
另请参阅
路线图
- 提供更详细的教程
- 创建一个展示自动映射器的示例应用程序
- 允许从
stdClass
映射, - 或者甚至是一个关联数组(可能是这样的)
- 允许映射到
stdClass
- 提供复制映射的选项
- 允许设置名称解析器的前缀(见 automapper)
- 创建从属性复制值的操作
- 允许传递构造函数
- 允许在 AutoMapperConfig 中配置选项 -> 在尝试使用已注册的映射时出错
- 考虑:将选项传递给单个映射操作
- MapTo: 允许映射集合
- 清理 Mapping::forMember() 方法中的属性检查。
- 重构测试
- 允许设置最大深度,请参阅 #14
- 提供一个 NameResolver,它接受一个数组映射,作为多个
FromProperty
的替代方案