Symfony 7.4 landed November 2025, alongside 8.0. It’s the last LTS of the 7.x line: PHP 8.2 minimum, three years of bug fixes, four of security. For teams that can’t or won’t follow 8.0’s PHP 8.4 requirement, 7.4 is where you land.

:envelope_with_arrow: Message signing in Messenger

Transport security in Messenger has always been the application’s problem to solve. 7.4 adds message signing: a stamp-based mechanism that signs dispatched messages and validates signatures on reception.

The target use case is multi-tenant or external transport scenarios where you need cryptographic proof that a message wasn’t tampered with or injected from outside. Configuration lives at the transport level:

framework:
    messenger:
        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    signing_key: '%env(MESSENGER_SIGNING_KEY)%'

:gear: PHP array configuration

Symfony’s configuration formats have always been YAML (default), XML, and PHP. The PHP format existed but it was awkward: a fluent builder DSL that required method chaining and gave your IDE nothing useful to work with.

7.4 swaps the fluent format for standard PHP arrays. IDEs can now actually analyze it, config/reference.php is auto-generated as a type-annotated reference, and the result reads like data rather than code:

return static function (FrameworkConfig $framework): void {
    $framework->router()->strictRequirements(null);
    $framework->session()->enabled(true);
};

The fluent format is deprecated. Arrays are the future, and honestly it’s a better format.

:shield: OIDC improvements

#[IsSignatureValid] validates signed URLs directly in controllers, cutting out the boilerplate of manual validation. OpenID Connect now supports multiple discovery endpoints, and a new security:oidc-token:generate command makes dev and testing a lot less painful.

:calendar: The support window

7.4 LTS: bugs until November 2028, security fixes until November 2029. The path to 8.4 LTS (the next long-term target) goes through 7.4’s deprecation notices and the PHP 8.4 upgrade. Fix the deprecations now and the jump to 8.x will be much less painful.

Attributes get more precise

#[CurrentUser] now accepts union types, which matters in practice when a route can be reached by more than one user class:

public function index(#[CurrentUser] AdminUser|Customer $user): Response

#[Route] accepts an array for the env option, so a debug route active only in dev and test no longer needs two separate definitions. #[AsDecorator] is now repeatable, meaning one class can decorate multiple services at once. #[AsEventListener] method signatures accept union event types. #[IsGranted] gets a methods option to scope an authorization check to specific HTTP verbs without duplicating the route.

Request class stops doing too much

Request::get() is deprecated, and honestly good riddance. The method searched route attributes, then query parameters, then request body, in that order, silently returning whatever it found first. That ambiguity caused real bugs. It’s gone in 8.0; in 7.4 it still works but triggers a deprecation. The replacements are explicit: $request->attributes->get(), $request->query->get(), $request->request->get().

Body parsing for PUT, PATCH, DELETE, and QUERY requests arrives at the same time. Previously Symfony only parsed application/x-www-form-urlencoded and multipart/form-data for POST. Those same content types now get parsed for the other writable methods too, which kills a common REST API workaround.

HTTP method override for GET, HEAD, CONNECT, and TRACE is deprecated. Overriding a safe method with a header was always semantically broken anyway. You can now explicitly allow only the methods that make sense for your app:

Request::setAllowedHttpMethodOverride(['PUT', 'PATCH', 'DELETE']);

Workflows accept BackedEnums

Workflow places and transitions can now be defined with PHP backed enums, both in YAML (via the !php/enum tag) and in PHP config. The marking store works with enum values directly, so your domain model and your workflow definition finally use the same types:

framework:
    workflows:
        blog_publishing:
            initial_marking: !php/enum App\Status\PostStatus::Draft
            places: !php/enum App\Status\PostStatus
            transitions:
                publish:
                    from: !php/enum App\Status\PostStatus::Review
                    to: !php/enum App\Status\PostStatus::Published

Extending validation and serialization for third-party classes

Ever needed to add validation or serialization metadata to a class from a bundle you don’t own? 7.4 has #[ExtendsValidationFor] and #[ExtendsSerializationFor] for that. You write a companion class with your extra annotations, point the attribute at the target class, and Symfony merges the metadata at container compilation time:

#[ExtendsValidationFor(UserRegistration::class)]
abstract class UserRegistrationValidation
{
    #[Assert\NotBlank(groups: ['my_app'])]
    #[Assert\Length(min: 3, groups: ['my_app'])]
    public string $name = '';

    #[Assert\Email(groups: ['my_app'])]
    public string $email = '';
}

Symfony verifies at compile time that the declared properties actually exist on the target class. A rename won’t silently break your validation.

DX: the things that don’t headline but matter

The Question helper in Console accepts a timeout. Ask the user to confirm something, and if they don’t respond in N seconds, the default answer kicks in. Very handy in deployment scripts that can’t afford to wait forever for a human.

messenger:consume gets --exclude-receivers. Combined with --all, it lets you consume from every transport except specific ones:

bin/console messenger:consume --all --exclude-receivers=low_priority

FrankenPHP worker mode is now auto-detected. If the process is running inside FrankenPHP, Symfony switches to worker mode automatically. No extra package needed.

The debug:router command hides the Scheme and Host columns when all routes use ANY, which removes a lot of noise from the default output. HTTP methods are now color-coded too.

Functional tests get $client->getSession() before the first request. Previously you had to make at least one request to access the session, which was annoying. Now you can pre-seed CSRF tokens or A/B testing flags up front:

$session = $client->getSession();
$session->set('_csrf/checkout', 'test-token');
$session->save();

Lock: DynamoDB store

DynamoDbStore lands as a new Lock backend. Useful in AWS-native deployments where Redis isn’t in the stack, and it works exactly like any other store:

$store = new DynamoDbStore('dynamodb://default/locks');
$factory = new LockFactory($store);

Doctrine bridge: day and time point types

Two new Doctrine column types: day_point stores a date-only value (no time component) and time_point stores a time-only value, both mapping to DatePoint. Good when your domain genuinely separates date from time:

#[ORM\Column(type: 'day_point')]
public DatePoint $birthDate;

#[ORM\Column(type: 'time_point')]
public DatePoint $openingTime;

Routing: explicit query parameters

The _query key in URL generation lets you set query parameters explicitly, separate from route parameters. This matters when a route parameter and a query parameter share the same name:

$url = $urlGenerator->generate('report', [
    'site' => 'fr',
    '_query' => ['site' => 'us'],
]);
// /report/fr?site=us

HttpHeaderParser parses Link response headers into structured objects. Before this, parsing Link headers from API responses meant either pulling in a third-party library or writing regex. The use case is HTTP APIs that advertise related resources or pagination via Link headers, like GitHub’s API does.

HTML5 parsing gets faster on PHP 8.4

DomCrawler and HtmlSanitizer switch to PHP 8.4’s native HTML5 parser when available. No code changes needed on your end. The native parser is faster and more spec-compliant than the previous fallback. On PHP 8.2 or 8.3 nothing changes.

Translation: StaticMessage

StaticMessage implements TranslatableInterface but intentionally doesn’t translate. It passes the string through unchanged regardless of locale. The use case is API responses that must stay in a fixed language regardless of the user’s locale, or audit log entries where you need to preserve the original text as-is.