eventsauce/object-hydrator

将结构化数据转换为严格的对象。

资助包维护!
frankdejonge

安装: 984,437

依赖项: 21

建议者: 0

安全: 0

星标: 316

关注者: 6

分支: 23

开放性问题: 11

1.5.0 2024-08-17 12:23 UTC

README

安装

composer require eventsauce/object-hydrator

跳转到 使用文档

关于

这个库允许无魔法地将序列化数据转换为对象并返回。与其它对象映射库不同,这个库不依赖于魔法反射来设置私有属性。它手动地填充和序列化对象。填充机制检查构造函数,确定哪些键需要映射到哪些属性。序列化机制检查所有公共属性和getter方法,将对象值转换为普通数据结构。与能够抓取私有属性的“魔法”填充机制不同,这种方法映射对象打开了无需反射的对象映射之门。您获得了所有便利,而没有一丝罪恶感(或性能损失)。

这是一个将结构化请求数据(例如:解码后的JSON)转换为复杂对象结构的实用程序。这个实用程序的目标用途是接收请求数据并将其转换为命令或查询对象。该库设计遵循约定且不验证输入。

何时以及为什么使用它?

这是一个好问题,让我们深入了解。最初,这个库是为了将普通数据(如JSON请求体)映射到严格的对象结构而创建的。使用对象(DTOs、查询和命令对象)是创建易于理解的表达性代码的绝佳方式。对象可以信任它们正确地表示领域中的概念。使用这些对象的缺点是它们可能难以使用。构建和序列化变得重复且编写相同的代码令人厌倦。这个库旨在消除对象填充和序列化的无聊部分。

这个库是针对两个特定的用例构建的

  1. DTO、查询对象和命令对象的构建。
  2. 事件对象的序列化和填充。

由于使用代码生成进行的预解析步骤,对象填充和序列化可以在 成本的情况下实现。

快速链接

设计目标

这个包是根据一些设计目标创建的。它们如下

  • 对象创建不应该太神奇(实例化时不使用反射)
  • 不应有对反射的硬运行时要求
  • 构造的对象应该是从构造时就有效的
  • 支持通过(静态)命名构造函数进行构造

使用

这个库支持对象的填充和序列化。

填充使用

默认情况下,输入通过属性名称进行映射,并且类型需要匹配。默认情况下,键从 snake_case 输入映射到 camelCase 属性。如果您想保留键名,可以使用 KeyFormatterWithoutConversion

use EventSauce\ObjectHydrator\ObjectMapperUsingReflection;

$mapper = new ObjectMapperUsingReflection();

class ExampleCommand
{
    public function __construct(
        public readonly string $name,
        public readonly int $birthYear,
    ) {}
}

$command = $mapper->hydrateObject(
    ExampleCommand::class,
    [
        'name' => 'de Jonge',
        'birth_year' => 1987
    ],
);

$command->name === 'de Jonge';
$command->birthYear === 1987;

复杂对象将自动解析。

class ChildObject
{
    public function __construct(
        public readonly string $value,
    ) {}
}

class ParentObject
{
    public function __construct(
        public readonly string $value,
        public readonly ChildObject $child,
    ) {}
}

$command = $mapper->hydrateObject(
    ParentObject::class,
    [
        'value' => 'parent value',
        'child' => [
            'value' => 'child value',
        ]
    ],
);

简单的文档注释可以确保对象数组自动转换。

class ChildObject
{
    public function __construct(
        public readonly string $value,
    ) {}
}

class ParentObject
{
    /**
     * @param ChildObject[] $list
     */
    public function __construct(
        public readonly array $list,
    ) {}
}

$object = $mapper->hydrateObject(ParentObject::class, [
  'list' => [
    ['value' => 'one'],
    ['value' => 'two'],
  ],
]);

$object->list[0]->value === 'one';
$object->list[1]->value === 'two';

该库支持以下格式

  • @param Type[] $name
  • @param array $name
  • @param array $name
  • @param array $name

自定义映射键

use EventSauce\ObjectHydrator\MapFrom;

class ExampleCommand
{
    public function __construct(
        public readonly string $name,
        #[MapFrom('year')]
        public readonly int $birthYear,
    ) {}
}

从多个键进行映射

您可以通过传递数组来捕获多个输入键的输入。当多个值代表单一代码概念时,这非常有用。数组还允许您重命名键,进一步解耦输入和构建的对象图。

use EventSauce\ObjectHydrator\MapFrom;

class BirthDate
{
    public function __construct(
        public int $year,
        public int $month,
        public int $day
    ){}
}

class ExampleCommand
{
    public function __construct(
        public readonly string $name,
        #[MapFrom(['year_of_birth' => 'year', 'month', 'day'])]
        public readonly BirthDate $birthDate,
    ) {}
}

$mapper->hydrateObject(ExampleCommand::class, [
    'name' => 'Frank',
    'year_of_birth' => 1987,
    'month' => 11,
    'day' => 24,
]);

属性转换

当输入类型和属性类型不兼容时,可以将值强制转换为特定的标量类型。

转换为标量值

use EventSauce\ObjectHydrator\PropertyCasters\CastToType;

class ExampleCommand
{
    public function __construct(
        #[CastToType('integer')]
        public readonly int $number,
    ) {}
}

$command = $mapper->hydrateObject(
    ExampleCommand::class,
    [
        'number' => '1234',
    ],
);

转换为标量值列表

use EventSauce\ObjectHydrator\PropertyCasters\CastListToType;

class ExampleCommand
{
    public function __construct(
        #[CastListToType('integer')]
        public readonly array $numbers,
    ) {}
}

$command = $mapper->hydrateObject(
    ExampleCommand::class,
    [
        'numbers' => ['1234', '2345'],
    ],
);

转换为对象列表

use EventSauce\ObjectHydrator\PropertyCasters\CastListToType;

class Member
{
    public function __construct(
        public readonly string $name,
    ) {}
}

class ExampleCommand
{
    public function __construct(
        #[CastListToType(Member::class)]
        public readonly array $members,
    ) {}
}

$command = $mapper->hydrateObject(
    ExampleCommand::class,
    [
        'members' => [
            ['name' => 'Frank'],
            ['name' => 'Renske'],
        ],
    ],
);

转换为DateTimeImmutable对象

use EventSauce\ObjectHydrator\PropertyCasters\CastToDateTimeImmutable;

class ExampleCommand
{
    public function __construct(
        #[CastToDateTimeImmutable('!Y-m-d')]
        public readonly DateTimeImmutable $birthDate,
    ) {}
}

$command = $mapper->hydrateObject(
    ExampleCommand::class,
    [
        'birthDate' => '1987-11-24',
    ],
);

转换为Uuid对象(ramsey/uuid)

use EventSauce\ObjectHydrator\PropertyCasters\CastToUuid;
use Ramsey\Uuid\UuidInterface;

class ExampleCommand
{
    public function __construct(
        #[CastToUuid]
        public readonly UuidInterface $id,
    ) {}
}

$command = $mapper->hydrateObject(
    ExampleCommand::class,
    [
        'id' => '9f960d77-7c9b-4bfd-9fc4-62d141efc7e5',
    ],
);

每个属性使用多个转换器

通过使用多个转换器,创建丰富的转换组合。

use EventSauce\ObjectHydrator\PropertyCasters\CastToArrayWithKey;
use EventSauce\ObjectHydrator\PropertyCasters\CastToType;
use EventSauce\ObjectHydrator\MapFrom;
use Ramsey\Uuid\UuidInterface;

class ExampleCommand
{
    public function __construct(
        #[CastToType('string')]
        #[CastToArrayWithKey('nested')]
        #[MapFrom('number')]
        public readonly array $stringNumbers,
    ) {}
}

$command = $mapper->hydrateObject(
    ExampleCommand::class,
    [
        'number' => [1234],
    ],
);

$command->stringNumbers === ['nested' => [1234]];

创建自己的属性转换器

您可以为无法遵循默认约定的情况创建自己的属性转换器来处理复杂情况。转换器的常见情况包括 联合类型交集类型

属性转换器让您可以完全控制属性是如何构建的。属性转换器通过属性附加到属性上,实际上,它们 就是 属性。

让我们看看属性转换器的示例

use Attribute;
use EventSauce\ObjectHydrator\ObjectMapper;
use EventSauce\ObjectHydrator\PropertyCaster;

#[Attribute(Attribute::TARGET_PARAMETER)]
class CastToMoney implements PropertyCaster
{
    public function __construct(
        private string $currency
    ) {}

    public function cast(mixed $value, ObjectMapper $mapper) : mixed
    {
        return new Money($value, Currency::fromString($this->currency));
    }
}

// ----------------------------------------------------------------------

#[Attribute(Attribute::TARGET_PARAMETER)]
class CastUnionToType implements PropertyCaster
{
    public function __construct(
        private array $typeToClassMap
    ) {}

    public function cast(mixed $value, ObjectMapper $mapper) : mixed
    {
        assert(is_array($value));

        $type = $value['type'] ?? 'unknown';
        unset($value['type']);
        $className = $this->typeToClassMap[$type] ?? null;

        if ($className === null) {
            throw new LogicException("Unable to map type '$type' to class.");
        }

        return $mapper->hydrateObject($className, $value);
    }
}

您现在可以使用这些作为要填充的对象上的属性

class ExampleCommand
{
    public function __construct(
        #[CastToMoney('EUR')]
        public readonly Money $money,
        #[CastUnionToType(['some' => SomeObject::class, 'other' => OtherObject::class])]
        public readonly SomeObject|OtherObject $money,
    ) {}
}

静态构造函数

支持通过静态构造来构建的对象。使用 Constructor 属性标记静态方法。在这些情况下,属性应放在静态构造函数的参数上,而不是放在 __construct 上。

use EventSauce\ObjectHydrator\Constructor;
use EventSauce\ObjectHydrator\MapFrom;

class ExampleCommand
{
    private function __construct(
        public readonly string $value,
    ) {}

    #[Constructor]
    public static function create(
        #[MapFrom('some_value')]
        string $value
    ): static {
        return new static($value);
    }
}

键格式化

默认情况下,键从 snake_case 输入转换为 camelCase 属性。但是,您可以通过向对象映射构造函数提供的 DefinitionProvider 传递键格式化程序来控制键的映射方式

use EventSauce\ObjectHydrator\DefinitionProvider;
use EventSauce\ObjectHydrator\KeyFormatterWithoutConversion;
use EventSauce\ObjectHydrator\ObjectMapperUsingReflection;

$mapper = new ObjectMapperUsingReflection(
    new DefinitionProvider(
        keyFormatter: new KeyFormatterWithoutConversion(),
    ),
);

此库附带了一个 KeyFormatterWithoutConversion,如果不需要转换,则可以用于。如果您需要自定义转换逻辑,可以实现 KeyFormatter 接口。

序列化用法

默认情况下,此库将公共属性和获取器映射到具有纯数据的 snake_cased 数组。当遇到用户定义的对象时,这些对象将自动转换为纯数据对应物。

class ExampleCommand
{
    public function __construct(
        public readonly string $name,
        public readonly int $birthYear,
    ) {}
}


$command = new ExampleCommand('de Jonge', 1987);
$payload = $mapper->serializeObject($command);

$payload['name'] === 'de Jonge';
$payload['birth_year'] === 1987;

自定义键映射

序列化以对称的方式反转了填充过程中使用的键映射,包括多个键的映射。

use EventSauce\ObjectHydrator\MapFrom;

class BirthDate
{
    public function __construct(
        public int $year,
        public int $month,
        public int $day
    ){}
}

class ExampleCommand
{
    public function __construct(
        #[MapFrom('my_name')]
        public readonly string $name,
        #[MapFrom(['year_of_birth' => 'year', 'month', 'day'])]
        public readonly BirthDate $birthDate,
    ) {}
}

$command = new ExampleCommand(
  'de Jonge',
  new BirthDate(1987, 11, 24)
);

$payload = $mapper->serializeObject($command);

$payload['my_name'] === 'de Jonge';
$payload['year_of_birth'] === 1987;
$payload['month'] === 11;
$payload['day'] === 24;

属性序列化

类似于转换器,可以通过使用 "属性序列化器" 来添加自定义序列化逻辑。属性序列化是自定义注释,它提供了对序列化过程的钩子,允许您在需要时完全控制序列化机制。

use EventSauce\ObjectHydrator\ObjectMapper;
use EventSauce\ObjectHydrator\PropertySerializer;

#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
class UppercaseString implements PropertySerializer
{
    public function serialize(mixed $value, ObjectMapper $hydrator): string
    {
        assert(is_string($value));
        
        return strtoupper($value);
    }
}

class Shout
{
    public function __construct(
        private readonly string $message
    ) {}
    
    #[UppercaseString]
    public function what(): string
    {
        return $this->message();
    }
}

$payload = $mapper->serializeObject(new Shout('Hello, World!');

$payload['what'] === 'HELLO, WORLD!';

对称转换

如果配置一致,填充和序列化可以用来将对象转换为原始数据,然后再将其转换回原始对象。一个类可以实现 PropertyCasterPropertySerializer 接口,以成为对称转换机制。

CastToArrayWithKey 的实现是一个例子

use EventSauce\ObjectHydrator\ObjectMapper;
use EventSauce\ObjectHydrator\PropertyCaster;
use EventSauce\ObjectHydrator\PropertySerializer;

class CastToArrayWithKey implements PropertyCaster, PropertySerializer
{
    public function __construct(private string $key)
    {
    }

    public function cast(mixed $value, ObjectMapper $hydrator): mixed
    {
        return [$this->key => $value];
    }

    public function serialize(mixed $value, ObjectMapper $hydrator): mixed
    {
        if (is_object($value)) {
            $value = $hydrator->serializeObject($value);
        }

        return $value[$this->key] ?? null;
    }
}

重要的是要知道,在发生任何默认转换之前,会触发序列化和填充钩子。如果您想操作序列化或填充的数据,可以填充/序列化内部数据/对象。

⚠️ 转换器和序列化器的顺序

为了使填充和序列化对称(允许双向转换),序列化器调用的顺序对于 提升 属性是相反的。

省略序列化数据

数据可以省略在序列化中。默认情况下,所有公共方法和属性都包括在序列化中。有两种方法可以排除数据,两种方法都使用属性。

可以在类级别指定MapperSettings属性来排除所有公共属性、公共方法或两者(最后一个实际上相当无用)。

use EventSauce\ObjectHydrator\MapperSettings;

#[MapperSettings(serializePublicMethods: false)]
class ClassThatDoesNotSerializePublicMethods
{
    // class body
}

#[MapperSettings(serializePublicProperties: false)]
class ClassThatDoesNotSerializePublicProperties
{
    // class body
}

也可以在由类实现的面接口上指定MapperSettings属性。在这种情况下,将使用第一个可用的MapperSettings

use EventSauce\ObjectHydrator\MapperSettings;

#[MapperSettings(serializePublicMethods: false)]
interface InterfaceThatDoesNotSerializePublicMethods
{
    public function resource(): string;
}

class ClassThatInheritsMapperSettingsFromInterface implements InterfaceThatDoesNotSerializePublicMethods
{
    public function resource(): string
    {
        return 'foo';
    }
}

⚠️ 注意,父类上定义的MapperSettings将不会被使用,以避免混淆并确保与类继承保持一致的性能。

另外,可以通过使用DoNotSerialize属性来排除特定方法和属性以进行序列化。

use EventSauce\ObjectHydrator\DoNotSerialize;

class ClassThatExcludesCertainDataPoints
{
    public function __construct(
        public string $includedProperty,
        #[DoNotSerialize]
        public string $excludedProperty,
    ) {}
    
    public function includedMethod(): string
    {
        return 'included';
    }
    
    #[DoNotSerialize]
    public function excludedMethod(): string
    {
        return 'excluded';
    }
}

优化性能

反射和动态代码路径可能在热路径中引起性能问题。为了移除这种开销,可以生成一个优化的实现。生成的PHP代码执行与基于反射的实现相同的类的水化和序列化。它就像是这个逻辑的预编译器。

可以为已知的一组类生成一个完全优化的映射器。生成器将产生构建整个对象树所需的代码,自动解析它必须映射的嵌套属性。

生成的代码比基于反射的实现快3-10倍。

use EventSauce\ObjectHydrator\ObjectMapper;
use EventSauce\ObjectHydrator\ObjectMapperCodeGenerator;

$dumpedClassNamed = "AcmeCorp\\YourOptimizedMapper";
$dumper = new ObjectMapperCodeGenerator();
$classesToDump = [SomeCommand::class, AnotherCommand::class];

$code = $dumper->dump($classesToDump, $dumpedClassNamed);
file_put_contents('src/AcmeCorp/YourOptimizedMapper.php', $code);

/** @var ObjectMapper $mapper */
$mapper = new AcmeCorp\YourOptimizedMapper();
$someObject = $mapper->hydrateObject(SomeObject::class, $payload);

提示:使用league/construct-finder

您可以使用来自PHP League的构造查找器包来查找给定目录中的所有类。

composer require league/construct-finder
$classesToDump = ConstructFinder::locatedIn($directoryName)->findClassNames();

替代方案

此包并非独一无二,还有几个实现与此包相同、类似或更复杂的实现。