Attributes to define PHP language extensions (to be enforced by static analysis)

Last update: Jun 23, 2022

PHP Language Extensions (currently in BETA)

This library provides attributes for extending the PHP language (e.g. adding package visibility). 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

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

To use PHPStan to enforce package level visibility add this extension.

composer require --dev dave-liddament/phpstan-php-language-extensions

Psalm

Coming soon.

New language features

Package

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.

Example applying #[Package] to methods:

namespace Foo {

  class Person 
  {
    #[Package]
    public function __construct(
      private string $name;
    ) {
    }
    
    #[Package]
    public function updateName(string $name): void
    {
        $this->name = $name;
    }
    
    public function getName(): string
    {
       return $this->name;
    }
  }

  class PersonFactory
  {
    public static function create(string $name): Person
    {
      return new Person($name); // This is allowed
    }
  }
}

namespace Bar {

  class Demo 
  {
    public function allowed(): void 
    {
      // Code below is OK. Only calling public methods
      $jane = PersonBuilder::create("Jane");
      echo $jane->getName();
    }
  
    public function notAllowed1(Person $person): void
    {
      // ERROR with line below: `update` method has package visibility. It can only be called from the '`Foo` namespace.
      $person->updateName("Robert")
    }
  
    public function notAllowed2(): void
    {
      // ERROR with line below. Person's __construct method has package visibility. It can only be called by code in the `Foo` namespace.
      $jane = new Person(); 
    }
  }
}

Example applying #[Package] to classes:

namespace Foo {

  #[Package]
  class Mailer 
  {
    public function sendMessage(string $message): void
    {
      // Some implementation
    } 
  }
}

namespace Bar {

  class PdfSender
  {
    public function __invoke(Mailer $mailer): void
    {
      // ERROR: The method Mailer::sendMessage is on a package level class. 
      $mailer->sendMessage("some message");
    }
  }

}

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 from Foo\Bar. It is not accessible from Foo or Foo\Bar\Baz

Friend

A method or class can supply via a #[Friend] attribute a list of classes, they are friends with. Only their friend's classes may call the method. Friendship is not reciprocated, e.g. if Dog makes Cat a friend, this does not mean that Cat considers Dog a friend. This is loosely based on C++ friend feature.

Example:

class Person
{
    #[Friend(PersonBuilder::class)]
    public function __construct()
    {
        // Some implementation
    }
}


class PersonBuilder
{
    public function build(): Person
    {
        $person = new Person(): // OK PersonBuilder is a friend of Person
        // set up Person
        return $person;
    }
}


// ERROR Call to Person::__construct is not from PersonBuilder
$person = new Person();

NOTES:

  • Multiple friends can be specified. E.g. #[Friend(Foo::class, Bar::class)]
  • A class can have a #[Friend] attribute. Friendship is additive. E.g.
    #[Friend(Foo::class)]
    class Entity
    {
      #[Friend(Bar::class)] 
      public function ping(): void // ping is friends with Foo and Bar
      {
      }
    }
  • This is currently limited to method calls (including __construct).

Sealed

This attribute is a work in progress

This replicates 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. 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:

  • 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:

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.
}

Further examples

More detailed examples of how to use attributes is found in examples.

Contributing

See Contributing.

TODO

  • [] Add examples for Sealed

GitHub

https://github.com/DaveLiddament/php-language-extensions
You might also like...

The SensioLabs DeprecationDetector runs a static code analysis against your project's source code to find usages of deprecated methods, classes and interfaces

SensioLabs DeprecationDetector CAUTION: This package is abandoned and will no longer receive any updates. The SensioLabs DeprecationDetector runs a st

Jun 7, 2022

Dockerise Symfony Application (Symfony 6 + Clean Architecture+ DDD+ CQRS + Docker + Xdebug + PHPUnit + Doctrine ORM + JWT Auth + Static analysis)

Symfony Dockerise Symfony Application Install Docker Install Docker Compose Docker PHP & Nginx Create Symfony Application Debugging Install Xdebug Con

Jun 28, 2022

Zephir is a compiled high level language aimed to the creation of C-extensions for PHP.

Zephir - is a high level programming language that eases the creation and maintainability of extensions for PHP. Zephir extensions are exported to C c

Jun 15, 2022

Enforce that your classes get only instantiated by the factories you define!

Enforce that your classes get only instantiated by the factories you define!

Nov 15, 2021

Allow composer packages to define compilation steps

Composer Compile Plugin The "Compile" plugin enables developers of PHP libraries to define free-form "compilation" tasks, such as: Converting SCSS to

May 16, 2022

Static Web App to train Filipinos in using MS Word with the use of Filipino language

Static Web App to train Filipinos in using MS Word with the use of Filipino language

MS Word Filipino Isang static web application na layuning magturo ng paggamit ng MS Word sa wikang Filipino. Ito ay isang proyekto na bahagi ng panana

May 13, 2022

A repository for showcasing my knowledge of the PHP programming language, and continuing to learn the language.

Learning PHP (programming language) I know very little about PHP. This document will list all my knowledge of the PHP programming language. Basic synt

Nov 26, 2021

Allows reflection of object attributes, including inherited and non-public ones

sebastian/object-reflector Allows reflection of object attributes, including inherited and non-public ones. Installation You can add this library as a

Jun 25, 2022
Comments
  • 1. Update #[Package] functionality to allow sub namespaces

    Currently #[Package] is modelled on Java's package visibility modifier. This does not allow access to sub namespaces. E.g. in Java a call from namespace Foo\Bar would not be allowed to call a method with package visibility in Foo\Bar\Baz.

    Options:

    1. Consider changing the default behaviour of #[Package] to allow calls to sub namespaces.
    2. Add an argument to the #[Package] attribute to allow/disallow sub namespaces (possibly allowing as default).

    See this twitter comment.

    Reviewed by DaveLiddament at 2022-05-29 16:40
  • 2. Update #[Friend] to a more obvious name

    The #[Friend] attribute was named after the C++ feature. However this is not likely to be well known in the PHP community.

    Update #[Friend] to a more meaningful name. e.g. #[CallableFrom]

    See this twitter discussion.

    Reviewed by DaveLiddament at 2022-05-29 16:32
The package provides an expressive "fluent" way to define model attributes.

The package provides an expressive "fluent" way to define model attributes. It automatically builds casts at the runtime and adds a native autocompletion to the models' properties.

Jul 1, 2022
An extension for PHPStan for adding analysis for PHP Language Extensions.

PHPStan PHP Language Extensions (currently in BETA) This is an extension for PHPStan for adding analysis for PHP Language Extensions. Language feature

Jun 3, 2022
A pure PHP implementation of the open Language Server Protocol. Provides static code analysis for PHP for any IDE.
A pure PHP implementation of the open Language Server Protocol. Provides static code analysis for PHP for any IDE.

A pure PHP implementation of the open Language Server Protocol. Provides static code analysis for PHP for any IDE.

Jun 18, 2022
PHP Text Analysis is a library for performing Information Retrieval (IR) and Natural Language Processing (NLP) tasks using the PHP language

php-text-analysis PHP Text Analysis is a library for performing Information Retrieval (IR) and Natural Language Processing (NLP) tasks using the PHP l

Jun 22, 2022
Enforced disposal of objects in PHP. 🐘

Enforced disposal of objects in PHP. This package provides a Disposable interface and using() global function that can be used to enforce the disposal

Mar 25, 2022
Docker image that provides static analysis tools for PHP

Static Analysis Tools for PHP Docker image providing static analysis tools for PHP. The list of available tools and the installer are actually managed

Jun 19, 2022
Beautiful and understandable static analysis tool for PHP
Beautiful and understandable static analysis tool for PHP

PhpMetrics PhpMetrics provides metrics about PHP project and classes, with beautiful and readable HTML report. Documentation | Twitter | Contributing

Jun 24, 2022
Find undefined and unused variables with the PHP Codesniffer static analysis tool.

PHP_CodeSniffer VariableAnalysis Plugin for PHP_CodeSniffer static analysis tool that adds analysis of problematic variable use. Warns if variables ar

Jun 18, 2022