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 Aug 1, 2023
1 parent b2a17ea commit b8e51a2
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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\Bundle\FrameworkBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Workflow\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) {

Check failure on line 28 in src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/WorkflowDebugPass.php

View workflow job for this annotation

GitHub Actions / Psalm

UnusedForeachValue

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/WorkflowDebugPass.php:28:73: UnusedForeachValue: $attributes is never referenced or the value is not used (see https://psalm.dev/275)
$container->register("debug.{$id}", TraceableWorkflow::class)
->setDecoratedService($id)
->setArguments([
new Reference("debug.{$id}.inner"),
]);
}
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\WorkflowDebugPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\WorkflowGuardListenerPass;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
Expand Down Expand Up @@ -188,6 +189,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);
$container->addCompilerPass(new WorkflowDebugPass());
}
}

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,65 @@
});
</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 }}</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 class="">
<code>{{ call.method }}()</code>
{% if call.previousMarking is defined %}
<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 class="nowrap">{{ '%0.2f'|format((call.duration) * 1000) }} ms</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
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\Dumper\MermaidDumper;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\StateMachine;
use Symfony\Component\Workflow\TraceableWorkflow;
use Symfony\Component\Workflow\TransitionBlocker;

/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
Expand All @@ -35,10 +40,16 @@ 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());
}

$type = $workflow instanceof StateMachine ? MermaidDumper::TRANSITION_TYPE_STATEMACHINE : MermaidDumper::TRANSITION_TYPE_WORKFLOW;
$dumper = new MermaidDumper($type);
$this->data['workflows'][$workflow->getName()] = [
'dump' => $dumper->dump($workflow->getDefinition()),
'calls' => $calls,
];
}
}
Expand All @@ -57,4 +68,36 @@ 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()
{
$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')],
);

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

return $a;
},
];

return $casters;
}
}
5 changes: 4 additions & 1 deletion src/Symfony/Component/Workflow/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ public function has(object $subject, string $workflowName = null): bool
return false;
}

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

Expand Down
112 changes: 112 additions & 0 deletions src/Symfony/Component/Workflow/TraceableWorkflow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?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;

use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;

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

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

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
{
$startAt = microtime(true);


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

try {
return $return = $this->workflow->{$method}(...$args);
} catch (\Throwable $exception) {
throw $exception;
} finally {
$call = [
'method' => $method,
'duration' => microtime(true) - $startAt,
'args' => $args,
'previousMarking' => $previousMarking ?? null,
];
if (isset($exception)) {
$call['exception'] = $exception;
} elseif (isset($return)) {
$call['return'] = $return;
}

$this->calls[] = $call;
}
}
}

0 comments on commit b8e51a2

Please sign in to comment.