Revision: Tue, 02 Nov 2021 18:17:51 GMT

Attributes

The spiral/attributes component serves two very important purposes:

  • The ability to combine different types of metadata in one place. You shouldn't care if the developer is using Doctrine Annotations or PHP Attributes added in PHP 8.
  • The ability to read attributes in any version of the language. This means that you can use the PHP 8 Attributes right now, even if you are using PHP 7.2.

Component provides a metadata reader bridge allowing both modern PHP attributes and Doctrine annotations to be used in the same project.

This documentation uses the term "metadata" to refer to both "attributes" and "annotations".

Installation

To install the component:

$ composer require spiral/attributes

Framework Integration

Please note that the spiral/framework >= 2.8 already includes this component.

To enable the component, you just need to add the Spiral\Bootloader\AttributesBootloader class to the bootloader list, which is located in the class of your application.

protected const LOAD = [
    // ...
    Spiral\Bootloader\AttributesBootloader::class
];

After that, the following interfaces will become available to you for access through the container:

  • Spiral\Attributes\ReaderInterface

Usage

The reader provides a set of following methods:

interface ReaderInterface
{
    public function getClassMetadata(\ReflectionClass $class, string $name = null): iterable;
    public function getPropertyMetadata(\ReflectionProperty $property, string $name = null): iterable;
    public function getFunctionMetadata(\ReflectionFunctionAbstract $function, string $name = null): iterable;
    public function getConstantMetadata(\ReflectionClassConstant $constant, string $name = null): iterable;
    public function getParameterMetadata(\ReflectionParameter $parameter, string $name = null): iterable;

    public function firstClassMetadata(\ReflectionClass $class, string $name): ?object;
    public function firstPropertyMetadata(\ReflectionProperty $property, string $name): ?object;
    public function firstFunctionMetadata(\ReflectionFunctionAbstract $function, string $name): ?object;
    public function firstConstantMetadata(\ReflectionClassConstant $constant, string $name): ?object;
    public function firstParameterMetadata(\ReflectionParameter $parameter, string $name): ?object;
}

Class Metadata

To read the class metadata, use the $reader->getClassMetadata() method. It receives as input the ReflectionClass of the required class and returns a list of available metadata objects.

$reflection = new ReflectionClass(User::class);

$attributes = $reader->getClassMetadata($reflection); 
// returns iterable<object>

The second optional argument $name of the method allows you to specify which specific metadata objects you want to retrieve.

$reflection = new ReflectionClass(User::class);

$attributes = $reader->getClassMetadata($reflection, Entity::class); 
// returns iterable<Entity>

To get one metadata object, you can use the method $reader->firstClassMetadata().

$reflection = new ReflectionClass(User::class);

$attribute = $reader->firstClassMetadata($reflection, Entity::class); 
// returns Entity|null

Property Metadata

To read the property metadata, use the $reader->getPropertyMetadata() method. It receives as input the ReflectionProperty of the required property and returns a list of available metadata objects.

$reflection = new ReflectionProperty(User::class, 'name');

$attributes = $reader->getPropertyMetadata($reflection); 
// returns iterable<object>

The second optional argument $name of the method allows you to specify which specific metadata objects you want to retrieve.

$reflection = new ReflectionProperty(User::class, 'name');

$attributes = $reader->getPropertyMetadata($reflection, Column::class); 
// returns iterable<Column>

To get one metadata object, you can use the method $reader->firstPropertyMetadata().

$reflection = new ReflectionProperty(User::class, 'name');

$column = $reader->firstPropertyMetadata($reflection, Column::class); 
// returns Column|null

Function Metadata

To read the function metadata, use the $reader->getFunctionMetadata() method. It receives an argument of the ReflectionFunction or ReflectionMethod type of the required function and returns a list of available metadata objects.

$reflection = new ReflectionMethod(RequestData::class, 'getEmail');

$attributes = $reader->getFunctionMetadata($reflection); 
// returns iterable<object>

The second optional argument $name of the method allows you to specify which specific metadata objects you want to retrieve.

$reflection = new ReflectionMethod(RequestData::class, 'getEmail');

$attributes = $reader->getPropertyMetadata($reflection, DTOGetter::class); 
// returns iterable<DTOGetter>

To get one metadata object, you can use the method $reader->firstFunctionMetadata().

$reflection = new ReflectionMethod(RequestData::class, 'getEmail');

$getter = $reader->firstFunctionMetadata($reflection, DTOGetter::class); 
// returns DTOGetter|null

Constant Metadata

To read the class constant metadata, use the $reader->getConstantMetadata() method. It receives an argument of the ReflectionClassConstant type of the required function and returns a list of available metadata objects.

$reflection = new ReflectionClassConstant(Example::class, 'CONSTANT_NAME');

$attributes = $reader->getConstantMetadata($reflection); 
// returns iterable<object>

The second optional argument $name of the method allows you to specify which specific metadata objects you want to retrieve.

$reflection = new ReflectionClassConstant(Example::class, 'CONSTANT_NAME');

$attributes = $reader->getConstantMetadata($reflection, Deprecated::class); 
// returns iterable<Deprecated>

To get one metadata object, you can use the method $reader->firstConstantMetadata().

$reflection = new ReflectionClassConstant(Example::class, 'CONSTANT_NAME');

$getter = $reader->firstConstantMetadata($reflection, Deprecated::class); 
// returns Deprecated|null

Parameter Metadata

To read the function/method parameter metadata, use the $reader->getParameterMetadata() method. It receives an argument of the ReflectionParameter type of the required function and returns a list of available metadata objects.

$reflection = new ReflectionParameter('send_email', 'email');

$attributes = $reader->getParameterMetadata($reflection); 
// returns iterable<object>

The second optional argument $name of the method allows you to specify which specific metadata objects you want to retrieve.

$reflection = new ReflectionParameter('send_email', 'email');

$attributes = $reader->getParameterMetadata($reflection, PreCondition::class); 
// returns iterable<PreCondition>

To get one metadata object, you can use the method $reader->firstParameterMetadata().

$reflection = new ReflectionParameter('send_email', 'email');

$getter = $reader->firstConstantMetadata($reflection, PreCondition::class); 
// returns PreCondition|null

Create Annotations

For details on using doctrine annotations, please see the doctrine documentation.

You should use "hybrid" syntax to create metadata classes that will work on any version of PHP.

/**
 * @Annotation
 * @Target({ "CLASS" })
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
class MyEntityMetadata
{
    public string $table;
}

In this case you can use this metadata class on any PHP version.

/**
 * PHP 8.0 syntax
 */
#[MyEntityMetadata(table: 'users')] 
class User {}

/**
 * Doctrine syntax
 * @MyEntityMetadata(table="users")
 */
class User {}

Instantiation

The package supports different ways of instantiating attributes, but by default it uses Doctrine logic for compatibility.

Let's say you use your metadata class as follows, passing in one field "property" with the string value "value".

/** @CustomMetadataClass(property="value") */
#[CustomMetadataClass(property: "value")]
class AnnotatedClass
{
}

In this case, the annotation class itself might look like this:

Doctrine Basic Instantiator

In this case, when declaring a metadata class, the attribute/annotation properties will be filled.

See also Doctrine Custom Annotations

/** @Annotation */
#[\Attribute]
class CustomMetadataClass
{
    public $property;
}

Doctrine Constructor Instantiator

In the case of a constructor declaration, all data when using the metadata class will be passed to this constructor as an array.

See also Doctrine Custom Annotations

/** @Annotation */
#[\Attribute]
class CustomMetadataClass
{
    public function __construct(array $properties)
    {
        // $properties = [ "property" => "value" ]
    }
}

Named Arguments (Interface Marker)

If you want to use named constructor parameters (see also https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments), then you have to add an interface Spiral\Attributes\NamedArgumentConstructorAttribute to the metadata class that will mark the required metadata class as one that takes named arguments.

/** @Annotation */
#[\Attribute]
class CustomMetadataClass implements \Spiral\Attributes\NamedArgumentConstructorAttribute
{
    public function __construct($property)
    {
        // $property = "value"
    }
}

Named Arguments (Metadata Marker)

Please note that using the previous method will require you to have a spiral/attributes package and you will not be able to use these classes in other projects where this package is missing.

To solve this problem, you can use the metadata class, which will mean the same thing (the metadata class uses named arguments), but does not directly implement the interface, and therefore does not require a spiral/attributes package in the project.

/**
 * @Annotation
 * @Spiral\Attributes\NamedArgumentConstructor
 */
#[\Attribute]
#[\Spiral\Attributes\NamedArgumentConstructor]
class CustomMetadataClass
{
    public function __construct($property)
    {
        // $property = "value"
    }
}

Drivers

The Spiral\Attributes\Factory encapsulates several implementations behind it and returns a selective reader implementation by default, which is suitable for most cases. However, you can require a specific implementation if available on your platform and/or in your application.

use Spiral\Attributes\Factory;

$reader = (new Factory())->create();

Annotation Reader

Please note that in order for this reader to be available in the application, you need to require "doctrine/annotations" component.

This reader implementation allows reading doctrine annotations.

/** @ExampleAnnotation */
class Example {}

$reader = new \Spiral\Attributes\AnnotationReader();

$annotations = $reader->getClassMetadata(new ReflectionClass(Example::class));
// returns iterable<ExampleAnnotation>

Attribute Reader

This reader implementation allows reading native PHP attributes on any PHP version.

#[ExampleAttribute]
class Example {}

$reader = new \Spiral\Attributes\AttributeReader();

$attributes = $reader->getClassMetadata(new ReflectionClass(Example::class));
// returns iterable<ExampleAttribute>

Selective Reader

The implementation automatically selects the correct reader based on the syntax used in the application. This behavior is required if you use both syntaxes at the same time in the same project. For example, in the case of an already working project, which is refactored and the syntax of annotations is translated into the modern syntax of attributes.

/** @ExampleAnnotation */
class ClassWithAnnotations {}
#[ExampleAttribute]
class ClassWithAttributes {}

$reader = new \Spiral\Attributes\Composite\SelectiveReader([
    new \Spiral\Attributes\AnnotationReader(),
    new \Spiral\Attributes\AttributeReader(),
]);

$annotations = $reader->getClassMetadata(new ReflectionClass(ClassWithAnnotations::class));
// returns iterable<ExampleAnnotation>

$attributes = $reader->getClassMetadata(new ReflectionClass(ClassWithAttributes::class));
// returns iterable<ExampleAttribute>

When using both annotations and attributes in the same place, the behavior of this reader is non-deterministic.

Merge Reader

The reader's implementation combines several syntaxes in one. This behavior is required if you are working with multiple libraries at the same time that support either only the old or only the new syntax.

/** @DoctrineAnnotation */
#[NativeAttribute]
class ExampleClass {}

$reader = new \Spiral\Attributes\Composite\MergeReader([
    new \Spiral\Attributes\AnnotationReader(),
    new \Spiral\Attributes\AttributeReader(),
]);

$metadata = $reader->getClassMetadata(new ReflectionClass(ExampleClass::class));
// returns iterable { DoctrineAnnotation, NativeAttribute }

Cache

Some implementations can slow things down to some extent because they read the metadata from scratch. This is especially true when there is a lot of such data.

To optimize and speed up the work of readers, it is recommended to use the cache. The attribute package supports the PSR-6 and PSR-16 specifications. To create them, you need to use the corresponding classes.

use Spiral\Attributes\Psr6CachedReader;
use Spiral\Attributes\Psr16CachedReader;
use Spiral\Attributes\AttributeReader;

$psr6reader = new Psr6CachedReader(
    new AttributeReader(),
    new SomePsr6CacheImplementation() // Any PSR-6 cache implementation
);

$psr16reader = new Psr16CachedReader(
    new AttributeReader(),
    new SomePsr6CacheImplementation() // Any PSR-16 cache implementation
);

You can also pass a cache implementation instance to the factory class.

use Spiral\Attributes\Factory;

$reader = (new Factory)
    ->withCache($cacheDriver)
    ->create()
;

// Where $cacheDriver is PSR-6 or PSR-16 cache driver implementation
Edit this page