dgame/php-dto

受Rust的serde启发的数据传输对象

维护者

详细信息

github.com/Dgame/php-dto

源代码

问题

安装次数: 34

依赖项: 0

建议者: 0

安全: 0

星标: 40

关注者: 1

分支: 2

开放问题: 0

类型:package

v0.4.0 2021-09-12 19:19 UTC

This package is auto-updated.

Last update: 2024-09-11 00:20:33 UTC


README

想要动态反序列化对象?使用From特性试试。

你可能想知道,这个包和spatie的流行数据传输对象有什么不同?好吧,它不是任何意义上的替代品。但在我使用它的过程中,我经常发现一些从serde知道的事情,比如重命名和忽略属性,spatie的data-transfer-object 可能在未来不会得到。所以,这就是我的小DTO包:)希望它能帮助到一些人,就像它帮助我日常工作的那样。请随意打开问题或拉取请求——任何帮助都将非常感激!

需求

这个包是为PHP ≥ 8.0设计的,因为它使用了PHP 8.0 属性

属性

名称

如果你得到的参数名称与你的类中的参数名称不一致?使用#[Name(...)]来拯救——只需指定从请求中的名称

use Dgame\DataTransferObject\Annotation\Name;
use Dgame\DataTransferObject\DataTransfer;

final class Limit
{
    use DataTransfer;

    public int $offset;
    #[Name('size')]
    public int $limit;
}

现在键size将映射到属性$limit——但请注意:名称limit不再为人所知,因为你用size覆盖了它。如果你不是这个意思,请查看别名属性。

别名

如果你得到的参数名称不是你类中的参数名称?使用#[Alias(...)]可以帮你——只需指定请求中的别名

use Dgame\DataTransferObject\Annotation\Alias;
use Dgame\DataTransferObject\DataTransfer;

final class Limit
{
    use DataTransfer;

    public int $offset;
    #[Alias('size')]
    public int $limit;
}

现在键sizelimit将映射到属性$limit。你可以按需混合使用#[Name(...)]#[Alias(...)]

use Dgame\DataTransferObject\Annotation\Alias;
use Dgame\DataTransferObject\Annotation\Name;
use Dgame\DataTransferObject\DataTransfer;

final class Foo
{
    use DataTransfer;

    #[Name('a')]
    #[Alias('z')]
    public int $id;
}

az映射到属性id——但不是键id,因为你用a覆盖了它。但是以下

use Dgame\DataTransferObject\Annotation\Alias;
use Dgame\DataTransferObject\DataTransfer;

final class Foo
{
    use DataTransfer;

    #[Alias('a')]
    #[Alias('z')]
    public int $id;
}

将接受键azid

转换

如果你想在值分配给属性之前进行转换,你可以使用转换。你只需要实现Transformation接口。

转换

转换是目前唯一的内置转换,允许你在值分配给属性之前应用类型转换

如果没有其他说明,将执行简单的类型转换。在下面的示例中,它将调用类似$this->id = (int) $id的东西

use Dgame\DataTransferObject\Annotation\Cast;

final class Foo
{
    use DataTransfer;

    #[Cast]
    public int $id;
}

但是这将对任何输入都尝试。如果你想限制它只针对某些类型,你可以使用types

use Dgame\DataTransferObject\Annotation\Cast;

final class Foo
{
    use DataTransfer;

    #[Cast(types: ['string', 'float', 'bool'])]
    public int $id;
}

这里转换将只在对传入值是intstringfloatbool时执行

如果你想有更多的控制,你可以在类内部使用静态方法

use Dgame\DataTransferObject\Annotation\Cast;

final class Foo
{
    use DataTransfer;

    #[Cast(method: 'toInt', class: self::class)]
    public int $id;

    public static function toInt(string|int|float|bool $value): int
    {
        return (int) $value;
    }
}

或函数

use Dgame\DataTransferObject\Annotation\Cast;

function toInt(string|int|float|bool $value): int
{
    return (int) $value;
}

final class Foo
{
    use DataTransfer;

    #[Cast(method: 'toInt')]
    public int $id;
}

如果一个类被给出但不是method,则默认使用__invoke

use Dgame\DataTransferObject\Annotation\Cast;

final class Foo
{
    use DataTransfer;

    #[Cast(class: self::class)]
    public int $id;

    public function __invoke(string|int|float|bool $value): int
    {
        return (int) $value;
    }
}

验证

你想要在分配之前验证值?我们可以做到。我们已经准备了一些预定义的验证,但你可以很容易地通过实现Validation-接口来编写自己的验证。

最小值

use Dgame\DataTransferObject\Annotation\Min;
use Dgame\DataTransferObject\DataTransfer;

final class Limit
{
    use DataTransfer;
    
    #[Min(0)]
    public int $offset;
    #[Min(0)]
    public int $limit;
}

《$offset》和《$limit》都必须至少具有值《0》(因此它们必须是正整数)。如果不是,将抛出异常。您可以通过指定《message》参数来配置异常消息。

use Dgame\DataTransferObject\Annotation\Min;
use Dgame\DataTransferObject\DataTransfer;

final class Limit
{
    use DataTransfer;
    
    #[Min(0, message: 'Offset must be positive!')]
    public int $offset;
    #[Min(0, message: 'Limit must be positive!')]
    public int $limit;
}

最大值

use Dgame\DataTransferObject\Annotation\Max;
use Dgame\DataTransferObject\DataTransfer;

final class Limit
{
    use DataTransfer;
    
    #[Max(1000)]
    public int $offset;
    #[Max(1000)]
    public int $limit;
}

《$offset》和《$limit》不能超过《1000》。如果它们超过了,将抛出异常。您可以通过指定《message》参数来配置异常消息。

use Dgame\DataTransferObject\Annotation\Max;
use Dgame\DataTransferObject\DataTransfer;

final class Limit
{
    use DataTransfer;
    
    #[Max(1000, message: 'Offset may not be larger than 1000')]
    public int $offset;
    #[Max(1000, message: 'Limit may not be larger than 1000')]
    public int $limit;
}

实例

您想确保一个属性是某个类的实例,或者数组中的每个项都是该类的实例吗?

use Dgame\DataTransferObject\Annotation\Instance;

final class Collection
{
    #[Instance(class: Entity::class, message: 'We need an array of Entities!')]
    private array $entities;
}

类型

如果您正在尝试处理对象或其他类实例,您可能需要查看实例

只要您为属性指定了类型,就会自动添加《Type》验证,以确保指定的值可以被分配到指定的类型。如果不是,将抛出验证异常。没有这种验证,将抛出《TypeError》,这可能不是期望的结果。

所以这段代码

final class Foo
{
    private ?int $id;
}

实际上被看作是这样

use Dgame\DataTransferObject\Annotation\Type;

final class Foo
{
    #[Type(name: '?int')]
    private ?int $id;
}

以下片段与上面的片段等效

use Dgame\DataTransferObject\Annotation\Type;

final class Foo
{
    #[Type(name: 'int|null')]
    private ?int $id;
}
use Dgame\DataTransferObject\Annotation\Type;

final class Foo
{
    #[Type(name: 'int', allowsNull: true)]
    private ?int $id;
}

如果您想更改异常消息,可以使用《message》参数来更改。

use Dgame\DataTransferObject\Annotation\Type;

final class Foo
{
    #[Type(name: '?int', message: 'id is expected to be int or null')]
    private ?int $id;
}

自定义

您想使用自己的验证?只需实现《Validation》接口即可。

use Dgame\DataTransferObject\Annotation\Validation;
use Dgame\DataTransferObject\DataTransfer;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class NumberBetween implements Validation
{
    public function __construct(private int|float $min, private int|float $max)
    {
    }

    public function validate(mixed $value): void
    {
        if (!is_numeric($value)) {
            throw new InvalidArgumentException(var_export($value, true) . ' must be a numeric value');
        }

        if ($value < $this->min) {
            throw new InvalidArgumentException(var_export($value, true) . ' must be >= ' . $this->min);
        }

        if ($value > $this->max) {
            throw new InvalidArgumentException(var_export($value, true) . ' must be <= ' . $this->max);
        }
    }
}

final class ValidationStub
{
    use DataTransfer;

    #[NumberBetween(18, 125)]
    private int $age;

    public function getAge(): int
    {
        return $this->age;
    }
}

忽略

您不想让特定的键值覆盖您的属性?只需忽略它即可。

use Dgame\DataTransferObject\Annotation\Ignore;
use Dgame\DataTransferObject\DataTransfer;

final class Foo
{
    use DataTransfer;

    #[Ignore]
    public string $uuid = 'abc';
    public int $id = 0;
}

$foo = Foo::from(['uuid' => 'xyz', 'id' => 42]);
echo $foo->id; // 42
echo $foo->uuid; // abc

拒绝

您想比简单地忽略值更进一步?那么就《拒绝》它。

use Dgame\DataTransferObject\Annotation\Reject;
use Dgame\DataTransferObject\DataTransfer;

final class Foo
{
    use DataTransfer;

    #[Reject(reason: 'The attribute "uuid" is not supposed to be set')]
    public string $uuid = 'abc';
}

$foo = Foo::from(['id' => 42]); // Works fine
echo $foo->id; // 42
echo $foo->uuid; // abc

$foo = Foo::from(['uuid' => 'xyz', 'id' => 42]); // throws 'The attribute "uuid" is not supposed to be set'

必需

通常,可空属性或具有提供默认值的属性会使用该默认值或null(如果属性不能从提供的数据中分配)。如果没有提供默认值,并且属性不是可空的,则在属性未找到的情况下将抛出错误。但在某些情况下,您可能想要指定属性是必需的原因,甚至想要要求一个默认可用的属性。您可以使用《Required》来做到这一点。

use Dgame\DataTransferObject\Annotation\Required;
use Dgame\DataTransferObject\DataTransfer;

final class Foo
{
    use DataTransfer;

    #[Required(reason: 'We need an "id"')]
    public ?int $id;
    
    #[Required(reason: 'We need a "name"')]
    public string $name;
}

Foo::from(['id' => 42, 'name' => 'abc']); // Works
Foo::from(['name' => 'abc']); // Fails but would work without the `Required`-Attribute since $id is nullable
Foo::from(['id' => 42]); // Fails and would fail regardless of the `Required`-Attribute since $name is not nullable and has no default-value - but the reason why it is required is now more clear.

可选

《Required》的对立面。如果您不想或无法提供默认值或可空值,则《Optional》将在缺少值的情况下将属性类型的默认值分配给属性。

final class Foo
{
    use DataTransfer;
    
    #[Optional]
    public int $id;
}

$foo = Foo::from([]);
assert($foo->id === 0);

当然,您可以指定在未提供数据时应使用哪个值。

final class Foo
{
    use DataTransfer;
    
    #[Optional(value: 42)]
    public int $id;
}

$foo = Foo::from([]);
assert($foo->id === 42);

如果您在《Optional》和提供的默认值一起使用,则默认值始终具有优先权。

final class Foo
{
    use DataTransfer;
    
    #[Optional(value: 42)]
    public int $id = 23;
}

$foo = Foo::from([]);
assert($foo->id === 23);

数字

您有《int》或《float》属性,但不清楚这些值是否以《string》等格式提供?《Numeric》可以帮助您解决这个问题!它将值转换为数值表示(转换为《int》或《float》)。

use Dgame\DataTransferObject\Annotation\Numeric;

final class Foo
{
    use DataTransfer;
    
    #[Numeric(message: 'id must be numeric')]
    public int $id;
}

$foo = Foo::from(['id' => '23']);
assert($foo->id === 23);

布尔值

您有《bool》属性,但不清楚这些值是否以《string》或《int》格式提供?《Boolean》可以帮助您处理这个问题!

use Dgame\DataTransferObject\Annotation\Boolean;

final class Foo
{
    use DataTransfer;
    
    #[Boolean(message: 'checked must be a bool')]
    public bool $checked;
    #[Boolean(message: 'verified must be a bool')]
    public bool $verified;
}

$foo = Foo::from(['checked' => 'yes', 'verified' => 0]);
assert($foo->checked === true);
assert($foo->verified === false);

日期

您需要一个《DateTime》,但得到了一个字符串?没问题。

use Dgame\DataTransferObject\Annotation\Date;
use \DateTime;

final class Foo
{
    use DataTransfer;
    
    #[Date(format: 'd.m.Y', message: 'Your birthday must be a date')]
    public DateTime $birthday;
}

$foo = Foo::from(['birthday' => '19.09.1979']);
assert($foo->birthday === DateTime::createFromFormat('d.m.Y', '19.09.1979'));

在...

您的值必须是特定范围或枚举中的一个?您可以使用《In》来确保这一点。

use Dgame\DataTransferObject\Annotation\In;

final class Foo
{
    use DataTransfer;
    
    #[In(values: ['beginner', 'advanced', 'difficult'], message: 'Must be either "beginner", "advanced" or "difficult"')]
    public string $difficulty;
}

Foo::from(['difficulty' => 'foo']); // will throw a error, since difficulty is not in the provided values
$foo = Foo::from(['difficulty' => 'advanced']);
assert($foo->difficulty === 'advanced');

不在...

您的值必须不是特定范围或枚举中的一个?您可以使用《NotIn》来确保这一点。

use Dgame\DataTransferObject\Annotation\NotIn;

final class Foo
{
    use DataTransfer;
    
    #[NotIn(values: ['holy', 'shit', 'wtf'], message: 'Must not be a swear word')]
    public string $word;
}

匹配...

您必须确保您的值与特定模式匹配?您可以通过使用《Matches》为所有标量值做到这一点。

use Dgame\DataTransferObject\Annotation\Matches;

final class Foo
{
    use DataTransfer;
    
    #[Matches(pattern: '/^[a-z]+\w*/', message: 'Your name must start with a-z')]
    public string $name;
    
    #[Matches(pattern: '/[1-9][0-9]+/', message: 'products must be at least 10')]
    public int $products;
}

Foo::from(['name' => '_', 'products' => 99]); // will throw a error, since name does not start with a-z
Foo::from(['name' => 'John', 'products' => 9]); // will throw a error, since products must be at least 10

缩进...

您必须确保字符串值被缩进?不用担心,我们有《Trim》。

use Dgame\DataTransferObject\Annotation\Trim;

final class Foo
{
    use DataTransfer;
    
    #[Trim]
    public string $name;
}

$foo = Foo::from(['name' => ' John   ']);
assert($foo->name === 'John');

路径...

您是否曾经想要从提供的数组中提取一个值?《Path》可以帮助您解决这个问题。

final class Person
{
    use DataTransfer;

    #[Path('person.name')]
    public string $name;
}

当使用JSON的特殊$value属性时,这很有帮助

final class Person
{
    use DataTransfer;

    #[Path('married.$value')]
    public bool $married;
}

以及XML的#text

final class Person
{
    use DataTransfer;

    #[Path('first.name.#text')]
    public string $firstname;
}

但我们能做得更多。您可以选择字段的哪些部分被取出

final class Person
{
    use DataTransfer;

    #[Path('child.{born, age}')]
    public array $firstChild = [];
}

甚至可以将它们直接分配给一个对象

final class Person
{
    use DataTransfer;
    
    public int $id;
    public string $name;
    public ?int $age = null;

    #[Path('ancestor.{id, name}')]
    public ?self $parent = null;
}

自我验证

除了通常的验证外,您可以在所有赋值完成后指定一个类级别的验证

#[SelfValidation(method: 'validate')]
final class SelfValidationStub
{
    use DataTransfer;

    public function __construct(public int $id)
    {
    }

    public function validate(): void
    {
        assert($this->id > 0);
    }
}

验证策略

默认的验证策略是fail-fast,这意味着一旦检测到错误就会抛出异常。但这可能不是期望的,因此您可以使用ValidationStrategy来配置它

#[ValidationStrategy(failFast: false)]
final class Foo
{
    use DataTransfer;

    #[Min(3)]
    public string $name;
    #[Min(0)]
    public int $id;
}

Foo::from(['name' => 'a', 'id' => -1]);

上面的示例将抛出一个组合异常,指出name不够长,而id必须至少为0。您也可以通过扩展ValidationStrategy并提供一个FailureHandler和/或一个FailureCollection来配置它。

属性提升

在上面的示例中,属性提升并不总是使用,因为这样做更易于阅读,但属性提升是支持的。因此,以下示例

use Dgame\DataTransferObject\Annotation\Min;
use Dgame\DataTransferObject\DataTransfer;

final class Limit
{
    use DataTransfer;
    
    #[Min(0)]
    public int $offset;
    #[Min(0)]
    public int $limit;
}

可以重写如下

use Dgame\DataTransferObject\Annotation\Min;
use Dgame\DataTransferObject\DataTransfer;

final class Limit
{
    use DataTransfer;

    public function __construct(
        #[Min(0)] public int $offset,
        #[Min(0)] public int $limit
    ) { }
}

它仍然可以工作。

嵌套对象检测

您有嵌套对象,并希望一次将它们全部反序列化?这是理所当然的

use Dgame\DataTransferObject\DataTransfer;

final class Bar
{
    public int $id;
}

final class Foo
{
    use DataTransfer;
    
    public Bar $bar;
}

$foo = Foo::from(['bar' => ['id' => 42]]);
echo $foo->bar->id; // 42

您注意到在Bar中缺少了From吗?From只是实际DTO的一个小包装器。所以您的嵌套类根本不需要使用它。

嵌套的深度没有限制,责任由您负责! :)