nolikein/objectable-struct

一种使用 PHP 软类型 struct 的方法

2.2.1 2023-07-04 09:01 UTC

README

PHP 1.0 Packagist link Test Pass

[已弃用] 请使用 DTO 库

你正在寻找一种创建类似 struct 特性的方法吗?我们不在 C 语言中,但我希望使用这个包能实现类似的功能。

另一方面,Laravel 模型 在灵活性和简单性方面很棒,可以用作数组/对象,并进行类型转换,我真的很喜欢它们。当前的库添加了一个 Struct 类,允许你选择执行这些操作以及更多...

这个概念很简单,你创建一个类或实例,并定义你的约束,然后每次设置一个值,这个值将根据你的选择进行检查或转换。如果有什么不合适的地方,它将抛出异常。你将在下面看到!

目录

  1. 安装
  2. 简单使用
  3. 基础
  4. 约束
    1. 转换
      1. 可用的转换器
      2. 添加自己的转换器
      3. 关于转换的说明
    2. 实例匹配
  5. 继承
  6. 钩子
    1. 简介
    2. 禁用钩子
    3. 避免手动更新属性
    4. 钩子示例
  7. 可用的方法
  8. 待办事项
  9. 许可证

安装

你需要使用 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 的实例,可以在约束包中注册。然而,你看到我们使用了 stringinteger 等名称,但这些名称在底层被转换为实例。因此,下面的代码可以正常工作

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 类的类。此类实现了两个接口:ConstraintCastPattern。第一个允许您创建一个新的约束,第二个允许您创建一个转换器。

以下是定义布尔值的代码

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

许可证

MIT