dave-liddament / php-language-extensions
Attributes for extending the PHP language, using static analysis to enforce new language constructs
Installs: 224 363
Dependents: 2
Suggesters: 0
Security: 0
Stars: 132
Watchers: 7
Forks: 5
Open Issues: 4
Requires
- php: ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
Requires (Dev)
README
This library provides attributes that are used by static analysers to enforce new language features. The intention, at least initially, is that these extra language features are enforced by static analysis tools (such as Psalm, PHPStan and, ideally, PhpStorm) and NOT at runtime.
Language feature added:
Contents
-
- Friend
- MustUseResult
- NamespaceVisibility
- InjectableVersion
- Override
- RestrictTraitTo
- Sealed
- TestTag
- Deprecated
- Package replace with NamespaceVisibility
Installation
To make the attributes available for your codebase use:
composer require dave-liddament/php-language-extensions
NOTE: This only installs the attributes. A static analysis tool is used to enforce these language extensions. Use one of these:
PHPStan
If you're using PHPStan then use this extension to enforce the rules.
composer require --dev dave-liddament/phpstan-php-language-extensions
Psalm
Coming soon.
New language features
Friend
A method or class can supply via a #[Friend]
attribute a list of classes. Only these classes can call the method.
This is loosely based on the C++ friend feature.
In the example below the Person::__construct
method can only be called from 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();
NOTES:
- Multiple classes can be specified. E.g.
#[Friend(Foo::class, Bar::class)]
- A class can have a
#[Friend]
attribute, classes listed here are applied to every method.#[Friend(Foo::class)] class Entity { public function ping(): void // ping has friend Bar { } }
- The
#[Friend]
attribute is additive. If a class and a method have the#[Friend]
the method can be called from any of the classes listed. E.g.#[Friend(Foo::class)] class Entity { #[Friend(Bar::class)] public function pong(): void // pong has friends Foo and Bar { } }
- This is currently limited to method calls (including
__construct
).
MustUseResult
A #[MustUseResult]
attribute can be used on methods. This enforces the result from the method call must be used.
E.g. if you have a class like this:
class Money { public function __construct(public readonly int $pence) {} #[MustUseResult] public function add(int $pence): self { return new self($pence + $this->pence); } }
You might misuse the add
method in this way:
$cost = new Money(5); $cost->add(6); // ERROR - The call to the add method has no effect.
But this would be OK:
$cost = new Money(5); $updatedCost = $cost->add(6); // OK - The return from add method is being used.
NamespaceVisibility
The #[NamespaceVisibility]
attribute acts as extra visibility modifier like public
, protected
and private
.
By default, the #[NamespaceVisibility]
attribute limits the visibility of a class or method to only being accessible from in the same namespace, or sub namespace.
Example applying #[NamespaceVisibility]
to the Telephone::ring
method:
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 } } }
The #[NamespaceVisibility]
attribute has 2 optional arguments:
excludeSubNamespaces option
This is a boolean value. Its default value is false. If set to true then calls to methods from sub namespaces are not allowed. E.g.
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 } } }
namespace option
This is a string or null value. Its default value is null. If it is set, then this is the namespace that you are allowed to call the method on.
In the example below you can only call Telephone::ring
from the Bar
namespace.
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 on classes
If a class was the #[NamespaceVisibility]
Attribute, then all its public methods are treated as Namespace visibility.
E.g.
namespace Foo { #[NamespaceVisibility()] class Telephone { public function ring(): void // This method has NamespaceVisibility { } } }
If both the class and one of the class's methods has a #[NamespaceVisibility]
attribute, then the method's attribute
takes precedence.
namespace Foo { #[NamespaceVisibility(namespace: 'Bar')] class Telephone { #[NamespaceVisibility(namespace: 'Baz')] public function ring(): void // This method can only be called from the namespace Baz { } } }
NOTES:
- If adding the
#[NamespaceVisibility]
to a method, this method MUST have public visibility. - This is currently limited to method calls (including
__construct
).
InjectableVersion
The #[InjectableVersion]
is used in conjunction with dependency injection.
#[InjectableVersion]
is applied to a class or interface.
It denotes that it is this version and not any classes that implement/extend that should be used in the codebase.
E.g.
#[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 ) }
This also works for collections:
#[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 ) }
By default, only constructor arguments are checked. Most DI should be done via constructor injection.
In cases where dependencies are injected by methods that aren't constructors, the method must be marked with a #[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
The #[Override]
attribute is used to denote that a method is overriding a method in a parent class. This is the functionality is similar to the @override
annotation in Java.
This is temporary until PHP 8.3 is released. See the RFC that will be implemented in PHP 8.3.
NOTE:
- If you are using PHP 8.3 then use the real
#[Override]
attribute. - This implementation doesn't consider traits.
RestrictTraitTo
This limits the use of a Trait to only be used by a specified class of a child of that class.
E.g. this trait is limited to classes that are or extend Controller
#[RestrictTraitTo(Controller::class)] trait ControllerHelpers {}
This would be allowed:
class LoginController extends Controller { use ControllerHelpers; }
But this would NOT be allowed:
class Repository { use ControllerHelpers; }
Sealed
This is inspired by the rejected sealed classes RFC
The #[Sealed]
attribute takes a list of classes or interfaces that can extend/implement the class/interface.
E.g.
#[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
The #[TestTag]
attribute is an idea borrowed from hardware testing. Classes or methods marked with this attribute are only available to test code.
E.g.
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. } }
NOTES:
- Classes with the
#[TestTag]
will have an error when any interaction with the class is done. - Methods with the
#[TestTag]
MUST have public visibility. - For determining what is "test code" see the relevant plugin. E.g. the PHPStan extension can be setup to either:
- Assume all classes that end
Test
is test code. See className config option. - Assume all classes within a given namespace is test code. See namespace config option.
- Assume all classes that end
Deprecated Attributes
Package (deprecated)
The #[Package]
attribute acts like an extra visibility modifier like public
, protected
and private
. It is inspired by Java's package
visibility modifier.
The #[Package]
attribute limits the visibility of a class or method to only being accessible from code in the same namespace.
This has been replaced by the #[NamespaceVisibility]
attribute. To upgrade replace:
#[Package]
with #[NamespaceVisibility(excludeSubNamespaces=true)]
NOTES:
- If adding the
#[Package]
to a method, this method MUST have public visibility. - If a class is marked with
#[Package]
then all its public methods are treated as having package visibility. - This is currently limited to method calls (including
__construct
). - Namespaces must match exactly. E.g. a package level method in
Foo\Bar
is only accessible fromFoo\Bar
. It is not accessible fromFoo
orFoo\Bar\Baz
Further examples
More detailed examples of how to use attributes is found in examples.
Contributing
See Contributing.