From bd1f2c858385bca965cb6e8d32b1a21085029981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 8 Feb 2018 18:13:25 +0100 Subject: [PATCH] [Workflow] Add a MetadataStore --- UPGRADE-4.1.md | 1 + src/Symfony/Bridge/Twig/CHANGELOG.md | 5 ++ .../Twig/Extension/WorkflowExtension.php | 19 ++++ .../Tests/Extension/WorkflowExtensionTest.php | 32 ++++++- .../DependencyInjection/Configuration.php | 63 ++++++++++++-- .../FrameworkExtension.php | 48 +++++++++-- .../Resources/config/schema/symfony-1.0.xsd | 17 +++- .../Fixtures/php/workflows.php | 27 ++++-- .../workflow_with_arguments_and_service.xml | 4 +- ...th_multiple_transitions_with_same_name.xml | 12 +-- ...flow_with_support_and_support_strategy.xml | 4 +- .../xml/workflow_with_type_and_service.xml | 4 +- ...w_without_support_and_support_strategy.xml | 4 +- .../Fixtures/xml/workflows.xml | 35 +++++--- .../Fixtures/yml/workflows.yml | 24 ++++-- .../FrameworkExtensionTest.php | 30 ++++++- src/Symfony/Component/Workflow/CHANGELOG.md | 1 + src/Symfony/Component/Workflow/Definition.php | 12 ++- .../Component/Workflow/Event/Event.php | 48 ++++++++++- .../Workflow/Metadata/GetMetadataTrait.php | 48 +++++++++++ .../Metadata/InMemoryMetadataStore.php | 48 +++++++++++ .../Metadata/MetadataStoreInterface.php | 39 +++++++++ .../Tests/EventListener/GuardListenerTest.php | 5 +- .../Metadata/InMemoryMetadataStoreTest.php | 86 +++++++++++++++++++ src/Symfony/Component/Workflow/Workflow.php | 23 +++-- .../Component/Workflow/WorkflowInterface.php | 3 + 26 files changed, 569 insertions(+), 73 deletions(-) create mode 100644 src/Symfony/Component/Workflow/Metadata/GetMetadataTrait.php create mode 100644 src/Symfony/Component/Workflow/Metadata/InMemoryMetadataStore.php create mode 100644 src/Symfony/Component/Workflow/Metadata/MetadataStoreInterface.php create mode 100644 src/Symfony/Component/Workflow/Tests/Metadata/InMemoryMetadataStoreTest.php diff --git a/UPGRADE-4.1.md b/UPGRADE-4.1.md index 32c16dd9ccf9..4de5c58cfc23 100644 --- a/UPGRADE-4.1.md +++ b/UPGRADE-4.1.md @@ -104,3 +104,4 @@ Workflow * Deprecated the `add` method in favor of the `addWorkflow` method in `Workflow\Registry`. * Deprecated `SupportStrategyInterface` in favor of `WorkflowSupportStrategyInterface`. * Deprecated the class `ClassInstanceSupportStrategy` in favor of the class `InstanceOfSupportStrategy`. + * Deprecated passing the workflow name as 4th parameter of `Event` constructor in favor of the workflow itself. diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 8d5d6f56b677..fcdb5e275608 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.1.0 +----- + + * add a `workflow_metadata` function + 3.4.0 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php index 54c12f16d4cb..02d2f6aefc67 100644 --- a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php @@ -37,6 +37,7 @@ public function getFunctions() new TwigFunction('workflow_transitions', array($this, 'getEnabledTransitions')), new TwigFunction('workflow_has_marked_place', array($this, 'hasMarkedPlace')), new TwigFunction('workflow_marked_places', array($this, 'getMarkedPlaces')), + new TwigFunction('workflow_metadata', array($this, 'getMetadata')), ); } @@ -101,6 +102,24 @@ public function getMarkedPlaces($subject, $placesNameOnly = true, $name = null) return $places; } + /** + * Returns the metadata for a specific subject. + * + * @param object $subject A subject + * @param null|string|Transition $metadataSubject Use null to get workflow metadata + * Use a string (the place name) to get place metadata + * Use a Transition instance to get transition metadata + */ + public function getMetadata($subject, string $key, $metadataSubject = null, string $name = null): ?string + { + return $this + ->workflowRegistry + ->get($subject, $name) + ->getMetadataStore() + ->getMetadata($key, $metadataSubject) + ; + } + public function getName() { return 'workflow'; diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php index af675d34ffb5..aa0c2d49e5d3 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\WorkflowExtension; use Symfony\Component\Workflow\Definition; +use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\SupportStrategy\ClassInstanceSupportStrategy; use Symfony\Component\Workflow\SupportStrategy\InstanceOfSupportStrategy; @@ -23,6 +24,7 @@ class WorkflowExtensionTest extends TestCase { private $extension; + private $t1; protected function setUp() { @@ -32,10 +34,21 @@ protected function setUp() $places = array('ordered', 'waiting_for_payment', 'processed'); $transitions = array( - new Transition('t1', 'ordered', 'waiting_for_payment'), + $this->t1 = new Transition('t1', 'ordered', 'waiting_for_payment'), new Transition('t2', 'waiting_for_payment', 'processed'), ); - $definition = new Definition($places, $transitions); + + $metadataStore = null; + if (class_exists(InMemoryMetadataStore::class)) { + $transitionsMetadata = new \SplObjectStorage(); + $transitionsMetadata->attach($this->t1, array('title' => 't1 title')); + $metadataStore = new InMemoryMetadataStore( + array('title' => 'workflow title'), + array('orderer' => array('title' => 'ordered title')), + $transitionsMetadata + ); + } + $definition = new Definition($places, $transitions, null, $metadataStore); $workflow = new Workflow($definition); $registry = new Registry(); @@ -88,4 +101,19 @@ public function testGetMarkedPlaces() $this->assertSame(array('ordered', 'waiting_for_payment'), $this->extension->getMarkedPlaces($subject)); $this->assertSame($subject->marking, $this->extension->getMarkedPlaces($subject, false)); } + + public function testGetMetadata() + { + if (!class_exists(InMemoryMetadataStore::class)) { + $this->markTestSkipped('This test requires symfony/workflow:4.1.'); + } + $subject = new \stdClass(); + $subject->marking = array(); + + $this->assertSame('workflow title', $this->extension->getMetadata($subject, 'title')); + $this->assertSame('ordered title', $this->extension->getMetadata($subject, 'title', 'orderer')); + $this->assertSame('t1 title', $this->extension->getMetadata($subject, 'title', $this->t1)); + $this->assertNull($this->extension->getMetadata($subject, 'not found')); + $this->assertNull($this->extension->getMetadata($subject, 'not found', $this->t1)); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 4eea195b99d2..eb3f48350dd4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -31,6 +31,7 @@ * FrameworkExtension configuration structure. * * @author Jeremy Mikola + * @author Grégoire Pineau */ class Configuration implements ConfigurationInterface { @@ -292,23 +293,61 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->defaultNull() ->end() ->arrayNode('places') + ->beforeNormalization() + ->always() + ->then(function ($places) { + // It's an indexed array of shape ['place1', 'place2'] + if (isset($places[0]) && is_string($places[0])) { + return array_map(function (string $place) { + return array('name' => $place); + }, $places); + } + + // It's an indexed array, we let the validation occur + if (isset($places[0]) && is_array($places[0])) { + return $places; + } + + foreach ($places as $name => $place) { + if (is_array($place) && array_key_exists('name', $place)) { + continue; + } + $place['name'] = $name; + $places[$name] = $place; + } + + return array_values($places); + }) + ->end() ->isRequired() ->requiresAtLeastOneElement() - ->prototype('scalar') - ->cannotBeEmpty() + ->prototype('array') + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->arrayNode('metadata') + ->normalizeKeys(false) + ->defaultValue(array()) + ->example(array('color' => 'blue', 'description' => 'Workflow to manage article.')) + ->prototype('variable') + ->end() + ->end() + ->end() ->end() ->end() ->arrayNode('transitions') ->beforeNormalization() ->always() ->then(function ($transitions) { - // It's an indexed array, we let the validation occurs - if (isset($transitions[0])) { + // It's an indexed array, we let the validation occur + if (isset($transitions[0]) && is_array($transitions[0])) { return $transitions; } foreach ($transitions as $name => $transition) { - if (array_key_exists('name', $transition)) { + if (is_array($transition) && array_key_exists('name', $transition)) { continue; } $transition['name'] = $name; @@ -351,9 +390,23 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->cannotBeEmpty() ->end() ->end() + ->arrayNode('metadata') + ->normalizeKeys(false) + ->defaultValue(array()) + ->example(array('color' => 'blue', 'description' => 'Workflow to manage article.')) + ->prototype('variable') + ->end() + ->end() ->end() ->end() ->end() + ->arrayNode('metadata') + ->normalizeKeys(false) + ->defaultValue(array()) + ->example(array('color' => 'blue', 'description' => 'Workflow to manage article.')) + ->prototype('variable') + ->end() + ->end() ->end() ->validate() ->ifTrue(function ($v) { diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 8fbc4c276100..b36b17977567 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -466,32 +466,68 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ foreach ($config['workflows'] as $name => $workflow) { $type = $workflow['type']; + // Process Metadata (workflow + places (transition is done in the "create transition" block)) + $metadataStoreDefinition = new Definition(Workflow\Metadata\InMemoryMetadataStore::class, array(null, null, null)); + if ($workflow['metadata']) { + $metadataStoreDefinition->replaceArgument(0, $workflow['metadata']); + } + $placesMetadata = array(); + foreach ($workflow['places'] as $place) { + if ($place['metadata']) { + $placesMetadata[$place['name']] = $place['metadata']; + } + } + if ($placesMetadata) { + $metadataStoreDefinition->replaceArgument(1, $placesMetadata); + } + + // Create transitions $transitions = array(); + $transitionsMetadataDefinition = new Definition(\SplObjectStorage::class); foreach ($workflow['transitions'] as $transition) { if ('workflow' === $type) { - $transitions[] = new Definition(Workflow\Transition::class, array($transition['name'], $transition['from'], $transition['to'])); + $transitionDefinition = new Definition(Workflow\Transition::class, array($transition['name'], $transition['from'], $transition['to'])); + $transitions[] = $transitionDefinition; + if ($transition['metadata']) { + $transitionsMetadataDefinition->addMethodCall('attach', array( + $transitionDefinition, + $transition['metadata'], + )); + } } elseif ('state_machine' === $type) { foreach ($transition['from'] as $from) { foreach ($transition['to'] as $to) { - $transitions[] = new Definition(Workflow\Transition::class, array($transition['name'], $from, $to)); + $transitionDefinition = new Definition(Workflow\Transition::class, array($transition['name'], $from, $to)); + $transitions[] = $transitionDefinition; + if ($transition['metadata']) { + $transitionsMetadataDefinition->addMethodCall('attach', array( + $transitionDefinition, + $transition['metadata'], + )); + } } } } } + $metadataStoreDefinition->replaceArgument(2, $transitionsMetadataDefinition); + + // Create places + $places = array_map(function (array $place) { + return $place['name']; + }, $workflow['places']); // Create a Definition $definitionDefinition = new Definition(Workflow\Definition::class); $definitionDefinition->setPublic(false); - $definitionDefinition->addArgument($workflow['places']); + $definitionDefinition->addArgument($places); $definitionDefinition->addArgument($transitions); + $definitionDefinition->addArgument($workflow['initial_place'] ?? null); + $definitionDefinition->addArgument($metadataStoreDefinition); $definitionDefinition->addTag('workflow.definition', array( 'name' => $name, 'type' => $type, 'marking_store' => isset($workflow['marking_store']['type']) ? $workflow['marking_store']['type'] : null, )); - if (isset($workflow['initial_place'])) { - $definitionDefinition->addArgument($workflow['initial_place']); - } // Create MarkingStore if (isset($workflow['marking_store']['type'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 2b65d09f52da..d8ef61bbb456 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -273,8 +273,9 @@ - + + @@ -302,10 +303,24 @@ + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php index 3d3f4266b7eb..e8a54059d43c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php @@ -48,18 +48,29 @@ FrameworkExtensionTest::class, ), 'initial_place' => 'start', + 'metadata' => array( + 'title' => 'workflow title', + ), 'places' => array( - 'start', - 'coding', - 'travis', - 'review', - 'merged', - 'closed', + 'start_name_not_used' => array( + 'name' => 'start', + 'metadata' => array( + 'title' => 'place start title', + ), + ), + 'coding' => null, + 'travis' => null, + 'review' => null, + 'merged' => null, + 'closed' => null, ), 'transitions' => array( 'submit' => array( 'from' => 'start', 'to' => 'travis', + 'metadata' => array( + 'title' => 'transition submit title', + ), ), 'update' => array( 'from' => array('coding', 'travis', 'review'), @@ -96,8 +107,8 @@ FrameworkExtensionTest::class, ), 'places' => array( - 'first', - 'last', + array('name' => 'first'), + array('name' => 'last'), ), 'transitions' => array( 'go' => array( diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_arguments_and_service.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_arguments_and_service.xml index 02502296f77d..51023de59dab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_arguments_and_service.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_arguments_and_service.xml @@ -13,8 +13,8 @@ a Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - first - last + + a a diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml index d52aed8c9523..7375e7429a2a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml @@ -13,12 +13,12 @@ a Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - draft - wait_for_journalist - approved_by_journalist - wait_for_spellchecker - approved_by_spellchecker - published + + + + + + draft wait_for_journalist diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_support_and_support_strategy.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_support_and_support_strategy.xml index 92e26ff327d9..b640c929ecf5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_support_and_support_strategy.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_support_and_support_strategy.xml @@ -10,8 +10,8 @@ Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - first - last + + a a diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_type_and_service.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_type_and_service.xml index 7ec450f6537e..b6ae96ca2399 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_type_and_service.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_type_and_service.xml @@ -10,8 +10,8 @@ Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - first - last + + a a diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_without_support_and_support_strategy.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_without_support_and_support_strategy.xml index 14bb287cca48..fb65b0b01855 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_without_support_and_support_strategy.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_without_support_and_support_strategy.xml @@ -9,8 +9,8 @@ - first - last + + a a diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml index 02f964bc3a43..d6a1b78efb5b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml @@ -13,12 +13,12 @@ a Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - draft - wait_for_journalist - approved_by_journalist - wait_for_spellchecker - approved_by_spellchecker - published + + + + + + draft wait_for_journalist @@ -42,15 +42,22 @@ Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - start - coding - travis - review - merged - closed + + + place start title + + + + + + + start travis + + transition submit title + coding @@ -78,11 +85,15 @@ closed review + + workflow title + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + first last diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml index 8efb803c12ad..c82c92791c86 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml @@ -8,6 +8,7 @@ framework: - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest initial_place: draft places: + # simple format - draft - wait_for_journalist - approved_by_journalist @@ -33,17 +34,24 @@ framework: supports: - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest initial_place: start + metadata: + title: workflow title places: - - start - - coding - - travis - - review - - merged - - closed + start_name_not_used: + name: start + metadata: + title: place start title + coding: ~ + travis: ~ + review: ~ + merged: ~ + closed: ~ transitions: submit: from: start to: travis + metadata: + title: transition submit title update: from: [coding, travis, review] to: travis @@ -69,8 +77,8 @@ framework: supports: - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest places: - - first - - last + - { name: first } + - { name: last } transitions: go: from: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index ffcaeeb94a30..d49111c65ced 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -43,7 +43,7 @@ use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; -use Symfony\Component\Workflow\Registry; +use Symfony\Component\Workflow; abstract class FrameworkExtensionTest extends TestCase { @@ -209,12 +209,12 @@ public function testWorkflows() 'Places are passed to the workflow definition' ); $this->assertSame(array('workflow.definition' => array(array('name' => 'article', 'type' => 'workflow', 'marking_store' => 'multiple_state'))), $workflowDefinition->getTags()); + $this->assertCount(4, $workflowDefinition->getArgument(1)); + $this->assertSame('draft', $workflowDefinition->getArgument(2)); $this->assertTrue($container->hasDefinition('state_machine.pull_request'), 'State machine is registered as a service'); $this->assertSame('state_machine.abstract', $container->getDefinition('state_machine.pull_request')->getParent()); $this->assertTrue($container->hasDefinition('state_machine.pull_request.definition'), 'State machine definition is registered as a service'); - $this->assertCount(4, $workflowDefinition->getArgument(1)); - $this->assertSame('draft', $workflowDefinition->getArgument(2)); $stateMachineDefinition = $container->getDefinition('state_machine.pull_request.definition'); @@ -234,6 +234,28 @@ public function testWorkflows() $this->assertCount(9, $stateMachineDefinition->getArgument(1)); $this->assertSame('start', $stateMachineDefinition->getArgument(2)); + $metadataStoreDefinition = $stateMachineDefinition->getArgument(3); + $this->assertInstanceOf(Definition::class, $metadataStoreDefinition); + $this->assertSame(Workflow\Metadata\InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); + + $workflowMetadata = $metadataStoreDefinition->getArgument(0); + $this->assertSame(array('title' => 'workflow title'), $workflowMetadata); + + $placesMetadata = $metadataStoreDefinition->getArgument(1); + $this->assertArrayHasKey('start', $placesMetadata); + $this->assertSame(array('title' => 'place start title'), $placesMetadata['start']); + + $transitionsMetadata = $metadataStoreDefinition->getArgument(2); + $this->assertSame(\SplObjectStorage::class, $transitionsMetadata->getClass()); + $transitionsMetadataCall = $transitionsMetadata->getMethodCalls()[0]; + $this->assertSame('attach', $transitionsMetadataCall[0]); + $params = $transitionsMetadataCall[1]; + $this->assertCount(2, $params); + $this->assertInstanceOf(Definition::class, $params[0]); + $this->assertSame(Workflow\Transition::class, $params[0]->getClass()); + $this->assertSame(array('submit', 'start', 'travis'), $params[0]->getArguments()); + $this->assertSame(array('title' => 'transition submit title'), $params[1]); + $serviceMarkingStoreWorkflowDefinition = $container->getDefinition('workflow.service_marking_store_workflow'); /** @var Reference $markingStoreRef */ $markingStoreRef = $serviceMarkingStoreWorkflowDefinition->getArgument(1); @@ -308,7 +330,7 @@ public function testWorkflowServicesCanBeEnabled() { $container = $this->createContainerFromFile('workflows_enabled'); - $this->assertTrue($container->has(Registry::class)); + $this->assertTrue($container->has(Workflow\Registry::class)); $this->assertTrue($container->hasDefinition('console.command.workflow_dump')); } diff --git a/src/Symfony/Component/Workflow/CHANGELOG.md b/src/Symfony/Component/Workflow/CHANGELOG.md index 79996f4626d2..4908db1b412b 100644 --- a/src/Symfony/Component/Workflow/CHANGELOG.md +++ b/src/Symfony/Component/Workflow/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Deprecated the class `ClassInstanceSupportStrategy` in favor of the class `InstanceOfSupportStrategy`. * Added TransitionBlockers as a way to pass around reasons why exactly transitions can't be made. + * Added a `MetadataStore`. 4.0.0 ----- diff --git a/src/Symfony/Component/Workflow/Definition.php b/src/Symfony/Component/Workflow/Definition.php index 98536ddf8fed..9e9e1e796fcc 100644 --- a/src/Symfony/Component/Workflow/Definition.php +++ b/src/Symfony/Component/Workflow/Definition.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Workflow; use Symfony\Component\Workflow\Exception\LogicException; +use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; +use Symfony\Component\Workflow\Metadata\MetadataStoreInterface; /** * @author Fabien Potencier @@ -23,13 +25,14 @@ final class Definition private $places = array(); private $transitions = array(); private $initialPlace; + private $metadataStore; /** * @param string[] $places * @param Transition[] $transitions * @param string|null $initialPlace */ - public function __construct(array $places, array $transitions, string $initialPlace = null) + public function __construct(array $places, array $transitions, string $initialPlace = null, MetadataStoreInterface $metadataStore = null) { foreach ($places as $place) { $this->addPlace($place); @@ -40,6 +43,8 @@ public function __construct(array $places, array $transitions, string $initialPl } $this->setInitialPlace($initialPlace); + + $this->metadataStore = $metadataStore ?: new InMemoryMetadataStore(); } /** @@ -66,6 +71,11 @@ public function getTransitions(): array return $this->transitions; } + public function getMetadataStore(): MetadataStoreInterface + { + return $this->metadataStore; + } + private function setInitialPlace(string $place = null) { if (null === $place) { diff --git a/src/Symfony/Component/Workflow/Event/Event.php b/src/Symfony/Component/Workflow/Event/Event.php index 19c78d47082d..943a4da5a681 100644 --- a/src/Symfony/Component/Workflow/Event/Event.php +++ b/src/Symfony/Component/Workflow/Event/Event.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Workflow\Event; use Symfony\Component\EventDispatcher\Event as BaseEvent; +use Symfony\Component\Workflow\Exception\InvalidArgumentException; use Symfony\Component\Workflow\Marking; use Symfony\Component\Workflow\Transition; +use Symfony\Component\Workflow\WorkflowInterface; /** * @author Fabien Potencier @@ -24,20 +26,28 @@ class Event extends BaseEvent private $subject; private $marking; private $transition; + private $workflow; private $workflowName; /** * @param object $subject * @param Marking $marking * @param Transition $transition - * @param string $workflowName + * @param Workflow $workflow */ - public function __construct($subject, Marking $marking, Transition $transition, string $workflowName = 'unnamed') + public function __construct($subject, Marking $marking, Transition $transition, $workflow = null) { $this->subject = $subject; $this->marking = $marking; $this->transition = $transition; - $this->workflowName = $workflowName; + if (is_string($workflow)) { + @trigger_error(sprintf('Passing a string as 4th parameter of "%s" is deprecated since Symfony 4.1. Pass a %s instance instead.', __METHOD__, WorkflowInterface::class), E_USER_DEPRECATED); + $this->workflowName = $workflow; + } elseif ($workflow instanceof WorkflowInterface) { + $this->workflow = $workflow; + } else { + throw new InvalidArgumentException(sprintf('The 4th parameter of "%s" should be a "%s" instance instead.', __METHOD__, WorkflowInterface::class)); + } } public function getMarking() @@ -55,8 +65,38 @@ public function getTransition() return $this->transition; } + public function getWorkflow(): WorkflowInterface + { + // BC layer + if (!$this->workflow instanceof WorkflowInterface) { + throw new \RuntimeException(sprintf('The 4th parameter of "%s"::__construct() should be a "%s" instance.', __CLASS__, WorkflowInterface::class)); + } + + return $this->workflow; + } + public function getWorkflowName() { - return $this->workflowName; + // BC layer + if ($this->workflowName) { + return $this->workflowName; + } + + // BC layer + if (!$this->workflow instanceof WorkflowInterface) { + throw new \RuntimeException(sprintf('The 4th parameter of "%s"::__construct() should be a "%s" instance.', __CLASS__, WorkflowInterface::class)); + } + + return $this->workflow->getName(); + } + + public function getMetadata(string $key, $subject) + { + // BC layer + if (!$this->workflow instanceof WorkflowInterface) { + throw new \RuntimeException(sprintf('The 4th parameter of "%s"::__construct() should be a "%s" instance.', __CLASS__, WorkflowInterface::class)); + } + + return $this->workflow->getMetadataStore()->getMetadata($key, $subject); } } diff --git a/src/Symfony/Component/Workflow/Metadata/GetMetadataTrait.php b/src/Symfony/Component/Workflow/Metadata/GetMetadataTrait.php new file mode 100644 index 000000000000..19c5d6bf9dd6 --- /dev/null +++ b/src/Symfony/Component/Workflow/Metadata/GetMetadataTrait.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Metadata; + +use Symfony\Component\Workflow\Exception\InvalidArgumentException; +use Symfony\Component\Workflow\Transition; + +/** + * @author Grégoire Pineau + */ +trait GetMetadataTrait +{ + public function getMetadata(string $key, $subject = null) + { + if (null === $subject) { + return $this->getWorkflowMetadata()[$key] ?? null; + } + + if (\is_string($subject)) { + $metadataBag = $this->getPlaceMetadata($subject); + if (!$metadataBag) { + return null; + } + + return $metadataBag[$key] ?? null; + } + + if ($subject instanceof Transition) { + $metadataBag = $this->getTransitionMetadata($subject); + if (!$metadataBag) { + return null; + } + + return $metadataBag[$key] ?? null; + } + + throw new InvalidArgumentException(sprintf('Could not find a MetadataBag for the subject of type "%s".', is_object($subject) ? get_class($subject) : gettype($subject))); + } +} diff --git a/src/Symfony/Component/Workflow/Metadata/InMemoryMetadataStore.php b/src/Symfony/Component/Workflow/Metadata/InMemoryMetadataStore.php new file mode 100644 index 000000000000..44aebe419f87 --- /dev/null +++ b/src/Symfony/Component/Workflow/Metadata/InMemoryMetadataStore.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Metadata; + +use Symfony\Component\Workflow\Transition; + +/** + * @author Grégoire Pineau + */ +final class InMemoryMetadataStore implements MetadataStoreInterface +{ + use GetMetadataTrait; + + private $workflowMetadata; + private $placesMetadata; + private $transitionsMetadata; + + public function __construct($workflowMetadata = array(), array $placesMetadata = array(), \SplObjectStorage $transitionsMetadata = null) + { + $this->workflowMetadata = $workflowMetadata; + $this->placesMetadata = $placesMetadata; + $this->transitionsMetadata = $transitionsMetadata ?: new \SplObjectStorage(); + } + + public function getWorkflowMetadata(): array + { + return $this->workflowMetadata; + } + + public function getPlaceMetadata(string $place): array + { + return $this->placesMetadata[$place] ?? array(); + } + + public function getTransitionMetadata(Transition $transition): array + { + return $this->transitionsMetadata[$transition] ?? array(); + } +} diff --git a/src/Symfony/Component/Workflow/Metadata/MetadataStoreInterface.php b/src/Symfony/Component/Workflow/Metadata/MetadataStoreInterface.php new file mode 100644 index 000000000000..a5d4483eceb1 --- /dev/null +++ b/src/Symfony/Component/Workflow/Metadata/MetadataStoreInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Metadata; + +use Symfony\Component\Workflow\Transition; + +/** + * MetadataStoreInterface is able to fetch metadata for a specific workflow. + * + * @author Grégoire Pineau + */ +interface MetadataStoreInterface +{ + public function getWorkflowMetadata(): array; + + public function getPlaceMetadata(string $place): array; + + public function getTransitionMetadata(Transition $transition): array; + + /** + * Returns the metadata for a specific subject. + * + * This is a proxy method. + * + * @param null|string|Transition $subject Use null to get workflow metadata + * Use a string (the place name) to get place metadata + * Use a Transition instance to get transition metadata + */ + public function getMetadata(string $key, $subject = null); +} diff --git a/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php b/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php index f532639ae09c..b7269d5d7143 100644 --- a/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php +++ b/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\Marking; use Symfony\Component\Workflow\Transition; +use Symfony\Component\Workflow\WorkflowInterface; class GuardListenerTest extends TestCase { @@ -102,7 +103,9 @@ private function createEvent() $subject->marking = new Marking(); $transition = new Transition('name', 'from', 'to'); - return new GuardEvent($subject, $subject->marking, $transition); + $workflow = $this->getMockBuilder(WorkflowInterface::class)->getMock(); + + return new GuardEvent($subject, $subject->marking, $transition, $workflow); } private function configureAuthenticationChecker($isUsed, $granted = true) diff --git a/src/Symfony/Component/Workflow/Tests/Metadata/InMemoryMetadataStoreTest.php b/src/Symfony/Component/Workflow/Tests/Metadata/InMemoryMetadataStoreTest.php new file mode 100644 index 000000000000..f153d545c927 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/Metadata/InMemoryMetadataStoreTest.php @@ -0,0 +1,86 @@ + + */ +class InMemoryMetadataStoreTest extends TestCase +{ + private $store; + private $transition; + + protected function setUp() + { + $workflowMetadata = array( + 'title' => 'workflow title', + ); + $placesMetadata = array( + 'place_a' => array( + 'title' => 'place_a title', + ), + ); + $transitionsMetadata = new \SplObjectStorage(); + $this->transition = new Transition('transition_1', array(), array()); + $transitionsMetadata[$this->transition] = array( + 'title' => 'transition_1 title', + ); + + $this->store = new InMemoryMetadataStore($workflowMetadata, $placesMetadata, $transitionsMetadata); + } + + public function testGetWorkflowMetadata() + { + $metadataBag = $this->store->getWorkflowMetadata(); + $this->assertSame('workflow title', $metadataBag['title']); + } + + public function testGetUnexistingPlaceMetadata() + { + $metadataBag = $this->store->getPlaceMetadata('place_b'); + $this->assertSame(array(), $metadataBag); + } + + public function testGetExistingPlaceMetadata() + { + $metadataBag = $this->store->getPlaceMetadata('place_a'); + $this->assertSame('place_a title', $metadataBag['title']); + } + + public function testGetUnexistingTransitionMetadata() + { + $metadataBag = $this->store->getTransitionMetadata(new Transition('transition_2', array(), array())); + $this->assertSame(array(), $metadataBag); + } + + public function testGetExistingTransitionMetadata() + { + $metadataBag = $this->store->getTransitionMetadata($this->transition); + $this->assertSame('transition_1 title', $metadataBag['title']); + } + + public function testGetMetadata() + { + $this->assertSame('workflow title', $this->store->getMetadata('title')); + $this->assertNull($this->store->getMetadata('description')); + $this->assertSame('place_a title', $this->store->getMetadata('title', 'place_a')); + $this->assertNull($this->store->getMetadata('description', 'place_a')); + $this->assertNull($this->store->getMetadata('description', 'place_b')); + $this->assertSame('transition_1 title', $this->store->getMetadata('title', $this->transition)); + $this->assertNull($this->store->getMetadata('description', $this->transition)); + $this->assertNull($this->store->getMetadata('description', new Transition('transition_2', array(), array()))); + } + + /** + * @expectedException \Symfony\Component\Workflow\Exception\InvalidArgumentException + * @expectedExceptionMessage Could not find a MetadataBag for the subject of type "boolean". + */ + public function testGetMetadataWithUnknownType() + { + $this->store->getMetadata('title', true); + } +} diff --git a/src/Symfony/Component/Workflow/Workflow.php b/src/Symfony/Component/Workflow/Workflow.php index b31cd12ce795..37409c9cd7db 100644 --- a/src/Symfony/Component/Workflow/Workflow.php +++ b/src/Symfony/Component/Workflow/Workflow.php @@ -19,6 +19,7 @@ use Symfony\Component\Workflow\Exception\UndefinedTransitionException; use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface; use Symfony\Component\Workflow\MarkingStore\MultipleStateMarkingStore; +use Symfony\Component\Workflow\Metadata\MetadataStoreInterface; /** * @author Fabien Potencier @@ -219,6 +220,14 @@ public function getMarkingStore() return $this->markingStore; } + /** + * {@inheritdoc} + */ + public function getMetadataStore(): MetadataStoreInterface + { + return $this->definition->getMetadataStore(); + } + private function buildTransitionBlockerListForTransition($subject, Marking $marking, Transition $transition) { foreach ($transition->getFroms() as $place) { @@ -248,7 +257,7 @@ private function guardTransition($subject, Marking $marking, Transition $transit return null; } - $event = new GuardEvent($subject, $marking, $transition, $this->name); + $event = new GuardEvent($subject, $marking, $transition, $this); $this->dispatcher->dispatch('workflow.guard', $event); $this->dispatcher->dispatch(sprintf('workflow.%s.guard', $this->name), $event); @@ -262,7 +271,7 @@ private function leave($subject, Transition $transition, Marking $marking): void $places = $transition->getFroms(); if (null !== $this->dispatcher) { - $event = new Event($subject, $marking, $transition, $this->name); + $event = new Event($subject, $marking, $transition, $this); $this->dispatcher->dispatch('workflow.leave', $event); $this->dispatcher->dispatch(sprintf('workflow.%s.leave', $this->name), $event); @@ -283,7 +292,7 @@ private function transition($subject, Transition $transition, Marking $marking): return; } - $event = new Event($subject, $marking, $transition, $this->name); + $event = new Event($subject, $marking, $transition, $this); $this->dispatcher->dispatch('workflow.transition', $event); $this->dispatcher->dispatch(sprintf('workflow.%s.transition', $this->name), $event); @@ -295,7 +304,7 @@ private function enter($subject, Transition $transition, Marking $marking): void $places = $transition->getTos(); if (null !== $this->dispatcher) { - $event = new Event($subject, $marking, $transition, $this->name); + $event = new Event($subject, $marking, $transition, $this); $this->dispatcher->dispatch('workflow.enter', $event); $this->dispatcher->dispatch(sprintf('workflow.%s.enter', $this->name), $event); @@ -316,7 +325,7 @@ private function entered($subject, Transition $transition, Marking $marking): vo return; } - $event = new Event($subject, $marking, $transition, $this->name); + $event = new Event($subject, $marking, $transition, $this); $this->dispatcher->dispatch('workflow.entered', $event); $this->dispatcher->dispatch(sprintf('workflow.%s.entered', $this->name), $event); @@ -332,7 +341,7 @@ private function completed($subject, Transition $transition, Marking $marking): return; } - $event = new Event($subject, $marking, $transition, $this->name); + $event = new Event($subject, $marking, $transition, $this); $this->dispatcher->dispatch('workflow.completed', $event); $this->dispatcher->dispatch(sprintf('workflow.%s.completed', $this->name), $event); @@ -345,7 +354,7 @@ private function announce($subject, Transition $initialTransition, Marking $mark return; } - $event = new Event($subject, $marking, $initialTransition, $this->name); + $event = new Event($subject, $marking, $initialTransition, $this); $this->dispatcher->dispatch('workflow.announce', $event); $this->dispatcher->dispatch(sprintf('workflow.%s.announce', $this->name), $event); diff --git a/src/Symfony/Component/Workflow/WorkflowInterface.php b/src/Symfony/Component/Workflow/WorkflowInterface.php index 2460963cba4f..5a1f2c74e81a 100644 --- a/src/Symfony/Component/Workflow/WorkflowInterface.php +++ b/src/Symfony/Component/Workflow/WorkflowInterface.php @@ -13,6 +13,7 @@ use Symfony\Component\Workflow\Exception\LogicException; use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface; +use Symfony\Component\Workflow\Metadata\MetadataStoreInterface; /** * @author Amrouche Hamza @@ -82,4 +83,6 @@ public function getDefinition(); * @return MarkingStoreInterface */ public function getMarkingStore(); + + public function getMetadataStore(): MetadataStoreInterface; }