Symfony 7.4 and Symfony 8.0 are released simultaneously at the end of November 2025. According to the Symfony release process, both versions have the same features, but Symfony 8.0 doesn't include any deprecated features. To upgrade, make sure to resolve all deprecation notices. Read more about this in the Symfony documentation.
Note
Symfony v8 requires PHP v8.4 or higher
- Remove
ImportMapConfigReader::splitPackageNameAndFilePath(), useImportMapEntry::splitPackageNameAndFilePath()instead
- Remove
AbstractBrowser::useHtml5Parser(); the native HTML5 parser is used unconditionally
- Remove
CouchbaseBucketAdapter, useCouchbaseCollectionAdapterinstead
- Remove support for accessing the internal scope of the loader in PHP config files, use only its public API instead
- Add argument
$singulartoNodeBuilder::arrayNode() - Add argument
$infotoArrayNodeDefinition::canBeDisabled()andcanBeEnabled() - Ensure configuration nodes do not have both
isRequired()anddefaultValue() - Remove generation of fluent methods in config builders
-
The
AsCommandattribute class is nowfinal -
Remove methods
Command::getDefaultName()andCommand::getDefaultDescription()in favor of the#[AsCommand]attributeBefore
use Symfony\Component\Console\Command\Command; class CreateUserCommand extends Command { public static function getDefaultName(): ?string { return 'app:create-user'; } public static function getDefaultDescription(): ?string { return 'Creates users'; } // ... }
After
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; #[AsCommand('app:create-user', 'Creates users')] class CreateUserCommand { // ... }
-
Add argument
$finishedIndicatortoProgressIndicator::finish() -
Ensure closures set via
Command::setCode()method have proper parameter and return types+use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; -$command->setCode(function ($input, $output) { +$command->setCode(function (InputInterface $input, OutputInterface $output): int { // ... + + return 0; });
-
Add method
isSilent()toOutputInterface -
Remove deprecated
Symfony\Component\Console\Application::add()method in favor ofSymfony\Component\Console\Application::addCommand()use Symfony\Component\Console\Application; $application = new Application(); -$application->add(new CreateUserCommand()); +$application->addCommand(new CreateUserCommand());
-
Remove support for using
$thisor the loader's internal scope from PHP config files; use the$loadervariable instead -
Remove
ExtensionInterface::getXsdValidationBasePath()andgetNamespace()without alternatives, the XML configuration format is no longer supported -
Add argument
$throwOnAbstracttoContainerBuilder::findTaggedResourceIds() -
Registering a service without a class when its id is a non-existing FQCN throws an error
-
Replace
#[TaggedIterator]and#[TaggedLocator]attributes with#[AutowireLocator]and#[AutowireIterator]+use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; -use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; class MyService { - public function __construct(#[TaggedIterator('app.my_tag')] private iterable $services) {} + public function __construct(#[AutowireIterator('app.my_tag')] private iterable $services) {} }
-
Remove
!taggedtag, use!tagged_iteratorinstead -
Remove the
ContainerBuilder::getAutoconfiguredAttributes()method, usegetAttributeAutoconfigurators()instead to retrieve all the callbacks for a specific attribute class -
Add argument
$targettoContainerBuilder::registerAliasForArgument() -
Remove support for the fluent PHP format for semantic configuration, instantiate builders inline with the config array as argument and return them instead:
-return function (AcmeConfig $config) { - $config->color('red'); -} +return new AcmeConfig([ + 'color' => 'red', +]);
-
Remove the
DoctrineExtractor::getTypes()method, useDoctrineExtractor::getType()instead-$types = $extractor->getTypes(Foo::class, 'property'); +$type = $extractor->getType(Foo::class, 'property');
-
Remove support for auto-mapping Doctrine entities to controller arguments; use explicit mapping instead
-
Make
ProxyCacheWarmerclassfinal
- Remove argument
$useHtml5ParserofCrawler's constructor; the native HTML5 parser is used unconditionally
-
Remove support for passing
nullas the allowed variable names toExpressionLanguage::lint()andParser::lint(), pass theIGNORE_UNKNOWN_VARIABLESflag instead to ignore unknown variables during linting-$expressionLanguage->lint($expression, null); +$expressionLanguage->lint($expression, [], ExpressionLanguage::IGNORE_UNKNOWN_VARIABLES);
-
The
default_protocoloption inUrlTypenow defaults tonullinstead of'http'Before
// URLs without protocol were automatically prefixed with 'http://' $builder->add('website', UrlType::class); // Input: 'example.com' → Value: 'http://example.com'
After
// URLs without protocol are now kept as-is $builder->add('website', UrlType::class); // Input: 'example.com' → Value: 'example.com' // To restore the previous behavior, explicitly set the option: $builder->add('website', UrlType::class, [ 'default_protocol' => 'http', ]);
-
Made
ResizeFormListener::postSetData()methodfinal -
Remove the
VersionAwareTesttrait, use feature detection instead -
Remove deprecated
ResizeFormListener::preSetData()method, usepostSetData()instead -
Remove
validation.xmlinResources/config, replaced by attributes on theFormclass
- Remove the
WorkflowDumpCommandclass; theworkflow:dumpcommand and its class were moved to the Workflow component, but the command still works as before - Remove
errors.xmlandwebhook.xmlrouting configuration files (use their PHP equivalent instead) - Make
Routerclassfinal - Make
SerializerCacheWarmerclassfinal - Make
Translatorclassfinal - Make
ConfigBuilderCacheWarmerclassfinal - Make
TranslationsCacheWarmerclassfinal - Make
ValidatorCacheWarmerclassfinal - Remove autowiring aliases for
RateLimiterFactory; useRateLimiterFactoryInterfaceinstead - Remove
session.sid_lengthandsession.sid_bits_per_characterconfig options - Remove the
router.cache_dirconfig option - Remove the
validation.cacheoption - Remove
TranslationUpdateCommandin favor ofTranslationExtractCommand - Remove deprecated
--show-argumentsoption fromdebug:containercommand
- Remove
MastermindsParser; useNativeParserinstead - Add argument
$contexttoParserInterface::parse()
- Drop HTTP method override support for methods GET, HEAD, CONNECT and TRACE
- Add argument
$subtypeFallbacktoRequest::getFormat() - Remove the following deprecated session options from
NativeSessionStorage:referer_check,use_only_cookies,use_trans_sid,sid_length,sid_bits_per_character,trans_sid_hosts,trans_sid_tags - Trigger PHP warning when using
Request::sendHeaders()after headers have already been sent; use aStreamedResponseinstead - Add arguments
$v4Bytesand$v6BytestoIpUtils::anonymize() - Add argument
$partitionedtoResponseHeaderBag::clearCookie() - Add argument
$expirationtoUriSigner::sign() - Remove
Request::get(), use properties->attributes,queryorrequestdirectly instead - Remove accepting null
$formatargument toRequest::setFormat()
- Remove support for passing an instance of
StoreInterfaceas$cacheargument toCachingHttpClientconstructor, use aTagAwareCacheInterfaceinstead - Remove support for amphp/http-client < 5
- Remove setLogger() methods on decorators; configure the logger on the wrapped client directly instead
- Remove
AddAnnotatedClassesToCachePass - Remove
Extension::getAnnotatedClassesToCompile()andExtension::addAnnotatedClassesToCompile() - Remove
Kernel::getAnnotatedClassesToCompile()andKernel::setAnnotatedClassCache() - Make
ServicesResetterclassfinal - Add argument
$logChanneltoErrorListener::logException() - Add argument
$eventtoDumpListener::configure() - Replace
__sleep/wakeup()by__(un)serialize()on kernels and data collectors - Add method
getShareDir()toKernelInterface
- Remove
Symfony\Component\Intl\Transliterator\EmojiTransliterator, useSymfony\Component\Emoji\EmojiTransliteratorinstead
- Remove
$streamToNativeValueTransformersargument ofPropertyMetadata::__construct(), use$valueTransformerinstead - Remove
PropertyMetadata::getNativeToStreamValueTransformer()andPropertyMetadata::getStreamToNativeValueTransformers(), usePropertyMetadata::getValueTransformers()instead - Remove
PropertyMetadata::withNativeToStreamValueTransformers()andPropertyMetadata::withStreamToNativeValueTransformers(), usePropertyMetadata::withValueTransformers()instead - Remove
PropertyMetadata::withAdditionalNativeToStreamValueTransformer()andPropertyMetadata::withAdditionalStreamToNativeValueTransformer, usePropertyMetadata::withAdditionalValueTransformer()instead
- Remove the
sizeLimitoption ofAbstractQuery - Remove
LdapUser::eraseCredentials()in favor of__serialize() - Add methods for
saslBind()andwhoami()toConnectionInterfaceandLdapInterface
- Remove
TransportFactoryTestCase, extendAbstractTransportFactoryTestCaseinstead
- Remove
textformat when using themessenger:statscommand - Add method
getRetryDelay()toRecoverableExceptionInterface
- Replace
__sleep/wakeup()by__(un)serialize()onAbstractPartimplementations
- Remove
NotFoundActivationStrategy, useHttpCodeActivationStrategyinstead
- Remove the Sms77 Notifier bridge
- Remove
TransportFactoryTestCase, extendAbstractTransportFactoryTestCaseinstead. To keep using thetestIncompleteDsnException()andtestMissingRequiredOptionException()tests, you now need to useIncompleteDsnTestTraitorMissingRequiredOptionTestTraitrespectively.
-
Remove support for nested options definition via
setDefault(), usesetOptions()instead-$resolver->setDefault('option', function (OptionsResolver $resolver) { +$resolver->setOptions('option', function (OptionsResolver $resolver) { // ... });
-
Remove the
PropertyTypeExtractorInterface::getTypes()method, usePropertyTypeExtractorInterface::getType()instead-$types = $extractor->getTypes(Foo::class, 'property'); +$type = $extractor->getType(Foo::class, 'property');
-
Remove the
ConstructorArgumentTypeExtractorInterface::getTypesFromConstructor()method, useConstructorArgumentTypeExtractorInterface::getTypeFromConstructor()instead-$types = $extractor->getTypesFromConstructor(Foo::class, 'property'); +$type = $extractor->getTypeFromConstructor(Foo::class, 'property');
-
Remove the
Typeclass, useSymfony\Component\TypeInfo\Typeclass fromsymfony/type-infoinsteadBefore
use Symfony\Component\PropertyInfo\Type; // create types $int = [new Type(Type::BUILTIN_TYPE_INT)]; $nullableString = [new Type(Type::BUILTIN_TYPE_STRING, true)]; $object = [new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class)]; $boolList = [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(BUILTIN_TYPE_INT), new Type(BUILTIN_TYPE_BOOL))]; $union = [new Type(Type::BUILTIN_TYPE_STRING), new Type(BUILTIN_TYPE_INT)]; $intersection = [new Type(Type::BUILTIN_TYPE_OBJECT, false, \Traversable::class), new Type(Type::BUILTIN_TYPE_OBJECT, false, \Stringable::class)]; // test if a type is nullable $intIsNullable = $int[0]->isNullable(); // echo builtin types of union foreach ($union as $type) { echo $type->getBuiltinType(); } // test if a type represents an instance of \ArrayAccess if ($object[0]->getClassName() instanceof \ArrayAccess::class) { // ... } // handle collections if ($boolList[0]->isCollection()) { $k = $boolList->getCollectionKeyTypes(); $v = $boolList->getCollectionValueTypes(); // ... }
After
use Symfony\Component\TypeInfo\BuiltinType; use Symfony\Component\TypeInfo\CollectionType; use Symfony\Component\TypeInfo\Type; // create types $int = Type::int(); $nullableString = Type::nullable(Type::string()); $object = Type::object(Foo::class); $boolList = Type::list(Type::bool()); $union = Type::union(Type::string(), Type::int()); $intersection = Type::intersection(Type::object(\Traversable::class), Type::object(\Stringable::class)); // test if a type is nullable $intIsNullable = $int->isNullable(); // echo builtin types of union foreach ($union->traverse() as $type) { if ($type instanceof BuiltinType) { echo $type->getTypeIdentifier()->value; } } // test if a type represents an instance of \ArrayAccess if ($object->isIdentifiedBy(\ArrayAccess::class)) { // ... } // handle collections if ($boolList instanceof CollectionType) { $k = $boolList->getCollectionKeyType(); $v = $boolList->getCollectionValueType(); // ... }
- Remove support for accessing the internal scope of the loader in PHP config files, use only its public API instead
- Providing a non-array
_queryparameter toUrlGeneratorcauses anInvalidParameterException - Remove the protected
AttributeClassLoader::$routeAnnotationClassproperty and thesetRouteAnnotationClass()method, useAttributeClassLoader::setRouteAttributeClass()instead - Remove class aliases in the
Annotationnamespace, use attributes instead - Remove getters and setters in attribute classes in favor of public properties
-
When extending the
RememberMeDetailsclass and overriding its constructor, the$userFqcnparameter has to be removed from its signature:Before
class CustomRememberMeDetails extends RememberMeDetails { public function __construct(string $userFqcn, string $userIdentifier, int $expires, string $value) { parent::__construct($userFqcn, $userIdentifier, $expires, $value); } }
After
class CustomRememberMeDetails extends RememberMeDetails { public function __construct(string $userIdentifier, int $expires, string $value) { parent::__construct($userIdentifier, $expires, $value); } }
-
Add argument
$accessDecisiontoAccessDecisionStrategyInterface::decide() -
Remove
PersistentTokenInterface::getClass()andRememberMeDetails::getUserFqcn() -
Remove the user FQCN from the remember-me cookie
-
Remove
UserInterface::eraseCredentials()andTokenInterface::eraseCredentials(); erase credentials e.g. using__serialize()instead:
-public function eraseCredentials(): void
-{
-}
+// If your eraseCredentials() method was used to empty a "password" property:
+public function __serialize(): array
+{
+ $data = (array) $this;
+ unset($data["\0".self::class."\0password"]);
+
+ return $data;
+}- Throw a
BadCredentialsExceptionwhen passing an empty string as$userIdentifierargument toUserBadgeconstructor - Accept only
ExposeSecurityLevelenums forAuthenticatorManager's$exposeSecurityErrorsargument - Respectively accept only
AlgorithmManagerandJWKSetforOidcTokenHandler's$signatureAlgorithmand$signatureKeysetarguments - Remove callable firewall listeners support, extend
AbstractListeneror implementFirewallListenerInterfaceinstead - Remove
AbstractListener::__invoke - Remove
LazyFirewallContext::__invoke() - Remove
RememberMeToken::getSecret() - Add argument
$accessDecisiontoAccessDecisionManagerInterface::decide()andAuthorizationCheckerInterface::isGranted() - Add argument
$votetoVoterInterface::vote()andVoter::voteOnAttribute() - Add argument
$tokentoUserCheckerInterface::checkPostAuth() - Add argument
$attributestoUserAuthenticatorInterface::authenticateUser() - Make
UserChainProviderimplementAttributesBasedUserProviderInterface
-
Remove the deprecated
hide_user_not_foundconfiguration option, useexpose_security_errorsinstead# config/packages/security.yaml security: - hide_user_not_found: false + expose_security_errors: 'all'
# config/packages/security.yaml security: - hide_user_not_found: true + expose_security_errors: 'none'
Note: The
expose_security_errorsoption accepts three values:'none': Equivalent tohide_user_not_found: true(hides all security-related errors)'all': Equivalent tohide_user_not_found: false(exposes all security-related errors)'account_status': A new option that only exposes account status errors (e.g., account locked, disabled)
-
Make
ExpressionCacheWarmerclassfinal -
Remove the deprecated
algorithmandkeyoptions from the OIDC token handler configuration, usealgorithmsandkeysetinstead# config/packages/security.yaml security: firewalls: main: access_token: token_handler: oidc: - algorithm: 'RS256' - key: 'https://example.com/.well-known/jwks.json' + algorithms: ['RS256'] + keyset: 'https://example.com/.well-known/jwks.json' -
Remove autowiring aliases for
RateLimiterFactory; useRateLimiterFactoryInterfaceinstead
-
Remove escape character functionality from
CsvEncoderuse Symfony\Component\Serializer\Encoder\CsvEncoder; // Using escape character in encoding $encoder = new CsvEncoder(); -$csv = $encoder->encode($data, 'csv', [ - CsvEncoder::ESCAPE_CHAR_KEY => '\\', -]); +$csv = $encoder->encode($data, 'csv'); // Using escape character with context builder use Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder; $context = (new CsvEncoderContextBuilder()) - ->withEscapeChar('\\') ->toArray();
-
Remove
AbstractNormalizerContextBuilder::withDefaultContructorArguments(), usewithDefaultConstructorArguments()instead -
Change signature of
NameConverterInterface::normalize()andNameConverterInterface::denormalize()methods:-public function normalize(string $propertyName): string; -public function denormalize(string $propertyName): string; +public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string; +public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string;
-
Remove
AdvancedNameConverterInterface, useNameConverterInterfaceinstead -
Remove
ClassMetadataFactoryCompiler,CompiledClassMetadataFactoryandCompiledClassMetadataCacheWarmer -
Remove class aliases in the
Annotationnamespace, use attributes instead -
Remove getters in attribute classes in favor of public properties
-
Remove the
$escapeparameter fromCsvFileLoader::setCsvControl()use Symfony\Component\Translation\Loader\CsvFileLoader; $loader = new CsvFileLoader(); // Set CSV control characters including escape character -$loader->setCsvControl(';', '"', '\\'); +$loader->setCsvControl(';', '"');
-
Remove
TranslatableMessage::__toString()method, usetrans()orgetMessage()instead -
Make
DataCollectorTranslatorclassfinal -
Remove
ProviderFactoryTestCase, extendAbstractProviderFactoryTestCaseinstead
- Replace
__sleep/wakeup()by__(un)serialize()on string implementations
- Remove support for passing a tag to the constructor of
FormThemeNode - Remove
textformat from thedebug:twigcommand, use thetxtformat instead
- Make
TemplateCacheWarmerclassfinal - Remove the
base_template_classconfig option
-
Constructing a
CollectionTypeinstance as a list that is not an array throws anInvalidArgumentException -
Remove the third
$asListargument ofTypeFactoryTrait::iterable(), useTypeFactoryTrait::list()insteaduse Symfony\Component\TypeInfo\Type; -$type = Type::iterable(Type::string(), asList: true); +$type = Type::list(Type::string());
- Add argument
$formattoUuid::isValid()
-
Remove support for configuring constraint options implicitly with the XML format
Before
<class name="Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity"> <constraint name="Callback"> <value>Symfony\Component\Validator\Tests\Fixtures\CallbackClass</value> <value>callback</value> </constraint> </class>
After
<class name="Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity"> <constraint name="Callback"> <option name="callback"> <value>Symfony\Component\Validator\Tests\Fixtures\CallbackClass</value> <value>callback</value> </option> </constraint> </class>
-
Remove support for configuring constraint options implicitly with the YAML format
Before
Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity: constraints: - Callback: validateMeStatic - Callback: [Symfony\Component\Validator\Tests\Fixtures\CallbackClass, callback]
After
Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity: constraints: - Callback: callback: validateMeStatic - Callback: callback: [Symfony\Component\Validator\Tests\Fixtures\CallbackClass, callback]
-
Remove support for passing associative arrays to
GroupSequenceBefore
$groupSequence = GroupSequence(['value' => ['group 1', 'group 2']]);
After
$groupSequence = GroupSequence(['group 1', 'group 2']);
-
Change the default value of the
$requireTldoption of theUrlconstraint totrue -
Add method
getGroupProvider()toClassMetadataInterface -
Replace
__sleep/wakeup()by__(un)serialize()onGenericMetadataimplementations -
Remove the
getRequiredOptions()andgetDefaultOption()methods from theAll,AtLeastOneOf,CardScheme,Collection,CssColor,Expression,Regex,Sequentially,Type, andWhenconstraints -
Remove support for evaluating options in the base
Constraintclass. Initialize properties in the constructor of the concrete constraint class instead.Before
class CustomConstraint extends Constraint { public $option1; public $option2; public function __construct(?array $options = null) { parent::__construct($options); } }
After
class CustomConstraint extends Constraint { public function __construct( public $option1 = null, public $option2 = null, ?array $groups = null, mixed $payload = null, ) { parent::__construct(null, $groups, $payload); } }
-
Remove the
getRequiredOptions()method from the baseConstraintclass. Use mandatory constructor arguments instead.Before
class CustomConstraint extends Constraint { public $option1; public $option2; public function __construct(?array $options = null) { parent::__construct($options); } public function getRequiredOptions() { return ['option1']; } }
After
class CustomConstraint extends Constraint { public function __construct( public $option1, public $option2 = null, ?array $groups = null, mixed $payload = null, ) { parent::__construct(null, $groups, $payload); } }
-
Remove the
normalizeOptions()andgetDefaultOption()methods of the baseConstraintclass without replacements. Overriding them in child constraint does not have any effects. -
Remove support for passing an array of options to the
Compositeconstraint class. Initialize the properties referenced withgetNestedConstraints()in child classes before calling the constructor ofComposite.Before
class CustomCompositeConstraint extends Composite { public array $constraints = []; public function __construct(?array $options = null) { parent::__construct($options); } protected function getCompositeOption(): string { return 'constraints'; } }
After
class CustomCompositeConstraint extends Composite { public function __construct( public array $constraints, ?array $groups = null, mixed $payload = null, ) { parent::__construct(null, $groups, $payload); } }
-
Remove
Bic::INVALID_BANK_CODE_ERRORconstant. This error code was not used in the Bic constraint validator anymore
- Restrict
ProxyHelper::generateLazyProxy()to generating abstraction-based lazy decorators; use native lazy proxies otherwise - Remove
LazyGhostTraitandLazyProxyTrait, use native lazy objects instead - Remove
ProxyHelper::generateLazyGhost(), use native lazy objects instead
- Add argument
$requesttoRequestParserInterface::createSuccessfulResponse()andRequestParserInterface::createRejectedResponse()
- Remove
profiler.xmlandwdt.xmlrouting configuration files (use their PHP equivalent instead)
-
Add method
getEnabledTransition()toWorkflowInterface -
Add
$nbTokenargument toMarking::mark()andMarking::unmark() -
Add
$asArcargument toTransition::getFroms()andTransition::getTos() -
Remove
Event::getWorkflow()methodBefore
use Symfony\Component\Workflow\Attribute\AsCompletedListener; use Symfony\Component\Workflow\Event\CompletedEvent; class MyListener { #[AsCompletedListener('my_workflow', 'to_state2')] public function terminateOrder(CompletedEvent $event): void { $subject = $event->getSubject(); if ($event->getWorkflow()->can($subject, 'to_state3')) { $event->getWorkflow()->apply($subject, 'to_state3'); } } }
After
use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\Workflow\Attribute\AsCompletedListener; use Symfony\Component\Workflow\Event\CompletedEvent; use Symfony\Component\Workflow\WorkflowInterface; class MyListener { public function __construct( #[Target('my_workflow')] private readonly WorkflowInterface $workflow, ) { } #[AsCompletedListener('my_workflow', 'to_state2')] public function terminateOrder(CompletedEvent $event): void { $subject = $event->getSubject(); if ($this->workflow->can($subject, 'to_state3')) { $this->workflow->apply($subject, 'to_state3'); } } }
Or
use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; use Symfony\Component\Workflow\Attribute\AsTransitionListener; use Symfony\Component\Workflow\Event\TransitionEvent; class GenericListener { public function __construct( #[AutowireLocator('workflow', 'name')] private ServiceLocator $workflows ) { } #[AsTransitionListener] public function doSomething(TransitionEvent $event): void { $workflow = $this->workflows->get($event->getWorkflowName()); } }
- Remove support for parsing duplicate mapping keys whose value is
null