Symfony 7 Attribute Cheatsheet: All the New PHP Attributes at a Glance
AI generated
SF
{ }
Symfony 7 · PHP 8.4 · Attributes · Cheatsheet
Symfony 7 Attribute Cheatsheet
All the New PHP Attributes at a Glance

PHP 8 attributes have largely replaced YAML and XML in Symfony configuration. Symfony 7 brings dozens of PHP attributes for routing, dependency injection, events, caching, security and request mapping, all with semantics identical to the previous configuration formats, but considerably more compact and placed directly on the code.

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

1. Why PHP Attributes Replace YAML in Symfony

Native PHP 8 attributes replace three different configuration formats in Symfony 7: YAML, XML and annotations (Doctrine annotations via @Route, @IsGranted). The decisive advantage over YAML is that PHP attributes sit directly on the code, on the controller, on the service class, on the event listener. There is no separate configuration file that has to be kept in sync with the code. When a class is deleted, its configuration disappears automatically along with it. That eliminates a common source of errors in larger Symfony projects.

Compared to the old Doctrine annotations (use Doctrine\Common\Annotations), native PHP attributes have another advantage: they are parsed natively by the PHP interpreter, no extra Doctrine package, no custom parser, no annotation classes with constructors. The IDE understands them as normal PHP syntax, can check types and offer autocompletion. PHP attributes in Symfony 7 are not syntax magic, they are genuine first-class PHP citizens. Symfony 7 reads them via ReflectionAttribute, fast, maintainable and without an external dependency.

2. Routing Attributes: Route, RoutePrefix and Requirements

The #[Route] attribute is the best known PHP attribute in Symfony. It defines path, HTTP methods, name, requirements, defaults and host directly on the controller method. At class level, #[Route] acts as a prefix for all method routes. Requirements are passed as an array of regex patterns: requirements: ['id' => '\d+'] restricts the path parameter to digits. The #[Route] attribute can be placed multiple times on a single method to register the same method under several paths, useful for versioned APIs.

The name attribute inside #[Route] sets the route name explicitly. If it is missing, Symfony generates a name from the controller class name and the method. In larger projects an explicit name is recommended, it makes generate() calls in the code unambiguous and prevents name collisions during renames. The locale attribute enables localized routes: #[Route(path: ['de' => '/produkte/{id}', 'en' => '/products/{id}'], name: 'product_show')] registers both path variants under one name and Symfony picks one based on the current locale. That is a powerful PHP attribute feature for international Symfony projects.


<?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 Attributes: Autowire, AutowireIterator, AsAlias

The #[Autowire] attribute from Symfony\Component\DependencyInjection\Attribute is the most important PHP attribute for dependency injection in Symfony 7. It allows a service, a parameter or an environment variable value to be wired in explicitly, directly on the constructor argument, without a services.yaml entry. #[Autowire(service: 'logger')] injects a specific service by ID. #[Autowire('%app.api_key%')] injects a container parameter. #[Autowire(env: 'DATABASE_URL')] injects an environment variable. All of this works without touching services.yaml.

#[AutowireIterator] collects all services tagged with a given tag into an iterable collection and injects them as a bundle. The classic example: a handler registry that gathers all services tagged app.handler and sorts them by type or priority. #[AsTaggedItem] on the handler classes sets the tag and optional metadata such as priority. #[AsAlias] registers a service under an alternative interface name, useful for publishing a concrete class under an interface alias in the container. #[AsDecorator] decorates another service without changing services.yaml.


<?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 Attributes: AsEventListener and AsMessageHandler

The PHP attribute #[AsEventListener] registers a class or a method as an event listener without any services.yaml configuration. At class level, the class must have an __invoke method that accepts the event type as a parameter, Symfony infers the event type from the type hint. At method level, several listeners can be defined in one class: #[AsEventListener(event: ProductCreatedEvent::class, priority: 10)] registers the annotated method for exactly that event at the given priority. Multiple #[AsEventListener] attributes on the same method register it for several events.

#[AsMessageHandler] registers a class as a Messenger handler without manual YAML configuration. With PHP 8.4 and constructor property promotion, a separate __invoke method is no longer required, the attribute can also sit on a named method. #[AsMessageHandler(bus: 'messenger.bus.commands')] restricts the handler to a specific bus. For saga-like handlers that receive several messages, the attribute is placed multiple times on different methods of one class. These PHP attributes for event and message handling eliminate a large part of the services.yaml configuration in event-driven Symfony projects.

5. Request Attributes: MapQueryString, MapRequestPayload, MapUploadedFile

The request mapping PHP attributes in Symfony 7 cover every common input source. #[MapQueryString] maps query parameters, #[MapRequestPayload] the request body, and #[MapUploadedFile] file uploads directly into controller arguments. #[MapUploadedFile] accepts an UploadedFile object or an array of them and can be validated against file type and size using Symfony constraints, all without manual access to $request->files. The attribute #[MapRequestPayload(type: MyDto::class)] allows the target type to be specified explicitly when PHP typing alone is not enough.

The attribute #[ValueResolver] is the more generic counterpart for custom argument resolvers. Anyone who needs complex mapping logic, for example a resolver that derives a user value object from a JWT token, implements a ValueResolverInterface and annotates the argument with #[ValueResolver(MyResolver::class)]. This replaces the old ArgumentValueResolverInterface from Symfony 5/6 with a more explicit, attribute-based pattern. For standard REST APIs, #[MapQueryString] and #[MapRequestPayload] are entirely sufficient in the vast majority of cases.


<?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 Attributes: Cache, IsGranted and HttpCache

The #[Cache] attribute from the successor of SensioFrameworkExtraBundle controls HTTP caching headers directly on the controller method. #[Cache(maxage: 3600, public: true)] sets Cache-Control: public, max-age=3600 without a manual call to $response->setMaxAge(3600). The smaxage attribute sets the shared max-age for reverse proxies such as Varnish or Symfony HttpCache. lastModified and etag enable conditional requests, the browser sends If-Modified-Since or If-None-Match, Symfony checks and returns 304 without executing the controller if the resource is unchanged.

In Symfony 7, the symfony/http-kernel package also offers the #[WithHttpCache] attribute, which works without an extra bundle. For granular cache control, for example different TTLs depending on the user role, a manual call on the response object stays more flexible. The PHP attribute is ideal for public, static resources such as product listings or categories, where a single cache header fits every response of the method. For dynamic cache keys or Vary headers that depend on the request context, the manual approach in the controller is preferable.

7. Security Attributes: IsGranted and Security

The PHP attribute #[IsGranted] from Symfony\Component\Security\Http\Attribute protects controller methods declaratively. #[IsGranted('ROLE_ADMIN')] checks the role of the logged-in user and automatically throws an AccessDeniedException if the check fails. #[IsGranted('EDIT', subject: 'product')] passes the route argument $product as a subject to the voter, so the voter class can perform ownership-based checks. The statusCode attribute controls the HTTP status on access denied: the default is 403, but 404 avoids leaking information about a resource's existence.

The more general #[Security] attribute accepts a full security expression: #[Security("is_granted('ROLE_USER') and user.isVerified()")]. It can be placed multiple times on a method, in which case all expressions must be true simultaneously (AND-combined). Both PHP attributes can be combined at class and method level: the class protects all methods with a base rule, individual methods override or extend it. The result is a readable security configuration placed directly on the code, without separate firewall rules in security.yaml for every endpoint.

8. Serializer Attributes: Groups, SerializedName, Ignore

The Symfony serializer PHP attributes control how objects are serialized and deserialized. #[Groups(['product:read', 'product:list'])] on a property marks it for particular serialization groups, only fields whose groups match the active context appear in the output. This prevents information leakage and enables optimized payload sizes for list vs. detail views without separate DTOs. #[SerializedName('product_name')] maps a PHP property name onto a different JSON key, useful for backward compatibility or API conventions that expect camelCase turned into snake_case.

#[Ignore] excludes a property from serialization entirely, even if a group is active. This is the more precise alternative to omitted group annotations and makes the intent explicit. #[Context] injects serialization context values directly onto the property: #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] formats a DateTimeInterface field with the given format, independent of the global context. This granular control makes property-level formatting possible without a custom normalizer for every date format. In API Platform projects, these serializer PHP attributes complement the resource configuration and control the output format precisely.

9. Attributes vs. YAML: When to Use What?

Migrating from YAML to PHP attributes is advisable in Symfony 7 for most configurations, but not sensible in every case. The decision rests on the proximity of the configuration to the code: attributes that describe a single class or method directly belong in PHP, attributes that control system-wide settings are still better placed in YAML.

Configuration PHP Attribute YAML Preferred Rationale
Routes #[Route] ✓ Possible Directly on the controller, no YAML sync needed
Firewall / Security Supplementary security.yaml ✓ Global rules are better kept central in YAML
Services / DI #[Autowire] ✓ Fallback Explicit injection visible on the constructor
Event Listener #[AsEventListener] ✓ Legacy Listener class is self-documenting
Bundle Configuration Not possible config/packages/*.yaml ✓ Bundles configure themselves via YAML

The rule of thumb for PHP attributes in Symfony 7: everything that directly concerns a single class or method becomes an attribute in PHP. Everything that concerns several classes or the entire system stays in YAML. Bundle configurations, global firewall rules and system-wide cache settings have their rightful place in YAML files, not as attributes on a single class. With this dividing line, you benefit from both approaches: compact, self-documenting PHP classes and clear, central configuration files for system-wide settings.

Mironsoft

Symfony 7 development, migration and PHP 8.4 modernization

Ready to modernize your Symfony project to PHP attributes and Symfony 7?

We migrate existing Symfony projects from YAML/annotation configuration to native PHP attributes in Symfony 7, for more compact, more maintainable and IDE-friendly code with PHP 8.4.

Migration Audit

Analysis of existing YAML/annotation configuration and a migration plan for Symfony 7 PHP attributes

Code Modernization

Migration to PHP 8.4 constructor promotion, attributes and Symfony 7 best practices

Team Training

Workshop on Symfony 7 PHP attributes, DI patterns and clean architecture for developer teams

10. Summary

Symfony 7 PHP attributes replace YAML and Doctrine annotations for all class- and method-related configuration. #[Route] defines routes directly on the controller. #[Autowire], #[AutowireIterator] and #[AsTaggedItem] control dependency injection without services.yaml. #[AsEventListener] and #[AsMessageHandler] register event listeners and Messenger handlers declaratively. #[MapQueryString] and #[MapRequestPayload] map request data into typed DTOs. #[IsGranted] protects endpoints. #[Groups] and #[SerializedName] control serialization.

The pattern behind all PHP attributes in Symfony 7 is consistent: configuration belongs close to the code it describes. That reduces the context switch between a PHP class and a YAML file, makes configuration navigable for the IDE and prevents orphaned configuration for deleted classes. For system-wide settings such as bundle configuration and global firewall rules, YAML remains the right place. Everything in between is better expressed with PHP attributes in Symfony 7.

Symfony 7 Attribute Cheatsheet, the Essentials at a Glance

Routing & Request

#[Route] for endpoints, #[MapQueryString] for query parameters, #[MapRequestPayload] for the body, #[MapUploadedFile] for file uploads.

Dependency Injection

#[Autowire] for services and parameters, #[AutowireIterator] for tagged collections, #[AsAlias] and #[AsDecorator] for service configuration.

Events & Messenger

#[AsEventListener] for event listeners with priority, #[AsMessageHandler] for Messenger handlers, both without services.yaml.

Security & Serializer

#[IsGranted] for access control, #[Groups] for serialization groups, #[SerializedName] for JSON key mapping.

11. FAQ: Symfony 7 PHP Attributes

1What are PHP attributes in Symfony 7?
Native PHP 8 metadata annotations that replace YAML and Doctrine annotations in Symfony 7. Directly on classes, methods and properties, parsed natively by the PHP interpreter, no custom parser required.
2Do I have to migrate everything to attributes?
No. YAML and attributes can coexist. Bundle configuration and global firewall rules are better kept in YAML. Class- and method-related configuration benefits from attributes.
3Difference between #[IsGranted] and #[Security]?
#[IsGranted] checks a permission/role with an optional subject. #[Security] accepts full security expressions, more flexible for complex conditions such as user.isVerified().
4How does #[AutowireIterator] work?
Collects all services with the given tag as an iterable. Services mark themselves with #[AsTaggedItem]. Priorities and index methods control order and identification.
5Multiple #[AsEventListener] on one method?
Yes, multiple attributes register the same method for several events. At class level, the __invoke parameter type determines the event.
6Migrating from @Route to #[Route]?
Only a syntax change: @Route('/path', methods={"GET"}) becomes #[Route('/path', methods: ['GET'])]. All parameters identical. sensio/framework-extra-bundle no longer needed.
7#[Autowire(env: 'VAR')] in older Symfony versions?
Before Symfony 6.1 you had to bind $variableName: '%env(VAR)%' in services.yaml. Since 6.1, #[Autowire(env: 'VAR')] handles this directly on the constructor argument.
8PHP attributes compatible with PHP 8.1 and 8.2?
Yes. Attributes since PHP 8.0, compatible with 8.1, 8.2, 8.3, 8.4. Symfony 7 requires at least PHP 8.2.
9Combine #[Route] at class and method level?
Yes, class-level #[Route] acts as a prefix. Method attributes extend path, methods and name relative to the class attribute. Standard pattern for controllers sharing an API path prefix.
10Which attribute for serializer groups?
#[Groups(['group:read'])] from Symfony\Component\Serializer\Attribute\Groups on properties. Fields only appear when the active normalization group matches.