Attribute, auch bekannt als Annotations in anderen Programmiersprachen, wurden mit PHP 8.0 eingeführt und stellen eine bedeutende Erweiterung der Metaprogrammierung in PHP dar. Sie ermöglichen es, Klassen, Methoden, Funktionen, Eigenschaften, Parameter und andere Code-Elemente mit strukturierten Metadaten zu versehen, die zur Laufzeit über Reflection ausgelesen werden können.
Attribute werden in PHP mit der doppelten Klammernotation
(<< und >>) eingeführt:
<<AttributeName>>
class MeineKlasse
{
<<PropertyAttribute>>
public string $eigenschaft;
<<MethodAttribute("parameter", wert: true)>>
public function meineMethode(
<<ParameterAttribute>> $parameter
): void {
// Methodenimplementierung
}
}Die Syntax unterscheidet sich bewusst von der zuvor in PHPDoc
verwendeten Annotation-Syntax (@Annotation), um einen
klaren Unterschied zwischen dokumentationsbasierten Annotationen und zur
Laufzeit auslesbaren Attributen zu schaffen.
Vor PHP 8.0 wurden Metainformationen primär über PHPDoc-Blöcke bereitgestellt:
/**
* @Entity
* @Table(name="users")
*/
class User
{
/**
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
private $id;
}Docblock-Annotationen haben jedoch mehrere Nachteile: - Sie sind nur Kommentare und werden nicht vom PHP-Parser validiert - Sie benötigen eine Parsing-Bibliothek zur Laufzeit - Es gibt keine standardisierte Syntax oder Validierung - Es können Fehler auftreten, wenn Docblocks während der Minimierung entfernt werden
Attribute beheben diese Probleme:
<<Entity>>
<<Table(name: "users")>>
class User
{
<<Id>>
<<Column(type: "integer")>>
<<GeneratedValue>>
private int $id;
}Um ein eigenes Attribut zu erstellen, definieren Sie eine Klasse und
versehen diese mit dem Meta-Attribut
<<Attribute>>:
<<Attribute>>
class Route
{
public function __construct(
public string $path,
public string $name = '',
public array $methods = ['GET'],
public array $requirements = []
) {}
}Das Meta-Attribut Attribute selbst kann Parameter
erhalten, um das Verhalten des benutzerdefinierten Attributs zu
steuern:
<<Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)>>
class Route
{
// ...
}Sie können einschränken, auf welche Code-Elemente ein Attribut
angewendet werden kann, indem Sie die TARGET_*-Konstanten
im Meta-Attribut verwenden:
<<Attribute(Attribute::TARGET_CLASS)>>
class Entity {}
<<Attribute(Attribute::TARGET_PROPERTY)>>
class Column {}
<<Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)>>
class Cached {}
<<Attribute(Attribute::TARGET_ALL)>> // Standard, wenn nicht angegeben
class Debug {}Die verfügbaren Ziel-Konstanten sind: - TARGET_CLASS -
TARGET_FUNCTION - TARGET_METHOD -
TARGET_PROPERTY - TARGET_CLASS_CONSTANT -
TARGET_PARAMETER - TARGET_ALL (Kombination
aller obigen)
Standardmäßig kann ein Attribut nur einmal auf ein Code-Element
angewendet werden. Mit dem Flag IS_REPEATABLE können Sie
ein Attribut so konfigurieren, dass es mehrfach angewendet werden
kann:
<<Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)>>
class Route
{
public function __construct(
public string $path,
public array $methods = ['GET']
) {}
}
<<Route(path: "/users", methods: ["GET", "POST"])>>
<<Route(path: "/users/{id}", methods: ["GET"])>>
class UserController
{
// ...
}Attribute können zur Laufzeit über die Reflection-API ausgelesen werden:
function parseRoutes(string $controllerClass): array
{
$routes = [];
$reflectionClass = new ReflectionClass($controllerClass);
// Lese Klassenattribute
$attributes = $reflectionClass->getAttributes(Route::class);
foreach ($attributes as $attribute) {
// Instanziiere das Attributobjekt
$route = $attribute->newInstance();
$routes[] = [
'path' => $route->path,
'methods' => $route->methods,
'controller' => $controllerClass,
'action' => null
];
}
// Lese Methodenattribute
foreach ($reflectionClass->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
$routes[] = [
'path' => $route->path,
'methods' => $route->methods,
'controller' => $controllerClass,
'action' => $method->getName()
];
}
}
return $routes;
}Mit getAttributes() können Sie optional Attribute nach
Namen filtern. Wenn Sie keinen Klassennamen angeben, werden alle
Attribute zurückgegeben.
Da Attribute echte PHP-Klassen sind, können Sie alle Typisierungsfeatures von PHP nutzen, um die übergebenen Parameter zu validieren:
<<Attribute>>
class ValidRange
{
public function __construct(
public int|float $min,
public int|float $max
) {
if ($min > $max) {
throw new InvalidArgumentException('Min cannot be greater than max');
}
}
}
class Product
{
<<ValidRange(0, 1000)>>
public float $price;
}<<Attribute(Attribute::TARGET_METHOD)>>
class Route
{
public function __construct(
public string $path,
public array $methods = ['GET']
) {}
}
class UserController
{
<<Route("/users", methods: ["GET"])>>
public function index(): void
{
// Zeige Benutzerliste
}
<<Route("/users/{id}", methods: ["GET"])>>
public function show(int $id): void
{
// Zeige Benutzer mit der ID $id
}
<<Route("/users", methods: ["POST"])>>
public function store(): void
{
// Erstelle neuen Benutzer
}
}
// Router-Implementierung
class Router
{
private array $routes = [];
public function registerController(string $controllerClass): void
{
$reflectionClass = new ReflectionClass($controllerClass);
foreach ($reflectionClass->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
$this->routes[] = [
'path' => $route->path,
'methods' => $route->methods,
'controller' => $controllerClass,
'action' => $method->getName()
];
}
}
}
public function dispatch(string $path, string $method): mixed
{
foreach ($this->routes as $route) {
// Vereinfachte Pfadüberprüfung (in der Praxis würde man Regex verwenden)
if ($route['path'] === $path && in_array($method, $route['methods'])) {
$controller = new $route['controller']();
return $controller->{$route['action']}();
}
}
throw new RuntimeException('Route not found');
}
}<<Attribute(Attribute::TARGET_PROPERTY)>>
class Required
{
}
<<Attribute(Attribute::TARGET_PROPERTY)>>
class Email
{
}
<<Attribute(Attribute::TARGET_PROPERTY)>>
class MinLength
{
public function __construct(public int $length) {}
}
class UserRegistration
{
<<Required>>
<<MinLength(3)>>
public string $username;
<<Required>>
<<Email>>
public string $email;
<<Required>>
<<MinLength(8)>>
public string $password;
}
class Validator
{
public function validate(object $object): array
{
$errors = [];
$reflection = new ReflectionClass($object);
foreach ($reflection->getProperties() as $property) {
$propertyName = $property->getName();
$value = $property->getValue($object);
// Prüfe Required-Attribut
if ($property->getAttributes(Required::class) && empty($value)) {
$errors[$propertyName][] = "$propertyName is required";
}
// Prüfe Email-Attribut
if ($property->getAttributes(Email::class) && !empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$propertyName][] = "$propertyName must be a valid email";
}
// Prüfe MinLength-Attribut
$minLengthAttrs = $property->getAttributes(MinLength::class);
foreach ($minLengthAttrs as $attr) {
$minLength = $attr->newInstance()->length;
if (!empty($value) && strlen($value) < $minLength) {
$errors[$propertyName][] = "$propertyName must be at least $minLength characters";
}
}
}
return $errors;
}
}
// Verwendung:
$registration = new UserRegistration();
$registration->username = "jo"; // Zu kurz
$registration->email = "not-an-email"; // Keine gültige E-Mail
$registration->password = "weak"; // Zu kurz
$validator = new Validator();
$errors = $validator->validate($registration);<<Attribute(Attribute::TARGET_PARAMETER)>>
class Inject
{
public function __construct(public ?string $serviceName = null) {}
}
class Container
{
private array $services = [];
public function register(string $name, callable $factory): void
{
$this->services[$name] = $factory;
}
public function get(string $className): object
{
$reflection = new ReflectionClass($className);
$constructor = $reflection->getConstructor();
if (!$constructor) {
return new $className();
}
$parameters = [];
foreach ($constructor->getParameters() as $parameter) {
$injectAttrs = $parameter->getAttributes(Inject::class);
if (empty($injectAttrs)) {
// Kein Inject-Attribut - versuche, nach Typ zu injizieren
$type = $parameter->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$parameters[] = $this->get($type->getName());
} elseif ($parameter->isDefaultValueAvailable()) {
$parameters[] = $parameter->getDefaultValue();
} else {
throw new RuntimeException("Cannot resolve parameter '{$parameter->getName()}'");
}
} else {
// Inject-Attribut vorhanden
$inject = $injectAttrs[0]->newInstance();
$serviceName = $inject->serviceName ?? $parameter->getName();
if (isset($this->services[$serviceName])) {
$parameters[] = ($this->services[$serviceName])();
} elseif ($parameter->isDefaultValueAvailable()) {
$parameters[] = $parameter->getDefaultValue();
} else {
throw new RuntimeException("Service '$serviceName' not found");
}
}
}
return new $className(...$parameters);
}
}
interface LoggerInterface
{
public function log(string $message): void;
}
class FileLogger implements LoggerInterface
{
public function __construct(private string $logFile) {}
public function log(string $message): void
{
file_put_contents($this->logFile, $message . PHP_EOL, FILE_APPEND);
}
}
class UserService
{
public function __construct(
private UserRepository $repository,
<<Inject("logger")>> private LoggerInterface $logger
) {}
}
// Verwendung:
$container = new Container();
$container->register('logger', fn() => new FileLogger('/var/log/app.log'));
$container->register(UserRepository::class, fn() => new UserRepository());
$userService = $container->get(UserService::class);Viele moderne PHP-Frameworks haben bereits Attribute in ihre APIs integriert:
Symfony:
use Symfony\Component\Routing\Annotation\Route;
class ProductController
{
<<Route("/product/{id}", name: "product_show")>>
public function show(int $id): Response
{
// ...
}
}Laravel (ab Version 8.x mit externen Paketen):
use Spatie\RouteAttributes\Attributes\Get;
class UserController
{
<<Get("/users/{id}")>>
public function show(User $user)
{
// ...
}
}Doctrine:
use Doctrine\ORM\Mapping as ORM;
<<ORM\Entity>>
<<ORM\Table(name: "products")>>
class Product
{
<<ORM\Id>>
<<ORM\GeneratedValue>>
<<ORM\Column(type: "integer")>>
private int $id;
<<ORM\Column(type: "string", length: 255)>>
private string $name;
}Trotz ihrer Mächtigkeit haben Attribute einige Einschränkungen:
Kompatibilität: Attribute sind nur in PHP 8.0 und höher verfügbar, was die Kompatibilität mit älteren Projekten einschränkt.
Metaprogrammierungskomplexität: Übermäßiger Einsatz von Attributen kann zu schwer verständlichem “magischem” Code führen.
IDE-Unterstützung: Obwohl sich die IDE-Unterstützung verbessert, ist sie noch nicht so ausgereift wie für andere Sprachfeatures.
Debugging: Fehler in Attributen können manchmal schwer zu debuggen sein, da sie zur Laufzeit ausgewertet werden.
Leistungsaspekte: Das wiederholte Auslesen von Attributen über Reflection kann die Leistung beeinträchtigen, wenn kein geeignetes Caching implementiert ist.