UPGRADE FROM 7.4 to 8.0 ======================= 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](https://symfony.com/doc/8.0/setup/upgrade_major.html). > [!NOTE] > Symfony v8 requires PHP v8.4 or higher AssetMapper ----------- * Remove `ImportMapConfigReader::splitPackageNameAndFilePath()`, use `ImportMapEntry::splitPackageNameAndFilePath()` instead BrowserKit ---------- * Remove `AbstractBrowser::useHtml5Parser()`; the native HTML5 parser is used unconditionally Cache ----- * Remove `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` instead Config ------ * Remove support for accessing the internal scope of the loader in PHP config files, use only its public API instead * Add argument `$singular` to `NodeBuilder::arrayNode()` * Add argument `$info` to `ArrayNodeDefinition::canBeDisabled()` and `canBeEnabled()` * Ensure configuration nodes do not have both `isRequired()` and `defaultValue()` * Remove generation of fluent methods in config builders Console ------- * The `AsCommand` attribute class is now `final` * Remove methods `Command::getDefaultName()` and `Command::getDefaultDescription()` in favor of the `#[AsCommand]` attribute *Before* ```php 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* ```php use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; #[AsCommand('app:create-user', 'Creates users')] class CreateUserCommand { // ... } ``` * Add argument `$finishedIndicator` to `ProgressIndicator::finish()` * Ensure closures set via `Command::setCode()` method have proper parameter and return types ```diff +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()` to `OutputInterface` * Remove deprecated `Symfony\Component\Console\Application::add()` method in favor of `Symfony\Component\Console\Application::addCommand()` ```diff use Symfony\Component\Console\Application; $application = new Application(); -$application->add(new CreateUserCommand()); +$application->addCommand(new CreateUserCommand()); ``` DependencyInjection ------------------- * Remove support for using `$this` or the loader's internal scope from PHP config files; use the `$loader` variable instead * Remove `ExtensionInterface::getXsdValidationBasePath()` and `getNamespace()` without alternatives, the XML configuration format is no longer supported * Add argument `$throwOnAbstract` to `ContainerBuilder::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]` ```diff +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 `!tagged` tag, use `!tagged_iterator` instead * Remove the `ContainerBuilder::getAutoconfiguredAttributes()` method, use `getAttributeAutoconfigurators()` instead to retrieve all the callbacks for a specific attribute class * Add argument `$target` to `ContainerBuilder::registerAliasForArgument()` * Remove support for the fluent PHP format for semantic configuration, instantiate builders inline with the config array as argument and return them instead: ```diff -return function (AcmeConfig $config) { - $config->color('red'); -} +return new AcmeConfig([ + 'color' => 'red', +]); ``` DoctrineBridge -------------- * Remove the `DoctrineExtractor::getTypes()` method, use `DoctrineExtractor::getType()` instead ```diff -$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 `ProxyCacheWarmer` class `final` DomCrawler ---------- * Remove argument `$useHtml5Parser` of `Crawler`'s constructor; the native HTML5 parser is used unconditionally ExpressionLanguage ------------------ * Remove support for passing `null` as the allowed variable names to `ExpressionLanguage::lint()` and `Parser::lint()`, pass the `IGNORE_UNKNOWN_VARIABLES` flag instead to ignore unknown variables during linting ```diff -$expressionLanguage->lint($expression, null); +$expressionLanguage->lint($expression, [], ExpressionLanguage::IGNORE_UNKNOWN_VARIABLES); ``` Form ---- * The `default_protocol` option in `UrlType` now defaults to `null` instead of `'http'` *Before* ```php // URLs without protocol were automatically prefixed with 'http://' $builder->add('website', UrlType::class); // Input: 'example.com' → Value: 'http://example.com' ``` *After* ```php // 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()` method `final` * Remove the `VersionAwareTest` trait, use feature detection instead * Remove deprecated `ResizeFormListener::preSetData()` method, use `postSetData()` instead * Remove `validation.xml` in `Resources/config`, replaced by attributes on the `Form` class FrameworkBundle --------------- * Remove the `WorkflowDumpCommand` class; the `workflow:dump` command and its class were moved to the Workflow component, but the command still works as before * Remove `errors.xml` and `webhook.xml` routing configuration files (use their PHP equivalent instead) * Make `Router` class `final` * Make `SerializerCacheWarmer` class `final` * Make `Translator` class `final` * Make `ConfigBuilderCacheWarmer` class `final` * Make `TranslationsCacheWarmer` class `final` * Make `ValidatorCacheWarmer` class `final` * Remove autowiring aliases for `RateLimiterFactory`; use `RateLimiterFactoryInterface` instead * Remove `session.sid_length` and `session.sid_bits_per_character` config options * Remove the `router.cache_dir` config option * Remove the `validation.cache` option * Remove `TranslationUpdateCommand` in favor of `TranslationExtractCommand` * Remove deprecated `--show-arguments` option from `debug:container` command HtmlSanitizer ------------- * Remove `MastermindsParser`; use `NativeParser` instead * Add argument `$context` to `ParserInterface::parse()` HttpFoundation -------------- * Drop HTTP method override support for methods GET, HEAD, CONNECT and TRACE * Add argument `$subtypeFallback` to `Request::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 a `StreamedResponse` instead * Add arguments `$v4Bytes` and `$v6Bytes` to `IpUtils::anonymize()` * Add argument `$partitioned` to `ResponseHeaderBag::clearCookie()` * Add argument `$expiration` to `UriSigner::sign()` * Remove `Request::get()`, use properties `->attributes`, `query` or `request` directly instead * Remove accepting null `$format` argument to `Request::setFormat()` HttpClient ---------- * Remove support for passing an instance of `StoreInterface` as `$cache` argument to `CachingHttpClient` constructor, use a `TagAwareCacheInterface` instead * Remove support for amphp/http-client < 5 * Remove setLogger() methods on decorators; configure the logger on the wrapped client directly instead HttpKernel ---------- * Remove `AddAnnotatedClassesToCachePass` * Remove `Extension::getAnnotatedClassesToCompile()` and `Extension::addAnnotatedClassesToCompile()` * Remove `Kernel::getAnnotatedClassesToCompile()` and `Kernel::setAnnotatedClassCache()` * Make `ServicesResetter` class `final` * Add argument `$logChannel` to `ErrorListener::logException()` * Add argument `$event` to `DumpListener::configure()` * Replace `__sleep/wakeup()` by `__(un)serialize()` on kernels and data collectors * Add method `getShareDir()` to `KernelInterface` Intl ---- * Remove `Symfony\Component\Intl\Transliterator\EmojiTransliterator`, use `Symfony\Component\Emoji\EmojiTransliterator` instead JsonStreamer ------------ * Remove `$streamToNativeValueTransformers` argument of `PropertyMetadata::__construct()`, use `$valueTransformer` instead * Remove `PropertyMetadata::getNativeToStreamValueTransformer()` and `PropertyMetadata::getStreamToNativeValueTransformers()`, use `PropertyMetadata::getValueTransformers()` instead * Remove `PropertyMetadata::withNativeToStreamValueTransformers()` and `PropertyMetadata::withStreamToNativeValueTransformers()`, use `PropertyMetadata::withValueTransformers()` instead * Remove `PropertyMetadata::withAdditionalNativeToStreamValueTransformer()` and `PropertyMetadata::withAdditionalStreamToNativeValueTransformer`, use `PropertyMetadata::withAdditionalValueTransformer()` instead Ldap ---- * Remove the `sizeLimit` option of `AbstractQuery` * Remove `LdapUser::eraseCredentials()` in favor of `__serialize()` * Add methods for `saslBind()` and `whoami()` to `ConnectionInterface` and `LdapInterface` Mailer ------ * Remove `TransportFactoryTestCase`, extend `AbstractTransportFactoryTestCase` instead Messenger --------- * Remove `text` format when using the `messenger:stats` command * Add method `getRetryDelay()` to `RecoverableExceptionInterface` Mime ---- * Replace `__sleep/wakeup()` by `__(un)serialize()` on `AbstractPart` implementations MonologBridge ------------- * Remove `NotFoundActivationStrategy`, use `HttpCodeActivationStrategy` instead Notifier -------- * Remove the Sms77 Notifier bridge * Remove `TransportFactoryTestCase`, extend `AbstractTransportFactoryTestCase` instead. To keep using the `testIncompleteDsnException()` and `testMissingRequiredOptionException()` tests, you now need to use `IncompleteDsnTestTrait` or `MissingRequiredOptionTestTrait` respectively. OptionsResolver --------------- * Remove support for nested options definition via `setDefault()`, use `setOptions()` instead ```diff -$resolver->setDefault('option', function (OptionsResolver $resolver) { +$resolver->setOptions('option', function (OptionsResolver $resolver) { // ... }); ``` PropertyInfo ------------ * Remove the `PropertyTypeExtractorInterface::getTypes()` method, use `PropertyTypeExtractorInterface::getType()` instead ```diff -$types = $extractor->getTypes(Foo::class, 'property'); +$type = $extractor->getType(Foo::class, 'property'); ``` * Remove the `ConstructorArgumentTypeExtractorInterface::getTypesFromConstructor()` method, use `ConstructorArgumentTypeExtractorInterface::getTypeFromConstructor()` instead ```diff -$types = $extractor->getTypesFromConstructor(Foo::class, 'property'); +$type = $extractor->getTypeFromConstructor(Foo::class, 'property'); ``` * Remove the `Type` class, use `Symfony\Component\TypeInfo\Type` class from `symfony/type-info` instead *Before* ```php 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* ```php 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(); // ... } ``` Routing ------- * Remove support for accessing the internal scope of the loader in PHP config files, use only its public API instead * Providing a non-array `_query` parameter to `UrlGenerator` causes an `InvalidParameterException` * Remove the protected `AttributeClassLoader::$routeAnnotationClass` property and the `setRouteAnnotationClass()` method, use `AttributeClassLoader::setRouteAttributeClass()` instead * Remove class aliases in the `Annotation` namespace, use attributes instead * Remove getters and setters in attribute classes in favor of public properties Security -------- * When extending the `RememberMeDetails` class and overriding its constructor, the `$userFqcn` parameter has to be removed from its signature: *Before* ```php class CustomRememberMeDetails extends RememberMeDetails { public function __construct(string $userFqcn, string $userIdentifier, int $expires, string $value) { parent::__construct($userFqcn, $userIdentifier, $expires, $value); } } ``` *After* ```php class CustomRememberMeDetails extends RememberMeDetails { public function __construct(string $userIdentifier, int $expires, string $value) { parent::__construct($userIdentifier, $expires, $value); } } ``` * Add argument `$accessDecision` to `AccessDecisionStrategyInterface::decide()` * Remove `PersistentTokenInterface::getClass()` and `RememberMeDetails::getUserFqcn()` * Remove the user FQCN from the remember-me cookie * Remove `UserInterface::eraseCredentials()` and `TokenInterface::eraseCredentials()`; erase credentials e.g. using `__serialize()` instead: ```diff -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 `BadCredentialsException` when passing an empty string as `$userIdentifier` argument to `UserBadge` constructor * Accept only `ExposeSecurityLevel` enums for `AuthenticatorManager`'s `$exposeSecurityErrors` argument * Respectively accept only `AlgorithmManager` and `JWKSet` for `OidcTokenHandler`'s `$signatureAlgorithm` and `$signatureKeyset` arguments * Remove callable firewall listeners support, extend `AbstractListener` or implement `FirewallListenerInterface` instead * Remove `AbstractListener::__invoke` * Remove `LazyFirewallContext::__invoke()` * Remove `RememberMeToken::getSecret()` * Add argument `$accessDecision` to `AccessDecisionManagerInterface::decide()` and `AuthorizationCheckerInterface::isGranted()` * Add argument `$vote` to `VoterInterface::vote()` and `Voter::voteOnAttribute()` * Add argument `$token` to `UserCheckerInterface::checkPostAuth()` * Add argument `$attributes` to `UserAuthenticatorInterface::authenticateUser()` * Make `UserChainProvider` implement `AttributesBasedUserProviderInterface` SecurityBundle -------------- * Remove the deprecated `hide_user_not_found` configuration option, use `expose_security_errors` instead ```diff # config/packages/security.yaml security: - hide_user_not_found: false + expose_security_errors: 'all' ``` ```diff # config/packages/security.yaml security: - hide_user_not_found: true + expose_security_errors: 'none' ``` Note: The `expose_security_errors` option accepts three values: - `'none'`: Equivalent to `hide_user_not_found: true` (hides all security-related errors) - `'all'`: Equivalent to `hide_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 `ExpressionCacheWarmer` class `final` * Remove the deprecated `algorithm` and `key` options from the OIDC token handler configuration, use `algorithms` and `keyset` instead ```diff # 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`; use `RateLimiterFactoryInterface` instead Serializer ---------- * Remove escape character functionality from `CsvEncoder` ```diff use 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()`, use `withDefaultConstructorArguments()` instead * Change signature of `NameConverterInterface::normalize()` and `NameConverterInterface::denormalize()` methods: ```diff -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`, use `NameConverterInterface` instead * Remove `ClassMetadataFactoryCompiler`, `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` * Remove class aliases in the `Annotation` namespace, use attributes instead * Remove getters in attribute classes in favor of public properties Translation ----------- * Remove the `$escape` parameter from `CsvFileLoader::setCsvControl()` ```diff use Symfony\Component\Translation\Loader\CsvFileLoader; $loader = new CsvFileLoader(); // Set CSV control characters including escape character -$loader->setCsvControl(';', '"', '\\'); +$loader->setCsvControl(';', '"'); ``` * Remove `TranslatableMessage::__toString()` method, use `trans()` or `getMessage()` instead * Make `DataCollectorTranslator` class `final` * Remove `ProviderFactoryTestCase`, extend `AbstractProviderFactoryTestCase` instead String ------ * Replace `__sleep/wakeup()` by `__(un)serialize()` on string implementations TwigBridge ---------- * Remove support for passing a tag to the constructor of `FormThemeNode` * Remove `text` format from the `debug:twig` command, use the `txt` format instead TwigBundle ---------- * Make `TemplateCacheWarmer` class `final` * Remove the `base_template_class` config option TypeInfo -------- * Constructing a `CollectionType` instance as a list that is not an array throws an `InvalidArgumentException` * Remove the third `$asList` argument of `TypeFactoryTrait::iterable()`, use `TypeFactoryTrait::list()` instead ```diff use Symfony\Component\TypeInfo\Type; -$type = Type::iterable(Type::string(), asList: true); +$type = Type::list(Type::string()); ``` Uid --- * Add argument `$format` to `Uuid::isValid()` Validator --------- * Remove support for configuring constraint options implicitly with the XML format *Before* ```xml Symfony\Component\Validator\Tests\Fixtures\CallbackClass callback ``` *After* ```xml ``` * Remove support for configuring constraint options implicitly with the YAML format *Before* ```yaml Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity: constraints: - Callback: validateMeStatic - Callback: [Symfony\Component\Validator\Tests\Fixtures\CallbackClass, callback] ``` *After* ```yaml 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 `GroupSequence` *Before* ```php $groupSequence = GroupSequence(['value' => ['group 1', 'group 2']]); ``` *After* ```php $groupSequence = GroupSequence(['group 1', 'group 2']); ``` * Change the default value of the `$requireTld` option of the `Url` constraint to `true` * Add method `getGroupProvider()` to `ClassMetadataInterface` * Replace `__sleep/wakeup()` by `__(un)serialize()` on `GenericMetadata` implementations * Remove the `getRequiredOptions()` and `getDefaultOption()` methods from the `All`, `AtLeastOneOf`, `CardScheme`, `Collection`, `CssColor`, `Expression`, `Regex`, `Sequentially`, `Type`, and `When` constraints * Remove support for evaluating options in the base `Constraint` class. Initialize properties in the constructor of the concrete constraint class instead. *Before* ```php class CustomConstraint extends Constraint { public $option1; public $option2; public function __construct(?array $options = null) { parent::__construct($options); } } ``` *After* ```php 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 base `Constraint` class. Use mandatory constructor arguments instead. *Before* ```php class CustomConstraint extends Constraint { public $option1; public $option2; public function __construct(?array $options = null) { parent::__construct($options); } public function getRequiredOptions() { return ['option1']; } } ``` *After* ```php 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()` and `getDefaultOption()` methods of the base `Constraint` class without replacements. Overriding them in child constraint does not have any effects. * Remove support for passing an array of options to the `Composite` constraint class. Initialize the properties referenced with `getNestedConstraints()` in child classes before calling the constructor of `Composite`. *Before* ```php class CustomCompositeConstraint extends Composite { public array $constraints = []; public function __construct(?array $options = null) { parent::__construct($options); } protected function getCompositeOption(): string { return 'constraints'; } } ``` *After* ```php 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_ERROR` constant. This error code was not used in the Bic constraint validator anymore VarExporter ----------- * Restrict `ProxyHelper::generateLazyProxy()` to generating abstraction-based lazy decorators; use native lazy proxies otherwise * Remove `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead * Remove `ProxyHelper::generateLazyGhost()`, use native lazy objects instead Webhook ------- * Add argument `$request` to `RequestParserInterface::createSuccessfulResponse()` and `RequestParserInterface::createRejectedResponse()` WebProfilerBundle ----------------- * Remove `profiler.xml` and `wdt.xml` routing configuration files (use their PHP equivalent instead) Workflow -------- * Add method `getEnabledTransition()` to `WorkflowInterface` * Add `$nbToken` argument to `Marking::mark()` and `Marking::unmark()` * Add `$asArc` argument to `Transition::getFroms()` and `Transition::getTos()` * Remove `Event::getWorkflow()` method *Before* ```php 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* ```php 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* ```php 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()); } } ``` Yaml ---- * Remove support for parsing duplicate mapping keys whose value is `null`