AbstractInstantiationCmd.php$0 ➔ isFulfilled()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 2
1
<?php
2
3
namespace Jabe\Impl\Cmd;
4
5
use Jabe\ProcessEngineException;
6
use Jabe\Exception\NotValidException;
7
use Jabe\Impl\ActivityExecutionTreeMapping;
8
use Jabe\Impl\Bpmn\Behavior\SequentialMultiInstanceActivityBehavior;
9
use Jabe\Impl\Bpmn\Helper\BpmnProperties;
10
use Jabe\Impl\Core\Model\CoreModelElement;
11
use Jabe\Impl\Interceptor\CommandContext;
12
use Jabe\Impl\Persistence\Entity\ExecutionEntity;
13
use Jabe\Impl\Pvm\{
14
    PvmActivityInterface,
15
    PvmScopeInterface,
16
    PvmTransitionInterface
17
};
18
use Jabe\Impl\Pvm\Process\{
19
    ActivityImpl,
20
    ActivityStartBehavior,
21
    ProcessDefinitionImpl,
22
    ScopeImpl,
23
    TransitionImpl
24
};
25
use Jabe\Impl\Tree\{
26
    ActivityStackCollector,
27
    FlowScopeWalker,
28
    ReferenceWalker,
29
    WalkConditionInterface
30
};
31
use Jabe\Impl\Util\EnsureUtil;
32
use Jabe\Variable\VariableMapInterface;
33
use Jabe\Variable\Impl\VariableMapImpl;
34
35
abstract class AbstractInstantiationCmd extends AbstractProcessInstanceModificationCommand
36
{
37
    protected $variables;
38
    protected $variablesLocal;
39
    protected $ancestorActivityInstanceId;
40
41
    public function __construct(string $processInstanceId, ?string $ancestorActivityInstanceId = null)
42
    {
43
        parent::__construct($processInstanceId);
44
        $this->ancestorActivityInstanceId = $ancestorActivityInstanceId;
45
        $this->variables = new VariableMapImpl();
46
        $this->variablesLocal = new VariableMapImpl();
47
    }
48
49
    public function addVariable(string $name, $value): void
50
    {
51
        $this->variables->put($name, $value);
52
    }
53
54
    public function addVariableLocal(string $name, $value): void
55
    {
56
        $this->variablesLocal->put($name, $value);
57
    }
58
59
    public function addVariables(array $variables): void
60
    {
61
        $this->variables->putAll($variables);
62
    }
63
64
    public function addVariablesLocal(array $variables): void
65
    {
66
        $this->variablesLocal->putAll($variables);
67
    }
68
69
    public function getVariables(): VariableMapInterface
70
    {
71
        return $this->variables;
72
    }
73
74
    public function getVariablesLocal(): VariableMapInterface
75
    {
76
        return $this->variablesLocal;
77
    }
78
79
    public function execute(CommandContext $commandContext)
80
    {
81
        $processInstance = $commandContext->getExecutionManager()->findExecutionById($this->processInstanceId);
82
83
        $processDefinition = $processInstance->getProcessDefinition();
84
85
        $elementToInstantiate = $this->getTargetElement($processDefinition);
86
87
        EnsureUtil::ensureNotNull(
88
            $this->describeFailure("Element '" . $this->getTargetElementId() . "' does not exist in process '" . $processDefinition->getId() . "'"),
89
            "element",
90
            $elementToInstantiate
91
        );
92
93
        // rebuild the mapping because the execution tree changes with every iteration
94
        $mapping = new ActivityExecutionTreeMapping($commandContext, $this->processInstanceId);
95
96
        // before instantiating an activity, two things have to be determined:
97
        //
98
        // activityStack:
99
        // For the activity to instantiate, we build a stack of parent flow scopes
100
        // for which no executions exist yet and that have to be instantiated
101
        //
102
        // scopeExecution:
103
        // This is typically the execution under which a new sub tree has to be created.
104
        // if an explicit ancestor activity instance is set:
105
        //   - this is the scope execution for that ancestor activity instance
106
        //   - throws exception if that scope execution is not in the parent hierarchy
107
        //     of the activity to be started
108
        // if no explicit ancestor activity instance is set:
109
        //   - this is the execution of the first parent/ancestor flow scope that has an execution
110
        //   - throws an exception if there is more than one such execution
111
112
        $targetFlowScope = $this->getTargetFlowScope($processDefinition);
113
114
        // prepare to walk up the flow scope hierarchy and collect the flow scope activities
115
        $stackCollector = new ActivityStackCollector();
116
        $walker = new FlowScopeWalker($targetFlowScope);
117
        $walker->addPreVisitor($stackCollector);
118
119
        $scopeExecution = null;
120
121
        // if no explicit ancestor activity instance is set
122
        if ($this->ancestorActivityInstanceId === null) {
123
            // walk until a scope is reached for which executions exist
124
            $walker->walkWhile(new class ($processDefinition, $mapping) implements WalkConditionInterface {
125
126
                private $processDefinition;
127
                private $mapping;
128
129
                public function __construct(ProcessDefinitionImpl $processDefinition, ActivityExecutionTreeMapping $mapping)
130
                {
131
                    $this->processDefinition = $processDefinition;
132
                    $this->mapping = $mapping;
133
                }
134
135
                public function isFulfilled(ScopeImpl $element): bool
136
                {
137
                    return !empty($this->mapping->getExecutions($element)) || $element == $this->processDefinition;
138
                }
139
            });
140
141
            $flowScopeExecutions = $mapping->getExecutions($walker->getCurrentElement());
142
143
            if (count($flowScopeExecutions) > 1) {
144
                throw new ProcessEngineException("Ancestor activity execution is ambiguous for activity " . $targetFlowScope);
145
            }
146
            $scopeExecution = $flowScopeExecutions[0];
147
        } else {
148
            $processInstanceId = $this->processInstanceId;
149
            $tree = $commandContext->runWithoutAuthorization(function () use ($commandContext, $processInstanceId) {
150
                $cmd = new GetActivityInstanceCmd($processInstanceId);
151
                return $cmd->execute($commandContext);
152
            });
153
154
            $ancestorInstance = $this->findActivityInstance($tree, $this->ancestorActivityInstanceId);
0 ignored issues
show
Bug introduced by
It seems like $tree can also be of type null; however, parameter $tree of Jabe\Impl\Cmd\AbstractPr...:findActivityInstance() does only seem to accept Jabe\Runtime\ActivityInstanceInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

154
            $ancestorInstance = $this->findActivityInstance(/** @scrutinizer ignore-type */ $tree, $this->ancestorActivityInstanceId);
Loading history...
155
            EnsureUtil::ensureNotNull(
156
                describeFailure("Ancestor activity instance '" . $this->ancestorActivityInstanceId . "' does not exist"),
0 ignored issues
show
Bug introduced by
The function describeFailure was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

156
                /** @scrutinizer ignore-call */ 
157
                describeFailure("Ancestor activity instance '" . $this->ancestorActivityInstanceId . "' does not exist"),
Loading history...
157
                "ancestorInstance",
158
                $ancestorInstance
159
            );
160
161
            // determine ancestor activity scope execution and activity
162
            $ancestorScopeExecution = $this->getScopeExecutionForActivityInstance(
163
                $processInstance,
0 ignored issues
show
Bug introduced by
It seems like $processInstance can also be of type null; however, parameter $processInstance of Jabe\Impl\Cmd\AbstractPr...onForActivityInstance() does only seem to accept Jabe\Impl\Persistence\Entity\ExecutionEntity, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
                /** @scrutinizer ignore-type */ $processInstance,
Loading history...
164
                $mapping,
165
                $ancestorInstance
166
            );
167
            $ancestorScope = $this->getScopeForActivityInstance($processDefinition, $ancestorInstance);
168
169
            // walk until the scope of the ancestor scope execution is reached
170
            $walker->walkWhile(new class ($mapping, $ancestorScope, $ancestorScopeExecution, $processDefinition) implements WalkConditionInterface {
171
                private $mapping;
172
                private $ancestorScope;
173
                private $ancestorScopeExecution;
174
                private $processDefinition;
175
176
                public function __construct($mapping, $ancestorScope, $ancestorScopeExecution, $processDefinition)
177
                {
178
                    $this->mapping = $mapping;
179
                    $this->ancestorScope = $ancestorScope;
180
                    $this->ancestorScopeExecution = $ancestorScopeExecution;
181
                    $this->processDefinition = $processDefinition;
182
                }
183
184
                public function isFulfilled(ScopeImpl $element): bool
185
                {
186
                    return (
187
                        in_array($this->ancestorScopeExecution, $this->mapping->getExecutions($element))
188
                        && $element == $this->ancestorScope)
189
                        || $element == $this->processDefinition;
190
                }
191
            });
192
193
            $flowScopeExecutions = $mapping->getExecutions($walker->getCurrentElement());
194
195
            if (!in_array($ancestorScopeExecution, $flowScopeExecutions)) {
196
                throw new NotValidException(describeFailure("Scope execution for '" . $this->ancestorActivityInstanceId .
197
                "' cannot be found in parent hierarchy of flow element '" . $elementToInstantiate->getId() . "'"));
198
            }
199
200
            $scopeExecution = $ancestorScopeExecution;
201
        }
202
203
        $activitiesToInstantiate = $stackCollector->getActivityStack();
204
        $activitiesToInstantiate = array_reverse($activitiesToInstantiate);
205
206
        // We have to make a distinction between
207
        // - "regular" activities for which the activity stack can be instantiated and started
208
        //   right away
209
        // - interrupting or cancelling activities for which we have to ensure that
210
        //   the interruption and cancellation takes place before we instantiate the activity stack
211
        $topMostActivity = null;
212
        $flowScope = null;
213
        if (!empty($activitiesToInstantiate)) {
214
            $topMostActivity = $activitiesToInstantiate[0];
215
            $flowScope = $topMostActivity->getFlowScope();
216
        } elseif (is_a($elementToInstantiate, ActivityImpl::class)) {
217
            $topMostActivity = $elementToInstantiate;
218
            $flowScope = $topMostActivity->getFlowScope();
219
        } elseif (is_a($elementToInstantiate, TransitionImpl::class)) {
220
            $transitionToInstantiate = $elementToInstantiate;
221
            $flowScope = $transitionToInstantiate->getSource()->getFlowScope();
222
        }
223
224
        if (!$this->supportsConcurrentChildInstantiation($flowScope)) {
225
            throw new ProcessEngineException(
226
                "Concurrent instantiation not possible for activities in scope " . $flowScope->getId()
227
            );
228
        }
229
230
        $startBehavior = ActivityStartBehavior::CONCURRENT_IN_FLOW_SCOPE;
231
        if ($topMostActivity !== null) {
232
            $startBehavior = $topMostActivity->getActivityStartBehavior();
233
234
            if (!empty($activitiesToInstantiate)) {
235
                // this is in BPMN relevant if there is an interrupting event sub process.
236
                // we have to distinguish between instantiation of the start event and any other activity.
237
                // instantiation of the start event means interrupting behavior; instantiation
238
                // of any other task means no interruption.
239
                $initialActivity = null;
240
                $props = $topMostActivity->getProperties();
241
                if (array_key_exists(BpmnProperties::INITIAL_ACTIVITY, $props)) {
0 ignored issues
show
Bug introduced by
The constant Jabe\Impl\Bpmn\Helper\Bp...rties::INITIAL_ACTIVITY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
242
                    $initialActivity = $props[BpmnProperties::INITIAL_ACTIVITY];
243
                }
244
                $secondTopMostActivity = null;
245
                if (count($activitiesToInstantiate) > 1) {
246
                    $secondTopMostActivity = $activitiesToInstantiate[1];
247
                } elseif (is_a($elementToInstantiate, ActivityImpl::class)) {
248
                    $secondTopMostActivity = $elementToInstantiate;
249
                }
250
251
                if ($initialActivity != $secondTopMostActivity) {
252
                    $startBehavior = ActivityStartBehavior::CONCURRENT_IN_FLOW_SCOPE;
253
                }
254
            }
255
        }
256
257
        switch ($startBehavior) {
258
            case self::CANCEL_EVENT_SCOPE:
0 ignored issues
show
Bug introduced by
The constant Jabe\Impl\Cmd\AbstractIn...Cmd::CANCEL_EVENT_SCOPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
259
                $scopeToCancel = $topMostActivity->getEventScope();
260
                $executionToCancel = $this->getSingleExecutionForScope($mapping, $scopeToCancel);
261
                if ($executionToCancel !== null) {
262
                    $executionToCancel->deleteCascade("Cancelling activity " . $topMostActivity . " executed.", $this->skipCustomListeners, $this->skipIoMappings);
263
                    $this->instantiate($executionToCancel->getParent(), $activitiesToInstantiate, $elementToInstantiate);
0 ignored issues
show
Bug introduced by
It seems like $executionToCancel->getParent() can also be of type null; however, parameter $ancestorScopeExecution of Jabe\Impl\Cmd\AbstractIn...ationCmd::instantiate() does only seem to accept Jabe\Impl\Persistence\Entity\ExecutionEntity, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

263
                    $this->instantiate(/** @scrutinizer ignore-type */ $executionToCancel->getParent(), $activitiesToInstantiate, $elementToInstantiate);
Loading history...
264
                } else {
265
                    $flowScopeExecution = $this->getSingleExecutionForScope($mapping, $topMostActivity->getFlowScope());
266
                    $this->instantiateConcurrent($flowScopeExecution, $activitiesToInstantiate, $elementToInstantiate);
0 ignored issues
show
Bug introduced by
It seems like $flowScopeExecution can also be of type null; however, parameter $ancestorScopeExecution of Jabe\Impl\Cmd\AbstractIn...instantiateConcurrent() does only seem to accept Jabe\Impl\Persistence\Entity\ExecutionEntity, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

266
                    $this->instantiateConcurrent(/** @scrutinizer ignore-type */ $flowScopeExecution, $activitiesToInstantiate, $elementToInstantiate);
Loading history...
267
                }
268
                break;
269
            case self::INTERRUPT_EVENT_SCOPE:
0 ignored issues
show
Bug introduced by
The constant Jabe\Impl\Cmd\AbstractIn...::INTERRUPT_EVENT_SCOPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
270
                 $scopeToCancel = $topMostActivity->getEventScope();
271
                $executionToCancel = $this->getSingleExecutionForScope($mapping, $scopeToCancel);
272
                $executionToCancel->interrupt("Interrupting activity " . $topMostActivity . " executed.", $this->skipCustomListeners, $this->skipIoMappings, false);
273
                $executionToCancel->setActivity(null);
274
                $executionToCancel->leaveActivityInstance();
275
                $this->instantiate($executionToCancel, $activitiesToInstantiate, $elementToInstantiate);
276
                break;
277
            case self::INTERRUPT_FLOW_SCOPE:
0 ignored issues
show
Bug introduced by
The constant Jabe\Impl\Cmd\AbstractIn...d::INTERRUPT_FLOW_SCOPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
278
                $scopeToCancel = $topMostActivity->getFlowScope();
279
                $executionToCancel = $this->getSingleExecutionForScope($mapping, $scopeToCancel);
280
                $executionToCancel->interrupt("Interrupting activity " . $topMostActivity . " executed.", $this->skipCustomListeners, $this->skipIoMappings, false);
281
                $executionToCancel->setActivity(null);
282
                $executionToCancel->leaveActivityInstance();
283
                $this->instantiate($executionToCancel, $activitiesToInstantiate, $elementToInstantiate);
284
                break;
285
            default:
286
                // if all child executions have been cancelled
287
                // or this execution has ended executing its scope, it can be reused
288
                if (
289
                    !$scopeExecution->hasChildren() &&
290
                    ($scopeExecution->getActivity() === null || $scopeExecution->isEnded())
291
                ) {
292
                    // reuse the scope execution
293
                    $this->instantiate($scopeExecution, $activitiesToInstantiate, $elementToInstantiate);
294
                } else {
295
                    // if the activity is not cancelling/interrupting, it can simply be instantiated as
296
                    // a concurrent child of the scopeExecution
297
                    $this->instantiateConcurrent($scopeExecution, $activitiesToInstantiate, $elementToInstantiate);
298
                }
299
                break;
300
        }
301
302
        return null;
303
    }
304
305
    /**
306
     * Cannot create more than inner instance in a sequential MI construct
307
     */
308
    protected function supportsConcurrentChildInstantiation(ScopeImpl $flowScope): bool
309
    {
310
        $behavior = $flowScope->getActivityBehavior();
311
        return $behavior === null || !($behavior instanceof SequentialMultiInstanceActivityBehavior);
312
    }
313
314
    protected function getSingleExecutionForScope(ActivityExecutionTreeMapping $mapping, ScopeImpl $scope): ?ExecutionEntity
315
    {
316
        $executions = $mapping->getExecutions($scope);
317
318
        if (!empty($executions)) {
319
            if (count($executions) > 1) {
320
                throw new ProcessEngineException("Executions for activity " . $scope . " ambiguous");
321
            }
322
            return $executions[0];
323
        } else {
324
            return null;
325
        }
326
    }
327
328
    protected function isConcurrentStart(string $startBehavior): bool
329
    {
330
        return $startBehavior == ActivityStartBehavior::DEFAULT
331
            || $startBehavior == ActivityStartBehavior::CONCURRENT_IN_FLOW_SCOPE;
332
    }
333
334
    protected function instantiate(ExecutionEntity $ancestorScopeExecution, array $parentFlowScopes, CoreModelElement $targetElement): void
335
    {
336
        if (is_a($targetElement, PvmTransitionInterface::class)) {
337
            $ancestorScopeExecution->executeActivities(
338
                $parentFlowScopes,
339
                null,
340
                $targetElement,
341
                $this->variables,
0 ignored issues
show
Bug introduced by
$this->variables of type Jabe\Variable\Impl\VariableMapImpl is incompatible with the type array|null expected by parameter $variables of Jabe\Impl\Pvm\Runtime\Pv...pl::executeActivities(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

341
                /** @scrutinizer ignore-type */ $this->variables,
Loading history...
342
                $this->variablesLocal,
0 ignored issues
show
Bug introduced by
$this->variablesLocal of type Jabe\Variable\Impl\VariableMapImpl is incompatible with the type array|null expected by parameter $localVariables of Jabe\Impl\Pvm\Runtime\Pv...pl::executeActivities(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

342
                /** @scrutinizer ignore-type */ $this->variablesLocal,
Loading history...
343
                $this->skipCustomListeners,
344
                $this->skipIoMappings
345
            );
346
        } elseif (is_a($targetElement, PvmActivityInterface::class)) {
347
            $ancestorScopeExecution->executeActivities(
348
                $parentFlowScopes,
349
                $targetElement,
350
                null,
351
                $this->variables,
352
                $this->variablesLocal,
353
                $this->skipCustomListeners,
354
                $this->skipIoMappings
355
            );
356
        } else {
357
            throw new ProcessEngineException("Cannot instantiate element " . $targetElement);
0 ignored issues
show
Bug introduced by
Are you sure $targetElement of type Jabe\Impl\Core\Model\CoreModelElement can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

357
            throw new ProcessEngineException("Cannot instantiate element " . /** @scrutinizer ignore-type */ $targetElement);
Loading history...
358
        }
359
    }
360
361
    protected function instantiateConcurrent(ExecutionEntity $ancestorScopeExecution, array $parentFlowScopes, CoreModelElement $targetElement): void
362
    {
363
        if (is_a($targetElement, PvmTransitionInterface::class)) {
364
            $ancestorScopeExecution->executeActivitiesConcurrent(
365
                $parentFlowScopes,
366
                null,
367
                $targetElement,
368
                $this->variables,
0 ignored issues
show
Bug introduced by
$this->variables of type Jabe\Variable\Impl\VariableMapImpl is incompatible with the type array|null expected by parameter $variables of Jabe\Impl\Pvm\Runtime\Pv...eActivitiesConcurrent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

368
                /** @scrutinizer ignore-type */ $this->variables,
Loading history...
369
                $this->variablesLocal,
0 ignored issues
show
Bug introduced by
$this->variablesLocal of type Jabe\Variable\Impl\VariableMapImpl is incompatible with the type array|null expected by parameter $localVariables of Jabe\Impl\Pvm\Runtime\Pv...eActivitiesConcurrent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

369
                /** @scrutinizer ignore-type */ $this->variablesLocal,
Loading history...
370
                $this->skipCustomListeners,
371
                $this->skipIoMappings
372
            );
373
        } elseif (is_a($targetElement, PvmActivityInterface::class)) {
374
            $ancestorScopeExecution->executeActivitiesConcurrent(
375
                $parentFlowScopes,
376
                $targetElement,
377
                null,
378
                $this->variables,
379
                $this->variablesLocal,
380
                $this->skipCustomListeners,
381
                $this->skipIoMappings
382
            );
383
        } else {
384
            throw new ProcessEngineException("Cannot instantiate element " . $targetElement);
0 ignored issues
show
Bug introduced by
Are you sure $targetElement of type Jabe\Impl\Core\Model\CoreModelElement can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

384
            throw new ProcessEngineException("Cannot instantiate element " . /** @scrutinizer ignore-type */ $targetElement);
Loading history...
385
        }
386
    }
387
388
    abstract protected function getTargetFlowScope(ProcessDefinitionImpl $processDefinition): ScopeImpl;
389
390
    abstract protected function getTargetElement(ProcessDefinitionImpl $processDefinition): CoreModelElement;
391
392
    abstract protected function getTargetElementId(): string;
393
}
394