stasa87/auto-mapper-plus

一个PHP的AutoMapper

2.0.2 2018-06-12 10:22 UTC

This package is not auto-updated.

Last update: 2024-09-27 12:27:15 UTC


README

一个由.NET的AutoMapper启发的PHP自动映射器。它可以将数据从一个对象传输到另一个对象,允许自定义映射操作。

Build Status

目录

安装

此库可在 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);

注册映射

映射通过使用AutoMapperConfigregisterMapping()方法定义。在使用之前,必须显式定义每个映射。

映射通过提供源类和目标类来定义。最基本定义如下:

<?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。默认的 MapToMapFromWithMapper 操作都使用这些。

处理嵌套映射

可以使用 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' 属性的值”。

FromPropertyReversible 的,这意味着当您应用 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 对象是一个值对象,包含 AutoMapperConfigMapping 实例的可能选项。

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 的替代方案