Skip to content

Commit 124706b

Browse files
committed
feature #51220 [Workflow] Add a TraceableWorkflow (lyrixx)
This PR was merged into the 6.4 branch. Discussion ---------- [Workflow] Add a `TraceableWorkflow` | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | | License | MIT | Doc PR | ![image](https://github.com/symfony/symfony/assets/408368/fd6baf45-7b70-452d-beac-d673a8eeef8c) ![image](https://github.com/symfony/symfony/assets/408368/c58d8e00-abf2-4967-a22a-71bde33e3fe2) --- Initially, I wanted to display the current marking on the graph, but it's not possible. "current" does not mean anything. What if I use the workflow for 2 entities? What if I call twice `apply()`. Which marking should I choose? => this is not possible. But, DoctrineBundle use ajax, like for explaining SQL request. It would be nice to be able to click on a marking, and it'll re-render the graph with this marking. WDYT? Commits ------- 963cc7a [Worflow] Add a TraceableWorkflow
2 parents ad081be + 963cc7a commit 124706b

File tree

6 files changed

+278
-3
lines changed

6 files changed

+278
-3
lines changed

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
7171
use Symfony\Component\VarExporter\Internal\Hydrator;
7272
use Symfony\Component\VarExporter\Internal\Registry;
73+
use Symfony\Component\Workflow\DependencyInjection\WorkflowDebugPass;
7374
use Symfony\Component\Workflow\DependencyInjection\WorkflowGuardListenerPass;
7475

7576
// Help opcache.preload discover always-needed symbols
@@ -189,6 +190,7 @@ public function build(ContainerBuilder $container)
189190
$container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING);
190191
$container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_BEFORE_REMOVING, -255);
191192
$container->addCompilerPass(new CacheCollectorPass(), PassConfig::TYPE_BEFORE_REMOVING);
193+
$this->addCompilerPassIfExists($container, WorkflowDebugPass::class);
192194
}
193195
}
194196

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/workflow.html.twig

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
22

3+
{% block toolbar %}
4+
{% if collector.callsCount > 0 %}
5+
{% set icon %}
6+
{{ source('@WebProfiler/Icon/workflow.svg') }}
7+
<span class="sf-toolbar-value">{{ collector.callsCount }}</span>
8+
{% endset %}
9+
{% set text %}
10+
<div class="sf-toolbar-info-piece">
11+
<b>Workflow Calls</b>
12+
<span>{{ collector.callsCount }}</span>
13+
</div>
14+
{% endset %}
15+
16+
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
17+
{% endif %}
18+
{% endblock %}
19+
320
{% block menu %}
421
<span class="label {{ collector.workflows|length == 0 ? 'disabled' }}">
522
<span class="icon">
@@ -45,15 +62,67 @@
4562
});
4663
</script>
4764

48-
<h2>Definitions</h2>
4965
<div class="sf-tabs js-tabs">
5066
{% for name, data in collector.workflows %}
5167
<div class="tab">
52-
<h3 class="tab-title">{{ name }}</h3>
68+
<h2 class="tab-title">{{ name }}{% if data.calls|length %} ({{ data.calls|length }}){% endif %}</h2>
69+
5370
<div class="tab-content">
71+
<h3>Definition</h3>
5472
<pre class="sf-mermaid">
5573
{{ data.dump|raw }}
5674
</pre>
75+
76+
<h3>Calls</h3>
77+
<table>
78+
<thead>
79+
<tr>
80+
<th>#</th>
81+
<th>Call</th>
82+
<th>Args</th>
83+
<th>Return</th>
84+
<th>Exception</th>
85+
<th>Duration</th>
86+
</tr>
87+
</thead>
88+
<tbody>
89+
{% for call in data.calls %}
90+
<tr>
91+
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
92+
<td>
93+
<code>{{ call.method }}()</code>
94+
{% if call.previousMarking ?? null %}
95+
<hr />
96+
Previous marking:
97+
{{ profiler_dump(call.previousMarking) }}
98+
{% endif %}
99+
</td>
100+
<td>
101+
{{ profiler_dump(call.args) }}
102+
</td>
103+
<td>
104+
{% if call.return is defined %}
105+
{% if call.return is same as true %}
106+
<code>true</code>
107+
{% elseif call.return is same as false %}
108+
<code>false</code>
109+
{% else %}
110+
{{ profiler_dump(call.return) }}
111+
{% endif %}
112+
{% endif %}
113+
</td>
114+
<td>
115+
{% if call.exception is defined %}
116+
{{ profiler_dump(call.exception) }}
117+
{% endif %}
118+
</td>
119+
<td>
120+
{{ call.duration }}ms
121+
</td>
122+
</tr>
123+
{% endfor %}
124+
</tbody>
125+
</table>
57126
</div>
58127
</div>
59128
{% endfor %}

src/Symfony/Component/Workflow/DataCollector/WorkflowDataCollector.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
use Symfony\Component\HttpFoundation\Response;
1616
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
1717
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
18+
use Symfony\Component\VarDumper\Caster\Caster;
19+
use Symfony\Component\VarDumper\Cloner\Stub;
20+
use Symfony\Component\Workflow\Debug\TraceableWorkflow;
1821
use Symfony\Component\Workflow\Dumper\MermaidDumper;
22+
use Symfony\Component\Workflow\Marking;
23+
use Symfony\Component\Workflow\TransitionBlocker;
1924

2025
/**
2126
* @author Grégoire Pineau <lyrixx@lyrixx.info>
@@ -34,11 +39,17 @@ public function collect(Request $request, Response $response, \Throwable $except
3439
public function lateCollect(): void
3540
{
3641
foreach ($this->workflows as $workflow) {
42+
$calls = [];
43+
if ($workflow instanceof TraceableWorkflow) {
44+
$calls = $this->cloneVar($workflow->getCalls());
45+
}
46+
3747
// We always use a workflow type because we want to mermaid to
3848
// create a node for transitions
3949
$dumper = new MermaidDumper(MermaidDumper::TRANSITION_TYPE_WORKFLOW);
4050
$this->data['workflows'][$workflow->getName()] = [
4151
'dump' => $dumper->dump($workflow->getDefinition()),
52+
'calls' => $calls,
4253
];
4354
}
4455
}
@@ -57,4 +68,38 @@ public function getWorkflows(): array
5768
{
5869
return $this->data['workflows'] ?? [];
5970
}
71+
72+
public function getCallsCount(): int
73+
{
74+
$i = 0;
75+
foreach ($this->getWorkflows() as $workflow) {
76+
$i += \count($workflow['calls']);
77+
}
78+
79+
return $i;
80+
}
81+
82+
protected function getCasters(): array
83+
{
84+
$casters = [
85+
...parent::getCasters(),
86+
TransitionBlocker::class => function ($v, array $a, Stub $s, $isNested) {
87+
unset(
88+
$a[sprintf(Caster::PATTERN_PRIVATE, $v::class, 'code')],
89+
$a[sprintf(Caster::PATTERN_PRIVATE, $v::class, 'parameters')],
90+
);
91+
92+
$s->cut += 2;
93+
94+
return $a;
95+
},
96+
Marking::class => function ($v, array $a, Stub $s, $isNested) {
97+
$a[Caster::PREFIX_VIRTUAL.'.places'] = array_keys($v->getPlaces());
98+
99+
return $a;
100+
},
101+
];
102+
103+
return $casters;
104+
}
60105
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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\Stopwatch\Stopwatch;
15+
use Symfony\Component\Workflow\Definition;
16+
use Symfony\Component\Workflow\Marking;
17+
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
18+
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;
19+
use Symfony\Component\Workflow\TransitionBlockerList;
20+
use Symfony\Component\Workflow\WorkflowInterface;
21+
22+
/**
23+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
24+
*/
25+
class TraceableWorkflow implements WorkflowInterface
26+
{
27+
private array $calls = [];
28+
29+
public function __construct(
30+
private readonly WorkflowInterface $workflow,
31+
private readonly Stopwatch $stopwatch,
32+
) {
33+
}
34+
35+
public function getMarking(object $subject, array $context = []): Marking
36+
{
37+
return $this->callInner(__FUNCTION__, \func_get_args());
38+
}
39+
40+
public function can(object $subject, string $transitionName): bool
41+
{
42+
return $this->callInner(__FUNCTION__, \func_get_args());
43+
}
44+
45+
public function buildTransitionBlockerList(object $subject, string $transitionName): TransitionBlockerList
46+
{
47+
return $this->callInner(__FUNCTION__, \func_get_args());
48+
}
49+
50+
public function apply(object $subject, string $transitionName, array $context = []): Marking
51+
{
52+
return $this->callInner(__FUNCTION__, \func_get_args());
53+
}
54+
55+
public function getEnabledTransitions(object $subject): array
56+
{
57+
return $this->callInner(__FUNCTION__, \func_get_args());
58+
}
59+
60+
public function getName(): string
61+
{
62+
return $this->workflow->getName();
63+
}
64+
65+
public function getDefinition(): Definition
66+
{
67+
return $this->workflow->getDefinition();
68+
}
69+
70+
public function getMarkingStore(): MarkingStoreInterface
71+
{
72+
return $this->workflow->getMarkingStore();
73+
}
74+
75+
public function getMetadataStore(): MetadataStoreInterface
76+
{
77+
return $this->workflow->getMetadataStore();
78+
}
79+
80+
public function getCalls(): array
81+
{
82+
return $this->calls;
83+
}
84+
85+
private function callInner(string $method, array $args): mixed
86+
{
87+
$sMethod = $this->workflow::class.'::'.$method;
88+
$this->stopwatch->start($sMethod, 'workflow');
89+
90+
$previousMarking = null;
91+
if ('apply' === $method) {
92+
try {
93+
$previousMarking = $this->workflow->getMarking($args[0]);
94+
} catch (\Throwable) {
95+
}
96+
}
97+
98+
try {
99+
$return = $this->workflow->{$method}(...$args);
100+
101+
$this->calls[] = [
102+
'method' => $method,
103+
'duration' => $this->stopwatch->stop($sMethod)->getDuration(),
104+
'args' => $args,
105+
'previousMarking' => $previousMarking ?? null,
106+
'return' => $return,
107+
];
108+
109+
return $return;
110+
} catch (\Throwable $exception) {
111+
$this->calls[] = [
112+
'method' => $method,
113+
'duration' => $this->stopwatch->stop($sMethod)->getDuration(),
114+
'args' => $args,
115+
'previousMarking' => $previousMarking ?? null,
116+
'exception' => $exception,
117+
];
118+
119+
throw $exception;
120+
}
121+
}
122+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
use Symfony\Component\Workflow\Debug\TraceableWorkflow;
18+
19+
/**
20+
* Adds all configured security voters to the access decision manager.
21+
*
22+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
23+
*/
24+
class WorkflowDebugPass implements CompilerPassInterface
25+
{
26+
public function process(ContainerBuilder $container): void
27+
{
28+
foreach ($container->findTaggedServiceIds('workflow') as $id => $attributes) {
29+
$container->register("debug.{$id}", TraceableWorkflow::class)
30+
->setDecoratedService($id)
31+
->setArguments([
32+
new Reference("debug.{$id}.inner"),
33+
new Reference('debug.stopwatch'),
34+
]);
35+
}
36+
}
37+
}

src/Symfony/Component/Workflow/Registry.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function has(object $subject, string $workflowName = null): bool
4141
return false;
4242
}
4343

44-
public function get(object $subject, string $workflowName = null): Workflow
44+
public function get(object $subject, string $workflowName = null): WorkflowInterface
4545
{
4646
$matched = [];
4747

0 commit comments

Comments
 (0)