feature #26092 [Workflow] Add a MetadataStore to fetch some metadata (lyrixx)

This PR was merged into the 4.1-dev branch.

Discussion
----------

[Workflow] Add a MetadataStore to fetch some metadata

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes (little)
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #23257
| License       | MIT
| Doc PR        | TODO

---

This is an attempt to fix #23257. I first started to implement
`Ẁorkflow::getMetadata()`, `Transition::getMetadata()` and
`Place::getMetadata()`. **BUT**, there are no `Place` class. For now it's just a
`string`. So dealing with BC is a nightmare.

So I tried to find another way to fix the issue. [This
comment](https://github.com/symfony/symfony/issues/23257#issuecomment-315551397)
summary well the two options. But this PR is (will be) a mix of theses 2
options.

First it will be possible to configure the workflow/metadata like this:

```yaml
blog_publishing:
    supports:
        - AppBundle\Entity\BlogPost
    metada:
         label: Blog publishing
         description: Manages blog publishing
    places:
        draft:
            metadata:
                 description: Blog has just been created
                 color: grey
        review:
            metadata:
                 description: Blog is waiting for review
                 color: blue
    transitions:
        to_review:
            from: draft
            to: review
            metadata:
                label: Submit for review
                route: admin.blog.review
```

I think is very good for the DX. Simple to understand.

All metadata will live in a `MetadataStoreInterface`. If metadata are set via
the configuration (workflows.yaml), then we will use the
`InMemoryMetadataStore`.

Having a MetadataStoreInterface allow user to get dynamic value for a place /
transitions. It's really flexible. (But is it a valid use case ?)

Then, to retrieve these data, the end user will have to write this code:

```php
public function onReview(Event $event) {
    $metadataStore = $event->getWorkflow()->getMetadataStore();
    foreach ($event->getTransition()->getTos() as $place) {
        $this->flashbag->add('info', $metadataStore->getPlaceMetadata($place)->get('description'));
    }
}
```

Note: I might add some shortcut to the Event class

or in twig:

```jinja
{% for transition in workflow_transitions(post) %}
    <a href="{{ workflow_metadata_transition(post, route) }}">
         {{ workflow_metadata_transition(post, transition) }}
   </a>
{% endfor %}
```

---

WDYT ?

Should I continue this way, or should I introduce a `Place` class (there will be
so many deprecation ...)

Commits
-------

bd1f2c8583 [Workflow] Add a MetadataStore
This commit is contained in:
Fabien Potencier 2018-03-21 11:17:02 +01:00
commit 07a2f6cef4
26 changed files with 569 additions and 73 deletions

View File

@ -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.

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
4.1.0
-----
* add a `workflow_metadata` function
3.4.0
-----

View File

@ -37,6 +37,7 @@ class WorkflowExtension extends AbstractExtension
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 @@ class WorkflowExtension extends AbstractExtension
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';

View File

@ -14,6 +14,7 @@ namespace Symfony\Bridge\Twig\Tests\Extension;
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 @@ use Symfony\Component\Workflow\Workflow;
class WorkflowExtensionTest extends TestCase
{
private $extension;
private $t1;
protected function setUp()
{
@ -32,10 +34,21 @@ class WorkflowExtensionTest extends TestCase
$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 @@ class WorkflowExtensionTest extends TestCase
$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));
}
}

View File

@ -31,6 +31,7 @@ use Symfony\Component\WebLink\HttpHeaderSerializer;
* FrameworkExtension configuration structure.
*
* @author Jeremy Mikola <jmikola@gmail.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Configuration implements ConfigurationInterface
{
@ -292,23 +293,61 @@ class Configuration implements ConfigurationInterface
->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 @@ class Configuration implements ConfigurationInterface
->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) {

View File

@ -466,32 +466,68 @@ class FrameworkExtension extends Extension
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'])) {

View File

@ -273,8 +273,9 @@
<xsd:sequence>
<xsd:element name="marking-store" type="marking_store" minOccurs="0" maxOccurs="1" />
<xsd:element name="support" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="place" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="place" type="place" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="transition" type="transition" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="metadata" type="metadata" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="workflow_type" />
@ -302,10 +303,24 @@
<xsd:sequence>
<xsd:element name="from" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
<xsd:element name="to" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
<xsd:element name="metadata" type="metadata" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="place" mixed="true">
<xsd:sequence>
<xsd:element name="metadata" type="metadata" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="metadata">
<xsd:sequence>
<xsd:any minOccurs="0" processContents="lax"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="workflow_type">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="state_machine" />

View File

@ -48,18 +48,29 @@ $container->loadFromExtension('framework', array(
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 @@ $container->loadFromExtension('framework', array(
FrameworkExtensionTest::class,
),
'places' => array(
'first',
'last',
array('name' => 'first'),
array('name' => 'last'),
),
'transitions' => array(
'go' => array(

View File

@ -13,8 +13,8 @@
<framework:argument>a</framework:argument>
</framework:marking-store>
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<framework:place>first</framework:place>
<framework:place>last</framework:place>
<framework:place name="first" />
<framework:place name="last" />
<framework:transition name="foobar">
<framework:from>a</framework:from>
<framework:to>a</framework:to>

View File

@ -13,12 +13,12 @@
<framework:argument>a</framework:argument>
</framework:marking-store>
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<framework:place>draft</framework:place>
<framework:place>wait_for_journalist</framework:place>
<framework:place>approved_by_journalist</framework:place>
<framework:place>wait_for_spellchecker</framework:place>
<framework:place>approved_by_spellchecker</framework:place>
<framework:place>published</framework:place>
<framework:place name="draft" />
<framework:place name="wait_for_journalist" />
<framework:place name="approved_by_journalist" />
<framework:place name="wait_for_spellchecker" />
<framework:place name="approved_by_spellchecker" />
<framework:place name="published" />
<framework:transition name="request_review">
<framework:from>draft</framework:from>
<framework:to>wait_for_journalist</framework:to>

View File

@ -10,8 +10,8 @@
<framework:workflow name="my_workflow" support-strategy="foobar">
<framework:marking-store type="multiple_state"/>
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<framework:place>first</framework:place>
<framework:place>last</framework:place>
<framework:place name="first" />
<framework:place name="last" />
<framework:transition name="foobar">
<framework:from>a</framework:from>
<framework:to>a</framework:to>

View File

@ -10,8 +10,8 @@
<framework:workflow name="my_workflow">
<framework:marking-store type="multiple_state" service="workflow_service" />
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<framework:place>first</framework:place>
<framework:place>last</framework:place>
<framework:place name="first" />
<framework:place name="last" />
<framework:transition name="foobar">
<framework:from>a</framework:from>
<framework:to>a</framework:to>

View File

@ -9,8 +9,8 @@
<framework:config>
<framework:workflow name="my_workflow">
<framework:marking-store type="multiple_state"/>
<framework:place>first</framework:place>
<framework:place>last</framework:place>
<framework:place name="first" />
<framework:place name="last" />
<framework:transition name="foobar">
<framework:from>a</framework:from>
<framework:to>a</framework:to>

View File

@ -13,12 +13,12 @@
<framework:argument>a</framework:argument>
</framework:marking-store>
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<framework:place>draft</framework:place>
<framework:place>wait_for_journalist</framework:place>
<framework:place>approved_by_journalist</framework:place>
<framework:place>wait_for_spellchecker</framework:place>
<framework:place>approved_by_spellchecker</framework:place>
<framework:place>published</framework:place>
<framework:place name="draft"></framework:place>
<framework:place name="wait_for_journalist"></framework:place>
<framework:place name="approved_by_journalist"></framework:place>
<framework:place name="wait_for_spellchecker"></framework:place>
<framework:place name="approved_by_spellchecker"></framework:place>
<framework:place name="published"></framework:place>
<framework:transition name="request_review">
<framework:from>draft</framework:from>
<framework:to>wait_for_journalist</framework:to>
@ -42,15 +42,22 @@
<framework:workflow name="pull_request" initial-place="start">
<framework:marking-store type="single_state"/>
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<framework:place>start</framework:place>
<framework:place>coding</framework:place>
<framework:place>travis</framework:place>
<framework:place>review</framework:place>
<framework:place>merged</framework:place>
<framework:place>closed</framework:place>
<framework:place name="start">
<framework:metadata>
<framework:title>place start title</framework:title>
</framework:metadata>
</framework:place>
<framework:place name="coding"></framework:place>
<framework:place name="travis"></framework:place>
<framework:place name="review"></framework:place>
<framework:place name="merged"></framework:place>
<framework:place name="closed"></framework:place>
<framework:transition name="submit">
<framework:from>start</framework:from>
<framework:to>travis</framework:to>
<framework:metadata>
<framework:title>transition submit title</framework:title>
</framework:metadata>
</framework:transition>
<framework:transition name="update">
<framework:from>coding</framework:from>
@ -78,11 +85,15 @@
<framework:from>closed</framework:from>
<framework:to>review</framework:to>
</framework:transition>
<framework:metadata>
<framework:title>workflow title</framework:title>
</framework:metadata>
</framework:workflow>
<framework:workflow name="service_marking_store_workflow" type="workflow">
<framework:marking-store service="workflow_service"/>
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<!-- Simple format -->
<framework:place>first</framework:place>
<framework:place>last</framework:place>
<framework:transition name="go">

View File

@ -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:

View File

@ -43,7 +43,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
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 @@ abstract class FrameworkExtensionTest extends TestCase
'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 @@ abstract class FrameworkExtensionTest extends TestCase
$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 @@ abstract class FrameworkExtensionTest extends TestCase
{
$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'));
}

View File

@ -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
-----

View File

@ -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 <fabien@symfony.com>
@ -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 @@ final class Definition
}
$this->setInitialPlace($initialPlace);
$this->metadataStore = $metadataStore ?: new InMemoryMetadataStore();
}
/**
@ -66,6 +71,11 @@ final class Definition
return $this->transitions;
}
public function getMetadataStore(): MetadataStoreInterface
{
return $this->metadataStore;
}
private function setInitialPlace(string $place = null)
{
if (null === $place) {

View File

@ -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 <fabien@symfony.com>
@ -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 @@ class Event extends BaseEvent
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);
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <lyrixx@lyrixx.info>
*/
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)));
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <lyrixx@lyrixx.info>
*/
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();
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <lyrixx@lyrixx.info>
*/
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);
}

View File

@ -14,6 +14,7 @@ use Symfony\Component\Workflow\EventListener\GuardListener;
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 @@ class GuardListenerTest extends TestCase
$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)

View File

@ -0,0 +1,86 @@
<?php
namespace Symfony\Component\Workflow\Tests\Metadata;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Transition;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
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);
}
}

View File

@ -19,6 +19,7 @@ use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
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 <fabien@symfony.com>
@ -219,6 +220,14 @@ class Workflow implements WorkflowInterface
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 @@ class Workflow implements WorkflowInterface
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 @@ class Workflow implements WorkflowInterface
$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 @@ class Workflow implements WorkflowInterface
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 @@ class Workflow implements WorkflowInterface
$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 @@ class Workflow implements WorkflowInterface
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 @@ class Workflow implements WorkflowInterface
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 @@ class Workflow implements WorkflowInterface
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);

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Workflow;
use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;
/**
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
@ -82,4 +83,6 @@ interface WorkflowInterface
* @return MarkingStoreInterface
*/
public function getMarkingStore();
public function getMetadataStore(): MetadataStoreInterface;
}