PHP Attributes Overview, plus Symfony, Doctrine ORM, laravel-php-attributes

PHP8 includes a feature called Attributes, that’s similar to docblock annotations familiar to framework users, or annotations in Java or other languages. The main difference is that Attributes are built into PHP.

Conceptually, Attributes are a kind of configuration or metadata that’s interspersed into the code. By “configuration” I don’t mean runtime configuration, or configuration performed at application installation.

Rather, it’s for frameworks, during the “wiring up the application” phase, performed before your code is executed. The application framework analyzes the code, plucks out Attributes, and wires up the application.

Implicit in this is the fact you aren’t programming your application using the traditional class instantiation style like this:

class Foo
{
    function __construct() { ... }
    function dothis() { ... }
    function dothat() { ... }
}

$foo = new Foo('abc');
$foo->dothis();
$foo->dothat();

Rather, you are using programming using a framework (using the Hollywood principle), where the framework controls when a method is called:

class Foo extends Service
{
    function __construct() { ... }
    function dothis() { ... }
    function dothat() { ... }
}

// tell the framework to use this class
$framework->register(class::Foo, 'fooservice');
// the framework will call dothis() and dothat() for me
$framework->callManyMethods('fooservice', ['dothis', 'dothat']);

The purpose of the framework is to be able to swap out different service subclasses. This way, we can develop applications using generic logic, that calls out to generic services (theoretically).

PHP Attributes allow you to take what would have been runtime configuration within the service subclass, and move it into an Attribute.

Here’s an example of a runtime configuration. Imagine that we have a command line interface (CLI) framework that helps to create command line tools.

class Foo extends Service
{
    function __construct($cliframework) {
       $cliframework->makeCommand('app dothis', () => $this->dothis());
       $cliframework->makeCommand('app dothat', () => $this->dothat());
    }
    function dothis() { ... }
    function dothat() { ... }
}

$cli = new CLIFramework();
$framework->register(new Foo($cli), 'fooservice');

# we can now call
# bin/cli app dothis
# bin/cli app dothat

We could use Attributes to develop a system that implements makeCommand as an attribute, and the code could look like this:

class Foo extends Service
{
    function __construct($cliframework) { ... }

    #[MakeCommand('app dothis')]
    function dothis() { ... }

    #[MakeCommand('app dothat')]
    function dothat() { ... }
}

$framework->register(new CLIFramework(), 'cliservice');
$framework->register(new Foo(), 'fooservice');

# we can now call
# bin/cli app dothis
# bin/cli app dothat

Behind the scenes, the code is a lot more convoluted. When the Foo service is registered, the framework would use the Reflection API to examine the Foo class, and extract all the Attributes. For each Attribute, it would execute the CLIFramework’s makeCommand() method, roughly like this:

$cli = $framework->getService('cliservice');
$foo = $framework->getService('fooservice');
$cli->makeCommand('app dothis', () => $foo->dothis() );
$cli->makeCommand('app dothat', () => $foo->dothat() );

While the number of lines of code in the application doesn’t really change, having the configs in the Attributes is a bit cleaner looking. When you’re reading the code, you don’t need to jump from the setup code in the constructor, to the function definition. You also don’t need to see the details how the method is called. In the example, I used a closure, but I could also have set it up to save it as an array, and call it a whole different way, later:

$cli->makeCommand('app dothis', array('fooservice', 'dothis'));

I didn’t get into how we parse the following:

#[MakeCommand('app dothis')]

This is the main advantage of PHP Attributes: we don’t parse it. We use PHP to parse it.

The Attribute name can be treated like a class name, and PHP can instantiate the class with the parameters specified. This saves you from writing code to parse these attributes, and all your metadata will now have the same syntax!

See the PHP Docs for more information.

The MakeCommand Attribute class might be defined like this:

#[Attribute]
class MakeCommand
{
    public $name;

    function __construct($name)
    {
        $this->name = $name;
    }
}

# Then, in the framework, we can use this method to instantiate the class:

$makecommand = $attribute->newInstance();

While that class above doesn’t do anything special, you could easily add some validation code, maybe add a strtolower() around $name, add getter methods, caching, etc. The #[Attribute] can also take parameters to restrict how the new Attribute is used.

So, while this isn’t a complete framework for implementing Attributes, PHP gives you some basic tools that will save you time, and code, for implementing your own Attributes.

Examples of Implementations

To get an idea of how to design attributes and use them in your own code, here are some implementations of Attributes.

Doctrine ORM supports Attributes.

Symfony uses Attributes throughout the framework.

Laravel doesn’t seem to use Attributes, but the laravel-php-attribute framework allows programmers to add attributes to their own code. This package implements caching so that the attributes are read only the first time the framework is loaded, and then cached to PHP files. Subsequent reads are fast.

Also read the PHP Documentation for Attributes.