cerbero/dto

数据传输对象 (DTO)

资助包维护!
cerbero90

2.2.0 2021-05-18 03:29 UTC

This package is auto-updated.

Last update: 2024-09-09 14:35:00 UTC


README

Author PHP Version Build Status Coverage Status Quality Score Latest Version Software License PSR-12 Total Downloads

此包受Lachlan Krautz的出色data-transfer-object启发。

数据传输对象(DTO)是一个在进程之间携带数据的对象。DTO除了存储、检索、序列化和反序列化其自身数据外,没有其他行为。DTO是简单的对象,不应包含任何业务逻辑,而应用于数据传输。

安装

通过 Composer

composer require cerbero/dto

用法

实例化 DTO

DTO可以通过普通类或通过工厂方法 make() 实例化。参数是可选的,包括要携带的数据和指定DTO如何行为的标志

use const Cerbero\Dto\PARTIAL;

$data = [
    'name' => 'John',
    'address' => [
        'street' => 'King Street',
    ],
];

$dto = new SampleDto($data, PARTIAL);

$dto = SampleDto::make($data, PARTIAL);

在上面的例子中,$data 是一个包含DTO中声明的属性的数组,PARTIAL 是一个标志,允许即使DTO没有设置所有属性也可以实例化(我们将在稍后详细介绍标志)。

$data 数组中的键可以是蛇形或驼峰式,正确的格式会自动检测以匹配DTO属性。

声明属性

可以通过使用文档注释标签在DTO中声明属性

use Cerbero\Dto\Dto;
use Sample\Dtos\AddressDto;

/**
 * A sample user DTO.
 *
 * @property string $name
 * @property bool $isAdmin
 * @property mixed $something
 * @property \DateTime|null $birthday
 * @property UserDto[] $friends
 * @property AddressDto $address
 */
class UserDto extends Dto
{
    //
}

可以使用 @property@property-read,后跟预期的数据类型和所需的属性名称。当期望多个类型时,我们可以使用管道字符 | 分隔它们,例如 \DateTime|null

可以通过在数据类型后添加后缀 [] 声明一系列类型,例如 UserDto[]。重要的是在文档注释或作为 use 语句中声明类的完全限定名称。

还可以指定原始类型,例如 stringboolintarray 等。伪类型 mixed 允许任何类型。

默认值

虽然可以在实例化DTO时设置值,但也可以在DTO类中定义默认值

use Cerbero\Dto\Dto;

/**
 * A sample user DTO.
 *
 * @property string $name
 */
class UserDto extends Dto
{
    protected static $defaultValues = [
        'name' => 'John',
    ];
}

// $user1->name will return: John
$user1 = new UserDto();

// $user2->name will return: Jack
$user2 = new UserDto(['name' => 'Jack']);

请注意,在上面的例子中,默认值会被在DTO创建过程中传递的值覆盖。

与值交互

可以通过几种方式访问DTO属性值,但如果请求的属性未设置,则会抛出 Cerbero\Dto\Exceptions\UnknownDtoPropertyException

// as an object
$user->address->street;

// as an array
$user['address']['street'];

// via dot notation
$user->get('address.street');

// via nested DTO
$user->address->get('street');

要检查属性是否有值,可以调用以下方法

// as an object
isset($user->address->street);

// as an array
isset($user['address']['street']);

// via dot notation
$user->has('address.street');

// via nested DTO
$user->address->has('street');

请注意,上述方法会在属性值设置为NULL时返回FALSE(就像PHP的默认行为一样)。要检查属性是否实际上已设置,我们可以调用 $user->hasProperty('address.street')(我们将在稍后详细介绍属性)。

设置值的后果取决于DTO中设置的标志。默认情况下,DTO是不可变的,因此设置值时将创建一个新的实例。只有设置了 MUTABLE 标志,才能在同一DTO实例中更改值

// throw Cerbero\Dto\Exceptions\ImmutableDtoException if immutable
$user->address->street = 'King Street';

// throw Cerbero\Dto\Exceptions\ImmutableDtoException if immutable
$user['address']['street'] = 'King Street';

// set the new value in the same instance if mutable or in a new instance if immutable
$user->set('address.street', 'King Street');

// set the new value in the same instance if mutable or in a new instance if immutable
$user->address->set('street', 'King Street');

同样适用于取消设置值,但只有 PARTIAL DTO才能取消设置值,否则会抛出 Cerbero\Dto\Exceptions\UnsetDtoPropertyException

// throw Cerbero\Dto\Exceptions\ImmutableDtoException if immutable
unset($user->address->street);

// throw Cerbero\Dto\Exceptions\ImmutableDtoException if immutable
unset($user['address']['street']);

// unset the new value in the same instance if mutable or in a new instance if immutable
$user->unset('address.street');

// unset the new value in the same instance if mutable or in a new instance if immutable
$user->address->unset('street');

可用标志

标志决定了DTO的行为,可以在实例化新的DTO时设置。它们支持位运算,因此我们可以通过PARTIAL | MUTABLE组合多个行为。

NONE

标志Cerbero\Dto\NONE只是一个占位符,不会以任何方式改变DTO的行为。

IGNORE_UNKNOWN_PROPERTIES

标志Cerbero\Dto\IGNORE_UNKNOWN_PROPERTIES允许DTO忽略其属性之外的数据。如果没有提供此标志,当尝试设置未声明的属性时,将抛出Cerbero\Dto\Exceptions\UnknownDtoPropertyException异常。

MUTABLE

标志Cerbero\Dto\MUTABLE允许DTO在不创建新DTO实例的情况下覆盖其属性值,因为DTO默认是不可变的。如果没有提供,当尝试更改属性而不调用set()unset()时,例如$dto->property = 'foo'unset($dto['property']),将抛出Cerbero\Dto\Exceptions\ImmutableDtoException异常。

PARTIAL

标志Cerbero\Dto\PARTIAL允许DTO在没有一些属性的情况下实例化。如果没有提供,当属性缺失或取消设置属性时,将抛出Cerbero\Dto\Exceptions\MissingValueException异常。

CAST_PRIMITIVES

标志Cerbero\Dto\CAST_PRIMITIVES允许DTO在属性值不匹配预期原始类型时进行转换。如果没有提供,当尝试设置错误原始类型的值时,将抛出Cerbero\Dto\Exceptions\UnexpectedValueException异常。

CAMEL_CASE_ARRAY

标志Cerbero\Dto\CAMEL_CASE_ARRAY允许在将DTO转换为数组时保留所有DTO属性的首字母大写驼峰命名。

默认标志

虽然可以在实例化DTO时设置标志,但默认标志也可以在DTO类中定义。

use Cerbero\Dto\Dto;

use const Cerbero\Dto\PARTIAL;
use const Cerbero\Dto\IGNORE_UNKNOWN_PROPERTIES;
use const Cerbero\Dto\MUTABLE;

/**
 * A sample user DTO.
 *
 * @property string $name
 */
class UserDto extends Dto
{
    protected static $defaultFlags = PARTIAL | IGNORE_UNKNOWN_PROPERTIES;
}

// $user->getFlags() will return: PARTIAL | IGNORE_UNKNOWN_PROPERTIES | MUTABLE
$user = UserDto::make($data, MUTABLE);

默认标志与在DTO创建期间传递的标志组合,这意味着在上面的代码中,$user设置了以下标志:PARTIALIGNORE_UNKNOWN_PROPERTIESMUTABLE

与标志交互

可以通过调用静态方法getDefaultFlags()获取DTO中的默认标志,而DTO实例的标志可以通过getFlags()读取。

// PARTIAL | IGNORE_UNKNOWN_PROPERTIES
UserDto::getDefaultFlags();

// PARTIAL | IGNORE_UNKNOWN_PROPERTIES | MUTABLE
$user->getFlags();

要确定DTO是否设置了标志,可以调用hasFlags()

$user->hasFlags(PARTIAL); // true
$user->hasFlags(PARTIAL | MUTABLE); // true
$user->hasFlags(PARTIAL | NULLABLE); // false

可以通过调用方法setFlags()再次设置DTO标志。如果DTO是可变的,则标志针对当前实例设置,否则创建一个新的DTO实例并设置给定的标志。

$user = $user->setFlags(PARTIAL | NULLABLE);

如果我们想向已设置的标志中添加一个或多个标志,可以调用addFlags()。如果DTO是可变的,则标志添加到当前实例,否则添加到新实例。

$user = $user->addFlags(CAMEL_CASE_ARRAY | CAST_PRIMITIVES);

最后,要删除标志,可以调用removeFlags()。如果DTO是可变的,则从当前实例中删除标志,否则从新实例中删除。

$user = $user->removeFlags(IGNORE_UNKNOWN_PROPERTIES | MUTABLE);

请注意,当添加、删除或设置标志并影响DTO值时,属性将被重新映射以应用新标志的影响。

操作属性

除了set()之外,还有其他可以调用以操作DTO属性的方法。方法merge()将DTO的属性与另一个DTO或任何可迭代的对象(例如数组)合并。

$user1 = UserDto::make([
    'name' => 'John',
    'address' => [
        'street' => 'King Street',
    ],
], PARTIAL | IGNORE_UNKNOWN_PROPERTIES);

$user2 = UserDto::make([
    'name' => 'Anna',
    'address' => [
        'unit' => 10,
    ],
], PARTIAL | CAMEL_CASE_ARRAY);

// [
//     'name' => 'Anna',
//     'address' => [
//         'street' => 'King Street',
//         'unit' => 10,
//     ],
// ]
$mergedDto = $user1->merge($user2);

// PARTIAL | IGNORE_UNKNOWN_PROPERTIES | CAMEL_CASE_ARRAY
$mergedDto->getFlags();

在上面的示例中,由于两个DTO是不可变的,合并后将会创建一个新的DTO。如果$user1是可变的,则其自己的属性将改变而不会创建新的DTO实例。请注意,DTO标志也会合并。

为了使DTO只携带一些特定的属性,可以调用only()方法并传递要保留的属性列表。

$result = $user->only(['name', 'address'], CAST_PRIMITIVES);

作为第二个参数传递的任何可选标志将与DTO现有的标志合并。如果DTO是不可变的,则更改将应用于新实例;如果是可变的,则应用于同一实例。

only() 方法还有一个相反的方法,称为 except,它保留所有 DTO 属性,除了排除的属性。

$result = $user->except(['name', 'address'], CAST_PRIMITIVES);

有时我们可能需要快速更改不可变 DTO 的数据。为了在更改过程中保留 DTO 的不可变性,我们可以调用 mutate() 方法。

$user->mutate(function (UserData $user) {
    $user->name = 'Jack';
});

与属性交互

在创建 DTO 的过程中,属性会从提供的数据内部映射。属性映射是一个关联数组,包含属性名称作为键,Cerbero\Dto\DtoProperty 的实例作为值。要检索此类映射(可能用于检查),我们可以调用 getPropertiesMap() 方法。

// ['name' => Cerbero\Dto\DtoProperty, ...]
$map = $user->getPropertiesMap();

还有方法可以检索属性名称、所有 DtoProperty 实例、单个 DtoProperty 实例,以及一个确定属性是否已设置的方法(例如,当属性值为 NULL 时,可用于避免错误否定)。

// ['name', 'isAdmin', ...]
$names = $user->getPropertyNames();

// [Cerbero\Dto\DtoProperty, Cerbero\Dto\DtoProperty, ...]
$properties = $user->getProperties();

// Cerbero\Dto\DtoProperty instance for the property "name"
$nameProperty = $user->getProperty('name');

// TRUE as long as the property "name" is set (even if its value is NULL)
$hasName = $user->hasProperty('name');

转换为数组

如上所示,DTO 可以像数组一样表现,它们的值可以以数组方式设置和检索。DTO 本身是可迭代的,因此可以用于循环。

foreach($dto as $propertyName => $propertyValue) {
    // ...
}

我们可以调用 toArray() 方法来获取 DTO 及其嵌套 DTO 的数组表示形式。默认情况下,结果数组将使用蛇形命名法作为键,除非 DTO 具有具有 CAMEL_CASE_ARRAY 标志。

// [
//     'name' => 'Anna',
//     'is_admin' => true,
//     'address' => [
//         'street' => 'King Street',
//         'unit' => 10,
//     ],
// ]
$user->toArray();

有时我们可能希望在 DTO 转换为数组时对值进行转换。为此,我们可以在 ArrayConverter 中注册值转换器。

use Cerbero\Dto\Manipulators\ArrayConverter;
use Cerbero\Dto\Manipulators\ValueConverter;

class DateTimeConverter implements ValueConverter
{
    public function fromDto($value)
    {
        return $value->format('Y-m-d');
    }

    public function toDto($value)
    {
        return new DateTime($value);
    }
}

ArrayConverter::instance()->setConversions([
    DateTime::class => DateTimeConverter::class,
]);

$user = UserDto::make(['birthday' => '01/01/2000']);
$user->birthday; // instance of DateTime
$user->toArray(); // ['birthday' => '01/01/2000']

请注意,在 ArrayConverter 中注册的转换将应用于所有 DTO,无论何时将它们转换为数组。为了仅对特定 DTO 转换值,请参阅有关 Listener 类的说明。

可以使用 addConversion()removeConversion() 方法添加或删除单个转换。

ArrayConverter::instance()->addConversion(DateTime::class, DateTimeConverter::class);

ArrayConverter::instance()->removeConversion(DateTime::class);

监听事件

每当 DTO 设置或获取其属性值时,侦听器可以截获事件并更改结果。每个 DTO 都可以有一个与侦听器类关联的侦听器进行注册。

use Cerbero\Dto\Manipulators\Listener;

class UserDtoListener
{
    public function setName($value)
    {
        return ucwords($value);
    }

    public function getSomething($value)
    {
        return $value === null ? rand() : $value;
    }
}

Listener::instance()->listen([
    UserDto::class => UserDtoListener::class,
]);

$user = UserDto::make(['name' => 'john doe', 'something' => null]);
$user->name; // John Doe
$user->something; // random integer

在上面的示例中,UserDtoListener 监听每次 UserDto 属性设置或访问,并在存在相关方法时调用它。侦听器方法名称的约定是将事件(setget)与以驼峰格式表示的监听属性名称连接,例如 setNamegetIsAdmin

侦听器方法返回的值会覆盖实际的属性值。侦听器不仅用于更改值,还用于在读取或设置 DTO 属性时执行任意逻辑。

可以使用 addListener()removeListener() 方法添加或删除单个侦听器。

Listener::instance()->addListener(UserDto::class, UserDtoListener::class);

Listener::instance()->removeListener(UserDto::class);

转换为字符串

最后,DTO 可以转换为字符串。当发生这种情况时,返回其 JSON 表示形式。

// {"name":"John Doe"}
(string) $user;

将 DTO 转换为 JSON 的更明确的方法是调用 toJson() 方法,它具有与通过 json_encode() 编码 DTO 相同的效果。

$user->toJson();

json_encode($user);

如果某些 DTO 值在编码为 JSON 时需要特殊转换,则可以在 ArrayConverter 中定义此类转换(有关详细信息,请参阅转换为数组部分)。

变更日志

有关最近更改的更多信息,请参阅 变更日志

测试

$ composer test

贡献

有关详细信息,请参阅 贡献指南行为准则

安全

如果您发现任何安全相关的问题,请通过电子邮件发送给 andrea.marco.sartori@gmail.com 而不是使用问题跟踪器。

鸣谢

许可

MIT 许可证(MIT)。有关更多信息,请参阅 许可文件