PHP 8.0.26 Released!

アトリビュートの概要

(PHP 8)

アトリビュートを使うと、 コンピューターが解析できる構造化されたメタデータの情報を、 コードの宣言時に埋め込むことができます。 つまり、クラス、メソッド、関数、パラメータ、プロパティ、クラス定数にアトリビュートを指定することができます。 アトリビュートで定義されたメタデータは、 実行時に リフレクションAPI を使って調べることが出来ます。 よって、アトリビュートは、 コードに直接埋め込むことが出来る、 設定のための言語とみなすことができます。

アトリビュートを使うと、機能の抽象的な実装と、アプリケーションでの具体的な利用を分離できます。 この点でアトリビュートは、インターフェイスとその実装と比較できます。 インターフェイスとその実装はコードに関する情報ですが、 アトリビュートはコードの追加情報と設定に注釈を付けるものです。 インターフェイスはクラスによって実装できますが、 アトリビュートはメソッドや関数、パラメータ、プロパティ、クラス定数で宣言できます。 よって、アトリビュートはインターフェイスより柔軟です。

アトリビュートの使い方の簡単な例として、 必須でないメソッドを持つインターフェイスを、 アトリビュートを使うように変換するコードを示します。 アプリケーションの操作を表現する ActionHandler があるとします。 この 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

User Contributed Notes 3 notes

up
30
Harshdeep
8 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
0
Florian Krmer
24 days ago
I've tried Harshdeeps example and it didn't run out of the box and I think it is not complete, so I wrote a complete and working naive example regarding attribute based serialization.

<?php
declare(strict_types=1);

#[Attribute(Attribute::TARGET_CLASS_CONSTANT|Attribute::TARGET_PROPERTY)]
class JsonSerialize
{
    public function
__construct(public ?string $fieldName = null) {}
}

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

class
UserLandClass extends VersionedObject
{
    protected
string $notSerialized = 'nope';

   
#[JsonSerialize('foobar')]
   
public string $myValue = '';

   
#[JsonSerialize('companyName')]
   
public string $company = '';

   
#[JsonSerialize('userLandClass')]
   
protected ?UserLandClass $test;

    public function
__construct(?UserLandClass $userLandClass = null)
    {
       
$this->test = $userLandClass;
    }
}

class
AttributeBasedJsonSerializer {

    protected const
ATTRIBUTE_NAME = 'JsonSerialize';

    public function
serialize($object)
    {
       
$data = $this->extract($object);

        return
json_encode($data, JSON_THROW_ON_ERROR);
    }

    protected function
reflectProperties(array $data, ReflectionClass $reflectionClass, object $object)
    {
       
$reflectionProperties = $reflectionClass->getProperties();
        foreach (
$reflectionProperties as $reflectionProperty) {
           
$attributes = $reflectionProperty->getAttributes(static::ATTRIBUTE_NAME);
            foreach (
$attributes as $attribute) {
               
$instance = $attribute->newInstance();
               
$name = $instance->fieldName ?? $reflectionProperty->getName();
               
$value = $reflectionProperty->getValue($object);
                if (
is_object($value)) {
                   
$value = $this->extract($value);
                }
               
$data[$name] = $value;
            }
        }

        return
$data;
    }

    protected function
reflectConstants(array $data, ReflectionClass $reflectionClass)
    {
       
$reflectionConstants = $reflectionClass->getReflectionConstants();
        foreach (
$reflectionConstants as $reflectionConstant) {
           
$attributes = $reflectionConstant->getAttributes(static::ATTRIBUTE_NAME);
            foreach (
$attributes as $attribute) {
               
$instance = $attribute->newInstance();
               
$name = $instance->fieldName ?? $reflectionConstant->getName();
               
$value = $reflectionConstant->getValue();
                if (
is_object($value)) {
                   
$value = $this->extract($value);
                }
               
$data[$name] = $value;
            }
        }

        return
$data;
    }

    protected function
extract(object $object)
    {
       
$data = [];
       
$reflectionClass = new ReflectionClass($object);
       
$data = $this->reflectProperties($data, $reflectionClass, $object);
       
$data = $this->reflectConstants($data, $reflectionClass);

        return
$data;
    }
}

$userLandClass = new UserLandClass();
$userLandClass->company = 'some company name';
$userLandClass->myValue = 'my value';

$userLandClass2 = new UserLandClass($userLandClass);
$userLandClass2->company = 'second';
$userLandClass2->myValue = 'my second value';

$serializer = new AttributeBasedJsonSerializer();
$json = $serializer->serialize($userLandClass2);

var_dump(json_decode($json, true));
up
-28
Justin
9 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