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

Overview

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
You might also like...
Perform static analysis of Drupal PHP code with phpstan-drupal.

Perform static analysis of Drupal PHP code with PHPStan and PHPStan-Drupal on Drupal using PHP 8. For example: docker run --rm \ -v $(pwd)/example01

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

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

WooCommerce function and class declaration stubs for static analysis.

WooCommerce Stubs This package provides stub declarations for WooCommerce functions, classes and interfaces. These stubs can help plugin and theme dev

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

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

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

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

[READ-ONLY] Properties define model metadata.

Charcoal Property Properties define object's metadata. They provide the building blocks of the Model's definition. Properties are defined globally for

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

Comments
  • Fix PHP version constraints

    Fix PHP version constraints

    PHP version constraints are, I think, incorrect:

    From composer.json

    Constraints are:

    "php": "^8.0 || ^8.1 || ^8.2"
    

    I think this will allow 8.3, which is wrong as we don't know 8.3 will introduce a break to attributes. (Admittedly it is very unlikely that it will).

    Version should either be:

    "php": "8.0.* || 8.1.* || 8.2.*"
    

    or

    "php": ">=8.0 <8.3"
    
    opened by DaveLiddament 6
  • New attributes CallableFrom and NamespaceVisibility

    New attributes CallableFrom and NamespaceVisibility

    1. Adds new attributes:
    • #[CallableFrom] (Addresses issue #2)
    • #[NamespaceVisibility] (Addresses issue #3)
    1. Deprecates:
    • #[Friend] (Addresses issue #2)
    • #[Package] (Addresses issue #3)
    1. Updates to README

    2. Addition of Sealed examples

    Sorry, this PR is way too big!

    opened by DaveLiddament 0
  • Update #[Package] functionality to allow sub namespaces

    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.

    opened by DaveLiddament 5
  • Update #[Friend] to a more obvious name

    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.

    opened by DaveLiddament 1
Releases(0.3.0)
Owner
Dave Liddament
Dave Liddament
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

Dave Liddament 9 Nov 30, 2022
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.

Boris Lepikhin 506 Dec 28, 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.

Felix Becker 1.1k Jan 4, 2023
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

null 464 Dec 28, 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

Ryan Chandler 52 Oct 12, 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

Jakub Zalas 1.1k Jan 1, 2023
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

PhpMetrics 2.3k Jan 5, 2023
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

Payton Swick 116 Dec 14, 2022