Using Attributes in PHP 8.0

Using Attributes in PHP 8.0

PHP allows specifying doc comments on declarations in code, which can be retrieved using the Reflection API. However, doc comments are just strings that provide unstructured metadata information. There are PHP libraries that allow to specify structured metadata information on declarations via annotations, which implemented using doc comments.

<?php

class User
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="integer")
     */
    private int $id;

    /**
     * @Column(type="string", length=20)
     */
    private string $username;
}

Since PHP 8.0, we can use attributes, which allows specifying structured metadata information on declarations in code.

<?php

class User
{
    #[Id]
    #[GeneratedValue]
    #[Column('integer')]
    private int $id;

    #[Column('string', length: 20)]
    private string $username;
}

Attribute syntax

Attribute declaration starts with #[ and ends with ].

<?php

#[Entity]
class User {}

Inside #[ ] square brackets, we can specify one or many attributes, separated by comma.

<?php

#[Entity, Table]
class User {}

Also, each attribute can be enclosed in its own #[ ] square brackets.

<?php

#[Entity]
#[Table]
class User {}

Attributes can have zero or more arguments, which are enclosed in ( ) parenthesis.

<?php

#[Table('user', 'test_db')]
class User {}

Instead of positional arguments, we can use named arguments.

<?php

#[Table(name: 'user', scheme: 'test_db')]
class User {}

Arguments can be scalar types, arrays, constants or mathematical expressions.

<?php

define('MESSAGE', 'Hello');

#[ScalarTypeAttribute('string', 2, 2.5, true)]
#[ArrayAttribute(['first', 'second'])]
#[ExpressionAttribute(1 + 2)]
#[ConstantAttribute(MESSAGE)]
class User {}

Attributes can be specified before and after doc comments.

<?php

#[Entity]
/**
 * @Class
 */
class User {}
<?php

/**
 * @Class
 */
#[Entity]
class User {}

Apply attributes

Attributes can be applied on various declarations in the code:

  • Classes
  • Class constants
  • Class properties
  • Class methods
  • Method parameters
<?php

#[ClassAttribute]
class User
{
    #[ConstantAttribute]
    public const STATUS_ACTIVE = 'A';

    #[PropertyAttribute]
    private string $username;

    #[MethodAttribute]
    public function setUsername(#[MethodParameterAttribute] string $username): void {}
}
  • Interfaces
<?php

#[InterfaceAttribute]
interface UserInterface {}
  • Traits
<?php

#[TraitAttribute]
trait UserTrait {}
  • Functions
  • Function parameters
  • Closures
  • Short closures
  • Anonymous classes
<?php

#[FunctionAttribute]
function saySomething(#[FunctionParameterAttribute] string $message) {}


$fn1 = #[ClosureAttribute] function() {
    return 'Hello';
};


$fn2 = #[ShortClosureAttribute] fn() => 'Hello';


$object = new #[AnonymousClassAttribute] class {};

Reading attributes

Attributes can be retrieved using the Reflection API. Reflection classes have the getAttributes method that returns an array of ReflectionAttribute instances.

User.php

<?php

#[Entity]
#[Table('user', 'test_db')]
class User {}

main.php

<?php

require_once 'User.php';

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

foreach ($class->getAttributes() as $attribute) {
    $name = $attribute->getName();
    $args = $attribute->getArguments();
    echo $name.': '.implode(', ', $args).PHP_EOL;
}

Example will output:

Entity:
Table: user, test_db

Attribute classes

Not required, but recommended creating a class that defines attribute. In simple case, we only need to create an empty class which declared with #[Attribute] attribute.

Table.php

<?php

#[Attribute]
class Table {}

Then we can apply this attribute on declaration:

User.php

<?php

#[Table]
class User {}

When an attribute is declared with an actual class, we can retrieve an instance of the attribute using the Reflection API.

main.php

<?php

require_once 'User.php';
require_once 'Table.php';

$class = new ReflectionClass(User::class);
$attribute = $class->getAttributes()[0];

$instance = $attribute->newInstance();
print_r($instance); // Table Object ()

Attribute classes can use namespace.

Table.php

<?php

namespace App\Orm\Attributes;

use Attribute;

#[Attribute]
class Table {}

User.php

<?php

namespace App\Entity;

use App\Orm\Attributes\Table;

#[Table]
class User {}

Attribute class constructor

Attribute class can have constructor.

Table.php

<?php

#[Attribute]
class Table
{
    public string $name;

    public string $scheme;

    public function __construct(string $name, string $sheme)
    {
        $this->name = $name;
        $this->scheme = $sheme;
    }
}

The arguments defined in the attribute declaration are passed to its constructor.

User.php

<?php

#[Table('user', 'test_db')]
class User {}

We can use the Reflection API to retrieve an instance of the attribute and its properties:

main.php

<?php

require_once 'User.php';
require_once 'Table.php';

$class = new ReflectionClass(User::class);
$attribute = $class->getAttributes()[0];

$instance = $attribute->newInstance();
echo $instance->name;   // user
echo $instance->scheme; // test_db

Targets of the attribute

By default, the attribute can be applied to any declaration that accepts attributes. We can define targets of the attribute. This means that attribute can be applied to specific type of declarations.

The following attribute can be applied only on class declarations:

<?php

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

We can use bitmask to specify more than one targets of the attribute.

<?php

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

The following targets can be used:

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

Note that targets are validated only when calling the newInstance method using the Reflection API.

User.php

<?php

class User
{
    #[Table]
    private string $username;
}

main.php

<?php

require_once 'User.php';
require_once 'Table.php';

$property = new ReflectionProperty(User::class, 'username');
$attribute = $property->getAttributes()[0];

$instance = $attribute->newInstance();
Fatal error: Uncaught Error: Attribute "Table" cannot target property (allowed targets: class) in main.php:9

Repeatable attribute

By default, the attribute can be applied only one time on the same declaration. We can use Attribute::IS_REPEATABLE flag that allows to apply attribute on declaration multiple times.

UniqueConstraint.php

<?php

#[Attribute(Attribute::TARGET_CLASS|Attribute::IS_REPEATABLE)]
class UniqueConstraint
{
    public array $columns;

    public function __construct(array $columns)
    {
        $this->columns = $columns;
    }
}

User.php

<?php

#[UniqueConstraint(['company_id', 'username'])]
#[UniqueConstraint(['company_id', 'email'])]
class User {}

Note that this flag is validated only when calling the newInstance method.

main.php

<?php

require_once 'User.php';
require_once 'UniqueConstraint.php';

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

foreach ($class->getAttributes() as $attribute) {
    $instance = $attribute->newInstance();
    $name = $attribute->getName();
    echo $name.': '.implode(', ', $instance->columns).PHP_EOL;
}

Example will output:

UniqueConstraint: company_id, username
UniqueConstraint: company_id, email

Leave a Comment

Cancel reply

Your email address will not be published.