SF
{ }
Symfony 7 · PHP 8.4 · Attribute · Cheatsheet
Symfony 7 Attribute Cheatsheet
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.

20 Min. Lesezeit Route · DI · Events · Cache · Security · Request · Serializer Symfony 7.x · PHP 8.2+

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.

11. FAQ: Symfony 7 PHP-Attribute

1Was sind PHP-Attribute in Symfony 7?
Native PHP-8-Metadaten-Annotationen, die in Symfony 7 YAML und Doctrine-Annotations ersetzen. Direkt an Klassen, Methoden und Properties — vom PHP-Interpreter nativ geparst, kein Custom-Parser nötig.
2Muss ich alles auf Attribute migrieren?
Nein. YAML und Attribute können nebeneinander existieren. Bundle-Konfigurationen und globale Firewall-Regeln bleiben besser in YAML. Klassen- und methodenbezogene Konfigurationen profitieren von Attributen.
3Unterschied #[IsGranted] vs. #[Security]?
#[IsGranted] prüft eine Berechtigung/Rolle mit optionalem Subject. #[Security] akzeptiert vollständige Security-Ausdrücke — flexibler für komplexe Bedingungen wie user.isVerified().
4Wie funktioniert #[AutowireIterator]?
Sammelt alle Services mit dem angegebenen Tag als iterable. Services markieren sich mit #[AsTaggedItem]. Prioritäten und Index-Methoden steuern Reihenfolge und Identifikation.
5Mehrere #[AsEventListener] auf einer Methode?
Ja — mehrere Attribute registrieren dieselbe Methode für mehrere Events. Auf Klassenebene bestimmt der __invoke-Parameter-Typ den Event.
6Migration von @Route zu #[Route]?
Nur Syntax-Änderung: @Route('/path', methods={"GET"}) wird #[Route('/path', methods: ['GET'])]. Alle Parameter identisch. sensio/framework-extra-bundle nicht mehr nötig.
7#[Autowire(env: 'VAR')] in älteren Symfony-Versionen?
In Symfony vor 6.1 musste man $variableName: '%env(VAR)%' in services.yaml binden. Ab 6.1 übernimmt #[Autowire(env: 'VAR')] das direkt am Konstruktor-Argument.
8PHP-Attribute in PHP 8.1 und 8.2 kompatibel?
Ja. Attribute seit PHP 8.0 — kompatibel mit 8.1, 8.2, 8.3, 8.4. Symfony 7 erfordert mindestens PHP 8.2.
9#[Route] auf Klassen- und Methodenebene kombinieren?
Ja — Klassen-#[Route] wirkt als Präfix. Methoden-Attribute ergänzen Pfad, Methoden und Name relativ zum Klassen-Attribut. Standard-Pattern für Controller mit gemeinsamem API-Pfad-Präfix.
10Welches Attribut für Serializer-Gruppen?
#[Groups(['group:read'])] aus Symfony\Component\Serializer\Attribute\Groups auf Properties. Felder erscheinen nur wenn die aktive Normalisierungsgruppe übereinstimmt.