├── .coveralls.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── UPGRADE-3.0.md
├── composer.json
├── phpunit.xml.dist
├── src
├── Action
│ ├── Executor.php
│ ├── ExpressionLanguage
│ │ └── ActionExpressionLanguage.php
│ ├── Reference
│ │ ├── ActionReferenceInterface.php
│ │ ├── CallableMethod.php
│ │ ├── ContainerAwareInterface.php
│ │ └── ContainerAwareTrait.php
│ ├── Registry.php
│ └── ValueObject
│ │ └── Action.php
├── Actions
│ └── TransitionApplier.php
├── Command
│ └── ExecuteActionCommand.php
├── DependencyInjection
│ ├── Compiler
│ │ └── CheckSubjectManipulatorConfigPass.php
│ ├── Configuration.php
│ ├── Enum
│ │ └── ActionArgumentTypes.php
│ └── WorkflowExtensionsExtension.php
├── Entity
│ ├── Repository
│ │ └── ScheduledJobRepository.php
│ └── ScheduledJob.php
├── Exception
│ ├── ActionException.php
│ ├── InvalidArgumentException.php
│ ├── NonUniqueReschedulabeJobFoundException.php
│ ├── RuntimeException.php
│ ├── SubjectIdRetrievingException.php
│ ├── SubjectManipulatorException.php
│ ├── SubjectRetrievingFromDomainException.php
│ ├── UnsupportedGuardEventException.php
│ ├── UnsupportedTriggerEventException.php
│ └── WorkflowExceptionInterface.php
├── ExpressionLanguage
│ └── ContainerAwareExpressionLanguage.php
├── Guard
│ └── ExpressionGuard.php
├── MarkingStore
│ └── OrmPersistentMarkingStore.php
├── Resources
│ └── config
│ │ ├── actions.xml
│ │ ├── guard.xml
│ │ ├── scheduler.xml
│ │ ├── subject_manipulator.xml
│ │ └── triggers.xml
├── Schedule
│ ├── ActionScheduler.php
│ └── ValueObject
│ │ └── ScheduledAction.php
├── Trigger
│ └── Event
│ │ ├── AbstractActionListener.php
│ │ ├── AbstractListener.php
│ │ ├── ActionListener.php
│ │ ├── ExpressionListener.php
│ │ └── SchedulerListener.php
├── Utils
│ └── ArrayUtils.php
├── WorkflowContext.php
├── WorkflowExtensionsBundle.php
└── WorkflowSubject
│ └── SubjectManipulator.php
└── tests
├── Action
└── ExecutorTest.php
├── Actions
└── TransitionApplierTest.php
├── DependencyInjection
├── Compiler
│ └── CheckSubjectManipulatorConfigPassTest.php
└── ConfigurationTest.php
├── Functional
├── Configuration
│ ├── BaseConfig
│ │ ├── config.yml
│ │ └── routing.yml
│ ├── EventTriggerWithGuardsCase
│ │ ├── Fixtures
│ │ │ ├── Event.php
│ │ │ └── TargetWorkflowSubject.php
│ │ ├── bundles.php
│ │ └── config.yml
│ └── ScheduleCase
│ │ ├── Fixtures
│ │ ├── ClientBundle
│ │ │ ├── ClientBundle.php
│ │ │ ├── DataFixtures
│ │ │ │ └── ORM
│ │ │ │ │ └── LoadData.php
│ │ │ └── Entity
│ │ │ │ └── Client.php
│ │ └── Event.php
│ │ ├── bundles.php
│ │ ├── config.yml
│ │ └── console
├── EventTriggerWithGuardsCaseTest.php
├── Kernel
│ ├── BaseTestKernel.php
│ ├── KernelBuilder.php
│ └── TestKernelInterface.php
├── ScheduleCaseTest.php
└── TestCase.php
├── Guard
└── ExpressionGuardTest.php
├── Schedule
└── ActionSchedulerTest.php
├── Trigger
└── Event
│ ├── AbstractActionListenerTest.php
│ └── AbstractListenerTest.php
└── bootstrap.php
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | coverage_clover: build/logs/clover.xml
2 | json_path: coveralls-upload.json
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 | .phpunit.result.cache
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - '7.2'
5 | - '7.3'
6 | - nightly
7 |
8 | matrix:
9 | allow_failures:
10 | - php: nightly
11 |
12 | before_install:
13 | - echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
14 |
15 | before_script:
16 | - composer self-update
17 | - composer global --no-suggest require hirak/prestissimo
18 | - composer require satooshi/php-coveralls -n
19 |
20 | install:
21 | - composer install --no-suggest --prefer-dist
22 |
23 | script:
24 | - mkdir -p build/logs
25 | - vendor/bin/phpunit --coverage-clover build/logs/clover.xml
26 |
27 | after_script:
28 | - php vendor/bin/coveralls -v
29 |
30 | git:
31 | depth: 5
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 GlobalTradingTechnologies
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | WorkflowExtensionsBundle
2 | ========================
3 |
4 | [](https://travis-ci.org/GlobalTradingTechnologies/workflow-extensions-bundle)
5 | [](https://coveralls.io/github/GlobalTradingTechnologies/workflow-extensions-bundle?branch=master)
6 | [](https://scrutinizer-ci.com/g/GlobalTradingTechnologies/workflow-extensions-bundle/?branch=master)
7 | [](https://packagist.org/packages/gtt/workflow-extensions-bundle)
8 | [](//packagist.org/packages/gtt/workflow-extensions-bundle)
9 | [](https://packagist.org/packages/gtt/workflow-extensions-bundle)
10 |
11 | Original Symfony 3 [Workflow component](https://github.com/symfony/workflow) and the [part of Symfony's FrameworkBundle](https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L359-L398) that
12 | integrates it in Symfony's ecosystem state explicit declaring all the things as a main concept.
13 | This means that if you need to apply workflow transition you must find target workflow object and call ```$workflow->apply()``` inside your business logic code.
14 | Another example: if you want to block transition you must create a listener that subscribes to GuardEvent and decide whether to block or
15 | allow transition inside.
16 |
17 | But sometimes workflow processes are complex and the code handles transition management grows quickly.
18 |
19 | WorkflowExtensionsBundle provides extensions and additional features to original Symfony 3 Workflow component that can help you to automate some actions you must do manually when you deal with workflow component out of the box:
20 |
21 | 1. [Event-based transitions triggering](#event-based-transitions-processing)
22 |
23 | 2. [Event-based transitions scheduling](#event-based-transitions-triggering)
24 |
25 | 3. [Configurable transition blocking](#event-based-transitions-scheduling)
26 |
27 | Requirements
28 | ============
29 |
30 | Since Symfony's Workflow component requires PHP 5.5.9+ WorkflowExtensionsBundle supports PHP 5.5.9 and newer.
31 |
32 | Workflow component is integrated in Symfony 3 ecosystem starting from 3.2 version. In order to use it in applications based on Symfony 3.1 and lower you can use [1.x version](https://github.com/GlobalTradingTechnologies/workflow-extensions-bundle/tree/1.x) of the Bundle.
33 |
34 | Besides [symfony/framework-bundle](https://github.com/symfony/framework-bundle) and [symfony/expression-language](https://github.com/symfony/expression-language) packages are required.
35 |
36 |
37 | Installation
38 | ============
39 |
40 | Bundle should be installed via composer
41 |
42 | ```
43 | composer require gtt/workflow-extensions-bundle
44 | ```
45 | After that you need to register the bundle inside your application kernel:
46 | ```php
47 | public function registerBundles()
48 | {
49 | $bundles = array(
50 | // ...
51 | new \Gtt\Bundle\WorkflowExtensionsBundle\WorkflowExtensionsBundle(),
52 | );
53 | }
54 | ```
55 |
56 | Configuration and Usage
57 | =======================
58 |
59 | ## Workflow subjects
60 | First of all you need to tell WorkflowExtensionsBundle what kind of workflow subjects you want it to deal with.
61 | List it inside `subject_manipulator` section.
62 | ```yml
63 | workflow_extensions:
64 | subject_manipulator:
65 | My\Bundle\Entity\Order: ~
66 | My\Bundle\Entity\Claim: ~
67 | ```
68 |
69 | ## Logging
70 | Since all the things WorkflowExtensionsBundle does are basically automated (and even asynchronous) it is reasonable
71 | to log important aspects in details. All the WorkflowExtensionsBundle subsystems log (when it is possible) workflow name, subject class and subject id during execution.
72 |
73 | There is one non-trivial thing here: how to retrieve subject id from subject. More often subject id can be fetched by invoking `getId()` method, - in this case you have nothing to do.
74 | Otherwise (when your subject class has no `getId()` method or there is the other one should be used to get subject's identifier) you need to specify expression to get subject identifier. This expression will be evaluated by [ExpressionLanguage](https://github.com/symfony/expression-language) component with `subject` variable that represents subject object:
75 | ```yaml
76 | workflow_extensions:
77 | subject_manipulator:
78 | My\Bundle\Entity\Order: ~
79 | My\Bundle\Entity\Claim:
80 | id_from_subject: 'subject.getCustomId()'
81 | ```
82 | ## Event-based transitions processing
83 | One of the most important use cases of WorkflowExtensionsBundle is to execute some workflow manipulations as a reaction to the particular system events. Any [Symfony's Event](https://github.com/symfony/event-dispatcher/blob/3.1/Event.php) instance can play the role of such firing event.
84 | In order to subscribe workflow processing to such an event you should start with config like this:
85 | ```yaml
86 | workflow_extensions:
87 | workflows:
88 | simple:
89 | triggers:
90 | event:
91 | some.event:
92 | ...
93 | another.event:
94 | ...
95 | complex:
96 | triggers:
97 | event:
98 | some.event:
99 | ...
100 | third.event:
101 | ...
102 | ...
103 | ```
104 | This config firstly specifies target workflow name (`simple`) that should be equal to one of defined workflows in symfony/framework-bundle or fduch/workflow-bundle config.
105 | For each workflow then you define target event and configure processing details as described in sections below.
106 |
107 | ### Event-based transitions triggering
108 | WorkflowExtensionsBundle makes possible to trigger workflow transitions when particular event is fired.
109 | For example if you want to trigger transition `to_processing` when workflow subject (My\Bundle\Entity\Order instance) is created (order_created.event is fired) the WorkflowExtensionsBundle's config can look like this:
110 | ```yaml
111 | workflow_extensions:
112 | workflows:
113 | simple:
114 | triggers:
115 | event:
116 | order_created.event:
117 | actions:
118 | apply_transition:
119 | - [to_processing]
120 | subject_retrieving_expression: 'event.getOrder()'
121 | subject_manipulator:
122 | My\Bundle\Entity\Order: ~
123 | ```
124 | In example above `subject_retrieving_expression` section contains expression (it will be evaluated by [ExpressionLanguage](https://github.com/symfony/expression-language)) used to retrieve workflow subject.
125 | Since expression language that evaluates these expressions has container variable (represents DI Container) enabled you can construct more complicated things for example like this here: ```"container.get('doctrine').getEntityMangerForClass('My\\\\Bundle\\\\Entity\\\\Order').find('My\\\\Bundle\\\\Entity\\\\Order', event.getId())"``` (Lot of backslashes is set due to specialty of [expression language syntax](http://symfony.com/doc/current/components/expression_language/syntax.html)).
126 |
127 | You can also specify more then one transition to be tried to perform when event is fired by using `apply_transitions` construction like this:
128 | ```yaml
129 | apply_transitions:
130 | - [[to_processing, closing]]
131 | ```
132 | In this case (by default) the first applicable transition would be applied.
133 | You can also consequentially try to apply several transitions without breaking execution after first successfully applied transition using ability to
134 | invoke `apply_transition` action several times in line with different arguments:
135 | ```yaml
136 | apply_transition:
137 | - [to_processing]
138 | - [closing]
139 | ```
140 |
141 | ### Event-based transitions scheduling
142 | Events can be used not only to immediately apply transitions, - you can also schedule it with specified offset.
143 | WorkflowExtensionsBundle uses [jms/job-queue-bundle](https://packagist.org/packages/jms/job-queue-bundle) as a scheduler engine.
144 | Imagine you need to apply transition `set_problematic` that places workflow subject `Order` into state "Problematic" if it is not correctly processed in 30 days.
145 | Such goal can be achieved using config like this:
146 |
147 | ```yaml
148 | workflow_extensions:
149 | workflows:
150 | simple:
151 | triggers:
152 | event:
153 | order_created.event:
154 | schedule:
155 | apply_transition:
156 | -
157 | arguments: [closing]
158 | offset: P30D
159 | subject_retrieving_expression: 'event.getOrder()'
160 | scheduler: ~
161 | subject_manipulator:
162 | My\Bundle\Entity\Order:
163 | subject_from_domain: "container.get('doctrine').getManagerForClass(subjectClass).find(subjectClass, subjectId)"
164 | context:
165 | doctrine: ~
166 | ```
167 | Configuration above is similar to previous one with several differences.
168 |
169 | The first difference is that `actions` is replaced with `schedule` key to tell the engine that actions below should be executed deferred.
170 |
171 | The second difference is each action's arguments are defined now under explicit `arguments` key (which is automatically set under the hood for [simple triggering](#event-based-transitions-triggering) thanks to [configuration normalization rules](http://symfony.com/doc/current/components/config/definition.html#normalization) and also can be set explicitly there) and
172 | `offset` key that defines time interval (according to [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601#Durations)) started from the moment when corresponding trigger event occurred and after that scheduled transition should be applied.
173 |
174 | The third difference is that you need to configure `scheduler` section to activate scheduler engine. Also it can be used to set particular entity manager to persist scheduler jobs.
175 |
176 | The fourth difference is that you must configure under `subject_manipulator`'s `subject_from_domain` key expression (it will be evaluated by [ExpressionLanguage](https://github.com/symfony/expression-language)) that will be used to retrieve workflow subject when scheduled transition will be tried to be applied.
177 | The subjectClass (for example My\Bundle\Entity\Order) and subjectId (i.e. identifier you can use to fetch the object) are the expression variables here. Moreover you can use DI container here again since it also registered as expression variable.
178 |
179 | Another feature here is that if you have frequent repeatable event that schedules transition then for the first time when event is fired transition would be simply scheduled and next event occurrences will just reset scheduler countdown to restart it from current moment.
180 | This behaviour can be very useful when you need continuously delay particular transition until specific event is fired regularly. You should not configure something specific to achieve this since this feature is enabled by default.
181 | ## Transition blocking
182 | Basically you can prevent transition from applying explicitly by listening special [GuardEvent](https://github.com/symfony/workflow/blob/master/Event/GuardEvent.php) and call its `setBlocked` method inside. With the help of WorkflowExtensionsBundle you can automate things again.
183 | For example if you to block all the transitions invoked by non-ROLE_USER users and allow only managers (ROLE_MANAGER holders) to apply `dangerous` transition you should use config like this:
184 | ```yaml
185 | workflow_extensions:
186 | workflows:
187 | simple:
188 | guard:
189 | expression: 'not container.get("access_checker").isGranted("ROLE_USER")'
190 | transitions:
191 | dangerous: 'not container.get("access_checker").isGranted("ROLE_MANAGER")'
192 | subject_manipulator:
193 | My\Bundle\Entity\Order: ~
194 | context:
195 | access_checker: ~
196 | ```
197 | Note that here again we use expression evaluated by [ExpressionLanguage](https://github.com/symfony/expression-language) with container variable represents DI Container allowing usage of public services to decide whether to block transitions or not.
198 |
199 | ## Contexts
200 | When expressions use some container service it is fetched from container using `container.get()` method. Since Symfony 4
201 | private services can not be fetched from container in such way. To access required service inside the expression the
202 | former must be explicitly exposed in bundle configuration. This is done inside `context` array in bundle
203 | configuration:
204 |
205 | ```yaml
206 | workflow_extensions:
207 | ...
208 | context:
209 | # This will expose "doctrine" service from DI under "doctrine" alias inside expression container
210 | doctrine: ~
211 |
212 | # This will expose "security.authorization_checker" from DI and make it available under
213 | # "auth_checker" alias inside expression container
214 | auth_checker: 'security.authorization_checker'
215 |
216 | workflows:
217 | simple:
218 | guard:
219 | expression: 'not container.get("auth_checker").isGranted("ROLE_USER")'
220 | transitions:
221 | dangerous: 'not container.get("auth_checker").isGranted("ROLE_MANAGER")'
222 | ```
223 |
224 | Tests
225 | =====
226 | WorkflowExtensionsBundle is covered by unit and functional tests. [Functional tests](https://github.com/GlobalTradingTechnologies/workflow-extensions-bundle/tree/master/Tests/Functional) can probably make more clear how the bundle works if you have some misunderstanding.
227 |
--------------------------------------------------------------------------------
/UPGRADE-3.0.md:
--------------------------------------------------------------------------------
1 | # Upgrade from 2.x to 3.0
2 |
3 | ## Moving to PSR-11 container
4 |
5 | Under the hood the bundle now uses PSR-11 container implementation for accessing services
6 | inside all expressions. If you implemented `Symfony\Component\DependencyInjection\ContainerAwareInterface`
7 | for actions, you should replace the implementation with `Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ContainerAwareInterface`
8 | and change type-hint to `Psr\Container\ContainerInterface`
9 |
10 | Before:
11 | ```php
12 | use Symfony\Component\DependencyInjection\ContainerAwareInterface;
13 | use Symfony\Component\DependencyInjection\ContainerInterface;
14 |
15 | class MyAction implements ContainerAwareInterface {
16 | public function setContainer(ContainerInterface $container) {
17 | // ...
18 | }
19 | }
20 | ```
21 |
22 | After:
23 | ```php
24 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ContainerAwareInterface;
25 | use Psr\Container\ContainerInterface;
26 |
27 | class MyAction implements ContainerAwareInterface {
28 | public function setContainer(ContainerInterface $container) {
29 | // ...
30 | }
31 | }
32 | ```
33 |
34 | If `Symfony\Component\DependencyInjection\ContainerAwareTrait` was used just replace the use to
35 | `Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ContainerAwareTrait`.
36 |
37 | Before:
38 | ```php
39 | use Symfony\Component\DependencyInjection\ContainerAwareInterface;
40 | use Symfony\Component\DependencyInjection\ContainerAwareTrait;
41 |
42 | class MyAction implements ContainerAwareInterface {
43 | use ContainerAwareTrait;
44 | }
45 | ```
46 |
47 | After:
48 | ```php
49 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ContainerAwareInterface;
50 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ContainerAwareTrait;
51 |
52 | class MyAction implements ContainerAwareInterface {
53 | use ContainerAwareTrait;
54 | }
55 | ```
56 |
57 | ## Explicitly exposed services
58 |
59 | Prior to symfony 4.0 any service could be fetched from DI container using `$container->get` method. This is
60 | no longer valid. To keep working expressions services must be explicitly defined in bundle configuration:
61 |
62 | Before:
63 | ```yaml
64 | subject_manipulator:
65 | Entity\Client:
66 | subject_from_domain: "container.get('doctrine') ..."
67 | ```
68 |
69 | After:
70 | ```yaml
71 | subject_manipulator:
72 | Entity\Client:
73 | subject_from_domain: "container.get('doctrine') ..."
74 | context:
75 | doctrine: ~
76 | ```
77 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gtt/workflow-extensions-bundle",
3 | "description": "Bundle for extended workflow management and automation",
4 | "type": "symfony-bundle",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Alex Medvedev (fduch)",
9 | "email": "alex.medwedew@gmail.com"
10 | }
11 | ],
12 | "require": {
13 | "php": "~7.2",
14 | "psr/container": "^1.0",
15 | "symfony/expression-language": "^4.0",
16 | "symfony/framework-bundle": "^4.0",
17 | "symfony/workflow": "^4.0"
18 | },
19 | "require-dev": {
20 | "doctrine/doctrine-bundle": "~1.0",
21 | "doctrine/doctrine-fixtures-bundle": "^2.2",
22 | "doctrine/orm": "^2.4.8",
23 | "jms/job-queue-bundle": "^2.0",
24 | "nesbot/carbon": "^1.21",
25 | "phpunit/phpunit": "~8.0",
26 | "symfony/browser-kit": "^4.0",
27 | "symfony/monolog-bundle": "^3.0",
28 | "symfony/property-access": "^4.0",
29 | "symfony/yaml": "^4.0"
30 | },
31 | "suggest": {
32 | "jms/job-queue-bundle": "Needed for workflow transitions scheduling",
33 | "symfony/console": "Needed for workflow transitions scheduling",
34 | "nesbot/carbon": "Needed for workflow transitions scheduling",
35 | "doctrine/doctrine-bundle": "Required for OrmPersistentMarkingStore usage"
36 | },
37 | "autoload": {
38 | "psr-4": { "Gtt\\Bundle\\WorkflowExtensionsBundle\\": "src" }
39 | },
40 | "autoload-dev": {
41 | "psr-4": { "Gtt\\Bundle\\WorkflowExtensionsBundle\\": "tests" }
42 | },
43 | "config": {
44 | "sort-packages": true
45 | },
46 | "extra": {
47 | "branch-alias": {
48 | "dev-master": "3.x-dev"
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | tests
7 |
8 |
9 |
10 |
11 |
12 | src
13 |
14 | ./Resources
15 | ./Tests
16 | ./vendor
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Action/Executor.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 23.09.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Action;
15 |
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ActionReferenceInterface;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ContainerAwareInterface;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
19 | use Psr\Container\ContainerInterface;
20 |
21 | /**
22 | * Action executor
23 | *
24 | * @author fduch
25 | */
26 | class Executor
27 | {
28 | /**
29 | * Action registry
30 | *
31 | * @var Registry
32 | */
33 | private $registry;
34 |
35 | /**
36 | * Container DI
37 | *
38 | * @var ContainerInterface
39 | */
40 | private $container;
41 |
42 | /**
43 | * Executor constructor.
44 | *
45 | * @param Registry $registry action registry
46 | * @param ContainerInterface $container container DI
47 | */
48 | public function __construct(Registry $registry, ContainerInterface $container)
49 | {
50 | $this->registry = $registry;
51 | $this->container = $container;
52 | }
53 |
54 | /**
55 | * Executes action
56 | *
57 | * @param WorkflowContext $workflowContext workflow context
58 | * @param string $actionName action name
59 | * @param array $args action arguments
60 | *
61 | * @return mixed
62 | */
63 | public function execute(WorkflowContext $workflowContext, string $actionName, array $args = [])
64 | {
65 | $actionReference = $this->registry->get($actionName);
66 |
67 | if ($actionReference->getType() === ActionReferenceInterface::TYPE_WORKFLOW) {
68 | array_unshift($args, $workflowContext);
69 | }
70 |
71 | if ($actionReference instanceof ContainerAwareInterface) {
72 | // container should be injected to allow action to use service as an method owner
73 | $actionReference->setContainer($this->container);
74 | }
75 |
76 | return $actionReference->invoke($args);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Action/ExpressionLanguage/ActionExpressionLanguage.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 01.09.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Action\ExpressionLanguage;
15 |
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Executor;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Registry;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\ExpressionLanguage\ContainerAwareExpressionLanguage;
19 | use Psr\Cache\CacheItemPoolInterface;
20 | use Psr\Container\ContainerInterface;
21 |
22 | /**
23 | * Expression language allows to use actions inside expressions
24 | *
25 | * @author fduch
26 | */
27 | class ActionExpressionLanguage extends ContainerAwareExpressionLanguage
28 | {
29 | /**
30 | * {@inheritdoc}
31 | */
32 | public function __construct(
33 | Registry $actionRegistry,
34 | Executor $actionExecutor,
35 | ContainerInterface $container,
36 | CacheItemPoolInterface $cache = null,
37 | array $providers = []
38 | ) {
39 | parent::__construct($container, $cache, $providers);
40 |
41 | foreach ($actionRegistry as $actionName => $action) {
42 | $this->register(
43 | $actionName,
44 | static function () use ($actionName, $actionExecutor)
45 | {
46 | $rawArgs = func_get_args();
47 | $compiledArgsArray = var_export($rawArgs, true);
48 |
49 | return sprintf(
50 | '$container->get("gtt.workflow.action.executor")->execute($workflowContext, "%s", %s)',
51 | $actionName,
52 | $compiledArgsArray
53 | );
54 | },
55 | static function () use ($actionName, $actionExecutor)
56 | {
57 | $args = func_get_args();
58 | $variables = array_shift($args);
59 |
60 | return $actionExecutor->execute($variables['workflowContext'], $actionName, $args);
61 | }
62 | );
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Action/Reference/ActionReferenceInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 14.09.16
11 | */
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference;
14 |
15 | /**
16 | * Reference to some method can be treated as an action
17 | *
18 | * @author fduch
19 | */
20 | interface ActionReferenceInterface
21 | {
22 | /**
23 | * Base default action type
24 | */
25 | public const TYPE_REGULAR = "regular";
26 |
27 | /**
28 | * Action type requires WorkflowContext instance as first argument in arguments list
29 | */
30 | public const TYPE_WORKFLOW = "workflow";
31 |
32 | /**
33 | * Returns action type
34 | *
35 | * For now can be regular or workflow
36 | *
37 | * @return string
38 | */
39 | public function getType(): string;
40 |
41 | /**
42 | * Invokes action
43 | *
44 | * @param array $args action args
45 | *
46 | * @return mixed
47 | */
48 | public function invoke(array $args);
49 | }
50 |
--------------------------------------------------------------------------------
/src/Action/Reference/CallableMethod.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 14.09.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference;
15 |
16 | /**
17 | * Reference to some callable
18 | *
19 | * @author fduch
20 | */
21 | class CallableMethod implements ActionReferenceInterface
22 | {
23 | /**
24 | * Action type
25 | *
26 | * @var string
27 | */
28 | private $type;
29 |
30 | /**
31 | * Callable which will be used as an action
32 | *
33 | * @var callable
34 | */
35 | private $method;
36 |
37 | /**
38 | * ActionReference constructor.
39 | *
40 | * @param callable $method Callable which will be used as an action
41 | * @param string $type action reference type
42 | */
43 | public function __construct(callable $method, string $type = self::TYPE_REGULAR)
44 | {
45 | $this->type = $type;
46 | $this->method = $method;
47 | }
48 |
49 | /**
50 | * Returns action type
51 | *
52 | * @return string
53 | */
54 | public function getType(): string
55 | {
56 | return $this->type;
57 | }
58 |
59 | /**
60 | * Invokes action
61 | *
62 | * @param array $args action args
63 | *
64 | * @return mixed
65 | */
66 | public function invoke(array $args)
67 | {
68 | $callable = $this->method;
69 |
70 | return $callable(...$args);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Action/Reference/ContainerAwareInterface.php:
--------------------------------------------------------------------------------
1 | container = $container;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Action/Registry.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 31.08.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Action;
15 |
16 | use ArrayIterator;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ActionReferenceInterface;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\ActionException;
19 | use IteratorAggregate;
20 |
21 | /**
22 | * Workflow action registry
23 | *
24 | * @author fduch
25 | */
26 | class Registry implements IteratorAggregate
27 | {
28 | /**
29 | * List of workflow actions
30 | *
31 | * @var ActionReferenceInterface[]
32 | */
33 | private $actions = [];
34 |
35 | /**
36 | * Registry constructor.
37 | *
38 | * @param array $actions list of action names associated to action references
39 | */
40 | public function __construct(array $actions = [])
41 | {
42 | foreach ($actions as $actionName => $actionReference) {
43 | $this->add($actionName, $actionReference);
44 | }
45 | }
46 |
47 | /**
48 | * Registers action by name in repository
49 | *
50 | * @param string $actionName action name
51 | * @param ActionReferenceInterface $action action reference
52 | */
53 | public function add($actionName, ActionReferenceInterface $action): void
54 | {
55 | if (array_key_exists($actionName, $this->actions)) {
56 | throw ActionException::actionAlreadyRegistered($actionName);
57 | }
58 |
59 | $this->actions[$actionName] = $action;
60 | }
61 |
62 | /**
63 | * Returns ActionInterface by name
64 | *
65 | * @param string $actionName action name
66 | *
67 | * @return ActionReferenceInterface
68 | */
69 | public function get(string $actionName): ActionReferenceInterface
70 | {
71 | if (!array_key_exists($actionName, $this->actions)) {
72 | throw ActionException::actionNotFound($actionName);
73 | }
74 |
75 | return $this->actions[$actionName];
76 | }
77 |
78 | public function getIterator()
79 | {
80 | return new ArrayIterator($this->actions);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Action/ValueObject/Action.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 20.09.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Action\ValueObject;
15 |
16 | /**
17 | * Data Value Object represents action name and action arguments
18 | *
19 | * @author fduch
20 | */
21 | class Action
22 | {
23 | /**
24 | * Name
25 | *
26 | * @var string
27 | */
28 | private $name;
29 |
30 | /**
31 | * Arguments
32 | *
33 | * @var array
34 | */
35 | private $arguments;
36 |
37 | /**
38 | * Action constructor
39 | *
40 | * @param string $name action name
41 | * @param array $arguments action arguments
42 | */
43 | public function __construct(string $name, array $arguments = [])
44 | {
45 | $this->name = $name;
46 | $this->arguments = $arguments;
47 | }
48 |
49 | /**
50 | * Returns action name
51 | *
52 | * @return string
53 | */
54 | public function getName(): string
55 | {
56 | return $this->name;
57 | }
58 |
59 | /**
60 | * Returns action arguments
61 | *
62 | * @return array
63 | */
64 | public function getArguments(): array
65 | {
66 | return $this->arguments;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Actions/TransitionApplier.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 03.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Actions;
14 |
15 | use Exception;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
17 | use Psr\Log\LoggerInterface;
18 | use Symfony\Component\Workflow\Exception\LogicException as WorkflowLogicException;
19 | use Throwable;
20 |
21 | /**
22 | * Applies workflow transitions
23 | */
24 | class TransitionApplier
25 | {
26 | /**
27 | * Logger
28 | *
29 | * @var LoggerInterface
30 | */
31 | private $logger;
32 |
33 | /**
34 | * TransitionApplier constructor.
35 | *
36 | * @param LoggerInterface $logger logger
37 | */
38 | public function __construct(LoggerInterface $logger)
39 | {
40 | $this->logger = $logger;
41 | }
42 |
43 | /**
44 | * Applies single transition
45 | *
46 | * @param WorkflowContext $workflowContext workflow context
47 | * @param string $transition transition to be applied
48 | */
49 | public function applyTransition(WorkflowContext $workflowContext, string $transition): void
50 | {
51 | $this->applyTransitions($workflowContext, [$transition]);
52 | }
53 |
54 | /**
55 | * Applies list of transitions
56 | *
57 | * @param WorkflowContext $workflowContext workflow context
58 | * @param array $transitions list of transitions to be applied
59 | * @param bool $cascade if this flag is set all the available transitions should be applied (it
60 | * may be cascade); otherwise the first applied transition breaks execution
61 | */
62 | public function applyTransitions(WorkflowContext $workflowContext, array $transitions = [], bool $cascade = false): void
63 | {
64 | if (!$transitions) {
65 | return;
66 | }
67 |
68 | $workflow = $workflowContext->getWorkflow();
69 | $subject = $workflowContext->getSubject();
70 | $loggerContext = $workflowContext->getLoggerContext();
71 |
72 | $this->logger->debug('Resolved workflow for subject', $loggerContext);
73 |
74 | $applied = false;
75 | foreach ($transitions as $transition) {
76 | try {
77 | // We do not call Workflow:can method here due to performance reasons in order to prevent
78 | // execution of all the doCan-listeners (guards) in case when transition can be applied.
79 | // Therefore we catch LogicException and interpreting it as case when transition can not be applied
80 | $workflow->apply($subject, $transition);
81 | $this->logger->info(sprintf('Workflow successfully applied transition "%s"', $transition), $loggerContext);
82 | $applied = true;
83 | if (!$cascade) {
84 | break;
85 | }
86 | } catch (WorkflowLogicException $e) {
87 | // transition cannot be applied because it is not allowed
88 | $this->logger->info(
89 | sprintf('Workflow transition "%s" cannot be applied due to it is not allowed', $transition),
90 | $loggerContext
91 | );
92 | } catch (Exception $e) {
93 | $this->logger->error(
94 | sprintf('Workflow cannot apply transition "%s" due to exception. Details: %s', $transition, $e->getMessage()),
95 | $loggerContext
96 | );
97 | throw $e;
98 | } catch (Throwable $e) {
99 | $this->logger->critical(
100 | sprintf('Workflow cannot apply transition "%s" due to error. Details: %s', $transition, $e->getMessage()),
101 | $loggerContext
102 | );
103 | throw $e;
104 | }
105 | }
106 |
107 | if (!$applied) {
108 | $this->logger->warning('All transitions to apply are not allowed', $loggerContext);
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Command/ExecuteActionCommand.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 28.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Command;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Executor as ActionExecutor;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
18 | use Symfony\Component\Console\Command\Command;
19 | use Symfony\Component\Console\Input\InputInterface;
20 | use Symfony\Component\Console\Input\InputOption;
21 | use Symfony\Component\Console\Output\OutputInterface;
22 | use Symfony\Component\Workflow\Registry as WorkflowRegistry;
23 |
24 | /**
25 | * Console command executes action
26 | *
27 | * Useful for scheduled execution
28 | */
29 | class ExecuteActionCommand extends Command
30 | {
31 | /**
32 | * Command name
33 | */
34 | protected static $defaultName = 'workflow:action:execute';
35 |
36 | /**
37 | * Action executor
38 | *
39 | * @var ActionExecutor
40 | */
41 | private $actionExecutor;
42 |
43 | /**
44 | * Workflow registry
45 | *
46 | * @var WorkflowRegistry
47 | */
48 | private $workflowRegistry;
49 |
50 | /**
51 | * Subject manipulator
52 | *
53 | * @var SubjectManipulator
54 | */
55 | private $subjectManipulator;
56 |
57 | /**
58 | * ExecuteActionCommand constructor.
59 | *
60 | * @param ActionExecutor $actionExecutor
61 | * @param WorkflowRegistry $workflowRegistry
62 | * @param SubjectManipulator $subjectManipulator
63 | */
64 | public function __construct(ActionExecutor $actionExecutor, WorkflowRegistry $workflowRegistry, SubjectManipulator $subjectManipulator)
65 | {
66 | $this->actionExecutor = $actionExecutor;
67 | $this->workflowRegistry = $workflowRegistry;
68 | $this->subjectManipulator = $subjectManipulator;
69 |
70 | parent::__construct();
71 | }
72 |
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | protected function configure()
78 | {
79 | $this
80 | ->setDefinition(array(
81 | new InputOption(
82 | 'action',
83 | null,
84 | InputOption::VALUE_REQUIRED,
85 | 'Action name should be executed'
86 | ),
87 | new InputOption(
88 | 'arguments',
89 | null,
90 | InputOption::VALUE_REQUIRED,
91 | 'Json-encoded list of action parameters'
92 | ),
93 | new InputOption(
94 | 'workflow',
95 | 'w',
96 | InputOption::VALUE_REQUIRED,
97 | 'Name of the current workflow'
98 | ),
99 | new InputOption(
100 | 'subjectId',
101 | 'sid',
102 | InputOption::VALUE_REQUIRED,
103 | 'Id of the workflow subject'
104 | ),
105 | new InputOption(
106 | 'subjectClass',
107 | null,
108 | InputOption::VALUE_REQUIRED,
109 | 'FQCN of the workflow subject'
110 | )
111 | ))
112 | ->setDescription('Execute action command')
113 | ->setHelp(<<%command.name% executes action by name with parameters specified
115 | EOT
116 | );
117 | }
118 |
119 | /**
120 | * Tries to execute action specified
121 | *
122 | * {@inheritdoc}
123 | */
124 | protected function execute(InputInterface $input, OutputInterface $output)
125 | {
126 | $actionName = $input->getOption('action');
127 | $encodedParameters = $input->getOption('arguments');
128 | $parameters = json_decode($encodedParameters);
129 |
130 | $workflowName = $input->getOption('workflow');
131 | $subjectClass = $input->getOption('subjectClass');
132 | $subjectId = $input->getOption('subjectId');
133 |
134 | $subject = $this->subjectManipulator->getSubjectFromDomain($subjectClass, $subjectId);
135 | $workflowContext = new WorkflowContext($this->workflowRegistry->get($subject, $workflowName), $subject, $subjectId);
136 |
137 | $this->actionExecutor->execute($workflowContext, $actionName, $parameters);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/CheckSubjectManipulatorConfigPass.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 08.08.16
10 | */
11 |
12 | namespace Gtt\Bundle\WorkflowExtensionsBundle\DependencyInjection\Compiler;
13 |
14 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\RuntimeException;
15 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17 | use Symfony\Component\DependencyInjection\ContainerBuilder;
18 | use Symfony\Component\DependencyInjection\Reference;
19 |
20 | /**
21 | * Checks that all necessary options for scheduler are set for classes supported by corresponding workflows
22 | */
23 | class CheckSubjectManipulatorConfigPass implements CompilerPassInterface
24 | {
25 | /**
26 | * Workflow id prefix used in main workflow bundle
27 | */
28 | public const WORKFLOW_ID_PREFIX = 'workflow.';
29 |
30 | /**
31 | * Name of the method used to register workflows in registry
32 | */
33 | public const WORKFLOW_REGISTRY_ADD_WORKFLOW_METHOD_NAME = "add";
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function process(ContainerBuilder $container)
39 | {
40 | if (false === $container->hasDefinition('workflow.registry')) {
41 | return;
42 | }
43 |
44 | if (false === $container->hasDefinition('gtt.workflow.transition_scheduler')) {
45 | return;
46 | }
47 |
48 | $subjectClassesWithSubjectFromDomain = $container->getParameter('gtt.workflow.subject_classes_with_subject_from_domain');
49 | $workflowsWithScheduling = $container->getParameter('gtt.workflow.workflows_with_scheduling');
50 |
51 | $registryDefinition = $container->getDefinition('workflow.registry');
52 | foreach ($registryDefinition->getMethodCalls() as [$call, $callArgs]) {
53 | if ($call === self::WORKFLOW_REGISTRY_ADD_WORKFLOW_METHOD_NAME) {
54 | if (count($callArgs) !== 2 || !$callArgs[0] instanceof Reference || !is_string($callArgs[1])) {
55 | throw new RuntimeException(
56 | sprintf(
57 | 'Workflow registry service have unsupported signature for "%s" method',
58 | self::WORKFLOW_REGISTRY_ADD_WORKFLOW_METHOD_NAME
59 | )
60 | );
61 | }
62 |
63 | /** @var Reference $workflowReference */
64 | [$workflowReference, $workflowSupportedClass] = $callArgs;
65 |
66 | $workflowIdWithPrefix = (string) $workflowReference;
67 | if (strpos($workflowIdWithPrefix, self::WORKFLOW_ID_PREFIX) !== 0) {
68 | throw new RuntimeException(
69 | sprintf(
70 | "Workflow registry works with workflow id '%s'in unsupported format",
71 | $workflowIdWithPrefix
72 | )
73 | );
74 | }
75 |
76 | $workflowId = substr($workflowIdWithPrefix, strlen(self::WORKFLOW_ID_PREFIX));
77 |
78 | if (\in_array($workflowId, $workflowsWithScheduling) &&
79 | !\in_array(ltrim($workflowSupportedClass, "\\"), $subjectClassesWithSubjectFromDomain)) {
80 | throw new InvalidConfigurationException(
81 | sprintf(
82 | 'Workflow "%s" configured to use scheduler so all the supported subject classes for it'.
83 | 'must be configured with "subject_from_domain" option under "subject_manipulator". '.
84 | 'This option for "%s" class is missing.',
85 | $workflowId,
86 | $workflowSupportedClass
87 | )
88 | );
89 | }
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Enum/ActionArgumentTypes.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 20.09.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\DependencyInjection\Enum;
15 |
16 | /**
17 | * Holds available argument types for action arguments
18 | *
19 | * @author fduch
20 | */
21 | class ActionArgumentTypes
22 | {
23 | /**
24 | * Scalar value
25 | */
26 | public const TYPE_SCALAR = 'scalar';
27 |
28 | /**
29 | * Expression that will be executed and result will be treated as action argument
30 | * Result must be scalar or non-associate array
31 | */
32 | public const TYPE_EXPRESSION = 'expression';
33 |
34 | /**
35 | * Non-associative array of arguments of other types
36 | */
37 | public const TYPE_ARRAY = 'array';
38 | }
39 |
--------------------------------------------------------------------------------
/src/Entity/Repository/ScheduledJobRepository.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 01.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Entity\Repository;
14 |
15 | use Doctrine\ORM\EntityRepository;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Entity\ScheduledJob;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\NonUniqueReschedulabeJobFoundException;
18 | use JMS\JobQueueBundle\Entity\Job;
19 |
20 | /**
21 | * ScheduledJobRepository
22 | */
23 | class ScheduledJobRepository extends EntityRepository
24 | {
25 | /**
26 | * Finds ScheduledJob by original Job can be scheduled earlier
27 | *
28 | * @param $originalJob $originalJob original job
29 | *
30 | * @return ScheduledJob|null
31 | */
32 | public function findScheduledJobToReschedule(Job $originalJob)
33 | {
34 | // fetching scheduled job that was not started before - it can be rescheduled
35 | $queryString = <<<'QUERY'
36 | SELECT sj FROM WorkflowExtensionsBundle:ScheduledJob sj
37 | JOIN sj.job j
38 | WHERE
39 | j.state in (:stateNew, :statePending) AND
40 | j.command = :command AND
41 | j.args = :args AND
42 | sj.reschedulable = 1
43 | QUERY;
44 |
45 | /** @var ScheduledJob[] $scheduledJobsToReschedule */
46 | $scheduledJobsToReschedule = $this->_em
47 | ->createQuery($queryString)
48 | ->setParameters([
49 | 'stateNew' => Job::STATE_NEW,
50 | 'statePending' => Job::STATE_PENDING,
51 | 'command' => $originalJob->getCommand(),
52 | 'args' => json_encode($originalJob->getArgs())
53 | ])
54 | ->getResult()
55 | ;
56 |
57 | if (!$scheduledJobsToReschedule) {
58 | return null;
59 | }
60 |
61 | if (\count($scheduledJobsToReschedule) > 1) {
62 | // since there is normally only one scheduled pending/new job here
63 | // (because an attempt to schedule duplicate job raises rescheduling of the first one)
64 | // we throwing exception in case of several results here
65 | // TODO probably we need support several jobs for the same transition, workflow and subject scheduled for different times?
66 | $duplicateReschedulableJobsIds = [];
67 | foreach ($scheduledJobsToReschedule as $scheduledJobToReschedule) {
68 | $duplicateReschedulableJobsIds[] = $scheduledJobToReschedule->getJob()->getId();
69 | }
70 |
71 | throw new NonUniqueReschedulabeJobFoundException($originalJob, $duplicateReschedulableJobsIds);
72 | }
73 |
74 | return reset($scheduledJobsToReschedule);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Entity/ScheduledJob.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 27.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Entity;
14 |
15 | use Doctrine\ORM\Mapping as ORM;
16 | use JMS\JobQueueBundle\Entity\Job;
17 |
18 | /**
19 | * Persists information about scheduled transition (as jms Job) for specified workflow and subject
20 | * and allows to fetch it in order to manipulate or re-schedule
21 | * @see Job
22 | *
23 | * @ORM\Entity(repositoryClass = "Gtt\Bundle\WorkflowExtensionsBundle\Entity\Repository\ScheduledJobRepository")
24 | *
25 | * @ORM\Table(name = "gtt_workflow_scheduled_job")
26 | *
27 | * (c) fduch
28 | */
29 | class ScheduledJob
30 | {
31 | /**
32 | * Autogenerated id
33 | *
34 | * @var int
35 | *
36 | * @ORM\Id
37 | * @ORM\GeneratedValue(strategy = "AUTO")
38 | * @ORM\Column(type = "integer", options = {"unsigned": true})
39 | */
40 | private $id;
41 |
42 | /**
43 | * Reschedulable or not
44 | *
45 | * @var boolean
46 | *
47 | * @ORM\Column(type = "boolean", columnDefinition="COMMENT 'Defines whether related job can be rescheduled or not'")
48 | */
49 | private $reschedulable = true;
50 |
51 | /**
52 | * Related jms Job to be executed later
53 | *
54 | * @var Job
55 | *
56 | * @ORM\OneToOne(targetEntity="JMS\JobQueueBundle\Entity\Job")
57 | * @ORM\JoinColumn(name="jms_job_id", referencedColumnName="id", nullable=false)
58 | */
59 | private $job;
60 |
61 | /**
62 | * ScheduledJob constructor.
63 | *
64 | * @param Job $transitionTriggerJob transition trigger job
65 | * @param boolean $reschedulable reschedulable or not
66 | */
67 | public function __construct(Job $transitionTriggerJob, bool $reschedulable = true)
68 | {
69 | $this->reschedulable = $reschedulable;
70 | $this->job = $transitionTriggerJob;
71 | }
72 |
73 |
74 | /**
75 | * Returns id
76 | *
77 | * @return int
78 | */
79 | public function getId()
80 | {
81 | return $this->id;
82 | }
83 |
84 | /**
85 | * Return true if scheduled job can be rescheduled
86 | *
87 | * @return boolean
88 | */
89 | public function isReschedulable(): bool
90 | {
91 | return $this->reschedulable;
92 | }
93 |
94 | /**
95 | * Sets reschedulable flag
96 | *
97 | * @param boolean $reschedulable
98 | */
99 | public function setReschedulable(bool $reschedulable = true): void
100 | {
101 | $this->reschedulable = $reschedulable;
102 | }
103 |
104 | /**
105 | * Returns Job
106 | *
107 | * @return Job
108 | */
109 | public function getJob(): Job
110 | {
111 | return $this->job;
112 | }
113 |
114 | /**
115 | * Sets Job
116 | *
117 | * @param Job $job Related jms job
118 | */
119 | public function setJob(Job $job): void
120 | {
121 | $this->job = $job;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/Exception/ActionException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 02.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Utils\ArrayUtils;
16 |
17 | /**
18 | * Action reference runtime exception
19 | */
20 | class ActionException extends RuntimeException
21 | {
22 | public static function containerUnavailableForServiceMethodReference(string $serviceId): self
23 | {
24 | return new static(
25 | sprintf(
26 | 'Cannot retrieve object for action reference for service id "%s" due to ContainerInterface instance is not set',
27 | $serviceId
28 | )
29 | );
30 | }
31 |
32 | public static function actionAlreadyRegistered(string $actionName): self
33 | {
34 | return new static(sprintf('Action reference with name "%s" is already registered', $actionName));
35 | }
36 |
37 | public static function actionNotFound(string $actionName): self
38 | {
39 | return new static(sprintf('Action reference with name "%s" is not found in action registry', $actionName));
40 | }
41 |
42 | public static function actionExpressionArgumentIsMalformed(string $actionName, string $expression, $expressionResult): self
43 | {
44 | if (is_array($expressionResult) && ArrayUtils::isArrayAssoc($expressionResult)) {
45 | // assoc array
46 | $actualResultDescription = sprintf('associative array "%s"', json_encode($expressionResult));
47 | } else {
48 | // non scalar value
49 | $actualResultDescription = gettype($expressionResult);
50 | }
51 |
52 | return new static(
53 | sprintf('Action reference with name "%s" has expression-defined argument "%s" which'.
54 | ' result must be scalar or non-associative array. Actual result is %s',
55 | $actionName,
56 | $expression,
57 | $actualResultDescription
58 | )
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Exception/InvalidArgumentException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 02.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | /**
16 | * Bundle-level invalid argument exception
17 | */
18 | class InvalidArgumentException extends \InvalidArgumentException implements WorkflowExceptionInterface
19 | {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exception/NonUniqueReschedulabeJobFoundException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 03.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | use JMS\JobQueueBundle\Entity\Job;
16 | use Throwable;
17 |
18 | /**
19 | * Exception for cases whe several reschedulable jobs for workflow transition and subject found
20 | */
21 | class NonUniqueReschedulabeJobFoundException extends \RuntimeException implements WorkflowExceptionInterface
22 | {
23 | /**
24 | * NonUniqueReschedulabeJobFoundException constructor
25 | *
26 | * @param Job $originalJob duplicated job
27 | * @param array $jobIds list of ids of reschedulable jms jobs
28 | * @param int $code exception code
29 | * @param Throwable|null $previous previous exception
30 | */
31 | public function __construct(Job $originalJob, array $jobIds, int $code = 0, Throwable $previous = null)
32 | {
33 | $message = sprintf(
34 | "There are several scheduled '%s' jobs (id's: '%s') available for rescheduling found for command '%s' with args '%s'",
35 | Job::class,
36 | implode(", ", $jobIds),
37 | $originalJob->getCommand(),
38 | json_encode($originalJob->getArgs())
39 | );
40 | parent::__construct($message, $code, $previous);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Exception/RuntimeException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 02.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | /**
16 | * Bundle-level runtime exception
17 | */
18 | class RuntimeException extends \RuntimeException implements WorkflowExceptionInterface
19 | {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exception/SubjectIdRetrievingException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 03.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | /**
16 | * Subject id retrieving faults exception
17 | */
18 | class SubjectIdRetrievingException extends SubjectManipulatorException
19 | {
20 | /**
21 | * Creates exception instance in case of configuration is not found for subject
22 | *
23 | * @param string $subjectClass subject class
24 | *
25 | * @return static
26 | */
27 | public static function expressionNotFound(string $subjectClass): self
28 | {
29 | return new static(sprintf('Cannot find expression for retrieving subject id for class "%s"', $subjectClass));
30 | }
31 |
32 | /**
33 | * Creates exception instance in case of subject specified is not an object
34 | *
35 | * @param mixed $subject subject
36 | *
37 | * @return static
38 | */
39 | public static function subjectIsNotAnObject($subject): self
40 | {
41 | return new static(sprintf('Subject manipulator cannot operate with non-object subjects. "%s" given', gettype($subject)));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Exception/SubjectManipulatorException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 03.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | /**
16 | * Subject manipulator faults exception
17 | */
18 | class SubjectManipulatorException extends \RuntimeException implements WorkflowExceptionInterface
19 | {
20 | /**
21 | * Creates exception instance in case of configurations for subject are already set
22 | *
23 | * @param string $subjectClass subject class
24 | *
25 | * @return static
26 | */
27 | public static function subjectConfigIsAlreadySet(string $subjectClass): self
28 | {
29 | return new static(sprintf('Subject manipulator config is already set for "%s"', $subjectClass));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Exception/SubjectRetrievingFromDomainException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 03.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | /**
16 | * Subject from domain retrieving faults exception
17 | */
18 | class SubjectRetrievingFromDomainException extends SubjectManipulatorException
19 | {
20 | /**
21 | * Creates exception instance in case of configuration is not found for subject
22 | *
23 | * @param string $subjectClass subject class
24 | *
25 | * @return static
26 | */
27 | public static function expressionNotFound(string $subjectClass): self
28 | {
29 | return new static(
30 | sprintf('Cannot find expression for retrieving subject from domain for class "%s". '.
31 | 'Probably you forget to set value for "subject_from_domain" in bundle configuration',
32 | $subjectClass)
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Exception/UnsupportedGuardEventException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | /**
16 | * Exception for cases when workflow guard event is not supported
17 | */
18 | class UnsupportedGuardEventException extends \RuntimeException implements WorkflowExceptionInterface
19 | {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exception/UnsupportedTriggerEventException.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | /**
16 | * Exception for cases when workflow trigger event is not supported
17 | */
18 | class UnsupportedTriggerEventException extends \RuntimeException implements WorkflowExceptionInterface
19 | {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exception/WorkflowExceptionInterface.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Exception;
14 |
15 | use Throwable;
16 |
17 | /**
18 | * Base bundle exception marker
19 | */
20 | interface WorkflowExceptionInterface extends Throwable
21 | {
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/ExpressionLanguage/ContainerAwareExpressionLanguage.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 19.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\ExpressionLanguage;
14 |
15 | use Psr\Cache\CacheItemPoolInterface;
16 | use Psr\Container\ContainerInterface;
17 | use Symfony\Component\DependencyInjection\ExpressionLanguage;
18 | use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
19 |
20 | /**
21 | * Extends DI Expression Language with container variable holds ContainerInterface implementation
22 | */
23 | class ContainerAwareExpressionLanguage extends ExpressionLanguage
24 | {
25 | /**
26 | * DI ContainerInterface implementation
27 | *
28 | * @var ContainerInterface
29 | */
30 | private $container;
31 |
32 | /**
33 | * ContainerAwareExpressionLanguage constructor
34 | *
35 | * @param ContainerInterface $container DI Container
36 | * @param CacheItemPoolInterface $cache cache
37 | * @param ExpressionFunctionProviderInterface[] $providers providers list
38 | */
39 | public function __construct(ContainerInterface $container, CacheItemPoolInterface $cache = null, array $providers = array())
40 | {
41 | $this->container = $container;
42 | parent::__construct($cache, $providers);
43 | }
44 |
45 | /**
46 | * Compile an expression with ContainerInterface context
47 | *
48 | * {@inheritdoc}
49 | */
50 | public function compile($expression, $names = array())
51 | {
52 | return parent::compile($expression, array_unique(array_merge($names, ["container"])));
53 | }
54 |
55 | /**
56 | * Evaluate an expression with ContainerInterface context
57 | *
58 | * {@inheritdoc}
59 | */
60 | public function evaluate($expression, $values = array())
61 | {
62 | return parent::evaluate($expression, $values + ["container" => $this->container]);
63 | }
64 |
65 | /**
66 | * Parse an expression with ContainerInterface context
67 | *
68 | * {@inheritdoc}
69 | */
70 | public function parse($expression, $names)
71 | {
72 | return parent::parse($expression, array_unique(array_merge($names, ["container"])));
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Guard/ExpressionGuard.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 14.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Guard;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\UnsupportedGuardEventException;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
18 | use Psr\Log\LoggerInterface;
19 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
20 | use Symfony\Component\Workflow\Event\GuardEvent;
21 | use Symfony\Component\Workflow\Registry as WorkflowRegistry;
22 | use Throwable;
23 |
24 | /**
25 | * Listener for workflow guard events that can block or allow transition with specified expression
26 | */
27 | class ExpressionGuard
28 | {
29 | /**
30 | * Expression language
31 | *
32 | * @var ExpressionLanguage
33 | */
34 | private $language;
35 |
36 | /**
37 | * Subject manipulator
38 | *
39 | * @var SubjectManipulator
40 | */
41 | private $subjectManipulator;
42 |
43 | /**
44 | * Workflow registry
45 | *
46 | * @var WorkflowRegistry
47 | */
48 | private $workflowRegistry;
49 |
50 | /**
51 | * Logger
52 | *
53 | * @var LoggerInterface
54 | */
55 | private $logger;
56 |
57 | /**
58 | * Maps guard event name to expression and workflow used to block or allow transition
59 | *
60 | * @var array
61 | */
62 | private $supportedEventsConfig = [];
63 |
64 | /**
65 | * ExpressionGuard constructor
66 | *
67 | * @param ExpressionLanguage $language expression language
68 | * @param SubjectManipulator $subjectManipulator subject manipulator
69 | * @param WorkflowRegistry $workflowRegistry workflow registry
70 | * @param LoggerInterface $logger logger
71 | */
72 | public function __construct(
73 | ExpressionLanguage $language,
74 | SubjectManipulator $subjectManipulator,
75 | WorkflowRegistry $workflowRegistry,
76 | LoggerInterface $logger
77 | ) {
78 | $this->language = $language;
79 | $this->subjectManipulator = $subjectManipulator;
80 | $this->workflowRegistry = $workflowRegistry;
81 | $this->logger = $logger;
82 | }
83 |
84 | /**
85 | * Registers guard expression
86 | *
87 | * @param string $eventName guard event name
88 | * @param string $workflowName workflow name
89 | * @param string $expression guard expression
90 | */
91 | public function registerGuardExpression(string $eventName, string $workflowName, string $expression): void
92 | {
93 | $this->supportedEventsConfig[$eventName] = [$workflowName, $expression];
94 | }
95 |
96 | /**
97 | * Blocks or allows workflow transitions by guard expression evaluation
98 | *
99 | * @param GuardEvent $event
100 | * @param string $eventName
101 | *
102 | * @throws \Exception in case of failure
103 | */
104 | public function guardTransition(GuardEvent $event, string $eventName): void
105 | {
106 | if (!array_key_exists($eventName, $this->supportedEventsConfig)) {
107 | throw new UnsupportedGuardEventException(sprintf("Cannot find registered guard event by name '%s'", $eventName));
108 | }
109 |
110 | [$workflowName, $expression] = $this->supportedEventsConfig[$eventName];
111 | $subject = $event->getSubject();
112 | $workflowContext = new WorkflowContext(
113 | $this->workflowRegistry->get($subject, $workflowName),
114 | $subject,
115 | $this->subjectManipulator->getSubjectId($subject)
116 | );
117 | $loggerContext = $workflowContext->getLoggerContext();
118 |
119 | $errorMessage = null;
120 |
121 | try {
122 | $expressionResult = $this->language->evaluate($expression, ['event' => $event]);
123 | } catch (Throwable $e) {
124 | $errorMessage = sprintf(
125 | "Guard expression '%s' for guard event '%s' cannot be evaluated. Details: '%s'",
126 | $expression,
127 | $eventName,
128 | $e->getMessage()
129 | );
130 |
131 | $this->logger->error($errorMessage, $loggerContext);
132 |
133 | // simply skipping processing here without blocking transition
134 | return;
135 | }
136 |
137 | if (!is_bool($expressionResult)) {
138 | $this->logger->debug(
139 | sprintf("Guard expression '%s' for guard event '%s' evaluated with non-boolean result".
140 | " and will be converted to boolean", $expression, $eventName),
141 | $loggerContext
142 | );
143 |
144 | $expressionResult = (bool) $expressionResult;
145 | }
146 |
147 | $event->setBlocked($expressionResult);
148 |
149 | if ($expressionResult) {
150 | $this->logger->debug(
151 | sprintf("Transition '%s' is blocked by guard expression '%s' for guard event '%s'",
152 | $event->getTransition()->getName(),
153 | $expression,
154 | $eventName
155 | ),
156 | $loggerContext
157 | );
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/MarkingStore/OrmPersistentMarkingStore.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 02.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\MarkingStore;
14 |
15 | use Doctrine\Bundle\DoctrineBundle\Registry;
16 | use Symfony\Component\Workflow\Marking;
17 | use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
18 |
19 | /**
20 | * Decorates original marking store with ORM persisting feature during mutating the subject marking
21 | */
22 | class OrmPersistentMarkingStore implements MarkingStoreInterface
23 | {
24 | /**
25 | * Origin marking store
26 | *
27 | * @var MarkingStoreInterface
28 | */
29 | private $originMarkingStore;
30 |
31 | /**
32 | * Doctrine registry
33 | *
34 | * @var Registry
35 | */
36 | private $doctrineRegistry;
37 |
38 | /**
39 | * OrmPersistentMarkingStore constructor.
40 | *
41 | * @param MarkingStoreInterface $originMarkingStore origin marking store
42 | * @param Registry $doctrineRegistry doctrine registry
43 | */
44 | public function __construct(MarkingStoreInterface $originMarkingStore, Registry $doctrineRegistry)
45 | {
46 | $this->doctrineRegistry = $doctrineRegistry;
47 | $this->originMarkingStore = $originMarkingStore;
48 | }
49 |
50 | /**
51 | * {@inheritdoc}
52 | */
53 | public function getMarking($subject)
54 | {
55 | return $this->originMarkingStore->getMarking($subject);
56 | }
57 |
58 | /**
59 | * Updates subject's marking and persists it using ORM
60 | *
61 | * {@inheritdoc}
62 | */
63 | public function setMarking($subject, Marking $marking)
64 | {
65 | $this->originMarkingStore->setMarking($subject, $marking);
66 |
67 | $manager = $this->doctrineRegistry->getManagerForClass(get_class($subject));
68 | // for DEFERRED_EXPLICIT change tracking policies we also persisting subject here
69 | $manager->persist($subject);
70 | $manager->flush();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Resources/config/actions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | applyTransitions
17 |
18 |
19 | Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ActionReferenceInterface::TYPE_WORKFLOW
20 |
21 |
22 |
23 |
24 |
25 |
26 | applyTransition
27 |
28 |
29 | Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ActionReferenceInterface::TYPE_WORKFLOW
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/Resources/config/guard.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Resources/config/scheduler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Resources/config/subject_manipulator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Resources/config/triggers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/Schedule/ActionScheduler.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 28.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Schedule;
14 |
15 | use Carbon\Carbon;
16 | use Doctrine\Common\Persistence\ObjectManager;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Entity\Repository\ScheduledJobRepository;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\Entity\ScheduledJob;
19 | use Gtt\Bundle\WorkflowExtensionsBundle\Schedule\ValueObject\ScheduledAction;
20 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
21 | use JMS\JobQueueBundle\Entity\Job;
22 | use Psr\Log\LoggerInterface;
23 |
24 | /**
25 | * Schedules action to be executed after some time
26 | */
27 | class ActionScheduler
28 | {
29 | /**
30 | * Persistance object manager
31 | *
32 | * @var ObjectManager
33 | */
34 | private $em;
35 |
36 | /**
37 | * Logger
38 | *
39 | * @var LoggerInterface
40 | */
41 | private $logger;
42 |
43 | /**
44 | * ActionScheduler constructor.
45 | *
46 | * @param ObjectManager $em entity manager
47 | * @param LoggerInterface $logger logger
48 | */
49 | public function __construct(ObjectManager $em, LoggerInterface $logger)
50 | {
51 | $this->em = $em;
52 | $this->logger = $logger;
53 | }
54 |
55 | /**
56 | * Schedules actions
57 | *
58 | * @param WorkflowContext $workflowContext workflow context
59 | * @param ScheduledAction $scheduledAction scheduled action
60 | */
61 | public function scheduleAction(WorkflowContext $workflowContext, ScheduledAction $scheduledAction): void
62 | {
63 | /** @var ScheduledJobRepository $scheduledJobRepository */
64 | $scheduledJobRepository = $this->em->getRepository(ScheduledJob::class);
65 |
66 | $jobToSchedule = new Job('workflow:action:execute',
67 | [
68 | '--action=' . $scheduledAction->getName(),
69 | '--arguments=' . json_encode($scheduledAction->getArguments()),
70 | '--workflow=' . $workflowContext->getWorkflow()->getName(),
71 | '--subjectClass=' . get_class($workflowContext->getSubject()),
72 | '--subjectId=' . $workflowContext->getSubjectId(),
73 | ]
74 | );
75 |
76 | $scheduledJob = null;
77 | if ($scheduledAction->isReschedulable()) {
78 | $scheduledJob = $scheduledJobRepository->findScheduledJobToReschedule($jobToSchedule);
79 | }
80 |
81 | if ($scheduledJob) {
82 | // the job was already scheduled but not executed. Now we need to reschedule it
83 | $this->rescheduleActionJob($scheduledAction, $scheduledJob, $workflowContext);
84 | } else {
85 | // creating new jms job to trigger action
86 | $this->scheduleActionJob($scheduledAction, $jobToSchedule, $workflowContext);
87 | }
88 | }
89 |
90 | /**
91 | * Reschedules already scheduled action
92 | *
93 | * @param ScheduledAction $scheduledAction scheduled action
94 | * @param Job $jobToSchedule job to schedule
95 | * @param WorkflowContext $workflowContext workflow context
96 | */
97 | private function scheduleActionJob(
98 | ScheduledAction $scheduledAction,
99 | Job $jobToSchedule,
100 | WorkflowContext $workflowContext
101 | ): void {
102 | $executionDate = $this->getActionExecutionDate($scheduledAction);
103 | $jobToSchedule->setExecuteAfter($executionDate);
104 |
105 | $scheduledJob = new ScheduledJob($jobToSchedule, $scheduledAction->isReschedulable());
106 |
107 | $this->em->persist($jobToSchedule);
108 | $this->em->persist($scheduledJob);
109 |
110 | $this->em->flush();
111 |
112 | $this->logger->info(
113 | sprintf(
114 | "Workflow successfully scheduled action '%s' with parameters '%s'",
115 | $scheduledAction->getName(),
116 | json_encode($scheduledAction->getArguments())
117 | ),
118 | $workflowContext->getLoggerContext()
119 | );
120 | }
121 |
122 | /**
123 | * Reschedules already scheduled action
124 | *
125 | * @param ScheduledAction $scheduledAction scheduled action
126 | * @param ScheduledJob $scheduledJob scheduled job for action
127 | * @param WorkflowContext $workflowContext workflow context
128 | */
129 | private function rescheduleActionJob(
130 | ScheduledAction $scheduledAction,
131 | ScheduledJob $scheduledJob,
132 | WorkflowContext $workflowContext
133 | ): void {
134 | $actionJob = $scheduledJob->getJob();
135 | $actionJob->setExecuteAfter($this->getActionExecutionDate($scheduledAction));
136 |
137 | // since jms Job states DEFERRED_EXPLICIT change tracking policy we should explicitly persist entity now
138 | $this->em->persist($actionJob);
139 | $this->em->flush();
140 |
141 | $this->logger->info(
142 | sprintf("Workflow successfully rescheduled action '%s' with parameters '%s'",
143 | $scheduledAction->getName(),
144 | json_encode($scheduledAction->getArguments())
145 | ),
146 | $workflowContext->getLoggerContext()
147 | );
148 | }
149 |
150 | /**
151 | * Calculates execution date date for scheduled action
152 | *
153 | * @param ScheduledAction $scheduledAction scheduled action
154 | *
155 | * @return \DateTime
156 | */
157 | private function getActionExecutionDate(ScheduledAction $scheduledAction): \DateTime
158 | {
159 | $executionDate = Carbon::now();
160 | $executionDate->add($scheduledAction->getOffset());
161 |
162 | return $executionDate;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/Schedule/ValueObject/ScheduledAction.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 20.09.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Schedule\ValueObject;
15 |
16 | use DateInterval;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\ValueObject\Action;
18 |
19 | /**
20 | * Data Value Object represents scheduled action
21 | *
22 | * @author fduch
23 | */
24 | class ScheduledAction extends Action
25 | {
26 | /**
27 | * Offset for transition
28 | *
29 | * @var DateInterval
30 | */
31 | private $offset;
32 |
33 | /**
34 | * Flag defines current scheduled action can be rescheduled or not
35 | *
36 | * @var boolean
37 | */
38 | private $isReschedulable;
39 |
40 | public function __construct(string $name, array $arguments, string $offset, bool $isReschedulable = false)
41 | {
42 | parent::__construct($name, $arguments);
43 | $this->offset = new DateInterval($offset);
44 | $this->isReschedulable = $isReschedulable;
45 | }
46 |
47 | /**
48 | * Returns offset date interval
49 | *
50 | * @return DateInterval
51 | */
52 | public function getOffset(): DateInterval
53 | {
54 | return $this->offset;
55 | }
56 |
57 | /**
58 | * @return boolean
59 | */
60 | public function isReschedulable(): bool
61 | {
62 | return $this->isReschedulable;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Trigger/Event/AbstractActionListener.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Trigger\Event;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\ValueObject\Action;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\DependencyInjection\Enum\ActionArgumentTypes;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\ActionException;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\Schedule\ValueObject\ScheduledAction;
19 | use Gtt\Bundle\WorkflowExtensionsBundle\Utils\ArrayUtils;
20 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
21 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
22 | use Psr\Log\LoggerInterface;
23 | use Symfony\Component\EventDispatcher\Event;
24 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
25 | use Symfony\Component\Workflow\Registry;
26 |
27 | /**
28 | * Abstract implementation for all action-related listeners
29 | */
30 | abstract class AbstractActionListener extends AbstractListener
31 | {
32 | /**
33 | * Expression language for execute expressions with actions
34 | *
35 | * @var ExpressionLanguage
36 | */
37 | private $actionLanguage;
38 |
39 | /**
40 | * AbstractListener constructor.
41 | *
42 | * @param ExpressionLanguage $subjectRetrieverLanguage subject retriever expression language
43 | * @param SubjectManipulator $subjectManipulator subject manipulator
44 | * @param Registry $workflowRegistry workflow registry
45 | * @param LoggerInterface $logger logger
46 | * @param ExpressionLanguage $actionLanguage action expression language
47 | */
48 | public function __construct(
49 | ExpressionLanguage $subjectRetrieverLanguage,
50 | SubjectManipulator $subjectManipulator,
51 | Registry $workflowRegistry,
52 | LoggerInterface $logger,
53 | ExpressionLanguage $actionLanguage
54 | ) {
55 | parent::__construct($subjectRetrieverLanguage, $subjectManipulator, $workflowRegistry, $logger);
56 | $this->actionLanguage = $actionLanguage;
57 | }
58 |
59 | /**
60 | * Composes Action instances based on bundle actions config
61 | *
62 | * @param array $actions list of action names associated with action arguments
63 | * @param Event $event current event to be handled
64 | * @param WorkflowContext $workflowContext workflow context
65 | * @param bool $scheduled flag defines whether current action is scheduled one or not
66 | *
67 | * @return ScheduledAction[]|Action[]
68 | */
69 | protected function prepareActions(
70 | array $actions,
71 | Event $event,
72 | WorkflowContext $workflowContext,
73 | bool $scheduled = false
74 | ): array {
75 | $preparedActions = [];
76 |
77 | foreach ($actions as $actionName => $actionCalls) {
78 | foreach ($actionCalls as $actionCall) {
79 | $actionArguments = $this->resolveActionArguments($actionName, $actionCall['arguments'], $event, $workflowContext);
80 |
81 | if ($scheduled) {
82 | $preparedActions[] = new ScheduledAction($actionName, $actionArguments, $actionCall['offset'], $actionCall['reschedulable']);
83 | } else {
84 | $preparedActions[] = new Action($actionName, $actionArguments);
85 | }
86 | }
87 | }
88 |
89 | return $preparedActions;
90 | }
91 |
92 | /**
93 | * Resolves action arguments
94 | *
95 | * NOTE: the input array can be assoc here (but keys would be replaced with sequences)
96 | * because associativity is validated in Configuration.php already
97 | * (@see \Gtt\Bundle\WorkflowExtensionsBundle\DependencyInjection\Configuration::buildActionArgumentsNode)
98 | * Expression results must be non-assoc since expressions are evaluated in runtime
99 | *
100 | * @param string $actionName action name
101 | * @param array $arguments list of raw action arguments
102 | * @param Event $event event instance
103 | * @param WorkflowContext $workflowContext workflow context
104 | *
105 | * @return array
106 | */
107 | private function resolveActionArguments(
108 | $actionName,
109 | array $arguments,
110 | Event $event,
111 | WorkflowContext $workflowContext
112 | ): array {
113 | $result = [];
114 | foreach ($arguments as $argument) {
115 | switch ($argument['type']) {
116 | case ActionArgumentTypes::TYPE_SCALAR:
117 | $result[] = $argument['value'];
118 | break;
119 | case ActionArgumentTypes::TYPE_EXPRESSION:
120 | $expressionResult = $this->actionLanguage->evaluate($argument['value'], ['event' => $event, 'workflowContext' => $workflowContext]);
121 | $isNonAssocArrayResult = is_array($expressionResult) && !ArrayUtils::isArrayAssoc($expressionResult);
122 | // expression result should be scalar or non assoc Array
123 | if (!($expressionResult === null || \is_scalar($expressionResult) || $isNonAssocArrayResult)) {
124 | throw ActionException::actionExpressionArgumentIsMalformed($actionName, $argument['value'], $expressionResult);
125 | }
126 | $result[] = $expressionResult;
127 | break;
128 | case ActionArgumentTypes::TYPE_ARRAY:
129 | $result[] = $this->resolveActionArguments($actionName, $argument['value'], $event, $workflowContext);
130 | break;
131 | }
132 | }
133 |
134 | return $result;
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/Trigger/Event/AbstractListener.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Trigger\Event;
14 |
15 | use Exception;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\UnsupportedTriggerEventException;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
19 | use Psr\Log\LoggerInterface;
20 | use Symfony\Component\EventDispatcher\Event;
21 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
22 | use Symfony\Component\Workflow\Registry;
23 | use Throwable;
24 |
25 | /**
26 | * Holds base functionality for all workflow event listeners
27 | */
28 | abstract class AbstractListener
29 | {
30 | /**
31 | * Holds listener configurations for events to be dispatched by current listener
32 | *
33 | * @var array
34 | */
35 | protected $supportedEventsConfig = [];
36 |
37 | /**
38 | * Logger
39 | *
40 | * @var LoggerInterface
41 | */
42 | protected $logger;
43 |
44 | /**
45 | * Expression language for retrieving subject from event
46 | *
47 | * @var ExpressionLanguage
48 | */
49 | private $subjectRetrieverLanguage;
50 |
51 | /**
52 | * Subject manipulator
53 | *
54 | * @var SubjectManipulator
55 | */
56 | private $subjectManipulator;
57 |
58 | /**
59 | * Workflow registry
60 | *
61 | * @var Registry
62 | */
63 | private $workflowRegistry;
64 |
65 | /**
66 | * AbstractListener constructor.
67 | *
68 | * @param ExpressionLanguage $subjectRetrieverLanguage subject retriever expression language
69 | * @param SubjectManipulator $subjectManipulator subject manipulator
70 | * @param Registry $workflowRegistry workflow registry
71 | * @param LoggerInterface $logger logger
72 | */
73 | public function __construct(
74 | ExpressionLanguage $subjectRetrieverLanguage,
75 | SubjectManipulator $subjectManipulator,
76 | Registry $workflowRegistry,
77 | LoggerInterface $logger
78 | ) {
79 | $this->subjectRetrieverLanguage = $subjectRetrieverLanguage;
80 | $this->subjectManipulator = $subjectManipulator;
81 | $this->workflowRegistry = $workflowRegistry;
82 | $this->logger = $logger;
83 | }
84 |
85 | /**
86 | * Sets configs for event to be dispatched by current listener
87 | *
88 | * @param string $eventName event name
89 | * @param string $workflowName workflow name
90 | * @param string $subjectRetrievingExpression expression used to retrieve subject from event
91 | */
92 | protected function configureSubjectRetrievingForEvent(
93 | string $eventName,
94 | string $workflowName,
95 | string $subjectRetrievingExpression
96 | ): void {
97 | if (!isset($this->supportedEventsConfig[$eventName])) {
98 | $this->supportedEventsConfig[$eventName] = [];
99 | }
100 |
101 | $this->supportedEventsConfig[$eventName][$workflowName] = [
102 | 'subject_retrieving_expression' => $subjectRetrievingExpression
103 | ];
104 | }
105 |
106 | /**
107 | * Dispatches registered event
108 | *
109 | * @param Event $event event
110 | * @param string $eventName event name
111 | */
112 | final public function dispatchEvent(Event $event, string $eventName): void
113 | {
114 | if (!array_key_exists($eventName, $this->supportedEventsConfig)) {
115 | throw new UnsupportedTriggerEventException(sprintf("Cannot find registered trigger event by name '%s'", $eventName));
116 | }
117 |
118 | foreach ($this->supportedEventsConfig[$eventName] as $workflowName => $eventConfigForWorkflow) {
119 | $subjectRetrievingExpression = $eventConfigForWorkflow['subject_retrieving_expression'];
120 | $subject = $this->retrieveSubjectFromEvent($event, $eventName, $workflowName, $subjectRetrievingExpression);
121 | if (!$subject) {
122 | continue;
123 | }
124 |
125 | $this->handleEvent(
126 | $eventName,
127 | $event,
128 | $eventConfigForWorkflow,
129 | $this->getWorkflowContext($subject, $workflowName)
130 | );
131 | }
132 | }
133 |
134 | /**
135 | * Reacts on the event occurred with some activity
136 | *
137 | * @param string $eventName event name
138 | * @param Event $event event instance
139 | * @param array $eventConfigForWorkflow registered config for particular event handling
140 | * @param WorkflowContext $workflowContext workflow context
141 | *
142 | * @return void
143 | */
144 | abstract protected function handleEvent(string $eventName, Event $event, array $eventConfigForWorkflow, WorkflowContext $workflowContext): void;
145 |
146 | /**
147 | * Allows to execute any listener callback with control of internal errors and exceptions handling.
148 | * For now all the errors and exceptions are logged and rethrown.
149 | * There is an ability to configure exception catching in the future in order to make possible next execution.
150 | *
151 | * @param \Closure $closure closure to be executed safely
152 | * @param string $eventName event name
153 | * @param WorkflowContext $workflowContext workflow context
154 | * @param string $activity description of the current listener activity (required for logging
155 | * purposes)
156 | *
157 | * @throws Exception|Throwable in case of failure
158 | */
159 | protected function execute(
160 | \Closure $closure,
161 | string $eventName,
162 | WorkflowContext $workflowContext,
163 | string $activity = 'react'
164 | ): void {
165 | try {
166 | $closure();
167 | } catch (Exception $e) {
168 | $this->logger->error(
169 | sprintf('Cannot %s on event "%s" due to exception. Details: %s', $activity, $eventName, $e->getMessage()),
170 | $workflowContext->getLoggerContext()
171 | );
172 | throw $e;
173 | } catch (Throwable $e) {
174 | $this->logger->critical(
175 | sprintf('Cannot %s on event "%s" due to error. Details: %s', $activity, $eventName, $e->getMessage()),
176 | $workflowContext->getLoggerContext()
177 | );
178 | throw $e;
179 | }
180 | }
181 |
182 | /**
183 | * Retrieves workflow subject from event
184 | *
185 | * @param Event $event event to be dispatched
186 | * @param string $eventName event name
187 | * @param string $workflowName workflow
188 | * @param string $subjectRetrievingExpression expression used to retrieve subject from event
189 | *
190 | * @return object|null
191 | */
192 | private function retrieveSubjectFromEvent(Event $event, string $eventName, string $workflowName, string $subjectRetrievingExpression)
193 | {
194 | try {
195 | /** @var object|mixed $subject */
196 | $subject = $this->subjectRetrieverLanguage->evaluate($subjectRetrievingExpression, ['event' => $event]);
197 |
198 | if (!\is_object($subject)) {
199 | $error = sprintf(
200 | "Subject retrieving from '%s' event by expression '%s' ended with empty or non-object result",
201 | $eventName,
202 | $subjectRetrievingExpression
203 | );
204 | $this->logger->error($error, ['workflow' => $workflowName]);
205 | }
206 |
207 | $this->logger->debug(sprintf('Retrieved subject from "%s" event', $eventName), ['workflow' => $workflowName]);
208 |
209 | return $subject;
210 | } catch (Throwable $e) {
211 | $error = sprintf(
212 | "Cannot retrieve subject from event '%s' by evaluating expression '%s'. Error: '%s'. Please check retrieving expression",
213 | $eventName,
214 | $subjectRetrievingExpression,
215 | $e->getMessage()
216 | );
217 |
218 | $this->logger->error($error, ['workflow' => $workflowName]);
219 |
220 | return null;
221 | }
222 | }
223 |
224 | /**
225 | * Creates workflow context
226 | *
227 | * @param object $subject workflow subject
228 | * @param string $workflowName workflow name
229 | *
230 | * @return WorkflowContext
231 | */
232 | private function getWorkflowContext($subject, string $workflowName): WorkflowContext
233 | {
234 | $workflowContext = new WorkflowContext(
235 | $this->workflowRegistry->get($subject, $workflowName),
236 | $subject,
237 | $this->subjectManipulator->getSubjectId($subject)
238 | );
239 |
240 | return $workflowContext;
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/src/Trigger/Event/ActionListener.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Trigger\Event;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Executor as ActionExecutor;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\ValueObject\Action;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
19 | use Psr\Log\LoggerInterface;
20 | use Symfony\Component\EventDispatcher\Event;
21 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
22 | use Symfony\Component\Workflow\Registry;
23 |
24 | /**
25 | * Executes action as a result of event fired
26 | */
27 | class ActionListener extends AbstractActionListener
28 | {
29 | /**
30 | * Action executor
31 | *
32 | * @var ActionExecutor
33 | */
34 | private $actionExecutor;
35 |
36 | /**
37 | * AbstractListener constructor.
38 | *
39 | * @param ExpressionLanguage $subjectRetrieverLanguage subject retriever expression language
40 | * @param SubjectManipulator $subjectManipulator subject manipulator
41 | * @param Registry $workflowRegistry workflow registry
42 | * @param LoggerInterface $logger logger
43 | * @param ExpressionLanguage $actionLanguage action expression language
44 | * @param ActionExecutor $actionExecutor action executor
45 | */
46 | public function __construct(
47 | ExpressionLanguage $subjectRetrieverLanguage,
48 | SubjectManipulator $subjectManipulator,
49 | Registry $workflowRegistry,
50 | LoggerInterface $logger,
51 | ExpressionLanguage $actionLanguage,
52 | ActionExecutor $actionExecutor
53 | ) {
54 | parent::__construct($subjectRetrieverLanguage, $subjectManipulator, $workflowRegistry, $logger, $actionLanguage);
55 | $this->actionExecutor = $actionExecutor;
56 | }
57 |
58 | /**
59 | * {@inheritdoc}
60 | *
61 | * @param array $actions list of actions to try to apply by specified workflow
62 | */
63 | public function registerEvent(
64 | string $eventName,
65 | string $workflowName,
66 | string $subjectRetrievingExpression,
67 | array $actions = []
68 | ): void {
69 | $this->configureSubjectRetrievingForEvent($eventName, $workflowName, $subjectRetrievingExpression);
70 | $this->supportedEventsConfig[$eventName][$workflowName]['actions'] = $actions;
71 | }
72 |
73 | /**
74 | * {@inheritdoc}
75 | */
76 | protected function handleEvent(
77 | string $eventName,
78 | Event $event,
79 | array $eventConfigForWorkflow,
80 | WorkflowContext $workflowContext
81 | ): void {
82 | $actions = $this->supportedEventsConfig[$eventName][$workflowContext->getWorkflow()->getName()]['actions'];
83 | /** @var Action[] $actions */
84 | $actions = $this->prepareActions($actions, $event, $workflowContext);
85 |
86 | foreach ($actions as $action) {
87 | $this->execute(
88 | function () use ($workflowContext, $action) {
89 | $this->actionExecutor->execute($workflowContext, $action->getName(), $action->getArguments());
90 | },
91 | $eventName,
92 | $workflowContext,
93 | sprintf('execute action "%s" with arguments "%s"', $action->getName(), json_encode($action->getArguments()))
94 | );
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/Trigger/Event/ExpressionListener.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Trigger\Event;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
17 | use Psr\Log\LoggerInterface;
18 | use Symfony\Component\EventDispatcher\Event;
19 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
20 | use Symfony\Component\Workflow\Registry;
21 |
22 | /**
23 | * Evaluates expression as a result of event fired
24 | */
25 | class ExpressionListener extends AbstractActionListener
26 | {
27 | /**
28 | * Expression language for execute expressions with actions
29 | *
30 | * @var ExpressionLanguage
31 | */
32 | private $actionLanguage;
33 |
34 | /**
35 | * AbstractListener constructor.
36 | *
37 | * @param ExpressionLanguage $subjectRetrieverLanguage subject retriever expression language
38 | * @param SubjectManipulator $subjectManipulator subject manipulator
39 | * @param Registry $workflowRegistry workflow registry
40 | * @param LoggerInterface $logger logger
41 | * @param ExpressionLanguage $actionLanguage action expression language
42 | */
43 | public function __construct(
44 | ExpressionLanguage $subjectRetrieverLanguage,
45 | SubjectManipulator $subjectManipulator,
46 | Registry $workflowRegistry,
47 | LoggerInterface $logger,
48 | ExpressionLanguage $actionLanguage
49 | ) {
50 | parent::__construct($subjectRetrieverLanguage, $subjectManipulator, $workflowRegistry, $logger, $actionLanguage);
51 | $this->actionLanguage = $actionLanguage;
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | *
57 | * @param string $expression expression to be executed by event
58 | */
59 | public function registerEvent(
60 | string $eventName,
61 | string $workflowName,
62 | string $subjectRetrievingExpression,
63 | string $expression)
64 | {
65 | $this->configureSubjectRetrievingForEvent($eventName, $workflowName, $subjectRetrievingExpression);
66 | $this->supportedEventsConfig[$eventName][$workflowName]['expression'] = $expression;
67 | }
68 |
69 | /**
70 | * {@inheritdoc}
71 | */
72 | protected function handleEvent(string $eventName, Event $event, array $eventConfigForWorkflow, WorkflowContext $workflowContext): void
73 | {
74 | $expression = $this->supportedEventsConfig[$eventName][$workflowContext->getWorkflow()->getName()]['expression'];
75 |
76 | $this->execute(
77 | function () use ($expression, $event, $workflowContext) {
78 | $this->actionLanguage->evaluate($expression, ['event' => $event, 'workflowContext' => $workflowContext]);
79 | },
80 | $eventName,
81 | $workflowContext,
82 | sprintf('execute expression "%s"', $expression)
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Trigger/Event/SchedulerListener.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Trigger\Event;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Schedule\ActionScheduler;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Schedule\ValueObject\ScheduledAction;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
19 | use Psr\Log\LoggerInterface;
20 | use Symfony\Component\EventDispatcher\Event;
21 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
22 | use Symfony\Component\Workflow\Registry;
23 |
24 | /**
25 | * Schedules action as a result of event fired
26 | */
27 | class SchedulerListener extends AbstractActionListener
28 | {
29 | /**
30 | * Action scheduler
31 | *
32 | * @var ActionScheduler
33 | */
34 | private $actionScheduler;
35 |
36 | /**
37 | * AbstractListener constructor.
38 | *
39 | * @param ExpressionLanguage $subjectRetrieverLanguage subject retriever expression language
40 | * @param SubjectManipulator $subjectManipulator subject manipulator
41 | * @param Registry $workflowRegistry workflow registry
42 | * @param LoggerInterface $logger logger
43 | * @param ExpressionLanguage $actionLanguage action expression language
44 | * @param ActionScheduler $actionScheduler action scheduler
45 | */
46 | public function __construct(
47 | ExpressionLanguage $subjectRetrieverLanguage,
48 | SubjectManipulator $subjectManipulator,
49 | Registry $workflowRegistry,
50 | LoggerInterface $logger,
51 | ExpressionLanguage $actionLanguage,
52 | ActionScheduler $actionScheduler)
53 | {
54 | parent::__construct($subjectRetrieverLanguage, $subjectManipulator, $workflowRegistry, $logger, $actionLanguage);
55 | $this->actionScheduler = $actionScheduler;
56 | }
57 |
58 | /**
59 | * {@inheritdoc}
60 | *
61 | * @param string $expression expression to be executed by event
62 | */
63 | public function registerEvent(
64 | string $eventName,
65 | string $workflowName,
66 | string $subjectRetrievingExpression,
67 | array $scheduledActions = [])
68 | {
69 | $this->configureSubjectRetrievingForEvent($eventName, $workflowName, $subjectRetrievingExpression);
70 | $this->supportedEventsConfig[$eventName][$workflowName]['scheduled_actions'] = $scheduledActions;
71 | }
72 |
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | protected function handleEvent(string $eventName, Event $event, array $eventConfigForWorkflow, WorkflowContext $workflowContext): void
78 | {
79 | $actions = $this->supportedEventsConfig[$eventName][$workflowContext->getWorkflow()->getName()]['scheduled_actions'];
80 | /** @var ScheduledAction[] $actions */
81 | $actions = $this->prepareActions($actions, $event, $workflowContext, true);
82 |
83 | foreach ($actions as $scheduledAction) {
84 | $this->execute(
85 | function () use ($workflowContext, $scheduledAction) {
86 | $this->actionScheduler->scheduleAction($workflowContext, $scheduledAction);
87 | },
88 | $eventName,
89 | $workflowContext,
90 | sprintf(
91 | 'schedule action "%s" with arguments "%s"',
92 | $scheduledAction->getName(),
93 | json_encode($scheduledAction->getArguments())
94 | )
95 | );
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Utils/ArrayUtils.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 22.09.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Utils;
15 |
16 | /**
17 | * Service array utils
18 | *
19 | * @author fduch
20 | */
21 | class ArrayUtils
22 | {
23 | /**
24 | * Recursively checks that passed array is associative or not
25 | *
26 | * @param array $array input array
27 | * @param bool $recursiveCheck whether to perform recursive check or not
28 | *
29 | * @return bool
30 | */
31 | public static function isArrayAssoc(array $array, bool $recursiveCheck = true): bool
32 | {
33 | foreach ($array as $key => $value) {
34 | if (is_string($key)) {
35 | return true;
36 | }
37 | if ($recursiveCheck && is_array($value) && static::isArrayAssoc($value)) {
38 | return true;
39 | }
40 | }
41 |
42 | return false;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/WorkflowContext.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 01.09.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle;
15 |
16 | use Symfony\Component\Workflow\Workflow;
17 |
18 | /**
19 | * Workflow context
20 | *
21 | * Data Value Object holds workflow, subject, subjectId.
22 | *
23 | * TODO: probably we need fetch subject id (using SubjectManipulator) dynamically in order to exclude
24 | * probability of diverging of real subject id with the one stored in WorkflowContext
25 | * (this can be occurred if workflow subject changes it's ID).
26 | *
27 | * @author fduch
28 | */
29 | class WorkflowContext
30 | {
31 | /**
32 | * Workflow instance
33 | *
34 | * @var Workflow
35 | */
36 | private $workflow;
37 |
38 | /**
39 | * Workflow subject instance
40 | *
41 | * @var object
42 | */
43 | private $subject;
44 |
45 | /**
46 | * Subject id
47 | *
48 | * @var string|int
49 | */
50 | private $subjectId;
51 |
52 | /**
53 | * WorkflowContext constructor.
54 | *
55 | * @param Workflow $workflow
56 | * @param object $subject
57 | * @param string|int $subjectId
58 | */
59 | public function __construct(Workflow $workflow, $subject, $subjectId)
60 | {
61 | $this->workflow = $workflow;
62 | $this->subject = $subject;
63 | $this->subjectId = $subjectId;
64 | }
65 |
66 | /**
67 | * @return Workflow
68 | */
69 | public function getWorkflow(): Workflow
70 | {
71 | return $this->workflow;
72 | }
73 |
74 | /**
75 | * @return object
76 | */
77 | public function getSubject()
78 | {
79 | return $this->subject;
80 | }
81 |
82 | /**
83 | * @return int|string
84 | */
85 | public function getSubjectId()
86 | {
87 | return $this->subjectId;
88 | }
89 |
90 | /**
91 | * Returns workflow logger context
92 | *
93 | * @return array
94 | */
95 | public function getLoggerContext(): array
96 | {
97 | return [
98 | 'workflow' => $this->workflow->getName(),
99 | 'class' => get_class($this->subject),
100 | 'id' => $this->subjectId
101 | ];
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/WorkflowExtensionsBundle.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 17.07.15
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\DependencyInjection\Compiler\CheckSubjectManipulatorConfigPass;
16 | use Symfony\Component\DependencyInjection\ContainerBuilder;
17 | use Symfony\Component\HttpKernel\Bundle\Bundle;
18 |
19 | /**
20 | * Bundle class
21 | */
22 | class WorkflowExtensionsBundle extends Bundle
23 | { /**
24 | * {@inheritdoc}
25 | */
26 | public function build(ContainerBuilder $container)
27 | {
28 | parent::build($container);
29 | $container->addCompilerPass(new CheckSubjectManipulatorConfigPass());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/WorkflowSubject/SubjectManipulator.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\SubjectIdRetrievingException;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\SubjectManipulatorException;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\SubjectRetrievingFromDomainException;
18 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
19 |
20 | /**
21 | * Class helps to retrieve workflow subject from domain and retrieve id from subject used by scheduler subsystem
22 | */
23 | class SubjectManipulator
24 | {
25 | /**
26 | * Expression language
27 | *
28 | * @var ExpressionLanguage
29 | */
30 | private $language;
31 |
32 | /**
33 | * Holds expressions used to retrieve subject from domain and id from subject
34 | *
35 | * @var array
36 | */
37 | private $supportedSubjectsConfig = [];
38 |
39 | /**
40 | * SubjectManipulator constructor.
41 | *
42 | * @param ExpressionLanguage $language expression language
43 | */
44 | public function __construct(ExpressionLanguage $language)
45 | {
46 | $this->language = $language;
47 | }
48 |
49 | /**
50 | * Sets expressions for supported subject
51 | *
52 | * @param string $subjectClass subject class
53 | * @param string $idFromSubjectExpression expression used to retrieve subject id from subject object
54 | * @param string|null $subjectFromDomainExpression expression used to retrieve subject from domain
55 | *
56 | * @throws SubjectManipulatorException
57 | */
58 | public function addSupportedSubject(
59 | string $subjectClass,
60 | string $idFromSubjectExpression,
61 | string $subjectFromDomainExpression = null
62 | ): void {
63 | if (isset($this->supportedSubjectsConfig[$subjectClass])) {
64 | throw SubjectManipulatorException::subjectConfigIsAlreadySet($subjectClass);
65 | }
66 |
67 | $this->supportedSubjectsConfig[$subjectClass] = [
68 | 'id_from_subject' => $idFromSubjectExpression,
69 | 'subject_from_domain' => $subjectFromDomainExpression
70 | ];
71 | }
72 |
73 | /**
74 | * Retrieves workflow subject from domain
75 | *
76 | * @param string $subjectClass subject class
77 | * @param int $subjectId subject id
78 | *
79 | * @return object
80 | *
81 | * @throws SubjectRetrievingFromDomainException
82 | */
83 | public function getSubjectFromDomain(string $subjectClass, $subjectId)
84 | {
85 | $subjectClass = ltrim($subjectClass, "\\");
86 | if (!array_key_exists($subjectClass, $this->supportedSubjectsConfig) ||
87 | !array_key_exists('subject_from_domain', $this->supportedSubjectsConfig[$subjectClass])) {
88 | throw SubjectRetrievingFromDomainException::expressionNotFound($subjectClass);
89 | }
90 |
91 | return $this->language->evaluate(
92 | $this->supportedSubjectsConfig[$subjectClass]['subject_from_domain'],
93 | ['subjectClass' => $subjectClass, 'subjectId' => $subjectId]
94 | );
95 | }
96 |
97 | /**
98 | * Fetches subject id from subject object
99 | *
100 | * @param object $subject subject
101 | *
102 | * @return int
103 | *
104 | * @throws SubjectIdRetrievingException
105 | */
106 | public function getSubjectId($subject)
107 | {
108 | if (!is_object($subject)) {
109 | throw SubjectIdRetrievingException::subjectIsNotAnObject($subject);
110 | }
111 |
112 | $subjectClass = get_class($subject);
113 | if (!array_key_exists($subjectClass, $this->supportedSubjectsConfig) ||
114 | !array_key_exists('id_from_subject', $this->supportedSubjectsConfig[$subjectClass])) {
115 | throw SubjectIdRetrievingException::expressionNotFound($subjectClass);
116 | }
117 |
118 | return $this->language->evaluate(
119 | $this->supportedSubjectsConfig[$subjectClass]['id_from_subject'],
120 | ['subject' => $subject]
121 | );
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/tests/Action/ExecutorTest.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 19.10.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Action;
15 |
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Action\Reference\ActionReferenceInterface;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
18 | use PHPUnit\Framework\TestCase;
19 | use Symfony\Component\DependencyInjection\ContainerInterface;
20 | use Symfony\Component\Workflow\Workflow;
21 |
22 | class ExecutorTest extends TestCase
23 | {
24 | /**
25 | * @dataProvider actionReferenceProvider
26 | */
27 | public function testEvaluatesAction(
28 | string $actionReferenceType,
29 | array $inputArgs,
30 | array $expectedArgs,
31 | WorkflowContext $wc
32 | ): void {
33 | $container = $this->getMockBuilder(ContainerInterface::class)->getMockForAbstractClass();
34 |
35 | $actionName = 'a1';
36 | $actionReference = $this->getMockBuilder(ActionReferenceInterface::class)->disableOriginalConstructor()->getMock();
37 | $actionReference->expects(self::once())->method('invoke')->with($expectedArgs);
38 | $actionReference->expects(self::once())->method('getType')->willReturn($actionReferenceType);
39 |
40 | $actionRegistry = $this->getMockBuilder(Registry::class)->disableOriginalConstructor()->getMock();
41 | $actionRegistry->expects(self::once())->method('get')->with(self::equalTo($actionName))->willReturn($actionReference);
42 |
43 | $executor = new Executor($actionRegistry, $container);
44 | $executor->execute($wc, $actionName, $inputArgs);
45 | }
46 |
47 | public function actionReferenceProvider(): array
48 | {
49 | $data = [];
50 |
51 | $workflowContext = new WorkflowContext(
52 | $this->getMockBuilder(Workflow::class)->disableOriginalConstructor()->getMock(),
53 | new \StdClass(),
54 | 1
55 | );
56 |
57 | $defaultInputActionArgs = ['some' => 'arg', 'more'];
58 |
59 | // regular action (non-container-aware)
60 | $data[] = [ActionReferenceInterface::TYPE_REGULAR, $defaultInputActionArgs, $defaultInputActionArgs, $workflowContext];
61 |
62 | // regular action (container-aware)
63 | $data[] = [ActionReferenceInterface::TYPE_REGULAR, $defaultInputActionArgs, $defaultInputActionArgs, $workflowContext];
64 |
65 | $workflowActionInputArgs = $defaultInputActionArgs;
66 | array_unshift($workflowActionInputArgs, $workflowContext);
67 |
68 | // workflow action (non-container-aware)
69 | $data[] = [ActionReferenceInterface::TYPE_WORKFLOW, $defaultInputActionArgs, $workflowActionInputArgs, $workflowContext];
70 |
71 | // workflow action (container-aware)
72 | $data[] = [ActionReferenceInterface::TYPE_WORKFLOW, $defaultInputActionArgs, $workflowActionInputArgs, $workflowContext];
73 |
74 | return $data;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tests/Actions/TransitionApplierTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 08.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Actions;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
16 | use PHPUnit\Framework\TestCase;
17 | use Psr\Log\LoggerInterface;
18 | use Symfony\Component\Workflow\Exception\LogicException;
19 | use Symfony\Component\Workflow\Workflow;
20 |
21 | class TransitionApplierTest extends TestCase
22 | {
23 | /**
24 | * @dataProvider exceptionAndLogLevelProvider
25 | */
26 | public function testUnavailabilityToPerformTransitionIsReportedByLogger(
27 | \Exception $exception,
28 | string $loggerLevel,
29 | bool $throw = true
30 | ): void {
31 | $transitionName = "t1";
32 | $subject = new \StdClass();
33 |
34 | $workflow = $this->getMockBuilder(Workflow::class)->disableOriginalConstructor()->getMock();
35 | $workflow->expects(self::once())->method('apply')->with($subject, $transitionName)->willThrowException($exception);
36 | if ($throw) {
37 | $this->expectException(get_class($exception));
38 | }
39 |
40 | $workflowContext = new WorkflowContext($workflow, $subject, "id");
41 |
42 | $logger = $this->getMockBuilder(LoggerInterface::class)->getMockForAbstractClass();
43 | $logger->expects(self::once())->method($loggerLevel);
44 |
45 | $applier = new TransitionApplier($logger);
46 |
47 | $applier->applyTransition($workflowContext, $transitionName);
48 | }
49 |
50 | public function exceptionAndLogLevelProvider(): array
51 | {
52 | return [
53 | // workflow logic exception should trigger info-level logging
54 | [new LogicException(), "info", false],
55 | [new \Exception(), "error"]
56 | ];
57 | }
58 |
59 | /**
60 | * @dataProvider transitionEnvironmentProvider
61 | */
62 | public function testSeveralTransitionsHandledCorrectly(
63 | array $transitions = [],
64 | array $transitionsToBeTriedToApply = [],
65 | bool $cascade = false
66 | ): void {
67 | $subject = new \StdClass();
68 | $workflow = $this->getMockBuilder(Workflow::class)->disableOriginalConstructor()->getMock();
69 |
70 | $callIndex = 0;
71 | $transitionsToApplyCount = 0;
72 | $workflow->expects(self::at($callIndex))->method('getName');
73 | $callIndex++;
74 | foreach ($transitionsToBeTriedToApply as $transitionName => $applicationResult) {
75 | $invocationMocker = $workflow->expects(self::at($callIndex))->method('apply')->with($subject, $transitionName);
76 | $transitionsToApplyCount++;
77 | if (is_array($applicationResult)) {
78 | [$exception, $throw] = $applicationResult;
79 | $invocationMocker->willThrowException($exception);
80 | if ($throw) {
81 | $this->expectException(get_class($exception));
82 | break;
83 | }
84 | }
85 | $callIndex++;
86 | }
87 | $workflow->expects($this->exactly($transitionsToApplyCount))->method('apply');
88 |
89 | $workflowContext = new WorkflowContext($workflow, $subject, "id");
90 |
91 | $applier = new TransitionApplier($this->getMockBuilder(LoggerInterface::class)->getMockForAbstractClass());
92 |
93 | $applier->applyTransitions($workflowContext, $transitions, $cascade);
94 | }
95 |
96 | public function transitionEnvironmentProvider()
97 | {
98 | return [
99 | [['t1', 't2'], ['t1' => true]],
100 | [['t1', 't2'], ['t1' => [new \Exception(), true], 't2' => true], true],
101 | [['t1', 't2'], ['t1' => [new \Exception(), true], 't2' => true], false],
102 | [['t1', 't2'], ['t1' => [new LogicException(), false], 't2' => true], true],
103 | [['t1', 't2'], ['t1' => [new LogicException(), false], 't2' => true], false],
104 | ];
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/Compiler/CheckSubjectManipulatorConfigPassTest.php:
--------------------------------------------------------------------------------
1 |
9 | * Date: 30.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\DependencyInjection\Compiler;
14 |
15 | use PHPUnit\Framework\TestCase;
16 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
17 | use Symfony\Component\DependencyInjection\ContainerBuilder;
18 | use Symfony\Component\DependencyInjection\Definition;
19 | use Symfony\Component\DependencyInjection\Reference;
20 |
21 | class CheckSubjectManipulatorConfigPassTest extends TestCase
22 | {
23 | public function testThrowingExceptionIfTargetSubjectClassesNotConfigured(): void
24 | {
25 | $this->expectException(InvalidConfigurationException::class);
26 |
27 | $container = new ContainerBuilder();
28 | $container->addCompilerPass(new CheckSubjectManipulatorConfigPass());
29 |
30 | $registryDefinition = new Definition("RegistryClass");
31 | $container->setDefinition('workflow.test', new Definition('class'));
32 | $container->setDefinition('gtt.workflow.transition_scheduler', new Definition('class'));
33 |
34 | $workflowName = 'test';
35 | $registryDefinition->addMethodCall(
36 | CheckSubjectManipulatorConfigPass::WORKFLOW_REGISTRY_ADD_WORKFLOW_METHOD_NAME,
37 | [
38 | new Reference(CheckSubjectManipulatorConfigPass::WORKFLOW_ID_PREFIX.$workflowName),
39 | '\Some\Target\Class'
40 | ]
41 | );
42 | $container->setDefinition('workflow.registry', $registryDefinition);
43 |
44 | $container->setParameter('gtt.workflow.subject_classes_with_subject_from_domain', ['\Some\Target\AnotherClass']);
45 | $container->setParameter('gtt.workflow.workflows_with_scheduling', [$workflowName]);
46 |
47 | $container->compile();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/ConfigurationTest.php:
--------------------------------------------------------------------------------
1 | processConfiguration($config)['context']);
30 | }
31 |
32 | public function contextDataProvider(): iterable
33 | {
34 | $nomatter = ['workflows' => ['some' => []], 'subject_manipulator' => [TargetWorkflowSubject::class => null]];
35 |
36 | yield [['context' => 'my_service'] + $nomatter, ['my_service' => 'my_service']];
37 |
38 | yield [['context' => null] + $nomatter, []];
39 |
40 | yield [$nomatter, []];
41 |
42 | yield [['context' => ['test' => null]] + $nomatter, ['test' => 'test']];
43 |
44 | yield [['context' => ['test' => 'another']] + $nomatter, ['test' => 'another']];
45 | }
46 |
47 | private function processConfiguration(array $config): array
48 | {
49 | return (new Processor())->processConfiguration(new Configuration(), [$config]);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/BaseConfig/config.yml:
--------------------------------------------------------------------------------
1 | framework:
2 | secret: test
3 | router: { resource: "routing.yml" }
4 | test: ~
--------------------------------------------------------------------------------
/tests/Functional/Configuration/BaseConfig/routing.yml:
--------------------------------------------------------------------------------
1 | # no need for routing here
--------------------------------------------------------------------------------
/tests/Functional/Configuration/EventTriggerWithGuardsCase/Fixtures/Event.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\EventTriggerWithGuardsCase\Fixtures;
14 |
15 | use Symfony\Component\EventDispatcher\Event as BaseEvent;
16 |
17 | class Event extends BaseEvent
18 | {
19 | private $subject;
20 |
21 | public function __construct(TargetWorkflowSubject $subject)
22 | {
23 | $this->subject = $subject;
24 | }
25 |
26 | public function getSubject()
27 | {
28 | return $this->subject;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/EventTriggerWithGuardsCase/Fixtures/TargetWorkflowSubject.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\EventTriggerWithGuardsCase\Fixtures;
14 |
15 | class TargetWorkflowSubject
16 | {
17 | private $status;
18 |
19 | public function __construct()
20 | {
21 | $this->status = ['inactive' => 1];
22 | }
23 |
24 | public function getStatus() {
25 | return $this->status;
26 | }
27 |
28 | public function setStatus($status) {
29 | $this->status = $status;
30 | }
31 |
32 | public function getId() {
33 | return "1";
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/EventTriggerWithGuardsCase/bundles.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | return array(
14 | new \Symfony\Bundle\MonologBundle\MonologBundle(),
15 | new \Gtt\Bundle\WorkflowExtensionsBundle\WorkflowExtensionsBundle()
16 | );
17 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/EventTriggerWithGuardsCase/config.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: "../BaseConfig/config.yml" }
3 |
4 | framework:
5 | workflows:
6 | simple:
7 | type: workflow
8 | marking_store:
9 | type: multiple_state
10 | arguments:
11 | - status
12 | supports:
13 | - Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\EventTriggerWithGuardsCase\Fixtures\TargetWorkflowSubject
14 | places:
15 | - inactive
16 | - processing
17 | - vip
18 | - active
19 | - regular
20 | - closed
21 | transitions:
22 | processing:
23 | from:
24 | - inactive
25 | to:
26 | - processing
27 | activating:
28 | from:
29 | - processing
30 | to:
31 | - active
32 | - regular
33 | viping:
34 | from:
35 | - regular
36 | to:
37 | - vip
38 | closing:
39 | from:
40 | - active
41 | - regular
42 | to:
43 | - closed
44 | closing_vip:
45 | from:
46 | - active
47 | - vip
48 | to:
49 | - closed
50 | crazy_closing:
51 | from:
52 | - active
53 | to:
54 | - closed
55 |
56 | workflow_extensions:
57 | workflows:
58 | simple:
59 | triggers:
60 | event:
61 | processing.event:
62 | actions:
63 | apply_transitions:
64 | - [[processing, activating, closing]]
65 | subject_retrieving_expression: 'event.getSubject()'
66 | viping.event:
67 | actions:
68 | apply_transitions:
69 | - [[viping, closing_vip]]
70 | subject_retrieving_expression: 'event.getSubject()'
71 | crazy_closing.event:
72 | # example of expression usage (it is the same as direct usage of action apply_transition or apply_transitions like above)
73 | expression: "apply_transition('crazy_closing')"
74 | subject_retrieving_expression: 'event.getSubject()'
75 | guard:
76 | # prevent closing vips
77 | expression: 'event.getTransition().getName() == "closing_vip"'
78 | transitions:
79 | # prevent direct transiticn of active=>closed when marking contains other places
80 | crazy_closing: 'event.getMarking().getPlaces() != {"active": 1}'
81 | subject_manipulator:
82 | Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\EventTriggerWithGuardsCase\Fixtures\TargetWorkflowSubject: ~
83 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/ScheduleCase/Fixtures/ClientBundle/ClientBundle.php:
--------------------------------------------------------------------------------
1 |
10 | * @date 17.07.15
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle;
15 |
16 | use Symfony\Component\HttpKernel\Bundle\Bundle;
17 |
18 | class ClientBundle extends Bundle
19 | {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/ScheduleCase/Fixtures/ClientBundle/DataFixtures/ORM/LoadData.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 17.07.15
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle\DataFixtures\ORM;
14 |
15 | use Doctrine\Common\DataFixtures\FixtureInterface;
16 | use Doctrine\Common\Persistence\ObjectManager;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle\Entity\Client;
18 |
19 | class LoadData implements FixtureInterface
20 | {
21 | /**
22 | * {@inheritDoc}
23 | */
24 | public function load(ObjectManager $manager)
25 | {
26 | $client = new Client();
27 | $client->setName("Johnny");
28 |
29 | $manager->persist($client);
30 |
31 | $manager->flush();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/ScheduleCase/Fixtures/ClientBundle/Entity/Client.php:
--------------------------------------------------------------------------------
1 | status = "inactive";
42 | }
43 |
44 | /**
45 | * Get id
46 | *
47 | * @return integer
48 | */
49 | public function getId()
50 | {
51 | return $this->id;
52 | }
53 |
54 | /**
55 | * Set name
56 | *
57 | * @param string $name
58 | * @return Client
59 | */
60 | public function setName($name)
61 | {
62 | $this->name = $name;
63 |
64 | return $this;
65 | }
66 |
67 | /**
68 | * Get name
69 | *
70 | * @return string
71 | */
72 | public function getName()
73 | {
74 | return $this->name;
75 | }
76 |
77 | /**
78 | * Returns status
79 | *
80 | * @return string
81 | */
82 | public function getStatus()
83 | {
84 | return $this->status;
85 | }
86 |
87 | /**
88 | * Sets status marking
89 | *
90 | * @param $status
91 | */
92 | public function setStatus($status)
93 | {
94 | $this->status = $status;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/ScheduleCase/Fixtures/Event.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle\Entity\Client;
16 | use Symfony\Component\EventDispatcher\Event as BaseEvent;
17 |
18 | class Event extends BaseEvent
19 | {
20 | private $subject;
21 |
22 | public function __construct(Client $subject)
23 | {
24 | $this->subject = $subject;
25 | }
26 |
27 | public function getSubject()
28 | {
29 | return $this->subject;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/ScheduleCase/bundles.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | return array(
14 | new \Symfony\Bundle\MonologBundle\MonologBundle(),
15 | new \Gtt\Bundle\WorkflowExtensionsBundle\WorkflowExtensionsBundle(),
16 | new \Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
17 | new \Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle\ClientBundle(),
18 | new \JMS\JobQueueBundle\JMSJobQueueBundle()
19 | );
20 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/ScheduleCase/config.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: "../BaseConfig/config.yml" }
3 |
4 | services:
5 | gtt.workflow.marking_store.single_state:
6 | class: Symfony\Component\Workflow\MarkingStore\SingleStateMarkingStore
7 | arguments:
8 | - status
9 | gtt.workflow.marking_store.orm.marking.store:
10 | class: Gtt\Bundle\WorkflowExtensionsBundle\MarkingStore\OrmPersistentMarkingStore
11 | arguments: ["@gtt.workflow.marking_store.single_state", "@doctrine"]
12 |
13 | doctrine:
14 | dbal:
15 | default_connection: default
16 | connections:
17 | default:
18 | driver: pdo_sqlite
19 | path: "%kernel.cache_dir%/database.sqlite"
20 | orm:
21 | auto_generate_proxy_classes: "%kernel.debug%"
22 | naming_strategy: doctrine.orm.naming_strategy.underscore
23 | auto_mapping: true
24 |
25 | framework:
26 | workflows:
27 | simple:
28 | type: workflow
29 | marking_store:
30 | service: gtt.workflow.marking_store.orm.marking.store
31 | supports:
32 | - Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle\Entity\Client
33 | places:
34 | - inactive
35 | - active
36 | - sleeping
37 | - closed
38 | transitions:
39 | activating:
40 | from:
41 | - inactive
42 | to:
43 | - active
44 | sleeping:
45 | from:
46 | - active
47 | to:
48 | - sleeping
49 | closing:
50 | from:
51 | - sleeping
52 | to:
53 | - closed
54 |
55 | workflow_extensions:
56 | workflows:
57 | simple:
58 | triggers:
59 | event:
60 | activating.event:
61 | actions:
62 | apply_transition:
63 | - [activating]
64 | subject_retrieving_expression: 'event.getSubject()'
65 | # during activation schedule sleeping
66 | workflow.simple.transition.activating:
67 | schedule:
68 | apply_transition:
69 | -
70 | arguments: [sleeping]
71 | offset: PT1S
72 | subject_retrieving_expression: 'event.getSubject()'
73 | # during sleeping schedule closing
74 | workflow.simple.transition.sleeping:
75 | schedule:
76 | apply_transition:
77 | -
78 | arguments: [closing]
79 | offset: PT1S
80 | subject_retrieving_expression: 'event.getSubject()'
81 | # prolongate closing for 1 sec
82 | prolong.event:
83 | schedule:
84 | apply_transition:
85 | -
86 | arguments: [closing]
87 | offset: PT1S
88 | subject_retrieving_expression: 'event.getSubject()'
89 | scheduler: ~
90 | subject_manipulator:
91 | Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle\Entity\Client:
92 | subject_from_domain: "container.get('doctrine').getManagerForClass(subjectClass).find(subjectClass, subjectId)"
93 | context:
94 | doctrine: ~
95 |
--------------------------------------------------------------------------------
/tests/Functional/Configuration/ScheduleCase/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | initApplication();
17 | $application->run(new ArgvInput());
18 |
--------------------------------------------------------------------------------
/tests/Functional/EventTriggerWithGuardsCaseTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Functional;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\EventTriggerWithGuardsCase\Fixtures\Event;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\EventTriggerWithGuardsCase\Fixtures\TargetWorkflowSubject;
17 | use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18 |
19 | class EventTriggerWithGuardsCaseTest extends TestCase
20 | {
21 | /**
22 | * WebClient emulator
23 | *
24 | * @var \Symfony\Bundle\FrameworkBundle\Client
25 | */
26 | protected $client;
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function setUp(): void
32 | {
33 | parent::setUp();
34 |
35 | $this->client = self::createClient(
36 | array(
37 | "app_name" => "EventTriggerWithGuardsCaseTest",
38 | "test_case" => "EventTriggerWithGuardsCase",
39 | "root_config" => "config.yml",
40 | "config_dir" => __DIR__ . "/Configuration",
41 | "environment" => "test",
42 | "debug" => false
43 | )
44 | );
45 | }
46 |
47 | /**
48 | * @group functional
49 | */
50 | public function testSimple(): void
51 | {
52 | $container = $this->client->getContainer();
53 | /** @var EventDispatcherInterface $eventDispatcher */
54 | $eventDispatcher = $container->get('event_dispatcher');
55 |
56 | $subject = new TargetWorkflowSubject();
57 | $event = new Event($subject);
58 |
59 | $eventDispatcher->dispatch('processing.event', $event);
60 | self::assertEquals(['processing' => 1], $subject->getStatus());
61 |
62 | $eventDispatcher->dispatch('processing.event', $event);
63 | self::assertEquals(['active' => 1, "regular" => 1], $subject->getStatus());
64 |
65 | $eventDispatcher->dispatch('viping.event', $event);
66 | self::assertEquals(['active' => 1, 'vip' => 1], $subject->getStatus());
67 |
68 | $eventDispatcher->dispatch('processing.event', $event);
69 | // nothing happens
70 | self::assertEquals(['active' => 1, 'vip' => 1], $subject->getStatus());
71 |
72 | $eventDispatcher->dispatch('viping.event', $event);
73 | // guard prevents closing vips
74 | self::assertEquals(['active' => 1, 'vip' => 1], $subject->getStatus());
75 |
76 | $eventDispatcher->dispatch('crazy_closing.event', $event);
77 | // guard allows closing only if there is only one place "active" in marking
78 | self::assertEquals(['active' => 1, 'vip' => 1], $subject->getStatus());
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/tests/Functional/Kernel/BaseTestKernel.php:
--------------------------------------------------------------------------------
1 | setRootDir($rootDir);
72 |
73 | $this->configDir = realpath($configDir);
74 | if (!is_dir($this->configDir . '/' . $testCase)) {
75 | throw new \InvalidArgumentException(sprintf('The test case "%s" does not exist.', $testCase));
76 | }
77 | $this->testCase = $testCase;
78 |
79 | $fs = new Filesystem();
80 | if (!$fs->isAbsolutePath($rootConfig) &&
81 | !file_exists($rootConfig = $this->configDir . '/' . $testCase . '/' . $rootConfig)) {
82 | throw new \InvalidArgumentException(sprintf('The root config "%s" does not exist.', $rootConfig));
83 | }
84 | $this->rootConfig = $rootConfig;
85 | $this->rawName = $appName;
86 | $this->name = preg_replace('/[^a-zA-Z0-9_]+/', '', $this->rawName)."_".
87 | preg_replace('/[^a-zA-Z0-9_]+/', '', str_replace(DIRECTORY_SEPARATOR, "_", $testCase));
88 | }
89 |
90 | /**
91 | * Sets kernel root directory
92 | *
93 | * @param string $rootDir kernel root dir
94 | *
95 | * @throws \RuntimeException if directory does not exist and can not be created
96 | *
97 | * @return void
98 | */
99 | protected function setRootDir($rootDir)
100 | {
101 | if (!is_dir($rootDir) && !mkdir($rootDir, 0777, true) && !is_dir($rootDir)) {
102 | throw new \RuntimeException(sprintf('Unable to create test kernel root directory %s ', $rootDir));
103 | }
104 | $this->rootDir = realpath($rootDir);
105 | }
106 |
107 | /**
108 | * {@inheritdoc}
109 | */
110 | public function getRootDir()
111 | {
112 | return $this->rootDir;
113 | }
114 |
115 | /**
116 | * Returns base bundle list
117 | *
118 | * @return array
119 | */
120 | protected function getBaseBundles()
121 | {
122 | return array(
123 | new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
124 | );
125 | }
126 |
127 | /**
128 | * {@inheritdoc}
129 | */
130 | public function registerBundles()
131 | {
132 | $bundles = array();
133 | $baseBundles = $this->getBaseBundles();
134 | if (file_exists($filename = $this->configDir . '/' . $this->testCase . '/bundles.php')) {
135 | $bundles = include $filename;
136 | }
137 | return array_merge($baseBundles, $bundles);
138 | }
139 |
140 | /**
141 | * {@inheritdoc}
142 | */
143 | public function init()
144 | {
145 | }
146 |
147 | /**
148 | * {@inheritdoc}
149 | */
150 | public function getCacheDir()
151 | {
152 | return $this->getTempAppDir()."/".$this->testCase.'/cache/'.$this->environment;
153 | }
154 |
155 | /**
156 | * {@inheritdoc}
157 | */
158 | public function getLogDir()
159 | {
160 | return $this->getTempAppDir()."/".$this->testCase.'/logs';
161 | }
162 |
163 | /**
164 | * {@inheritdoc}
165 | */
166 | public function getTempAppDir(): string
167 | {
168 | return sys_get_temp_dir().'/'.$this->rawName;
169 | }
170 |
171 | /**
172 | * {@inheritdoc}
173 | */
174 | public function registerContainerConfiguration(LoaderInterface $loader)
175 | {
176 | $loader->load($this->rootConfig);
177 | }
178 |
179 | /**
180 | * {@inheritdoc}
181 | */
182 | public function serialize()
183 | {
184 | return serialize(
185 | array(
186 | $this->getEnvironment(),
187 | $this->isDebug(),
188 | $this->rawName,
189 | $this->testCase,
190 | $this->configDir,
191 | $this->rootConfig,
192 | $this->rootDir));
193 | }
194 |
195 | /**
196 | * {@inheritdoc}
197 | */
198 | public function unserialize($str)
199 | {
200 | [$env, $debug, $appName, $testCase, $configDir, $rootConfig, $rootDir] = unserialize($str, ['allow_classes' => false]);
201 | $this->__construct($env, $debug);
202 | $this->setTestKernelConfiguration($appName, $testCase, $configDir, $rootConfig, $rootDir);
203 | }
204 |
205 | /**
206 | * {@inheritdoc}
207 | */
208 | protected function getKernelParameters()
209 | {
210 | $parameters = parent::getKernelParameters();
211 |
212 | $parameters['kernel.test_case'] = $this->testCase;
213 | $parameters['kernel.config_dir'] = $this->configDir;
214 |
215 | return $parameters;
216 | }
217 |
218 | /**
219 | * {@inheritdoc}
220 | */
221 | public function getName()
222 | {
223 | if ($this->name === null) {
224 | return static::class;
225 | }
226 |
227 | return parent::getName();
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/tests/Functional/Kernel/KernelBuilder.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class KernelBuilder
21 | {
22 | /**
23 | * Defines default test kernel class name
24 | *
25 | * @var string
26 | */
27 | protected static $defaultTestKernelClass = '\Gtt\Bundle\WorkflowExtensionsBundle\Functional\Kernel\BaseTestKernel';
28 |
29 | /**
30 | * Returns kernel instance
31 | *
32 | * @param string $kernelClass kernel class name need to be instantiated
33 | * @param array $options kernel configuration options
34 | * base options:
35 | * environment - environment
36 | * debug - debug mode
37 | * test kernel specific options (for TestKernelInterface instances)
38 | * app_name - name of test application kernel
39 | * test_case - directory name where kernel configs are stored
40 | * config_dir - path to directory with test cases configurations
41 | * root_config - name of the application config file
42 | * root_dir - path to root directory of test application. Can be unset
43 | *
44 | * @throws \InvalidArgumentException in case of invalid options specified
45 | *
46 | * @return \Symfony\Component\HttpKernel\KernelInterface
47 | */
48 | public static function getKernel($kernelClass = null, array $options = array())
49 | {
50 | if (!($kernelClass === null)) {
51 | if (!class_exists($kernelClass)) {
52 | throw new \InvalidArgumentException("Cannot load $kernelClass");
53 | }
54 | } else {
55 | $kernelClass = self::$defaultTestKernelClass;
56 | }
57 |
58 | if (defined("KERNEL_ENV")) {
59 | $options['environment'] = KERNEL_ENV;
60 | }
61 |
62 | if (defined("KERNEL_DEBUG")) {
63 | $options['debug'] = KERNEL_DEBUG;
64 | }
65 |
66 | $kernel = new $kernelClass(
67 | isset($options['environment']) ? $options['environment'] : 'test',
68 | isset($options['debug']) ? $options['debug'] : true
69 | );
70 |
71 | if ($kernel instanceof TestKernelInterface) {
72 | if (!isset($options['app_name'])) {
73 | throw new \InvalidArgumentException('The option "app_name" must be set.');
74 | }
75 | if (!isset($options['config_dir'])) {
76 | throw new \InvalidArgumentException('The option "config_dir" must be set.');
77 | }
78 | if (!isset($options['test_case'])) {
79 | throw new \InvalidArgumentException('The option "test_case" must be set.');
80 | }
81 |
82 | $kernel->setTestKernelConfiguration(
83 | $options['app_name'],
84 | $options['test_case'],
85 | $options['config_dir'],
86 | $options['root_config'] ?? 'config.yml',
87 | $options['root_dir'] ?? null
88 | );
89 | }
90 |
91 | return $kernel;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/tests/Functional/Kernel/TestKernelInterface.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | interface TestKernelInterface
21 | {
22 | /**
23 | * Sets Kernel configuration
24 | *
25 | * @param string $appName name of test application kernel
26 | * @param string $testCase directory name where kernel configs are stored
27 | * @param string $configDir path to directory with test cases configurations
28 | * @param string $rootConfig name of the application config file
29 | * @param string|null $rootDir path to root directory of test application. Can be unset
30 | * due to backward compatibility.
31 | *
32 | * @return void
33 | */
34 | public function setTestKernelConfiguration(
35 | string $appName,
36 | string $testCase,
37 | string $configDir,
38 | string $rootConfig,
39 | string $rootDir = null
40 | ): void;
41 |
42 | /**
43 | * Returns temporary application folder that is used to store cache, logs of test kernel.
44 | * This folder is always clear after tests run thanks to
45 | * \Gtt\Bundle\WorkflowExtensionsBundle\Functional\TestCase::tearDownAfterClass - you can always
46 | * change this behaviour by overriding this method.
47 | * Can be used to to store other temporary application data. For example can be used to construct
48 | * root directory if functional tests generate some file stuff that depends on $kernel->getRootDir() folder
49 | *
50 | * @return string
51 | */
52 | public function getTempAppDir(): string;
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Functional/ScheduleCaseTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 28.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Functional;
14 |
15 | use Doctrine\Bundle\DoctrineBundle\Command\Proxy\CreateSchemaDoctrineCommand;
16 | use Doctrine\Bundle\FixturesBundle\Command\LoadDataFixturesDoctrineCommand;
17 | use Doctrine\ORM\EntityManager;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle\ClientBundle;
19 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\ClientBundle\Entity\Client;
20 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Configuration\ScheduleCase\Fixtures\Event;
21 | use JMS\JobQueueBundle\Command\RunCommand;
22 | use Symfony\Bundle\FrameworkBundle\Console\Application;
23 | use Symfony\Component\Console\Command\Command;
24 | use Symfony\Component\Console\Input\InputOption;
25 | use Symfony\Component\Console\Tester\CommandTester;
26 | use Symfony\Component\DependencyInjection\ContainerInterface;
27 | use Symfony\Component\EventDispatcher\EventDispatcherInterface;
28 |
29 | class ScheduleCaseTest extends TestCase
30 | {
31 | /**
32 | * WebClient emulator
33 | *
34 | * @var \Symfony\Bundle\FrameworkBundle\Client
35 | */
36 | protected $client;
37 |
38 | /**
39 | * @var Application
40 | */
41 | protected $app;
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function setUp(): void
47 | {
48 | $this->initApplication();
49 |
50 | $this->initDbSchema();
51 |
52 | // load fixtures
53 | $fixturesBundle = new ClientBundle();
54 | $fixturesPath = $fixturesBundle->getPath() . '/DataFixtures/ORM';
55 | $this->loadFixtures($this->client->getContainer(), $fixturesPath);
56 | }
57 |
58 | public function initApplication(): Application
59 | {
60 | if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers())) {
61 | self::markTestSkipped('This test requires SQLite support in your environment');
62 | }
63 |
64 | parent::setUp();
65 |
66 | $this->client = self::createClient(
67 | array(
68 | "app_name" => "ScheduleCaseTest",
69 | "test_case" => "ScheduleCase",
70 | "root_config" => "config.yml",
71 | "config_dir" => __DIR__ . "/Configuration",
72 | "root_dir" => __DIR__ . "/Configuration/ScheduleCase",
73 | "environment" => "test",
74 | "debug" => false
75 | )
76 | );
77 | $this->app = new Application($this->client->getKernel());
78 | // add jms-job-id option to the application in order to be able to run scheduler
79 | $this->app->getDefinition()->addOption(
80 | new InputOption('--jms-job-id', null, InputOption::VALUE_REQUIRED, 'The ID of the Job.')
81 | );
82 | $this->app->setAutoExit(false);
83 | $this->app->setCatchExceptions(false);
84 |
85 | return $this->app;
86 | }
87 |
88 | /**
89 | * @param string $em
90 | */
91 | protected function initDbSchema($em = 'default'): void
92 | {
93 | $schemaCreateCommand = new CreateSchemaDoctrineCommand();
94 | $this->runConsoleCommand($schemaCreateCommand, ["--em" => $em]);
95 | }
96 |
97 | /**
98 | * @param ContainerInterface $container
99 | * @param array|string|false $fixturesPath
100 | * @param string $em
101 | * @param bool|true $append
102 | *
103 | * @return void
104 | */
105 | protected function loadFixtures(
106 | ContainerInterface $container,
107 | $fixturesPath = false,
108 | string $em = 'default',
109 | bool $append = true
110 | ): void {
111 | $fixtureLoadCommand = new LoadDataFixturesDoctrineCommand();
112 | $fixtureLoadCommand->setContainer($container);
113 | $params = array(
114 | "--em" => $em,
115 | "--append" => $append
116 | );
117 | if ($fixturesPath) {
118 | $params["--fixtures"] = $fixturesPath;
119 | }
120 |
121 | $this->runConsoleCommand($fixtureLoadCommand, $params);
122 | }
123 |
124 | /**
125 | * @large
126 | * @group functional
127 | */
128 | public function testScheduleWorks(): void
129 | {
130 | $container = $this->client->getContainer();
131 | /** @var EventDispatcherInterface $eventDispatcher */
132 | $eventDispatcher = $container->get('event_dispatcher');
133 |
134 | $_SERVER['SYMFONY_CONSOLE_FILE'] = $container->getParameter('kernel.root_dir') . DIRECTORY_SEPARATOR . 'console';
135 |
136 | /** @var EntityManager $clientEm */
137 | $clientEm = $container->get("doctrine")->getManagerForClass(Client::class);
138 | $clientRepo = $clientEm->getRepository(Client::class);
139 | /** @var Client $subject */
140 | $subject = $clientRepo->findOneBy(['name' => "Johnny"]);
141 | $event = new Event($subject);
142 |
143 | // simply check that client can be activated (also sleeping transition should be scheduled (+1s) inside)
144 | $eventDispatcher->dispatch('activating.event', $event);
145 | self::assertEquals('active', $subject->getStatus());
146 |
147 | // sleep for a while (wait 2 sec to avoid "the same second" collision)
148 | // and run scheduler with 1-sec runtime to check that sleeping transition is executed
149 | sleep(2);
150 | $this->runScheduler(1);
151 | $clientEm->refresh($subject);
152 | self::assertEquals('sleeping', $subject->getStatus());
153 |
154 | $eventDispatcher->dispatch('prolong.event', $event);
155 |
156 | // sleep for a while (in order to execute scheduler in the time when closing transition should be applied without prolong.event fired)
157 | // and run scheduler with 1-sec runtime to check that closed transition is not executed due to prolongation
158 | usleep(500000);
159 | $this->runScheduler(1);
160 | $clientEm->refresh($subject);
161 | self::assertEquals('sleeping', $subject->getStatus());
162 |
163 | // sleep for a while (some time we waste during waiting that scheduler finish his work so no need to wait so long)
164 | // and run scheduler with 1-sec runtime to check that closed transition is executed
165 | $this->runScheduler(1);
166 | $clientEm->refresh($subject);
167 | self::assertEquals('closed', $subject->getStatus());
168 | }
169 |
170 | protected function runScheduler($runtime = 1)
171 | {
172 | $schedulerCommand = new RunCommand();
173 | $this->runConsoleCommand(
174 | $schedulerCommand,
175 | [
176 | '--max-runtime' => $runtime,
177 | // set worker name explicitly in order to avoid errors caused by 50 characters name restrictions on
178 | // testing envs like travis
179 | '--worker-name' => uniqid("worker_", true)
180 | ]
181 | );
182 | }
183 |
184 | private function runConsoleCommand(Command $command, array $params = []): int
185 | {
186 | $command->setApplication($this->app);
187 | // use CommandTester to simple command running
188 | $commandRunner = new CommandTester($command);
189 | return $commandRunner->execute($params);
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/tests/Functional/TestCase.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 29.06.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Functional;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Kernel\KernelBuilder;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\Functional\Kernel\TestKernelInterface;
17 | use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
18 | use Symfony\Component\Filesystem\Filesystem;
19 |
20 | class TestCase extends BaseWebTestCase
21 | {
22 | /**
23 | * {@inheritdoc}
24 | */
25 | public static function tearDownAfterClass(): void
26 | {
27 | static::deleteTmpDirs();
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | protected static function getKernelClass()
34 | {
35 | if (defined("KERNEL_CLASS")) {
36 | return KERNEL_CLASS;
37 | } else {
38 | return null;
39 | }
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | protected static function createKernel(array $options = array())
46 | {
47 | return KernelBuilder::getKernel(static::getKernelClass(), $options);
48 | }
49 |
50 | /**
51 | * Clears temporary test kernel application folder in case of kernel implements TestKernelInterface
52 | * @see TestKernelInterface::getTempAppDir
53 | *
54 | * @return void
55 | */
56 | protected static function deleteTmpDirs(): void
57 | {
58 | if (static::$kernel) {
59 | $kernel = static::$kernel;
60 | $fs = new Filesystem();
61 | if ($kernel instanceof TestKernelInterface) {
62 | $fs->remove($kernel->getTempAppDir());
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/Guard/ExpressionGuardTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 18.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Guard;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\UnsupportedGuardEventException;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
17 | use PHPUnit\Framework\MockObject\MockObject;
18 | use PHPUnit\Framework\TestCase;
19 | use Psr\Log\LoggerInterface;
20 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
21 | use Symfony\Component\Workflow\Event\GuardEvent;
22 | use Symfony\Component\Workflow\Registry;
23 | use Symfony\Component\Workflow\Transition;
24 | use Symfony\Component\Workflow\Workflow;
25 |
26 | class ExpressionGuardTest extends TestCase
27 | {
28 | public function testHandlingUnsupportedEventsTriggersException(): void
29 | {
30 | $this->expectException(UnsupportedGuardEventException::class);
31 | $guard = new ExpressionGuard(
32 | $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock(),
33 | $this->getMockBuilder(SubjectManipulator::class)->disableOriginalConstructor()->getMock(),
34 | $this->getMockBuilder(Registry::class)->disableOriginalConstructor()->getMock(),
35 | $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock()
36 | );
37 | $event = $this->createMockedEvent();
38 |
39 | $guard->guardTransition($event, "ghost_event");
40 | }
41 |
42 | public function testGuardExpressionFailuresAreReportedByLogger(): void
43 | {
44 | $logger = $this->getMockBuilder(LoggerInterface::class)->getMockForAbstractClass();
45 | $logger->expects(self::once())->method('error');
46 |
47 | $invalidExpression = "expression";
48 | $language = $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock();
49 | $language->expects(self::once())->method("evaluate")->with($invalidExpression)->willThrowException(new \Exception());
50 |
51 | $guard = new ExpressionGuard(
52 | $language,
53 | $this->getMockBuilder(SubjectManipulator::class)->disableOriginalConstructor()->getMock(),
54 | $this->prepareValidWorkflowRegistryMock(),
55 | $logger
56 | );
57 |
58 | $guard->registerGuardExpression("eventName", "workflow", $invalidExpression);
59 |
60 | $guard->guardTransition(
61 | $this->createMockedEvent(),
62 | "eventName"
63 | );
64 | }
65 |
66 | public function testGuardExpressionFailuresDoNotBlocksTransition(): void
67 | {
68 | $logger = $this->getMockBuilder(LoggerInterface::class)->getMockForAbstractClass();
69 | $logger->expects(self::once())->method('error');
70 |
71 | $invalidExpression = "expression";
72 | $language = $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock();
73 | $language->expects(self::once())->method("evaluate")->with($invalidExpression)->willThrowException(new \Exception());
74 |
75 | $guard = new ExpressionGuard(
76 | $language,
77 | $this->getMockBuilder(SubjectManipulator::class)->disableOriginalConstructor()->getMock(),
78 | $this->prepareValidWorkflowRegistryMock(),
79 | $logger
80 | );
81 |
82 | $event = $this->createMockedEvent();
83 | $event->expects(self::never())->method("setBlocked");
84 |
85 | $guard->registerGuardExpression("eventName", "workflow", "expression");
86 | $guard->guardTransition($event ,"eventName");
87 | }
88 |
89 | /**
90 | * @dataProvider expressionProvider
91 | */
92 | public function testValidExpressionAllowsOrBlocksTransitionWithLogReport(
93 | string $expression,
94 | $expressionResult,
95 | bool $blockTransition,
96 | bool $convertToBoolean = false
97 | ): void {
98 | $logger = $this->getMockBuilder(LoggerInterface::class)->getMockForAbstractClass();
99 |
100 | $language = $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock();
101 | $language->expects(self::once())->method("evaluate")->with($expression)->willReturn($expressionResult);
102 |
103 | $guard = new ExpressionGuard(
104 | $language,
105 | $this->getMockBuilder(SubjectManipulator::class)->disableOriginalConstructor()->getMock(),
106 | $this->prepareValidWorkflowRegistryMock(),
107 | $logger
108 | );
109 |
110 | $event = $this->createMockedEvent();
111 | $event->expects(self::once())->method("setBlocked")->with($blockTransition);
112 |
113 | $loggerInvocationCount = 0;
114 | if ($convertToBoolean) {
115 | // in case of convertation logger should report about it
116 | $loggerInvocationCount++;
117 | }
118 | if ($blockTransition) {
119 | // in case of transition blocking logger should report about it
120 | $loggerInvocationCount++;
121 | $transition = $this->getMockBuilder(Transition::class)->disableOriginalConstructor()->getMock();
122 | $transition->expects(self::once())->method("getName");
123 | $event->expects(self::once())->method("getTransition")->willReturn($transition);
124 | }
125 | $logger->expects($this->exactly($loggerInvocationCount))->method("debug");
126 |
127 | $guard->registerGuardExpression("eventName", "workflow", $expression);
128 | $guard->guardTransition($event ,"eventName");
129 | }
130 |
131 | public function expressionProvider(): array
132 | {
133 | return [
134 | ["boolenFalseExpression", false, false],
135 | ["boolenTrueExpression", true, true],
136 | // convertations to boolean
137 | ["convertableToTrueExpression", "1", true, true],
138 | ["convertableToFalseExpression", "0", false, true],
139 | ["convertableToTrueExpression", 1, true, true],
140 | ["convertableToFalseExpression", 0, false, true],
141 | ];
142 | }
143 |
144 | /**
145 | * @return MockObject|Registry
146 | */
147 | private function prepareValidWorkflowRegistryMock(): Registry
148 | {
149 | $workflow = $this->getMockBuilder(Workflow::class)
150 | ->setMethods(['getName'])
151 | ->disableOriginalConstructor()
152 | ->getMock();
153 | $workflow->expects(self::once())->method('getName')->willReturn('test');
154 | $workflowRegistry = $this->getMockBuilder(Registry::class)
155 | ->setMethods(['get'])
156 | ->getMockForAbstractClass();
157 | $workflowRegistry->expects(self::once())->method('get')->willReturn($workflow);
158 |
159 | return $workflowRegistry;
160 | }
161 |
162 | /**
163 | * @return GuardEvent|MockObject
164 | */
165 | private function createMockedEvent(): GuardEvent
166 | {
167 | $mock = $this->getMockBuilder(GuardEvent::class)
168 | ->setMethods(['getSubject', 'setBlocked', 'getTransition'])
169 | ->disableOriginalConstructor()
170 | ->getMock();
171 |
172 | $mock->expects(self::any())
173 | ->method('getSubject')
174 | ->willReturn(new \stdClass());
175 |
176 | return $mock;
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/tests/Schedule/ActionSchedulerTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 09.08.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Schedule;
14 |
15 | use Carbon\Carbon;
16 | use Doctrine\Common\Persistence\ObjectManager;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Entity\Repository\ScheduledJobRepository;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\Entity\ScheduledJob;
19 | use Gtt\Bundle\WorkflowExtensionsBundle\Schedule\ValueObject\ScheduledAction;
20 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
21 | use JMS\JobQueueBundle\Entity\Job;
22 | use PHPUnit\Framework\MockObject\MockObject;
23 | use PHPUnit\Framework\TestCase;
24 | use Psr\Log\LoggerInterface;
25 | use Symfony\Component\Workflow\Workflow;
26 |
27 | class ActionSchedulerTest extends TestCase
28 | {
29 | public function tearDown(): void
30 | {
31 | parent::tearDown();
32 | Carbon::setTestNow();
33 | }
34 |
35 | /**
36 | * @dataProvider scheduledActionProvider
37 | */
38 | public function testSchedulerSchedulesCorrectJob(ScheduledAction $action, bool $alreadyScheduled): void
39 | {
40 | /** @var WorkflowContext $workflowContext */
41 | [$workflowContext, $repository, $em] = $this->setupSchedulerContext();
42 |
43 | Carbon::setTestNow(Carbon::now());
44 | $now = Carbon::now();
45 |
46 | $executeJobAfter = clone $now;
47 | $executeJobAfter->add($action->getOffset());
48 |
49 | if ($action->isReschedulable()) {
50 | if ($alreadyScheduled) {
51 | // expecting rescheduling
52 | $actionJob = new Job('command', ['--some' => 'args']);
53 | $scheduledJob = new ScheduledJob($actionJob);
54 |
55 | $expectedActionJob = clone $actionJob;
56 | $expectedActionJob->setExecuteAfter($executeJobAfter);
57 |
58 | $repository->expects(self::once())->method('findScheduledJobToReschedule')->willReturn($scheduledJob);
59 | $em->expects(self::once())->method('persist')->with(self::callback(static function (Job $actual) use ($expectedActionJob) {
60 | self::assertEquals($expectedActionJob->getExecuteAfter(), $actual->getExecuteAfter());
61 |
62 | return true;
63 | }));
64 | } else {
65 | // expecting the new one
66 | $repository->expects(self::once())->method('findScheduledJobToReschedule')->willReturn(null);
67 | $this->configureEmToExpectNewJobCreation($em, $action, $workflowContext, $executeJobAfter);
68 | }
69 | } else {
70 | // non-reschedulable action - expecting creation of the new job
71 | $repository->expects(self::never())->method('findScheduledJobToReschedule')->willReturn(null);
72 | $this->configureEmToExpectNewJobCreation($em, $action, $workflowContext, $executeJobAfter);
73 | }
74 |
75 | $scheduler = new ActionScheduler($em, $this->getMockBuilder(LoggerInterface::class)->getMockForAbstractClass());
76 | $scheduler->scheduleAction($workflowContext, $action);
77 | }
78 |
79 | public function scheduledActionProvider(): array
80 | {
81 | return [
82 | [new ScheduledAction('a1', [], 'PT1S'), true],
83 | [new ScheduledAction('a1', [], 'PT1S'), false],
84 | [new ScheduledAction('a1', [], 'PT1S', true), true],
85 | [new ScheduledAction('a1', [], 'PT1S', true), false]
86 | ];
87 | }
88 |
89 | private function configureEmToExpectNewJobCreation(ObjectManager $em, ScheduledAction $action, WorkflowContext $workflowContext, \DateTime $executeJobAfter): void
90 | {
91 | $expectedActionJob = new Job(
92 | 'workflow:action:execute',
93 | [
94 | '--action=' . $action->getName(),
95 | '--arguments=' . json_encode($action->getArguments()),
96 | '--workflow=' . $workflowContext->getWorkflow()->getName(),
97 | '--subjectClass=' . get_class($workflowContext->getSubject()),
98 | '--subjectId=' . $workflowContext->getSubjectId()
99 | ]
100 | );
101 |
102 | $expectedActionJob->setExecuteAfter($executeJobAfter);
103 |
104 | /** @var $em MockObject|ObjectManager */
105 | $em->expects(self::exactly(2))->method('persist')->withConsecutive(
106 | self::equalTo($expectedActionJob),
107 | self::equalTo(new ScheduledJob($expectedActionJob, $action->isReschedulable()))
108 | );
109 | }
110 |
111 | /**
112 | * @return array
113 | */
114 | private function setupSchedulerContext(): array
115 | {
116 | $subject = new \StdClass();
117 | $subjectId = '1';
118 |
119 | $workflow = $this->getMockBuilder(Workflow::class)->disableOriginalConstructor()->getMock();
120 | $workflow->expects(self::any())->method('getName')->willReturn('w1');
121 |
122 | $workflowContext = new WorkflowContext($workflow, $subject, $subjectId);
123 |
124 | $repository = $this->getMockBuilder(ScheduledJobRepository::class)->disableOriginalConstructor()->getMock();
125 |
126 | $em = $this->getMockBuilder(ObjectManager::class)->disableOriginalConstructor()->getMock();
127 | $em->expects(self::once())->method('getRepository')->with(ScheduledJob::class)->willReturn($repository);
128 |
129 | return [$workflowContext, $repository, $em];
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/tests/Trigger/Event/AbstractActionListenerTest.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * Date: 17.10.16
11 | */
12 | declare(strict_types=1);
13 |
14 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Trigger\Event;
15 |
16 | use Gtt\Bundle\WorkflowExtensionsBundle\DependencyInjection\Enum\ActionArgumentTypes;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\ActionException;
18 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
19 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
20 | use PHPUnit\Framework\TestCase;
21 | use Psr\Log\LoggerInterface;
22 | use ReflectionMethod;
23 | use Symfony\Component\EventDispatcher\Event;
24 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
25 | use Symfony\Component\Workflow\Registry;
26 |
27 | class AbstractActionListenerTest extends TestCase
28 | {
29 | /**
30 | * @dataProvider argumentsProvider
31 | */
32 | public function testResolveActionArguments(array $inputArgArray, $expectedResult, ExpressionLanguage $actionLanguage = null): void
33 | {
34 | if ($actionLanguage) {
35 | /** @var AbstractActionListener $listener */
36 | $listener = $this->getMockForAbstractClass(
37 | AbstractActionListener::class,
38 | [
39 | $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock(),
40 | $this->getMockBuilder(SubjectManipulator::class)->disableOriginalConstructor()->getMock(),
41 | $this->getMockBuilder(Registry::class)->disableOriginalConstructor()->getMock(),
42 | $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(),
43 | $actionLanguage
44 | ]
45 | );
46 | } else {
47 | /** @var AbstractActionListener $listener */
48 | $listener = $this->getMockForAbstractClass(AbstractActionListener::class, [], "", false);
49 | }
50 |
51 | $resolveActionArgumentsMethodRef = new ReflectionMethod($listener, 'resolveActionArguments');
52 | $resolveActionArgumentsMethodRef->setAccessible(true);
53 |
54 | $invokeArgs = [
55 | 'actionName',
56 | $inputArgArray,
57 | new Event(),
58 | $this->getMockBuilder(WorkflowContext::class)->disableOriginalConstructor()->getMock()
59 | ];
60 | if ($expectedResult instanceof \Exception) {
61 | $this->expectException(get_class($expectedResult));
62 | $resolveActionArgumentsMethodRef->invokeArgs($listener, $invokeArgs);
63 | } else {
64 | self::assertEquals($expectedResult, $resolveActionArgumentsMethodRef->invokeArgs($listener, $invokeArgs));
65 | }
66 | }
67 |
68 | public function argumentsProvider(): array
69 | {
70 | $data = [];
71 |
72 | // correct deep array
73 | $validExpression1 = 'e1';
74 | $validExpression2 = 'e2';
75 | $expressionResult1 = 'r1';
76 | $expressionResult2 = 'r2';
77 | $actionLanguage = $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock();
78 | $actionLanguage->expects(self::exactly(2))->method('evaluate')->will(
79 | self::onConsecutiveCalls($expressionResult1, $expressionResult2)
80 | );
81 |
82 | $data[] = [
83 | [
84 | [
85 | 'type' => ActionArgumentTypes::TYPE_EXPRESSION,
86 | 'value' => $validExpression1
87 | ],
88 | [
89 | 'type' => ActionArgumentTypes::TYPE_SCALAR,
90 | 'value' => 123e4
91 | ],
92 | [
93 | 'type' => ActionArgumentTypes::TYPE_ARRAY,
94 | 'value' =>
95 | [
96 | [
97 | 'type' => ActionArgumentTypes::TYPE_ARRAY,
98 | 'value' =>
99 | [
100 | [
101 | 'type' => ActionArgumentTypes::TYPE_EXPRESSION,
102 | 'value' => $validExpression2
103 | ]
104 | ]
105 | ],
106 | [
107 | 'type' => ActionArgumentTypes::TYPE_SCALAR,
108 | 'value' => "string"
109 | ],
110 | [
111 | 'type' => ActionArgumentTypes::TYPE_SCALAR,
112 | 'value' => 12
113 | ]
114 | ]
115 | ]
116 | ],
117 | [
118 | $expressionResult1,
119 | 123e4,
120 | [
121 | [
122 | $expressionResult2
123 | ],
124 | 'string',
125 | 12
126 | ]
127 | ],
128 | $actionLanguage
129 | ];
130 |
131 | // invalid expression 1
132 | $actionLanguage = $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock();
133 | $actionLanguage->expects(self::once())->method('evaluate')->willReturn(new \StdClass());
134 | $data[] = [
135 | [
136 | [
137 | 'type' => ActionArgumentTypes::TYPE_EXPRESSION,
138 | 'value' => "some exp"
139 | ]
140 | ],
141 | new ActionException(),
142 | $actionLanguage
143 | ];
144 |
145 | // invalid expression 2
146 | $actionLanguage = $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock();
147 | $actionLanguage->expects(self::once())->method('evaluate')->willReturn(["test" => 1]);
148 | $data[] = [
149 | [
150 | [
151 | 'type' => ActionArgumentTypes::TYPE_EXPRESSION,
152 | 'value' => "some exp"
153 | ]
154 | ],
155 | new ActionException(),
156 | $actionLanguage
157 | ];
158 |
159 | // associative array keys are dropped
160 | $data[] = [
161 | [
162 | [
163 | 'type' => ActionArgumentTypes::TYPE_ARRAY,
164 | 'value' => [
165 | "test" => [
166 | 'type' => ActionArgumentTypes::TYPE_SCALAR,
167 | 'value' => 123
168 | ]
169 | ]
170 | ]
171 | ],
172 | [[123]]
173 | ];
174 |
175 | return $data;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/tests/Trigger/Event/AbstractListenerTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 14.07.16
10 | */
11 | declare(strict_types=1);
12 |
13 | namespace Gtt\Bundle\WorkflowExtensionsBundle\Trigger\Event;
14 |
15 | use Gtt\Bundle\WorkflowExtensionsBundle\Exception\UnsupportedTriggerEventException;
16 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowContext;
17 | use Gtt\Bundle\WorkflowExtensionsBundle\WorkflowSubject\SubjectManipulator;
18 | use PHPUnit\Framework\MockObject\MockObject;
19 | use PHPUnit\Framework\TestCase;
20 | use Psr\Log\LoggerInterface;
21 | use ReflectionMethod;
22 | use Symfony\Component\EventDispatcher\Event;
23 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
24 | use Symfony\Component\Workflow\Registry;
25 | use Symfony\Component\Workflow\Workflow;
26 |
27 | class AbstractListenerTest extends TestCase
28 | {
29 | public function testHandlingUnsupportedEventsTriggersException(): void
30 | {
31 | $this->expectException(UnsupportedTriggerEventException::class);
32 | /** @var AbstractListener $listener */
33 | $listener = self::getMockForAbstractClass(AbstractListener::class, [], "", false);
34 | $listener->dispatchEvent(new Event(), "ghost_event");
35 | }
36 |
37 | public function testUnretrievableSubjectIsReportedByLogger()
38 | {
39 | $logger = $this->getMockBuilder(LoggerInterface::class)->getMockForAbstractClass();
40 | $logger->expects(self::once())->method('error');
41 |
42 | [$event, $expression, $language] = $this->getEventExpressionAndLanguage(true);
43 |
44 | /** @var AbstractListener $listener */
45 | $listener = self::getMockForAbstractClass(
46 | AbstractListener::class,
47 | [
48 | $language,
49 | $this->getMockBuilder(SubjectManipulator::class)->disableOriginalConstructor()->getMock(),
50 | $this->getMockBuilder(Registry::class)->disableOriginalConstructor()->getMock(),
51 | $logger
52 | ]
53 | );
54 |
55 | $configureEventMethodRef = new ReflectionMethod($listener, 'configureSubjectRetrievingForEvent');
56 | $configureEventMethodRef->setAccessible(true);
57 | $configureEventMethodRef->invokeArgs($listener, ['eventName', 'workflowName', $expression]);
58 |
59 | $listener->dispatchEvent($event, "eventName");
60 | }
61 |
62 | public function testSupportedEventIsHandled(): void
63 | {
64 | [$event, $expression, $language] = $this->getEventExpressionAndLanguage();
65 | $eventName = "eventName";
66 |
67 | $workflowRegistry = $this->getMockBuilder(Registry::class)->disableOriginalConstructor()->getMock();
68 | $workflowRegistry->expects(self::once())->method('get')->willReturn(
69 | $this->getMockBuilder(Workflow::class)->disableOriginalConstructor()->getMock()
70 | );
71 |
72 | /** @var AbstractListener|MockObject $listener */
73 | $listener = self::getMockForAbstractClass(
74 | AbstractListener::class,
75 | [
76 | $language,
77 | $this->getMockBuilder(SubjectManipulator::class)->disableOriginalConstructor()->getMock(),
78 | $workflowRegistry,
79 | $this->getMockBuilder(LoggerInterface::class)->getMockForAbstractClass()
80 | ]
81 | );
82 |
83 | $listener
84 | ->expects(self::once())
85 | ->method('handleEvent')
86 | ->with(
87 | self::equalTo($eventName),
88 | self::equalTo($event),
89 | // event config
90 | self::isType('array'),
91 | self::isInstanceOf(WorkflowContext::class)
92 | )
93 | ;
94 |
95 | $configureEventMethodRef = new ReflectionMethod($listener, 'configureSubjectRetrievingForEvent');
96 | $configureEventMethodRef->setAccessible(true);
97 | $configureEventMethodRef->invokeArgs($listener, ['eventName', 'workflowName', $expression]);
98 |
99 | $listener->dispatchEvent($event, $eventName);
100 | }
101 |
102 | /**
103 | * @return array
104 | */
105 | private function getEventExpressionAndLanguage(bool $expressionEvaluationShouldThrowException = false): array
106 | {
107 | $subject = new \StdClass();
108 | $event = new Event();
109 |
110 | $expression = "expression";
111 | $language = $this->getMockBuilder(ExpressionLanguage::class)->disableOriginalConstructor()->getMock();
112 | if ($expressionEvaluationShouldThrowException) {
113 | $language->expects(self::once())->method("evaluate")->with($expression, ['event' => $event])->willThrowException(new \Exception());
114 | } else {
115 | $language->expects(self::once())->method("evaluate")->with($expression, ['event' => $event])->willReturn($subject);
116 | }
117 | return array($event, $expression, $language);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
9 | * @date 17.07.15
10 | */
11 |
12 | use Composer\Autoload\ClassLoader;
13 | use Doctrine\Common\Annotations\AnnotationRegistry;
14 |
15 | /**
16 | * @var ClassLoader $loader
17 | */
18 | $loader = require __DIR__.'/../vendor/autoload.php';
19 | AnnotationRegistry::registerLoader(array($loader, 'loadClass'));
20 | return $loader;
21 |
--------------------------------------------------------------------------------