dave-liddament / php-language-extensions
用于扩展PHP语言的属性,使用静态分析来强制执行新的语言结构
Requires
- php: ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.8
- php-parallel-lint/php-parallel-lint: ^1.3
- phpstan/phpstan: ^1.5
- vimeo/psalm: ^4.22
README
此库提供了由静态分析器(如Psalm、PHPStan和理想的PhpStorm)强制执行的新语言功能的属性。至少最初,这些额外的语言功能是通过静态分析工具强制执行的,而不是在运行时。
添加的语言功能
内容
安装
要使属性可用于您的代码库,请使用
composer require dave-liddament/php-language-extensions
注意:此操作仅安装属性。需要使用静态分析工具来强制这些语言扩展。请使用以下之一
PHPStan
如果您使用PHPStan,则使用 此扩展 来强制规则。
composer require --dev dave-liddament/phpstan-php-language-extensions
Psalm
即将推出。
新语言功能
朋友
一个方法或类可以通过 #[Friend]
属性提供一系列类。只有这些类可以调用该方法。这 loosely 基于C++的friend功能。
在下面的示例中,Person::__construct
方法只能从 PersonBuilder
调用
class Person { #[Friend(PersonBuilder::class)] public function __construct() { // Some implementation } } class PersonBuilder { public function build(): Person { $person = new Person(): // OK as PersonBuilder is allowed to call Person's construct method. // set up Person return $person; } } // ERROR Call to Person::__construct is not from PersonBuilder $person = new Person();
注意
- 可以指定多个类。例如,
#[Friend(Foo::class, Bar::class)]
- 一个类可以有一个
#[Friend]
属性,列出的类应用于每个方法。#[Friend(Foo::class)] class Entity { public function ping(): void // ping has friend Bar { } }
#[Friend]
属性是可添加的。如果一个类和一个方法都有#[Friend]
,则可以从列出的任何类调用该方法。例如:#[Friend(Foo::class)] class Entity { #[Friend(Bar::class)] public function pong(): void // pong has friends Foo and Bar { } }
- 这目前仅限于方法调用(包括
__construct
)。
必须使用结果
可以在方法上使用 #[MustUseResult]
属性。这强制从方法调用中必须使用结果。
例如,如果您有一个这样的类:
class Money { public function __construct(public readonly int $pence) {} #[MustUseResult] public function add(int $pence): self { return new self($pence + $this->pence); } }
您可能会这样误用 add
方法
$cost = new Money(5); $cost->add(6); // ERROR - The call to the add method has no effect.
但这将是正确的
$cost = new Money(5); $updatedCost = $cost->add(6); // OK - The return from add method is being used.
命名空间可见性
#[NamespaceVisibility]
属性类似于 public
、protected
和 private
的额外可见性修饰符。默认情况下,#[NamespaceVisibility]
属性将类或方法的可见性限制在同一个命名空间或子命名空间内。
应用 #[NamespaceVisibility]
到 Telephone::ring
方法的示例
namespace Foo { class Telephone { #[NamespaceVisibility] public function ring(): void { } } class Ringer { public function ring(Telephone $telephone): Person { $telephone->ring(); // OK calling Telephone::ring() from same namespace } } } namespace Foo\SubNamespace { use Foo\Telephone; class SubNamespaceRinger { public function ring(Telephone $telephone): Person { $telephone->ring(); // OK calling Telephone::ring() from sub namespace } } } namespace Bar { use Foo\Telephone; class DifferentNamespaceRinger { public function ring(Telephone $telephone): Person { $telephone->ring(); // ERROR calling Telephone::ring() from different namespace } } }
#[NamespaceVisibility]
属性有两个可选参数
excludeSubNamespaces 选项
这是一个布尔值。默认值为 false。如果设置为 true,则不允许从子命名空间调用方法。例如:
namespace Foo { class Telephone { #[NamespaceVisibility(excludeSubNamespaces: true)] public function ring(): void { } } } namespace Foo\SubNamespace { use Foo\Telephone; class SubNamespaceRinger { public function ring(Telephone $telephone): Person { $telephone->ring(); // ERROR - Not allowed to call Telephone::ring() from a sub namespace } } }
命名空间选项
这是一个字符串或 null 值。默认值为 null。如果设置,则这是您可以调用该方法的命名空间。
在下面的示例中,您只能从 Bar
命名空间调用 Telephone::ring
。
namespace Foo { class Telephone { #[NamespaceVisibility(namespace: "Bar")] public function ring(): void { } } class Ringer { public function ring(Telephone $telephone): void { $telephone->ring(); // ERROR - Can only all Telephone::ring() from namespace Bar } } } namespace Bar { use Foo\Telephone; class AnotherRinger { public function ring(Telephone $telephone): void { $telephone->ring(); // OK - Allowed to call Telephone::ring() from namespace Bar } } }
类上的命名空间可见性
如果类是 #[NamespaceVisibility]
属性,则所有公共方法都被视为命名空间可见性。
例如:
namespace Foo { #[NamespaceVisibility()] class Telephone { public function ring(): void // This method has NamespaceVisibility { } } }
如果类及其中的一个方法都具有 #[NamespaceVisibility]
属性,则方法的属性优先。
namespace Foo { #[NamespaceVisibility(namespace: 'Bar')] class Telephone { #[NamespaceVisibility(namespace: 'Baz')] public function ring(): void // This method can only be called from the namespace Baz { } } }
注意
- 如果将
#[NamespaceVisibility]
添加到方法中,则此方法必须具有公共可见性。 - 这目前仅限于方法调用(包括
__construct
)。
可注入版本
#[InjectableVersion]
用于与依赖注入结合使用。 #[InjectableVersion]
应用到类或接口。它表示应使用此版本,而不是实现/扩展该类的任何类。
例如:
#[InjectableVersion] class PersonRepository {...} // This is the version that should be type hinted in constructors. class DoctrinePersonRepository extends PersonRepository {...} class PersonCreator { public function __construct( private PersonRepository $personRepository, // OK - using the injectable version ) } class PersonUpdater { public function __construct( private DoctrinePersonRepository $personRepository, // ERROR - not using the InjectableVersion ) }
这也适用于集合。
#[InjectableVersion] interface Validator {...} // This is the version that should be type hinted in constructors. class NameValidator implements Validator {...} class AddressValidator implements Validator {...} class PersonValidator { /** @param Validator[] $validators */ public function __construct( private array $validators, // OK - using the injectable version ) }
默认情况下,只有构造函数参数会被检查。大多数依赖注入应通过构造函数注入完成。
在依赖注入是通过非构造函数的方法进行的情况下,该方法必须标记为 #[CheckInjectableVersion]
#[InjectableVersion] interface Logger {...} class FileLogger implements Logger {...} class MyService { #[CheckInjectableVersion] public function setLogger(Logger $logger): void {} // OK - Injectable Version injected public function addLogger(FileLogger $logger): void {} // No issue raised because addLogger doesn't have the #[CheckInjectableVersion] attribute. }
覆盖
#[Override]
属性用于表示方法正在覆盖父类中的方法。这与 Java 中的 @override
注解的功能相似。
这将是临时的,直到 PHP 8.3 发布。请参阅将要在 PHP 8.3 中实现的RFC。
注意
- 如果您正在使用 PHP 8.3,则请使用真实的
#[Override]
属性。 - 此实现不考虑特性。
限制特质到
这限制了特性只能由指定类的子类使用。
例如,此特性仅限于那些是或扩展 Controller
的类。
#[RestrictTraitTo(Controller::class)] trait ControllerHelpers {}
这将是被允许的
class LoginController extends Controller { use ControllerHelpers; }
但以下将不被允许
class Repository { use ControllerHelpers; }
密封
这灵感来源于被拒绝的 密封类 RFC
#[Sealed]
属性接受一个可以扩展/实现类/接口的类或接口列表。
例如:
#[Sealed([Success::class, Failure::class])] abstract class Result {} // Result can only be extended by Success or Failure // OK class Success extends Result {} // OK class Failure extends Result {} // ERROR AnotherClass is not allowed to extend Result class AnotherClass extends Result {}
测试标签
#[TestTag]
属性是从硬件测试中借鉴的一个想法。带有此属性的类或方法仅对测试代码可用。
例如:
class Person { #[TestTag] public function setId(int $id) { $this->id = $id; } } function updatePersonId(Person $person): void { $person->setId(10); // ERROR - not test code. } class PersonTest { public function setup(): void { $person = new Person(); $person->setId(10); // OK - This is test code. } }
注意
- 带有
#[TestTag]
的类在与类有任何交互时都会出现错误。 - 带有
#[TestTag]
的方法必须具有公共可见性。 - 有关确定“测试代码”的信息,请参阅相关插件。例如,PHPStan 扩展 可以设置以
- 假定所有以
Test
结尾的类都是测试代码。请参阅 className 配置选项。 - 假定给定命名空间内的所有类都是测试代码。请参阅 namespace 配置选项。
- 假定所有以
已弃用的属性
包(已弃用)
#[Package]
属性类似于额外的可见性修饰符,如 public
、protected
和 private
。它受到 Java 的 package
可见性修饰符的启发。 #[Package]
属性将类或方法的可见性限制为仅从同一命名空间中的代码访问。
这已被 #[NamespaceVisibility]
属性所取代。要升级,请将
#[Package]
替换为 #[NamespaceVisibility(excludeSubNamespaces=true)]
注意
- 如果将
#[Package]
添加到方法中,则此方法必须具有公共可见性。 - 如果类标记为
#[Package]
,则其所有公共方法都被视为具有包可见性。 - 这目前仅限于方法调用(包括
__construct
)。 - 命名空间必须完全匹配。例如,在
Foo\Bar
中的包级别方法仅从Foo\Bar
访问。它不能从Foo
或Foo\Bar\Baz
访问。
更多示例
有关如何使用属性的更详细示例,请参阅 示例。
贡献
请参阅 贡献。