fab2s/dt0

Dt0是一个DTO PHP实现,它既可以确保可变性,也可以方便地在各种格式中控制输入和输出

0.0.1 2024-04-28 15:36 UTC

This package is auto-updated.

Last update: 2024-08-29 19:53:45 UTC


README

CI QA codecov Latest Stable Version Total Downloads Monthly Downloads PRs Welcome License

Dt0(即DeeTODeTZerO)是一个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来执行转换:CastsCast

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文档中有记录。

用法

Dt0Caster没有要求,就绪地支持所有枚举,包括UnitEnum

Dt0对它的继承者也很清楚,无需任何转换。当属性类型不够具体时,您可以在某些情况下使用Dt0Caster(阅读目标Dt0类)。

Dt0支持inout转换。例如,您可以转换任何DateTimeInterfacestringToTimeAble字符串到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',
] 
*/

CastrenameFrom参数也可以是一个数组,用于处理单个内部属性的多重输入属性名。

    #[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方法将始终按预期工作,并具有该包的完整转换功能,因为在这种情况下,所有魔法操作将在构造函数甚至被调用之前发生。

作为结论,始终使用静态工厂方法(如makefromtryFromfromArrayfromStringfromJson)创建实例是最好的实践。考虑到这可以实现完全不可变的DTO以及随之而来的安心,这最终并不是什么大事。

这并不意味着你不应该使用public readonly 提升属性,因为这同样是提供public readonly属性硬默认值的唯一方式。这只是在处理此包时需要注意的一点。

验证

Dt0提供了完整的验证逻辑,但没有具体的实现。要查看完全功能性的实现示例,请参阅Laravel Dt0

异常

Dt0的异常都继承自ContextException,并且携带可用于异常日志记录的上下文信息。

要求

Dt0已在php 8.1和8.2上进行了测试

贡献

欢迎贡献,请随时提出问题和提交拉取请求。

许可证

Dt0是开源软件,许可协议为MIT许可证