Skip to content

Commit b8f8c45

Browse files
Merge branch '7.4' into 8.0
* 7.4: [Messenger] Allow to use custom http client for sqs messenger transport [JsonStreamer] Merge `PropertyMetadata` value transformers [Mailer] Relax regexp to parse message ids [Mailer] Fix parsing message ids in SMTP responses Reviewed translations [HttpClient] Consider cached responses without expiration as immediately stale [Routing] Indicate type of rejected object in CompiledUrlMatcherDumper [Workflow] Extract code from the data collector to a dedicated class [Messenger] Add `MessageSentToTransportsEvent` Add FormFlow for multistep forms management [HttpKernel][DebugBundle] Collect dumps when console profiling is enabled
2 parents bd319d8 + c99cf92 commit b8f8c45

File tree

3 files changed

+238
-105
lines changed

3 files changed

+238
-105
lines changed

DataCollector/WorkflowDataCollector.php

Lines changed: 15 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@
1919
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
2020
use Symfony\Component\VarDumper\Caster\Caster;
2121
use Symfony\Component\VarDumper\Cloner\Stub;
22+
use Symfony\Component\Workflow\Debug\ListenerExtractor;
2223
use Symfony\Component\Workflow\Debug\TraceableWorkflow;
2324
use Symfony\Component\Workflow\Dumper\MermaidDumper;
24-
use Symfony\Component\Workflow\EventListener\GuardExpression;
25-
use Symfony\Component\Workflow\EventListener\GuardListener;
2625
use Symfony\Component\Workflow\Marking;
27-
use Symfony\Component\Workflow\Transition;
2826
use Symfony\Component\Workflow\TransitionBlocker;
2927
use Symfony\Component\Workflow\WorkflowInterface;
3028

@@ -33,11 +31,14 @@
3331
*/
3432
final class WorkflowDataCollector extends DataCollector implements LateDataCollectorInterface
3533
{
34+
private readonly ListenerExtractor $listenerExtractor;
35+
3636
public function __construct(
3737
private readonly iterable $workflows,
38-
private readonly EventDispatcherInterface $eventDispatcher,
39-
private readonly FileLinkFormatter $fileLinkFormatter,
38+
EventDispatcherInterface $eventDispatcher,
39+
?FileLinkFormatter $fileLinkFormatter = null,
4040
) {
41+
$this->listenerExtractor = new ListenerExtractor($eventDispatcher, $fileLinkFormatter);
4142
}
4243

4344
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
@@ -130,112 +131,21 @@ protected function getCasters(): array
130131

131132
private function getEventListeners(WorkflowInterface $workflow): array
132133
{
133-
$listeners = [];
134+
$listeners = $this->listenerExtractor->extractListeners($workflow->getName(), $workflow->getDefinition());
135+
$normalizedListeners = [];
134136
$placeId = 0;
135-
foreach ($workflow->getDefinition()->getPlaces() as $place) {
136-
$eventNames = [];
137-
$subEventNames = [
138-
'leave',
139-
'enter',
140-
'entered',
141-
];
142-
foreach ($subEventNames as $subEventName) {
143-
$eventNames[] = \sprintf('workflow.%s', $subEventName);
144-
$eventNames[] = \sprintf('workflow.%s.%s', $workflow->getName(), $subEventName);
145-
$eventNames[] = \sprintf('workflow.%s.%s.%s', $workflow->getName(), $subEventName, $place);
146-
}
147-
foreach ($eventNames as $eventName) {
148-
foreach ($this->eventDispatcher->getListeners($eventName) as $listener) {
149-
$listeners["place{$placeId}"][$eventName][] = $this->summarizeListener($listener);
150-
}
137+
foreach ($workflow->getDefinition()->getPlaces() as $k => $_) {
138+
if (\array_key_exists('place__'.$k, $listeners)) {
139+
$normalizedListeners["place{$placeId}"] = $listeners['place__'.$k];
151140
}
152-
153141
++$placeId;
154142
}
155-
156-
foreach ($workflow->getDefinition()->getTransitions() as $transitionId => $transition) {
157-
$eventNames = [];
158-
$subEventNames = [
159-
'guard',
160-
'transition',
161-
'completed',
162-
'announce',
163-
];
164-
foreach ($subEventNames as $subEventName) {
165-
$eventNames[] = \sprintf('workflow.%s', $subEventName);
166-
$eventNames[] = \sprintf('workflow.%s.%s', $workflow->getName(), $subEventName);
167-
$eventNames[] = \sprintf('workflow.%s.%s.%s', $workflow->getName(), $subEventName, $transition->getName());
168-
}
169-
foreach ($eventNames as $eventName) {
170-
foreach ($this->eventDispatcher->getListeners($eventName) as $listener) {
171-
$listeners["transition{$transitionId}"][$eventName][] = $this->summarizeListener($listener, $eventName, $transition);
172-
}
173-
}
174-
}
175-
176-
return $listeners;
177-
}
178-
179-
private function summarizeListener(callable $callable, ?string $eventName = null, ?Transition $transition = null): array
180-
{
181-
$extra = [];
182-
183-
if ($callable instanceof \Closure) {
184-
$r = new \ReflectionFunction($callable);
185-
if ($r->isAnonymous()) {
186-
$title = (string) $r;
187-
} elseif ($class = $r->getClosureCalledClass()) {
188-
$title = $class->name.'::'.$r->name.'()';
189-
} else {
190-
$title = $r->name;
191-
}
192-
} elseif (\is_string($callable)) {
193-
$title = $callable.'()';
194-
$r = new \ReflectionFunction($callable);
195-
} elseif (\is_object($callable) && method_exists($callable, '__invoke')) {
196-
$r = new \ReflectionMethod($callable, '__invoke');
197-
$title = $callable::class.'::__invoke()';
198-
} elseif (\is_array($callable)) {
199-
if ($callable[0] instanceof GuardListener) {
200-
if (null === $eventName || null === $transition) {
201-
throw new \LogicException('Missing event name or transition.');
202-
}
203-
$extra['guardExpressions'] = $this->extractGuardExpressions($callable[0], $eventName, $transition);
204-
}
205-
$r = new \ReflectionMethod($callable[0], $callable[1]);
206-
$title = (\is_string($callable[0]) ? $callable[0] : \get_class($callable[0])).'::'.$callable[1].'()';
207-
} else {
208-
throw new \RuntimeException('Unknown callable type.');
209-
}
210-
211-
$file = null;
212-
if ($r->isUserDefined()) {
213-
$file = $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine());
214-
}
215-
216-
return [
217-
'title' => $title,
218-
'file' => $file,
219-
...$extra,
220-
];
221-
}
222-
223-
private function extractGuardExpressions(GuardListener $listener, string $eventName, Transition $transition): array
224-
{
225-
$configuration = (new \ReflectionProperty(GuardListener::class, 'configuration'))->getValue($listener);
226-
227-
$expressions = [];
228-
foreach ($configuration[$eventName] as $guard) {
229-
if ($guard instanceof GuardExpression) {
230-
if ($guard->getTransition() !== $transition) {
231-
continue;
232-
}
233-
$expressions[] = $guard->getExpression();
234-
} else {
235-
$expressions[] = $guard;
143+
foreach ($workflow->getDefinition()->getTransitions() as $k => $_) {
144+
if (\array_key_exists('transition__'.$k, $listeners)) {
145+
$normalizedListeners["transition$k"] = $listeners['transition__'.$k];
236146
}
237147
}
238148

239-
return $expressions;
149+
return $normalizedListeners;
240150
}
241151
}

Debug/ListenerExtractor.php

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow\Debug;
13+
14+
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
15+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
16+
use Symfony\Component\Workflow\Definition;
17+
use Symfony\Component\Workflow\EventListener\GuardExpression;
18+
use Symfony\Component\Workflow\EventListener\GuardListener;
19+
use Symfony\Component\Workflow\Transition;
20+
21+
/**
22+
* @internal
23+
*
24+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
25+
*/
26+
final readonly class ListenerExtractor
27+
{
28+
public function __construct(
29+
private ?EventDispatcherInterface $dispatcher = null,
30+
private ?FileLinkFormatter $fileLinkFormatter = null,
31+
) {
32+
}
33+
34+
public function extractListeners(string $name, Definition $definition): array
35+
{
36+
if (!$this->dispatcher) {
37+
return [];
38+
}
39+
40+
$listeners = [];
41+
foreach ($definition->getPlaces() as $placeId => $place) {
42+
$eventNames = [];
43+
$subEventNames = [
44+
'leave',
45+
'enter',
46+
'entered',
47+
];
48+
foreach ($subEventNames as $subEventName) {
49+
$eventNames[] = \sprintf('workflow.%s', $subEventName);
50+
$eventNames[] = \sprintf('workflow.%s.%s', $name, $subEventName);
51+
$eventNames[] = \sprintf('workflow.%s.%s.%s', $name, $subEventName, $place);
52+
}
53+
foreach ($eventNames as $eventName) {
54+
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
55+
$listeners["place__$placeId"][$eventName][] = $this->summarizeListener($listener);
56+
}
57+
}
58+
}
59+
60+
foreach ($definition->getTransitions() as $transitionId => $transition) {
61+
$eventNames = [];
62+
$subEventNames = [
63+
'guard',
64+
'transition',
65+
'completed',
66+
'announce',
67+
];
68+
foreach ($subEventNames as $subEventName) {
69+
$eventNames[] = \sprintf('workflow.%s', $subEventName);
70+
$eventNames[] = \sprintf('workflow.%s.%s', $name, $subEventName);
71+
$eventNames[] = \sprintf('workflow.%s.%s.%s', $name, $subEventName, $transition->getName());
72+
}
73+
foreach ($eventNames as $eventName) {
74+
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
75+
$listeners["transition__{$transitionId}"][$eventName][] = $this->summarizeListener($listener, $eventName, $transition);
76+
}
77+
}
78+
}
79+
80+
return $listeners;
81+
}
82+
83+
private function summarizeListener(callable $callable, ?string $eventName = null, ?Transition $transition = null): array
84+
{
85+
$extra = [];
86+
87+
if ($callable instanceof \Closure) {
88+
$r = new \ReflectionFunction($callable);
89+
if ($r->isAnonymous()) {
90+
$title = (string) $r;
91+
} elseif ($class = $r->getClosureCalledClass()) {
92+
$title = $class->name.'::'.$r->name.'()';
93+
} else {
94+
$title = $r->name;
95+
}
96+
} elseif (\is_string($callable)) {
97+
$title = $callable.'()';
98+
$r = new \ReflectionFunction($callable);
99+
} elseif (\is_object($callable) && method_exists($callable, '__invoke')) {
100+
$r = new \ReflectionMethod($callable, '__invoke');
101+
$title = $callable::class.'::__invoke()';
102+
} elseif (\is_array($callable)) {
103+
if ($callable[0] instanceof GuardListener) {
104+
if (null === $eventName || null === $transition) {
105+
throw new \LogicException('Missing event name or transition.');
106+
}
107+
$extra['guardExpressions'] = $this->extractGuardExpressions($callable[0], $eventName, $transition);
108+
}
109+
$r = new \ReflectionMethod($callable[0], $callable[1]);
110+
$title = (\is_string($callable[0]) ? $callable[0] : \get_class($callable[0])).'::'.$callable[1].'()';
111+
} else {
112+
throw new \RuntimeException('Unknown callable type.');
113+
}
114+
115+
$file = null;
116+
if ($r->isUserDefined()) {
117+
$file = $this->fileLinkFormatter?->format($r->getFileName(), $r->getStartLine());
118+
}
119+
120+
return [
121+
'title' => $title,
122+
'file' => $file,
123+
...$extra,
124+
];
125+
}
126+
127+
private function extractGuardExpressions(GuardListener $listener, string $eventName, Transition $transition): array
128+
{
129+
$configuration = (new \ReflectionProperty(GuardListener::class, 'configuration'))->getValue($listener);
130+
131+
$expressions = [];
132+
foreach ($configuration[$eventName] as $guard) {
133+
if ($guard instanceof GuardExpression) {
134+
if ($guard->getTransition() !== $transition) {
135+
continue;
136+
}
137+
$expressions[] = $guard->getExpression();
138+
} else {
139+
$expressions[] = $guard;
140+
}
141+
}
142+
143+
return $expressions;
144+
}
145+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow\Tests\Debug;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
16+
use Symfony\Component\EventDispatcher\EventDispatcher;
17+
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
18+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
19+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
20+
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
21+
use Symfony\Component\Validator\Validator\ValidatorInterface;
22+
use Symfony\Component\Workflow\Debug\ListenerExtractor;
23+
use Symfony\Component\Workflow\EventListener\ExpressionLanguage;
24+
use Symfony\Component\Workflow\EventListener\GuardListener;
25+
use Symfony\Component\Workflow\Tests\WorkflowBuilderTrait;
26+
use Symfony\Component\Workflow\Workflow;
27+
28+
class ListenerExtractorTest extends TestCase
29+
{
30+
use WorkflowBuilderTrait;
31+
32+
public function test()
33+
{
34+
$workflow1 = new Workflow($this->createComplexWorkflowDefinition(), name: 'workflow1');
35+
$workflow2 = new Workflow($this->createSimpleWorkflowDefinition(), name: 'workflow2');
36+
$dispatcher = new EventDispatcher();
37+
$dispatcher->addListener('workflow.workflow2.leave.a', fn () => true);
38+
$dispatcher->addListener('workflow.workflow2.leave.a', [self::class, 'noop']);
39+
$dispatcher->addListener('workflow.workflow2.leave.a', [$this, 'noop']);
40+
$dispatcher->addListener('workflow.workflow2.leave.a', $this->noop(...));
41+
$dispatcher->addListener('workflow.workflow2.leave.a', 'var_dump');
42+
$guardListener = new GuardListener(
43+
['workflow.workflow2.guard.t1' => ['my_expression']],
44+
$this->createMock(ExpressionLanguage::class),
45+
$this->createMock(TokenStorageInterface::class),
46+
$this->createMock(AuthorizationCheckerInterface::class),
47+
$this->createMock(AuthenticationTrustResolverInterface::class),
48+
$this->createMock(RoleHierarchyInterface::class),
49+
$this->createMock(ValidatorInterface::class)
50+
);
51+
$dispatcher->addListener('workflow.workflow2.guard.t1', [$guardListener, 'onTransition']);
52+
53+
$extractor = new ListenerExtractor($dispatcher, new FileLinkFormatter());
54+
55+
$workflow1 = $extractor->extractListeners($workflow1->getName(), $workflow1->getDefinition());
56+
57+
$this->assertSame([], $workflow1);
58+
59+
$workflow2 = $extractor->extractListeners($workflow2->getName(), $workflow2->getDefinition());
60+
$this->assertArrayHasKey('place__a', $workflow2);
61+
$this->assertArrayHasKey('workflow.workflow2.leave.a', $workflow2['place__a']);
62+
$descriptions = $workflow2['place__a']['workflow.workflow2.leave.a'];
63+
$this->assertCount(5, $descriptions);
64+
$this->assertStringContainsString('Closure', $descriptions[0]['title']);
65+
$this->assertSame('Symfony\Component\Workflow\Tests\Debug\ListenerExtractorTest::noop()', $descriptions[1]['title']);
66+
$this->assertSame('Symfony\Component\Workflow\Tests\Debug\ListenerExtractorTest::noop()', $descriptions[2]['title']);
67+
$this->assertSame('Symfony\Component\Workflow\Tests\Debug\ListenerExtractorTest::noop()', $descriptions[3]['title']);
68+
$this->assertSame('var_dump()', $descriptions[4]['title']);
69+
$this->assertArrayHasKey('transition__0', $workflow2);
70+
$this->assertArrayHasKey('workflow.workflow2.guard.t1', $workflow2['transition__0']);
71+
$this->assertSame('Symfony\Component\Workflow\EventListener\GuardListener::onTransition()', $workflow2['transition__0']['workflow.workflow2.guard.t1'][0]['title']);
72+
$this->assertSame(['my_expression'], $workflow2['transition__0']['workflow.workflow2.guard.t1'][0]['guardExpressions']);
73+
}
74+
75+
public static function noop()
76+
{
77+
}
78+
}

0 commit comments

Comments
 (0)