Symfony 8.0: PHP 8.4 minimum, native lazy objects, and FormFlow
Symfony 8.0 shipped November 27, 2025, same day as 7.4. It requires PHP 8.4 and drops everything that was deprecated in 7.4. The two most interesting changes are what it stops doing and what it starts doing with PHP 8.4.
Native lazy objects
Symfony’s proxy system, used for lazy service initialization and Doctrine’s entity proxies, has historically relied on code generation. The proxy classes were generated at cache warmup, stored as files, and loaded when needed. It worked, but it added real complexity: generated files to manage, cache to invalidate, code that looked nothing like the class it proxied.
PHP 8.4 added native lazy objects. Symfony 8.0 uses them. The LazyGhostTrait and LazyProxyTrait that powered the old system are removed. Proxy creation is now a runtime operation backed by the engine itself, not a code generation step.
For application developers the change is mostly invisible: lazy services still work. For framework and library authors, a significant surface of complexity just disappears.
FormFlow
Multi-step forms have always been a DIY exercise in Symfony. Session management, step tracking, partial validation, navigation between steps: every project rolled its own solution or pulled in a third-party bundle.
8.0 introduces FormFlow: a built-in mechanism for multi-step form wizards. Steps are defined as a sequence of form types, partial validation is scoped to the current step, and session management is handled automatically.
#[AsFormFlow]
class CheckoutFlow extends AbstractFormFlow
{
protected function defineSteps(): Steps
{
return Steps::create()
->add('shipping', ShippingType::class)
->add('payment', PaymentType::class)
->add('review', ReviewType::class);
}
}
XML and fluent PHP config removed
The 7.4 deprecation of the fluent PHP configuration format becomes a hard removal in 8.0. XML configuration also exits as a first-class format. The supported formats for application configuration are now YAML and PHP arrays. The footprint shrinks, but what remains is genuinely better.
What else is gone
- PHP 8.2 and 8.3 support (8.4 minimum)
- The
ContainerAwareInterfaceandContainerAwareTrait - Symfony’s internal use of
LazyGhostTraitandLazyProxyTrait - HTTP method override for GET and HEAD (only POST makes sense semantically)
Symfony 8.0 is a clean break, and that kind of break only becomes possible when the PHP floor rises. PHP 8.4’s lazy objects are the clearest example: the feature now exists in the language, so the framework can just stop implementing it.
Console becomes more ergonomic for invokable commands
Invokable commands get a significant upgrade. The #[Input] attribute turns a DTO into the command’s argument/option bag. No more calling $input->getArgument() inside the handler:
#[AsCommand(name: 'app:send-report')]
class SendReportCommand
{
public function __invoke(
#[Input] SendReportInput $input,
): int {
// $input->email, $input->dryRun, etc.
return Command::SUCCESS;
}
}
BackedEnum is supported in invokable commands, so an option declared as a Status enum gets validated and cast automatically. Interactive commands get #[Interact] and #[Ask] attributes to declare question prompts inline. CommandTester works with invokable commands without any extra wiring.
Routing finds its own controllers
Routes defined via #[Route] on controller classes are auto-registered without needing an explicit resource: entry in config/routes.yaml. The tag routing.controller is applied automatically. You still control which directories are scanned, but your YAML config shrinks to a pointer at a directory rather than a manual file list.
#[Route] also gains a _query parameter for setting query parameters at generation time, and multiple environments in the env option.
Security: CSRF and OIDC get better tooling
#[IsCsrfTokenValid] gets a $tokenSource argument so you can specify where the token comes from (header, cookie, form field) rather than relying on a fixed convention. SameOriginCsrfTokenManager adds Sec-Fetch-Site header validation, a browser-native CSRF protection mechanism that doesn’t need token injection at all.
The security:oidc-token:generate command creates tokens for testing OIDC-protected endpoints locally. Multiple OIDC discovery endpoints are supported now, useful in multi-tenant setups where each tenant has its own identity provider.
Two new Twig functions: access_decision() and access_decision_for_user() expose the authorization voter result in templates without going through the security facade. #[IsGranted] can be subclassed for repeated authorization patterns that deserve their own named attribute.
ObjectMapper and JsonStreamer leave experimental
Both components introduced in 7.x graduate to stable in 8.0. ObjectMapper maps between objects without hand-written transformers, using attribute-based configuration. JsonStreamer reads and writes large JSON without loading the full document into memory, and it now supports synthetic properties: virtual fields computed at serialization time.
JsonStreamer also drops its dependency on nikic/php-parser. The code generation for the reader/writer now uses a simpler internal mechanism, cutting a heavy dev dependency.
Uid defaults to UUIDv7
UuidFactory now generates UUIDv7 by default instead of UUIDv4. The difference: v7 is time-ordered, so generated UUIDs sort chronologically. That matters a lot for database index performance. MockUuidFactory provides deterministic UUID generation in tests.
Yaml raises an error on duplicate keys
Previously, a YAML file with two identical keys silently kept the last one. 8.0 raises a parse error. This catches real bugs: duplicate keys in services.yaml or config/packages/*.yaml are almost always copy-paste mistakes and you definitely want to know about them.
Validator: Video constraint and wildcard protocols
A Video constraint joins the Image constraint for validating uploaded video files (MIME type, duration, codec). The Url constraint accepts protocols: ['*'] to allow any RFC 3986-compliant scheme, useful when storing arbitrary URLs that include git+ssh://, file://, or custom app schemes.
Messenger: SQS native retry and new events
SQS transport can now use its own native retry and dead-letter queue configuration instead of Symfony’s retry middleware. For high-volume queues on AWS, this removes a round-trip through PHP for transient failures. A MessageSentToTransportsEvent fires after a message is dispatched, carrying information about which transports actually received it.
messenger:consume gets --exclude-receivers to pair with --all.
Mailer: Microsoft Graph transport
A new transport sends mail via the Microsoft Graph API, which is what Microsoft recommends for applications on Azure Active Directory these days. The other options (SMTP relay, Exchange EWS) still work, but Graph is the right choice for new Azure deployments.
Workflow: weighted transitions
Transitions can now declare weights. When multiple transitions are enabled from the same place, the highest-weight one wins. This lets you express priority directly in the workflow definition without adding a guard that reads an external counter.
return (new Definition(states: ['draft', 'review', 'published']))
->addTransition(new Transition('publish', 'review', 'published', weight: 10))
->addTransition(new Transition('reject', 'review', 'draft', weight: 1));
Lock: LockKeyNormalizer
LockKeyNormalizer normalizes a lock key to a consistent string before hashing. Useful when the key is derived from user input or external data that may vary in whitespace or casing: the normalizer makes sure the same logical key always maps to the same lock.
HttpFoundation: QUERY method and cleaner body parsing
The IETF QUERY method (a safe, idempotent method with a body, unlike GET) is now supported throughout the stack: Request, HTTP cache, WebProfiler, and HttpClient. If you build search APIs that need a structured request body and also want caching, QUERY is the right semantic choice.
Request::createFromGlobals() now parses the body of PUT, DELETE, PATCH, and QUERY requests automatically.
Config: JSON schema for YAML validation
Symfony 8.0 auto-generates a JSON Schema file for each configuration section. IDEs that support JSON Schema for YAML files (VS Code, PhpStorm) can now validate config/packages/*.yaml against these schemas and provide autocompletion without any plugin. The schema is generated during cache warmup and placed at config/reference.php.
Runtime: FrankenPHP auto-detection
The Runtime component detects FrankenPHP automatically and activates worker mode without any extra package or environment variable. If $_SERVER['APP_RUNTIME'] is set, that runtime class takes precedence. You can also pick the error renderer based on APP_RUNTIME_MODE, which is useful when running the same codebase in HTTP and CLI contexts with different error presentation needs.