PHP 8.2.0 Alpha 2 available for testing

Введение в атрибуты

(PHP 8)

Атрибуты предлагают возможность добавлять структурированные, машиночитаемые метаданные для следующих деклараций в коде: классы, методы, функции, параметры, свойства и константы класса. Привязанные метаданные можно получить во время исполнения, используя Reflection API. Таким образом, атрибуты можно рассматривать как язык конфигурации, встроенный непосредственно в код.

С помощью атрибутов можно разделить абстрактную реализацию какой-либо функциональности и особенности её использования в коде. В некотором смысле это можно сравнить с разделением интерфейса и его реализаций. Но интерфейсы и реализации - это про код, а атрибуты - про добавление дополнительной информации и конфигурацию. Интерфейсы могут реализовываться только классами, тогда как атрибуты также применимы для методов, функций, параметров, свойств и констант классов. Таким образом, они представляют собой гораздо более гибкий механизм, чем интерфейсы.

Давайте разберём использование атрибутов на простом примере реализации опциональных методов для интерфейса. Допустим, что интерфейс ActionHandler описывает некую операцию в приложении. Одни реализации этого интерфейса требуют предварительной настройки, а другие - нет. И вместо того, чтобы вносить в интерфейс ActionHandler дополнительный метод setUp(), который для части реализаций будет пустым, можно использовать атрибут. Одним из преимуществ этого подхода является то, что мы можем использовать атрибут несколько раз.

Пример #1 Реализация опциональных методов интерфейса с помощью атрибутов

<?php
interface ActionHandler
{
    public function 
execute();
}

#[Attribute]
class SetUp {}

class 
CopyFile implements ActionHandler
{
    public 
string $fileName;
    public 
string $targetDirectory;

    
#[SetUp]
    
public function fileExists()
    {
        if (!
file_exists($this->fileName)) {
            throw new 
RuntimeException("File does not exist");
        }
    }

    
#[SetUp]
    
public function targetDirectoryExists()
    {
        if (!
file_exists($this->targetDirectory)) {
            
mkdir($this->targetDirectory);
        } elseif (!
is_dir($this->targetDirectory)) {
            throw new 
RuntimeException("Target directory $this->targetDirectory is not a directory");
        }
    }

    public function 
execute()
    {
        
copy($this->fileName$this->targetDirectory '/' basename($this->fileName));
    }
}

function 
executeAction(ActionHandler $actionHandler)
{
    
$reflection = new ReflectionObject($actionHandler);

    foreach (
$reflection->getMethods() as $method) {
        
$attributes $method->getAttributes(SetUp::class);

        if (
count($attributes) > 0) {
            
$methodName $method->getName();

            
$actionHandler->$methodName();
        }
    }

    
$actionHandler->execute();
}

$copyAction = new CopyFile();
$copyAction->fileName "/tmp/foo.jpg";
$copyAction->targetDirectory "/home/user";

executeAction($copyAction);
add a note add a note

User Contributed Notes 2 notes

up
9
Harshdeep
3 months ago
While the example displays us what we can accomplish with attributes, it should be kept in mind that the main idea behind attributes is to attach static metadata to code (methods, properties, etc.).

This metadata often includes concepts such as "markers" and "configuration". For example, you can write a serializer using reflection that only serializes marked properties (with optional configuration, such as field name in serialized file). This is reminiscent of serializers written for C# applications.

That said, full reflection and attributes go hand in hand. If your use case is satisfied by inheritance or interfaces, prefer that. The most common use case for attributes is when you have no prior information about the provided object/class.

<?php
interface JsonSerializable
{
    public function
toJson() : array;
}
?>

versus, using attributes,
<?php

#[Attribute]
class JsonSerialize
{
    public function
__constructor(public ?string $fieldName = null) {}
}

class
VersionedObject
{
  
#[JsonSerialize]
   
public const version = '0.0.1';
}

public class
UserLandClass extends VersionedObject
{
   
#[JsonSerialize('call it Jackson')]
   
public string $myValue;
}

?>
The example above is a little extra convoluted with the existence of the VersionedObject class as I wished to display that with attribute mark ups, you do not need to care how the base class manages its attributes (no call to parent in overriden method).
up
-14
Justin
4 months ago
Allowing multiple functions to be tagged with the same Attribute is promoting weird design patterns. Because now the order of the tagged functions within the class becomes relevant. The order of functions within a class should remain arbitrary.

It would be better to limit function tagging to one Attribute only. This would force people to implement one function per attribute, which can then call all the other functions they would otherwise tag with these Attribute's.
To Top