Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
77.78% covered (warning)
77.78%
14 / 18
CRAP
97.44% covered (success)
97.44%
152 / 156
Workflow
0.00% covered (danger)
0.00%
0 / 1
77.78% covered (warning)
77.78%
14 / 18
64
97.44% covered (success)
97.44%
152 / 156
 __construct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 getMarking
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
18 / 18
 can
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
9 / 9
 buildTransitionBlockerList
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
14 / 14
 apply
0.00% covered (danger)
0.00%
0 / 1
9
96.67% covered (success)
96.67%
29 / 30
 getEnabledTransitions
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 getName
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getDefinition
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getMarkingStore
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getMetadataStore
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 buildTransitionBlockerListForTransition
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
10 / 10
 guardTransition
0.00% covered (danger)
0.00%
0 / 1
3.03
85.71% covered (warning)
85.71%
6 / 7
 leave
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 transition
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
 enter
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 entered
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
9 / 9
 completed
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 announce
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
8 / 8
<?php
/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace Symfony\Component\Workflow;
use Symfony\Component\Workflow\Event\AnnounceEvent;
use Symfony\Component\Workflow\Event\CompletedEvent;
use Symfony\Component\Workflow\Event\EnteredEvent;
use Symfony\Component\Workflow\Event\EnterEvent;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Event\LeaveEvent;
use Symfony\Component\Workflow\Event\TransitionEvent;
use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
use Symfony\Component\Workflow\Exception\UndefinedTransitionException;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore;
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
 * @author Fabien Potencier <fabien@symfony.com>
 * @author GrĂ©goire Pineau <lyrixx@lyrixx.info>
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class Workflow implements WorkflowInterface
{
    private $definition;
    private $markingStore;
    private $dispatcher;
    private $name;
    public function __construct(Definition $definition, MarkingStoreInterface $markingStore = null, EventDispatcherInterface $dispatcher = null, string $name = 'unnamed')
    {
        $this->definition = $definition;
        $this->markingStore = $markingStore ?: new MethodMarkingStore();
        $this->dispatcher = $dispatcher;
        $this->name = $name;
    }
    /**
     * {@inheritdoc}
     */
    public function getMarking(object $subject)
    {
        $marking = $this->markingStore->getMarking($subject);
        if (!$marking instanceof Marking) {
            throw new LogicException(sprintf('The value returned by the MarkingStore is not an instance of "%s" for workflow "%s".', Marking::class, $this->name));
        }
        // check if the subject is already in the workflow
        if (!$marking->getPlaces()) {
            if (!$this->definition->getInitialPlaces()) {
                throw new LogicException(sprintf('The Marking is empty and there is no initial place for workflow "%s".', $this->name));
            }
            foreach ($this->definition->getInitialPlaces() as $place) {
                $marking->mark($place);
            }
            // update the subject with the new marking
            $this->markingStore->setMarking($subject, $marking);
            $this->entered($subject, null, $marking);
        }
        // check that the subject has a known place
        $places = $this->definition->getPlaces();
        foreach ($marking->getPlaces() as $placeName => $nbToken) {
            if (!isset($places[$placeName])) {
                $message = sprintf('Place "%s" is not valid for workflow "%s".', $placeName, $this->name);
                if (!$places) {
                    $message .= ' It seems you forgot to add places to the current workflow.';
                }
                throw new LogicException($message);
            }
        }
        return $marking;
    }
    /**
     * {@inheritdoc}
     */
    public function can(object $subject, string $transitionName)
    {
        $transitions = $this->definition->getTransitions();
        $marking = $this->getMarking($subject);
        foreach ($transitions as $transition) {
            if ($transition->getName() !== $transitionName) {
                continue;
            }
            $transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
            if ($transitionBlockerList->isEmpty()) {
                return true;
            }
        }
        return false;
    }
    /**
     * {@inheritdoc}
     */
    public function buildTransitionBlockerList(object $subject, string $transitionName): TransitionBlockerList
    {
        $transitions = $this->definition->getTransitions();
        $marking = $this->getMarking($subject);
        $transitionBlockerList = null;
        foreach ($transitions as $transition) {
            if ($transition->getName() !== $transitionName) {
                continue;
            }
            $transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
            if ($transitionBlockerList->isEmpty()) {
                return $transitionBlockerList;
            }
            // We prefer to return transitions blocker by something else than
            // marking. Because it means the marking was OK. Transitions are
            // deterministic: it's not possible to have many transitions enabled
            // at the same time that match the same marking with the same name
            if (!$transitionBlockerList->has(TransitionBlocker::BLOCKED_BY_MARKING)) {
                return $transitionBlockerList;
            }
        }
        if (!$transitionBlockerList) {
            throw new UndefinedTransitionException($subject, $transitionName, $this);
        }
        return $transitionBlockerList;
    }
    /**
     * {@inheritdoc}
     */
    public function apply(object $subject, string $transitionName, array $context = [])
    {
        $marking = $this->getMarking($subject);
        $transitionExist = false;
        $approvedTransitions = [];
        $bestTransitionBlockerList = null;
        foreach ($this->definition->getTransitions() as $transition) {
            if ($transition->getName() !== $transitionName) {
                continue;
            }
            $transitionExist = true;
            $tmpTransitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
            if ($tmpTransitionBlockerList->isEmpty()) {
                $approvedTransitions[] = $transition;
                continue;
            }
            if (!$bestTransitionBlockerList) {
                $bestTransitionBlockerList = $tmpTransitionBlockerList;
                continue;
            }
            // We prefer to return transitions blocker by something else than
            // marking. Because it means the marking was OK. Transitions are
            // deterministic: it's not possible to have many transitions enabled
            // at the same time that match the same marking with the same name
            if (!$tmpTransitionBlockerList->has(TransitionBlocker::BLOCKED_BY_MARKING)) {
                $bestTransitionBlockerList = $tmpTransitionBlockerList;
            }
        }
        if (!$transitionExist) {
            throw new UndefinedTransitionException($subject, $transitionName, $this);
        }
        if (!$approvedTransitions) {
            throw new NotEnabledTransitionException($subject, $transitionName, $this, $bestTransitionBlockerList);
        }
        foreach ($approvedTransitions as $transition) {
            $this->leave($subject, $transition, $marking);
            $context = $this->transition($subject, $transition, $marking, $context);
            $this->enter($subject, $transition, $marking);
            $this->markingStore->setMarking($subject, $marking, $context);
            $this->entered($subject, $transition, $marking);
            $this->completed($subject, $transition, $marking);
            $this->announce($subject, $transition, $marking);
        }
        return $marking;
    }
    /**
     * {@inheritdoc}
     */
    public function getEnabledTransitions(object $subject)
    {
        $enabledTransitions = [];
        $marking = $this->getMarking($subject);
        foreach ($this->definition->getTransitions() as $transition) {
            $transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
            if ($transitionBlockerList->isEmpty()) {
                $enabledTransitions[] = $transition;
            }
        }
        return $enabledTransitions;
    }
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return $this->name;
    }
    /**
     * {@inheritdoc}
     */
    public function getDefinition()
    {
        return $this->definition;
    }
    /**
     * {@inheritdoc}
     */
    public function getMarkingStore()
    {
        return $this->markingStore;
    }
    /**
     * {@inheritdoc}
     */
    public function getMetadataStore(): MetadataStoreInterface
    {
        return $this->definition->getMetadataStore();
    }
    private function buildTransitionBlockerListForTransition(object $subject, Marking $marking, Transition $transition): TransitionBlockerList
    {
        foreach ($transition->getFroms() as $place) {
            if (!$marking->has($place)) {
                return new TransitionBlockerList([
                    TransitionBlocker::createBlockedByMarking($marking),
                ]);
            }
        }
        if (null === $this->dispatcher) {
            return new TransitionBlockerList();
        }
        $event = $this->guardTransition($subject, $marking, $transition);
        if ($event->isBlocked()) {
            return $event->getTransitionBlockerList();
        }
        return new TransitionBlockerList();
    }
    private function guardTransition(object $subject, Marking $marking, Transition $transition): ?GuardEvent
    {
        if (null === $this->dispatcher) {
            return null;
        }
        $event = new GuardEvent($subject, $marking, $transition, $this);
        $this->dispatcher->dispatch($event, WorkflowEvents::GUARD);
        $this->dispatcher->dispatch($event, sprintf('workflow.%s.guard', $this->name));
        $this->dispatcher->dispatch($event, sprintf('workflow.%s.guard.%s', $this->name, $transition->getName()));
        return $event;
    }
    private function leave(object $subject, Transition $transition, Marking $marking): void
    {
        $places = $transition->getFroms();
        if (null !== $this->dispatcher) {
            $event = new LeaveEvent($subject, $marking, $transition, $this);
            $this->dispatcher->dispatch($event, WorkflowEvents::LEAVE);
            $this->dispatcher->dispatch($event, sprintf('workflow.%s.leave', $this->name));
            foreach ($places as $place) {
                $this->dispatcher->dispatch($event, sprintf('workflow.%s.leave.%s', $this->name, $place));
            }
        }
        foreach ($places as $place) {
            $marking->unmark($place);
        }
    }
    private function transition(object $subject, Transition $transition, Marking $marking, array $context): array
    {
        if (null === $this->dispatcher) {
            return $context;
        }
        $event = new TransitionEvent($subject, $marking, $transition, $this);
        $event->setContext($context);
        $this->dispatcher->dispatch($event, WorkflowEvents::TRANSITION);
        $this->dispatcher->dispatch($event, sprintf('workflow.%s.transition', $this->name));
        $this->dispatcher->dispatch($event, sprintf('workflow.%s.transition.%s', $this->name, $transition->getName()));
        return $event->getContext();
    }
    private function enter(object $subject, Transition $transition, Marking $marking): void
    {
        $places = $transition->getTos();
        if (null !== $this->dispatcher) {
            $event = new EnterEvent($subject, $marking, $transition, $this);
            $this->dispatcher->dispatch($event, WorkflowEvents::ENTER);
            $this->dispatcher->dispatch($event, sprintf('workflow.%s.enter', $this->name));
            foreach ($places as $place) {
                $this->dispatcher->dispatch($event, sprintf('workflow.%s.enter.%s', $this->name, $place));
            }
        }
        foreach ($places as $place) {
            $marking->mark($place);
        }
    }
    private function entered(object $subject, ?Transition $transition, Marking $marking): void
    {
        if (null === $this->dispatcher) {
            return;
        }
        $event = new EnteredEvent($subject, $marking, $transition, $this);
        $this->dispatcher->dispatch($event, WorkflowEvents::ENTERED);
        $this->dispatcher->dispatch($event, sprintf('workflow.%s.entered', $this->name));
        if ($transition) {
            foreach ($transition->getTos() as $place) {
                $this->dispatcher->dispatch($event, sprintf('workflow.%s.entered.%s', $this->name, $place));
            }
        }
    }
    private function completed(object $subject, Transition $transition, Marking $marking): void
    {
        if (null === $this->dispatcher) {
            return;
        }
        $event = new CompletedEvent($subject, $marking, $transition, $this);
        $this->dispatcher->dispatch($event, WorkflowEvents::COMPLETED);
        $this->dispatcher->dispatch($event, sprintf('workflow.%s.completed', $this->name));
        $this->dispatcher->dispatch($event, sprintf('workflow.%s.completed.%s', $this->name, $transition->getName()));
    }
    private function announce(object $subject, Transition $initialTransition, Marking $marking): void
    {
        if (null === $this->dispatcher) {
            return;
        }
        $event = new AnnounceEvent($subject, $marking, $initialTransition, $this);
        $this->dispatcher->dispatch($event, WorkflowEvents::ANNOUNCE);
        $this->dispatcher->dispatch($event, sprintf('workflow.%s.announce', $this->name));
        foreach ($this->getEnabledTransitions($subject) as $transition) {
            $this->dispatcher->dispatch($event, sprintf('workflow.%s.announce.%s', $this->name, $transition->getName()));
        }
    }
}