GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — dev ( 2441e9...bd6cbe )
by Андрей
05:34
created

AbstractWorkflow::executeFunction()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 29
Code Lines 18

Duplication

Lines 4
Ratio 13.79 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 4
loc 29
ccs 0
cts 15
cp 0
rs 8.439
cc 5
eloc 18
nc 7
nop 3
crap 30
1
<?php
2
/**
3
 * @link https://github.com/old-town/old-town-workflow
4
 * @author  Malofeykin Andrey  <[email protected]>
5
 */
6
namespace OldTown\Workflow;
7
8
use OldTown\Log\LogFactory;
9
use OldTown\PropertySet\PropertySetInterface;
10
use OldTown\PropertySet\PropertySetManager;
11
use OldTown\Workflow\Config\ConfigurationInterface;
12
use OldTown\Workflow\Config\DefaultConfiguration;
13
use OldTown\Workflow\Exception\FactoryException;
14
use OldTown\Workflow\Exception\InternalWorkflowException;
15
use OldTown\Workflow\Exception\InvalidActionException;
16
use OldTown\Workflow\Exception\InvalidArgumentException;
17
use OldTown\Workflow\Exception\InvalidEntryStateException;
18
use OldTown\Workflow\Exception\InvalidInputException;
19
use OldTown\Workflow\Exception\InvalidRoleException;
20
use OldTown\Workflow\Exception\StoreException;
21
use OldTown\Workflow\Exception\WorkflowException;
22
use OldTown\Workflow\Loader\ActionDescriptor;
23
use OldTown\Workflow\Loader\ConditionDescriptor;
24
use OldTown\Workflow\Loader\ConditionsDescriptor;
25
use OldTown\Workflow\Loader\FunctionDescriptor;
26
use OldTown\Workflow\Loader\RegisterDescriptor;
27
use OldTown\Workflow\Loader\ResultDescriptor;
28
use OldTown\Workflow\Loader\ValidatorDescriptor;
29
use OldTown\Workflow\Loader\WorkflowDescriptor;
30
use OldTown\Workflow\Query\WorkflowExpressionQuery;
31
use OldTown\Workflow\Spi\SimpleWorkflowEntry;
32
use OldTown\Workflow\Spi\StepInterface;
33
use OldTown\Workflow\Spi\WorkflowEntryInterface;
34
use OldTown\Workflow\Spi\WorkflowStoreInterface;
35
use OldTown\Workflow\TransientVars\TransientVarsInterface;
36
use Psr\Log\LoggerInterface;
37
use Traversable;
38
use SplObjectStorage;
39
use DateTime;
40
use OldTown\Workflow\TransientVars\BaseTransientVars;
41
use ReflectionClass;
42
use ArrayObject;
43
44
45
/**
46
 * Class AbstractWorkflow
47
 *
48
 * @package OldTown\Workflow
49
 */
50
abstract class  AbstractWorkflow implements WorkflowInterface
51
{
52
    /**
53
     * @var WorkflowContextInterface
54
     */
55
    protected $context;
56
57
    /**
58
     * @var ConfigurationInterface
59
     */
60
    protected $configuration;
61
62
    /**
63
     *
64
     * @var array
65
     */
66
    protected $stateCache = [];
67
68
    /**
69
     * @var TypeResolverInterface
70
     */
71
    protected $typeResolver;
72
73
    /**
74
     * Логер
75
     *
76
     * @var LoggerInterface
77
     */
78
    protected $log;
79
80
    /**
81
     * Резолвер для создания провайдеров отвечающих за исполнение функций, проверку условий, выполнение валидаторов и т.д.
82
     *
83
     * @var string
84
     */
85
    protected $defaultTypeResolverClass = TypeResolver::class;
86
87
    /**
88
     *
89
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
90
     * @throws \OldTown\Log\Exception\InvalidArgumentException
91
     * @throws \OldTown\Log\Exception\DomainException
92
     *
93
     */
94
    public function __construct()
95
    {
96
        $this->log = LogFactory::getLog();
97
    }
98
99
    /**
100
     * Инициализация workflow. Workflow нужно иницаилизровать прежде, чем выполнять какие либо действия.
101
     * Workflow может быть инициализированно только один раз
102
     *
103
     * @param string $workflowName Имя workflow
104
     * @param integer $initialAction Имя первого шага, с которого начинается workflow
105
     * @param TransientVarsInterface $inputs Данные введеные пользователем
106 19
     * @return integer
107
     * @throws \OldTown\Workflow\Exception\InvalidRoleException
108 19
     * @throws \OldTown\Workflow\Exception\InvalidInputException
109 19
     * @throws \OldTown\Workflow\Exception\WorkflowException
110 19
     * @throws \OldTown\Workflow\Exception\InvalidEntryStateException
111
     * @throws \OldTown\Workflow\Exception\InvalidActionException
112
     * @throws \OldTown\Workflow\Exception\FactoryException
113
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
114
     * @throws \OldTown\Workflow\Exception\StoreException
115
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
116
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
117 19
     *
118
     */
119 19
    public function initialize($workflowName, $initialAction, TransientVarsInterface $inputs = null)
120 19
    {
121
        try {
122 19
            $initialAction = (integer)$initialAction;
123
124 19
            $wf = $this->getConfiguration()->getWorkflow($workflowName);
125
126
            $store = $this->getPersistence();
127
128
            $entry = $store->createEntry($workflowName);
129
130
            $ps = $store->getPropertySet($entry->getId());
131
132
133
            if (null === $inputs) {
134
                $inputs = $this->transientVarsFactory();
135
            }
136
            $transientVars = $inputs;
137
            $inputs = clone $transientVars;
138
139
            $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), $initialAction, new ArrayObject(), $ps);
140
141
            if (!$this->canInitializeInternal($workflowName, $initialAction, $transientVars, $ps)) {
142
                $this->context->setRollbackOnly();
143
                $errMsg = 'You are restricted from initializing this workflow';
144 19
                throw new InvalidRoleException($errMsg);
145
            }
146 19
147 19
            $action = $wf->getInitialAction($initialAction);
148 19
149 19
            $currentSteps = new SplObjectStorage();
150 19
            $this->transitionWorkflow($entry, $currentSteps, $store, $wf, $action, $transientVars, $inputs, $ps);
0 ignored issues
show
Bug introduced by
It seems like $action defined by $wf->getInitialAction($initialAction) on line 147 can be null; however, OldTown\Workflow\Abstrac...w::transitionWorkflow() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
151 19
152 19
            $entryId = $entry->getId();
153 19
        } catch (WorkflowException $e) {
154
            $this->context->setRollbackOnly();
155 19
            throw new InternalWorkflowException($e->getMessage(), $e->getCode(), $e);
156 19
        }
157 19
158 19
159 19
        // now clone the memory PS to the real PS
160 19
        //PropertySetManager.clone(ps, store.getPropertySet(entryId));
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
161 19
        return $entryId;
162 19
    }
163 19
164 19
    /**
165 19
     * @param WorkflowEntryInterface $entry
166
     * @param TransientVarsInterface $transientVars
167
     * @param array|Traversable|RegisterDescriptor[]|SplObjectStorage $registersStorage
168
     * @param integer $actionId
169
     * @param array|Traversable $currentSteps
170
     * @param PropertySetInterface $ps
171
     *
172 19
     *
173
     * @return TransientVarsInterface
174 1
     *
175 19
     * @throws \OldTown\Workflow\Exception\WorkflowException
176 19
     * @throws \OldTown\Workflow\Exception\FactoryException
177
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
178
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
179 19
     * @throws \OldTown\Workflow\Exception\StoreException
180
     */
181
    protected function populateTransientMap(WorkflowEntryInterface $entry, TransientVarsInterface $transientVars, $registersStorage, $actionId = null, $currentSteps, PropertySetInterface $ps)
182
    {
183 View Code Duplication
        if (!is_array($currentSteps) && !$currentSteps  instanceof Traversable) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
            $errMsg = 'CurrentSteps должен быть массивом, либо реализовывать интерфейс Traversable';
185
            throw new InvalidArgumentException($errMsg);
186
        }
187
188 View Code Duplication
        if ($registersStorage instanceof Traversable) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
189
            $registers = [];
190
            foreach ($registersStorage as $k => $v) {
191
                $registers[$k] = $v;
192
            }
193
        } elseif (is_array($registersStorage)) {
194
            $registers = $registersStorage;
195
        } else {
196
            $errMsg = 'Registers должен быть массивом, либо реализовывать интерфейс Traversable';
197
            throw new InvalidArgumentException($errMsg);
198 19
        }
199
        /** @var RegisterDescriptor[] $registers */
200
201 19
        $transientVars['context'] = $this->context;
202
        $transientVars['entry'] = $entry;
203 19
        $transientVars['entryId'] = $entry->getId();
204
        $transientVars['store'] = $this->getPersistence();
205 19
        $transientVars['configuration'] = $this->getConfiguration();
206
        $transientVars['descriptor'] = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
207 19
208
        if (null !== $actionId) {
209 19
            $transientVars['actionId'] = $actionId;
210
        }
211
212 19
        $transientVars['currentSteps'] = $currentSteps;
213
214
215
        foreach ($registers as $register) {
216 19
            $args = $register->getArgs();
217
            $type = $register->getType();
218 19
219 19
            try {
220
                $r = $this->getResolver()->getRegister($type, $args);
221 19
            } catch (\Exception $e) {
222
                $errMsg = 'Ошибка при инициализации register';
223 19
                $this->context->setRollbackOnly();
224 1
                throw new WorkflowException($errMsg, $e->getCode(), $e);
225 1
            }
226 1
227
            $variableName = $register->getVariableName();
228
            try {
229 18
                $value = $r->registerVariable($this->context, $entry, $args, $ps);
230
231 18
                $transientVars[$variableName] = $value;
232
            } catch (\Exception $e) {
233
                $this->context->setRollbackOnly();
234
235
                $errMsg = sprintf(
236 18
                    'При получение значения переменной %s из registry %s произошла ошибка',
237 18
                    $variableName,
238 18
                    get_class($r)
239
                );
240 16
241 19
                throw new WorkflowException($errMsg, $e->getCode(), $e);
242
            }
243
        }
244
245
        return $transientVars;
246 16
    }
247
248
    /**
249
     * Переход между двумя статусами
250
     *
251
     * @param WorkflowEntryInterface $entry
252
     * @param SplObjectStorage|StepInterface[] $currentSteps
253
     * @param WorkflowStoreInterface $store
254
     * @param WorkflowDescriptor $wf
255
     * @param ActionDescriptor $action
256 2
     * @param TransientVarsInterface $transientVars
257 2
     * @param TransientVarsInterface $inputs
258
     * @param PropertySetInterface $ps
259
     *
260
     * @return boolean
261
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
262
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
263
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
264
     * @throws \OldTown\Workflow\Exception\WorkflowException
265
     * @throws \OldTown\Workflow\Exception\StoreException
266
     * @throws \OldTown\Workflow\Exception\InvalidEntryStateException
267
     * @throws \OldTown\Workflow\Exception\InvalidInputException
268
     * @throws \OldTown\Workflow\Exception\FactoryException
269
     * @throws \OldTown\Workflow\Exception\InvalidActionException
270
     */
271
    protected function transitionWorkflow(WorkflowEntryInterface $entry, SplObjectStorage $currentSteps, WorkflowStoreInterface $store, WorkflowDescriptor $wf, ActionDescriptor $action, TransientVarsInterface $transientVars, TransientVarsInterface $inputs, PropertySetInterface $ps)
272
    {
273
        $step = $this->getCurrentStep($wf, $action->getId(), $currentSteps, $transientVars, $ps);
274
275
        $validators = $action->getValidators();
276
        if ($validators->count() > 0) {
277
            $this->verifyInputs($entry, $validators, $transientVars, $ps);
278
        }
279
280
281
        if (null !== $step) {
282
            $stepPostFunctions = $wf->getStep($step->getStepId())->getPostFunctions();
283
            foreach ($stepPostFunctions as $function) {
284
                $this->executeFunction($function, $transientVars, $ps);
285
            }
286
        }
287
288
        $preFunctions = $action->getPreFunctions();
289
        foreach ($preFunctions as $preFunction) {
290
            $this->executeFunction($preFunction, $transientVars, $ps);
291
        }
292
293
        $conditionalResults = $action->getConditionalResults();
294
        $extraPreFunctions = null;
295
        $extraPostFunctions = null;
296
297
        $theResult = null;
298
299
300
        $currentStepId = null !== $step ? $step->getStepId()  : -1;
301
        foreach ($conditionalResults as $conditionalResult) {
302
            if ($this->passesConditionsWithType(null, $conditionalResult->getConditions(), $transientVars, $ps, $currentStepId)) {
303
                $theResult = $conditionalResult;
304
305
                $validatorsStorage = $conditionalResult->getValidators();
306
                if ($validatorsStorage->count() > 0) {
307
                    $this->verifyInputs($entry, $validatorsStorage, $transientVars, $ps);
308
                }
309
310
                $extraPreFunctions = $conditionalResult->getPreFunctions();
311
                $extraPostFunctions = $conditionalResult->getPostFunctions();
312
313
                break;
314
            }
315
        }
316
317
318
        if (null ===  $theResult) {
319
            $theResult = $action->getUnconditionalResult();
320
            $this->verifyInputs($entry, $theResult->getValidators(), $transientVars, $ps);
321
            $extraPreFunctions = $theResult->getPreFunctions();
322
            $extraPostFunctions = $theResult->getPostFunctions();
323
        }
324
325
        $logMsg = sprintf('theResult=%s %s', $theResult->getStep(), $theResult->getStatus());
326
        $this->getLog()->debug($logMsg);
327
328
329
        if ($extraPreFunctions && $extraPreFunctions->count() > 0) {
330
            foreach ($extraPreFunctions as $function) {
331
                $this->executeFunction($function, $transientVars, $ps);
332
            }
333
        }
334
335
        $split = $theResult->getSplit();
336
        $join = $theResult->getJoin();
337
        if (null !== $split && 0 !== $split) {
338
            $splitDesc = $wf->getSplit($split);
339
            $results = $splitDesc->getResults();
340
            $splitPreFunctions = [];
341
            $splitPostFunctions = [];
342
343
            foreach ($results as $resultDescriptor) {
344
                if ($resultDescriptor->getValidators()->count() > 0) {
345
                    $this->verifyInputs($entry, $resultDescriptor->getValidators(), $transientVars, $ps);
346
                }
347
348
                foreach ($resultDescriptor->getPreFunctions() as $function) {
349
                    $splitPreFunctions[] = $function;
350
                }
351
                foreach ($resultDescriptor->getPostFunctions() as $function) {
352
                    $splitPostFunctions[] = $function;
353
                }
354
            }
355 8
356
            foreach ($splitPreFunctions as $function) {
357 8
                $this->executeFunction($function, $transientVars, $ps);
358 8
            }
359
360
            if (!$action->isFinish()) {
361 8
                $moveFirst = true;
362 8
363
                //???????????????????
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
364 8
//                $theResults = [];
365 8
//                foreach ($results as $result) {
366
//                    $theResults[] = $result;
367 8
//                }
368
369
                foreach ($results as $resultDescriptor) {
370
                    $moveToHistoryStep = null;
371 8
372
                    if ($moveFirst) {
373 8
                        $moveToHistoryStep = $step;
374 8
                    }
375
376
                    $previousIds = [];
377
378
                    if (null !== $step) {
379 8
                        $previousIds[] = $step->getStepId();
380
                    }
381 8
382
                    $this->createNewCurrentStep($resultDescriptor, $entry, $store, $action->getId(), $moveToHistoryStep, $previousIds, $transientVars, $ps);
383 8
                    $moveFirst = false;
384 8
                }
385
            }
386 8
387 8
388
            foreach ($splitPostFunctions as $function) {
389
                $this->executeFunction($function, $transientVars, $ps);
390 8
            }
391 8
        } elseif (null !== $join && 0 !== $join) {
392 6
            $joinDesc = $wf->getJoin($join);
393 6
            $oldStatus = $theResult->getOldStatus();
394
            $caller = $this->context->getCaller();
395 8
            $step = $store->markFinished($step, $action->getId(), new DateTime(), $oldStatus, $caller);
0 ignored issues
show
Bug introduced by
It seems like $step can be null; however, markFinished() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
396 2
397 2
            $store->moveToHistory($step);
398
399 2
            /** @var StepInterface[] $joinSteps */
400 2
            $joinSteps = [];
401
            $joinSteps[] = $step;
402
403 View Code Duplication
            foreach ($currentSteps as $currentStep) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
404
                if ($currentStep->getId() !== $step->getId()) {
405 6
                    $stepDesc = $wf->getStep($currentStep->getStepId());
406 6
407
                    if ($stepDesc->resultsInJoin($join)) {
408
                        $joinSteps[] = $currentSteps;
409 6
                    }
410
                }
411
            }
412
413
            $historySteps = $store->findHistorySteps($entry->getId());
414 5
415 View Code Duplication
            foreach ($historySteps as $historyStep) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
416
                if ($historyStep->getId() !== $step->getId()) {
417
                    $stepDesc = $wf->getStep($historyStep->getStepId());
418
419
                    if ($stepDesc->resultsInJoin($join)) {
420
                        $joinSteps[] = $currentSteps;
421
                    }
422
                }
423
            }
424 8
425
            $jn = new JoinNodes($joinSteps);
426 8
            $transientVars['jn'] = $jn;
427 8
428 8
429
            if ($this->passesConditionsWithType(null, $joinDesc->getConditions(), $transientVars, $ps, 0)) {
430
                $joinResult = $joinDesc->getResult();
431
432 8
                $joinResultValidators = $joinResult->getValidators();
433
                if ($joinResultValidators->count() > 0) {
434 8
                    $this->verifyInputs($entry, $joinResultValidators, $transientVars, $ps);
435
                }
436
437
                foreach ($joinResult->getPreFunctions() as $function) {
438
                    $this->executeFunction($function, $transientVars, $ps);
439
                }
440
441
                $previousIds = [];
442
                $i = 1;
443
444
                foreach ($joinSteps as  $currentStep) {
445
                    if (!$historySteps->contains($currentStep) && $currentStep->getId() !== $step->getId()) {
0 ignored issues
show
Bug introduced by
The method getId does only exist in OldTown\Workflow\Spi\StepInterface, but not in SplObjectStorage.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
446
                        $store->moveToHistory($step);
447
                    }
448
449
                    $previousIds[$i] = $currentStep->getId();
450
                }
451 8
452
                if (!$action->isFinish()) {
453 8
                    $previousIds[0] = $step->getId();
454 8
                    $theResult = $joinDesc->getResult();
455 8
456 8
                    $this->createNewCurrentStep($theResult, $entry, $store, $action->getId(), null, $previousIds, $transientVars, $ps);
457
                }
458
459
                foreach ($joinResult->getPostFunctions() as $function) {
460 8
                    $this->executeFunction($function, $transientVars, $ps);
461
                }
462 8
            }
463 8
        } else {
464
            $previousIds = [];
465
466
            if (null !== $step) {
467
                $previousIds[] = $step->getId();
468 8
            }
469 6
470 6
            if (!$action->isFinish()) {
471 8
                $this->createNewCurrentStep($theResult, $entry, $store, $action->getId(), $step, $previousIds, $transientVars, $ps);
472 8
            }
473
        }
474 8
475
        if ($extraPostFunctions && $extraPostFunctions->count() > 0) {
476
            foreach ($extraPostFunctions as $function) {
477
                $this->executeFunction($function, $transientVars, $ps);
478
            }
479
        }
480
481
        if (WorkflowEntryInterface::COMPLETED !== $entry->getState() && null !== $wf->getInitialAction($action->getId())) {
482
            $this->changeEntryState($entry->getId(), WorkflowEntryInterface::ACTIVATED);
483
        }
484
485
        if ($action->isFinish()) {
486
            $this->completeEntry($action, $entry->getId(), $this->getCurrentSteps($entry->getId()), WorkflowEntryInterface::COMPLETED);
487 8
            return true;
488 8
        }
489
490
        $availableAutoActions = $this->getAvailableAutoActions($entry->getId(), $inputs);
491
492
        if (count($availableAutoActions) > 0) {
493
            $this->doAction($entry->getId(), $availableAutoActions[0], $inputs);
494
        }
495
496
        return false;
497
    }
498
499
500
    /**
501
     * @param       $id
502
     * @param TransientVarsInterface $inputs
503
     *
504
     * @return array
505
     * @throws \OldTown\Workflow\Exception\WorkflowException
506
     */
507
    protected function getAvailableAutoActions($id, TransientVarsInterface $inputs)
508
    {
509
        try {
510
            $store = $this->getPersistence();
511
            $entry = $store->findEntry($id);
512
513
            if (null === $entry) {
514
                $errMsg = sprintf(
515
                    'Нет сущности workflow c id %s',
516
                    $id
517
                );
518
                throw new InvalidArgumentException($errMsg);
519
            }
520
521
522
            if (WorkflowEntryInterface::ACTIVATED !== $entry->getState()) {
523
                $logMsg = sprintf('--> состояние %s', $entry->getState());
524
                $this->getLog()->debug($logMsg);
525
                return [0];
526
            }
527
528
            $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
529 16
530 View Code Duplication
            if (null === $wf) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
531 16
                $errMsg = sprintf(
532 16
                    'Нет workflow c именем %s',
533
                    $entry->getWorkflowName()
534 16
                );
535
                throw new InvalidArgumentException($errMsg);
536 16
            }
537
538
            $l = [];
539
            $ps = $store->getPropertySet($id);
540
            $transientVars = $inputs;
541
            $currentSteps = $store->findCurrentSteps($id);
542
543
            $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), 0, $currentSteps, $ps);
544
545
            $globalActions = $wf->getGlobalActions();
546
547 View Code Duplication
            foreach ($globalActions as $action) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
548
                $transientVars['actionId'] = $action->getId();
549
550
                if ($action->getAutoExecute() && $this->isActionAvailable($action, $transientVars, $ps, 0)) {
551
                    $l[] = $action->getId();
552
                }
553
            }
554
555 View Code Duplication
            foreach ($currentSteps as $step) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
556
                $availableAutoActionsForStep = $this->getAvailableAutoActionsForStep($wf, $step, $transientVars, $ps);
557
                foreach ($availableAutoActionsForStep as $v) {
558
                    $l[] = $v;
559
                }
560
                //$l = array_merge($l, $availableAutoActionsForStep);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
561
            }
562
563
            $l = array_unique($l);
564
565
            return $l;
566
        } catch (\Exception $e) {
567
            $errMsg = 'Ошибка при проверке доступных действий';
568
            $this->getLog()->error($errMsg, [$e]);
569
        }
570
571
        return [];
572
    }
573
574
575
    /**
576
     * @param WorkflowDescriptor   $wf
577
     * @param StepInterface        $step
578
     * @param TransientVarsInterface                $transientVars
579 19
     * @param PropertySetInterface $ps
580
     *
581
     * @return array
582
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
583
     * @throws \OldTown\Workflow\Exception\WorkflowException
584
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
585
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
586
     * @throws \OldTown\Workflow\Exception\InvalidActionException
587
     */
588 19
    protected function getAvailableAutoActionsForStep(WorkflowDescriptor $wf, StepInterface $step, TransientVarsInterface $transientVars, PropertySetInterface $ps)
589
    {
590
        $l = [];
591
        $s = $wf->getStep($step->getStepId());
592
593
        if (null === $s) {
594
            $msg = sprintf('getAvailableAutoActionsForStep вызвана с несуществующим id %s', $step->getStepId());
595
            $this->getLog()->debug($msg);
596
            return $l;
597
        }
598
599
600
        $actions = $s->getActions();
601
        if (null === $actions || 0 === $actions->count()) {
602
            return $l;
603
        }
604
605 View Code Duplication
        foreach ($actions as $action) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
606
            $transientVars['actionId'] = $action->getId();
607
608
            if ($action->getAutoExecute() && $this->isActionAvailable($action, $transientVars, $ps, 0)) {
609
                $l[] = $action->getId();
610
            }
611
        }
612
613 16
        return $l;
614
    }
615 16
616 16
    /**
617
     * @param ActionDescriptor $action
618 16
     * @param                  $id
619
     * @param array|Traversable $currentSteps
620
     * @param                  $state
621
     *
622 16
     * @return void
623 16
     *
624
     * @throws \OldTown\Workflow\Exception\StoreException
625
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
626
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
627
     */
628
    protected function completeEntry(ActionDescriptor $action = null, $id, $currentSteps, $state)
629
    {
630 View Code Duplication
        if (!($currentSteps instanceof Traversable || is_array($currentSteps))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
631
            $errMsg = 'CurrentSteps должен быть массивом, либо реализовывать интерфейс Traversable';
632 16
            throw new InvalidArgumentException($errMsg);
633 16
        }
634
635
636
        $this->getPersistence()->setEntryState($id, $state);
637
638
        $oldStatus = null !== $action ? $action->getUnconditionalResult()->getOldStatus() : 'Finished';
639
        $actionIdValue = null !== $action ? $action->getId() : -1;
640
        foreach ($currentSteps as $step) {
641
            $this->getPersistence()->markFinished($step, $actionIdValue, new DateTime(), $oldStatus, $this->context->getCaller());
642
            $this->getPersistence()->moveToHistory($step);
643
        }
644 16
    }
645 16
    /**
646 16
     * @param ResultDescriptor       $theResult
647 16
     * @param WorkflowEntryInterface $entry
648 16
     * @param WorkflowStoreInterface $store
649 16
     * @param integer                $actionId
650 16
     * @param StepInterface          $currentStep
651
     * @param array                  $previousIds
652
     * @param TransientVarsInterface                  $transientVars
653
     * @param PropertySetInterface   $ps
654
     *
655
     * @return StepInterface
656
     * @throws \OldTown\Workflow\Exception\WorkflowException
657
     * @throws \OldTown\Workflow\Exception\StoreException
658
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
659
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
660
     */
661
    protected function createNewCurrentStep(
662
        ResultDescriptor $theResult,
663
        WorkflowEntryInterface $entry,
664
        WorkflowStoreInterface $store,
665
        $actionId,
666
        StepInterface $currentStep = null,
667
        array $previousIds = [],
668
        TransientVarsInterface $transientVars,
669
        PropertySetInterface $ps
670
    ) {
671
        try {
672
            $nextStep = $theResult->getStep();
673
674
            if (-1 === $nextStep) {
675
                if (null !== $currentStep) {
676
                    $nextStep = $currentStep->getStepId();
677
                } else {
678
                    $errMsg = 'Неверный аргумент. Новый шаг является таким же как текущий. Но текущий шаг не указан';
679
                    throw new StoreException($errMsg);
680
                }
681
            }
682
683
            $owner = $theResult->getOwner();
684
685
            $logMsg = sprintf(
686
                'Результат: stepId=%s, status=%s, owner=%s, actionId=%s, currentStep=%s',
687
                $nextStep,
688
                $theResult->getStatus(),
689
                $owner,
690
                $actionId,
691
                null !== $currentStep ? $currentStep->getId() : 0
692
            );
693
            $this->getLog()->debug($logMsg);
694
695
            $variableResolver = $this->getConfiguration()->getVariableResolver();
696
697
            if (null !== $owner) {
698
                $o = $variableResolver->translateVariables($owner, $transientVars, $ps);
699
                $owner = null !== $o ? (string)$o : null;
700
            }
701
702
703
            $oldStatus = $theResult->getOldStatus();
704
            $oldStatus = (string)$variableResolver->translateVariables($oldStatus, $transientVars, $ps);
705
706
            $status = $theResult->getStatus();
707
            $status = (string)$variableResolver->translateVariables($status, $transientVars, $ps);
708
709
710
            if (null !== $currentStep) {
711
                $store->markFinished($currentStep, $actionId, new DateTime(), $oldStatus, $this->context->getCaller());
712
                $store->moveToHistory($currentStep);
713
            }
714
715
            $startDate = new DateTime();
716
            $dueDate = null;
717
718
            $theResultDueDate = (string)$theResult->getDueDate();
719
            $theResultDueDate = trim($theResultDueDate);
720
            if (strlen($theResultDueDate) > 0) {
721
                $dueDateObject = $variableResolver->translateVariables($theResultDueDate, $transientVars, $ps);
722
723
                if ($dueDateObject instanceof DateTime) {
724
                    $dueDate = $dueDateObject;
725
                } elseif (is_string($dueDateObject)) {
726
                    $dueDate = new DateTime($dueDate);
727
                } elseif (is_numeric($dueDateObject)) {
728
                    $dueDate = DateTime::createFromFormat('U', $dueDateObject);
729
                } else {
730
                    $errMsg = 'Ошибка при преобразование DueData';
731 19
                    throw new InternalWorkflowException($errMsg);
732
                }
733 19
            }
734
735 19
            $newStep = $store->createCurrentStep($entry->getId(), $nextStep, $owner, $startDate, $dueDate, $status, $previousIds);
0 ignored issues
show
Security Bug introduced by
It seems like $dueDate defined by \DateTime::createFromFormat('U', $dueDateObject) on line 728 can also be of type false; however, OldTown\Workflow\Spi\Wor...ce::createCurrentStep() does only seem to accept null|object<DateTime>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
736
            $transientVars['createdStep'] =  $newStep;
737 19
738
            if (null === $currentStep && 0 === count($previousIds)) {
739
                $currentSteps = [];
740
                $currentSteps[] = $newStep;
741
                $transientVars['currentSteps'] =  $currentSteps;
742
            }
743
744
            if (! $transientVars->offsetExists('descriptor')) {
745 19
                $errMsg = 'Ошибка при получение дескриптора workflow из transientVars';
746
                throw new InternalWorkflowException($errMsg);
747
            }
748 19
749 19
            /** @var WorkflowDescriptor $descriptor */
750 2
            $descriptor = $transientVars['descriptor'];
751 2
            $step = $descriptor->getStep($nextStep);
752
753 19
            if (null === $step) {
754 19
                $errMsg = sprintf('Шаг #%s не найден', $nextStep);
755
                throw new WorkflowException($errMsg);
756 19
            }
757
758
            $preFunctions = $step->getPreFunctions();
759
760
            foreach ($preFunctions as $function) {
761
                $this->executeFunction($function, $transientVars, $ps);
762
            }
763
        } catch (WorkflowException $e) {
764 19
            $this->context->setRollbackOnly();
765
            throw $e;
766 19
        }
767 16
    }
768
769
    /**
770 19
     * Создает хранилище переменных
771 19
     *
772 19
     * @param $class
773 19
     *
774
     * @return TransientVarsInterface
775 19
     */
776
    protected function transientVarsFactory($class = BaseTransientVars::class)
777
    {
778
        $r = new \ReflectionClass($class);
779
        return $r->newInstance();
780
    }
781
782
    /**
783
     *
784
     *
785 19
     * Perform an action on the specified workflow instance.
786
     * @param integer $id The workflow instance id.
787 19
     * @param integer $actionId The action id to perform (action id's are listed in the workflow descriptor).
788
     * @param TransientVarsInterface $inputs The inputs to the workflow instance.
789
     * @throws \OldTown\Workflow\Exception\InvalidInputException if a validator is specified and an input is invalid.
790
     * @throws WorkflowException if the action is invalid for the specified workflow
791
     * instance's current state.
792
     *
793
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
794
     * @throws \OldTown\Workflow\Exception\StoreException
795
     * @throws \OldTown\Workflow\Exception\FactoryException
796
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
797
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
798
     * @throws \OldTown\Workflow\Exception\InvalidActionException
799
     * @throws \OldTown\Workflow\Exception\InvalidEntryStateException
800 19
     * @throws \OldTown\Workflow\Exception\WorkflowException
801
     */
802 19
    public function doAction($id, $actionId, TransientVarsInterface $inputs = null)
803
    {
804 19
        $actionId = (integer)$actionId;
805
        if (null === $inputs) {
806
            $inputs = $this->transientVarsFactory();
807
        }
808
        $transientVars = $inputs;
809
        $inputs = clone $transientVars;
810
811
        $store = $this->getPersistence();
812
        $entry = $store->findEntry($id);
813
814 19
        if (WorkflowEntryInterface::ACTIVATED !== $entry->getState()) {
815
            return;
816
        }
817
818
        $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
819
820 17
        $currentSteps = $store->findCurrentSteps($id);
821
        $action = null;
822 17
823
        $ps = $store->getPropertySet($id);
824
825
        $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), $actionId, $currentSteps, $ps);
826
827
828
        $validAction = false;
829
830 View Code Duplication
        foreach ($wf->getGlobalActions() as $actionDesc) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
831
            if ($actionId === $actionDesc->getId()) {
832
                $action = $actionDesc;
833
834
                if ($this->isActionAvailable($action, $transientVars, $ps, 0)) {
835
                    $validAction = true;
836
                }
837
            }
838
        }
839
840
841
        foreach ($currentSteps as $step) {
842
            $s = $wf->getStep($step->getStepId());
843
844 View Code Duplication
            foreach ($s->getActions() as $actionDesc) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
845
                if ($actionId === $actionDesc->getId()) {
846
                    $action = $actionDesc;
847
848
                    if ($this->isActionAvailable($action, $transientVars, $ps, $s->getId())) {
849
                        $validAction = true;
850
                    }
851
                }
852
            }
853
        }
854
855
856
        if (!$validAction) {
857
            $errMsg = sprintf(
858
                'Action %s is invalid',
859
                $actionId
860
            );
861
            throw new InvalidActionException($errMsg);
862
        }
863
864
865
        try {
866
            if ($this->transitionWorkflow($entry, $currentSteps, $store, $wf, $action, $transientVars, $inputs, $ps)) {
0 ignored issues
show
Documentation introduced by
$currentSteps is of type array<integer,object<Old...low\Spi\StepInterface>>, but the function expects a object<SplObjectStorage>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
867
                $this->checkImplicitFinish($action, $id);
868
            }
869
        } catch (WorkflowException $e) {
870
            $this->context->setRollbackOnly();
871
            throw $e;
872
        }
873
    }
874
875
    /**
876
     * @param ActionDescriptor $action
877
     * @param                  $id
878
     *
879
     * @return void
880
     *
881
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
882
     * @throws \OldTown\Workflow\Exception\StoreException
883
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
884
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
885
     *
886
     */
887
    protected function checkImplicitFinish(ActionDescriptor $action, $id)
888
    {
889
        $store = $this->getPersistence();
890
        $entry = $store->findEntry($id);
891
892
        $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
893
894
        $currentSteps = $store->findCurrentSteps($id);
895
896
        $isCompleted = $wf->getGlobalActions()->count() === 0;
897
898
        foreach ($currentSteps as $step) {
899
            if ($isCompleted) {
900
                break;
901
            }
902
903
            $stepDes = $wf->getStep($step->getStepId());
904
905
            if ($stepDes->getActions()->count() > 0) {
906
                $isCompleted = true;
907
            }
908
        }
909
910
        if ($isCompleted) {
911
            $this->completeEntry($action, $id, $currentSteps, WorkflowEntryInterface::COMPLETED);
912
        }
913
    }
914
915
    /**
916
     *
917
     * Check if the state of the specified workflow instance can be changed to the new specified one.
918
     * @param integer $id The workflow instance id.
919
     * @param integer $newState The new state id.
920
     * @return boolean true if the state of the workflow can be modified, false otherwise.
921
     * @throws \OldTown\Workflow\Exception\StoreException
922
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
923
     *
924
     */
925
    public function canModifyEntryState($id, $newState)
926
    {
927
        $store = $this->getPersistence();
928
        $entry = $store->findEntry($id);
929
930
        $currentState = $entry->getState();
931
932
        $result = false;
933
        try {
934
            switch ($newState) {
935
                case WorkflowEntryInterface::COMPLETED: {
936
                    if (WorkflowEntryInterface::ACTIVATED === $currentState) {
937
                        $result = true;
938
                    }
939
                    break;
940
                }
941
942
                //@TODO Разобраться с бизнес логикой. Может быть нужно добавить break
943
                /** @noinspection PhpMissingBreakStatementInspection */
944
                case WorkflowEntryInterface::CREATED: {
945
                    $result = false;
946
                }
947 View Code Duplication
                case WorkflowEntryInterface::ACTIVATED: {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
948
                    if (WorkflowEntryInterface::CREATED === $currentState || WorkflowEntryInterface::SUSPENDED === $currentState) {
949
                        $result = true;
950
                    }
951
                    break;
952
                }
953
                case WorkflowEntryInterface::SUSPENDED: {
954
                    if (WorkflowEntryInterface::ACTIVATED === $currentState) {
955
                        $result = true;
956
                    }
957
                    break;
958
                }
959 View Code Duplication
                case WorkflowEntryInterface::KILLED: {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
960
                    if (WorkflowEntryInterface::CREATED === $currentState || WorkflowEntryInterface::ACTIVATED === $currentState || WorkflowEntryInterface::SUSPENDED === $currentState) {
961
                        $result = true;
962
                    }
963
                    break;
964
                }
965
                default: {
966
                    $result = false;
967
                    break;
968 19
                }
969
970 19
            }
971
972 19
            return $result;
973
        } catch (StoreException $e) {
0 ignored issues
show
Unused Code introduced by
catch (\OldTown\Workflow...($errMsg, array($e)); } does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
974
            $errMsg = sprintf(
975
                'Ошибка проверки изменения состояния для инстанса #%s',
976
                $id
977
            );
978
            $this->getLog()->error($errMsg, [$e]);
979
        }
980
981
        return false;
982
    }
983
984
985
    /**
986
     *
987
     * Возвращает коллекцию объектов описывающие состояние для текущего экземпляра workflow
988
     *
989
     * @param integer $id id экземпляра workflow
990
     * @return SplObjectStorage|StepInterface[]
991
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
992
     */
993 View Code Duplication
    public function getCurrentSteps($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
994
    {
995
        try {
996
            $store = $this->getPersistence();
997
998
            return $store->findCurrentSteps($id);
999
        } catch (StoreException $e) {
1000
            $errMsg = sprintf(
1001
                'Ошибка при проверке текущего шага для инстанса # %s',
1002
                $id
1003
            );
1004
            $this->getLog()->error($errMsg, [$e]);
1005
1006
1007
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type declared by the interface OldTown\Workflow\Workflo...erface::getCurrentSteps of type SplObjectStorage|OldTown...low\Spi\StepInterface[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1008
        }
1009
    }
1010
1011
    /**
1012
     *
1013
     *
1014
     * Modify the state of the specified workflow instance.
1015
     * @param integer $id The workflow instance id.
1016
     * @param integer $newState the new state to change the workflow instance to.
1017
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1018
     * @throws \OldTown\Workflow\Exception\StoreException
1019
     * @throws \OldTown\Workflow\Exception\InvalidEntryStateException
1020
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1021
     */
1022
    public function changeEntryState($id, $newState)
1023
    {
1024
        $store = $this->getPersistence();
1025
        $entry = $store->findEntry($id);
1026
1027
        if ($newState === $entry->getState()) {
1028
            return;
1029
        }
1030
1031
        if ($this->canModifyEntryState($id, $newState)) {
1032
            if (WorkflowEntryInterface::KILLED === $newState || WorkflowEntryInterface::COMPLETED === $newState) {
1033
                $currentSteps = $this->getCurrentSteps($id);
1034
1035
                if (count($currentSteps) > 0) {
1036
                    $this->completeEntry(null, $id, $currentSteps, $newState);
1037
                }
1038
            }
1039
1040
            $store->setEntryState($id, $newState);
1041
        } else {
1042
            $errMsg = sprintf(
1043
                'Не возможен переход в экземпляре workflow #%s. Текущее состояние %s, ожидаемое состояние %s',
1044
                $id,
1045
                $entry->getState(),
1046
                $newState
1047
            );
1048
1049
            throw new InvalidEntryStateException($errMsg);
1050
        }
1051
1052
        $msg = sprintf(
1053
            '%s : Новое состояние: %s',
1054
            $entry->getId(),
1055
            $entry->getState()
1056
        );
1057
        $this->getLog()->debug($msg);
1058
    }
1059
1060
1061
    /**
1062
     * @param FunctionDescriptor $function
1063
     * @param TransientVarsInterface $transientVars
1064
     * @param PropertySetInterface $ps
1065
     *
1066
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1067
     * @throws \OldTown\Workflow\Exception\WorkflowException
1068
     */
1069
    protected function executeFunction(FunctionDescriptor $function, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1070
    {
1071
        if (null !== $function) {
1072
            $type = $function->getType();
1073
1074
            $argsOriginal = $function->getArgs();
1075
            $args = [];
1076
1077 View Code Duplication
            foreach ($argsOriginal as $k => $v) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1078
                $translateValue = $this->getConfiguration()->getVariableResolver()->translateVariables($v, $transientVars, $ps);
1079
                $args[$k] = $translateValue;
1080
            }
1081
1082
            $provider = $this->getResolver()->getFunction($type, $args);
1083
1084
            if (null === $provider) {
1085
                $this->context->setRollbackOnly();
1086
                $errMsg = 'Не загружен провайдер для функции';
1087
                throw new WorkflowException($errMsg);
1088
            }
1089
1090
            try {
1091
                $provider->execute($transientVars, $args, $ps);
1092
            } catch (WorkflowException $e) {
1093
                $this->context->setRollbackOnly();
1094
                throw $e;
1095
            }
1096
        }
1097
    }
1098
1099
1100
    /**
1101
     * @param WorkflowEntryInterface $entry
1102
     * @param $validatorsStorage
1103
     * @param TransientVarsInterface $transientVars
1104
     * @param PropertySetInterface $ps
1105
     * @throws \OldTown\Workflow\Exception\WorkflowException
1106
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1107
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1108
     * @throws \OldTown\Workflow\Exception\InvalidInputException
1109
     */
1110
    protected function verifyInputs(WorkflowEntryInterface $entry, $validatorsStorage, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1111
    {
1112 View Code Duplication
        if ($validatorsStorage instanceof Traversable) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1113
            $validators = [];
1114
            foreach ($validatorsStorage as $k => $v) {
1115
                $validators[$k] = $v;
1116
            }
1117
        } elseif (is_array($validatorsStorage)) {
1118
            $validators = $validatorsStorage;
1119
        } else {
1120
            $errMsg = sprintf(
1121
                'Validators должен быть массивом, либо реализовывать интерфейс Traversable. EntryId: %s',
1122
                $entry->getId()
1123
            );
1124
            throw new InvalidArgumentException($errMsg);
1125
        }
1126
1127
        /** @var ValidatorDescriptor[] $validators */
1128
        foreach ($validators as $input) {
1129
            if (null !== $input) {
1130
                $type = $input->getType();
1131
                $argsOriginal = $input->getArgs();
1132
1133
                $args = [];
1134
1135 View Code Duplication
                foreach ($argsOriginal as $k => $v) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1136
                    $translateValue = $this->getConfiguration()->getVariableResolver()->translateVariables($v, $transientVars, $ps);
1137
                    $args[$k] = $translateValue;
1138
                }
1139
1140
1141
                $validator = $this->getResolver()->getValidator($type, $args);
1142
1143
                if (null === $validator) {
1144
                    $this->context->setRollbackOnly();
1145
                    $errMsg = 'Ошибка при загрузке валидатора';
1146
                    throw new WorkflowException($errMsg);
1147
                }
1148
1149
                try {
1150
                    $validator->validate($transientVars, $args, $ps);
1151
                } catch (InvalidInputException $e) {
1152
                    throw $e;
1153
                } catch (\Exception $e) {
1154
                    $this->context->setRollbackOnly();
1155
1156
                    if ($e instanceof WorkflowException) {
1157
                        throw $e;
1158
                    }
1159
1160
                    throw new WorkflowException($e->getMessage(), $e->getCode(), $e);
1161
                }
1162
            }
1163
        }
1164
    }
1165
1166
1167
    /**
1168
     * Возвращает текущий шаг
1169
     *
1170
     * @param WorkflowDescriptor $wfDesc
1171
     * @param integer $actionId
1172
     * @param StepInterface[]|SplObjectStorage $currentSteps
1173
     * @param TransientVarsInterface $transientVars
1174
     * @param PropertySetInterface $ps
1175
     *
1176
     * @return StepInterface
1177
     * @throws \OldTown\Workflow\Exception\WorkflowException
1178
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1179
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1180
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1181
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1182
     */
1183
    protected function getCurrentStep(WorkflowDescriptor $wfDesc, $actionId, SplObjectStorage $currentSteps, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1184
    {
1185
        if (1 === $currentSteps->count()) {
1186
            $currentSteps->rewind();
1187
            return $currentSteps->current();
1188
        }
1189
1190
1191
        foreach ($currentSteps as $step) {
1192
            $stepId = $step->getId();
1193
            $action = $wfDesc->getStep($stepId)->getAction($actionId);
1194
1195
            if ($this->isActionAvailable($action, $transientVars, $ps, $stepId)) {
1196
                return $step;
1197
            }
1198
        }
1199
1200
        return null;
1201
    }
1202
1203
    /**
1204
     * @param ActionDescriptor|null $action
1205
     * @param TransientVarsInterface $transientVars
1206
     * @param PropertySetInterface $ps
1207
     * @param $stepId
1208
     *
1209
     * @return boolean
1210
     *
1211
     * @throws \OldTown\Workflow\Exception\WorkflowException
1212
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1213
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1214
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1215
     */
1216 19
    protected function isActionAvailable(ActionDescriptor $action = null, TransientVarsInterface $transientVars, PropertySetInterface $ps, $stepId)
1217
    {
1218 19
        if (null === $action) {
1219
            return false;
1220
        }
1221
1222
        $result = null;
1223
        $actionHash = spl_object_hash($action);
1224
1225
        $result = array_key_exists($actionHash, $this->stateCache) ? $this->stateCache[$actionHash] : $result;
1226
1227
        $wf = $this->getWorkflowDescriptorForAction($action);
1228
1229
1230
        if (null === $result) {
1231
            $restriction = $action->getRestriction();
1232
            $conditions = null;
1233
1234
            if (null !== $restriction) {
1235
                $conditions = $restriction->getConditionsDescriptor();
1236
            }
1237 19
1238
            $result = $this->passesConditionsByDescriptor($wf->getGlobalConditions(), $transientVars, $ps, $stepId)
1239 19
                && $this->passesConditionsByDescriptor($conditions, $transientVars, $ps, $stepId);
1240
1241
            $this->stateCache[$actionHash] = $result;
1242
        }
1243
1244
1245
        $result = (boolean)$result;
1246
1247
        return $result;
1248
    }
1249
1250
    /**
1251
     * По дейсвтию получаем дексрипторв workflow
1252
     *
1253
     * @param ActionDescriptor $action
1254
     * @return WorkflowDescriptor
1255
     *
1256
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1257
     */
1258
    private function getWorkflowDescriptorForAction(ActionDescriptor $action)
1259
    {
1260
        $objWfd = $action;
1261
1262
        $count = 0;
1263
        while (!$objWfd instanceof WorkflowDescriptor || null === $objWfd) {
1264
            $objWfd = $objWfd->getParent();
1265
1266
            $count++;
1267
            if ($count > 10) {
1268
                $errMsg = 'Ошибка при получение WorkflowDescriptor';
1269
                throw new InternalWorkflowException($errMsg);
1270
            }
1271
        }
1272
1273
        return $objWfd;
1274
    }
1275
1276
1277
    /**
1278
     * Проверяет имеет ли пользователь достаточно прав, что бы иниициировать вызываемый процесс
1279
     *
1280
     * @param string $workflowName имя workflow
1281
     * @param integer $initialAction id начального состояния
1282
     * @param TransientVarsInterface $inputs
1283
     *
1284
     * @return bool
1285
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1286
     * @throws \OldTown\Workflow\Exception\FactoryException
1287
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1288
     * @throws \OldTown\Workflow\Exception\StoreException
1289
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1290
     */
1291
    public function canInitialize($workflowName, $initialAction, TransientVarsInterface $inputs = null)
1292
    {
1293
        $mockWorkflowName = $workflowName;
1294
        $mockEntry = new SimpleWorkflowEntry(0, $mockWorkflowName, WorkflowEntryInterface::CREATED);
1295
1296
        try {
1297
            $ps = PropertySetManager::getInstance('memory', null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1298
        } catch (\Exception $e) {
1299
            $errMsg = sprintf('Ошибка при создание PropertySer: %s', $e->getMessage());
1300
            throw new InternalWorkflowException($errMsg);
1301
        }
1302
1303
        if (null === $inputs) {
1304
            $inputs = $this->transientVarsFactory();
1305
        }
1306
        $transientVars = $inputs;
1307
1308
        try {
1309
            $this->populateTransientMap($mockEntry, $transientVars, [], $initialAction, [], $ps);
0 ignored issues
show
Bug introduced by
It seems like $ps defined by \OldTown\PropertySet\Pro...nstance('memory', null) on line 1297 can be null; however, OldTown\Workflow\Abstrac...:populateTransientMap() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1310
1311
            $result = $this->canInitializeInternal($workflowName, $initialAction, $transientVars, $ps);
0 ignored issues
show
Bug introduced by
It seems like $ps defined by \OldTown\PropertySet\Pro...nstance('memory', null) on line 1297 can be null; however, OldTown\Workflow\Abstrac...canInitializeInternal() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1312
1313
            return $result;
1314
        } catch (InvalidActionException $e) {
1315
            $this->getLog()->error($e->getMessage(), [$e]);
1316
1317
            return false;
1318
        } catch (WorkflowException $e) {
1319
            $errMsg = sprintf(
1320
                'Ошибка при проверки canInitialize: %s',
1321
                $e->getMessage()
1322
            );
1323
            $this->getLog()->error($errMsg, [$e]);
1324
1325
            return false;
1326
        }
1327
    }
1328
1329
1330
    /**
1331
     * Проверяет имеет ли пользователь достаточно прав, что бы иниициировать вызываемый процесс
1332
     *
1333
     * @param string $workflowName имя workflow
1334
     * @param integer $initialAction id начального состояния
1335
     * @param TransientVarsInterface $transientVars
1336
     *
1337
     * @param PropertySetInterface $ps
1338
     *
1339
     * @return bool
1340
     *
1341
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1342
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1343
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1344
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1345
     * @throws \OldTown\Workflow\Exception\WorkflowException
1346
     */
1347
    protected function canInitializeInternal($workflowName, $initialAction, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1348
    {
1349
        $wf = $this->getConfiguration()->getWorkflow($workflowName);
1350
1351
        $actionDescriptor = $wf->getInitialAction($initialAction);
1352
1353
        if (null === $actionDescriptor) {
1354
            $errMsg = sprintf(
1355
                'Некорректное инициирующие действие # %s',
1356
                $initialAction
1357
            );
1358
            throw new InvalidActionException($errMsg);
1359
        }
1360
1361
        $restriction = $actionDescriptor->getRestriction();
1362
1363
1364
        $conditions = null;
1365
        if (null !== $restriction) {
1366
            $conditions = $restriction->getConditionsDescriptor();
1367
        }
1368
1369
        $passesConditions = $this->passesConditionsByDescriptor($conditions, $transientVars, $ps, 0);
1370
1371
        return $passesConditions;
1372
    }
1373
1374
    /**
1375
     * Возвращает резолвер
1376
     *
1377
     * @return TypeResolverInterface
1378
     */
1379
    public function getResolver()
1380
    {
1381
        if (null !== $this->typeResolver) {
1382
            return $this->typeResolver;
1383
        }
1384
1385
        $classResolver = $this->getDefaultTypeResolverClass();
1386
        $r = new ReflectionClass($classResolver);
1387
        $resolver = $r->newInstance();
1388
        $this->typeResolver = $resolver;
1389
1390
        return $this->typeResolver;
1391
    }
1392
1393
    /**
1394
     * Возвращает хранилище состояния workflow
1395
     *
1396
     * @return WorkflowStoreInterface
1397
     * @throws \OldTown\Workflow\Exception\StoreException
1398
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1399
     */
1400
    protected function getPersistence()
1401
    {
1402
        return $this->getConfiguration()->getWorkflowStore();
1403
    }
1404
1405
    /**
1406
     * Получить конфигурацию workflow. Метод также проверяет была ли иницилазированн конфигурация, если нет, то
1407
     * инициализирует ее.
1408
     *
1409
     * Если конфигурация не была установленна, то возвращает конфигурацию по умолчанию
1410
     *
1411
     * @return ConfigurationInterface|DefaultConfiguration Конфигурация которая была установленна
1412
     *
1413
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1414
     */
1415
    public function getConfiguration()
1416
    {
1417
        $config = null !== $this->configuration ? $this->configuration : DefaultConfiguration::getInstance();
1418
1419
        if (!$config->isInitialized()) {
1420
            try {
1421
                $config->load(null);
1422
            } catch (FactoryException $e) {
1423
                $errMsg = 'Ошибка при иницилазации конфигурации workflow';
1424
                $this->getLog()->critical($errMsg, ['exception' => $e]);
1425
                throw new InternalWorkflowException($errMsg, $e->getCode(), $e);
1426
            }
1427
        }
1428
1429
        return $config;
1430
    }
1431
1432
    /**
1433
     * @return LoggerInterface
1434
     */
1435
    public function getLog()
1436
    {
1437
        return $this->log;
1438
    }
1439
1440
    /**
1441
     * @param LoggerInterface $log
1442
     *
1443
     * @return $this
1444
     * @throws InternalWorkflowException
1445
     */
1446
    public function setLog($log)
1447
    {
1448
        try {
1449
            LogFactory::validLogger($log);
1450
        } catch (\Exception $e) {
1451
            $errMsg = 'Ошибка при валидации логера';
1452
            throw new InternalWorkflowException($errMsg, $e->getCode(), $e);
1453
        }
1454
1455
1456
        $this->log = $log;
1457
1458
        return $this;
1459
    }
1460
1461
1462
    /**
1463
     * Get the workflow descriptor for the specified workflow name.
1464
     *
1465
     * @param string $workflowName The workflow name.
1466
     * @return WorkflowDescriptor
1467
     * @throws InternalWorkflowException
1468
     */
1469
    public function getWorkflowDescriptor($workflowName)
1470
    {
1471
        try {
1472
            return $this->getConfiguration()->getWorkflow($workflowName);
1473
        } catch (FactoryException $e) {
1474
            $errMsg = 'Ошибка при загрузке workflow';
1475
            $this->getLog()->error($errMsg, ['exception' => $e]);
1476
            throw new InternalWorkflowException($errMsg, $e->getCode(), $e);
1477
        }
1478
    }
1479
1480
1481
    /**
1482
     * Executes a special trigger-function using the context of the given workflow instance id.
1483
     * Note that this method is exposed for Quartz trigger jobs, user code should never call it.
1484
     * @param integer $id The workflow instance id
1485
     * @param integer $triggerId The id of the special trigger-function
1486
     * @throws \OldTown\Workflow\Exception\WorkflowException
1487
     * @throws \OldTown\Workflow\Exception\StoreException
1488
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1489
     * @throws \OldTown\Workflow\Exception\FactoryException
1490
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1491
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1492
     */
1493
    public function executeTriggerFunction($id, $triggerId)
1494
    {
1495
        $store = $this->getPersistence();
1496
        $entry = $store->findEntry($id);
1497
1498
        if (null === $entry) {
1499
            $errMsg = sprintf(
1500
                'Ошибка при выполнение тригера # %s для несуществующего экземпляра workflow id# %s',
1501
                $triggerId,
1502
                $id
1503
            );
1504
            $this->getLog()->warning($errMsg);
1505
            return;
1506
        }
1507
1508
        $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
1509
1510
        $ps = $store->getPropertySet($id);
1511
        $transientVars = $this->transientVarsFactory();
1512
1513
        $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), null, $store->findCurrentSteps($id), $ps);
1514
1515
        $this->executeFunction($wf->getTriggerFunction($triggerId), $transientVars, $ps);
1516
    }
1517
1518
    /**
1519
     * @param $id
1520
     * @param $inputs
1521
     *
1522
     * @return array
1523
     * @throws \OldTown\Workflow\Exception\StoreException
1524
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1525
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1526
     * @throws \OldTown\Workflow\Exception\WorkflowException
1527
     * @throws \OldTown\Workflow\Exception\FactoryException
1528
     */
1529
    public function getAvailableActions($id, TransientVarsInterface $inputs = null)
1530
    {
1531
        try {
1532
            $store = $this->getPersistence();
1533
            $entry = $store->findEntry($id);
1534
1535
            if (null === $entry) {
1536
                $errMsg = sprintf(
1537
                    'Не существует экземпляра workflow c id %s',
1538
                    $id
1539
                );
1540
                throw new InvalidArgumentException($errMsg);
1541
            }
1542
1543
            if (WorkflowEntryInterface::ACTIVATED === $entry->getState()) {
1544
                return [];
1545
            }
1546
1547
            $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
1548
1549 View Code Duplication
            if (null === $wf) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1550
                $errMsg = sprintf(
1551
                    'Не существует workflow c именем %s',
1552
                    $entry->getWorkflowName()
1553
                );
1554
                throw new InvalidArgumentException($errMsg);
1555
            }
1556
1557
            $l = [];
1558
            $ps = $store->getPropertySet($id);
1559
1560
            $transientVars = $inputs;
1561
            if (null === $transientVars) {
1562
                $transientVars = $this->transientVarsFactory();
1563
            }
1564
1565
            $currentSteps = $store->findCurrentSteps($id);
1566
1567
            $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), 0, $currentSteps, $ps);
1568
1569
            $globalActions = $wf->getGlobalActions();
1570
1571
            foreach ($globalActions as $action) {
1572
                $restriction = $action->getRestriction();
1573
                $conditions = null;
1574
1575
                $transientVars['actionId'] = $action->getId();
1576
1577
                if (null !== $restriction) {
1578
                    $conditions = $restriction->getConditionsDescriptor();
1579
                }
1580
1581
                $flag = $this->passesConditionsByDescriptor($wf->getGlobalConditions(), $transientVars, $ps, 0) && $this->passesConditionsByDescriptor($conditions, $transientVars, $ps, 0);
1582
                if ($flag) {
1583
                    $l[] = $action->getId();
1584
                }
1585
            }
1586
1587
1588 View Code Duplication
            foreach ($currentSteps as $step) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1589
                $data = $this->getAvailableActionsForStep($wf, $step, $transientVars, $ps);
1590
                foreach ($data as $v) {
1591
                    $l[] = $v;
1592
                }
1593
            }
1594
            return array_unique($l);
1595
        } catch (\Exception $e) {
1596
            $errMsg = 'Ошибка проверки доступных действий';
1597
            $this->getLog()->error($errMsg, [$e]);
1598
        }
1599
1600
        return [];
1601
    }
1602
1603
    /**
1604
     * @param WorkflowDescriptor   $wf
1605
     * @param StepInterface        $step
1606
     * @param TransientVarsInterface                $transientVars
1607
     * @param PropertySetInterface $ps
1608
     *
1609
     * @return array
1610
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1611
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1612
     * @throws \OldTown\Workflow\Exception\WorkflowException
1613
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1614
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1615
     */
1616
    protected function getAvailableActionsForStep(WorkflowDescriptor $wf, StepInterface $step, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1617
    {
1618
        $l = [];
1619
        $s = $wf->getStep($step->getStepId());
1620
1621
        if (null === $s) {
1622
            $errMsg = sprintf(
1623
                'getAvailableActionsForStep вызван с не существующим id шага %s',
1624
                $step->getStepId()
1625
            );
1626
1627
            $this->getLog()->warning($errMsg);
1628
1629
            return $l;
1630
        }
1631
1632
        $actions  = $s->getActions();
1633
1634
        if (null === $actions || 0  === $actions->count()) {
1635
            return $l;
1636
        }
1637
1638
        foreach ($actions as $action) {
1639
            $restriction = $action->getRestriction();
1640
            $conditions = null;
1641
1642
            $transientVars['actionId'] = $action->getId();
1643
1644
1645
            if (null !== $restriction) {
1646
                $conditions = $restriction->getConditionsDescriptor();
1647
            }
1648
1649
            $f = $this->passesConditionsByDescriptor($wf->getGlobalConditions(), $transientVars, $ps, $s->getId())
1650
                 && $this->passesConditionsByDescriptor($conditions, $transientVars, $ps, $s->getId());
1651
            if ($f) {
1652
                $l[] = $action->getId();
1653
            }
1654
        }
1655
1656
        return $l;
1657
    }
1658
1659
    /**
1660
     * @param ConfigurationInterface $configuration
1661
     *
1662
     * @return $this
1663
     */
1664
    public function setConfiguration(ConfigurationInterface $configuration)
1665
    {
1666
        $this->configuration = $configuration;
1667
1668
        return $this;
1669
    }
1670
1671
    /**
1672
     * Возвращает состояние для текущего экземпляра workflow
1673
     *
1674
     * @param integer $id id экземпляра workflow
1675
     * @return integer id текущего состояния
1676
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1677
     */
1678
    public function getEntryState($id)
1679
    {
1680
        try {
1681
            $store = $this->getPersistence();
1682
1683
            return $store->findEntry($id)->getState();
1684
        } catch (StoreException $e) {
1685
            $errMsg = sprintf(
1686
                'Ошибка при получение состояния экземпляра workflow c id# %s',
1687
                $id
1688
            );
1689
            $this->getLog()->error($errMsg, [$e]);
1690
        }
1691
1692
        return WorkflowEntryInterface::UNKNOWN;
1693
    }
1694
1695
    /**
1696
     * Returns a list of all steps that are completed for the given workflow instance id.
1697
     *
1698
     * @param integer $id The workflow instance id.
1699
     * @return StepInterface[] a List of Steps
1700
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1701
     */
1702 View Code Duplication
    public function getHistorySteps($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1703
    {
1704
        try {
1705
            $store = $this->getPersistence();
1706
1707
            return $store->findHistorySteps($id);
0 ignored issues
show
Bug Compatibility introduced by
The expression $store->findHistorySteps($id); of type OldTown\Workflow\Spi\Ste...face[]|SplObjectStorage adds the type SplObjectStorage to the return on line 1707 which is incompatible with the return type declared by the interface OldTown\Workflow\Workflo...erface::getHistorySteps of type OldTown\Workflow\Spi\StepInterface[].
Loading history...
1708
        } catch (StoreException $e) {
1709
            $errMsg = sprintf(
1710
                'Ошибка при получение истории шагов для экземпляра workflow c id# %s',
1711
                $id
1712
            );
1713
            $this->getLog()->error($errMsg, [$e]);
1714
        }
1715
1716
        return [];
1717
    }
1718
1719
    /**
1720
     * Настройки хранилища
1721
     *
1722
     * @return array
1723
     *
1724
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1725
     */
1726
    public function getPersistenceProperties()
1727
    {
1728
        return $this->getConfiguration()->getPersistenceArgs();
1729
    }
1730
1731
1732
    /**
1733
     * Get the PropertySet for the specified workflow instance id.
1734
     * @param integer $id The workflow instance id.
1735
     * @return PropertySetInterface
1736
     *
1737
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1738
     */
1739 View Code Duplication
    public function getPropertySet($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1740
    {
1741
        $ps = null;
1742
1743
        try {
1744
            $ps = $this->getPersistence()->getPropertySet($id);
1745
        } catch (StoreException $e) {
1746
            $errMsg = sprintf(
1747
                'Ошибка при получение PropertySet для экземпляра workflow c id# %s',
1748
                $id
1749
            );
1750
            $this->getLog()->error($errMsg, [$e]);
1751
        }
1752
1753
        return $ps;
1754
    }
1755
1756
    /**
1757
     * @return \String[]
1758
     *
1759
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1760
     */
1761
    public function getWorkflowNames()
1762
    {
1763
        try {
1764
            return $this->getConfiguration()->getWorkflowNames();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getConfigu...()->getWorkflowNames(); (string[]) is incompatible with the return type documented by OldTown\Workflow\Abstrac...kflow::getWorkflowNames of type String[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1765
        } catch (FactoryException $e) {
1766
            $errMsg = 'Ошибка при получение имен workflow';
1767
            $this->getLog()->error($errMsg, [$e]);
1768
        }
1769
1770
        return [];
1771
    }
1772
1773
    /**
1774
     * @param TypeResolverInterface $typeResolver
1775
     *
1776
     * @return $this
1777
     */
1778
    public function setTypeResolver(TypeResolverInterface $typeResolver)
1779
    {
1780
        $this->typeResolver = $typeResolver;
1781
1782
        return $this;
1783
    }
1784
1785
1786
    /**
1787
     * Get a collection (Strings) of currently defined permissions for the specified workflow instance.
1788
     * @param integer $id id the workflow instance id.
1789
     * @param TransientVarsInterface $inputs inputs The inputs to the workflow instance.
1790
     * @return array  A List of permissions specified currently (a permission is a string name).
1791
     *
1792
     */
1793
    public function getSecurityPermissions($id, TransientVarsInterface $inputs = null)
1794
    {
1795
        try {
1796
            $store = $this->getPersistence();
1797
            $entry = $store->findEntry($id);
1798
            $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
1799
1800
            $ps = $store->getPropertySet($id);
1801
1802
            if (null === $inputs) {
1803
                $inputs = $this->transientVarsFactory();
1804
            }
1805
            $transientVars = $inputs;
1806
1807
            $currentSteps = $store->findCurrentSteps($id);
1808
1809
            try {
1810
                $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), null, $currentSteps, $ps);
1811
            } catch (\Exception $e) {
1812
                $errMsg = sprintf(
1813
                    'Внутреннея ошибка: %s',
1814
                    $e->getMessage()
1815
                );
1816
                throw new InternalWorkflowException($errMsg, $e->getCode(), $e);
1817
            }
1818
1819
1820
            $s = [];
1821
1822
            foreach ($currentSteps as $step) {
1823
                $stepId = $step->getStepId();
1824
1825
                $xmlStep = $wf->getStep($stepId);
1826
1827
                $securities = $xmlStep->getPermissions();
1828
1829
                foreach ($securities as $security) {
1830
                    $conditionsDescriptor = $security->getRestriction()->getConditionsDescriptor();
1831
                    if (null !== $security->getRestriction() && $this->passesConditionsByDescriptor($conditionsDescriptor, $transientVars, $ps, $xmlStep->getId())) {
1832
                        $s[$security->getName()] = $security->getName();
1833
                    }
1834
                }
1835
            }
1836
1837
            return $s;
1838
        } catch (\Exception $e) {
1839
            $errMsg = sprintf(
1840
                'Ошибка при получение информации о правах доступа для экземпляра workflow c id# %s',
1841
                $id
1842
            );
1843
            $this->getLog()->error($errMsg, [$e]);
1844
        }
1845
1846
        return [];
1847
    }
1848
1849
1850
    /**
1851
     * Get the name of the specified workflow instance.
1852
     *
1853
     * @param integer $id the workflow instance id.
1854
     * @return string
1855
     *
1856
     * @throws \OldTown\Workflow\Exception\StoreException
1857
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1858
     */
1859
    public function getWorkflowName($id)
1860
    {
1861
        try {
1862
            $store = $this->getPersistence();
1863
            $entry = $store->findEntry($id);
1864
1865
            if (null !== $entry) {
1866
                return $entry->getWorkflowName();
1867
            }
1868
        } catch (FactoryException $e) {
1869
            $errMsg = sprintf(
1870
                'Ошибка при получение имен workflow для инстанса с id # %s',
1871
                $id
1872
            );
1873
            $this->getLog()->error($errMsg, [$e]);
1874
        }
1875
1876
        return null;
1877
    }
1878
1879
    /**
1880
     * Удаляет workflow
1881
     *
1882
     * @param string $workflowName
1883
     *
1884
     * @return bool
1885
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1886
     */
1887
    public function removeWorkflowDescriptor($workflowName)
1888
    {
1889
        return $this->getConfiguration()->removeWorkflow($workflowName);
1890
    }
1891
1892
    /**
1893
     * @param                    $workflowName
1894
     * @param WorkflowDescriptor $descriptor
1895
     * @param                    $replace
1896
     *
1897
     * @return bool
1898
     *
1899
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1900
     */
1901
    public function saveWorkflowDescriptor($workflowName, WorkflowDescriptor $descriptor, $replace)
1902
    {
1903
        $success = $this->getConfiguration()->saveWorkflow($workflowName, $descriptor, $replace);
1904
1905
        return $success;
1906
    }
1907
1908
1909
    /**
1910
     * Query the workflow store for matching instances
1911
     *
1912
     * @param WorkflowExpressionQuery $query
1913
     *
1914
     * @return array
1915
     *
1916
     * @throws \OldTown\Workflow\Exception\WorkflowException
1917
     * @throws \OldTown\Workflow\Exception\StoreException
1918
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1919
1920
     */
1921
    public function query(WorkflowExpressionQuery $query)
1922
    {
1923
        return $this->getPersistence()->query($query);
1924
    }
1925
1926
    /**
1927
     * @return string
1928
     */
1929
    public function getDefaultTypeResolverClass()
1930
    {
1931
        return $this->defaultTypeResolverClass;
1932
    }
1933
1934
    /**
1935
     * @param string $defaultTypeResolverClass
1936
     *
1937
     * @return $this
1938
     */
1939
    public function setDefaultTypeResolverClass($defaultTypeResolverClass)
1940
    {
1941
        $this->defaultTypeResolverClass = (string)$defaultTypeResolverClass;
1942
1943
        return $this;
1944
    }
1945
1946
1947
    /**
1948
     * @param ConditionsDescriptor $descriptor
1949
     * @param TransientVarsInterface $transientVars
1950
     * @param PropertySetInterface $ps
1951
     * @param                      $currentStepId
1952
     *
1953
     * @return bool
1954
     *
1955
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1956
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1957
     * @throws \OldTown\Workflow\Exception\WorkflowException
1958
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1959
     */
1960
    protected function passesConditionsByDescriptor(ConditionsDescriptor $descriptor = null, TransientVarsInterface $transientVars, PropertySetInterface $ps, $currentStepId)
1961
    {
1962
        if (null === $descriptor) {
1963
            return true;
1964
        }
1965
1966
        $type = $descriptor->getType();
1967
        $conditions = $descriptor->getConditions();
1968
        $passesConditions = $this->passesConditionsWithType($type, $conditions, $transientVars, $ps, $currentStepId);
0 ignored issues
show
Bug introduced by
It seems like $conditions defined by $descriptor->getConditions() on line 1967 can also be of type array<integer,object<Old...\ConditionsDescriptor>>; however, OldTown\Workflow\Abstrac...sesConditionsWithType() does only seem to accept null|object<SplObjectStorage>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1969
1970
        return $passesConditions;
1971
    }
1972
1973
    /**
1974
     * @param string $conditionType
1975
     * @param SplObjectStorage $conditions
1976
     * @param TransientVarsInterface $transientVars
1977
     * @param PropertySetInterface $ps
1978
     * @param integer $currentStepId
1979
     *
1980
     * @return bool
1981
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1982
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1983
     * @throws \OldTown\Workflow\Exception\WorkflowException
1984
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1985
     */
1986
    protected function passesConditionsWithType($conditionType, SplObjectStorage $conditions = null, TransientVarsInterface $transientVars, PropertySetInterface $ps, $currentStepId)
1987
    {
1988
        if (null === $conditions) {
1989
            return true;
1990
        }
1991
1992
        if (0 === $conditions->count()) {
1993
            return true;
1994
        }
1995
1996
        $and = strtoupper($conditionType) === 'AND';
1997
        $or = !$and;
1998
1999
        foreach ($conditions as $descriptor) {
2000
            if ($descriptor instanceof ConditionsDescriptor) {
2001
                $result = $this->passesConditionsWithType($descriptor->getType(), $descriptor->getConditions(), $transientVars, $ps, $currentStepId);
0 ignored issues
show
Bug introduced by
It seems like $descriptor->getConditions() targeting OldTown\Workflow\Loader\...riptor::getConditions() can also be of type array<integer,object<Old...\ConditionsDescriptor>>; however, OldTown\Workflow\Abstrac...sesConditionsWithType() does only seem to accept null|object<SplObjectStorage>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2002
            } else {
2003
                $result = $this->passesCondition($descriptor, $transientVars, $ps, $currentStepId);
2004
            }
2005
2006
            if ($and && !$result) {
2007
                return false;
2008
            } elseif ($or && $result) {
2009
                return true;
2010
            }
2011
        }
2012
2013
        if ($and) {
2014
            return true;
2015
        }
2016
2017
        return false;
2018
    }
2019
2020
    /**
2021
     * @param ConditionDescriptor $conditionDesc
2022
     * @param TransientVarsInterface $transientVars
2023
     * @param PropertySetInterface $ps
2024
     * @param integer $currentStepId
2025
     *
2026
     * @return boolean
2027
     *
2028
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
2029
     * @throws \OldTown\Workflow\Exception\WorkflowException
2030
     */
2031
    protected function passesCondition(ConditionDescriptor $conditionDesc, TransientVarsInterface $transientVars, PropertySetInterface $ps, $currentStepId)
2032
    {
2033
        $type = $conditionDesc->getType();
2034
2035
        $argsOriginal = $conditionDesc->getArgs();
2036
2037
2038
        $args = [];
2039 View Code Duplication
        foreach ($argsOriginal as $key => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2040
            $translateValue = $this->getConfiguration()->getVariableResolver()->translateVariables($value, $transientVars, $ps);
2041
            $args[$key] = $translateValue;
2042
        }
2043
2044
        if (-1 !== $currentStepId) {
2045
            $stepId = array_key_exists('stepId', $args) ? (integer)$args['stepId'] : null;
2046
2047
            if (null !== $stepId && -1 === $stepId) {
2048
                $args['stepId'] = $currentStepId;
2049
            }
2050
        }
2051
2052
        $condition = $this->getResolver()->getCondition($type, $args);
2053
2054
        if (null === $condition) {
2055
            $this->context->setRollbackOnly();
2056
            $errMsg = 'Огибка при загрузки условия';
2057
            throw new WorkflowException($errMsg);
2058
        }
2059
2060
        try {
2061
            $passed = $condition->passesCondition($transientVars, $args, $ps);
2062
2063
            if ($conditionDesc->isNegate()) {
2064
                $passed = !$passed;
2065
            }
2066
        } catch (\Exception $e) {
2067
            $this->context->setRollbackOnly();
2068
2069
            $errMsg = sprintf(
2070
                'Ошбика при выполнение условия %s',
2071
                get_class($condition)
2072
            );
2073
2074
            throw new WorkflowException($errMsg, $e->getCode(), $e);
2075
        }
2076
2077
        return $passed;
2078
    }
2079
}
2080