margusk / accessors
提供自动属性访问器(setter/getter)和不可变支持
Requires
- php: ^8.0
- phpstan/phpdoc-parser: ^1.13
Requires (Dev)
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^9.5
README
访问器
当前库可以创建自动访问器(例如 getters 和 setters)以访问对象属性。它通过向所需的类中注入一个具有属性重载的魔法方法的特质,在尝试访问 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 方法支持。
- 为 setters 支持 mutator。
要求
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\Get; use margusk\Accessors\Accessible; class A { use Accessible; #[Get] protected string $foo = "foo"; #[Get] protected string $bar = "bar"; #[Get] protected string $baz = "baz"; } $a = new A(); echo $a->getFoo(); // Outputs "foo" echo $a->getBar(); // Outputs "bar" echo $a->getBaz(); // Outputs "baz"
除了避免 getters 的样板代码之外,现在还有可用的 直接赋值 语法,这在初始类中甚至是不可能的
echo $a->foo; // Outputs "foo" echo $a->bar; // Outputs "bar" echo $a->baz; // Outputs "baz"
如果有大量的属性要公开,则单独标记每个属性是不合理的。在类声明中一次标记所有属性
use margusk\Accessors\Attr\Get; use margusk\Accessors\Accessible; #[Get] class A { use Accessible; protected string $foo = "foo"; protected string $bar = "bar"; protected string $baz = "baz"; }
现在关闭 $baz
的可读性
use margusk\Accessors\Attr\Get; use margusk\Accessors\Accessible; #[Get] class A { use Accessible; protected string $foo = "foo"; protected string $bar = "bar"; #[Get(false)] protected string $baz = "baz"; } $a = new A(); echo $a->getFoo(); // Outputs "foo" echo $a->getBar(); // Outputs "bar" echo $a->getBaz(); // Results in Exception
关于写入属性?是的,只需添加 #[Set]
属性
use margusk\Accessors\Attr\{ Get, Set }; use margusk\Accessors\Accessible; #[Get,Set] 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\Get; use margusk\Accessors\Accessible; #[Get] 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\{ Get, Set, Immutable }; use margusk\Accessors\Accessible; #[Get,Set,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\{ Get, Set }; use margusk\Accessors\Accessible; // Class A makes all it's properties readable/writable by default #[Get(true),Set(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 #[Set(false)] #[Set(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
属性名的大小写敏感
处理属性名称的大小写敏感度时,以下规则适用
- 当使用方法语法访问属性时,属性名称是方法名称的一部分,那么它被视为不区分大小写。因此,如果由于某种原因你有仅大小写不同的属性,那么最后定义的属性将被使用
use margusk\Accessors\Attr\{ Get, Set }; use margusk\Accessors\Accessible; #[Get,Set] 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
- 在所有其他情况下,属性名称始终被视为 大小写敏感
$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 中指定 @method 和 @property。
use margusk\Accessors\Accessible; use margusk\Accessors\Attr\{Get, Set}; /** * @property string $foo * @property-read string $bar * * @method string getFoo() * @method self setFoo(string $value) * @method string getBar() */ class A { use Accessible; #[Get,Set] protected string $foo = "foo"; #[Get] 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; #[Get] 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
使用 setter 时,有时需要将可赋值值通过某个中间函数传递,然后再将其分配给属性。这个函数称为 mutator,可以使用 #[Mutator]
属性来指定
use margusk\Accessors\Attr\{ Get, Set, Mutator }; use margusk\Accessors\Accessible; #[Get] class A { use Accessible; #[Set,Mutator("htmlspecialchars")] protected string $foo; } $a = (new A()); $a->setFoo('<>'); echo $a->getFoo(); // Outputs "<>"
它可以验证或以其他方式操作要分配给属性的值。
mutator 参数必须是表示 PHP callable
的字符串或数组。当传递字符串时,它必须具有以下语法之一
<function>
<class>::<method>
$this-><method>
($this
在运行时被替换为访问器所属的对象实例)
它可能包含名为 %property%
的特殊变量,该变量在运行时被替换为应用的属性名称。这当使用每个属性分别声明但只在类属性中声明一次的单独 mutator 时很有用。
可调用的函数/方法必须接受可赋值值作为第一个参数,并返回要分配给属性的值。
访问器端点
当前库可以制作任何手动创建的具有前缀 set
、get
、isset
、unset
或 with
的访问器方法,并跟随属性名称。这些被称为访问器端点。
例如,这允许无缝集成,其中当前库在现有的 方法语法 上提供 直接赋值语法
use margusk\Accessors\Attr\{ Get, Set }; use margusk\Accessors\Accessible; #[Get,Set] 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
trait 提供的__call
魔法方法进行。
- 如果端点的可见性是
注意
- 端点方法必须以字符串
set
、get
、isset
、unset
或with
开头,后面跟属性名称。 - 只有实例方法会被检测到,
static
方法不起作用。 - 仅检测
public
或protected
方法,private
方法将不起作用。 - mutator 被绕过,应在 setter 端点本身完成。
- 端点的返回值处理如下。来自
get
和isset
的值传递给调用者。set
和unset
被丢弃,并始终返回当前对象实例。with
端点仅在值是object
并且派生自当前类时才传递给调用者 仅限 此情况。其他值将被丢弃,原始调用者获得clone
-d 对象实例。
自定义访问器方法名的格式
通常,访问器方法的名称只是直接使用 camel-case 命名,如 get<property>
、set<property>
等。但如果需要,它可以进行自定义。
可以使用 #[Format(class-string<FormatContract>)]
属性来实现自定义,第一个参数指定类名。该类负责解析访问器方法名称。
以下示例将 camel-case 方法转换为 snake-case
use margusk\Accessors\Attr\{ Get, Set, 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; } } #[Get,Set,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
trait 而不是 Accessible
。这是为了明确使用 PHPDocs 支持。
PHPDoc 标签的问题在于,根据服务器的配置,它们可能完全没有用。
如果启用了 OPCache 扩展 并使用 opcache.save_comments=0 进行配置,那么来自源代码的注解将丢失,对于 ReflectionClass::getDocComment()。
通常,当使用 OPCache 默认设置时,将保留注释。但出于性能原因,始终有可能关闭它们。这就是为什么依赖于该功能需要明确考虑,以确保开发代码能够在部署的环境中运行。
如果您不确定,请坚持使用属性。它们始终有效。
API
公开属性
- 在您想公开属性的类中使用
margusk\Accessors\Accessible
trait。 - 使用以下属性进行配置
- 在要公开的属性或整个声明之前使用
#[Get]
、#[Set]
和/或#[Delete]
(来自命名空间margusk\Accessors\Attr
)- 所有这些都接受一个可选的
bool
参数,可以将其设置为false
以拒绝特定访问器对属性或整个类的访问。这在需要覆盖先前设置的情况下很有用。 #[Get(bool $enabled = true)]
:允许或禁用对属性的读取访问。与允许/禁止属性上的isset
一起工作。#[Set(bool $enabled = true)]
:允许或禁用对属性的写入访问。#[Delete(bool $enabled = true)]
:允许或禁用属性的unset()
。
- 所有这些都接受一个可选的
#[Mutator(string|array|null $callback)]
:$callback
参数几乎像callable
,但在string
类型上有一些调整。- 如果使用
string
类型,则它必须包含常规函数名或$this->someMutatorMethod
语法表示实例方法。 - 使用
array
类型指定静态类方法。 - 并使用
null
来丢弃任何之前设置的 mutator。
#[Immutable]
:将属性或整个类变为不可变。当用于类时,必须在层次结构的顶部定义。一旦定义,就不能在以后禁用它。#[Format(class-string<FormatContract>)]
:允许自定义访问器方法名称。
- 在要公开的属性或整个声明之前使用
注意
- 仅支持
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许可协议发布。