pluf/workflow

View on GitHub
src/Imp/StateImpl.php

Summary

Maintainability
F
4 days
Test Coverage
<?php
namespace Pluf\Workflow\Imp;

use Pluf\Workflow\HistoryType;
use Pluf\Workflow\ImmutableState;
use Pluf\Workflow\ImmutableTransition;
use Pluf\Workflow\MutableState;
use Pluf\Workflow\MutableTransition;
use Pluf\Workflow\StateCompositeType;
use Pluf\Workflow\StateContext;
use Pluf\Workflow\StateMachineDataReader;
use Pluf\Workflow\Visitor;
use Pluf\Workflow\Conditions\Never;
use Pluf\Workflow\Exceptions\IllegalArgumentException;
use Pluf\Workflow\Exceptions\IllegalStateException;
use Pluf\Workflow\Exceptions\UnsupportedOperationException;

class StateImpl implements MutableState
{

    // private static final Logger logger = LoggerFactory.getLogger(StateImpl.class);
    protected $stateId;

    protected array $entryActions = [];

    protected array $exitActions = [];

    private array $transitions = [];

    private array $acceptableEvents = [];

    /**
     * The super-state of this state.
     * Null for states with <code>level</code> equal to 1.
     */
    private ?ImmutableState $parentState = null;

    private array $childStates = [];

    /**
     * The initial child state of this state.
     */
    private ?ImmutableState $childInitialState = null;

    /**
     * The HistoryType of this state.
     */
    private string $historyType = HistoryType::NONE;

    /**
     * The level of this state within the state hierarchy [1..maxLevel].
     */
    private int $level = 0;

    /**
     * Whether the state is a final state
     */
    private bool $finalState = false;

    /**
     * Composite type of child states
     */
    private $compositeType = StateCompositeType::SEQUENTIAL;

    /**
     * Creates new instance of the state
     *
     * @param mixed $stateId
     */
    public function __construct($stateId)
    {
        $this->stateId = $stateId;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getStateId()
     */
    public function getStateId()
    {
        return $this->stateId;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getEntryActions()
     */
    public function getEntryActions(): array
    {
        return $this->entryActions;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getExitActions()
     */
    public function getExitActions(): array
    {
        return $this->exitActions;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getAllTransitions()
     */
    public function getAllTransitions(): array
    {
        $result = [];
        foreach ($this->transitions as $list) {
            $result = array_merge($result, $list);
        }
        return $result;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getTransitions()
     */
    public function getTransitions($event): array
    {
        if (! array_key_exists($event, $this->transitions)) {
            return [];
        }
        return $this->transitions[$event];
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getAcceptableEvents()
     */
    public function getAcceptableEvents(): array
    {
        // if($this->acceptableEvents==null) {
        // $events = [];
        // events.addAll(getTransitions().keySet());
        // acceptableEvents = Collections.unmodifiableSet(events);
        // }
        return $this->acceptableEvents;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::prioritizeTransitions()
     */
    public function prioritizeTransitions(): void
    {
        foreach ($this->transitions as $trans) {
            usort($trans, function ($a, $b) {
                return $a->priority - $b->priority;
            });
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::entry()
     */
    public function entry(StateContext $stateContext): void
    {
        $stateContext->getExecutor()->begin("STATE_ENTRY__" . $this->stateId);
        $actions = $this->entryActions;
        $event = $stateContext->getEvent();
        $context = $stateContext->getContext();
        $stateMachine = $stateContext->getStateMachine();

        foreach ($actions as $entryAction) {
            $stateContext->getExecutor()->defer($entryAction, null, $this->stateId, $event, $context, $stateMachine);
        }

        if ($this->isParallelState()) {
            // When a parallel state group is entered, all its child states will be simultaneously entered.
            $states = $this->childStates;
            foreach ($states as $parallelState) {
                $parallelState->entry($stateContext);
                $subState = $parallelState->enterByHistory($stateContext);
                $stateContext->getStateMachineData()
                    ->write()
                    ->setSubStateFor($this->stateId, $subState->getStateId());
            }
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::exit()
     */
    public function exit(StateContext $stateContext): void
    {
        if ($this->isParallelState()) {
            $subStates = $this->getSubStatesOn($this, $stateContext->getStateMachineData()
                ->read());
            foreach ($subStates as $subState) {
                if (! $subState->isFinalState()) {
                    $subState->exit($stateContext);
                }
                if ($subState->getParentState() != $this)
                    $subState->getParentState()->exit($stateContext);
            }
            $stateContext->getStateMachineData()
                ->write()
                ->removeSubStatesOn($this->getStateId());
        }

        if ($this->isFinalState()) {
            return;
        }

        $stateContext->getExecutor()->begin("STATE_EXIT__" . $this->stateId);
        foreach ($this->getExitActions() as $exitAction) {
            $stateContext->getExecutor()->defer($exitAction, $this->stateId, null, $stateContext->getEvent(), $stateContext->getContext(), $stateContext->getStateMachine());
        }

        if ($this->getParentState() != null) {
            // update historical state
            $parent = $this->getParentState();
            $shouldUpdateHistoricalState = $parent->getHistoryType() != HistoryType::NONE;
            if (! $shouldUpdateHistoricalState) {
                $iter = $parent->getParentState();
                while ($iter != null) {
                    if ($iter->getHistoryType() == HistoryType::DEEP) {
                        $shouldUpdateHistoricalState = true;
                        break;
                    }
                    $iter = $iter->getParentState();
                }
            }
            if ($shouldUpdateHistoricalState) {
                $stateContext->getStateMachineData()
                    ->write()
                    ->lastActiveChildStateFor($this->getParentState()
                    ->getStateId(), $this->stateId);
            }

            if ($this->getParentState()->isRegion()) {
                $grandParentId = $this->getParentState()
                    ->getParentState()
                    ->getStateId();
                $stateContext->getStateMachineData()
                    ->write()
                    ->removeSubState($grandParentId, $this->stateId);
            }
        }
        // logger.debug("State \""+getStateId()+"\" exit.");
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getParentState()
     */
    public function getParentState(): ?ImmutableState
    {
        return $this->parentState;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getChildStates()
     */
    public function getChildStates(): array
    {
        return $this->childStates;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::hasChildStates()
     */
    public function hasChildStates(): bool
    {
        return ! empty($this->childStates);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::setParentState()
     */
    public function setParentState(MutableState $parent): void
    {
        if ($this == $parent) {
            throw new IllegalArgumentException("parent state cannot be state itself.");
        }
        if ($this->parentState == null) {
            $this->parentState = parent;
            $this->setLevel($this->parentState != null ? $this->parentState->getLevel() + 1 : 1);
        } else {
            throw new UnsupportedOperationException("Cannot change state parent.");
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getInitialState()
     */
    public function getInitialState(): ImmutableState
    {
        return $this->childInitialState;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::setInitialState()
     */
    public function setInitialState(MutableState $childInitialState): void
    {
        if ($this->isParallelState()) {
            // logger . warn("Ignoring attempt to set initial state of parallel state group.");
            return;
        }
        if ($this->childInitialState == null) {
            $this->childInitialState = $childInitialState;
        } else {
            throw new UnsupportedOperationException("Cannot change child initial state.");
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::enterByHistory()
     */
    public function enterByHistory($stateContext): ImmutableState
    {
        if ($this->finalState || $this->isParallelState()) // no historical info
            return this;

        $result = null;
        switch ($this->historyType) {
            case HistoryType::NONE:
                $result = $this->enterHistoryNone($stateContext);
                break;
            case HistoryType::SHALLOW:
                $result = $this->enterHistoryShallow($stateContext);
                break;
            case HistoryType::DEEP:
                $result = $this->enterHistoryDeep($stateContext);
                break;
            default:
                throw new IllegalArgumentException("Unknown HistoryType : " . $this->historyType);
        }
        return $result;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::enterDeep()
     */
    public function enterDeep($stateContext): ImmutableState
    {
        $this->entry($stateContext);
        $lastActiveState = $this->getLastActiveChildStateOf($this, $stateContext->getStateMachineData()
            ->read());
        return $lastActiveState == null ? this : $lastActiveState->enterDeep($stateContext);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::enterShallow()
     */
    public function enterShallow($stateContext): ImmutableState
    {
        $this->entry($stateContext);
        return $this->childInitialState != null ? $this->childInitialState->enterShallow($stateContext) : $this;
    }

    /**
     * Enters this instance with history type = shallow.
     *
     * @param
     *            stateContext
     *            state context
     * @return ImmutableState the entered state
     */
    private function enterHistoryShallow($stateContext): ImmutableState
    {
        $lastActiveState = $this->getLastActiveChildStateOf($this, $stateContext->getStateMachineData()
            ->read());
        return $lastActiveState != null ? $lastActiveState->enterShallow($stateContext) : $this;
    }

    /**
     * Enters with history type = none.
     *
     * @param
     *            stateContext
     *            state context
     * @return ImmutableState the entered state.
     */
    private function enterHistoryNone($stateContext): ImmutableState
    {
        return $this->childInitialState != null ? $this->childInitialState->enterShallow($stateContext) : $this;
    }

    /**
     * Enters this instance with history type = deep.
     *
     * @param
     *            stateContext
     *            the state context.
     * @return ImmutableState the state
     */
    private function enterHistoryDeep($stateContext): ImmutableState
    {
        $lastActiveState = $this->getLastActiveChildStateOf($this, $stateContext->getStateMachineData()
            ->read());
        return $lastActiveState != null ? $lastActiveState->enterDeep($stateContext) : $this;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::addTransitionOn()
     */
    public function addTransitionOn($event): MutableTransition
    {
        // create transition
        $newTransition = FSM::newTransition();
        $newTransition->setSourceState($this);
        $newTransition->setEvent($event);
        // add to list of transition
        if (! array_key_exists($event, $this->transitions)) {
            $this->transitions[$event] = [];
        }
        $this->transitions[$event][] = $newTransition;

        // return the result
        return $newTransition;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::addEntryAction()
     */
    public function addEntryAction($newAction): void
    {
        array_push($this->entryActions, $newAction);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::addEntryActions()
     */
    public function addEntryActions(array $newActions): void
    {
        $this->entryActions = array_merge($this->entryActions, $newActions);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::addExitAction()
     */
    public function addExitAction($newAction): void
    {
        array_push($this->exitActions, $newAction);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::addExitActions()
     */
    public function addExitActions(array $newActions): void
    {
        $this->exitActions = array_merge($this->exitActions, $newActions);
    }

    private function isParentOf(ImmutableState $state): bool
    {
        $parent = $state->getParentState();
        while (isset($parent)) {
            if ($parent == $this) {
                return true;
            }
            $parent = $parent->getParentState();
        }
        return false;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::internalFire()
     */
    public function internalFire($stateContext): void
    {
        $currentTransitionResult = $stateContext->getResult();
        if ($this->isParallelState()) {
            /*
             * The parallelism in the State Machine framework follows an interleaved semantics.
             * All parallel operations will be executed in a single, atomic step of the event
             * processing, so no event can interrupt the parallel operations. However, events
             * will still be processed sequentially, since the machine itself is single threaded.
             *
             * The child states execute in parallel in the sense that any event that is processed
             * is processed in each child state independently, and each child state may take a different
             * transition in response to the event. (Similarly, one child state may take a transition
             * in response to an event, while another child ignores it.)
             */
            $parallelStates = $this->getSubStatesOn($this, $stateContext->getStateMachineData()
                ->read());
            foreach ($parallelStates as $parallelState) {
                if ($parallelState->isFinalState()) {
                    continue;
                }
                // context isolation as entering a new region
                $subTransitionResult = FSM::newResult(false, $parallelState, $currentTransitionResult);
                $subStateContext = FSM::newStateContext($stateContext->getStateMachine(), $stateContext->getStateMachineData(), $parallelState, $stateContext->getEvent(), $stateContext->getContext(), $subTransitionResult, $stateContext->getExecutor());
                $parallelState->internalFire($subStateContext);
                if ($subTransitionResult->isDeclined()) {
                    continue;
                }

                if (! $this->isParentOf($subTransitionResult->getTargetState())) {
                    // child state transition exit the parallel state
                    $currentTransitionResult->setTargetState($subTransitionResult->getTargetState());
                    return;
                }
                $stateContext->getStateMachineData()
                    ->write()
                    ->subStateFor($this->getStateId(), $subTransitionResult->getTargetState()
                    ->getStateId());
                if ($subTransitionResult->getTargetState()->isFinalState()) {
                    $parentState = $subTransitionResult->getTargetState()->getParentState();
                    $grandParentState = $parentState->getParentState();
                    // When all of the children reach final states, the parallel state itself is considered
                    // to be in a final state, and a completion event is generated.
                    if ($grandParentState != null && $grandParentState->isParallelState()) {
                        $allReachedFinal = true;
                        $nsubstates = $this->getSubStatesOn($grandParentState, $stateContext->getStateMachineData()
                            ->read());
                        foreach ($nsubstates as $subState) {
                            if (! $subState->isFinalState()) {
                                $allReachedFinal = false;
                                break;
                            }
                        }
                        if ($allReachedFinal) {
                            $stateMachine = $stateContext->getStateMachine();
                            $stateMachineImpl = $stateMachine;
                            $stateMachine->fireImmediate($stateMachineImpl->getFinishEvent(), $stateContext->getContext());
                            return;
                        }
                    }
                }
            }
        }

        $transitions = $this->getTransitions($stateContext->event);
        foreach ($transitions as $transition) {
            $transition->internalFire($stateContext);
            if ($currentTransitionResult->isAccepted()) {
                $targetState = $currentTransitionResult->getTargetState();
                if ($targetState->isFinalState() && ! $targetState->isRootState()) {
                    // TODO-hhe: fire event to notify listeners???
                    $parentState = $targetState->getParentState();
                    $abstractStateMachine = $stateContext->getStateMachine();
                    $finishContext = FSM::newStateContext($stateContext->getStateMachine(), $stateContext->getStateMachineData(), $parentState, $abstractStateMachine->getFinishEvent(), $stateContext->getContext(), $currentTransitionResult, $stateContext->getExecutor());
                    $parentState->internalFire($finishContext);
                    // if(!parentState.isRegion()) {
                    // currentTransitionResult.setTargetState(parentState);
                    // StateMachine<T, S, E, C> stateMachine = stateContext.getStateMachine();
                    // AbstractStateMachine<T, S, E, C> stateMachineImpl = (AbstractStateMachine<T, S, E, C>)
                    // stateContext.getStateMachine();
                    // stateMachine.fireImmediate(stateMachineImpl.getFinishEvent(), stateContext.getContext());
                    // }
                }
                return;
            }
        }

        // fire to super state
        if ($currentTransitionResult->isDeclined() && $this->getParentState() != null && ! $this->getParentState()->isRegion() && ! $this->getParentState()->isParallelState()) {
            // logger.debug("Internal notify the same event to parent state");
            $this->getParentState()->internalFire($stateContext);
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::isRootState()
     */
    public function isRootState(): bool
    {
        return ! isset($this->parentState);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::isFinalState()
     */
    public function isFinalState(): bool
    {
        return $this->finalState;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::setFinal()
     */
    public function setFinal(bool $isFinal): void
    {
        $this->finalState = $isFinal;
    }

    /**
     *
     * @param Visitor $visitor
     */
    public function accept(Visitor $visitor): void
    {
        $visitor->visitOnEntry($this);
        $list = $this->getAllTransitions();
        foreach ($list as $transition) {
            $transition->accept($visitor);
        }
        if ($this->childStates != null) {
            foreach ($this->childStates as $childState) {
                $childState->accept($visitor);
            }
        }
        $visitor->visitOnExit($this);
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getLevel()
     */
    public function getLevel(): int
    {
        return $this->level;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::setLevel()
     */
    public function setLevel(int $level): void
    {
        $this->level = $level;
        foreach ($this->childStates as $state) {
            $state->setLevel($this->level + 1);
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::addChildState()
     */
    public function addChildState(MutableState $childState): void
    {
        if (! isset($childState)) {
            return;
        }
        if (indexOf($childState, $this->childStates) < 0) {
            array_push($this->childStates, $childState);
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getHistoryType()
     */
    public function getHistoryType(): string
    {
        return $this->historyType;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::setHistoryType()
     */
    public function setHistoryType(string $historyType): void
    {
        $this->historyType = $historyType;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getCompositeType()
     */
    public function getCompositeType(): string
    {
        return $this->compositeType;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\MutableState::setCompositeType()
     */
    public function setCompositeType(string $compositeType): void
    {
        $this->compositeType = $compositeType;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::isParallelState()
     */
    public function isParallelState(): bool
    {
        return $this->compositeType == StateCompositeType::PARALLEL;
    }

    public function __toString()
    {
        return '' . $this->stateId;
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::isRegion()
     */
    public function isRegion(): bool
    {
        return $this->parentState != null && $this->parentState->isParallelState();
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::verify()
     */
    public function verify(): void
    {
        if ($this->isFinalState()) {
            if ($this->isParallelState()) {
                throw new IllegalStateException("Final state cannot be parallel state.");
            } else if ($this->hasChildStates()) {
                throw new IllegalStateException("Final state cannot have child states.");
            }
        }

        // make sure that every event can only trigger one transition happen at one time
        $allTransitions = $this->getAllTransitions();
        foreach ($allTransitions as $t) {
            $t->verify();
            $conflictTransition = $this->checkConflictTransitions($t, $allTransitions);
            if ($conflictTransition != null) {
                throw new \RuntimeException("Transition " . $t . " is conflicted with " . $conflictTransition . ".");
            }
        }
    }

    public function checkConflictTransitions(ImmutableTransition $target, array $allTransitions): ?ImmutableTransition
    {
        foreach ($allTransitions as $t) {
            if ($target == $t || $t->getCondition()::class == Never::class/* Conditions::Never::class */) {
                continue;
            }
            if ($t->isMatch($target->getSourceState()
                ->getStateId(), $target->getTargetState()
                ->getStateId(), $target->getEvent(), $target->getPriority())) {
                if ($t->getCondition()->getClass() == 'Always'/* Conditions:: Always::class*/)
                    return target;
                if ($target->getCondition()->getClass() == 'Always'/* Conditions. Always::class*/)
                    return target;
                if ($t->getCondition()
                    ->name()
                    ->equals($target->getCondition()
                    ->name()))
                    return $target;
            }
        }
        return null;
    }

    private function getSubStatesOn(ImmutableState $parentState, StateMachineDataReader $read): array
    {
        $subStates = [];
        foreach ($read->subStatesOn($parentState->getStateId()) as $stateId) {
            $subStates->add($read->rawStateFrom($stateId));
        }
        return $subStates;
    }

    private function getLastActiveChildStateOf(ImmutableState $parentState, StateMachineDataReader $read): ImmutableState
    {
        $childStateId = $read->lastActiveChildStateOf($parentState->getStateId());
        if ($childStateId != null) {
            return $read->rawStateFrom($childStateId);
        } else {
            return $parentState->getInitialState();
        }
    }

    protected function getKey($stateMachine): string
    {
        return $stateMachine->getIdentifier() . '@' . $this->getPath();
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::getPath()
     */
    public function getPath(): string
    {
        $currentId = $this->stateId; // RuntimeException
        if (isset($this->parentState)) {
            return $currentId;
        } else {
            return $this->parentState->getPath() . "/" . $currentId;
        }
    }

    /**
     *
     * {@inheritdoc}
     * @see \Pluf\Workflow\ImmutableState::isChildStateOf()
     */
    public function isChildStateOf(ImmutableState $input): bool
    {
        $curr = $this;
        while ($curr->getLevel() > $input->getLevel()) {
            $curr = $curr->getParentState();
        }
        return $this != $input && $curr == $input;
    }
}