Skip to content
  • Homepage
  • HTML
  • CSS
  • Symfony
  • PHP
  • How to
  • Contact
  • Donate

Teach Developer

Articles, Guides & Tips

PHP 8: Attributes

Home  »  Top Tutorials   »   PHP 8: Attributes
Posted on September 25, 2022September 25, 2022
798

As of PHP 8, we’ll be able to use attributes. The goal of these attributes, also known as annotations in many other languages, is to add metadata to classes, methods, variables, and whatnot; in a structured way.

The concept of attributes isn’t new at all, we’ve been using docblocks to simulate their behavior for years now. With the addition of attributes though, we now have a first-class citizen in the language to represent this kind of metadata, instead of having to manually parse docblocks.

So what do they look like? How do we make custom attributes? Are there any caveats? Those are the questions that will be answered in this post. Let’s dive in!

Rundown

First things first, here’s what attribute would look like in the wild:

use \Support\Attributes\ListensTo;

class ProductSubscriber
{
    #[ListensTo(ProductCreated::class)]
    public function onProductCreated(ProductCreated $event) { /* … */ }

    #[ListensTo(ProductDeleted::class)]
    public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

I’ll be showing other examples later in this post, but I think the example of event subscribers is a good one to explain the use of attributes at first.

Also yes, I know, the syntax might not be what you wished or hoped for. You might have preferred @, or @:, or docblocks or, … It’s here to stay though, so we better learn to deal with it. The only thing that’s worth mentioning on the syntax is that all options were discussed, and there are very good reasons why this syntax was chosen.

That being said, let’s focus on the cool stuff: how would this ListensTo work under the hood?

First of all, custom attributes are simple classes, annotated themselves with the #[Attribute] attribute; this base Attribute used to be called PhpAttribute in the original RFC, but was changed with another RFC afterward.

Here’s what it would look like:

#[Attribute]
class ListensTo
{
    public string $event;

    public function __construct(string $event)
    {
        $this->event = $event;
    }
}

That’s it — pretty simple, right? Keep in mind the goal of attributes: they are meant to add metadata to classes and methods, nothing more. They shouldn’t — and can’t — be used for, for example, argument input validation. In other words: you wouldn’t have access to the parameters passed to a method within its attributes. There was a previous RFC that allowed this behavior, but this RFC specifically kept things more simple.

event subscriber example: we still need to read the metadata and register our subscribers based somewhere. Coming from a Laravel background, I’d use a service provider as the place to do this, but feel free to come up with other solutions.

Here’s the boring boilerplate setup, just to provide a little context:

class EventServiceProvider extends ServiceProvider
{
    // In real life scenarios, 
    //  we'd automatically resolve and cache all subscribers
    //  instead of using a manual array.
    private array $subscribers = [
        ProductSubscriber::class,
    ];

    public function register(): void
    {
        // The event dispatcher is resolved from the container
        $eventDispatcher = $this->app->make(EventDispatcher::class);

        foreach ($this->subscribers as $subscriber) {
            // We'll resolve all listeners registered 
            //  in the subscriber class,
            //  and add them to the dispatcher.
            foreach (
                $this->resolveListeners($subscriber) 
                as [$event, $listener]
            ) {
                $eventDispatcher->listen($event, $listener);
            }       
        }       
    }
}

Note that if the [$event, $listener] If the syntax is unfamiliar to you, you can get up to speed with it in my post about array destructuring.

Now let’s look at, which is where the magic happens.

private function resolveListeners(string $subscriberClass): array
{
    $reflectionClass = new ReflectionClass($subscriberClass);

    $listeners = [];

    foreach ($reflectionClass->getMethods() as $method) {
        $attributes = $method->getAttributes(ListensTo::class);
        
        foreach ($attributes as $attribute) {
            $listener = $attribute->newInstance();
            
            $listeners[] = [
                // The event that's configured on the attribute
                $listener->event,
    
                // The listener for this event 
                [$subscriberClass, $method->getName()],
            ];
        }
    }

    return $listeners;
}

You can see it’s easier to read metadata this way, compared to parsing docblock strings. There are two intricacies worth looking into though.

First, there’s the $attribute->newInstance() call. This is actually the place where our custom attribute class is instantiated. It will take the parameters listed in the attribute definition in our subscriber class, and pass them to the constructor.

This means that, technically, you don’t even need to construct the custom attribute. You could call $attribute->getArguments() directly. Furthermore, instantiating the class means you’ve got the flexibility of the constructor the parse input in whatever way you like. All in all, I’d say it would be good to always instantiate the attribute using newInstance().

The second thing worth mentioning is the use of ReflectionMethod::getAttributes(), the function that returns all attributes for a method. You can pass two arguments to it, to filter its output.

In order to understand this filtering though, there’s one more thing you need to know about attributes first. This might have been obvious to you, but I wanted to mention it real quick anyway: it’s possible to add several attributes to the same method, class, property, or constant.

You could, for example, do this:

#[
    Route(Http::POST, '/products/create'),
    Autowire,
]
class ProductsCreateController
{
    public function __invoke() { /* … */ }
}

With that in mind, it’s clear why Reflection*::getAttributes() returns an array, so let’s look at how its output can be filtered.

Say you’re parsing controller routes, you’re only interested in the Route attribute. You can easily pass that class as a filter:

$attributes = $reflectionClass->getAttributes(Route::class);

The second parameter changes how that filtering is done. You can pass in ReflectionAttribute::IS_INSTANCEOF, which will return all attributes implementing a given interface.

For example, say you’re parsing container definitions, which rely on several attributes, you could do something like this:

$attributes = $reflectionClass->getAttributes(
    ContainerAttribute::class, 
    ReflectionAttribute::IS_INSTANCEOF
);

It’s a nice shorthand, built into the core.

Technical theory

Now that you have an idea of how attributes work in practice, it’s time for some more theory, making sure you understand them thoroughly. First of all, I mentioned this briefly before, attributes can be added in several places.

In classes, as well as anonymous classes;

#[ClassAttribute]
class MyClass { /* … */ }

$object = new #[ObjectAttribute] class () { /* … */ };

Properties and constants;

#[PropertyAttribute]
public int $foo;

#[ConstAttribute]
public const BAR = 1;

Methods and functions;

#[MethodAttribute]
public function doSomething(): void { /* … */ }

#[FunctionAttribute]
function foo() { /* … */ }

As well as closures;

$closure = #[ClosureAttribute] fn() => /* … */;

And method and function parameters;

function foo(#[ArgumentAttribute] $bar) { /* … */ }

They can be declared before or after docblocks;

/** @return void */
#[MethodAttribute]
public function doSomething(): void { /* … */ }

And can take no, one or several arguments, which are defined by the attribute’s constructor:

#[Listens(ProductCreatedEvent::class)]
#[Autowire]
#[Route(Http::POST, '/products/create')]

As for allowed parameters you can pass to an attribute, you’ve already seen that class constant, ::class names and scalar types are allowed. There’s a little more to be said about this though: attributes only accept constant expressions as input arguments.

This means that scalar expressions are allowed — even bit shifts — as well as ::class, constants, arrays and array unpacking, boolean expressions, and the null coalescing operator. A list of everything that’s allowed as a constant expression can be found in the source code.

#[AttributeWithScalarExpression(1 + 1)]
#[AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)]
#[AttributeWithClassConstant(Http::POST)]
#[AttributeWithBitShift(4 >> 1, 4 << 1)]

Attribute configuration

By default, attributes can be added in several places, as listed above. It’s possible, however, to configure them so they can only be used in specific places. For example, you could make it so that ClassAttribute can only be used in classes, and nowhere else. Opting in this behavior is done by passing a flag to the Attribute attribute on the attribute class.

It looks like this:

#[Attribute(Attribute::TARGET_CLASS)]
class ClassAttribute
{
}

The following flags are available:

Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL

These are bitmask flags, so you can combine them using a binary OR operation.

#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
class ClassAttribute
{
}

Another configuration flag is about repeatability. By default, the same attribute can’t be applied twice, unless it’s specifically marked as repeatable. This is done the same way as the target configuration, with a bit flag.

#[Attribute(Attribute::IS_REPEATABLE)]
class ClassAttribute
{
}

Note that all these flags are only validated when calling $attribute->newInstance(), not earlier.

Built-in attributes

Once the base RFC had been accepted, new opportunities arose to add built-in attributes to the core. One such example is the #[Deprecated] attribute and a popular example has been a #[Jit] attribute — if you’re not sure what that last one is about.

I’m sure we’ll see more and more built-in attributes in the future.

As a final note, for those worrying about generics: the syntax won’t conflict with them if they ever were to be added in PHP, so we’re safe!

Top Tutorials

Post navigation

Previous Post: Dealing with deprecations
Next Post: PHP 8.1: read-only properties

Related Posts

  • What’s new in PHP 8.2
  • How to Deploy a React application on a cPanel
  • How to Convert PHP CSV to JSON
  • PHP 8.1: read-only properties
  • How to Delete Files in Ubuntu Command Line
  • Dealing with deprecations

Categories

  • Codeigniter (3)
  • CSS (11)
  • eCommerce (1)
  • Framework (1)
  • Git (3)
  • How to (43)
  • HTML (5)
  • JavaScript (15)
  • Jquery (7)
  • Laravel (1)
  • Linux (4)
  • Magento-2 (1)
  • Node js (4)
  • Others (2)
  • PHP (11)
  • React (13)
  • Server (1)
  • SSH (3)
  • Symfony (6)
  • Tips (16)
  • Top Tutorials (10)
  • Ubuntu (3)
  • Vue (1)
  • Wordpress (7)

Latest Posts

  • What is SSH in Linux?
  • How to Delete Files in Ubuntu Command Line
  • How to Deploy a React application on a cPanel
  • How to use events listeners and Event Subscriber in Symfony
  • How to Convert PHP CSV to JSON

WEEKLY TAGS

AJAX (1) Codeigniter (1) Javascript (11) JQuery (1) PHP (16) Programming (1) React (3) Symfony (1)

Random Post

How to check PHP version using the command line
How to add two-factor authentication (2FA) to WordPress using the Google Authenticator plugin.
How do sum values from an array of key-value pairs in JavaScript?
How to Delete Files in Ubuntu Command Line
Here are 10 platforms where you can find your next remote job as a developer.

Quick Navigation

  • About
  • Contact
  • Privacy Policy

© Teach Developer 2021. All rights reserved