Understand the attributes in PHP 8

Time:2022-5-6

explain

Starting with PHP 8, we will be able to start using ‘annotations’.

The goal of these annotations (also known as annotations in many other languages) is to add metadata to classes, methods, variables, and so on in a structured manner.

The concept of annotations is not new. We have used document blocks to simulate their behavior for many years.

However, by adding annotations, we now have the syntax in the language to represent this metadata, rather than having to parse document blocks manually.

  • So what do they look like?
  • How do we create custom annotations?
  • Are there any precautions?

These are the questions to be answered in this post.

analysis

First, here’s what native annotations look like:

use \Support\Attributes\ListensTo;

class ProductSubscriber
{
    <<ListensTo(ProductCreated::class)>>
    public function onProductCreated(ProductCreated $event) { /* … */ }

    <<ListensTo(ProductDeleted::class)>>
    public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

I’ll show other examples later in this article, but I think the event subscriber example is a good example to explain the usage of annotations first.

In addition, I know that this syntax may not be what you want or see. You may prefer@, or@:, ornotesOr. However, this grammar has been merged, so we’d better learn to deal with and use it. As for syntax, the only thing worth mentioning is that all syntax schemes that can implement annotation are discussed. There is a good reason to choose this syntax. You can read a short summary of it in the RFC, or you can read a full discussion of the RFC in the internal list.

First, custom annotations are simple classes that use<<attribute>>Attribute to annotate itself; This basic attribute was called phpattribute in the original RFC, but was later changed in another RFC.

Here’s what it looks like:

<<Attribute>>
class ListensTo
{
    public string $event;

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

That’s it. It’s simple, isn’t it? Remember the goal of annotations: their purpose is to add metadata to classes and methods, that’s all.
For example, they should not and cannot be used for parameter input validation. In other words: you cannot access the parameters passed to the method in the annotation. There used to be an RFC that allowed this behavior, but this RFC made things easier in particular.

Back to the event subscriber example: we still need to read the metadata and register our subscriber somewhere. I have a background in using laravel. I will use a service provider to do this, but I am free to come up with other solutions.

class EventServiceProvider extends ServiceProvider
{
    //In reality,
    //We will automatically parse and cache all subscribers.
    //Instead of using manual arrays.
    private array $subscribers = [
        ProductSubscriber::class,
    ];

    public function register(): void
    {
        //Event scheduler resolves from container
        $eventDispatcher = $this->app->make(EventDispatcher::class);

        foreach ($this->subscribers as $subscriber) {
            //We will parse all registered listeners.
            //In the 'subscriber' class,
            //And add it to the scheduler.
            foreach (
                $this->resolveListeners($subscriber) 
                as [$event, $listener]
            ) {
                $eventDispatcher->listen($event, $listener);
            }       
        }       
    }
}

Please note that if you are not familiar with[$event,$listener]Syntax, you can quickly learn about it in my post on array parsing.

Now let’s have a lookResolutionveListeners, this is where magic triggers.

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[] = [
                //Events configured on annotations
                $listener->event,

                //Listener for this event
                [$subscriberClass, $method->getName()],
            ];
        }
    }

    return $listeners;
}

As you can see, this makes it easier to read metadata than parsing comment strings. However, there are two complex issues worth studying.

The first is$attribute->newInstance()Call of. This is actually where our custom attribute class is instantiated. It will accept the parameters listed in the property definition of the subscriber class and pass them to the constructor.

This means that technically, you don’t even need to construct custom annotations. You can call directly$attribute->getArguments()。 In addition, instantiating classes means that you have the flexibility to use constructors to parse input in any way you like. In a word, I want to say always usenewInstance()Instantiating properties would be nice.

It is worth mentioning thatReflectionMethod::getAttributes()This function returns all the properties of the method.
You can pass two parameters to it to filter its output.

However, in order to understand this filtering, you first need to know another thing about annotations.
This may be obvious to you, but I’d like to mention it quickly: you can add several properties to the same method, class, property, or constant.

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

With this in mind, it’s clear whyReflect*::getAttributes()Returns an array, so let’s see how to filter its output.

Suppose you are resolving the controller route, and you onlyRouteInterested in annotations. You can easily pass this class as a filter:

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

The second parameter changes the filtering method.
You can pass inReflectionAttribute::IS_INSTANCEOF, it will return all annotations that implement the given interface.

For example, suppose you are parsing a container definition, which depends on several properties, you can do this:

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

Technical theory

Now that you know how annotations work in practice, it’s time to do more theory and make sure you understand them thoroughly.
First of all, I mentioned this briefly earlier. You can add annotations in several places.

In classes and anonymous classes;

<<ClassAttribute>>
class MyClass { /* … */ }

$object = new <<ObjectAttribute>> class () { /* … */ };

Attributes and constants;

<<PropertyAttribute>>
public int $foo;

<<ConstAttribute>>
public const BAR = 1;

Methods and functions;

<<MethodAttribute>>
public function doSomething(): void { /* … */ }

<<FunctionAttribute>>
function foo() { /* … */ }

And closures;

$closure = <<ClosureAttribute>> fn() => /* … */;

Parameters of methods and functions;

function foo(<<ArgumentAttribute>> $bar) { /* … */ }

They can be declared before or after comments;

/** @return void */
<<MethodAttribute>>
public function doSomething(): void { /* … */ }

And can accept no parameters, one parameter or multiple parameters, which are defined by the constructor of the property:

<<Listens(ProductCreatedEvent::class)>>
<<Autowire>>
<<Route(Http::POST, '/products/create')>>

As for the allowed parameters that can be passed to annotations, you have seen that class constants,:: class, and scalar types are allowed. However, there is more to say about this: annotations only accept constant expressions as input parameters.

This means that scalar expressions – even bit shifts – and:: class, constants, array and array unpacking, Boolean expressions, and null merge operators are allowed. You can find a list of everything that is allowed as a constant expression in the source code.

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

Annotation configuration

By default, annotations can be added in multiple locations, as listed above.
However, they can be configured so that they can only be used in specific locations.
For example, you can set it toClassAttributeCan only be used on classes, not elsewhere.
Choosing to add this behavior is done by passing the flag to the annotation attribute on the annotation class.

<<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 bit mask flags, so you can combine them using binary or operators.

<<Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)>>
class ClassAttribute
{
}

Another configuration flag is about repeatability. By default, the same annotation cannot be applied twice unless it is specifically marked as repeatable. This is the same as the target configuration using the bit flag.

<<Attribute(Attribute::IS_REPEATABLE)>>
class ClassAttribute
{
}

Note that all of these flags are only called$attribute->newInstance()Time validation, not earlier.

Built in annotation

Once basicRFCOnce accepted, there is a new opportunity to add built-in annotations to the core.
<<deposated>>Annotations are an example of this, and<<JIT>>Annotations are a popular example – if you’re not sure what the last annotation is about, you can read my aboutJITWhat is it.

I believe we will see more and more built-in annotations in the future.

Finally, note that for those who are worried about generics: the syntax will not conflict with them, and if they are added to PHP, then we are safe!

I’ve thought of some annotated use cases. How about you?


php

This work adoptsCC agreement, reprint must indicate the author and the link to this article

By: Laravel-China NiZerin
Blog: nizer.in

Recommended Today

Uncover rollup tree shaking

Uncover rollup tree shaking preface Compare webpack How to use Rollup Using the tree shaking function Rollup source code analysis magic-string acorn Ast workflow Ast parsing process Scope Implement Rollup Implement tree shaking Dependent variables can be modified Support block level scope Treatment of tree shaking at the entrance Implement variable rename summary quote Uncover […]