Skip to content

Commit be94e07

Browse files
committed
[Workflow] Add support for executing custom workflow definition validators during the container compilation
1 parent 79fa5f2 commit be94e07

File tree

14 files changed

+246
-21
lines changed

14 files changed

+246
-21
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ CHANGELOG
2525
* Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default
2626
* Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead
2727
* Allow configuring compound rate limiters
28+
* Add support for executing custom workflow definition validators during the
29+
container compilation
2830

2931
7.2
3032
---

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
use Symfony\Component\Validator\Validation;
5252
use Symfony\Component\Webhook\Controller\WebhookController;
5353
use Symfony\Component\WebLink\HttpHeaderSerializer;
54+
use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface;
5455
use Symfony\Component\Workflow\WorkflowEvents;
5556

5657
/**
@@ -402,6 +403,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
402403
->useAttributeAsKey('name')
403404
->prototype('array')
404405
->fixXmlConfig('support')
406+
->fixXmlConfig('definition_validator')
405407
->fixXmlConfig('place')
406408
->fixXmlConfig('transition')
407409
->fixXmlConfig('event_to_dispatch', 'events_to_dispatch')
@@ -436,6 +438,23 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
436438
->end()
437439
->end()
438440
->end()
441+
->arrayNode('definition_validators')
442+
->prototype('scalar')
443+
->cannotBeEmpty()
444+
->validate()
445+
->ifTrue(fn ($v) => !class_exists($v))
446+
->thenInvalid('The validation class %s does not exist.')
447+
->end()
448+
->validate()
449+
->ifTrue(fn ($v) => !is_a($v, DefinitionValidatorInterface::class, true))
450+
->thenInvalid(\sprintf('The validation class %%s is not an instance of %s.', DefinitionValidatorInterface::class))
451+
->end()
452+
->validate()
453+
->ifTrue(fn ($v) => 1 <= (new \ReflectionClass($v))->getConstructor()?->getNumberOfRequiredParameters())
454+
->thenInvalid('The validation class %s constructor must not have any arguments.')
455+
->end()
456+
->end()
457+
->end()
439458
->scalarNode('support_strategy')
440459
->cannotBeEmpty()
441460
->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11171117
}
11181118
}
11191119
$metadataStoreDefinition->replaceArgument(2, $transitionsMetadataDefinition);
1120-
$container->setDefinition(\sprintf('%s.metadata_store', $workflowId), $metadataStoreDefinition);
1120+
$metadataStoreId = \sprintf('%s.metadata_store', $workflowId);
1121+
$container->setDefinition($metadataStoreId, $metadataStoreDefinition);
11211122

11221123
// Create places
11231124
$places = array_column($workflow['places'], 'name');
@@ -1128,7 +1129,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11281129
$definitionDefinition->addArgument($places);
11291130
$definitionDefinition->addArgument($transitions);
11301131
$definitionDefinition->addArgument($initialMarking);
1131-
$definitionDefinition->addArgument(new Reference(\sprintf('%s.metadata_store', $workflowId)));
1132+
$definitionDefinition->addArgument(new Reference($metadataStoreId));
1133+
$definitionDefinitionId = \sprintf('%s.definition', $workflowId);
11321134

11331135
// Create MarkingStore
11341136
$markingStoreDefinition = null;
@@ -1142,14 +1144,26 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11421144
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
11431145
}
11441146

1147+
// Validation
1148+
$workflow['definition_validators'][] = match($workflow['type']) {
1149+
'state_machine' => Workflow\Validator\StateMachineValidator::class,
1150+
'workflow' => Workflow\Validator\WorkflowValidator::class,
1151+
default => throw new \LogicException('Invalid workflow type.'),
1152+
};
1153+
11451154
// Create Workflow
11461155
$workflowDefinition = new ChildDefinition(\sprintf('%s.abstract', $type));
1147-
$workflowDefinition->replaceArgument(0, new Reference(\sprintf('%s.definition', $workflowId)));
1156+
$workflowDefinition->replaceArgument(0, new Reference($definitionDefinitionId));
11481157
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
11491158
$workflowDefinition->replaceArgument(3, $name);
11501159
$workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']);
11511160

1152-
$workflowDefinition->addTag('workflow', ['name' => $name, 'metadata' => $workflow['metadata']]);
1161+
$workflowDefinition->addTag('workflow', [
1162+
'name' => $name,
1163+
'metadata' => $workflow['metadata'],
1164+
'definition_validators' => $workflow['definition_validators'],
1165+
'definition_id' => $definitionDefinitionId,
1166+
]);
11531167
if ('workflow' === $type) {
11541168
$workflowDefinition->addTag('workflow.workflow', ['name' => $name]);
11551169
} elseif ('state_machine' === $type) {
@@ -1158,21 +1172,10 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11581172

11591173
// Store to container
11601174
$container->setDefinition($workflowId, $workflowDefinition);
1161-
$container->setDefinition(\sprintf('%s.definition', $workflowId), $definitionDefinition);
1175+
$container->setDefinition($definitionDefinitionId, $definitionDefinition);
11621176
$container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type);
11631177
$container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name);
11641178

1165-
// Validate Workflow
1166-
if ('state_machine' === $workflow['type']) {
1167-
$validator = new Workflow\Validator\StateMachineValidator();
1168-
} else {
1169-
$validator = new Workflow\Validator\WorkflowValidator();
1170-
}
1171-
1172-
$trs = array_map(fn (Reference $ref): Workflow\Transition => $container->get((string) $ref), $transitions);
1173-
$realDefinition = new Workflow\Definition($places, $trs, $initialMarking);
1174-
$validator->validate($realDefinition, $name);
1175-
11761179
// Add workflow to Registry
11771180
if ($workflow['supports']) {
11781181
foreach ($workflow['supports'] as $supportedClassName) {

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
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);

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 1 addition & 0 deletions
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+
}

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php

Lines changed: 3 additions & 0 deletions
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',

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml

Lines changed: 1 addition & 0 deletions
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" />

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml

Lines changed: 2 additions & 0 deletions
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

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php

Lines changed: 10 additions & 2 deletions
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()

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public function testWorkflowValidationStateMachine()
101101
{
102102
$this->expectException(InvalidDefinitionException::class);
103103
$this->expectExceptionMessage('A transition from a place/state must have an unique name. Multiple transitions named "a_to_b" from place/state "a" were found on StateMachine "article".');
104-
$this->createContainerFromClosure(function ($container) {
104+
$this->createContainerFromClosure(function (ContainerBuilder $container) {
105105
$container->loadFromExtension('framework', [
106106
'annotations' => false,
107107
'http_method_override' => false,
@@ -127,9 +127,57 @@ public function testWorkflowValidationStateMachine()
127127
],
128128
],
129129
]);
130+
$container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass());
131+
});
132+
}
133+
134+
/**
135+
* @dataProvider provideWorkflowValidationCustomTests
136+
*/
137+
public function testWorkflowValidationCustomBroken(string $class, string $message)
138+
{
139+
$this->expectException(InvalidConfigurationException::class);
140+
$this->expectExceptionMessage($message);
141+
$this->createContainerFromClosure(function ($container) use ($class) {
142+
$container->loadFromExtension('framework', [
143+
'annotations' => false,
144+
'http_method_override' => false,
145+
'handle_all_throwables' => true,
146+
'php_errors' => ['log' => true],
147+
'workflows' => [
148+
'article' => [
149+
'type' => 'state_machine',
150+
'supports' => [
151+
__CLASS__,
152+
],
153+
'places' => [
154+
'a',
155+
'b',
156+
],
157+
'transitions' => [
158+
'a_to_b' => [
159+
'from' => ['a'],
160+
'to' => ['b'],
161+
],
162+
],
163+
'definition_validators' => [
164+
$class,
165+
],
166+
],
167+
],
168+
]);
130169
});
131170
}
132171

172+
public static function provideWorkflowValidationCustomTests()
173+
{
174+
yield ['classDoesNotExist', 'Invalid configuration for path "framework.workflows.workflows.article.definition_validators.0": The validation class "classDoesNotExist" does not exist.'];
175+
176+
yield [\DateTime::class, 'Invalid configuration for path "framework.workflows.workflows.article.definition_validators.0": The validation class "DateTime" is not an instance of Symfony\Component\Workflow\Validator\DefinitionValidatorInterface.'];
177+
178+
yield [WorkflowValidatorWithConstructor::class, 'Invalid configuration for path "framework.workflows.workflows.article.definition_validators.0": The validation class "Symfony\\\\Bundle\\\\FrameworkBundle\\\\Tests\\\\DependencyInjection\\\\WorkflowValidatorWithConstructor" constructor must not have any arguments.'];
179+
}
180+
133181
public function testWorkflowDefaultMarkingStoreDefinition()
134182
{
135183
$container = $this->createContainerFromClosure(function ($container) {
@@ -366,3 +414,14 @@ public function testRateLimiterCompoundPolicyInvalidLimiters()
366414
});
367415
}
368416
}
417+
418+
class WorkflowValidatorWithConstructor implements \Symfony\Component\Workflow\Validator\DefinitionValidatorInterface
419+
{
420+
public function __construct(bool $enabled)
421+
{
422+
}
423+
424+
public function validate(\Symfony\Component\Workflow\Definition $definition, string $name): void
425+
{
426+
}
427+
}

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"symfony/json-streamer": "7.3.*",
7474
"symfony/uid": "^6.4|^7.0",
7575
"symfony/web-link": "^6.4|^7.0",
76-
"symfony/webhook": "^7.2",
76+
"symfony/webhook": "^7.3",
7777
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
7878
"twig/twig": "^3.12"
7979
},
@@ -108,7 +108,7 @@
108108
"symfony/validator": "<6.4",
109109
"symfony/web-profiler-bundle": "<6.4",
110110
"symfony/webhook": "<7.2",
111-
"symfony/workflow": "<6.4"
111+
"symfony/workflow": "<7.3"
112112
},
113113
"autoload": {
114114
"psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" },
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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\Exception\LogicException;
17+
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
18+
19+
/**
20+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
21+
*/
22+
class WorkflowValidatorPass implements CompilerPassInterface
23+
{
24+
public function process(ContainerBuilder $container): void
25+
{
26+
foreach ($container->findTaggedServiceIds('workflow') as $id => $attributes) {
27+
foreach ($attributes as $attribute) {
28+
foreach ($attribute['definition_validators'] ?? [] as $validatorClass) {
29+
$realDefinition = $container->get($attribute['definition_id'] ?? throw new \LogicException('The "definition_id" attribute is required.'));
30+
(new $validatorClass())->validate($realDefinition, $attribute['name'] ?? throw new \LogicException('The "name" attribute is required.'));
31+
}
32+
}
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)