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
Branch dev (1461e3)
by Андрей
11:51 queued 05:41
created

AbstractWorkflow::doAction()   D

Complexity

Conditions 13
Paths 202

Size

Total Lines 73
Code Lines 39

Duplication

Lines 18
Ratio 24.66 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 18
loc 73
rs 4.9963
cc 13
eloc 39
nc 202
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * @return integer
107
     * @throws \OldTown\Workflow\Exception\InvalidRoleException
108
     * @throws \OldTown\Workflow\Exception\InvalidInputException
109
     * @throws \OldTown\Workflow\Exception\WorkflowException
110
     * @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
     *
118
     */
119
    public function initialize($workflowName, $initialAction, TransientVarsInterface $inputs = null)
120
    {
121
        try {
122
            $initialAction = (integer)$initialAction;
123
124
            $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
                throw new InvalidRoleException($errMsg);
145
            }
146
147
            $action = $wf->getInitialAction($initialAction);
148
149
            if (null === $action) {
150
                $errMsg = sprintf('Invalid initial action id: %s', $initialAction);
151
                throw new InvalidActionException($errMsg);
152
            }
153
154
            $currentSteps = new SplObjectStorage();
155
            $this->transitionWorkflow($entry, $currentSteps, $store, $wf, $action, $transientVars, $inputs, $ps);
156
157
            $entryId = $entry->getId();
158
        } catch (WorkflowException $e) {
159
            $this->context->setRollbackOnly();
160
            throw new InternalWorkflowException($e->getMessage(), $e->getCode(), $e);
161
        }
162
163
164
        // now clone the memory PS to the real PS
165
        //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...
166
        return $entryId;
167
    }
168
169
    /**
170
     * @param WorkflowEntryInterface $entry
171
     * @param TransientVarsInterface $transientVars
172
     * @param array|Traversable|RegisterDescriptor[]|SplObjectStorage $registersStorage
173
     * @param integer $actionId
174
     * @param array|Traversable $currentSteps
175
     * @param PropertySetInterface $ps
176
     *
177
     *
178
     * @return TransientVarsInterface
179
     *
180
     * @throws \OldTown\Workflow\Exception\WorkflowException
181
     * @throws \OldTown\Workflow\Exception\FactoryException
182
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
183
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
184
     * @throws \OldTown\Workflow\Exception\StoreException
185
     */
186
    protected function populateTransientMap(WorkflowEntryInterface $entry, TransientVarsInterface $transientVars, $registersStorage, $actionId = null, $currentSteps, PropertySetInterface $ps)
187
    {
188 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...
189
            $errMsg = 'CurrentSteps должен быть массивом, либо реализовывать интерфейс Traversable';
190
            throw new InvalidArgumentException($errMsg);
191
        }
192
193 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...
194
            $registers = [];
195
            foreach ($registersStorage as $k => $v) {
196
                $registers[$k] = $v;
197
            }
198
        } elseif (is_array($registersStorage)) {
199
            $registers = $registersStorage;
200
        } else {
201
            $errMsg = 'Registers должен быть массивом, либо реализовывать интерфейс Traversable';
202
            throw new InvalidArgumentException($errMsg);
203
        }
204
        /** @var RegisterDescriptor[] $registers */
205
206
        $transientVars['context'] = $this->context;
207
        $transientVars['entry'] = $entry;
208
        $transientVars['entryId'] = $entry->getId();
209
        $transientVars['store'] = $this->getPersistence();
210
        $transientVars['configuration'] = $this->getConfiguration();
211
        $transientVars['descriptor'] = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
212
213
        if (null !== $actionId) {
214
            $transientVars['actionId'] = $actionId;
215
        }
216
217
        $transientVars['currentSteps'] = $currentSteps;
218
219
220
        foreach ($registers as $register) {
221
            $args = $register->getArgs();
222
            $type = $register->getType();
223
224
            try {
225
                $r = $this->getResolver()->getRegister($type, $args);
226
            } catch (\Exception $e) {
227
                $errMsg = 'Ошибка при инициализации register';
228
                $this->context->setRollbackOnly();
229
                throw new WorkflowException($errMsg, $e->getCode(), $e);
230
            }
231
232
            $variableName = $register->getVariableName();
233
            try {
234
                $value = $r->registerVariable($this->context, $entry, $args, $ps);
235
236
                $transientVars[$variableName] = $value;
237
            } catch (\Exception $e) {
238
                $this->context->setRollbackOnly();
239
240
                $errMsg = sprintf(
241
                    'При получение значения переменной %s из registry %s произошла ошибка',
242
                    $variableName,
243
                    get_class($r)
244
                );
245
246
                throw new WorkflowException($errMsg, $e->getCode(), $e);
247
            }
248
        }
249
250
        return $transientVars;
251
    }
252
253
    /**
254
     * Переход между двумя статусами
255
     *
256
     * @param WorkflowEntryInterface $entry
257
     * @param SplObjectStorage|StepInterface[] $currentSteps
258
     * @param WorkflowStoreInterface $store
259
     * @param WorkflowDescriptor $wf
260
     * @param ActionDescriptor $action
261
     * @param TransientVarsInterface $transientVars
262
     * @param TransientVarsInterface $inputs
263
     * @param PropertySetInterface $ps
264
     *
265
     * @return boolean
266
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
267
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
268
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
269
     * @throws \OldTown\Workflow\Exception\WorkflowException
270
     * @throws \OldTown\Workflow\Exception\StoreException
271
     * @throws \OldTown\Workflow\Exception\InvalidEntryStateException
272
     * @throws \OldTown\Workflow\Exception\InvalidInputException
273
     * @throws \OldTown\Workflow\Exception\FactoryException
274
     * @throws \OldTown\Workflow\Exception\InvalidActionException
275
     */
276
    protected function transitionWorkflow(WorkflowEntryInterface $entry, SplObjectStorage $currentSteps, WorkflowStoreInterface $store, WorkflowDescriptor $wf, ActionDescriptor $action, TransientVarsInterface $transientVars, TransientVarsInterface $inputs, PropertySetInterface $ps)
277
    {
278
        $step = $this->getCurrentStep($wf, $action->getId(), $currentSteps, $transientVars, $ps);
279
280
        $validators = $action->getValidators();
281
        if ($validators->count() > 0) {
282
            $this->verifyInputs($entry, $validators, $transientVars, $ps);
283
        }
284
285
286
        if (null !== $step) {
287
            $stepPostFunctions = $wf->getStep($step->getStepId())->getPostFunctions();
288
            foreach ($stepPostFunctions as $function) {
289
                $this->executeFunction($function, $transientVars, $ps);
290
            }
291
        }
292
293
        $preFunctions = $action->getPreFunctions();
294
        foreach ($preFunctions as $preFunction) {
295
            $this->executeFunction($preFunction, $transientVars, $ps);
296
        }
297
298
        $conditionalResults = $action->getConditionalResults();
299
        $extraPreFunctions = null;
300
        $extraPostFunctions = null;
301
302
        $theResult = null;
303
304
305
        $currentStepId = null !== $step ? $step->getStepId()  : -1;
306
        foreach ($conditionalResults as $conditionalResult) {
307
            if ($this->passesConditionsWithType(null, $conditionalResult->getConditions(), $transientVars, $ps, $currentStepId)) {
308
                $theResult = $conditionalResult;
309
310
                $validatorsStorage = $conditionalResult->getValidators();
311
                if ($validatorsStorage->count() > 0) {
312
                    $this->verifyInputs($entry, $validatorsStorage, $transientVars, $ps);
313
                }
314
315
                $extraPreFunctions = $conditionalResult->getPreFunctions();
316
                $extraPostFunctions = $conditionalResult->getPostFunctions();
317
318
                break;
319
            }
320
        }
321
322
323
        if (null ===  $theResult) {
324
            $theResult = $action->getUnconditionalResult();
325
            $this->verifyInputs($entry, $theResult->getValidators(), $transientVars, $ps);
326
            $extraPreFunctions = $theResult->getPreFunctions();
327
            $extraPostFunctions = $theResult->getPostFunctions();
328
        }
329
330
        $logMsg = sprintf('theResult=%s %s', $theResult->getStep(), $theResult->getStatus());
331
        $this->getLog()->debug($logMsg);
332
333
334
        if ($extraPreFunctions && $extraPreFunctions->count() > 0) {
335
            foreach ($extraPreFunctions as $function) {
336
                $this->executeFunction($function, $transientVars, $ps);
337
            }
338
        }
339
340
        $split = $theResult->getSplit();
341
        $join = $theResult->getJoin();
342
        if (null !== $split && 0 !== $split) {
343
            $splitDesc = $wf->getSplit($split);
344
            $results = $splitDesc->getResults();
345
            $splitPreFunctions = [];
346
            $splitPostFunctions = [];
347
348
            foreach ($results as $resultDescriptor) {
349
                if ($resultDescriptor->getValidators()->count() > 0) {
350
                    $this->verifyInputs($entry, $resultDescriptor->getValidators(), $transientVars, $ps);
351
                }
352
353
                foreach ($resultDescriptor->getPreFunctions() as $function) {
354
                    $splitPreFunctions[] = $function;
355
                }
356
                foreach ($resultDescriptor->getPostFunctions() as $function) {
357
                    $splitPostFunctions[] = $function;
358
                }
359
            }
360
361
            foreach ($splitPreFunctions as $function) {
362
                $this->executeFunction($function, $transientVars, $ps);
363
            }
364
365
            if (!$action->isFinish()) {
366
                $moveFirst = true;
367
368
                //???????????????????
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...
369
//                $theResults = [];
370
//                foreach ($results as $result) {
371
//                    $theResults[] = $result;
372
//                }
373
374
                foreach ($results as $resultDescriptor) {
375
                    $moveToHistoryStep = null;
376
377
                    if ($moveFirst) {
378
                        $moveToHistoryStep = $step;
379
                    }
380
381
                    $previousIds = [];
382
383
                    if (null !== $step) {
384
                        $previousIds[] = $step->getStepId();
385
                    }
386
387
                    $this->createNewCurrentStep($resultDescriptor, $entry, $store, $action->getId(), $moveToHistoryStep, $previousIds, $transientVars, $ps);
388
                    $moveFirst = false;
389
                }
390
            }
391
392
393
            foreach ($splitPostFunctions as $function) {
394
                $this->executeFunction($function, $transientVars, $ps);
395
            }
396
        } elseif (null !== $join && 0 !== $join) {
397
            $joinDesc = $wf->getJoin($join);
398
            $oldStatus = $theResult->getOldStatus();
399
            $caller = $this->context->getCaller();
400
            if (null !== $step) {
401
                $step = $store->markFinished($step, $action->getId(), new DateTime(), $oldStatus, $caller);
402
            } else {
403
                $errMsg = 'Invalid step';
404
                throw new InternalWorkflowException($errMsg);
405
            }
406
407
408
            $store->moveToHistory($step);
409
410
            /** @var StepInterface[] $joinSteps */
411
            $joinSteps = [];
412
            $joinSteps[] = $step;
413
414 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...
415
                if ($currentStep->getId() !== $step->getId()) {
416
                    $stepDesc = $wf->getStep($currentStep->getStepId());
417
418
                    if ($stepDesc->resultsInJoin($join)) {
419
                        $joinSteps[] = $currentSteps;
420
                    }
421
                }
422
            }
423
424
            $historySteps = $store->findHistorySteps($entry->getId());
425
426 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...
427
                if ($historyStep->getId() !== $step->getId()) {
428
                    $stepDesc = $wf->getStep($historyStep->getStepId());
429
430
                    if ($stepDesc->resultsInJoin($join)) {
431
                        $joinSteps[] = $currentSteps;
432
                    }
433
                }
434
            }
435
436
            $jn = new JoinNodes($joinSteps);
437
            $transientVars['jn'] = $jn;
438
439
440
            if ($this->passesConditionsWithType(null, $joinDesc->getConditions(), $transientVars, $ps, 0)) {
441
                $joinResult = $joinDesc->getResult();
442
443
                $joinResultValidators = $joinResult->getValidators();
444
                if ($joinResultValidators->count() > 0) {
445
                    $this->verifyInputs($entry, $joinResultValidators, $transientVars, $ps);
446
                }
447
448
                foreach ($joinResult->getPreFunctions() as $function) {
449
                    $this->executeFunction($function, $transientVars, $ps);
450
                }
451
452
                $previousIds = [];
453
                $i = 1;
454
455
                foreach ($joinSteps as  $currentStep) {
456
                    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...
457
                        $store->moveToHistory($step);
458
                    }
459
460
                    $previousIds[$i] = $currentStep->getId();
461
                }
462
463
                if (!$action->isFinish()) {
464
                    $previousIds[0] = $step->getId();
465
                    $theResult = $joinDesc->getResult();
466
467
                    $this->createNewCurrentStep($theResult, $entry, $store, $action->getId(), null, $previousIds, $transientVars, $ps);
468
                }
469
470
                foreach ($joinResult->getPostFunctions() as $function) {
471
                    $this->executeFunction($function, $transientVars, $ps);
472
                }
473
            }
474
        } else {
475
            $previousIds = [];
476
477
            if (null !== $step) {
478
                $previousIds[] = $step->getId();
479
            }
480
481
            if (!$action->isFinish()) {
482
                $this->createNewCurrentStep($theResult, $entry, $store, $action->getId(), $step, $previousIds, $transientVars, $ps);
483
            }
484
        }
485
486
        if ($extraPostFunctions && $extraPostFunctions->count() > 0) {
487
            foreach ($extraPostFunctions as $function) {
488
                $this->executeFunction($function, $transientVars, $ps);
489
            }
490
        }
491
492
        if (WorkflowEntryInterface::COMPLETED !== $entry->getState() && null !== $wf->getInitialAction($action->getId())) {
493
            $this->changeEntryState($entry->getId(), WorkflowEntryInterface::ACTIVATED);
494
        }
495
496
        if ($action->isFinish()) {
497
            $this->completeEntry($action, $entry->getId(), $this->getCurrentSteps($entry->getId()), WorkflowEntryInterface::COMPLETED);
498
            return true;
499
        }
500
501
        $availableAutoActions = $this->getAvailableAutoActions($entry->getId(), $inputs);
502
503
        if (count($availableAutoActions) > 0) {
504
            $this->doAction($entry->getId(), $availableAutoActions[0], $inputs);
505
        }
506
507
        return false;
508
    }
509
510
511
    /**
512
     * @param       $id
513
     * @param TransientVarsInterface $inputs
514
     *
515
     * @return array
516
     * @throws \OldTown\Workflow\Exception\WorkflowException
517
     */
518
    protected function getAvailableAutoActions($id, TransientVarsInterface $inputs)
519
    {
520
        try {
521
            $store = $this->getPersistence();
522
            $entry = $store->findEntry($id);
523
524
            if (null === $entry) {
525
                $errMsg = sprintf(
526
                    'Нет сущности workflow c id %s',
527
                    $id
528
                );
529
                throw new InvalidArgumentException($errMsg);
530
            }
531
532
533
            if (WorkflowEntryInterface::ACTIVATED !== $entry->getState()) {
534
                $logMsg = sprintf('--> состояние %s', $entry->getState());
535
                $this->getLog()->debug($logMsg);
536
                return [0];
537
            }
538
539
            $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
540
541 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...
542
                $errMsg = sprintf(
543
                    'Нет workflow c именем %s',
544
                    $entry->getWorkflowName()
545
                );
546
                throw new InvalidArgumentException($errMsg);
547
            }
548
549
            $l = [];
550
            $ps = $store->getPropertySet($id);
551
            $transientVars = $inputs;
552
            $currentSteps = $store->findCurrentSteps($id);
553
554
            $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), 0, $currentSteps, $ps);
555
556
            $globalActions = $wf->getGlobalActions();
557
558 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...
559
                $transientVars['actionId'] = $action->getId();
560
561
                if ($action->getAutoExecute() && $this->isActionAvailable($action, $transientVars, $ps, 0)) {
562
                    $l[] = $action->getId();
563
                }
564
            }
565
566 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...
567
                $availableAutoActionsForStep = $this->getAvailableAutoActionsForStep($wf, $step, $transientVars, $ps);
568
                foreach ($availableAutoActionsForStep as $v) {
569
                    $l[] = $v;
570
                }
571
                //$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...
572
            }
573
574
            $l = array_unique($l);
575
576
            return $l;
577
        } catch (\Exception $e) {
578
            $errMsg = 'Ошибка при проверке доступных действий';
579
            $this->getLog()->error($errMsg, [$e]);
580
        }
581
582
        return [];
583
    }
584
585
586
    /**
587
     * @param WorkflowDescriptor   $wf
588
     * @param StepInterface        $step
589
     * @param TransientVarsInterface                $transientVars
590
     * @param PropertySetInterface $ps
591
     *
592
     * @return array
593
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
594
     * @throws \OldTown\Workflow\Exception\WorkflowException
595
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
596
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
597
     * @throws \OldTown\Workflow\Exception\InvalidActionException
598
     */
599
    protected function getAvailableAutoActionsForStep(WorkflowDescriptor $wf, StepInterface $step, TransientVarsInterface $transientVars, PropertySetInterface $ps)
600
    {
601
        $l = [];
602
        $s = $wf->getStep($step->getStepId());
603
604
        if (null === $s) {
605
            $msg = sprintf('getAvailableAutoActionsForStep вызвана с несуществующим id %s', $step->getStepId());
606
            $this->getLog()->debug($msg);
607
            return $l;
608
        }
609
610
611
        $actions = $s->getActions();
612
        if (null === $actions || 0 === $actions->count()) {
613
            return $l;
614
        }
615
616 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...
617
            $transientVars['actionId'] = $action->getId();
618
619
            if ($action->getAutoExecute() && $this->isActionAvailable($action, $transientVars, $ps, 0)) {
620
                $l[] = $action->getId();
621
            }
622
        }
623
624
        return $l;
625
    }
626
627
    /**
628
     * @param ActionDescriptor $action
629
     * @param                  $id
630
     * @param array|Traversable $currentSteps
631
     * @param                  $state
632
     *
633
     * @return void
634
     *
635
     * @throws \OldTown\Workflow\Exception\StoreException
636
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
637
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
638
     */
639
    protected function completeEntry(ActionDescriptor $action = null, $id, $currentSteps, $state)
640
    {
641 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...
642
            $errMsg = 'CurrentSteps должен быть массивом, либо реализовывать интерфейс Traversable';
643
            throw new InvalidArgumentException($errMsg);
644
        }
645
646
647
        $this->getPersistence()->setEntryState($id, $state);
648
649
        $oldStatus = null !== $action ? $action->getUnconditionalResult()->getOldStatus() : 'Finished';
650
        $actionIdValue = null !== $action ? $action->getId() : -1;
651
        foreach ($currentSteps as $step) {
652
            $this->getPersistence()->markFinished($step, $actionIdValue, new DateTime(), $oldStatus, $this->context->getCaller());
653
            $this->getPersistence()->moveToHistory($step);
654
        }
655
    }
656
    /**
657
     * @param ResultDescriptor       $theResult
658
     * @param WorkflowEntryInterface $entry
659
     * @param WorkflowStoreInterface $store
660
     * @param integer                $actionId
661
     * @param StepInterface          $currentStep
662
     * @param array                  $previousIds
663
     * @param TransientVarsInterface                  $transientVars
664
     * @param PropertySetInterface   $ps
665
     *
666
     * @return StepInterface
667
     * @throws \OldTown\Workflow\Exception\WorkflowException
668
     * @throws \OldTown\Workflow\Exception\StoreException
669
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
670
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
671
     */
672
    protected function createNewCurrentStep(
673
        ResultDescriptor $theResult,
674
        WorkflowEntryInterface $entry,
675
        WorkflowStoreInterface $store,
676
        $actionId,
677
        StepInterface $currentStep = null,
678
        array $previousIds = [],
679
        TransientVarsInterface $transientVars,
680
        PropertySetInterface $ps
681
    ) {
682
        try {
683
            $nextStep = $theResult->getStep();
684
685
            if (-1 === $nextStep) {
686
                if (null !== $currentStep) {
687
                    $nextStep = $currentStep->getStepId();
688
                } else {
689
                    $errMsg = 'Неверный аргумент. Новый шаг является таким же как текущий. Но текущий шаг не указан';
690
                    throw new StoreException($errMsg);
691
                }
692
            }
693
694
            $owner = $theResult->getOwner();
695
696
            $logMsg = sprintf(
697
                'Результат: stepId=%s, status=%s, owner=%s, actionId=%s, currentStep=%s',
698
                $nextStep,
699
                $theResult->getStatus(),
700
                $owner,
701
                $actionId,
702
                null !== $currentStep ? $currentStep->getId() : 0
703
            );
704
            $this->getLog()->debug($logMsg);
705
706
            $variableResolver = $this->getConfiguration()->getVariableResolver();
707
708
            if (null !== $owner) {
709
                $o = $variableResolver->translateVariables($owner, $transientVars, $ps);
710
                $owner = null !== $o ? (string)$o : null;
711
            }
712
713
714
            $oldStatus = $theResult->getOldStatus();
715
            $oldStatus = (string)$variableResolver->translateVariables($oldStatus, $transientVars, $ps);
716
717
            $status = $theResult->getStatus();
718
            $status = (string)$variableResolver->translateVariables($status, $transientVars, $ps);
719
720
721
            if (null !== $currentStep) {
722
                $store->markFinished($currentStep, $actionId, new DateTime(), $oldStatus, $this->context->getCaller());
723
                $store->moveToHistory($currentStep);
724
            }
725
726
            $startDate = new DateTime();
727
            $dueDate = null;
728
729
            $theResultDueDate = (string)$theResult->getDueDate();
730
            $theResultDueDate = trim($theResultDueDate);
731
            if (strlen($theResultDueDate) > 0) {
732
                $dueDateObject = $variableResolver->translateVariables($theResultDueDate, $transientVars, $ps);
733
734
                if ($dueDateObject instanceof DateTime) {
735
                    $dueDate = $dueDateObject;
736
                } elseif (is_string($dueDateObject)) {
737
                    $dueDate = new DateTime($dueDate);
738
                } elseif (is_numeric($dueDateObject)) {
739
                    $dueDate = DateTime::createFromFormat('U', $dueDateObject);
740
                } else {
741
                    $errMsg = 'Ошибка при преобразование DueData';
742
                    throw new InternalWorkflowException($errMsg);
743
                }
744
            }
745
746
            $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 739 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...
747
            $transientVars['createdStep'] =  $newStep;
748
749
            if (null === $currentStep && 0 === count($previousIds)) {
750
                $currentSteps = [];
751
                $currentSteps[] = $newStep;
752
                $transientVars['currentSteps'] =  $currentSteps;
753
            }
754
755
            if (! $transientVars->offsetExists('descriptor')) {
756
                $errMsg = 'Ошибка при получение дескриптора workflow из transientVars';
757
                throw new InternalWorkflowException($errMsg);
758
            }
759
760
            /** @var WorkflowDescriptor $descriptor */
761
            $descriptor = $transientVars['descriptor'];
762
            $step = $descriptor->getStep($nextStep);
763
764
            if (null === $step) {
765
                $errMsg = sprintf('Шаг #%s не найден', $nextStep);
766
                throw new WorkflowException($errMsg);
767
            }
768
769
            $preFunctions = $step->getPreFunctions();
770
771
            foreach ($preFunctions as $function) {
772
                $this->executeFunction($function, $transientVars, $ps);
773
            }
774
        } catch (WorkflowException $e) {
775
            $this->context->setRollbackOnly();
776
            /** @var WorkflowException $e */
777
            throw $e;
778
        }
779
    }
780
781
    /**
782
     * Создает хранилище переменных
783
     *
784
     * @param $class
785
     *
786
     * @return TransientVarsInterface
787
     */
788
    protected function transientVarsFactory($class = BaseTransientVars::class)
789
    {
790
        $r = new \ReflectionClass($class);
791
        return $r->newInstance();
792
    }
793
794
    /**
795
     *
796
     *
797
     * Perform an action on the specified workflow instance.
798
     * @param integer $id The workflow instance id.
799
     * @param integer $actionId The action id to perform (action id's are listed in the workflow descriptor).
800
     * @param TransientVarsInterface $inputs The inputs to the workflow instance.
801
     * @throws \OldTown\Workflow\Exception\InvalidInputException if a validator is specified and an input is invalid.
802
     * @throws WorkflowException if the action is invalid for the specified workflow
803
     * instance's current state.
804
     *
805
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
806
     * @throws \OldTown\Workflow\Exception\StoreException
807
     * @throws \OldTown\Workflow\Exception\FactoryException
808
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
809
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
810
     * @throws \OldTown\Workflow\Exception\InvalidActionException
811
     * @throws \OldTown\Workflow\Exception\InvalidEntryStateException
812
     * @throws \OldTown\Workflow\Exception\WorkflowException
813
     */
814
    public function doAction($id, $actionId, TransientVarsInterface $inputs = null)
815
    {
816
        $actionId = (integer)$actionId;
817
        if (null === $inputs) {
818
            $inputs = $this->transientVarsFactory();
819
        }
820
        $transientVars = $inputs;
821
        $inputs = clone $transientVars;
822
823
        $store = $this->getPersistence();
824
        $entry = $store->findEntry($id);
825
826
        if (WorkflowEntryInterface::ACTIVATED !== $entry->getState()) {
827
            return;
828
        }
829
830
        $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
831
832
        $currentSteps = $store->findCurrentSteps($id);
833
        $action = null;
834
835
        $ps = $store->getPropertySet($id);
836
837
        $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), $actionId, $currentSteps, $ps);
838
839
840
        $validAction = false;
841
842 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...
843
            if ($actionId === $actionDesc->getId()) {
844
                $action = $actionDesc;
845
846
                if ($this->isActionAvailable($action, $transientVars, $ps, 0)) {
847
                    $validAction = true;
848
                }
849
            }
850
        }
851
852
853
        foreach ($currentSteps as $step) {
854
            $s = $wf->getStep($step->getStepId());
855
856 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...
857
                if ($actionId === $actionDesc->getId()) {
858
                    $action = $actionDesc;
859
860
                    if ($this->isActionAvailable($action, $transientVars, $ps, $s->getId())) {
861
                        $validAction = true;
862
                    }
863
                }
864
            }
865
        }
866
867
868
        if (!$validAction) {
869
            $errMsg = sprintf(
870
                'Action %s is invalid',
871
                $actionId
872
            );
873
            throw new InvalidActionException($errMsg);
874
        }
875
876
877
        try {
878
            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...
879
                $this->checkImplicitFinish($action, $id);
880
            }
881
        } catch (WorkflowException $e) {
882
            $this->context->setRollbackOnly();
883
            /** @var  WorkflowException $e*/
884
            throw $e;
885
        }
886
    }
887
888
    /**
889
     * @param ActionDescriptor $action
890
     * @param                  $id
891
     *
892
     * @return void
893
     *
894
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
895
     * @throws \OldTown\Workflow\Exception\StoreException
896
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
897
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
898
     *
899
     */
900
    protected function checkImplicitFinish(ActionDescriptor $action, $id)
901
    {
902
        $store = $this->getPersistence();
903
        $entry = $store->findEntry($id);
904
905
        $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
906
907
        $currentSteps = $store->findCurrentSteps($id);
908
909
        $isCompleted = $wf->getGlobalActions()->count() === 0;
910
911
        foreach ($currentSteps as $step) {
912
            if ($isCompleted) {
913
                break;
914
            }
915
916
            $stepDes = $wf->getStep($step->getStepId());
917
918
            if ($stepDes->getActions()->count() > 0) {
919
                $isCompleted = true;
920
            }
921
        }
922
923
        if ($isCompleted) {
924
            $this->completeEntry($action, $id, $currentSteps, WorkflowEntryInterface::COMPLETED);
925
        }
926
    }
927
928
    /**
929
     *
930
     * Check if the state of the specified workflow instance can be changed to the new specified one.
931
     * @param integer $id The workflow instance id.
932
     * @param integer $newState The new state id.
933
     * @return boolean true if the state of the workflow can be modified, false otherwise.
934
     * @throws \OldTown\Workflow\Exception\StoreException
935
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
936
     *
937
     */
938
    public function canModifyEntryState($id, $newState)
939
    {
940
        $store = $this->getPersistence();
941
        $entry = $store->findEntry($id);
942
943
        $currentState = $entry->getState();
944
945
        $result = false;
946
        try {
947
            switch ($newState) {
948
                case WorkflowEntryInterface::COMPLETED: {
949
                    if (WorkflowEntryInterface::ACTIVATED === $currentState) {
950
                        $result = true;
951
                    }
952
                    break;
953
                }
954
955
                //@TODO Разобраться с бизнес логикой. Может быть нужно добавить break
956
                /** @noinspection PhpMissingBreakStatementInspection */
957
                case WorkflowEntryInterface::CREATED: {
958
                    $result = false;
959
                }
960 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...
961
                    if (WorkflowEntryInterface::CREATED === $currentState || WorkflowEntryInterface::SUSPENDED === $currentState) {
962
                        $result = true;
963
                    }
964
                    break;
965
                }
966
                case WorkflowEntryInterface::SUSPENDED: {
967
                    if (WorkflowEntryInterface::ACTIVATED === $currentState) {
968
                        $result = true;
969
                    }
970
                    break;
971
                }
972 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...
973
                    if (WorkflowEntryInterface::CREATED === $currentState || WorkflowEntryInterface::ACTIVATED === $currentState || WorkflowEntryInterface::SUSPENDED === $currentState) {
974
                        $result = true;
975
                    }
976
                    break;
977
                }
978
                default: {
979
                    $result = false;
980
                    break;
981
                }
982
983
            }
984
985
            return $result;
986
        } 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...
987
            $errMsg = sprintf(
988
                'Ошибка проверки изменения состояния для инстанса #%s',
989
                $id
990
            );
991
            $this->getLog()->error($errMsg, [$e]);
992
        }
993
994
        return false;
995
    }
996
997
998
    /**
999
     *
1000
     * Возвращает коллекцию объектов описывающие состояние для текущего экземпляра workflow
1001
     *
1002
     * @param integer $id id экземпляра workflow
1003
     * @return SplObjectStorage|StepInterface[]
1004
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1005
     */
1006 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...
1007
    {
1008
        try {
1009
            $store = $this->getPersistence();
1010
1011
            return $store->findCurrentSteps($id);
1012
        } catch (StoreException $e) {
1013
            $errMsg = sprintf(
1014
                'Ошибка при проверке текущего шага для инстанса # %s',
1015
                $id
1016
            );
1017
            $this->getLog()->error($errMsg, [$e]);
1018
1019
1020
            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...
1021
        }
1022
    }
1023
1024
    /**
1025
     *
1026
     *
1027
     * Modify the state of the specified workflow instance.
1028
     * @param integer $id The workflow instance id.
1029
     * @param integer $newState the new state to change the workflow instance to.
1030
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1031
     * @throws \OldTown\Workflow\Exception\StoreException
1032
     * @throws \OldTown\Workflow\Exception\InvalidEntryStateException
1033
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1034
     */
1035
    public function changeEntryState($id, $newState)
1036
    {
1037
        $store = $this->getPersistence();
1038
        $entry = $store->findEntry($id);
1039
1040
        if ($newState === $entry->getState()) {
1041
            return;
1042
        }
1043
1044
        if ($this->canModifyEntryState($id, $newState)) {
1045
            if (WorkflowEntryInterface::KILLED === $newState || WorkflowEntryInterface::COMPLETED === $newState) {
1046
                $currentSteps = $this->getCurrentSteps($id);
1047
1048
                if (count($currentSteps) > 0) {
1049
                    $this->completeEntry(null, $id, $currentSteps, $newState);
1050
                }
1051
            }
1052
1053
            $store->setEntryState($id, $newState);
1054
        } else {
1055
            $errMsg = sprintf(
1056
                'Не возможен переход в экземпляре workflow #%s. Текущее состояние %s, ожидаемое состояние %s',
1057
                $id,
1058
                $entry->getState(),
1059
                $newState
1060
            );
1061
1062
            throw new InvalidEntryStateException($errMsg);
1063
        }
1064
1065
        $msg = sprintf(
1066
            '%s : Новое состояние: %s',
1067
            $entry->getId(),
1068
            $entry->getState()
1069
        );
1070
        $this->getLog()->debug($msg);
1071
    }
1072
1073
1074
    /**
1075
     * @param FunctionDescriptor $function
1076
     * @param TransientVarsInterface $transientVars
1077
     * @param PropertySetInterface $ps
1078
     *
1079
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1080
     * @throws \OldTown\Workflow\Exception\WorkflowException
1081
     */
1082
    protected function executeFunction(FunctionDescriptor $function, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1083
    {
1084
        if (null !== $function) {
1085
            $type = $function->getType();
1086
1087
            $argsOriginal = $function->getArgs();
1088
            $args = [];
1089
1090 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...
1091
                $translateValue = $this->getConfiguration()->getVariableResolver()->translateVariables($v, $transientVars, $ps);
1092
                $args[$k] = $translateValue;
1093
            }
1094
1095
            $provider = $this->getResolver()->getFunction($type, $args);
1096
1097
            if (null === $provider) {
1098
                $this->context->setRollbackOnly();
1099
                $errMsg = 'Не загружен провайдер для функции';
1100
                throw new WorkflowException($errMsg);
1101
            }
1102
1103
            try {
1104
                $provider->execute($transientVars, $args, $ps);
1105
            } catch (WorkflowException $e) {
1106
                $this->context->setRollbackOnly();
1107
                /** @var  WorkflowException $e*/
1108
                throw $e;
1109
            }
1110
        }
1111
    }
1112
1113
1114
    /**
1115
     * @param WorkflowEntryInterface $entry
1116
     * @param $validatorsStorage
1117
     * @param TransientVarsInterface $transientVars
1118
     * @param PropertySetInterface $ps
1119
     * @throws \OldTown\Workflow\Exception\WorkflowException
1120
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1121
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1122
     * @throws \OldTown\Workflow\Exception\InvalidInputException
1123
     */
1124
    protected function verifyInputs(WorkflowEntryInterface $entry, $validatorsStorage, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1125
    {
1126 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...
1127
            $validators = [];
1128
            foreach ($validatorsStorage as $k => $v) {
1129
                $validators[$k] = $v;
1130
            }
1131
        } elseif (is_array($validatorsStorage)) {
1132
            $validators = $validatorsStorage;
1133
        } else {
1134
            $errMsg = sprintf(
1135
                'Validators должен быть массивом, либо реализовывать интерфейс Traversable. EntryId: %s',
1136
                $entry->getId()
1137
            );
1138
            throw new InvalidArgumentException($errMsg);
1139
        }
1140
1141
        /** @var ValidatorDescriptor[] $validators */
1142
        foreach ($validators as $input) {
1143
            if (null !== $input) {
1144
                $type = $input->getType();
1145
                $argsOriginal = $input->getArgs();
1146
1147
                $args = [];
1148
1149 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...
1150
                    $translateValue = $this->getConfiguration()->getVariableResolver()->translateVariables($v, $transientVars, $ps);
1151
                    $args[$k] = $translateValue;
1152
                }
1153
1154
1155
                $validator = $this->getResolver()->getValidator($type, $args);
1156
1157
                if (null === $validator) {
1158
                    $this->context->setRollbackOnly();
1159
                    $errMsg = 'Ошибка при загрузке валидатора';
1160
                    throw new WorkflowException($errMsg);
1161
                }
1162
1163
                try {
1164
                    $validator->validate($transientVars, $args, $ps);
1165
                } catch (InvalidInputException $e) {
1166
                    /** @var  InvalidInputException $e*/
1167
                    throw $e;
1168
                } catch (\Exception $e) {
1169
                    $this->context->setRollbackOnly();
1170
1171
                    if ($e instanceof WorkflowException) {
1172
                        /** @var  WorkflowException $e*/
1173
                        throw $e;
1174
                    }
1175
1176
                    throw new WorkflowException($e->getMessage(), $e->getCode(), $e);
1177
                }
1178
            }
1179
        }
1180
    }
1181
1182
1183
    /**
1184
     * Возвращает текущий шаг
1185
     *
1186
     * @param WorkflowDescriptor $wfDesc
1187
     * @param integer $actionId
1188
     * @param StepInterface[]|SplObjectStorage $currentSteps
1189
     * @param TransientVarsInterface $transientVars
1190
     * @param PropertySetInterface $ps
1191
     *
1192
     * @return StepInterface
1193
     * @throws \OldTown\Workflow\Exception\WorkflowException
1194
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1195
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1196
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1197
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1198
     */
1199
    protected function getCurrentStep(WorkflowDescriptor $wfDesc, $actionId, SplObjectStorage $currentSteps, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1200
    {
1201
        if (1 === $currentSteps->count()) {
1202
            $currentSteps->rewind();
1203
            return $currentSteps->current();
1204
        }
1205
1206
1207
        foreach ($currentSteps as $step) {
1208
            $stepId = $step->getId();
1209
            $action = $wfDesc->getStep($stepId)->getAction($actionId);
1210
1211
            if ($this->isActionAvailable($action, $transientVars, $ps, $stepId)) {
1212
                return $step;
1213
            }
1214
        }
1215
1216
        return null;
1217
    }
1218
1219
    /**
1220
     * @param ActionDescriptor|null $action
1221
     * @param TransientVarsInterface $transientVars
1222
     * @param PropertySetInterface $ps
1223
     * @param $stepId
1224
     *
1225
     * @return boolean
1226
     *
1227
     * @throws \OldTown\Workflow\Exception\WorkflowException
1228
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1229
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1230
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1231
     */
1232
    protected function isActionAvailable(ActionDescriptor $action = null, TransientVarsInterface $transientVars, PropertySetInterface $ps, $stepId)
1233
    {
1234
        if (null === $action) {
1235
            return false;
1236
        }
1237
1238
        $result = null;
1239
        $actionHash = spl_object_hash($action);
1240
1241
        $result = array_key_exists($actionHash, $this->stateCache) ? $this->stateCache[$actionHash] : $result;
1242
1243
        $wf = $this->getWorkflowDescriptorForAction($action);
1244
1245
1246
        if (null === $result) {
1247
            $restriction = $action->getRestriction();
1248
            $conditions = null;
1249
1250
            if (null !== $restriction) {
1251
                $conditions = $restriction->getConditionsDescriptor();
1252
            }
1253
1254
            $result = $this->passesConditionsByDescriptor($wf->getGlobalConditions(), $transientVars, $ps, $stepId)
1255
                && $this->passesConditionsByDescriptor($conditions, $transientVars, $ps, $stepId);
1256
1257
            $this->stateCache[$actionHash] = $result;
1258
        }
1259
1260
1261
        $result = (boolean)$result;
1262
1263
        return $result;
1264
    }
1265
1266
    /**
1267
     * По дейсвтию получаем дексрипторв workflow
1268
     *
1269
     * @param ActionDescriptor $action
1270
     * @return WorkflowDescriptor
1271
     *
1272
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1273
     */
1274
    private function getWorkflowDescriptorForAction(ActionDescriptor $action)
1275
    {
1276
        $objWfd = $action;
1277
1278
        $count = 0;
1279
        while (!$objWfd instanceof WorkflowDescriptor || null === $objWfd) {
1280
            $objWfd = $objWfd->getParent();
1281
1282
            $count++;
1283
            if ($count > 10) {
1284
                $errMsg = 'Ошибка при получение WorkflowDescriptor';
1285
                throw new InternalWorkflowException($errMsg);
1286
            }
1287
        }
1288
1289
        return $objWfd;
1290
    }
1291
1292
1293
    /**
1294
     * Проверяет имеет ли пользователь достаточно прав, что бы иниициировать вызываемый процесс
1295
     *
1296
     * @param string $workflowName имя workflow
1297
     * @param integer $initialAction id начального состояния
1298
     * @param TransientVarsInterface $inputs
1299
     *
1300
     * @return bool
1301
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1302
     * @throws \OldTown\Workflow\Exception\FactoryException
1303
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1304
     * @throws \OldTown\Workflow\Exception\StoreException
1305
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1306
     */
1307
    public function canInitialize($workflowName, $initialAction, TransientVarsInterface $inputs = null)
1308
    {
1309
        $mockWorkflowName = $workflowName;
1310
        $mockEntry = new SimpleWorkflowEntry(0, $mockWorkflowName, WorkflowEntryInterface::CREATED);
1311
1312
        try {
1313
            $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...
1314
        } catch (\Exception $e) {
1315
            $errMsg = sprintf('Ошибка при создание PropertySer: %s', $e->getMessage());
1316
            throw new InternalWorkflowException($errMsg);
1317
        }
1318
1319
        if (null === $inputs) {
1320
            $inputs = $this->transientVarsFactory();
1321
        }
1322
        $transientVars = $inputs;
1323
1324
        try {
1325
            $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 1313 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...
1326
1327
            $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 1313 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...
1328
1329
            return $result;
1330
        } catch (InvalidActionException $e) {
1331
            $this->getLog()->error($e->getMessage(), [$e]);
1332
1333
            return false;
1334
        } catch (WorkflowException $e) {
1335
            $errMsg = sprintf(
1336
                'Ошибка при проверки canInitialize: %s',
1337
                $e->getMessage()
1338
            );
1339
            $this->getLog()->error($errMsg, [$e]);
1340
1341
            return false;
1342
        }
1343
    }
1344
1345
1346
    /**
1347
     * Проверяет имеет ли пользователь достаточно прав, что бы иниициировать вызываемый процесс
1348
     *
1349
     * @param string $workflowName имя workflow
1350
     * @param integer $initialAction id начального состояния
1351
     * @param TransientVarsInterface $transientVars
1352
     *
1353
     * @param PropertySetInterface $ps
1354
     *
1355
     * @return bool
1356
     *
1357
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1358
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1359
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1360
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1361
     * @throws \OldTown\Workflow\Exception\WorkflowException
1362
     */
1363
    protected function canInitializeInternal($workflowName, $initialAction, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1364
    {
1365
        $wf = $this->getConfiguration()->getWorkflow($workflowName);
1366
1367
        $actionDescriptor = $wf->getInitialAction($initialAction);
1368
1369
        if (null === $actionDescriptor) {
1370
            $errMsg = sprintf(
1371
                'Некорректное инициирующие действие # %s',
1372
                $initialAction
1373
            );
1374
            throw new InvalidActionException($errMsg);
1375
        }
1376
1377
        $restriction = $actionDescriptor->getRestriction();
1378
1379
1380
        $conditions = null;
1381
        if (null !== $restriction) {
1382
            $conditions = $restriction->getConditionsDescriptor();
1383
        }
1384
1385
        $passesConditions = $this->passesConditionsByDescriptor($conditions, $transientVars, $ps, 0);
1386
1387
        return $passesConditions;
1388
    }
1389
1390
    /**
1391
     * Возвращает резолвер
1392
     *
1393
     * @return TypeResolverInterface
1394
     */
1395
    public function getResolver()
1396
    {
1397
        if (null !== $this->typeResolver) {
1398
            return $this->typeResolver;
1399
        }
1400
1401
        $classResolver = $this->getDefaultTypeResolverClass();
1402
        $r = new ReflectionClass($classResolver);
1403
        $resolver = $r->newInstance();
1404
        $this->typeResolver = $resolver;
1405
1406
        return $this->typeResolver;
1407
    }
1408
1409
    /**
1410
     * Возвращает хранилище состояния workflow
1411
     *
1412
     * @return WorkflowStoreInterface
1413
     * @throws \OldTown\Workflow\Exception\StoreException
1414
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1415
     */
1416
    protected function getPersistence()
1417
    {
1418
        return $this->getConfiguration()->getWorkflowStore();
1419
    }
1420
1421
    /**
1422
     * Получить конфигурацию workflow. Метод также проверяет была ли иницилазированн конфигурация, если нет, то
1423
     * инициализирует ее.
1424
     *
1425
     * Если конфигурация не была установленна, то возвращает конфигурацию по умолчанию
1426
     *
1427
     * @return ConfigurationInterface|DefaultConfiguration Конфигурация которая была установленна
1428
     *
1429
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1430
     */
1431
    public function getConfiguration()
1432
    {
1433
        $config = null !== $this->configuration ? $this->configuration : DefaultConfiguration::getInstance();
1434
1435
        if (!$config->isInitialized()) {
1436
            try {
1437
                $config->load(null);
1438
            } catch (FactoryException $e) {
1439
                $errMsg = 'Ошибка при иницилазации конфигурации workflow';
1440
                $this->getLog()->critical($errMsg, ['exception' => $e]);
1441
                throw new InternalWorkflowException($errMsg, $e->getCode(), $e);
1442
            }
1443
        }
1444
1445
        return $config;
1446
    }
1447
1448
    /**
1449
     * @return LoggerInterface
1450
     */
1451
    public function getLog()
1452
    {
1453
        return $this->log;
1454
    }
1455
1456
    /**
1457
     * @param LoggerInterface $log
1458
     *
1459
     * @return $this
1460
     * @throws InternalWorkflowException
1461
     */
1462
    public function setLog($log)
1463
    {
1464
        try {
1465
            LogFactory::validLogger($log);
1466
        } catch (\Exception $e) {
1467
            $errMsg = 'Ошибка при валидации логера';
1468
            throw new InternalWorkflowException($errMsg, $e->getCode(), $e);
1469
        }
1470
1471
1472
        $this->log = $log;
1473
1474
        return $this;
1475
    }
1476
1477
1478
    /**
1479
     * Get the workflow descriptor for the specified workflow name.
1480
     *
1481
     * @param string $workflowName The workflow name.
1482
     * @return WorkflowDescriptor
1483
     * @throws InternalWorkflowException
1484
     */
1485
    public function getWorkflowDescriptor($workflowName)
1486
    {
1487
        try {
1488
            return $this->getConfiguration()->getWorkflow($workflowName);
1489
        } catch (FactoryException $e) {
1490
            $errMsg = 'Ошибка при загрузке workflow';
1491
            $this->getLog()->error($errMsg, ['exception' => $e]);
1492
            throw new InternalWorkflowException($errMsg, $e->getCode(), $e);
1493
        }
1494
    }
1495
1496
1497
    /**
1498
     * Executes a special trigger-function using the context of the given workflow instance id.
1499
     * Note that this method is exposed for Quartz trigger jobs, user code should never call it.
1500
     * @param integer $id The workflow instance id
1501
     * @param integer $triggerId The id of the special trigger-function
1502
     * @throws \OldTown\Workflow\Exception\WorkflowException
1503
     * @throws \OldTown\Workflow\Exception\StoreException
1504
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1505
     * @throws \OldTown\Workflow\Exception\FactoryException
1506
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1507
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1508
     */
1509
    public function executeTriggerFunction($id, $triggerId)
1510
    {
1511
        $store = $this->getPersistence();
1512
        $entry = $store->findEntry($id);
1513
1514
        if (null === $entry) {
1515
            $errMsg = sprintf(
1516
                'Ошибка при выполнение тригера # %s для несуществующего экземпляра workflow id# %s',
1517
                $triggerId,
1518
                $id
1519
            );
1520
            $this->getLog()->warning($errMsg);
1521
            return;
1522
        }
1523
1524
        $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
1525
1526
        $ps = $store->getPropertySet($id);
1527
        $transientVars = $this->transientVarsFactory();
1528
1529
        $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), null, $store->findCurrentSteps($id), $ps);
1530
1531
        $this->executeFunction($wf->getTriggerFunction($triggerId), $transientVars, $ps);
1532
    }
1533
1534
    /**
1535
     * @param $id
1536
     * @param $inputs
1537
     *
1538
     * @return array
1539
     * @throws \OldTown\Workflow\Exception\StoreException
1540
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1541
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1542
     * @throws \OldTown\Workflow\Exception\WorkflowException
1543
     * @throws \OldTown\Workflow\Exception\FactoryException
1544
     */
1545
    public function getAvailableActions($id, TransientVarsInterface $inputs = null)
1546
    {
1547
        try {
1548
            $store = $this->getPersistence();
1549
            $entry = $store->findEntry($id);
1550
1551
            if (null === $entry) {
1552
                $errMsg = sprintf(
1553
                    'Не существует экземпляра workflow c id %s',
1554
                    $id
1555
                );
1556
                throw new InvalidArgumentException($errMsg);
1557
            }
1558
1559
            if (WorkflowEntryInterface::ACTIVATED === $entry->getState()) {
1560
                return [];
1561
            }
1562
1563
            $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
1564
1565 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...
1566
                $errMsg = sprintf(
1567
                    'Не существует workflow c именем %s',
1568
                    $entry->getWorkflowName()
1569
                );
1570
                throw new InvalidArgumentException($errMsg);
1571
            }
1572
1573
            $l = [];
1574
            $ps = $store->getPropertySet($id);
1575
1576
            $transientVars = $inputs;
1577
            if (null === $transientVars) {
1578
                $transientVars = $this->transientVarsFactory();
1579
            }
1580
1581
            $currentSteps = $store->findCurrentSteps($id);
1582
1583
            $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), 0, $currentSteps, $ps);
1584
1585
            $globalActions = $wf->getGlobalActions();
1586
1587
            foreach ($globalActions as $action) {
1588
                $restriction = $action->getRestriction();
1589
                $conditions = null;
1590
1591
                $transientVars['actionId'] = $action->getId();
1592
1593
                if (null !== $restriction) {
1594
                    $conditions = $restriction->getConditionsDescriptor();
1595
                }
1596
1597
                $flag = $this->passesConditionsByDescriptor($wf->getGlobalConditions(), $transientVars, $ps, 0) && $this->passesConditionsByDescriptor($conditions, $transientVars, $ps, 0);
1598
                if ($flag) {
1599
                    $l[] = $action->getId();
1600
                }
1601
            }
1602
1603
1604 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...
1605
                $data = $this->getAvailableActionsForStep($wf, $step, $transientVars, $ps);
1606
                foreach ($data as $v) {
1607
                    $l[] = $v;
1608
                }
1609
            }
1610
            return array_unique($l);
1611
        } catch (\Exception $e) {
1612
            $errMsg = 'Ошибка проверки доступных действий';
1613
            $this->getLog()->error($errMsg, [$e]);
1614
        }
1615
1616
        return [];
1617
    }
1618
1619
    /**
1620
     * @param WorkflowDescriptor   $wf
1621
     * @param StepInterface        $step
1622
     * @param TransientVarsInterface                $transientVars
1623
     * @param PropertySetInterface $ps
1624
     *
1625
     * @return array
1626
     * @throws \OldTown\Workflow\Exception\ArgumentNotNumericException
1627
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1628
     * @throws \OldTown\Workflow\Exception\WorkflowException
1629
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1630
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1631
     */
1632
    protected function getAvailableActionsForStep(WorkflowDescriptor $wf, StepInterface $step, TransientVarsInterface $transientVars, PropertySetInterface $ps)
1633
    {
1634
        $l = [];
1635
        $s = $wf->getStep($step->getStepId());
1636
1637
        if (null === $s) {
1638
            $errMsg = sprintf(
1639
                'getAvailableActionsForStep вызван с не существующим id шага %s',
1640
                $step->getStepId()
1641
            );
1642
1643
            $this->getLog()->warning($errMsg);
1644
1645
            return $l;
1646
        }
1647
1648
        $actions  = $s->getActions();
1649
1650
        if (null === $actions || 0  === $actions->count()) {
1651
            return $l;
1652
        }
1653
1654
        foreach ($actions as $action) {
1655
            $restriction = $action->getRestriction();
1656
            $conditions = null;
1657
1658
            $transientVars['actionId'] = $action->getId();
1659
1660
1661
            if (null !== $restriction) {
1662
                $conditions = $restriction->getConditionsDescriptor();
1663
            }
1664
1665
            $f = $this->passesConditionsByDescriptor($wf->getGlobalConditions(), $transientVars, $ps, $s->getId())
1666
                 && $this->passesConditionsByDescriptor($conditions, $transientVars, $ps, $s->getId());
1667
            if ($f) {
1668
                $l[] = $action->getId();
1669
            }
1670
        }
1671
1672
        return $l;
1673
    }
1674
1675
    /**
1676
     * @param ConfigurationInterface $configuration
1677
     *
1678
     * @return $this
1679
     */
1680
    public function setConfiguration(ConfigurationInterface $configuration)
1681
    {
1682
        $this->configuration = $configuration;
1683
1684
        return $this;
1685
    }
1686
1687
    /**
1688
     * Возвращает состояние для текущего экземпляра workflow
1689
     *
1690
     * @param integer $id id экземпляра workflow
1691
     * @return integer id текущего состояния
1692
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1693
     */
1694
    public function getEntryState($id)
1695
    {
1696
        try {
1697
            $store = $this->getPersistence();
1698
1699
            return $store->findEntry($id)->getState();
1700
        } catch (StoreException $e) {
1701
            $errMsg = sprintf(
1702
                'Ошибка при получение состояния экземпляра workflow c id# %s',
1703
                $id
1704
            );
1705
            $this->getLog()->error($errMsg, [$e]);
1706
        }
1707
1708
        return WorkflowEntryInterface::UNKNOWN;
1709
    }
1710
1711
    /**
1712
     * Returns a list of all steps that are completed for the given workflow instance id.
1713
     *
1714
     * @param integer $id The workflow instance id.
1715
     * @return StepInterface[] a List of Steps
1716
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1717
     */
1718
    public function getHistorySteps($id)
1719
    {
1720
        try {
1721
            $store = $this->getPersistence();
1722
1723
            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 1723 which is incompatible with the return type declared by the interface OldTown\Workflow\Workflo...erface::getHistorySteps of type OldTown\Workflow\Spi\StepInterface[].
Loading history...
1724
        } catch (StoreException $e) {
1725
            $errMsg = sprintf(
1726
                'Ошибка при получение истории шагов для экземпляра workflow c id# %s',
1727
                $id
1728
            );
1729
            $this->getLog()->error($errMsg, [$e]);
1730
        }
1731
1732
        return [];
1733
    }
1734
1735
    /**
1736
     * Настройки хранилища
1737
     *
1738
     * @return array
1739
     *
1740
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1741
     */
1742
    public function getPersistenceProperties()
1743
    {
1744
        return $this->getConfiguration()->getPersistenceArgs();
1745
    }
1746
1747
1748
    /**
1749
     * Get the PropertySet for the specified workflow instance id.
1750
     * @param integer $id The workflow instance id.
1751
     * @return PropertySetInterface
1752
     *
1753
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1754
     */
1755 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...
1756
    {
1757
        $ps = null;
1758
1759
        try {
1760
            $ps = $this->getPersistence()->getPropertySet($id);
1761
        } catch (StoreException $e) {
1762
            $errMsg = sprintf(
1763
                'Ошибка при получение PropertySet для экземпляра workflow c id# %s',
1764
                $id
1765
            );
1766
            $this->getLog()->error($errMsg, [$e]);
1767
        }
1768
1769
        return $ps;
1770
    }
1771
1772
    /**
1773
     * @return \String[]
1774
     *
1775
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1776
     */
1777
    public function getWorkflowNames()
1778
    {
1779
        try {
1780
            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...
1781
        } catch (FactoryException $e) {
1782
            $errMsg = 'Ошибка при получение имен workflow';
1783
            $this->getLog()->error($errMsg, [$e]);
1784
        }
1785
1786
        return [];
1787
    }
1788
1789
    /**
1790
     * @param TypeResolverInterface $typeResolver
1791
     *
1792
     * @return $this
1793
     */
1794
    public function setTypeResolver(TypeResolverInterface $typeResolver)
1795
    {
1796
        $this->typeResolver = $typeResolver;
1797
1798
        return $this;
1799
    }
1800
1801
1802
    /**
1803
     * Get a collection (Strings) of currently defined permissions for the specified workflow instance.
1804
     * @param integer $id id the workflow instance id.
1805
     * @param TransientVarsInterface $inputs inputs The inputs to the workflow instance.
1806
     * @return array  A List of permissions specified currently (a permission is a string name).
1807
     *
1808
     */
1809
    public function getSecurityPermissions($id, TransientVarsInterface $inputs = null)
1810
    {
1811
        try {
1812
            $store = $this->getPersistence();
1813
            $entry = $store->findEntry($id);
1814
            $wf = $this->getConfiguration()->getWorkflow($entry->getWorkflowName());
1815
1816
            $ps = $store->getPropertySet($id);
1817
1818
            if (null === $inputs) {
1819
                $inputs = $this->transientVarsFactory();
1820
            }
1821
            $transientVars = $inputs;
1822
1823
            $currentSteps = $store->findCurrentSteps($id);
1824
1825
            try {
1826
                $this->populateTransientMap($entry, $transientVars, $wf->getRegisters(), null, $currentSteps, $ps);
1827
            } catch (\Exception $e) {
1828
                $errMsg = sprintf(
1829
                    'Внутреннея ошибка: %s',
1830
                    $e->getMessage()
1831
                );
1832
                throw new InternalWorkflowException($errMsg, $e->getCode(), $e);
1833
            }
1834
1835
1836
            $s = [];
1837
1838
            foreach ($currentSteps as $step) {
1839
                $stepId = $step->getStepId();
1840
1841
                $xmlStep = $wf->getStep($stepId);
1842
1843
                $securities = $xmlStep->getPermissions();
1844
1845
                foreach ($securities as $security) {
1846
                    $conditionsDescriptor = $security->getRestriction()->getConditionsDescriptor();
1847
                    if (null !== $security->getRestriction() && $this->passesConditionsByDescriptor($conditionsDescriptor, $transientVars, $ps, $xmlStep->getId())) {
1848
                        $s[$security->getName()] = $security->getName();
1849
                    }
1850
                }
1851
            }
1852
1853
            return $s;
1854
        } catch (\Exception $e) {
1855
            $errMsg = sprintf(
1856
                'Ошибка при получение информации о правах доступа для экземпляра workflow c id# %s',
1857
                $id
1858
            );
1859
            $this->getLog()->error($errMsg, [$e]);
1860
        }
1861
1862
        return [];
1863
    }
1864
1865
1866
    /**
1867
     * Get the name of the specified workflow instance.
1868
     *
1869
     * @param integer $id the workflow instance id.
1870
     * @return string
1871
     *
1872
     * @throws \OldTown\Workflow\Exception\StoreException
1873
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1874
     */
1875
    public function getWorkflowName($id)
1876
    {
1877
        try {
1878
            $store = $this->getPersistence();
1879
            $entry = $store->findEntry($id);
1880
1881
            if (null !== $entry) {
1882
                return $entry->getWorkflowName();
1883
            }
1884
        } catch (FactoryException $e) {
1885
            $errMsg = sprintf(
1886
                'Ошибка при получение имен workflow для инстанса с id # %s',
1887
                $id
1888
            );
1889
            $this->getLog()->error($errMsg, [$e]);
1890
        }
1891
1892
        return null;
1893
    }
1894
1895
    /**
1896
     * Удаляет workflow
1897
     *
1898
     * @param string $workflowName
1899
     *
1900
     * @return bool
1901
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1902
     */
1903
    public function removeWorkflowDescriptor($workflowName)
1904
    {
1905
        return $this->getConfiguration()->removeWorkflow($workflowName);
1906
    }
1907
1908
    /**
1909
     * @param                    $workflowName
1910
     * @param WorkflowDescriptor $descriptor
1911
     * @param                    $replace
1912
     *
1913
     * @return bool
1914
     *
1915
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1916
     */
1917
    public function saveWorkflowDescriptor($workflowName, WorkflowDescriptor $descriptor, $replace)
1918
    {
1919
        $success = $this->getConfiguration()->saveWorkflow($workflowName, $descriptor, $replace);
1920
1921
        return $success;
1922
    }
1923
1924
1925
    /**
1926
     * Query the workflow store for matching instances
1927
     *
1928
     * @param WorkflowExpressionQuery $query
1929
     *
1930
     * @return array
1931
     *
1932
     * @throws \OldTown\Workflow\Exception\WorkflowException
1933
     * @throws \OldTown\Workflow\Exception\StoreException
1934
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1935
1936
     */
1937
    public function query(WorkflowExpressionQuery $query)
1938
    {
1939
        return $this->getPersistence()->query($query);
1940
    }
1941
1942
    /**
1943
     * @return string
1944
     */
1945
    public function getDefaultTypeResolverClass()
1946
    {
1947
        return $this->defaultTypeResolverClass;
1948
    }
1949
1950
    /**
1951
     * @param string $defaultTypeResolverClass
1952
     *
1953
     * @return $this
1954
     */
1955
    public function setDefaultTypeResolverClass($defaultTypeResolverClass)
1956
    {
1957
        $this->defaultTypeResolverClass = (string)$defaultTypeResolverClass;
1958
1959
        return $this;
1960
    }
1961
1962
1963
    /**
1964
     * @param ConditionsDescriptor $descriptor
1965
     * @param TransientVarsInterface $transientVars
1966
     * @param PropertySetInterface $ps
1967
     * @param                      $currentStepId
1968
     *
1969
     * @return bool
1970
     *
1971
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1972
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1973
     * @throws \OldTown\Workflow\Exception\WorkflowException
1974
     * @throws \OldTown\Workflow\Exception\InvalidActionException
1975
     */
1976
    protected function passesConditionsByDescriptor(ConditionsDescriptor $descriptor = null, TransientVarsInterface $transientVars, PropertySetInterface $ps, $currentStepId)
1977
    {
1978
        if (null === $descriptor) {
1979
            return true;
1980
        }
1981
1982
        $type = $descriptor->getType();
1983
        $conditions = $descriptor->getConditions();
1984
        $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 1983 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...
1985
1986
        return $passesConditions;
1987
    }
1988
1989
    /**
1990
     * @param string $conditionType
1991
     * @param SplObjectStorage $conditions
1992
     * @param TransientVarsInterface $transientVars
1993
     * @param PropertySetInterface $ps
1994
     * @param integer $currentStepId
1995
     *
1996
     * @return bool
1997
     * @throws \OldTown\Workflow\Exception\InvalidArgumentException
1998
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
1999
     * @throws \OldTown\Workflow\Exception\WorkflowException
2000
     * @throws \OldTown\Workflow\Exception\InvalidActionException
2001
     */
2002
    protected function passesConditionsWithType($conditionType, SplObjectStorage $conditions = null, TransientVarsInterface $transientVars, PropertySetInterface $ps, $currentStepId)
2003
    {
2004
        if (null === $conditions) {
2005
            return true;
2006
        }
2007
2008
        if (0 === $conditions->count()) {
2009
            return true;
2010
        }
2011
2012
        $and = strtoupper($conditionType) === 'AND';
2013
        $or = !$and;
2014
2015
        foreach ($conditions as $descriptor) {
2016
            if ($descriptor instanceof ConditionsDescriptor) {
2017
                $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...
2018
            } else {
2019
                $result = $this->passesCondition($descriptor, $transientVars, $ps, $currentStepId);
2020
            }
2021
2022
            if ($and && !$result) {
2023
                return false;
2024
            } elseif ($or && $result) {
2025
                return true;
2026
            }
2027
        }
2028
2029
        if ($and) {
2030
            return true;
2031
        }
2032
2033
        return false;
2034
    }
2035
2036
    /**
2037
     * @param ConditionDescriptor $conditionDesc
2038
     * @param TransientVarsInterface $transientVars
2039
     * @param PropertySetInterface $ps
2040
     * @param integer $currentStepId
2041
     *
2042
     * @return boolean
2043
     *
2044
     * @throws \OldTown\Workflow\Exception\InternalWorkflowException
2045
     * @throws \OldTown\Workflow\Exception\WorkflowException
2046
     */
2047
    protected function passesCondition(ConditionDescriptor $conditionDesc, TransientVarsInterface $transientVars, PropertySetInterface $ps, $currentStepId)
2048
    {
2049
        $type = $conditionDesc->getType();
2050
2051
        $argsOriginal = $conditionDesc->getArgs();
2052
2053
2054
        $args = [];
2055 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...
2056
            $translateValue = $this->getConfiguration()->getVariableResolver()->translateVariables($value, $transientVars, $ps);
2057
            $args[$key] = $translateValue;
2058
        }
2059
2060
        if (-1 !== $currentStepId) {
2061
            $stepId = array_key_exists('stepId', $args) ? (integer)$args['stepId'] : null;
2062
2063
            if (null !== $stepId && -1 === $stepId) {
2064
                $args['stepId'] = $currentStepId;
2065
            }
2066
        }
2067
2068
        $condition = $this->getResolver()->getCondition($type, $args);
2069
2070
        if (null === $condition) {
2071
            $this->context->setRollbackOnly();
2072
            $errMsg = 'Огибка при загрузки условия';
2073
            throw new WorkflowException($errMsg);
2074
        }
2075
2076
        try {
2077
            $passed = $condition->passesCondition($transientVars, $args, $ps);
2078
2079
            if ($conditionDesc->isNegate()) {
2080
                $passed = !$passed;
2081
            }
2082
        } catch (\Exception $e) {
2083
            $this->context->setRollbackOnly();
2084
2085
            $errMsg = sprintf(
2086
                'Ошбика при выполнение условия %s',
2087
                get_class($condition)
2088
            );
2089
2090
            throw new WorkflowException($errMsg, $e->getCode(), $e);
2091
        }
2092
2093
        return $passed;
2094
    }
2095
}
2096