rootiteam/accessors

提供具有不可变支持的自动属性访问器(设置器/获取器)

1.0.0 2023-07-03 16:17 UTC

This package is auto-updated.

Last update: 2024-09-03 19:02:14 UTC


README

Tests

访问器

当前库可以创建对象属性的自动访问器(例如 获取器设置器)。它通过将带有 属性重载的魔术方法 的特质注入到所需的类中来实现,该特质将对尝试访问 protected外部不可访问) 属性的尝试进行操作。

功能

  • 多种访问器 语法
    • 直接赋值语法
      • $bar = $foo->bar
      • $foo->bar = 'new bar'
    • 方法语法(可定制格式
      • $bar = $foo->get('bar')
      • $bar = $foo->bar()
      • $foo->bar('value')
      • $foo->setBar('value')
      • $foo->set('bar', 'new bar')
      • $foo->set(['bar' => 'new bar', 'baz' => 'new baz', ..., 'qux' => 'new qux'])
  • 使用 属性 实现简单的 配置
    • 无需从类构造函数中调用自定义初始化代码即可使一切工作。
    • 访问器可以针对每个属性或针对整个类一次性进行配置。
    • 支持继承和覆盖,例如,为整个类设置默认行为,但对于特定属性则进行例外。
    • 除了必要的 __get()/__set()/__isset()/__unset()/__call() 之外,不会将任何变量、函数或方法污染到用户类或全局命名空间。
  • 不可变 支持,由 wither 方法支持。
  • 修改器设置器 的支持。

要求

PHP >= 8.0

安装

使用 composer 安装

composer require margusk/accessors

内容

基本用法

考虑以下手动生成访问器方法的类

class A
{
    protected string $foo = "foo";

    protected string $bar = "bar";

    protected string $baz = "baz";

    public function getFoo(): string
    {
        return $this->foo;
    }

    public function getBar(): string
    {
        return $this->bar;
    }

    public function getBaz(): string
    {
        return $this->baz;
    }
}

$a = new A();
echo $a->getFoo();  // Outputs "foo"
echo $a->getBar();  // Outputs "bar"
echo $a->getBaz();  // Outputs "baz"

这只是一个为使 3 个属性可读而编写的样板代码。如果有十几个属性,事情可能会变得相当繁琐。

通过使用 Accessible 特质,此类可以被重写

use margusk\Accessors\Attr\Getter;
use margusk\Accessors\Accessible;

class A
{
    use Accessible;

    #[Getter]
    protected string $foo = "foo";

    #[Getter]
    protected string $bar = "bar";

    #[Getter]
    protected string $baz = "baz";
}

$a = new A();
echo $a->getFoo();  // Outputs "foo"
echo $a->getBar();  // Outputs "bar"
echo $a->getBaz();  // Outputs "baz"

除了避免 获取器 的样板代码之外,现在还提供了 直接赋值 语法,这在初始类中甚至是不可能的

echo $a->foo;  // Outputs "foo"
echo $a->bar;  // Outputs "bar"
echo $a->baz;  // Outputs "baz"

如果有大量属性要公开,那么单独标记每个属性是不合理的。在类声明中一次性标记所有属性

use margusk\Accessors\Attr\Getter;
use margusk\Accessors\Accessible;

#[Getter]
class A
{
    use Accessible;

    protected string $foo = "foo";

    protected string $bar = "bar";

    protected string $baz = "baz";
}

现在关闭 $baz 的可读性

use margusk\Accessors\Attr\Getter;
use margusk\Accessors\Accessible;

#[Getter]
class A
{
    use Accessible;

    protected string $foo = "foo";

    protected string $bar = "bar";

    #[Getter(false)]
    protected string $baz = "baz";
}
$a = new A();
echo $a->getFoo();   // Outputs "foo"
echo $a->getBar();   // Outputs "bar"
echo $a->getBaz();   // Results in Exception

关于写入属性?是的,只需添加 #[Setter] 属性

use margusk\Accessors\Attr\{
    Getter, Setter
};
use margusk\Accessors\Accessible;

#[Getter,Setter]
class A
{
    use Accessible;

    protected string $foo = "foo";

    #[Get(false),Set(false)]
    protected string $bar = "bar";
}

$a = new A();
echo $a->setFoo("new foo")->getFoo();  // Outputs "new foo"
$a->setBar("new bar");                 // Results in Exception

不可变属性

允许其内容被更改的对象称为 可变的。反之,不更改其内容的是 不可变的

当谈论不可变性时,它通常意味着限制原始对象内部的变化,但提供复制/克隆对象并带有所需更改的功能。这样,原始对象保持完整,克隆对象可以用于新的操作。

考虑以下示例

use margusk\Accessors\Attr\Getter;
use margusk\Accessors\Accessible;

#[Getter]
class A
{
    use Accessible;

    public function __construct(
        protected $a,
        protected $b,
        protected $c,
        protected $d,
        protected $e,
        protected $f
    )
    {
    }
}

// Configure object $foo
$foo = new A(1, 2, 3, 4, 5, 6);

// Create object $bar, which differs from $foo only by single value (property A::$f).
//
// To achieve this, we have to retrieve the rest of the values from object $foo and
// pass to constructor to create new object.
// 
// This results in unnecessary complexity and unreadability.
$bar = new B($foo->a, $foo->b,  $foo->c,  $foo->d,  $foo->e,  7);

使用 #[Immutable] 属性,事情变得简单

use margusk\Accessors\Attr\{
    Getter, Setter, Immutable
};
use margusk\Accessors\Accessible;

#[Getter,Setter,Immutable]
class A
{
    use Accessible;

    public function __construct(
        protected $a,
        protected $b,
        protected $c,
        protected $d,
        protected $e,
        protected $f
    )
    {
    }
}

// Configure object $foo
$foo = new A(1, 2, 3, 4, 5, 6);

// Clone object $foo with having changed one property.
$bar = $foo->with('f', 7);

// Clone object $foo with having changed three properties.
$bar = $foo->with([
    'a' => 11,
    'b' => 12,
    'f' => 7
]);

// Original object still stays intact
echo (int)($foo === $bar); // Outputs "0"
echo $foo->f; // Outputs "6"
echo $bar->f; // Outputs "7"

注意

  • 这里的不可变性是 弱的,不应与 强不可变性 混淆,例如
    • 没有规则说明对象应该有多少部分是不可变的。如果想要的话,可以只对一个属性或整个对象(所有属性)进行不可变。
    • 不允许嵌套不可变性,因此属性可以包含另一个可变对象。
    • 不可变属性仍然可以在所有者对象内部被修改。
  • 为了防止歧义,必须使用方法with而不是set来修改不可变属性。对不可变属性使用set会导致异常,反之亦然。
  • 无法取消设置不可变属性,这会导致异常。
  • 当在类上定义#[Immutable]时,它必须在层次结构的顶部定义,而不是在中间某处。这是为了在整个继承中强制一致性。这实际上禁止了父类是可变的,但在派生类中,相同的继承属性突然变为不可变的情况。

配置继承

继承是直接工作的。父类声明中的属性继承到派生类中,可以被覆盖(除了#[Immutable]#[Format])。

以下示例说明了继承的内部机制:

use margusk\Accessors\Attr\{
    Getter, Setter
};
use margusk\Accessors\Accessible;

// Class A makes all it's properties readable/writable by default
#[Getter(true),Setter(true)]
class A
{
    use Accessible;

    protected string $foo = "foo";
}

// Class B inherits implicit #[Get(true)] from parent class but disables explicitly
// write access for all it's properties with #[Setter(false)]
#[Setter(false)]
class B extends A
{
    protected string $bar = "bar";

    // Make exception for $bar and disable it's read access explicitly
    #[Get(false)]
    protected string $baz = "baz";
}

$b = new B();
$b->foo = 'new foo';    // Works because $foo was defined by class A and that's where it gets it's
                        // write access
echo $b->foo;           // Outputs "new foo"
echo $b->bar;           // Outputs "bar"

$b->bar = "new bar";    // Results in Exception because class A disables write access by default for
                        // all it's properties

echo $b->baz;           // Results in Exception because read access was specifically for $baz disabled

属性名的大小写敏感

处理属性名称的大小写敏感性时,以下规则适用:

  1. 当使用方法语法访问属性时,其中属性名称是方法名称的一部分,那么它被视为不区分大小写。因此,如果您有属性仅在大小写上不同,那么将使用最后定义的属性。
use margusk\Accessors\Attr\{
    Getter, Setter
};
use margusk\Accessors\Accessible;

#[Getter,Setter]
class A
{
    use Accessible;

    protected string $foo = 'foo';
    protected string $FOO = 'FOO';
}

$a = new A();
$a->setFoo('value');        // Case insensitive => $FOO is modified
$a->foo('value');           // Case insensitive => $FOO is modified
$a->Foo('value');           // Case insensitive => $FOO is modified
  1. 在所有其他情况下,属性名称始终被视为区分大小写
$a->set('foo', 'value');    // $foo is modified
echo $a->foo;               // Outputs "foo"
echo $a->FOO;               // Outputs "FOO"
echo $a->Foo;               // Results in Exception because property $Foo doesn't exist

IDE 自动完成

使用魔法方法的访问器可能会带来一些缺点,例如失去IDE自动完成功能,并使静态代码分析器陷入困境。

为了通知静态代码解析器关于魔法方法和属性的可用性,应在类的DocBlock中指定PHPDocs的@method@property

use margusk\Accessors\Accessible;
use margusk\Accessors\Attr\{Getter, Setter};

/**
 * @property        string $foo
 * @property-read   string $bar
 * 
 * @method string   getFoo()
 * @method self     setFoo(string $value)
 * @method string   getBar()
 */
class A
{
    use Accessible;

    #[Getter,Setter]
    protected string $foo = "foo";
    #[Getter]
    protected string $bar = "bar";
}
$a = new A();
echo $a->setFoo('foo is updated')->foo; // Outputs "foo is updated"
echo $a->bar; // Outputs "bar"

取消属性

如果有必要,也可以使用#[Delete]取消属性设置。

use margusk\Accessors\Attr\{
    Get, Delete
};
use margusk\Accessors\Accessible;

#[Getter]
class A
{
    use Accessible;

    #[Delete]
    protected string $foo;

    protected string $bar;
}

$a = new A();
$a->unsetFoo();     // Ok.
unset($a->foo);     // Ok.
unset($a->bar);     // Results in Exception

注意

  • 由于Unset是保留字,因此使用Delete作为属性名称。

高级用法

修改器

使用设置器时,有时需要在将值赋给属性之前,通过某个中间函数传递可赋值值。这个函数称为变更器,可以使用#[Mutator]属性指定。

use margusk\Accessors\Attr\{
    Getter, Setter, Mutator
};
use margusk\Accessors\Accessible;

#[Getter]
class A
{
    use Accessible;

    #[Setter,Mutator("htmlspecialchars")]
    protected string $foo;
}

$a = (new A());
$a->setFoo('<>');
echo $a->getFoo();      // Outputs "&lt;&gt;"

变更器可以验证或以其他方式操作分配给属性的值。

变更器参数必须是表示PHP callable的字符串或数组。当传递字符串时,它必须具有以下语法之一:

  1. <function>
  2. <class>::<method>
  3. $this-<method> (在运行时用属于访问器的对象实例替换$this

它可能包含名为%property%的特殊变量,该变量在运行时用适用的属性名称替换。这在为每个属性使用单独的变更器但仅在类属性中声明一次时很有用。

可调用的函数/方法必须接受可赋值值作为第一个参数,并必须返回要分配给属性的值。

访问器端点

当前库可以利用任何手动创建的带有前缀setgetissetunsetwith的访问器方法,并后跟属性名称。这些被称为访问器端点。

例如,这允许在现有方法语法之上提供直接赋值语法的无缝集成。

use margusk\Accessors\Attr\{
    Getter, Setter
};
use margusk\Accessors\Accessible;

#[Getter,Setter]
class A
{
    use Accessible;

    protected int $foo = 0;

    public function getFoo(): int
    {
        return $this->foo + 1;
    }

    public function setFoo(int $value): void
    {
        $this->foo = $value & 0xFF;
    }
}

$a = new A();
$a->foo = 1023;
echo $a->foo;         // Outputs "256" instead of "1023"
echo $a->getFoo();    // Outputs "256" instead of "1023"

这两个端点(getFoo/setFoo)将在每种情况下都被调用:

  • 要么是使用直接赋值语法访问属性(例如$a->foo
  • 要么是使用方法语法访问属性(例如$a->getFoo()
    • 如果端点的可见性是public,那么它将由PHP引擎自然调用。
    • 如果它是protected,则通过Accessible特质提供的__call魔法方法传递。

注意

  • 端点方法必须以字符串setgetissetunsetwith开头,后跟属性名称。
  • 只有实例方法被检测到,static方法不起作用。
  • 只有publicprotected方法被检测到,private方法不起作用。
  • 修改器 被绕过,应该在setter端点本身内部完成。
  • 端点的返回值处理如下。来自
    • getisset 的值传递给调用者。
    • setunset 被丢弃,并且始终返回当前对象实例。
    • with 端点仅在值是 object 并且派生自当前类时才传递给调用者。其他值被丢弃,并且原始调用者获得 clone 的对象实例。

自定义访问器方法名的格式

通常访问器方法的名称只是直接使用 驼峰式命名法 的名称,如 get<属性>set<属性> 等。但如果需要,也可以自定义。

可以使用 #[Format(class-string<FormatContract>)] 属性来实现自定义,其中第一个参数指定类名。此类负责解析访问器方法名称。

以下示例将 驼峰式命名法 方法转换为 蛇形命名法

use margusk\Accessors\Attr\{
    Getter, Setter, Format
};
use margusk\Accessors\Format\Method;
use margusk\Accessors\Format\Standard;
use margusk\Accessors\Accessible;

class SnakeCaseFmt extends Standard
{
    public function matchCalled(string $method): ?Method
    {
        if (
            preg_match(
                '/^(' . implode('|', Method::TYPES) . ')_(.*)/i',
                strtolower($method),
                $matches
            )
        ) {
            $methodName = $matches[1];
            $propertyName = $matches[2];

            return new Method(
                Method::TYPES[$methodName],
                $propertyName
            );
        }

        return null;
    }
}

#[Getter,Setter,Format(SnakeCaseFmt::class)]
class A
{
    use Accessible;

    protected string $foo = "foo";
}

$a = new A();
echo $a->set_foo("new foo")->get_foo();     // Outputs "new foo"
echo $a->setFoo("new foo");                 // Results in Exception

注意

  • #[Format(...)] 只能定义在类和层次结构的顶部。这是为了在整个继承树中保持一致性。这有效地防止了在父类中使用一种语法,但在派生类中语法突然改变的情况。

PHPDocs 支持

但是,“我总是在类前面使用PHPDoc标签来指示IDE关于魔法属性。那些注释不会像魔法一样作为配置使用,这样我就不必使用属性重复输入相同的内容?

如果您考虑以下注意事项(如下),那么它们确实可以工作。仅使用 PHPDoc 标签配置的类

use margusk\Accessors\Accessible\WithPHPDocs as AccessibleWithPHPDocs;

/**
 * @property        string $foo 
 * @property-read   string $bar
 */
class A
{
    use AccessibleWithPHPDocs;

    protected string $foo = "foo";

    protected string $bar = "bar";
}

$a = new A();
echo $a->setFoo("new foo")->getFoo();   // Outputs "new foo"
echo $a->bar;                           // Outputs "bar"
$a->setBar("new bar");                  // Results in Exception

警告:这次注入了 AccessibleWithPHPDocs 特性而不是 Accessible。这是为了明确使用 PHPDocs 支持。

PHPDoc 标签的问题在于,根据服务器的配置,它们可能完全没有用处。

如果启用了 OPCache 扩展 并且与 opcache.save_comments=0 配置,那么源代码中的注释对于 ReflectionClass::getDocComment() 来说将丢失。

通常,当使用 OPCache 默认设置时,将保留注释。但出于性能考虑,始终有可能将其关闭。因此,依赖于此功能需要明确考虑,以确保开发的代码在部署的环境中能够正常工作。

如果您不确定,请坚持使用属性。它们总是可以工作。

API

公开属性

  1. 在您想要公开属性的类中使用 margusk\Accessors\Accessible 特性。
  2. 使用以下属性进行配置
    • 在您想要公开的属性或整个声明之前使用 #[Getter]#[Setter] 和/或 #[Delete](来自命名空间 margusk\Accessors\Attr
      • 它们都接受一个可选的 bool 参数,可以将其设置为 false 以拒绝特定访问器对属性或整个类的访问。这在需要覆盖先前设置的情况下很有用。
      • #[Getter(bool $enabled = true)]:允许或禁用对属性的读取访问。与允许/禁止属性上的 isset 一起使用。
      • #[Setter(bool $enabled = true)]:允许或禁用对属性的写入访问。
      • #[Delete(bool $enabled = true)]:允许或禁用属性的 unset()
      • #[ToString(bool $enabled = true)]:将类转换为字符串。
      • #[NotCloneable(bool $enabled = true)]:不允许类被克隆。
      • #[NotSerializable(bool $enabled = true)]:不允许类被序列化。
    • #[Mutator(string|array|null $callback)]:
      • $callback 参数的工作方式几乎与 callable 相同,但 string 类型有一些调整。
      • 如果使用 string 类型,则它必须包含常规函数名称或 $this->someMutatorMethod 语法,这表示实例方法。
      • 使用 array 类型来指定静态类方法。
      • 并使用 null 来弃用任何之前设置的突变器。
    • #[Immutable]:将属性或整个类变为不可变。当应用于类时,它必须在层次结构的顶部定义。一旦定义,之后就不能再禁用。
    • #[Format(class-string)]:允许自定义访问器方法名称。

注意

  • 仅支持 protected 属性。由于当前库的实现,private 属性可能会在继承链中引入意外的行为和异常,这次不考虑。

访问器方法

读取属性

  • $value = $obj->foo;
  • $value = $obj->getFoo();
  • $value = $obj->get('foo');
  • $value = $obj->foo();

更新可变属性(支持方法链)

  • $a->foo = 'new foo';
  • $a = $a->setFoo('new foo')->setBar('new bar');
  • $a = $a->set('foo', 'new foo')->set('bar', 'new bar');
  • $a = $a->set(['foo' => 'new foo', 'bar' => 'new bar', ..., 'baz' => 'new baz']);
  • $a = $a->foo('new foo')->bar('new bar');

更新不可变属性(支持方法链)

  • $b = $a->withFoo('new foo')->withBar('new bar');
  • $b = $a->with('foo', 'new foo')->with('bar', 'new bar');
  • $b = $a->with(['foo' => 'new foo', 'bar' => 'new bar', ..., 'baz' => 'new baz']);

取消设置属性(支持方法链)

  • unset($a->foo);
  • $a = $a->unsetFoo()->unsetBar();
  • $a = $a->unset('foo')->unset('bar');
  • $a = $a->unset(['foo', 'bar', ..., 'baz']);

检查属性是否已初始化(返回 bool

  • $isFooSet = isset($a->foo);
  • $isFooSet = $a->issetFoo();
  • $isFooSet = $a->isset('foo');

许可证

此库在 MIT 许可证下发布。