Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b3d4671

Browse filesBrowse files
committed
feature #54276 [Workflow] Add support for executing custom workflow definition validators during the container compilation (lyrixx)
This PR was merged into the 7.3 branch. Discussion ---------- [Workflow] Add support for executing custom workflow definition validators during the container compilation | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | Fix #54034 | License | MIT --- Usage: ```yaml framework: workflows: article: definition_validators: - App\Workflow\Validator\ArticleValidator ``` ```php class ArticleValidator implements DefinitionValidatorInterface { public function validate(Definition $definition, string $name): void { if (!$definition->getMetadataStore()->getMetadata('title')) { throw new InvalidDefinitionException(sprintf('The workflow metadata title is missing in Workflow "%s".', $name)); } } } ``` This code will ensure the title is defined here: ```yaml framework: workflows: article: metadata: # title: Manage article # Commented, so it will throw an exception ``` ![image](https://github.com/user-attachments/assets/949c5c69-a677-4e8d-be10-f974e1574455) Commits ------- 6ab4c7f [Workflow] Add support for executing custom workflow definition validators during the container compilation
2 parents cf554e1 + 6ab4c7f commit b3d4671
Copy full SHA for b3d4671

File tree

Expand file treeCollapse file tree

15 files changed

+256
-29
lines changed
Filter options
Expand file treeCollapse file tree

15 files changed

+256
-29
lines changed

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
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
---

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+27-8Lines changed: 27 additions & 8 deletions
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'] = [];

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+19-16Lines changed: 19 additions & 16 deletions
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) {

‎src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
+2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php
+3Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml
+2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
+10-2Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.