dgame / php-dto
受Rust的serde启发的数据传输对象
Requires
- php: ^8.0
- dgame/php-cast: ^0.1.0
- dgame/php-type: ^1.0
- thecodingmachine/safe: ^1.3
Requires (Dev)
- ergebnis/composer-normalize: ^2.4
- ergebnis/phpstan-rules: ^0.15
- php-parallel-lint/php-parallel-lint: ^1.2
- phpstan/phpstan: ^0.12
- phpstan/phpstan-deprecation-rules: ^0.12
- phpstan/phpstan-strict-rules: ^0.12
- phpunit/phpunit: ^9.4
- roave/security-advisories: dev-latest
- slevomat/coding-standard: dev-master
- spaceemotion/php-coding-standard: dev-master
- spaze/phpstan-disallowed-calls: ^1.5
- symplify/easy-coding-standard: ^9.3
- thecodingmachine/phpstan-safe-rule: ^1.0
- thecodingmachine/phpstan-strict-rules: ^0.12
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; }
现在键size
和limit
将映射到属性$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; }
键a
和z
映射到属性id
——但不是键id
,因为你用a
覆盖了它。但是以下
use Dgame\DataTransferObject\Annotation\Alias; use Dgame\DataTransferObject\DataTransfer; final class Foo { use DataTransfer; #[Alias('a')] #[Alias('z')] public int $id; }
将接受键a
、z
和id
。
转换
如果你想在值分配给属性之前进行转换,你可以使用转换。你只需要实现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; }
这里转换将只在对传入值是int
、string
、float
或bool
时执行
如果你想有更多的控制,你可以在类内部使用静态方法
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的一个小包装器。所以您的嵌套类根本不需要使用它。
嵌套的深度没有限制,责任由您负责! :)