The spiral/attributes
component serves two very important purposes:
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".
To install the component:
composer require spiral/attributes
Note
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
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;
}
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
Since v2.10.0, supports read attributes from traits that are used in the class.
#[Cycle\Entity]
class Entity {
use TsTrait;
}
#[Behavior\CreatedAt]
#[Behavior\UpdatedAt]
trait TsTrait
{
#[Cycle\Column(type: 'datetime')]
private DateTimeImmutable $createdAt;
#[Cycle\Column(type: 'datetime', nullable: true)]
private ?DateTimeImmutable $updatedAt = null;
}
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
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
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
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
Note
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 {}
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:
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;
}
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" ]
}
}
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"
}
}
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"
}
}
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();
Note
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>
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>
The implementation automatically selects the correct reader based on the syntax used in the application. This behavior is required if you use both syntax 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>
Note
When using both annotations and attributes in the same place, the behavior of this reader is non-deterministic.
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 }
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