├── .php_cs.dist
├── .styleci.yml
├── CHANGELOG.md
├── README.md
├── composer.json
├── src
├── Controller
│ └── DefaultTransitionController.php
├── DependencyInjection
│ ├── Compiler
│ │ └── WorkflowPass.php
│ ├── Configuration.php
│ └── WesnickWorkflowExtension.php
├── EventListener
│ ├── SubjectValidatorListener.php
│ └── WorkflowOperationListener.php
├── Metadata
│ └── WorkflowActionsResourceMetadataFactory.php
├── Model
│ ├── Action.php
│ ├── EntryPoint.php
│ ├── PotentialActionInterface.php
│ ├── PotentialActionsTrait.php
│ └── WorkflowDTO.php
├── Resources
│ ├── config
│ │ ├── api_patch.xml
│ │ ├── workflow.xml
│ │ └── workflow_validation.xml
│ └── meta
│ │ └── LICENSE
├── Serializer
│ ├── ActionsDocumentationNormalizer.php
│ ├── WorkflowActionContextBuilder.php
│ ├── WorkflowActionNormalizer.php
│ └── WorkflowNormalizer.php
├── Transformer
│ └── WorkflowDtoTransformer.php
├── Validation
│ ├── ChainedWorkflowValidationStrategy.php
│ ├── ValidationStateProviderInterface.php
│ ├── ValidationStateProviderStrategy.php
│ ├── WorkflowValidationStrategy.php
│ └── WorkflowValidationStrategyInterface.php
├── WesnickWorkflowBundle.php
└── WorkflowActionGenerator.php
└── tests
├── Controller
└── DefaultTransitionControllerTest.php
├── EventListener
└── WorkflowOperationListenerTest.php
├── Fixtures
├── ArticleWithWorkflow.php
└── StateProviderWithWorkflow.php
├── Metadata
└── WorkflowActionsResourceMetadataFactoryTest.php
└── Validation
├── ChainedWorkflowValidationStrategyTest.php
├── ValidationStateProviderStrategyTest.php
└── WorkflowValidationStrategyTest.php
/.php_cs.dist:
--------------------------------------------------------------------------------
1 | setRules([
11 | '@Symfony' => true,
12 | 'array_syntax' => ['syntax' => 'short'],
13 | 'declare_strict_types' => true,
14 | 'combine_consecutive_unsets' => true,
15 | 'header_comment' => ['header' => $header],
16 | 'linebreak_after_opening_tag' => true,
17 | 'no_php4_constructor' => true,
18 | 'no_useless_else' => true,
19 | 'ordered_class_elements' => true,
20 | 'ordered_imports' => true,
21 | 'php_unit_construct' => true,
22 | 'php_unit_strict' => true,
23 | 'phpdoc_no_empty_return' => false,
24 | ])
25 | ->setUsingCache(true)
26 | ->setRiskyAllowed(true)
27 | ->setFinder(
28 | PhpCsFixer\Finder::create()
29 | ->in(__DIR__)
30 | )
31 | ;
32 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: symfony
2 |
3 | enabled:
4 | - alpha_ordered_imports
5 | - short_array_syntax
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | WesnickWorkflowBundle
2 | ==================
3 |
4 | ### This bundle is a work-in-progress.
5 |
6 | [](https://travis-ci.org/wesnick/api-platform-workflow-extension)
7 | [](https://scrutinizer-ci.com/g/wesnick/api-platform-workflow-extension/build-status/master)
8 |
9 | Introduction
10 | ------------
11 |
12 | This bundle tries to allow relative easy integration between the API Platform and Symfony's Workflow component
13 |
14 | The goal is:
15 | - implement [actions as outlined in schema.org](https://schema.org/docs/actions.html)
16 | - automatically expose workflow information as part of the resource payload.
17 | - contains information about all available transitions
18 | - contains available but blocked transitions with ConstraintViolation messages
19 | - automatically expose endpoints to execute transitions.
20 | - workflows should try to do all mutations and persistence using listeners
21 |
22 | - Workflows that support API Platform resources can be toggled to include workflow data and operations
23 | - Workflow transition information is added to the resource representation as an array of [potentialAction](https://schema.org/docs/actions.html)
24 | ```json
25 | {
26 | "id": 1,
27 | "name": "Bob",
28 | "potentialAction": [
29 | {
30 | "@type": "ControlAction",
31 | "target": {
32 | "@type": "Entrypoint",
33 | "httpMethod": "PATCH",
34 | "url": "/users/1?workflow=user_status&transition=ban_user"
35 | }
36 | }
37 | ]
38 | }
39 |
40 | ```
41 | - If any potential action has a workflow transition blocker, these are displayed as an Api Platform ConstraintViolation class type under the property error
42 | ```json
43 | {
44 | "id": 1,
45 | "name": "Bob",
46 | "potentialAction": [
47 | {
48 | "@type": "ControlAction",
49 | "name": "ban",
50 | "description": "Ban a User",
51 | "target": {
52 | "@type": "Entrypoint",
53 | "httpMethod": "PATCH",
54 | "url": "/users/1?workflow=user_status&transition=ban_user"
55 | },
56 | "error": [{
57 | "message": "User must be enabled before banning.",
58 | "propertyPath": "/enabled"
59 | }]
60 | }
61 | ]
62 | }
63 | ```
64 | - The return type for workflows transition executions is always the subject. The subject will have updated potentialAction information as part of the response payload. If your transition succeeded 200, if it failed 400 with a ConstraintViolationList.
65 | - Depending on how you use workflows, you may or may not have data input for transitions. If there is no data, you can just PATCH a simple payload with the transition to the PATCH API endpoint. If your workflow transition requires data input, you can use the DTO feature to provide a custom input class
66 |
67 | ### Example
68 | Here are some example payloads with a basic user promote demote workflow
69 |
70 | GET a User
71 | ```
72 | GET http://localhost/api/users/5
73 | Content-Type: application/ld+json
74 |
75 | {
76 | "@context": "\/api\/contexts\/User",
77 | "@id": "\/api\/users\/5",
78 | "@type": "User",
79 | "id": 5,
80 | "email": "erik.ledner@gmail.com",
81 | "lastLogin": null,
82 | "expired": false,
83 | "locked": false,
84 | "credentialsExpired": false,
85 | "enabled": true,
86 | "roles": [
87 | "ROLE_ADMIN"
88 | ],
89 | "potentialAction": [
90 | {
91 | "@context": "http:\/\/schema.org",
92 | "@type": "Action",
93 | "actionStatus": "PotentialActionStatus",
94 | "target": {
95 | "httpMethod": "PATCH",
96 | "url": "\/api\/users\/5?workflow=user_role&transition=promote_to_superadmin"
97 | },
98 | "name": "promote_to_superadmin",
99 | "description": "Promote to Super Admin"
100 | },
101 | {
102 | "@context": "http:\/\/schema.org",
103 | "@type": "Action",
104 | "actionStatus": "PotentialActionStatus",
105 | "target": {
106 | "httpMethod": "PATCH",
107 | "url": "\/api\/users\/5?workflow=user_role&transition=demote_to_user"
108 | },
109 | "name": "demote_to_user",
110 | "description": "Demote to User"
111 | }
112 | ]
113 | }
114 | ```
115 | Execute demote_to_user transition
116 | ```
117 | PATCH http://localhost/api/users/5?workflow=user_role
118 | Content-Type: application/ld+json
119 |
120 | {
121 | "transition": "demote_to_user"
122 | }
123 | ```
124 | Response
125 | ```json
126 | {
127 | "@context": "\/api\/contexts\/User",
128 | "@id": "\/api\/users\/5",
129 | "@type": "User",
130 | "id": 5,
131 | "email": "erik.ledner@gmail.com",
132 | "lastLogin": null,
133 | "expired": false,
134 | "locked": false,
135 | "credentialsExpired": false,
136 | "enabled": true,
137 | "roles": [
138 | "ROLE_USER"
139 | ],
140 | "potentialAction": [
141 | {
142 | "@context": "http:\/\/schema.org",
143 | "@type": "Action",
144 | "actionStatus": "PotentialActionStatus",
145 | "target": {
146 | "httpMethod": "PATCH",
147 | "url": "\/api\/users\/5?workflow=user_role&transition=promote_to_admin"
148 | },
149 | "name": "promote_to_admin",
150 | "description": "Promote to Admin"
151 | },
152 | {
153 | "@context": "http:\/\/schema.org",
154 | "@type": "Action",
155 | "actionStatus": "PotentialActionStatus",
156 | "target": {
157 | "httpMethod": "PATCH",
158 | "url": "\/api\/users\/5?workflow=user_role&transition=promote_to_superadmin"
159 | },
160 | "name": "promote_to_superadmin",
161 | "description": "Promote to Super Admin"
162 | }
163 | ]
164 | }
165 | ```
166 |
167 |
168 | Features
169 | --------
170 |
171 | - Validation Helpers
172 | - React admin integration (WIP)
173 |
174 | Documentation
175 | -------------
176 |
177 | - enable the module
178 | ```yml
179 | # config/packages/wesnick_workflow.yaml
180 |
181 | wesnick_workflow:
182 | api_patch_transitions: true # default
183 | workflow_validation_guard: true # default
184 | ```
185 |
186 | - To enable API support for your workflows:
187 |
188 | - Subject class must implement PotentialActionInterface
189 | - You can implement this interface either with PotentialActionsTrait or on your own, be sure to set serialization groups appropriately. The bundle automatically pushes the group ```workflowAction:output``` during denormalization.
190 | - add descriptive messages for your workflow/transitions using workflow metadata
191 |
192 |
193 |
194 | Roadmap
195 | -------
196 |
197 | License
198 | -------
199 |
200 | This bundle is released under the MIT license. See the included
201 | [LICENSE](src/Resources/meta/LICENSE) file for more information.
202 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wesnick/api-platform-workflow-extension",
3 | "type": "symfony-bundle",
4 | "description": "Opinionated integration of api platform with workflow component",
5 | "keywords": [ "workflow", "api-platform" ],
6 | "homepage": "https://github.com/wesnick/api-platform-workflow-extension",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Wesley O. Nichols",
11 | "email": "spanishwes@gmail.com"
12 | }
13 | ],
14 | "require": {
15 | "php": "^7.1",
16 | "ext-json": "*",
17 | "api-platform/core": "^2.4",
18 | "symfony/workflow": "^3.4 || ^4.0",
19 | "symfony/validator": "^3.4 || ^4.0",
20 | "symfony/framework-bundle": "^3.4.4 || ^4.1.12"
21 | },
22 | "require-dev": {
23 | "symfony/phpunit-bridge": "^4.2"
24 | },
25 | "autoload": {
26 | "psr-4": {
27 | "Wesnick\\WorkflowBundle\\": "src/"
28 | }
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "Wesnick\\WorkflowBundle\\Tests\\": "tests/"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Controller/DefaultTransitionController.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | class DefaultTransitionController
26 | {
27 | private $registry;
28 |
29 | public function __construct(Registry $registry)
30 | {
31 | $this->registry = $registry;
32 | }
33 |
34 | /**
35 | * @param WorkflowDTO $data
36 | * @param mixed $subject
37 | * @param string $workflowName
38 | * @param string $transitionName
39 | *
40 | * @return mixed
41 | */
42 | public function __invoke($data, $subject, string $workflowName, string $transitionName)
43 | {
44 | if (!is_object($subject)) {
45 | throw new BadRequestHttpException(
46 | sprintf('Expected object for workflow "%s", got %s.', $workflowName, gettype($subject))
47 | );
48 | }
49 |
50 | try {
51 | $workflow = $this->registry->get($subject, $workflowName);
52 |
53 | if ($workflow->can($subject, $transitionName)) {
54 | // Symfony 4.3 added context to workflow transitions
55 | if (version_compare(Kernel::VERSION, '4.3.0', '<')) {
56 | $workflow->apply($subject, $transitionName);
57 | } else {
58 | $workflow->apply($subject, $transitionName, ['wesnick_workflow_dto' => $data]);
59 | }
60 |
61 | return $subject;
62 | }
63 |
64 | } catch (InvalidArgumentException $e) {
65 | throw new BadRequestHttpException($e->getMessage());
66 | }
67 |
68 | throw new BadRequestHttpException(
69 | sprintf('Transition "%s" in Workflow "%s" is not available.', $transitionName, $workflowName)
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/WorkflowPass.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | class WorkflowPass implements CompilerPassInterface
28 | {
29 | /**
30 | * {@inheritdoc}
31 | */
32 | public function process(ContainerBuilder $container)
33 | {
34 | if (!$container->hasDefinition('workflow.registry')) {
35 | return;
36 | }
37 | // @TODO: not sure if required to add Models to resource class directories.
38 | // $directories = $container->getParameter('api_platform.resource_class_directories');
39 | // $directories[] = realpath(__DIR__.'/../../Model');
40 | // $container->setParameter('api_platform.resource_class_directories', $directories);
41 |
42 | $config = $container->getExtensionConfig('wesnick_workflow');
43 |
44 | $classMap = [];
45 |
46 | // Iterate over workflows and create services
47 | foreach ($this->workflowGenerator($container) as [$workflow, $supportStrategy]) {
48 | // only support InstanceOfSupportStrategy for now
49 | if (InstanceOfSupportStrategy::class !== $supportStrategy->getClass()) {
50 | throw new \RuntimeException(sprintf('Wesnick Workflow Bundle requires use of InstanceOfSupportStrategy, workflow %s is using strategy %s', (string) $workflow, $supportStrategy->getClass()));
51 | }
52 |
53 | $className = $supportStrategy->getArgument(0);
54 | $workflowShortName = $workflow->getArgument(3);
55 | $classMap[$className][] = $workflowShortName;
56 |
57 | if ($config[0]['workflow_validation_guard']) {
58 | $container
59 | ->getDefinition(SubjectValidatorListener::class)
60 | ->addTag('kernel.event_listener', ['event' => 'workflow.'.$workflowShortName.'.guard', 'method' => 'onGuard']);
61 | }
62 | }
63 |
64 | $container->getDefinition(WorkflowActionGenerator::class)->setArgument('$enabledWorkflowMap', $classMap);
65 | $container->getDefinition(WorkflowOperationListener::class)->setArgument('$enabledWorkflowMap', $classMap);
66 | }
67 |
68 | private function workflowGenerator(ContainerBuilder $container): \Generator
69 | {
70 | $registry = $container->getDefinition('workflow.registry');
71 | foreach ($registry->getMethodCalls() as $call) {
72 | [, [$workflowReference, $supportStrategy]] = $call;
73 | yield [$container->getDefinition($workflowReference), $supportStrategy];
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class Configuration implements ConfigurationInterface
14 | {
15 | public function getConfigTreeBuilder()
16 | {
17 | $treeBuilder = new TreeBuilder('wesnick_workflow');
18 |
19 | $treeBuilder->getRootNode()
20 | ->children()
21 | ->booleanNode('api_patch_transitions')->defaultTrue()->end()
22 | ->booleanNode('workflow_validation_guard')->defaultTrue()->end()
23 | ->end()
24 | ;
25 |
26 | return $treeBuilder;
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/DependencyInjection/WesnickWorkflowExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($configuration, $configs);
34 |
35 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
36 | $loader->load('workflow.xml');
37 | if (true === $config['api_patch_transitions']) {
38 | $loader->load('api_patch.xml');
39 | }
40 |
41 | if (true === $config['workflow_validation_guard']) {
42 | $loader->load('workflow_validation.xml');
43 | // @TODO: add a tag
44 | $chainedValidator = $container->getDefinition(ChainedWorkflowValidationStrategy::class);
45 | $chainedValidator->setArgument(0, [
46 | $container->getDefinition(WorkflowValidationStrategy::class),
47 | $container->getDefinition(ValidationStateProviderStrategy::class)
48 | ]);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/EventListener/SubjectValidatorListener.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | class SubjectValidatorListener
25 | {
26 | private $validator;
27 | private $validationStrategy;
28 |
29 | /**
30 | * SubjectValidatorListener constructor.
31 | *
32 | * @param ValidatorInterface $validator
33 | * @param WorkflowValidationStrategyInterface $validationStrategy
34 | */
35 | public function __construct(ValidatorInterface $validator, WorkflowValidationStrategyInterface $validationStrategy)
36 | {
37 | $this->validator = $validator;
38 | $this->validationStrategy = $validationStrategy;
39 | }
40 |
41 | /**
42 | * @param GuardEvent $event
43 | */
44 | public function onGuard(GuardEvent $event)
45 | {
46 | $validationGroups = $this
47 | ->validationStrategy
48 | ->getValidationGroupsForSubject($event->getSubject(), $event->getWorkflow(), $event->getTransition())
49 | ;
50 |
51 | $violations = $this->validator->validate($event->getSubject(), null, $validationGroups);
52 |
53 | foreach ($violations as $violation) {
54 | $event->addTransitionBlocker(
55 | new TransitionBlocker(
56 | $violation->getMessage(),
57 | $violation->getCode(),
58 | $violation->getParameters() + ['original_violation' => $violation]
59 | )
60 | );
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/EventListener/WorkflowOperationListener.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class WorkflowOperationListener
24 | {
25 | /**
26 | * Classmap.
27 | *
28 | * [ className => [workflowName]]
29 | *
30 | * @var array
31 | */
32 | private $enabledWorkflowMap;
33 |
34 | public function __construct(array $enabledWorkflowMap)
35 | {
36 | $this->enabledWorkflowMap = $enabledWorkflowMap;
37 | }
38 |
39 | /**
40 | * @param GetResponseEvent $event
41 | */
42 | public function onKernelRequest(GetResponseEvent $event)
43 | {
44 | $request = $event->getRequest();
45 | if (!$request->isMethod(Request::METHOD_PATCH)
46 | || !($attributes = RequestAttributesExtractor::extractAttributes($request))
47 | || 'patch' !== $attributes['item_operation_name'] ?? null
48 | || !array_key_exists($attributes['resource_class'] ?? 'n/a', $this->enabledWorkflowMap)
49 | ) {
50 | return;
51 | }
52 |
53 | $requestContent = json_decode($request->getContent());
54 | // Set the data attribute as subject, since the DTO will be deserialized to the data attribute
55 | $request->attributes->set('subject', $request->attributes->get('data'));
56 | $request->attributes->set('workflowName', $request->query->get('workflow', ''));
57 | $request->attributes->set('transitionName', $requestContent->transition ?? '');
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Metadata/WorkflowActionsResourceMetadataFactory.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | class WorkflowActionsResourceMetadataFactory implements ResourceMetadataFactoryInterface
26 | {
27 | private $decorated;
28 |
29 | public function __construct(ResourceMetadataFactoryInterface $decorated)
30 | {
31 | $this->decorated = $decorated;
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function create(string $resourceClass): ResourceMetadata
38 | {
39 | $resourceMetadata = $this->decorated->create($resourceClass);
40 | if (!is_a($resourceClass, PotentialActionInterface::class, true)) {
41 | return $resourceMetadata;
42 | }
43 |
44 | // Set the pseudo-group for potentialAction to appear in name collection metadata factories
45 | $attributes = $resourceMetadata->getAttributes();
46 | $groups = $attributes['denormalization_context']['groups'] ?? [];
47 | if (!in_array('workflowAction:output', $groups, true)) {
48 | $attributes['denormalization_context']['groups'][] = 'workflowAction:output';
49 | }
50 |
51 | $operations = $resourceMetadata->getItemOperations();
52 | $operations['patch'] = [
53 | 'method' => 'PATCH',
54 | 'controller' => DefaultTransitionController::class,
55 | 'input' => ['class' => WorkflowDTO::class, 'name' => 'WorkflowDTO'],
56 | ];
57 |
58 | return $resourceMetadata
59 | ->withAttributes($attributes)
60 | ->withItemOperations($operations)
61 | ;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Model/Action.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | class Action
29 | {
30 | /**
31 | * @var string|null indicates the current disposition of the Action
32 | *
33 | * @ApiProperty(iri="http://schema.org/actionStatus")
34 | * @Groups({"workflowAction:output"})
35 | * @Assert\Type(type="string")
36 | * @Assert\Choice(choices={
37 | * "PotentialActionStatus",
38 | * "ActiveActionStatus",
39 | * "FailedActionStatus",
40 | * "CompletedActionStatus"
41 | * })
42 | */
43 | private $actionStatus = 'PotentialActionStatus';
44 |
45 | /**
46 | * @var EntryPoint|null indicates a target EntryPoint for an Action
47 | *
48 | * @ApiProperty(iri="http://schema.org/target")
49 | * @Groups({"workflowAction:output"})
50 | */
51 | private $target;
52 |
53 | /**
54 | * @var string|null the name of the item
55 | *
56 | * @ApiProperty(iri="http://schema.org/name")
57 | * @Groups({"workflowAction:output"})
58 | * @Assert\Type(type="string")
59 | */
60 | private $name;
61 |
62 | /**
63 | * @var string|null a description of the item
64 | *
65 | * @ApiProperty(iri="http://schema.org/description")
66 | * @Groups({"workflowAction:output"})
67 | * @Assert\Type(type="string")
68 | */
69 | private $description;
70 |
71 | /**
72 | * @var ConstraintViolationListInterface|null
73 | *
74 | * @ApiProperty(iri="http://schema.org/error")
75 | * @Groups({"workflowAction:output"})
76 | *
77 | * @var ConstraintViolationList
78 | */
79 | private $error;
80 |
81 | public function setActionStatus(?string $actionStatus): void
82 | {
83 | $this->actionStatus = $actionStatus;
84 | }
85 |
86 | public function getActionStatus(): ?string
87 | {
88 | return $this->actionStatus;
89 | }
90 |
91 | /**
92 | * @return EntryPoint|null
93 | */
94 | public function getTarget(): ?EntryPoint
95 | {
96 | return $this->target;
97 | }
98 |
99 | /**
100 | * @param EntryPoint $target
101 | */
102 | public function setTarget(EntryPoint $target): void
103 | {
104 | $this->target = $target;
105 | }
106 |
107 | /**
108 | * @return string|null
109 | */
110 | public function getName(): ?string
111 | {
112 | return $this->name;
113 | }
114 |
115 | /**
116 | * @param string|null $name
117 | */
118 | public function setName(?string $name): void
119 | {
120 | $this->name = $name;
121 | }
122 |
123 | /**
124 | * @return string|null
125 | */
126 | public function getDescription(): ?string
127 | {
128 | return $this->description;
129 | }
130 |
131 | /**
132 | * @param string|null $description
133 | */
134 | public function setDescription(?string $description): void
135 | {
136 | $this->description = $description;
137 | }
138 |
139 | /**
140 | * @return ConstraintViolationListInterface|null
141 | */
142 | public function getError(): ?ConstraintViolationListInterface
143 | {
144 | return $this->error;
145 | }
146 |
147 | /**
148 | * @param ConstraintViolationInterface $error
149 | */
150 | public function addError(ConstraintViolationInterface $error): void
151 | {
152 | if (null === $this->error) {
153 | $this->error = new ConstraintViolationList();
154 | }
155 |
156 | $this->error->add($error);
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/Model/EntryPoint.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | class EntryPoint
25 | {
26 | /**
27 | * @var string|null An HTTP method that specifies the appropriate HTTP method for a request to an HTTP EntryPoint. Values are capitalized strings as used in HTTP.
28 | *
29 | * @ApiProperty(iri="http://schema.org/httpMethod")
30 | * @Assert\Type(type="string")
31 | */
32 | private $httpMethod;
33 |
34 | /**
35 | * @var string|null an url template (RFC6570) that will be used to construct the target of the execution of the action
36 | *
37 | * @ApiProperty(iri="http://schema.org/urlTemplate")
38 | * @Assert\Type(type="string")
39 | */
40 | private $urlTemplate;
41 |
42 | /**
43 | * @var string|null URL of the item
44 | *
45 | * @ApiProperty(iri="http://schema.org/url")
46 | * @Assert\Url
47 | */
48 | private $url;
49 |
50 | public function setHttpMethod(?string $httpMethod): void
51 | {
52 | $this->httpMethod = $httpMethod;
53 | }
54 |
55 | public function getHttpMethod(): ?string
56 | {
57 | return $this->httpMethod;
58 | }
59 |
60 | public function setUrlTemplate(?string $urlTemplate): void
61 | {
62 | $this->urlTemplate = $urlTemplate;
63 | }
64 |
65 | public function getUrlTemplate(): ?string
66 | {
67 | return $this->urlTemplate;
68 | }
69 |
70 | public function setUrl(?string $url): void
71 | {
72 | $this->url = $url;
73 | }
74 |
75 | public function getUrl(): ?string
76 | {
77 | return $this->url;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Model/PotentialActionInterface.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | interface PotentialActionInterface
20 | {
21 | /**
22 | * @param Action $action
23 | */
24 | public function addPotentialAction(Action $action);
25 |
26 | /**
27 | * @return Action[]
28 | */
29 | public function getPotentialAction(): array;
30 | }
31 |
--------------------------------------------------------------------------------
/src/Model/PotentialActionsTrait.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | trait PotentialActionsTrait
23 | {
24 | /**
25 | * @var Action[] collection of potential Action, which describes an idealized action in which this thing
26 | * would play an 'object' role
27 | *
28 | * @ApiProperty(
29 | * iri="http://schema.org/potentialAction",
30 | * readable=true,
31 | * writable=false
32 | * )
33 | * @Groups({"workflowAction:output"})
34 | */
35 | private $potentialAction = [];
36 |
37 | /**
38 | * @param Action $action
39 | */
40 | public function addPotentialAction(Action $action)
41 | {
42 | $this->potentialAction[] = $action;
43 | }
44 |
45 | /**
46 | * @return array
47 | */
48 | public function getPotentialAction(): array
49 | {
50 | return $this->potentialAction;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Model/WorkflowDTO.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | class WorkflowDTO
20 | {
21 | /**
22 | * @var string
23 | */
24 | protected $transition;
25 |
26 | /**
27 | * @return string
28 | */
29 | public function getTransition(): string
30 | {
31 | return $this->transition;
32 | }
33 |
34 | /**
35 | * @param string $transition
36 | */
37 | public function setTransition(string $transition): void
38 | {
39 | $this->transition = $transition;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Resources/config/api_patch.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/Resources/config/workflow.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Resources/config/workflow_validation.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Resources/meta/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Wesley O. Nichols
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Serializer/ActionsDocumentationNormalizer.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class ActionsDocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
24 | {
25 | public const FORMAT = 'jsonld';
26 |
27 | private $decorated;
28 |
29 | public function __construct(DocumentationNormalizer $decorated)
30 | {
31 | $this->decorated = $decorated;
32 | }
33 |
34 | public function hasCacheableSupportsMethod(): bool
35 | {
36 | return true;
37 | }
38 |
39 | public function normalize($object, $format = null, array $context = [])
40 | {
41 | // @TODO: review that this is even gets hydra to do anything extra
42 | $data = $this->decorated->normalize($object, $format, $context);
43 |
44 | // Add in our empty payload class
45 | $data['hydra:supportedClass'][] = [
46 | '@id' => '#WorkflowDTO',
47 | '@type' => 'hydra:Class',
48 | 'hydra:title' => 'WorkflowDTO',
49 | 'hydra:label' => 'WorkflowDTO',
50 | 'hydra:description' => 'Represents workflow name and transition.',
51 | ];
52 |
53 | return $data;
54 | }
55 |
56 | public function supportsNormalization($data, $format = null)
57 | {
58 | return $this->decorated->supportsNormalization($data, $format);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Serializer/WorkflowActionContextBuilder.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class WorkflowActionContextBuilder implements SerializerContextBuilderInterface
24 | {
25 | private $decorated;
26 |
27 | public function __construct(SerializerContextBuilderInterface $decorated)
28 | {
29 | $this->decorated = $decorated;
30 | }
31 |
32 | public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
33 | {
34 | $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
35 | $resourceClass = $context['resource_class'] ?? null;
36 |
37 | if (is_a($resourceClass, PotentialActionInterface::class, true)
38 | && isset($context['groups'])
39 | && true === $normalization
40 | ) {
41 | $context['groups'][] = 'workflowAction:output';
42 | }
43 |
44 | return $context;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Serializer/WorkflowActionNormalizer.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | class WorkflowActionNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
25 | {
26 | private $customNormalizer;
27 |
28 | public function __construct(NormalizerInterface $customNormalizer)
29 | {
30 | $this->customNormalizer = $customNormalizer;
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | public function supportsNormalization($data, $format = null): bool
37 | {
38 | return $data instanceof Action || $data instanceof EntryPoint;
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function hasCacheableSupportsMethod(): bool
45 | {
46 | return true;
47 | }
48 |
49 | public function normalize($object, $format = null, array $context = [])
50 | {
51 | if ($object instanceof Action) {
52 | $resourceClass = get_class($object);
53 | $resourceShortName = substr($resourceClass, strrpos($resourceClass, '\\') + 1);
54 |
55 | return [
56 | '@context' => 'http://schema.org',
57 | '@type' => $resourceShortName,
58 | ] + array_filter($this->customNormalizer->normalize($object, 'json'));
59 | } elseif ($object instanceof EntryPoint) {
60 | $data = array_filter($this->customNormalizer->normalize($object, 'json'));
61 | // EntryPoint can be represented as an object or a string in case only url property is present
62 | if (1 === count($data) && array_key_exists('url', $data)) {
63 | return $data['url'];
64 | }
65 |
66 | return $data;
67 | }
68 |
69 | return null;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Serializer/WorkflowNormalizer.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | class WorkflowNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface, ContextAwareDenormalizerInterface, SerializerAwareInterface
29 | {
30 | /**
31 | * @var ItemNormalizer
32 | */
33 | private $decorated;
34 | private $customNormalizer;
35 | private $workflowActions;
36 |
37 | public function __construct(
38 | NormalizerInterface $decorated,
39 | NormalizerInterface $customNormalizer,
40 | WorkflowActionGenerator $workflowActions
41 | ) {
42 | $this->decorated = $decorated;
43 | $this->customNormalizer = $customNormalizer;
44 | $this->workflowActions = $workflowActions;
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | public function supportsNormalization($data, $format = null): bool
51 | {
52 | return $this->decorated->supportsNormalization($data, $format);
53 | }
54 |
55 | /**
56 | * {@inheritdoc}
57 | */
58 | public function hasCacheableSupportsMethod(): bool
59 | {
60 | return true;
61 | }
62 |
63 | public function normalize($object, $format = null, array $context = [])
64 | {
65 | if ($object instanceof PotentialActionInterface) {
66 | $actions = $this->workflowActions->getActionsForSubject($object);
67 | foreach ($actions as $action) {
68 | $object->addPotentialAction($action);
69 | }
70 | }
71 |
72 | return $this->decorated->normalize($object, $format, $context);
73 | }
74 |
75 | public function supportsDenormalization($data, $type, $format = null, array $context = [])
76 | {
77 | return $this->decorated->supportsDenormalization($data, $type, $format, $context);
78 | }
79 |
80 | public function denormalize($data, $class, $format = null, array $context = [])
81 | {
82 | return $this->decorated->denormalize($data, $class, $format, $context);
83 | }
84 |
85 | public function setSerializer(SerializerInterface $serializer)
86 | {
87 | $this->decorated->setSerializer($serializer);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Transformer/WorkflowDtoTransformer.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class WorkflowDtoTransformer implements DataTransformerInterface
23 | {
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function transform($data, string $to, array $context = [])
28 | {
29 | return $data;
30 | }
31 |
32 | /**
33 | * {@inheritdoc}
34 | */
35 | public function supportsTransformation($data, string $to, array $context = []): bool
36 | {
37 | if (is_object($data)) {
38 | return false;
39 | }
40 |
41 | return WorkflowDTO::class === ($context['input']['class'] ?? null);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Validation/ChainedWorkflowValidationStrategy.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class ChainedWorkflowValidationStrategy implements WorkflowValidationStrategyInterface
23 | {
24 | /**
25 | * @var WorkflowValidationStrategyInterface[]
26 | */
27 | private $strategies;
28 |
29 | /**
30 | * ChainedWorkflowValidationStrategy constructor.
31 | *
32 | * @param array $strategies
33 | */
34 | public function __construct(array $strategies)
35 | {
36 | $this->strategies = $strategies;
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function getValidationGroupsForSubject($subject, Workflow $workflow, Transition $transition): array
43 | {
44 | $groups = [];
45 |
46 | foreach ($this->strategies as $strategy) {
47 | $groups = array_merge($groups, $strategy->getValidationGroupsForSubject($subject, $workflow, $transition));
48 | }
49 |
50 | return array_unique($groups);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Validation/ValidationStateProviderInterface.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | interface ValidationStateProviderInterface
20 | {
21 | /**
22 | * Return validation groups for this subject based on a proposed state property.
23 | *
24 | * This method should not alter the state of the subject.
25 | *
26 | * @param string $state
27 | * @param string $workflowName
28 | *
29 | * @return array
30 | */
31 | public function getGroupSequenceForState(string $state, string $workflowName): array;
32 | }
33 |
--------------------------------------------------------------------------------
/src/Validation/ValidationStateProviderStrategy.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class ValidationStateProviderStrategy implements WorkflowValidationStrategyInterface
23 | {
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function getValidationGroupsForSubject($subject, Workflow $workflow, Transition $transition): array
28 | {
29 | $groups = [];
30 |
31 | if ($subject instanceof ValidationStateProviderInterface) {
32 | foreach ($transition->getTos() as $state) {
33 | $groups = array_merge($groups, $subject->getGroupSequenceForState($state, $workflow->getName()));
34 | }
35 | }
36 |
37 | return $groups;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Validation/WorkflowValidationStrategy.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class WorkflowValidationStrategy implements WorkflowValidationStrategyInterface
23 | {
24 | public function getValidationGroupsForSubject($subject, Workflow $workflow, Transition $transition): array
25 | {
26 | $groups = array_map(function ($state) use ($workflow) {
27 | return $workflow->getName().'_'.$state;
28 | }, $transition->getTos());
29 |
30 | array_unshift($groups, 'Default', $workflow->getName());
31 |
32 | return $groups;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Validation/WorkflowValidationStrategyInterface.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | interface WorkflowValidationStrategyInterface
23 | {
24 | /**
25 | * Return the validation groups that should be used to validate a subject for a given transition in a given workflow.
26 | *
27 | * @param object $subject
28 | * @param Workflow $workflow
29 | * @param Transition $transition
30 | *
31 | * @return array
32 | */
33 | public function getValidationGroupsForSubject($subject, Workflow $workflow, Transition $transition): array;
34 | }
35 |
--------------------------------------------------------------------------------
/src/WesnickWorkflowBundle.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new WorkflowPass());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/WorkflowActionGenerator.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | class WorkflowActionGenerator
28 | {
29 | private $registry;
30 | private $iriConverter;
31 |
32 | /**
33 | * Classmap.
34 | *
35 | * [ className => [workflowName]]
36 | *
37 | * @var array
38 | */
39 | private $enabledWorkflowMap;
40 |
41 | /**
42 | * WorkflowActionGenerator constructor.
43 | *
44 | * @param $registry
45 | * @param $iriConverter
46 | * @param array $enabledWorkflowMap
47 | */
48 | public function __construct(Registry $registry, IriConverterInterface $iriConverter, array $enabledWorkflowMap)
49 | {
50 | $this->registry = $registry;
51 | $this->iriConverter = $iriConverter;
52 | $this->enabledWorkflowMap = $enabledWorkflowMap;
53 | }
54 |
55 | public function getActionsForSubject($subject)
56 | {
57 | $workflows = $this->registry->all($subject);
58 | $actions = [];
59 |
60 | foreach ($workflows as $workflow) {
61 |
62 | $possibleTransitions = [];
63 | $marking = $workflow->getMarking($subject);
64 |
65 | foreach ($workflow->getDefinition()->getTransitions() as $transition) {
66 | $froms = array_flip($transition->getFroms());
67 | $intersection = array_intersect_key($froms, $marking->getPlaces());
68 | if ($intersection) {
69 | $possibleTransitions[] = $transition;
70 | }
71 | }
72 |
73 | foreach ($possibleTransitions as $transition) {
74 | $transitionBlockerList = $workflow->buildTransitionBlockerList($subject, $transition->getName());
75 |
76 | $transitionMeta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
77 |
78 | $url = sprintf(
79 | '%s?%s',
80 | $this->iriConverter->getIriFromItem($subject, UrlGeneratorInterface::ABS_URL),
81 | http_build_query([
82 | 'workflow' => $workflow->getName(),
83 | 'transition' => $transition->getName(),
84 | ])
85 | );
86 |
87 | $entryPoint = new EntryPoint();
88 | $entryPoint->setUrl($url);
89 | $entryPoint->setHttpMethod('PATCH');
90 |
91 | $currentAction = new Action();
92 | $currentAction->setTarget($entryPoint);
93 | $currentAction->setName($transition->getName());
94 | $currentAction->setDescription($transitionMeta['description'] ?? ucfirst($transition->getName()).' Action');
95 | // @TODO: add sub status (available, unavailable, access denied, invalid)
96 |
97 | foreach ($transitionBlockerList as $blocker) {
98 | $parameters = $blocker->getParameters();
99 |
100 | if (array_key_exists('original_violation', $parameters)) {
101 | $violation = $parameters['original_violation'];
102 | } else {
103 | // @TODO: add a factory or event for building Violations from TransitionBlockers
104 | $violation = new ConstraintViolation(
105 | $blocker->getMessage(),
106 | $blocker->getMessage(),
107 | $blocker->getParameters(),
108 | $subject,
109 | '/',
110 | ''
111 | );
112 | }
113 |
114 | $currentAction->addError($violation);
115 | }
116 |
117 | $actions[] = $currentAction;
118 | }
119 | }
120 |
121 | return $actions;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/tests/Controller/DefaultTransitionControllerTest.php:
--------------------------------------------------------------------------------
1 | createComplexWorkflowDefinition();
34 | $workflow = new Workflow($definition, new MultipleStateMarkingStore());
35 |
36 | $registry = $this->createRegistry($workflow);
37 | $controller = new DefaultTransitionController($registry->reveal());
38 |
39 | $this->expectException(BadRequestHttpException::class);
40 | $this->expectExceptionMessage('Expected object for workflow "workflow", got array');
41 | $controller(new WorkflowDTO(), [], 'workflow', 'transition');
42 | }
43 |
44 | /**
45 | * @dataProvider workflowProvider
46 | */
47 | public function test__invoke($workflow, $subject, $transition, bool $success)
48 | {
49 | $registry = $this->createRegistry($workflow, $success);
50 | $controller = new DefaultTransitionController($registry->reveal());
51 |
52 | if (!$success) {
53 | $this->expectException(BadRequestHttpException::class);
54 | $this->expectExceptionMessage(sprintf('Transition "%s" in Workflow "workflow" is not available.', $transition));
55 | }
56 |
57 | $result = $controller(new WorkflowDTO(), $subject, 'workflow', $transition);
58 |
59 | $this->assertSame($subject, $result);
60 | $this->assertFalse($workflow->can($subject, $transition));
61 | }
62 |
63 | public function workflowProvider()
64 | {
65 | $definition = $this->createComplexWorkflowDefinition();
66 | $workflow = new Workflow($definition, new MultipleStateMarkingStore());
67 |
68 | $subject = new \stdClass();
69 | $subject->marking = null;
70 | yield [$workflow, $subject, 't1', true];
71 |
72 | $subject = new \stdClass();
73 | $subject->marking = null;
74 | yield [$workflow, $subject, 't2', false];
75 |
76 | $subject = new \stdClass();
77 | $subject->marking = ['b' => 1];
78 | yield [$workflow, $subject, 't1', false];
79 |
80 | $subject = new \stdClass();
81 | $subject->marking = ['b' => 1];
82 | yield [$workflow, $subject, 't2', false];
83 |
84 | $subject = new \stdClass();
85 | $subject->marking = ['b' => 1, 'c' => 1];
86 | yield [$workflow, $subject, 't1', false];
87 |
88 | $subject = new \stdClass();
89 | $subject->marking = ['b' => 1, 'c' => 1];
90 | yield [$workflow, $subject, 't2', true];
91 |
92 | $subject = new \stdClass();
93 | $subject->marking = ['f' => 1];
94 | yield [$workflow, $subject, 't5', false];
95 |
96 | $subject = new \stdClass();
97 | $subject->marking = ['f' => 1];
98 | yield [$workflow, $subject, 't6', true];
99 | }
100 |
101 | private function createRegistry(Workflow $workflow)
102 | {
103 | $registry = $this->prophesize(Registry::class);
104 | $registry->get(Argument::type(\stdClass::class), Argument::type('string'))
105 | ->willReturn($workflow)
106 | ;
107 |
108 | return $registry;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/tests/EventListener/WorkflowOperationListenerTest.php:
--------------------------------------------------------------------------------
1 | [
24 | 'workflow',
25 | 'another_workflow',
26 | ],
27 | ];
28 |
29 | /**
30 | * @dataProvider eventProvider
31 | */
32 | public function testOnKernelRequest($method, $data, $workflowName, $transitionName, $query, $body, $hasParams)
33 | {
34 | $eventProphecy = $this->prophesize(GetResponseEvent::class);
35 |
36 | $request = new Request($query, [], ['data' => $data, '_api_resource_class' => ArticleWithWorkflow::class, '_api_item_operation_name' => strtolower($method)], [], [], [], $body);
37 | $request->setMethod($method);
38 | $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
39 |
40 | $listener = new WorkflowOperationListener(self::$classmap);
41 | $listener->onKernelRequest($eventProphecy->reveal());
42 |
43 | if ($hasParams) {
44 | $this->assertSame($request->attributes->get('subject'), $data);
45 | $this->assertSame($request->attributes->get('workflowName'), $workflowName);
46 | $this->assertSame($request->attributes->get('transitionName'), $transitionName);
47 | } else {
48 | $this->assertFalse($request->attributes->has('subject'));
49 | $this->assertFalse($request->attributes->has('workflowName'));
50 | $this->assertFalse($request->attributes->has('transitionName'));
51 | }
52 | }
53 |
54 | public function eventProvider()
55 | {
56 | yield ['GET', new \stdClass(), 'workflow', 'transition', ['workflow' => 'workflow'], json_encode(['transition' => 'transition']), false];
57 | yield ['POST', new \stdClass(), 'workflow', 'transition', [], [], false];
58 | yield ['PATCH', new ArticleWithWorkflow(), 'workflow', 'transition', ['workflow' => 'workflow'], json_encode(['transition' => 'transition']), true];
59 | yield ['PATCH', new ArticleWithWorkflow(), 'workflow', '', ['workflow' => 'workflow'], json_encode(['xxx' => 'transition']), true];
60 | yield ['PATCH', new ArticleWithWorkflow(), 'non-existing-workflow-is-ok', '', ['workflow' => 'non-existing-workflow-is-ok'], json_encode(['xxx' => 'transition']), true];
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/Fixtures/ArticleWithWorkflow.php:
--------------------------------------------------------------------------------
1 |
22 | * @ApiResource()
23 | */
24 | class ArticleWithWorkflow implements PotentialActionInterface
25 | {
26 | use PotentialActionsTrait;
27 | }
28 |
--------------------------------------------------------------------------------
/tests/Fixtures/StateProviderWithWorkflow.php:
--------------------------------------------------------------------------------
1 |
23 | * @ApiResource()
24 | */
25 | class StateProviderWithWorkflow implements ValidationStateProviderInterface
26 | {
27 | private $discriminator;
28 |
29 | /**
30 | * StateProviderWithWorkflow constructor.
31 | * @param string $discriminator
32 | */
33 | public function __construct(string $discriminator)
34 | {
35 | $this->discriminator = $discriminator;
36 | }
37 |
38 |
39 | public function getGroupSequenceForState(string $state, string $workflowName): array
40 | {
41 | return [
42 | sprintf('%s_%s', $state, $this->discriminator)
43 | ];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Metadata/WorkflowActionsResourceMetadataFactoryTest.php:
--------------------------------------------------------------------------------
1 | prophesize(ResourceMetadataFactoryInterface::class);
33 | $decoratedProphecy->create(ArticleWithWorkflow::class)->shouldBeCalled()->willReturn($before);
34 | $this->assertEquals($after, (new WorkflowActionsResourceMetadataFactory($decoratedProphecy->reveal()))->create(ArticleWithWorkflow::class));
35 | }
36 |
37 | public function getMetadata()
38 | {
39 | $operations = ['patch' => [
40 | 'method' => 'PATCH',
41 | 'controller' => DefaultTransitionController::class,
42 | 'input' => ['class' => WorkflowDTO::class, 'name' => 'WorkflowDTO'],
43 | ]];
44 | $attributes = ['denormalization_context' => ['groups' => ['workflowAction:output']]];
45 |
46 | return [
47 | // Item operations
48 | [
49 | new ResourceMetadata(null, null, null, null, [], null, [], []),
50 | new ResourceMetadata(null, null, null, $operations, [], $attributes, [], []),
51 | ],
52 | [
53 | new ResourceMetadata(null, null, null, ['patch' => []], [], null, [], []),
54 | new ResourceMetadata(null, null, null, $operations, [], $attributes, [], []),
55 | ],
56 | [
57 | new ResourceMetadata(null, null, null, [], [], ['denormalization_context' => ['groups' => ['workflowAction:output']]], [], []),
58 | new ResourceMetadata(null, null, null, $operations, [], $attributes, [], []),
59 | ],
60 | ];
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/Validation/ChainedWorkflowValidationStrategyTest.php:
--------------------------------------------------------------------------------
1 | getValidationGroupsForSubject($subject, $workflow, $transition);
31 | $this->assertEquals($result, $groups);
32 | }
33 |
34 | public function validationSubjectProvider()
35 | {
36 | $definition = $this->createComplexWorkflowDefinition();
37 | $transitions = $definition->getTransitions();
38 | $getTransitionByName = function ($name) use ($transitions) {
39 | foreach ($transitions as $transition) {
40 | if ($transition->getName() === $name) {
41 | return $transition;
42 | }
43 | }
44 | };
45 | $workflow = new Workflow($definition, new MultipleStateMarkingStore());
46 |
47 | yield [new ArticleWithWorkflow(), $workflow, $getTransitionByName('t1'), ['Default', 'unnamed', 'unnamed_b', 'unnamed_c']];
48 | yield [new ArticleWithWorkflow(), $workflow, $getTransitionByName('t2'), ['Default', 'unnamed', 'unnamed_d']];
49 | yield [new StateProviderWithWorkflow('blue'), $workflow, $getTransitionByName('t1'), ['Default', 'unnamed', 'unnamed_b', 'unnamed_c', 'b_blue', 'c_blue']];
50 | yield [new StateProviderWithWorkflow('red'), $workflow, $getTransitionByName('t1'), ['Default', 'unnamed', 'unnamed_b', 'unnamed_c', 'b_red', 'c_red']];
51 | yield [new StateProviderWithWorkflow('red'), $workflow, $getTransitionByName('t2'), ['Default', 'unnamed', 'unnamed_d', 'd_red']];
52 | yield [new StateProviderWithWorkflow('green'), $workflow, $getTransitionByName('t2'), ['Default', 'unnamed', 'unnamed_d', 'd_green']];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Validation/ValidationStateProviderStrategyTest.php:
--------------------------------------------------------------------------------
1 | getValidationGroupsForSubject($subject, $workflow, $transition);
24 | $this->assertEquals($result, $groups);
25 | }
26 |
27 | public function validationSubjectProvider()
28 | {
29 | $definition = $this->createComplexWorkflowDefinition();
30 | $transitions = $definition->getTransitions();
31 | $getTransitionByName = function ($name) use ($transitions) {
32 | foreach ($transitions as $transition) {
33 | if ($transition->getName() === $name) {
34 | return $transition;
35 | }
36 | }
37 | };
38 | $workflow = new Workflow($definition, new MultipleStateMarkingStore());
39 |
40 | yield [new \stdClass(), $workflow, $getTransitionByName('t1'), []];
41 | yield [new StateProviderWithWorkflow('blue'), $workflow, $getTransitionByName('t1'), ['b_blue', 'c_blue']];
42 | yield [new StateProviderWithWorkflow('red'), $workflow, $getTransitionByName('t1'), ['b_red', 'c_red']];
43 | yield [new StateProviderWithWorkflow('red'), $workflow, $getTransitionByName('t2'), ['d_red']];
44 | yield [new StateProviderWithWorkflow('green'), $workflow, $getTransitionByName('t2'), ['d_green']];
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Validation/WorkflowValidationStrategyTest.php:
--------------------------------------------------------------------------------
1 | getValidationGroupsForSubject($subject, $workflow, $transition);
24 | $this->assertEquals($result, $groups);
25 | }
26 |
27 | public function validationSubjectProvider()
28 | {
29 | $definition = $this->createComplexWorkflowDefinition();
30 | $transitions = $definition->getTransitions();
31 | $getTransitionByName = function ($name) use ($transitions) {
32 | foreach ($transitions as $transition) {
33 | if ($transition->getName() === $name) {
34 | return $transition;
35 | }
36 | }
37 | };
38 | $workflow = new Workflow($definition, new MultipleStateMarkingStore());
39 |
40 | yield [new ArticleWithWorkflow(), $workflow, $getTransitionByName('t1'), ['Default', 'unnamed', 'unnamed_b', 'unnamed_c']];
41 | yield [new ArticleWithWorkflow(), $workflow, $getTransitionByName('t2'), ['Default', 'unnamed', 'unnamed_d']];
42 | }
43 | }
44 |
--------------------------------------------------------------------------------