8000 [Workflow] Add support for executing custom workflow definition valid… · symfony/symfony@becb816 · GitHub
[go: up one dir, main page]

Skip to content

Commit becb816

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

File tree

14 files changed

+256
-29
lines changed

14 files changed

+256
-29
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ 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+
* Support executing custom workflow validators during container compilation
2829

2930
7.2
3031
---

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

Lines changed: 27 additions & 8 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')
@@ -431,11 +433,28 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
431433
->prototype('scalar')
432434
->cannotBeEmpty()
433435
->validate()
434-
->ifTrue(fn ($v) => !class_exists($v) && !interface_exists($v, false))
436+
->ifTrue(static fn ($v) => !class_exists($v) && !interface_exists($v, false))
435437
->thenInvalid('The supported class or interface "%s" does not exist.')
436438
->end()
437439
->end()
438440
->end()
441+
->arrayNode('definition_validators')
442+
->prototype('scalar')
443+
->cannotBeEmpty()
444+
->validate()
445+
->ifTrue(static fn ($v) => !class_exists($v))
446+
->thenInvalid('The validation class %s does not exist.')
447+
->end()
448+
->validate()
449+
->ifTrue(static 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(static 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()
@@ -447,7 +466,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
447466
->variableNode('events_to_dispatch')
448467
->defaultValue(null)
449468
->validate()
450-
->ifTrue(function ($v) {
469+
->ifTrue(static function ($v) {
451470
if (null === $v) {
452471
return false;
453472
}
@@ -474,14 +493,14 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
474493
->arrayNode('places')
475494
->beforeNormalization()
476495
->always()
477-
->then(function ($places) {
496+
->then(static function ($places) {
478497
if (!\is_array($places)) {
479498
throw new InvalidConfigurationException('The "places" option must be an array in workflow configuration.');
480499
}
481500

482501
// It's an indexed array of shape ['place1', 'place2']
483502
if (isset($places[0]) && \is_string($places[0])) {
484-
return array_map(function (string $place) {
503+
return array_map(static function (string $place) {
485504
return ['name' => $place];
486505
}, $places);
487506
}
@@ -521,7 +540,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
521540
->arrayNode('transitions')
522541
->beforeNormalization()
523542
->always()
524-
->then(function ($transitions) {
543+
->then(static function ($transitions) {
525544
if (!\is_array($transitions)) {
526545
throw new InvalidConfigurationException('The "transitions" option must be an array in workflow configuration.');
527546
}
@@ -588,20 +607,20 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
588607
->end()
589608
->end()
590609
->validate()
591-
->ifTrue(function ($v) {
610+
->ifTrue(static function ($v) {
592611
return $v['supports'] && isset($v['support_strategy']);
593612
})
594613
->thenInvalid('"supports" and "support_strategy" cannot be used together.')
595614
->end()
596615
->validate()
597-
->ifTrue(function ($v) {
616+
->ifTrue(static function ($v) {
598617
return !$v['supports'] && !isset($v['support_strategy']);
599618
})
600619
->thenInvalid('"supports" or "support_strategy" should be configured.')
601620
->end()
602621
->beforeNormalization()
603622
->always()
604-
->then(function ($values) {
623+
->then(static function ($values) {
605624
// Special case to deal with XML when the user wants an empty array
606625
if (\array_key_exists('event_to_dispatch', $values) && null === $values['event_to_dispatch']) {
607626
$values['events_to_dispatch'] = [];

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(\sprintf('Invalid workflow type "%s".', $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 ResettableS 10000 ervicePass(), 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()

0 commit comments

Comments
 (0)
0