cerbero / dto
数据传输对象 (DTO)
Requires
- php: ^7.1||^8.0
Requires (Dev)
- phpunit/phpunit: >=7.0
- squizlabs/php_codesniffer: ^3.0
README
此包受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
语句中声明类的完全限定名称。
还可以指定原始类型,例如 string
、bool
、int
、array
等。伪类型 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
设置了以下标志:PARTIAL
、IGNORE_UNKNOWN_PROPERTIES
和MUTABLE
。
与标志交互
可以通过调用静态方法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
属性设置或访问,并在存在相关方法时调用它。侦听器方法名称的约定是将事件(set
或 get
)与以驼峰格式表示的监听属性名称连接,例如 setName
或 getIsAdmin
。
侦听器方法返回的值会覆盖实际的属性值。侦听器不仅用于更改值,还用于在读取或设置 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)。有关更多信息,请参阅 许可文件。