nolikein / objectable-struct
一种使用 PHP 软类型 struct 的方法
Requires
- php: ^8.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.15
- nunomaduro/phpinsights: ^2.8
- pestphp/pest: ^2.2
- phpstan/phpstan: ^1.10
- romanzipp/php-cs-fixer-config: ^3.1
- symfony/var-dumper: ^6.2
This package is auto-updated.
Last update: 2024-09-16 14:08:26 UTC
README
[已弃用] 请使用 DTO 库
你正在寻找一种创建类似 struct 特性的方法吗?我们不在 C 语言中,但我希望使用这个包能实现类似的功能。
另一方面,Laravel 模型 在灵活性和简单性方面很棒,可以用作数组/对象,并进行类型转换,我真的很喜欢它们。当前的库添加了一个 Struct
类,允许你选择执行这些操作以及更多...
这个概念很简单,你创建一个类或实例,并定义你的约束,然后每次设置一个值,这个值将根据你的选择进行检查或转换。如果有什么不合适的地方,它将抛出异常。你将在下面看到!
目录
安装
你需要使用 composer 来安装这个库。
composer require nolikein/objectable-struct ^2.0
简单使用
结构是一个简单的数组/对象
use Nolikein\Objectable\Struct;
$chad = new Struct([
'can_be_used_as_array' => true,
'can_be_used_as_object' => true,
]);
$chad['can_be_used_as_array'] === true;
$chad->can_be_used_as_object === true;
$chad->doesnt_exists === null; // Doesnt throws any Exception because strict mode is disabled
// Strict mode will be showed further, by default it is disabled
// We can check if an item is set
isset($s['item'])
isset($s->item)
// We can unset an item
unset($s['item'])
unset($s->item)
你可以使用转换器约束强制类型
use Nolikein\Objectable\Struct;
$metaChad = new Struct([
'value' => 123, // Attributes parameter
], [
'value' => 'string', // Casters parameter
]);
$metaChad->value === '123'; // Casted value are automatically casted even at creation
$metaChad->value = 123; // The setted value is casted
$metaChad->value === '123'; // And default_string will ever be a string
你可以使用实例约束强制实例
use Nolikein\Objectable\Struct;
use Nolikein\Objectable\Constraints\Instance;
$igniChad = new Struct([
// Instances can be equal to null
'igni' => null
'aqua' => new \Model
], [
'igni' => 'instanceof:' . \Model::class,
'aqua' => Instance::of(\Model::class)->notNullable(),
]);
$igniChad->igni = new \Model(); // Allowed
$igniChad->igni = null; // Allowed
$igniChad->igni = new \OtherModel; // Not allowed
$igniChad->aqua = new \Model(); // Allowed
$igniChad->aqua = null; // Not allowed
$igniChad->aqua = new \OtherModel; // Not allowed
基础
结构类似于数组
use Nolikein\Objectable\Struct;
$notAnArray = new Struct([
'but' => 'works as :D',
]);
echo $notAnArray['but'];
// 'works as :D'
此外,它们是暗容器
use Nolikein\Objectable\Struct;
$darkContainer = new Struct([
'propertyNotDeclared' => 'Truly !',
]);
echo $darkContainer->propertyNotDeclared;
// 'Truly !'
当属性不存在时会发生什么?你会得到 null
。
use Nolikein\Objectable\Struct;
$darkContainer = new Struct();
echo $darkContainer->notDefined;
// null
约束
约束:转换
转换是将项强制转换为特定类型的能力。
以下是一个示例
use Nolikein\Objectable\Struct;
// Here is a classical struct with one attribute
$castChad = new Struct([
'defined' => 'its value',
]);
// We'll apply a constraint. Our 'defined' attribute must be a string
$castChad->setConstraint('defined', 'string');
// Now, if we try to use a different type than string, it will be casted
$castChad->defined = 123;
$castChad->defined === '123';
// We have a scalar that could be an integer, so nothing block us to do it now
$castChad->setConstraint('defined', 'integer');
// Our value was casted when calling setConstraint().
$castChad->defined === 123;
// Now... what happen if the attribute does not exists yet?
// It will have a default value !
$castChad->setConstraint('notDefined', 'int');
$castChad->notDefined === 0;
转换器表示法
转换器是 Nolikein\Objectable\Constraints\Cast
的实例,可以在约束包中注册。然而,你看到我们使用了 string 或 integer 等名称,但这些名称在底层被转换为实例。因此,下面的代码可以正常工作
use Nolikein\Objectable\Struct;
use Nolikein\Objectable\Casters\StringCast;
$castChad = new Struct();
// Faster way to write, slower way to guess
$castChad->setConstraint('defined', 'string');
// Slow way to write, fast way to guess
$castChad->setConstraint('defined', StringCast::class);
// Very slow way to write, very fast way to guess
$castChad->setConstraint('defined', new StringCast());
可用的转换器
以下是支持的转换列表
- int|integer
- float|double
- string
- bool|boolean
- array
- object
- json
- datetime
- struct
您还可以使用 PHP 8 的 Nolikein\Objectable\Enums\StructCasters
枚举来设置类型,例如
use Nolikein\Objectable\Struct;
use Nolikein\Objectable\Enums\StructCasters;
$s = new Struct([], [
'myInt' => StructCasters::Integer,
'myFloat' => StructCasters::Float,
'myString' => StructCasters::String,
'myBoolean' => StructCasters::Boolean,
'myArray' => StructCasters::Array,
'myStdObject' => StructCasters::Object,
'myDatetime' => StructCasters::Datetime,
'myStruct' => StructCasters::Struct,
]);
注意:您可以定义自己的转换器(见下文),然后使用自己的枚举来设置转换器类型。
添加自己的转换器
首先,您需要创建一个继承自 Nolikein\Objectable\Constraints\Cast
类的类。此类实现了两个接口:Constraint
和 CastPattern
。第一个允许您创建一个新的约束,第二个允许您创建一个转换器。
以下是定义布尔值的代码
use Nolikein\Objectable\Constraints\Cast;
class BooleanCast extends Cast
{
public function getTypeName(): string
{
return 'boolean';
}
/**
* @return array<int, string>
*/
public function getTypeAliases(): array
{
return [
'bool',
];
}
public function canCast(mixed $value): bool
{
return null !== filter_var($value, FILTER_VALIDATE_BOOLEAN, [
'flags' => FILTER_NULL_ON_FAILURE
]);
}
public function performCast(mixed $value): mixed
{
return (bool) $value;
}
public function getDefaultValue(): mixed
{
return false;
}
}
然后,使用 addCaster
静态方法或 Struct
添加您的类。
Struct::addCaster(MyNewType::class);
注意:该方法也可以接受实例作为参数(例如匿名类)。
关于转换的说明
- jsonable 内容可以是数组或标量。
- datetimable 内容可以是
\DateTimeInterface
的实例或字符串,该字符串的格式被DateTimeImmutable::__construct()
接受。 - 默认情况下,'object' 和 'datetime' 转换返回 null。
约束:实例匹配
您可以强制一个属性成为一个类的实例。如果您尝试设置不允许的实例,您将触发 \Nolikein\Objectable\Exceptions\NotInstanceOf
异常。
以下是一个示例
use Nolikein\Objectable\Struct;
use Nolikein\Objectable\Constraints\Instance;
class Model {}
class OtherModel {}
$rockAndStone = new Struct([
// Instances can be equal to null
'igni' => null,
], [
// Fast method
'igni' => 'instanceof:' . Model::class
// What is really done backside
'igni' => Instance::of(Model::class),
// Alternative method to pass instance
'igni' => Instance::ofObject(new Model()),
]);
$rockAndStone->igni = new Model(); // Allowed
$rockAndStone->igni = null; // Allowed
$rockAndStone->igni = new OtherModel; // Will throw a NotInstanceOf exception
// Relationships work
class Child extends Model {}
$rockAndStone->igni = new Child; // Allowed by relationship
// Note, interfaces also work
然而,如果实例默认可以为空,则可以禁用此功能
use Nolikein\Objectable\Struct;
use Nolikein\Objectable\Constraints\Instance;
$rockAndStone = new Struct([
// Not allowed
'terra' => null
], [
'terra' => Instance::of(\Model::class)->notNullable(),
]);
use Nolikein\Objectable\Struct;
use Nolikein\Objectable\Constraints\Instance;
$rockAndStone = new Struct([
'terra' => new \Model(),
], [
'terra' => Instance::of(\Model::class)->notNullable(),
]);
$rockAndStone->terra = new \Model; // Allowed
$rockAndStone->terra = null; // Not allowed
严格模式
你知道你可以添加未首先定义的新属性,而且你不必对每个属性应用约束。但在严格模式下,你不能获取或设置未首先定义的属性,也不能拥有没有约束的属性,甚至不能更改约束!
严格模式允许你拥有一个永远不会改变的模式,这与C语言的精神一致。
- 如果未声明属性,你将得到一个
\Nolikein\Objectable\Exceptions\AttributeDoesntExists
异常。 - 如果属性缺少约束,你将得到一个
\Nolikein\Objectable\Exceptions\ConstraintNotDeclared
异常。 - 如果你尝试添加/修改/删除任何约束,你将得到一个
\Nolikein\Objectable\Exceptions\CannotChangeConstraint
异常。
让我们看一个例子
use Nolikein\Objectable\Struct;
// !!! Attribute and constraints bag are reversed when using strict()
$gigaChad = Struct::strict([
'myBoolean' => 'bool',
'myString' => 'string',
], [
'myBoolean' => true,
'myString' => '123',
]);
// Can only access to defined items
$gigaChad->myBoolean === true;
$gigaChad->myString === '123';
// This will throw an exception : Nolikein\Objectable\Exceptions\AttributeDoesntExists
$gigaChad->notDefined === null;
$gigaChad->notDefined = 'any value';
$gigaChad = Struct::strict([], [
'myString' => 'string',
]); // Throws ConstraintNotDeclared
继承
你可以定义一个类来编写结构体声明,然后可以在多个地方使用它。
为了非常快,这就是定义我们特性的方法
- 属性 -> $attributes
- 约束 -> $constraints 或 constraints():使用实例的数组。
- 严格模式 -> $strictMode
这里是一个同时展示所有这些特性的例子
use App\Models\Phone;
use Nolikein\Objectable\Struct;
use Nolikein\Objectable\Casters\IntegerCast;
use Nolikein\Objectable\Casters\StringCast;
use Nolikein\Objectable\Constraints\Instance;
class User extends Struct
{
protected array $attributes = [
'firstname' => 'John',
'lastname' => 'NotDoe',
'description' => 'What did you expect ?',
];
protected array $constraints = [
'firstname' => 'string',
'age' => 'integer',
'phone' => 'instanceof:App\Models\Phone',
];
// Alternative way, allows to use instances
protected function constraints(): array
{
return [
'firstname' => StringCast::class,
'age' => IntegerCast::class,
'phone' => Instance::of(Phone::class),
// Alternative way
'phone' => Instance::ofObject(new Phone),
];
}
// In this example, all attributes was not defined so the program will trigger an exception.
protected bool $strictMode = true;
}
钩子
钩子介绍
2.1.0特性版本添加了钩子,用于在从类继承中更新数据时执行操作。
/**
* Before updating an attribute.
*/
protected function updatingAttribute(string $name, mixed $value): void
{
}
/**
* After updating a attribute.
*/
protected function updatedAttribute(string $name, mixed $value): void
{
}
/**
* Before updating an constraint.
*/
protected function updatingConstraint(string $name, Constraint $constraint): void
{
}
/**
* After updating a constraint.
*/
protected function updatedConstraint(string $name, Constraint $constraint): void
{
}
禁用钩子
有时,你可能需要临时禁用钩子。想象一下,由于更新了一些数据,会有级联效应。为了避免这种情况,你可以禁用钩子...
- 手动:disableHooks(),enableHooks()
- 使用装饰器:withoutHooks()
以下是一个示例
/**
* After updating a attribute.
*/
protected function updatedAttribute(string $name, mixed $value): void
{
$this->disableHooks();
$this->doSomething();
$this->enableHooks();
$this->withoutHooks(fn () => $this->doSomething());
}
所有这些方法都是公开的,所以如果你需要从更高范围禁用钩子,你可以!
避免手动更新属性
有时,你还需要避免任何人更新必须自动更新的数据。你需要在一个"更新"方法中,然后调用skipFilling()方法。
if ('data_automatically_updated' === $name) {
$this->skipFilling();
}
在底层,setAttribute方法捕获一个Nolikein\Objectable\Exceptions\SkipFilling
异常,然后只是传递填充操作。
钩子示例
这里是一个你可以使用钩子特性的例子
use Nolikein\Objectable\Struct;
use Nolikein\Objectable\Exceptions\SkipFilling;
class PricingResult extends Struct
{
/** @var array<string, mixed> The struct attributes */
protected array $attributes = [
'price' => 0.0,
'quantity' => 0.0,
'total' => 0.0,
];
/**
* The data will be updated.
*/
protected function updatingAttribute(string $name, mixed $value): void
{
// Avoid any developper to update the total manually
if ('total' === $name) {
$this->skipFilling();
}
}
/**
* The data has already been updated.
*/
protected function updatedAttribute(string $name, mixed $value): void
{
// If the attribute that has been updated is named "price" or "quantity"
if ('price' === $name || 'quantity' === $name) {
// Then calculate the total from the price and quantity.
// But you need to disable hooks, otherwise the attribute "total" will be skipped.
$this->withoutHooks(fn () => $this->total = $this->price * $this->quantity);
}
}
}
$s = new PricingResult([
'price' => 5,
'quantity' => 2,
]);
echo $s->total; // Gives you 10
在这个例子中,我们创建了一个表示定价结果的结构体。该结构体观察"价格"和"数量"属性,如果其中任何一个被更新,则总金额会自动更新。这个动作是由"updatedAttribute"钩子执行的。
这应该是更新总金额的唯一方式,所以我们执行了一个"保护"操作,以防开发者尝试更新"总金额"字段。这个动作是由"updatingAttribute"钩子执行的。
注意
请小心,如果你尝试设置只有$constraints而没有自动更新的$attributes,可能会发生错误。这仅在结构体创建时发生;因为约束包在$attributes包未定义时设置默认值,所以即使在$attributes包中未定义的属性也会被重写。
以定价结果为例:如果你从"价格"更新"总金额",因为"总金额"属性在$attributes数组中定义在"价格"之下,所以"总金额"会先被计算,然后被默认约束值替换。
可用的方法
结构体方法
use Nolikein\Objectable\Struct;
/**
* List of available struct methods
*/
$s = new Struct();
// Checks if an attribute has been set
$s->hasAttribute($name);
// Retrieve an attribute
$s->getAttribute($name);
// Set the value for an attribute
$s->setAttribute($name, $value);
// Set value for multiple items at the same time
$s->fill([$name => $value]);
// Print the content as json
$s->toJson();
$s->__toString();
echo $s; // use __toString()
// Print the content as array
$s->toArray();
$s->jsonSerialize();
/**
* Constraint feature
*/
// Checks if a constraint had been set
$s->hasConstraint($name);
// Retrieve a constraint as instance
$s->getConstraint($name);
// Set a new constraint value -> does not work in strict mode
$s->setConstraint($name, 'string' || StringCast::class || $castInstance || StructCasters::String);
// Note: $value can be the string name of a caster, its class name as class-string, an instance of Constraint. Is also supported a string that represent the cast or a BackEnum instance (PHP 8 enumerations).
// Set constraint value for multiple items at the same time -> does not work in strict mode
$s->fillConstraints([$name => $value]);
// Remove a constraint from the constraint bag -> does not work in strict mode
$s->removeConstraint($name);
// Create a new instance of constraint from its name
$s->newConstraintFromString('string' || StringCast::class || 'instanceof:model');
// Universal way to check if a caster is supported
$s->hasCaster('string' || StringCast::class || $instanceOfCast)
// Checks if a caster is supported from name
$s->hasCasterFromName('string');
// Checks if a caster is supported from class-string
$s->hasCasterFromClassString(StringCast::class);
// Checks if a caster is supported from an instance
$s->hasCasterFromInstance($instanceOfCast);
// Universal way to register a new caster
Struct::addCaster(CustomCast::class || $casterInstance);
// Register a new caster from a class-string
Struct::addCasterFromClassString(CustomCast::class);
// Register a new caster from a fresh instance
Struct::addCasterFromInstance($casterInstance);
/**
* Strict feature
*/
// Create a fresh instance of strict Struct
$s = Struct::strict($constraints, $attributes);
// Checks if a struct use strict mode
$s->isStrict();
// Enable the strict mode
$s->enableStrict();
// Enable the strict mode
$s->disableStrict();
/**
* Hooks feature
*/
// Checks if the hooks are enabled (yes by default)
$s->usesHooks()
// Disable the hooks
$s->disableHooks()
// Enable the hooks
$s->enableHooks()
// Perform action without hooks but enable it after that
$s->withoutHooks()
转换方法
use Nolikein\Objectable\Casters\IntegerCast;
// Create a new instance
$int = new IntegerCast();
// Checks if a cast is possible
$int->canCast('123'); // True
// Perform a cast
$result = $int->performCast('123');
echo $result; // 123
// Retrieve the main caster name
echo $int->getTypeName(); // integer
// Retrieve potential name aliases
echo implode(', ', $int->getTypeAliases()); // int
// Retrieve the default value
echo $int->getDefaultValue(); // 0
/**
* Special methods
*/
// JSON
use Nolikein\Objectable\Casters\JsonCast;
$json = new JsonCast();
// Is the content can be encoded
$json->isJsonable($mabeJsonable);
// Is the content can be decoded
$json->isJson($maybeJson);
// DATETIME
use Nolikein\Objectable\Casters\DatetimeCast;
$datetimeCast = new DatetimeCast();
$datetimeCast->isDatetimable(new Datetime()); // true
$datetimeCast->isDatetimable('now'); // true
$datetimeCast->isDatetimable('2023-02-02T16:07:27+01:00'); // true