Alle neuen PHP-Attribute im Überblick
PHP 8-Attribute haben YAML und XML in Symfony-Konfigurationen weitgehend abgelöst. Symfony 7 bringt Dutzende von PHP-Attributen für Routing, Dependency Injection, Events, Caching, Security und Request Mapping — alle mit identischer Semantik zu den bisherigen Konfigurationsformaten, aber deutlich kompakter und direkt am Code.
Inhaltsverzeichnis
- 1. Warum PHP-Attribute YAML in Symfony ersetzen
- 2. Routing-Attribute: Route, RoutePrefix und Requirements
- 3. Dependency-Injection-Attribute: Autowire, AutowireIterator, AsAlias
- 4. Event-Attribute: AsEventListener und AsMessageHandler
- 5. Request-Attribute: MapQueryString, MapRequestPayload, MapUploadedFile
- 6. Cache-Attribute: Cache, IsGranted und HttpCache
- 7. Security-Attribute: IsGranted und Security
- 8. Serializer-Attribute: Groups, SerializedName, Ignore
- 9. Attribute vs. YAML: Wann was verwenden?
- 10. Zusammenfassung
- 11. FAQ
1. Warum PHP-Attribute YAML in Symfony ersetzen
PHP 8 native Attribute ersetzen in Symfony 7 drei verschiedene Konfigurationsformate: YAML, XML und Annotations (Doctrine-Annotations via @Route, @IsGranted). Der entscheidende Vorteil gegenüber YAML: Die PHP-Attribute sind direkt am Code platziert — am Controller, an der Service-Klasse, am Event-Listener. Es gibt keine separate Konfigurationsdatei, die mit dem Code synchron gehalten werden muss. Wenn eine Klasse gelöscht wird, verschwindet auch ihre Konfiguration automatisch. Das eliminiert eine häufige Fehlerquelle in größeren Symfony-Projekten.
Gegenüber den alten Doctrine-Annotations (use Doctrine\Common\Annotations) haben native PHP-Attribute einen weiteren Vorteil: Sie werden vom PHP-Interpreter nativ geparst — kein zusätzliches Doctrine-Paket, kein Custom-Parser, keine Annotation-Klassen mit Konstruktors. Die IDE versteht sie als normale PHP-Syntax, kann Typen prüfen und Autovervollständigung anbieten. PHP-Attribute in Symfony 7 sind keine Syntaxmagie, sondern echte PHP-First-Class-Bürger. Symfony 7 liest sie über ReflectionAttribute aus — schnell, wartbar und ohne externe Abhängigkeit.
2. Routing-Attribute: Route, RoutePrefix und Requirements
Das #[Route]-Attribut ist das bekannteste PHP-Attribut in Symfony. Es definiert Pfad, HTTP-Methoden, Name, Requirements, Defaults und Host direkt an der Controller-Methode. Auf Klassenebene wirkt #[Route] als Präfix für alle Methoden-Routen. Requirements werden als Array von Regex-Mustern übergeben: requirements: ['id' => '\d+'] begrenzt den Pfadparameter auf Ziffern. Das Attribut #[Route] kann mehrfach auf einer Methode stehen, um dieselbe Methode unter mehreren Pfaden zu registrieren — nützlich für versionierte APIs.
Das name-Attribut in #[Route] setzt den Routennamen explizit. Fehlt es, generiert Symfony einen Namen aus dem Controller-Klassennamen und der Methode. In größeren Projekten empfiehlt sich der explizite Name — er macht generate()-Aufrufe im Code eindeutig und verhindert Namenskollisionen bei Umbenennungen. Das locale-Attribut ermöglicht lokalisierte Routen: #[Route(path: ['de' => '/produkte/{id}', 'en' => '/products/{id}'], name: 'product_show')] registriert beide Pfadvarianten unter einem Namen und Symfony wählt basierend auf dem aktuellen Locale. Das ist ein mächtiges PHP-Attribut-Feature für internationale Symfony-Projekte.
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
// Class-level Route acts as prefix for all methods
#[Route('/api/v1/products', name: 'product_')]
final class ProductController extends AbstractController
{
// GET /api/v1/products — name: product_list
#[Route('', name: 'list', methods: ['GET'])]
public function list(): JsonResponse
{
return $this->json([]);
}
// GET /api/v1/products/{id} — id must be numeric
#[Route('/{id}', name: 'show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(int $id): JsonResponse
{
return $this->json(['id' => $id]);
}
// POST /api/v1/products — also accessible via PUT for upsert
#[Route('', name: 'create', methods: ['POST'])]
#[Route('/{id}', name: 'upsert', methods: ['PUT'], requirements: ['id' => '\d+'])]
public function create(?int $id = null): JsonResponse
{
return $this->json([], Response::HTTP_CREATED);
}
// Localized route — Symfony picks based on current locale
#[Route(path: ['de' => '/suche', 'en' => '/search'], name: 'search')]
public function search(): JsonResponse
{
return $this->json([]);
}
}
3. Dependency-Injection-Attribute: Autowire, AutowireIterator, AsAlias
Das #[Autowire]-Attribut aus Symfony\Component\DependencyInjection\Attribute ist das wichtigste PHP-Attribut für Dependency Injection in Symfony 7. Es ermöglicht das explizite Einbinden eines Services, eines Parameters oder eines Umgebungsvariablen-Werts direkt im Konstruktor-Argument — ohne services.yaml-Eintrag. #[Autowire(service: 'logger')] injiziert einen spezifischen Service per ID. #[Autowire('%app.api_key%')] injiziert einen Container-Parameter. #[Autowire(env: 'DATABASE_URL')] injiziert eine Umgebungsvariable. All das funktioniert ohne Anpassung der services.yaml.
#[AutowireIterator] sammelt alle Services mit einem bestimmten Tag in einer iterierbaren Collection und injiziert sie gebündelt. Das klassische Anwendungsbeispiel: Ein Handler-Registry, der alle Services mit dem Tag app.handler aufnimmt und per Typ oder Priorität sortiert. #[AsTaggedItem] auf den Handler-Klassen setzt den Tag und optionale Metadaten wie Priorität. #[AsAlias] registriert einen Service unter einem alternativen Interface-Namen — nützlich, um eine konkrete Klasse unter einem Interface-Alias im Container zu veröffentlichen. #[AsDecorator] dekoriert einen anderen Service, ohne services.yaml zu ändern.
<?php
declare(strict_types=1);
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
// Register this service as the implementation of NotifierInterface
#[AsAlias(id: NotifierInterface::class)]
final class EmailNotifier implements NotifierInterface
{
public function __construct(
// Inject specific logger channel — no services.yaml entry needed
#[Autowire(service: 'monolog.logger.mailer')]
private readonly LoggerInterface $logger,
// Inject container parameter directly
#[Autowire('%mailer.from_address%')]
private readonly string $fromAddress,
// Inject environment variable with type casting
#[Autowire(env: 'int:MAIL_RETRY_COUNT')]
private readonly int $retryCount,
) {}
public function notify(string $message): void
{
$this->logger->info('Sending notification', ['message' => $message]);
}
}
// Collect all payment processors tagged with app.payment_processor
final class PaymentRegistry
{
/** @var iterable<PaymentProcessorInterface> */
private readonly iterable $processors;
public function __construct(
#[AutowireIterator('app.payment_processor', defaultIndexMethod: 'getIdentifier')]
iterable $processors,
) {
$this->processors = $processors;
}
}
// Tag a payment processor with priority — higher priority runs first
#[AsTaggedItem(tag: 'app.payment_processor', priority: 10)]
final class StripePaymentProcessor implements PaymentProcessorInterface
{
public static function getIdentifier(): string { return 'stripe'; }
}
4. Event-Attribute: AsEventListener und AsMessageHandler
Das PHP-Attribut #[AsEventListener] registriert eine Klasse oder eine Methode als Event-Listener, ohne eine services.yaml-Konfiguration. Auf Klassenebene muss die Klasse eine __invoke-Methode haben, die den Event-Typ als Parameter akzeptiert — Symfony leitet den Event-Typ aus dem Typhinweis ab. Auf Methodenebene können mehrere Listener in einer Klasse definiert werden: #[AsEventListener(event: ProductCreatedEvent::class, priority: 10)] registriert die annotierte Methode für genau diesen Event mit der angegebenen Priorität. Mehrere #[AsEventListener]-Attribute auf derselben Methode registrieren sie für mehrere Events.
#[AsMessageHandler] registriert eine Klasse als Messenger-Handler ohne manuelle YAML-Konfiguration. Mit PHP 8.4 und Constructor Property Promotion braucht man keine separate __invoke-Methode mehr — das Attribut kann auch auf einer benannten Methode stehen. #[AsMessageHandler(bus: 'messenger.bus.commands')] beschränkt den Handler auf einen spezifischen Bus. Für Saga-artige Handler, die mehrere Messages empfangen, setzt man das Attribut mehrfach auf verschiedene Methoden einer Klasse. Diese PHP-Attribute für Event und Message Handling eliminieren einen großen Teil der services.yaml-Konfiguration in event-getriebenen Symfony-Projekten.
5. Request-Attribute: MapQueryString, MapRequestPayload, MapUploadedFile
Die Request-Mapping-PHP-Attribute in Symfony 7 decken alle gängigen Eingabequellen ab. #[MapQueryString] mappt Query-Parameter, #[MapRequestPayload] den Request-Body, und #[MapUploadedFile] Datei-Uploads direkt in Controller-Argumente. #[MapUploadedFile] akzeptiert ein UploadedFile-Objekt oder ein Array davon und kann mit Symfony-Constraints auf Dateityp und -größe validiert werden — alles ohne manuellen Zugriff auf $request->files. Das Attribut #[MapRequestPayload(type: MyDto::class)] erlaubt das explizite Angeben des Zieltyps, wenn PHP-Typisierung allein nicht ausreicht.
Das Attribut #[ValueResolver] ist das generischere Pendant für benutzerdefinierte Argument-Resolver. Wer eine komplexe Mapping-Logik braucht — etwa einen Resolver, der aus dem JWT-Token ein User-Value-Object ableitet — implementiert einen ValueResolverInterface und annotiert das Argument mit #[ValueResolver(MyResolver::class)]. Das ersetzt das alte ArgumentValueResolverInterface aus Symfony 5/6 durch ein expliziteres, attribute-basiertes Muster. Für Standard-REST-APIs reichen #[MapQueryString] und #[MapRequestPayload] in den allermeisten Fällen vollständig aus.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Dto\CreateProductInput;
use App\Dto\ProductListQuery;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Attribute\MapUploadedFile;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Constraints as Assert;
#[Route('/api/products', name: 'product_')]
final class ProductController extends AbstractController
{
// Combine MapQueryString + MapRequestPayload in one method
#[Route('', name: 'list', methods: ['GET'])]
public function list(#[MapQueryString] ProductListQuery $query): JsonResponse
{
return $this->json(['page' => $query->page, 'limit' => $query->limit]);
}
// MapRequestPayload auto-detects JSON / form-data from Content-Type
#[Route('', name: 'create', methods: ['POST'])]
public function create(#[MapRequestPayload] CreateProductInput $input): JsonResponse
{
return $this->json(['name' => $input->name], Response::HTTP_CREATED);
}
// MapUploadedFile — validate file type and size via constraints
#[Route('/{id}/image', name: 'upload_image', methods: ['POST'])]
public function uploadImage(
int $id,
#[MapUploadedFile([
new Assert\NotNull(),
new Assert\File(maxSize: '5M', mimeTypes: ['image/jpeg', 'image/png', 'image/webp']),
])]
UploadedFile $image,
): JsonResponse {
// $image is validated — process the upload here
return $this->json(['uploaded' => $image->getClientOriginalName()]);
}
}
6. Cache-Attribute: Cache, IsGranted und HttpCache
Das #[Cache]-Attribut aus dem SensioFrameworkExtraBundle-Nachfolger steuert HTTP-Caching-Header direkt an der Controller-Methode. #[Cache(maxage: 3600, public: true)] setzt Cache-Control: public, max-age=3600 ohne manuellen Aufruf von $response->setMaxAge(3600). Das smaxage-Attribut setzt den Shared-Max-Age für Reverse-Proxies wie Varnish oder Symfony-HttpCache. lastModified und etag aktivieren konditionelle Requests — der Browser sendet If-Modified-Since oder If-None-Match, Symfony prüft und gibt 304 zurück ohne Controller-Ausführung, wenn die Ressource unverändert ist.
In Symfony 7 bietet das symfony/http-kernel-Paket auch das #[WithHttpCache]-Attribut, das direkt ohne extra Bundle funktioniert. Für granulare Cache-Kontrolle — etwa unterschiedliche TTLs je nach Nutzerrolle — bleibt der manuelle Aufruf auf dem Response-Objekt flexibler. Das PHP-Attribut ist ideal für öffentliche, statische Ressourcen wie Produktlisten oder Kategorien, bei denen ein einheitlicher Cache-Header auf alle Responses der Methode passt. Für dynamische Cache-Keys oder Vary-Header, die vom Request-Kontext abhängen, ist der manuelle Ansatz im Controller zu bevorzugen.
7. Security-Attribute: IsGranted und Security
Das PHP-Attribut #[IsGranted] aus Symfony\Component\Security\Http\Attribute schützt Controller-Methoden deklarativ. #[IsGranted('ROLE_ADMIN')] prüft die Rolle des eingeloggten Benutzers und wirft automatisch eine AccessDeniedException, wenn die Prüfung fehlschlägt. #[IsGranted('EDIT', subject: 'product')] übergibt das Route-Argument $product als Subject an den Voter — so kann die Voter-Klasse eigentumsbasierte Prüfungen durchführen. Das statusCode-Attribut steuert den HTTP-Status bei Access-Denied: Standard ist 403, aber 404 vermeidet Information-Leakage über die Existenz einer Ressource.
Das allgemeinere #[Security]-Attribut akzeptiert einen vollständigen Security-Ausdruck: #[Security("is_granted('ROLE_USER') and user.isVerified()")]. Es kann mehrfach auf einer Methode stehen, wobei alle Ausdrücke gleichzeitig wahr sein müssen (AND-Verknüpfung). Beide PHP-Attribute können auf Klassen- und Methoden-Ebene kombiniert werden: Die Klasse schützt alle Methoden mit einer Basisregel, einzelne Methoden überschreiben oder ergänzen sie. Das Ergebnis ist eine lesbare, direkt am Code platzierte Security-Konfiguration — ohne separate Firewall-Regeln in security.yaml für jeden Endpunkt.
8. Serializer-Attribute: Groups, SerializedName, Ignore
Die Symfony-Serializer-PHP-Attribute steuern, wie Objekte serialisiert und deserialisiert werden. #[Groups(['product:read', 'product:list'])] auf einer Property markiert sie für bestimmte Serialisierungsgruppen — nur Felder, deren Gruppen mit dem aktiven Kontext übereinstimmen, erscheinen in der Ausgabe. Das verhindert Information-Leakage und ermöglicht optimierte Payload-Größen für Listen- vs. Detailansichten ohne separate DTOs. #[SerializedName('product_name')] mappt einen PHP-Property-Namen auf einen anderen JSON-Schlüssel — nützlich für Backward-Kompatibilität oder API-Konventionen, die camelCase in snake_case erwarten.
#[Ignore] schließt eine Property vollständig aus der Serialisierung aus — auch wenn eine Gruppe aktiv ist. Das ist die präzisere Alternative zu fehlenden Gruppen-Annotationen und macht die Absicht explizit. #[Context] injiziert Serialisierungskontext-Werte direkt an der Property: #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] formatiert ein DateTimeInterface-Feld mit dem angegebenen Format, unabhängig vom globalen Kontext. Diese granulare Kontrolle macht property-level-Formatierung möglich ohne Custom-Normalizer für jedes Datumsformat. In API-Platform-Projekten ergänzen diese Serializer-PHP-Attribute die Ressourcenkonfiguration und steuern das Output-Format präzise.
9. Attribute vs. YAML: Wann was verwenden?
Die Migration von YAML zu PHP-Attributen ist in Symfony 7 für die meisten Konfigurationen empfehlenswert, aber nicht in jedem Fall sinnvoll. Die Entscheidungsgrundlage liegt in der Nähe der Konfiguration zum Code: Attribute, die direkt eine Klasse oder Methode beschreiben, gehören ans PHP — Attribute, die systemweite Einstellungen steuern, bleiben in YAML besser aufgehoben.
| Konfiguration | PHP-Attribut | YAML bevorzugt | Begründung |
|---|---|---|---|
| Routen | #[Route] ✓ | Möglich | Direkt am Controller — kein YAML-Sync nötig |
| Firewall / Security | Ergänzend | security.yaml ✓ | Globale Regeln besser zentral in YAML |
| Services / DI | #[Autowire] ✓ | Fallback | Explizite Injektion sichtbar am Konstruktor |
| Event-Listener | #[AsEventListener] ✓ | Legacy | Listener-Klasse ist selbstdokumentierend |
| Bundle-Konfiguration | Nicht möglich | config/packages/*.yaml ✓ | Bundles konfigurieren sich über YAML |
Die Faustregel für PHP-Attribute in Symfony 7: Alles, was eine einzelne Klasse oder Methode direkt betrifft, kommt als Attribut ans PHP. Alles, was mehrere Klassen oder das gesamte System betrifft, bleibt in YAML. Bundle-Konfigurationen, globale Firewall-Regeln und systemweite Cache-Einstellungen haben in YAML-Dateien ihren richtigen Platz — nicht als Attribute an einer einzelnen Klasse. Mit dieser Trennlinie profitiert man von beiden Ansätzen: kompakten, selbstdokumentierenden PHP-Klassen und übersichtlichen, zentralen Konfigurationsdateien für systemweite Einstellungen.
Mironsoft
Symfony 7 Entwicklung, Migration und PHP 8.4 Modernisierung
Symfony-Projekt auf PHP-Attribute und Symfony 7 modernisieren?
Wir migrieren bestehende Symfony-Projekte von YAML/Annotation-Konfiguration auf native PHP-Attribute in Symfony 7 — für kompakteren, wartbareren und IDE-freundlichen Code mit PHP 8.4.
Migrations-Audit
Analyse bestehender YAML/Annotation-Konfiguration und Migrationsplan für Symfony 7 PHP-Attribute
Code-Modernisierung
Migration auf PHP 8.4 Constructor Promotion, Attribute und Symfony 7 Best Practices
Team-Schulung
Workshop zu Symfony 7 PHP-Attributen, DI-Patterns und Clean Architecture für Entwickler-Teams
10. Zusammenfassung
Symfony 7 PHP-Attribute ersetzen YAML und Doctrine-Annotations für alle klassen- und methodenbezogenen Konfigurationen. #[Route] definiert Routen direkt am Controller. #[Autowire], #[AutowireIterator] und #[AsTaggedItem] steuern Dependency Injection ohne services.yaml. #[AsEventListener] und #[AsMessageHandler] registrieren Event-Listener und Messenger-Handler deklarativ. #[MapQueryString] und #[MapRequestPayload] mappen Request-Daten in typisierte DTOs. #[IsGranted] schützt Endpunkte. #[Groups] und #[SerializedName] steuern die Serialisierung.
Das Muster hinter allen PHP-Attributen in Symfony 7 ist konsistent: Konfiguration gehört nah an den Code, den sie beschreibt. Das reduziert den Kontext-Wechsel zwischen PHP-Klasse und YAML-Datei, macht Konfigurationen für die IDE navigierbar und verhindert verwaiste Konfigurationen für gelöschte Klassen. Für systemweite Einstellungen wie Bundle-Konfiguration und globale Firewall-Regeln bleibt YAML der richtige Ort. Alles dazwischen ist mit PHP-Attributen in Symfony 7 besser ausgedrückt.
Symfony 7 Attribute Cheatsheet — Das Wichtigste auf einen Blick
Routing & Request
#[Route] für Endpunkte, #[MapQueryString] für Query-Parameter, #[MapRequestPayload] für Body, #[MapUploadedFile] für Datei-Uploads.
Dependency Injection
#[Autowire] für Services und Parameter, #[AutowireIterator] für Tagged Collections, #[AsAlias] und #[AsDecorator] für Service-Konfiguration.
Events & Messenger
#[AsEventListener] für Event-Listener mit Priorität, #[AsMessageHandler] für Messenger-Handler — beide ohne services.yaml.
Security & Serializer
#[IsGranted] für Zugriffskontrolle, #[Groups] für Serialisierungsgruppen, #[SerializedName] für JSON-Schlüssel-Mapping.