dave-liddament/php-language-extensions

用于扩展PHP语言的属性,使用静态分析来强制执行新的语言结构

0.8.1 2024-08-12 19:13 UTC

This package is auto-updated.

Last update: 2024-09-12 20:06:40 UTC


README

PHP versions: 8.0 to 8.3 Latest Stable Version License Total Downloads

Continuous Integration Psalm level 1 PHPStan level 8

此库提供了由静态分析器(如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] 属性类似于 publicprotectedprivate 的额外可见性修饰符。默认情况下,#[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 扩展 可以设置以

已弃用的属性

包(已弃用)

#[Package] 属性类似于额外的可见性修饰符,如 publicprotectedprivate。它受到 Java 的 package 可见性修饰符的启发。 #[Package] 属性将类或方法的可见性限制为仅从同一命名空间中的代码访问。

这已被 #[NamespaceVisibility] 属性所取代。要升级,请将

#[Package] 替换为 #[NamespaceVisibility(excludeSubNamespaces=true)]

注意

  • 如果将 #[Package] 添加到方法中,则此方法必须具有公共可见性。
  • 如果类标记为 #[Package],则其所有公共方法都被视为具有包可见性。
  • 这目前仅限于方法调用(包括 __construct)。
  • 命名空间必须完全匹配。例如,在 Foo\Bar 中的包级别方法仅从 Foo\Bar 访问。它不能从 FooFoo\Bar\Baz 访问。

更多示例

有关如何使用属性的更详细示例,请参阅 示例

贡献

请参阅 贡献