CascadiaPHP 2024

Übersicht über die Attribute

(PHP 8)

Attribute bieten die Möglichkeit, strukturierte, maschinenlesbare Metadaten-Informationen über Deklarationen in den Code einfügen: Attribute können für Klassen, Methoden, Funktionen, Parameter, Eigenschaften und Klassenkonstanten verwendet werden. Die durch Attribute definierten Metadaten können dann zur Laufzeit mit Hilfe der Reflection-APIs inspiziert werden. Attribute können daher als eine direkt in den Code eingebettete Konfigurationssprache betrachtet werden.

Mit Attributen können die allgemeine Implementierung eines Merkmals und seine konkrete Verwendung in einer Anwendung voneinander getrennt werden. In gewisser Weise ist dies vergleichbar mit Schnittstellen und deren Implementierung. Während es aber bei Schnittstellen und deren Implementierung um Code geht, geht es bei Attributen um die Angabe zusätzlicher Informationen und Konfiguration. Schnittstellen können von Klassen implementiert werden, während Attribute auch für Methoden, Funktionen, Parameter, Eigenschaften und Klassenkonstanten deklariert werden können. Attribute sind daher flexibler als Schnittstellen.

Ein einfaches Beispiel dafür, wie Attribute verwendet werden können, wandelt eine Schnittstelle mit optionalen Methoden so um, dass Attribute verwendet werden. Nehmen wir dazu mal an, eine Schnittstelle ActionHandler stelle eine Operation in einer Anwendung dar, bei der einige Implementierungen eines Action-Handlers von allen Klassen, vorkonfiguriert werden müssen und andere nicht. Anstatt die ActionHandler implementieren, zu verlangen, eine setUp()-Methode zu implementieren, kann ein Attribut verwendet werden. Ein Vorteil dieses Ansatzes ist, dass das Attribut mehrfach verwendet werden kann.

Beispiel #1 Implementierung optionaler Methoden einer Schnittstelle mit Hilfe von Attributen

<?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("Die Datei existiert nicht");
}
}

#[
SetUp]
public function
targetDirectoryExists()
{
if (!
file_exists($this->targetDirectory)) {
mkdir($this->targetDirectory);
} elseif (!
is_dir($this->targetDirectory)) {
throw new
RuntimeException("Das Zielverzeichnis $this->targetDirectory ist kein Verzeichnis");
}
}

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
29
Florian Krmer
1 year 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
38
Harshdeep
2 years 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
-90
Justin
2 years 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