fab2s / dt0
Dt0是一个DTO PHP实现,它既可以确保可变性,也可以方便地在各种格式中控制输入和输出
Requires
- php: ^8.1
- fab2s/context-exception: ^2.0|^3.0
Requires (Dev)
- fab2s/math: ^2.0
- laravel/pint: ^1.10
- nesbot/carbon: ^2.62|^3.3
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
Suggests
- fab2s/Math: v2.x To cast any Dt0 property in abitrary base ten decimals
- fab2s/laravel-dt0: To use Dt0 in Laravel (the awesome) with full validation and attribute casting
- nesbot/carbon: To use CarbonCaster and handle both Carbon and CarbonImmutable casts
This package is auto-updated.
Last update: 2024-08-29 19:53:45 UTC
README
Dt0
(即DeeTO或DeTZerO)是一个Data-Transfer-Object(DTO)PHP实现,它既可以确保可变性,也可以方便地在各种格式中控制输入和输出。
任何扩展Dt0
的类都将具有其所有公共属性,包括readonly
属性,可以从所有支持的格式中填充:数组、JSON字符串和实例。
背后的逻辑在每个进程运行一次以实现更快的重用(单反射和属性逻辑编译)。
当Dt0
填充readonly
属性时,它会实现完全不可变性。作为最佳实践,所有Dt0
的公共接口都应该只使用public readonly
属性。
为什么还需要另一个DTO包
显然,已经有很多DTO包可用,其中一些非常好。但到目前为止,还没有任何一个实现了完全不可变性。
可变的DTO,带有writeable public properties
,似乎并没有达到提供信任,即没有发生意外的属性更新,也没有随之而来的安心。
当您需要以任何方式更新DTO时,似乎应该通过设计来促进一些思考,而不是仅仅允许它在实现中看似“似乎”可以接受的方式。
有些人可能会争辩说,没有人可以阻止Dt0与新的实例交换,但既然当需要的时候你可以跟踪对象ID,实际上你可以实现完整性,这在其他解决方案中是不可能的。
如果需要更多的保险,你可以轻松地添加一个public readonly property
来存储基于输入值的加密哈希,以在每个Dt0
上签名,并使用它来确保没有发生任何错误。
Laravel
Laravel用户可能会喜欢Laravel Dt0,它为Dt0
添加了正确的支持,包括Dt0验证和模型属性转换。
安装
可以使用composer安装Dt0
composer require "fab2s/dt0"
完成后,你可以开始玩耍
use fab2s\Dt0\Dt0; // works if all public props have defaults $dt0 = new SomeDt0; // set at least props without default $dt0 = new SomeDt0(readOnlyProp: $someValue /*, ... */); // <= argument order does not matter // unless SomeDt0 has a constructor // same as $dt0 = SomeDt0::make(readOnlyProp: $someValue /*, ... */); // <= argument order never matter $value = $dt0->readOnlyProp; // $someValue /** @var array|string|SomeDt0|Dt0|null|mixed $wannaBeDt0 */ $dt0 = SomeDt0::tryFrom($wannaBeDt0); // return null when nothing works /** @var Dt0 $dt0 */ $dto = SomeDt0::from($wannaBeDt0); // throws a Dt0Exception when nothing matched or more Throwable when something is too wrong // keeps objects as such $array = $dt0->toArray(); // toArray with call to jsonSerialize on implementing members $jsonArray = $dt0->toJsonArray(); // same as $jsonArray = $dt0->jsonSerialize(); // toJson $json = $dt0->toJson(); // same as $json = json_encode($dt0); // will work if Dt0 has consistent in/out casters // that is if caster out type is valid for input $fromJson = SomeDt0::fromJson($json); // same as $fromJson = SomeDt0::fromString($json); $fromJson->equals($dt0); // true // always true $dto->equals(SomeDt0::fromArray($dto->toArray())); // serializable $serialized = serialize($dt0); $unserialized = unserialize($serialized); $unserialized->equal($dt0); // true // Immutability with ... $anotherInstance = $dto->clone(); $anotherInstance->equals($dto); // true // ... updates :o $updated = $dto->update(readOnlyProp: $anotherValue); // or $updated = $dto->update(...['readOnlyProp' => $anotherValue]); $updated->->equals($dto); // false $updated->readOnlyProp; // $anotherValue
转换
Dt0
附带两个Attributes
来执行转换:Casts
和Cast
Cast
用于定义如何将属性作为属性属性处理,而Casts
用于将多个Cast
作为类属性一次性设置。
Casts可以通过两种方式添加
-
使用
Casts
类属性use fab2s\Dt0\Attribute\Casts; use fab2s\Dt0\Attribute\Cast; use fab2s\Dt0\Dt0; #[Casts( new Cast(default: 'defaultFromCast', propName: 'prop1'), // same as prop1: new Cast(default: 'defaultFromCast'), // ... )] class MyDt0 extends Dt0 { public readonly string $prop1; }
-
使用
Cast
属性属性use fab2s\Dt0\Attribute\Casts; use fab2s\Dt0\Attribute\Cast; use fab2s\Dt0\Dt0; class MyDt0 extends Dt0 { #[Cast(default: 'defaultFromCast')] public readonly string $prop1; }
上述两种方法的组合是允许的,如DefaultDt0
所示。
在冗余的情况下,优先级将首先在
Casts
中,然后在Cast
中。Dt0对定义Casts的方法没有意见。它们都将编译一次,并准备好供任何重用,所以都会执行相同的操作。
可用的Casters
Dt0
附带了一些Casters,可以直接使用。编写自己的Casters与实现CasterInterface
一样简单。
它们在Casters文档中有记录。
用法
Dt0
对Caster
没有要求,就绪地支持所有枚举,包括UnitEnum。
Dt0
对它的继承者也很清楚,无需任何转换。当属性类型不够具体时,您可以在某些情况下使用Dt0Caster
(阅读目标Dt0
类)。
Dt0
支持in
和out
转换。例如,您可以转换任何DateTimeInterface
或stringToTimeAble
字符串到Datetime
属性,并以特定格式输出Json格式。
use fab2s\Dt0\Attribute\Cast; use fab2s\Dt0\Caster\DateTimeCaster; use fab2s\Dt0\Caster\DateTimeFormatCaster; use fab2s\Dt0\Dt0; class MyDt0 extends Dt0 { #[Cast(in: DateTimeCaster::class, out: new DateTimeFormatCaster(DateTimeFormatCaster::ISO))] public readonly DateTime $date; } /** @var Dt0 $dt0 */ $dt0 = MyDt0::make(date:'1337-01-01 00:00:00'); $dt0->toArray(); /* [ 'date' => DateTimeInstance, ] */ $dt0->jsonSerialize(); /* [ 'date' => '1337-01-01T00:00:00.000000Z', ] */
每个Caster
也将支持默认值以及输入/输出重命名。
use fab2s\Dt0\Dt0; use fab2s\Dt0\Attribute\Cast; use fab2s\Dt0\Attribute\Casts; #[Casts( new Cast(default: 'defaultFromCast', propName: 'propClassCasted'), // same as propClassCasted: new Cast(default: 'defaultFromCast'), )] class MyDt0 extends Dt0 { public readonly string $propClassCasted; #[Cast(default: null)] public readonly ?string $propCasted; #[Cast(renameFrom: 'inputName', renameTo: 'outputName', default: 'default')] public readonly string $propRenamed; } $dt0 = MyDt0::make(); $dt0->propClassCasted; // 'defaultFromCast' $dt0->propCasted; // 'null' $dt0->propRenamed; // 'default' $dt0 = MyDt0::make(propCasted: 'Oh Yeah', inputName: "I don't exist"); // <= argument order never matter $dt0->propRenamed; // "I don't exist" $dt0->toArray(); /** [ 'propClassCasted' => 'defaultFromCast', 'propCasted' => 'Oh Yeah', 'propRenamed' => "I don't exist", ] */ // same as $dt0 = MyDt0::make(propCasted: 'Oh Yeah', outputName: "I don't exist"); $dt0->propRenamed; // "I don't exist" // all renameTo are added to renameFrom $dt0->equal(MyDt0::fromArray($dt0->toArray()); // true $dt0 = MyDt0::fromArray([ 'propCasted' => 'Oh', // <= order never matter 'propClassCasted' => 'Ho', ]); $dt0->propRenamed; // 'default' $dt0->toArray(); /** [ 'propClassCasted' => 'Ho', 'propCasted' => 'Oh', 'propRenamed' => 'default', ] */ // output renaming only occurs in json format $dt0->toJsonArray(); /** [ 'propClassCasted' => 'Ho', 'propCasted' => 'Oh', 'outputName' => 'default', ] */
Cast
的renameFrom
参数也可以是一个数组,用于处理单个内部属性的多重输入属性名。
#[Cast(renameFrom: ['alias', 'legacy_name'])] // first in wins the race public readonly string $prop;
默认值
Casts
可以携带默认值,即使没有硬属性默认值(在不可提升的只读属性中是不可能的)。
由于PHP没有实现Nil
概念(与null
或实际上设置为null
相对),Dt0
使用一个空字节("\0"
)作为Caster->default
值的默认值,以简化使用。另一种选择是要求设置一个额外的布尔参数hasDefault
,然后设置默认值或不允许null
作为实际默认值。
这种实现细节导致允许除了null byte
之外的所有值作为默认属性值。
如果您发现自己处于相对罕见的情况,实际上希望默认属性值是null byte
,那么您将需要使用具有这种硬默认值的非只读
属性,但这将破坏不可变性,或者将此属性设置为构造函数中的提升属性以保留readonly
,从而保持Dt0
的不可变性。
总的来说,与每个其他情况中额外参数的负担相比,这种对非常特定情况的额外关注似乎微不足道。
关于构造函数
Dt0
的构造函数可以具有提升属性,前提是它们正确地调用了它们的父类。
class ConstructedDt0 extends Dt0 { // un-casted public readonly string $stringNoCast; #[Cast(/*...*/)] public readonly ?string $stringCasted; public function __construct( public readonly string $promotedPropNoCast, #[Cast(/*...*/)] public readonly string $promotedPropCasted = 'default', // all constructor parameters, promoted on not, can be casted #[Cast(/*...*/)] ?string $myCustomVar = null, // Mandatory, the remaining $args will be used to further // initialize other public properties in this class ...$args, ) { // where the magic happens parent::__construct(...$args); } } // now you can $dt0 = new ConstructedDt0( promotedPropNoCast: 'The order', promotedPropCasted: 'matters', myCustomVar: 'for constructor parameters', stringCasted: 'but not', stringNoCast: 'for regular props', ); // ::make, ::fromArray, ::fromString, ::from ... don't care about argument orders $dt0 = ConstructedDt0::make( stringCasted: 'The Order', stringNoCast: 'never', promotedPropNoCast: 'matter', promotedPropCasted: 'outside', myCustomVar: 'of the constructor', );
make
和其他静态工厂方法与new
当处理只读属性时,当然有一些需要注意的问题,因为它们确实只能初始化一次。如果您的Dt0
使用带有public readonly
提升属性的构造函数,那么在您使用new
关键字创建Dt0
实例时不会使用任何转换,因为所有操作都将在此之前完成。
另一方面,使用make
方法将始终按预期工作,并具有该包的完整转换功能,因为在这种情况下,所有魔法操作将在构造函数甚至被调用之前发生。
作为结论,始终使用静态工厂方法(如make
、from
、tryFrom
、fromArray
、fromString
和fromJson
)创建实例是最好的实践。考虑到这可以实现完全不可变的DTO以及随之而来的安心,这最终并不是什么大事。
这并不意味着你不应该使用public readonly
提升属性,因为这同样是提供public readonly
属性硬默认值的唯一方式。这只是在处理此包时需要注意的一点。
验证
Dt0
提供了完整的验证逻辑,但没有具体的实现。要查看完全功能性的实现示例,请参阅Laravel Dt0
异常
Dt0
的异常都继承自ContextException
,并且携带可用于异常日志记录的上下文信息。
要求
Dt0
已在php 8.1和8.2上进行了测试
贡献
欢迎贡献,请随时提出问题和提交拉取请求。
许可证
Dt0
是开源软件,许可协议为MIT许可证。