Skip to content

Commit c5c6308

Browse files
lyrixxfabpot
authored andcommitted
[Workflow] Add support for executing custom workflow definition validators during the container compilation
1 parent 133d60a commit c5c6308

File tree

12 files changed

+144
-29
lines changed

12 files changed

+144
-29
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ CHANGELOG
5555
* Allow configuring compound rate limiters
5656
* Make `ValidatorCacheWarmer` use `kernel.build_dir` instead of `cache_dir`
5757
* Make `SerializeCacheWarmer` use `kernel.build_dir` instead of `cache_dir`
58+
* Support executing custom workflow validators during container compilation
5859

5960
7.2
6061
---

DependencyInjection/Configuration.php

+27-8
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
use Symfony\Component\Validator\Validation;
5353
use Symfony\Component\Webhook\Controller\WebhookController;
5454
use Symfony\Component\WebLink\HttpHeaderSerializer;
55+
use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface;
5556
use Symfony\Component\Workflow\WorkflowEvents;
5657

5758
/**
@@ -403,6 +404,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
403404
->useAttributeAsKey('name')
404405
->prototype('array')
405406
->fixXmlConfig('support')
407+
->fixXmlConfig('definition_validator')
406408
->fixXmlConfig('place')
407409
->fixXmlConfig('transition')
408410
->fixXmlConfig('event_to_dispatch', 'events_to_dispatch')
@@ -432,11 +434,28 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
432434
->prototype('scalar')
433435
->cannotBeEmpty()
434436
->validate()
435-
->ifTrue(fn ($v) => !class_exists($v) && !interface_exists($v, false))
437+
->ifTrue(static fn ($v) => !class_exists($v) && !interface_exists($v, false))
436438
->thenInvalid('The supported class or interface "%s" does not exist.')
437439
->end()
438440
->end()
439441
->end()
442+
->arrayNode('definition_validators')
443+
->prototype('scalar')
444+
->cannotBeEmpty()
445+
->validate()
446+
->ifTrue(static fn ($v) => !class_exists($v))
447+
->thenInvalid('The validation class %s does not exist.')
448+
->end()
449+
->validate()
450+
->ifTrue(static fn ($v) => !is_a($v, DefinitionValidatorInterface::class, true))
451+
->thenInvalid(\sprintf('The validation class %%s is not an instance of "%s".', DefinitionValidatorInterface::class))
452+
->end()
453+
->validate()
454+
->ifTrue(static fn ($v) => 1 <= (new \ReflectionClass($v))->getConstructor()?->getNumberOfRequiredParameters())
455+
->thenInvalid('The %s validation class constructor must not have any arguments.')
456+
->end()
457+
->end()
458+
->end()
440459
->scalarNode('support_strategy')
441460
->cannotBeEmpty()
442461
->end()
@@ -448,7 +467,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
448467
->variableNode('events_to_dispatch')
449468
->defaultValue(null)
450469
->validate()
451-
->ifTrue(function ($v) {
470+
->ifTrue(static function ($v) {
452471
if (null === $v) {
453472
return false;
454473
}
@@ -475,14 +494,14 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
475494
->arrayNode('places')
476495
->beforeNormalization()
477496
->always()
478-
->then(function ($places) {
497+
->then(static function ($places) {
479498
if (!\is_array($places)) {
480499
throw new InvalidConfigurationException('The "places" option must be an array in workflow configuration.');
481500
}
482501

483502
// It's an indexed array of shape ['place1', 'place2']
484503
if (isset($places[0]) && \is_string($places[0])) {
485-
return array_map(function (string $place) {
504+
return array_map(static function (string $place) {
486505
return ['name' => $place];
487506
}, $places);
488507
}
@@ -522,7 +541,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
522541
->arrayNode('transitions')
523542
->beforeNormalization()
524543
->always()
525-
->then(function ($transitions) {
544+
->then(static function ($transitions) {
526545
if (!\is_array($transitions)) {
527546
throw new InvalidConfigurationException('The "transitions" option must be an array in workflow configuration.');
528547
}
@@ -589,20 +608,20 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
589608
->end()
590609
->end()
591610
->validate()
592-
->ifTrue(function ($v) {
611+
->ifTrue(static function ($v) {
593612
return $v['supports'] && isset($v['support_strategy']);
594613
})
595614
->thenInvalid('"supports" and "support_strategy" cannot be used together.')
596615
->end()
597616
->validate()
598-
->ifTrue(function ($v) {
617+
->ifTrue(static function ($v) {
599618
return !$v['supports'] && !isset($v['support_strategy']);
600619
})
601620
->thenInvalid('"supports" or "support_strategy" should be configured.')
602621
->end()
603622
->beforeNormalization()
604623
->always()
605-
->then(function ($values) {
624+
->then(static function ($values) {
606625
// Special case to deal with XML when the user wants an empty array
607626
if (\array_key_exists('event_to_dispatch', $values) && null === $values['event_to_dispatch']) {
608627
$values['events_to_dispatch'] = [];

DependencyInjection/FrameworkExtension.php

+19-16
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11231123
}
11241124
}
11251125
$metadataStoreDefinition->replaceArgument(2, $transitionsMetadataDefinition);
1126-
$container->setDefinition(\sprintf('%s.metadata_store', $workflowId), $metadataStoreDefinition);
1126+
$metadataStoreId = \sprintf('%s.metadata_store', $workflowId);
1127+
$container->setDefinition($metadataStoreId, $metadataStoreDefinition);
11271128

11281129
// Create places
11291130
$places = array_column($workflow['places'], 'name');
@@ -1134,7 +1135,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11341135
$definitionDefinition->addArgument($places);
11351136
$definitionDefinition->addArgument($transitions);
11361137
$definitionDefinition->addArgument($initialMarking);
1137-
$definitionDefinition->addArgument(new Reference(\sprintf('%s.metadata_store', $workflowId)));
1138+
$definitionDefinition->addArgument(new Reference($metadataStoreId));
1139+
$definitionDefinitionId = \sprintf('%s.definition', $workflowId);
11381140

11391141
// Create MarkingStore
11401142
$markingStoreDefinition = null;
@@ -1148,14 +1150,26 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11481150
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
11491151
}
11501152

1153+
// Validation
1154+
$workflow['definition_validators'][] = match ($workflow['type']) {
1155+
'state_machine' => Workflow\Validator\StateMachineValidator::class,
1156+
'workflow' => Workflow\Validator\WorkflowValidator::class,
1157+
default => throw new \LogicException(\sprintf('Invalid workflow type "%s".', $workflow['type'])),
1158+
};
1159+
11511160
// Create Workflow
11521161
$workflowDefinition = new ChildDefinition(\sprintf('%s.abstract', $type));
1153-
$workflowDefinition->replaceArgument(0, new Reference(\sprintf('%s.definition', $workflowId)));
1162+
$workflowDefinition->replaceArgument(0, new Reference($definitionDefinitionId));
11541163
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
11551164
$workflowDefinition->replaceArgument(3, $name);
11561165
$workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']);
11571166

1158-
$workflowDefinition->addTag('workflow', ['name' => $name, 'metadata' => $workflow['metadata']]);
1167+
$workflowDefinition->addTag('workflow', [
1168+
'name' => $name,
1169+
'metadata' => $workflow['metadata'],
1170+
'definition_validators' => $workflow['definition_validators'],
1171+
'definition_id' => $definitionDefinitionId,
1172+
]);
11591173
if ('workflow' === $type) {
11601174
$workflowDefinition->addTag('workflow.workflow', ['name' => $name]);
11611175
} elseif ('state_machine' === $type) {
@@ -1164,21 +1178,10 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11641178

11651179
// Store to container
11661180
$container->setDefinition($workflowId, $workflowDefinition);
1167-
$container->setDefinition(\sprintf('%s.definition', $workflowId), $definitionDefinition);
1181+
$container->setDefinition($definitionDefinitionId, $definitionDefinition);
11681182
$container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type);
11691183
$container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name);
11701184

1171-
// Validate Workflow
1172-
if ('state_machine' === $workflow['type']) {
1173-
$validator = new Workflow\Validator\StateMachineValidator();
1174-
} else {
1175-
$validator = new Workflow\Validator\WorkflowValidator();
1176-
}
1177-
1178-
$trs = array_map(fn (Reference $ref): Workflow\Transition => $container->get((string) $ref), $transitions);
1179-
$realDefinition = new Workflow\Definition($places, $trs, $initialMarking);
1180-
$validator->validate($realDefinition, $name);
1181-
11821185
// Add workflow to Registry
11831186
if ($workflow['supports']) {
11841187
foreach ($workflow['supports'] as $supportedClassName) {

FrameworkBundle.php

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
use Symfony\Component\VarExporter\Internal\Registry;
7878
use Symfony\Component\Workflow\DependencyInjection\WorkflowDebugPass;
7979
use Symfony\Component\Workflow\DependencyInjection\WorkflowGuardListenerPass;
80+
use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass;
8081

8182
// Help opcache.preload discover always-needed symbols
8283
class_exists(ApcuAdapter::class);
@@ -173,6 +174,7 @@ public function build(ContainerBuilder $container): void
173174
$container->addCompilerPass(new CachePoolPrunerPass(), PassConfig::TYPE_AFTER_REMOVING);
174175
$this->addCompilerPassIfExists($container, FormPass::class);
175176
$this->addCompilerPassIfExists($container, WorkflowGuardListenerPass::class);
177+
$this->addCompilerPassIfExists($container, WorkflowValidatorPass::class);
176178
$container->addCompilerPass(new ResettableServicePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
177179
$container->addCompilerPass(new RegisterLocaleAwareServicesPass());
178180
$container->addCompilerPass(new TestServiceContainerWeakRefPass(), PassConfig::TYPE_BEFORE_REMOVING, -32);

Resources/config/schema/symfony-1.0.xsd

+1
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
<xsd:element name="initial-marking" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
450450
<xsd:element name="marking-store" type="marking_store" minOccurs="0" maxOccurs="1" />
451451
<xsd:element name="support" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
452+
<xsd:element name="definition-validator" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
452453
<xsd:element name="event-to-dispatch" type="event_to_dispatch" minOccurs="0" maxOccurs="unbounded" />
453454
<xsd:element name="place" type="place" minOccurs="0" maxOccurs="unbounded" />
454455
<xsd:element name="transition" type="transition" minOccurs="0" maxOccurs="unbounded" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator;
4+
5+
use Symfony\Component\Workflow\Definition;
6+
use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface;
7+
8+
class DefinitionValidator implements DefinitionValidatorInterface
9+
{
10+
public static bool $called = false;
11+
12+
public function validate(Definition $definition, string $name): void
13+
{
14+
self::$called = true;
15+
}
16+
}

Tests/DependencyInjection/Fixtures/php/workflows.php

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
'supports' => [
1414
FrameworkExtensionTestCase::class,
1515
],
16+
'definition_validators' => [
17+
Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator::class,
18+
],
1619
'initial_marking' => ['draft'],
1720
'metadata' => [
1821
'title' => 'article workflow',

Tests/DependencyInjection/Fixtures/xml/workflows.xml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<framework:audit-trail enabled="true"/>
1414
<framework:initial-marking>draft</framework:initial-marking>
1515
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTestCase</framework:support>
16+
<framework:definition-validator>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator</framework:definition-validator>
1617
<framework:place name="draft" />
1718
<framework:place name="wait_for_journalist" />
1819
<framework:place name="approved_by_journalist" />

Tests/DependencyInjection/Fixtures/yml/workflows.yml

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ framework:
99
type: workflow
1010
supports:
1111
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTestCase
12+
definition_validators:
13+
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator
1214
initial_marking: [draft]
1315
metadata:
1416
title: article workflow

Tests/DependencyInjection/FrameworkExtensionTestCase.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Psr\Log\LoggerAwareInterface;
1616
use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension;
1717
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
18+
use Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator;
1819
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage;
1920
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
2021
use Symfony\Bundle\FullStack;
@@ -287,7 +288,11 @@ public function testProfilerCollectSerializerDataEnabled()
287288

288289
public function testWorkflows()
289290
{
290-
$container = $this->createContainerFromFile('workflows');
291+
DefinitionValidator::$called = false;
292+
293+
$container = $this->createContainerFromFile('workflows', compile: false);
294+
$container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass());
295+
$container->compile();
291296

292297
$this->assertTrue($container->hasDefinition('workflow.article'), 'Workflow is registered as a service');
293298
$this->assertSame('workflow.abstract', $container->getDefinition('workflow.article')->getParent());
@@ -310,6 +315,7 @@ public function testWorkflows()
310315
], $tags['workflow'][0]['metadata'] ?? null);
311316

312317
$this->assertTrue($container->hasDefinition('workflow.article.definition'), 'Workflow definition is registered as a service');
318+
$this->assertTrue(DefinitionValidator::$called, 'DefinitionValidator is called');
313319

314320
$workflowDefinition = $container->getDefinition('workflow.article.definition');
315321

@@ -403,7 +409,9 @@ public function testWorkflowAreValidated()
403409
{
404410
$this->expectException(InvalidDefinitionException::class);
405411
$this->expectExceptionMessage('A transition from a place/state must have an unique name. Multiple transitions named "go" from place/state "first" were found on StateMachine "my_workflow".');
406-
$this->createContainerFromFile('workflow_not_valid');
412+
$container = $this->createContainerFromFile('workflow_not_valid', compile: false);
413+
$container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass());
414+
$container->compile();
407415
}
408416

409417
public function testWorkflowCannotHaveBothSupportsAndSupportStrategy()

0 commit comments

Comments
 (0)