Repository: symfony/symfony
Pull request #60195: [Workflow] Deprecate `Event::getWorkflow()` method
Author: login: lyrixx
Labels: name: Status: Needs Review
name: Deprecation
name: Workflow
Created: 2025-04-10T14:22:45Z | Merged: 2025-04-14T12:32:34Z

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | kind of
| New feature?  | no
| Deprecations? | yes
| Issues        | Fix #59748
| License       | MIT



```diff
+use Symfony\Component\DependencyInjection\Attribute\Target;
 
 class ContinueToState3 implements EventSubscriberInterface
 {
+    public function __construct(
+        #[Target('your_workflow_name')]
+        private readonly WorkflowInterface $workflow,
+    ) {
+    }
 
     public static function getSubscribedEvents(): array
     {
         return [
             'workflow.your_workflow_name.completed.to_state2' => ['terminateOrder', \PHP_INT_MIN],
         ];
     }
 
     public function terminateOrder(Event $event): void
     {
         $subject = $event->getSubject();
-        if ($event->getWorkflow()->workflow->can($subject, 'to_state3')) {
-            $event->getWorkflow()->apply($subject, 'to_state3');
+        if ($this->workflow->can($subject, 'to_state3')) {
+            $this->workflow->apply($subject, 'to_state3');
         }
     }
 }
```

If one has a listener able to run on many workflow, it need to inject a locator

```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());
       }
   }
```

Linked issue #59748: [Workflow][Profiler] No information from chainned transition
### Symfony version(s) affected

6.4.18

### Description

In the workflow profiler, I have information for calls `can` and `apply` about the transition `to_state2` but no information about `to_state3` transition.


### How to reproduce

Workflow:

```yaml
my_workflow:
  type: state_machine
  marking_store:
      type: method
      property: state
  supports:
    - MyClass
  places:
    - state1
    - state2
    - state3
  
  transitions:
    to_state2:
      from: state1
      to: state2
    to_state3:
      from: state2
      to: state3
```

Event listener/subscriber:

```php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ContinueToState3 implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'workflow.my_workflow.completed.to_state2' => ['terminateOrder', \PHP_INT_MIN],
        ];
    }

    public function terminateOrder(Event $event): void
    {
        $workflow = $event->getWorkflow();
        $subject = $event->getSubject();
        if ($workflow->can($subject, 'to_state3')) {
            $workflow->apply($subject, 'to_state3');
        }
    }
}
```



### Possible Solution

_No response_

### Additional Context

_No response_
Linked issue comments: user: login: lyrixx
created_at: 2025-04-10T14:01:05Z
body: Hello. 

This is "legit". We decorate the workflow instance to gather some data in the profiler.
But when we dispatch an event, we attach the real workflow instance. Not the one decorated.

If you weed proper profiling, you need to inject the workflow in your listener.

```diff
+use Symfony\Component\DependencyInjection\Attribute\Target;
 
 class ContinueToState3 implements EventSubscriberInterface
 {
+    public function __construct(
+        #[Target('your_workflow_name')]
+        private readonly WorkflowInterface $workflow,
+    ) {
+    }
 
     public static function getSubscribedEvents(): array
     {
         return [
             'workflow.my_workflow.completed.to_state2' => ['terminateOrder', \PHP_INT_MIN],
         ];
     }
 
     public function terminateOrder(Event $event): void
     {
         $subject = $event->getSubject();
-        if ($event->getWorkflow()->workflow->can($subject, 'to_state3')) {
-            $event->getWorkflow()->apply($subject, 'to_state3');
+        if ($this->workflow->can($subject, 'to_state3')) {
+            $this->workflow->apply($subject, 'to_state3');
         }
     }
 }
```

---

Not for other maintainer: I'm thinking about deprecating `Event::getWorkflow()`

---

Thanks for the issue. Since there is a solution, I'm closing this issue.
author_association: MEMBER
user: login: jbcr
created_at: 2025-04-10T14:05:24Z
body: Thank you.

Other solution : inject the decorated workflow into the event.
author_association: NONE
user: login: stof
created_at: 2025-04-10T14:16:29Z
body: @jbcr that's not possible because the inner workflow (creating the event to dispatch it) is not aware of the decorator (currently, it passes `$this` to the event)
author_association: MEMBER
user: login: lyrixx
created_at: 2025-04-10T14:16:42Z
body: > Thank you.
> 
> Other solution : inject the decorated workflow into the event.

I tried, but the decorated workflow does not know anything about the decorator 
author_association: MEMBER
user: login: stof
created_at: 2025-04-10T14:20:35Z
body: > Note for other maintainer: I'm thinking about deprecating `Event::getWorkflow()`

If you want to do that, you would probably need to introduce a `getWorkflowName()` method instead, as a common use case for accessing the workflow from the event is to identify it (in case the same listener could receive events from several workflows).
And maybe `$event->getDefinition()` could be needed to replace `$event->getWorkflow()->getDefinition()` if accessing the definition in a listener is a use case (I don't know enough about the workflow component to know whether this is an actual use case)
author_association: MEMBER
user: login: lyrixx
created_at: 2025-04-10T14:22:08Z
body: > If you want to do that, you would probably need to introduce a `getWorkflowName()` method instead, as a common use case for accessing the workflow from the event is to identify it (in case the same listener could receive events from several workflows).

It already exist.



> And maybe `$event->getDefinition()` could be needed to replace `$event->getWorkflow()->getDefinition()` if accessing the definition in a listener is a use case (I don't know enough about the workflow component to know whether this is an actual use case)

I never did that :) 
author_association: MEMBER
user: login: stof
created_at: 2025-04-10T14:53:59Z
body: > It already exist.

very good (I did not check before commenting).
author_association: MEMBER

Diff:
diff --git a/UPGRADE-7.3.md b/UPGRADE-7.3.md
index 14a32391b28dc..0f3163740cfac 100644
--- a/UPGRADE-7.3.md
+++ b/UPGRADE-7.3.md
@@ -249,3 +249,78 @@ VarExporter
  * Deprecate using `ProxyHelper::generateLazyProxy()` when native lazy proxies can be used - the method should be used to generate abstraction-based lazy decorators only
  * Deprecate `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead
  * Deprecate `ProxyHelper::generateLazyGhost()`, use native lazy objects instead
+
+Workflow
+--------
+
+ * Deprecate `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('your_workflow_name')]
+           private readonly WorkflowInterface $workflow,
+       ) {
+       }
+
+       #[AsCompletedListener('your_workflow_name', '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());
+       }
+   }
+   ```
diff --git a/src/Symfony/Component/Workflow/CHANGELOG.md b/src/Symfony/Component/Workflow/CHANGELOG.md
index 2926da4e6428d..5a37eadfc892d 100644
--- a/src/Symfony/Component/Workflow/CHANGELOG.md
+++ b/src/Symfony/Component/Workflow/CHANGELOG.md
@@ -1,13 +1,18 @@
 CHANGELOG
 =========
 
+7.3
+---
+
+ * Deprecate `Event::getWorkflow()` method
+
 7.1
 ---
 
  * Add method `getEnabledTransition()` to `WorkflowInterface`
  * Automatically register places from transitions
  * Add support for workflows that need to store many tokens in the marking
- * Add method `getName()` in event classes to build event names in subscribers 
+ * Add method `getName()` in event classes to build event names in subscribers
 
 7.0
 ---
diff --git a/src/Symfony/Component/Workflow/Event/Event.php b/src/Symfony/Component/Workflow/Event/Event.php
index c3e6a6f582434..c13818b93c115 100644
--- a/src/Symfony/Component/Workflow/Event/Event.php
+++ b/src/Symfony/Component/Workflow/Event/Event.php
@@ -46,8 +46,13 @@ public function getTransition(): ?Transition
         return $this->transition;
     }
 
+    /**
+     * @deprecated since Symfony 7.3, inject the workflow in the constructor where you need it
+     */
     public function getWorkflow(): WorkflowInterface
     {
+        trigger_deprecation('symfony/workflow', '7.3', 'The "%s()" method is deprecated, inject the workflow in the constructor where you need it.', __METHOD__);
+
         return $this->workflow;
     }
 
diff --git a/src/Symfony/Component/Workflow/composer.json b/src/Symfony/Component/Workflow/composer.json
index 44a300057c04b..ef6779c6de142 100644
--- a/src/Symfony/Component/Workflow/composer.json
+++ b/src/Symfony/Component/Workflow/composer.json
@@ -20,15 +20,16 @@
         }
     ],
     "require": {
-        "php": ">=8.2"
+        "php": ">=8.2",
+        "symfony/deprecation-contracts": "2.5|^3"
     },
     "require-dev": {
         "psr/log": "^1|^2|^3",
         "symfony/dependency-injection": "^6.4|^7.0",
-        "symfony/event-dispatcher": "^6.4|^7.0",
         "symfony/error-handler": "^6.4|^7.0",
-        "symfony/http-kernel": "^6.4|^7.0",
+        "symfony/event-dispatcher": "^6.4|^7.0",
         "symfony/expression-language": "^6.4|^7.0",
+        "symfony/http-kernel": "^6.4|^7.0",
         "symfony/security-core": "^6.4|^7.0",
         "symfony/stopwatch": "^6.4|^7.0",
         "symfony/validator": "^6.4|^7.0"


Reviews:
user: login: nicolas-grekas
body: Looks like my comment about `#[Deprecated]` would apply to the few other cases where we use it.
Alternatively, we could ban it from the codebase. It's not better than the annotation anyway.
We should maybe postpone using it to Symfony 8+, where we won't have to duplicate the runtime notice for PHP <8.4.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T14:28:21Z
user: login: nicolas-grekas
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T14:49:10Z
user: login: alexandre-daubois
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-11T07:15:48Z
user: login: stof
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T08:27:53Z
user: login: lyrixx
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T08:51:25Z

Review comments:
diff_hunk: @@ -46,8 +46,14 @@ public function getTransition(): ?Transition
         return $this->transition;
     }
 
+    /**
+     * @deprecated since Symfony 7.3, inject the workflow in the constructor where you need it
+     */
+    #[\Deprecated(since: 'symfony/workflow 7.3')]
path: src/Symfony/Component/Workflow/Event/Event.php
user: login: nicolas-grekas
body: can't we also add the same hint as below in this one?
created_at: 2025-04-10T14:26:35Z
author_association: MEMBER
diff_hunk: @@ -46,8 +46,14 @@ public function getTransition(): ?Transition
         return $this->transition;
     }
 
+    /**
+     * @deprecated since Symfony 7.3, inject the workflow in the constructor where you need it
+     */
+    #[\Deprecated(since: 'symfony/workflow 7.3')]
     public function getWorkflow(): WorkflowInterface
     {
+        trigger_deprecation('symfony/workflow', '7.4', 'The "%s()" method is deprecated, inject the workflow in the constructor where you need it.', __METHOD__);
path: src/Symfony/Component/Workflow/Event/Event.php
user: login: nicolas-grekas
body: ```suggestion
        if (\PHP_VERSION_ID < 80400) {
            trigger_deprecation('symfony/workflow', '7.4', 'The "%s()" method is deprecated, inject the workflow in the constructor where you need it.', __METHOD__);
        }
```
created_at: 2025-04-10T14:26:47Z
author_association: MEMBER
diff_hunk: @@ -249,3 +249,54 @@ VarExporter
  * Deprecate using `ProxyHelper::generateLazyProxy()` when native lazy proxies can be used - the method should be used to generate abstraction-based lazy decorators only
  * Deprecate `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead
  * Deprecate `ProxyHelper::generateLazyGhost()`, use native lazy objects instead
+
+Workflow
+--------
+
+ * Deprecate `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:
path: UPGRADE-7.3.md
user: login: stof
body: shouldn't the `after` show both alternatives, as done in your PR description ?
created_at: 2025-04-10T14:53:15Z
author_association: MEMBER
diff_hunk: @@ -249,3 +249,54 @@ VarExporter
  * Deprecate using `ProxyHelper::generateLazyProxy()` when native lazy proxies can be used - the method should be used to generate abstraction-based lazy decorators only
  * Deprecate `LazyGhostTrait` and `LazyProxyTrait`, use native lazy objects instead
  * Deprecate `ProxyHelper::generateLazyGhost()`, use native lazy objects instead
+
+Workflow
+--------
+
+ * Deprecate `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:
path: UPGRADE-7.3.md
user: login: lyrixx
body: I added it (and fixed typo on naming + added use stmt)
created_at: 2025-04-11T08:51:25Z
author_association: MEMBER

PR comments:
user: login: stof
created_at: 2025-04-10T14:52:03Z
body: > Alternatively, we could ban it from the codebase. It's not better than the annotation anyway.

The benefit of the native attribute is that PHP will report the deprecation in the place doing the call, not in the method being deprecated, so it gives a better location for the warning (in our system using `trigger_deprecation`, identifying the most useful location requires investigating the trace, which is not displayed by default by PHP for warnings IIRC, even though it is included when using `symfony/error-handler`).
But I agree we can delay its usage in our codebase until Symfony 8.x when we won't need a fallback anymore.
author_association: MEMBER
user: login: ro0NL
created_at: 2025-04-11T17:59:52Z
body: ```
$this->workflows->get($event->getWorkflowName());
```

im wondering if we can avoid `$this` and `$event` mixing to get a workflow, eg. is the deprecation really legit?

cant we fix `getWorkflow` instead? using the same solution (leveraging the actual DI target object)
author_association: CONTRIBUTOR
user: login: lyrixx
created_at: 2025-04-11T18:04:24Z
body: > cant we fix `getWorkflow` instead? using the same solution (leveraging the actual DI target object)

I means injecting a locator in every workflow. IMHO this is not the respectability of the workflow to do that.

I already thought about using a custom event dispatcher, but a bit overkill IMHO.

More over, having a generic listener is really rare, so this use case is really limited
author_association: MEMBER
user: login: nicolas-grekas
created_at: 2025-04-14T12:32:29Z
body: Thank you @lyrixx.
author_association: MEMBER