Skip to content

Commit

Permalink
[Worflow] Add a TraceableWorkflow
Browse files Browse the repository at this point in the history
  • Loading branch information
lyrixx committed Oct 19, 2023
1 parent 7402279 commit 023078b
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\Registry;
use Symfony\Component\Workflow\DependencyInjection\WorkflowDebugPass;

// Help opcache.preload discover always-needed symbols
class_exists(ApcuAdapter::class);
Expand Down Expand Up @@ -189,6 +190,7 @@ public function build(ContainerBuilder $container)
$container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_BEFORE_REMOVING, -255);
$container->addCompilerPass(new CacheCollectorPass(), PassConfig::TYPE_BEFORE_REMOVING);
$this->addCompilerPassIfExists($container, WorkflowDebugPass::class);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}

{% block toolbar %}
{% if collector.callsCount > 0 %}
{% set icon %}
{{ source('@WebProfiler/Icon/workflow.svg') }}
<span class="sf-toolbar-value">{{ collector.callsCount }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Workflow Calls</b>
<span>{{ collector.callsCount }}</span>
</div>
{% endset %}

{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endif %}
{% endblock %}

{% block menu %}
<span class="label {{ collector.workflows|length == 0 ? 'disabled' }}">
<span class="icon">
Expand Down Expand Up @@ -45,15 +62,67 @@
});
</script>

<h2>Definitions</h2>
<div class="sf-tabs js-tabs">
{% for name, data in collector.workflows %}
<div class="tab">
<h3 class="tab-title">{{ name }}</h3>
<h2 class="tab-title">{{ name }}{% if data.calls|length %} ({{ data.calls|length }}){% endif %}</h2>

<div class="tab-content">
<h3>Definition</h3>
<pre class="sf-mermaid">
{{ data.dump|raw }}
</pre>

<h3>Calls</h3>
<table>
<thead>
<tr>
<th>#</th>
<th>Call</th>
<th>Args</th>
<th>Return</th>
<th>Exception</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{% for call in data.calls %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td>
<code>{{ call.method }}()</code>
{% if call.previousMarking ?? null %}
<hr />
Previous marking:
{{ profiler_dump(call.previousMarking) }}
{% endif %}
</td>
<td>
{{ profiler_dump(call.args) }}
</td>
<td>
{% if call.return is defined %}
{% if call.return is same as true %}
<code>true</code>
{% elseif call.return is same as false %}
<code>false</code>
{% else %}
{{ profiler_dump(call.return) }}
{% endif %}
{% endif %}
</td>
<td>
{% if call.exception is defined %}
{{ profiler_dump(call.exception) }}
{% endif %}
</td>
<td>
{{ call.duration }}ms
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\Workflow\Debug\TraceableWorkflow;
use Symfony\Component\Workflow\Dumper\MermaidDumper;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\TransitionBlocker;

/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
Expand All @@ -34,11 +39,17 @@ public function collect(Request $request, Response $response, \Throwable $except
public function lateCollect(): void
{
foreach ($this->workflows as $workflow) {
$calls = [];
if ($workflow instanceof TraceableWorkflow) {
$calls = $this->cloneVar($workflow->getCalls());
}

// We always use a workflow type because we want to mermaid to
// create a node for transitions
$dumper = new MermaidDumper(MermaidDumper::TRANSITION_TYPE_WORKFLOW);
$this->data['workflows'][$workflow->getName()] = [
'dump' => $dumper->dump($workflow->getDefinition()),
'calls' => $calls,
];
}
}
Expand All @@ -57,4 +68,38 @@ public function getWorkflows(): array
{
return $this->data['workflows'] ?? [];
}

public function getCallsCount(): int
{
$i = 0;
foreach ($this->getWorkflows() as $workflow) {
$i += \count($workflow['calls']);
}

return $i;
}

protected function getCasters(): array
{
$casters = [
...parent::getCasters(),
TransitionBlocker::class => function ($v, array $a, Stub $s, $isNested) {
unset(
$a[sprintf(Caster::PATTERN_PRIVATE, $v::class, 'code')],
$a[sprintf(Caster::PATTERN_PRIVATE, $v::class, 'parameters')],
);

$s->cut += 2;

return $a;
},
Marking::class => function ($v, array $a, Stub $s, $isNested) {
$a[Caster::PREFIX_VIRTUAL.'.places'] = array_keys($v->getPlaces());

return $a;
},
];

return $casters;
}
}
122 changes: 122 additions & 0 deletions src/Symfony/Component/Workflow/Debug/TraceableWorkflow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Workflow\Debug;

use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;
use Symfony\Component\Workflow\TransitionBlockerList;
use Symfony\Component\Workflow\WorkflowInterface;

/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class TraceableWorkflow implements WorkflowInterface
{
private array $calls = [];

public function __construct(
private readonly WorkflowInterface $workflow,
private readonly Stopwatch $stopwatch,
) {
}

public function getMarking(object $subject, array $context = []): Marking
{
return $this->callInner(__FUNCTION__, \func_get_args());
}

public function can(object $subject, string $transitionName): bool
{
return $this->callInner(__FUNCTION__, \func_get_args());
}

public function buildTransitionBlockerList(object $subject, string $transitionName): TransitionBlockerList
{
return $this->callInner(__FUNCTION__, \func_get_args());
}

public function apply(object $subject, string $transitionName, array $context = []): Marking
{
return $this->callInner(__FUNCTION__, \func_get_args());
}

public function getEnabledTransitions(object $subject): array
{
return $this->callInner(__FUNCTION__, \func_get_args());
}

public function getName(): string
{
return $this->workflow->getName();
}

public function getDefinition(): Definition
{
return $this->workflow->getDefinition();
}

public function getMarkingStore(): MarkingStoreInterface
{
return $this->workflow->getMarkingStore();
}

public function getMetadataStore(): MetadataStoreInterface
{
return $this->workflow->getMetadataStore();
}

public function getCalls(): array
{
return $this->calls;
}

private function callInner(string $method, array $args): mixed
{
$sMethod = $this->workflow::class.'::'.$method;
$this->stopwatch->start($sMethod, 'workflow');

$previousMarking = null;
if ('apply' === $method) {
try {
$previousMarking = $this->workflow->getMarking($args[0]);
} catch (\Throwable) {
}
}

try {
$return = $this->workflow->{$method}(...$args);

$this->calls[] = [
'method' => $method,
'duration' => $this->stopwatch->stop($sMethod)->getDuration(),
'args' => $args,
'previousMarking' => $previousMarking ?? null,
'return' => $return,
];

return $return;
} catch (\Throwable $exception) {
$this->calls[] = [
'method' => $method,
'duration' => $this->stopwatch->stop($sMethod)->getDuration(),
'args' => $args,
'previousMarking' => $previousMarking ?? null,
'exception' => $exception,
];

throw $exception;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Workflow\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Workflow\Debug\TraceableWorkflow;

/**
* Adds all configured security voters to the access decision manager.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class WorkflowDebugPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
foreach ($container->findTaggedServiceIds('workflow') as $id => $attributes) {
$container->register("debug.{$id}", TraceableWorkflow::class)
->setDecoratedService($id)
->setArguments([
new Reference("debug.{$id}.inner"),
new Reference('debug.stopwatch'),
]);
}
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/Workflow/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function has(object $subject, string $workflowName = null): bool
return false;
}

public function get(object $subject, string $workflowName = null): Workflow
public function get(object $subject, string $workflowName = null): WorkflowInterface
{
$matched = [];

Expand Down

0 comments on commit 023078b

Please sign in to comment.