vendor/shopware/core/System/StateMachine/StateMachineRegistry.php line 165

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\System\StateMachine;
  3. use Shopware\Core\Content\Flow\Dispatching\Action\SetOrderStateAction;
  4. use Shopware\Core\Framework\Api\Context\AdminApiSource;
  5. use Shopware\Core\Framework\Context;
  6. use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
  7. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Exception\DefinitionNotFoundException;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\StateMachineStateField;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  14. use Shopware\Core\Framework\Feature;
  15. use Shopware\Core\Framework\Log\Package;
  16. use Shopware\Core\Framework\Uuid\Uuid;
  17. use Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateCollection;
  18. use Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateEntity;
  19. use Shopware\Core\System\StateMachine\Aggregation\StateMachineTransition\StateMachineTransitionEntity;
  20. use Shopware\Core\System\StateMachine\Event\StateMachineStateChangeEvent;
  21. use Shopware\Core\System\StateMachine\Event\StateMachineTransitionEvent;
  22. use Shopware\Core\System\StateMachine\Exception\IllegalTransitionException;
  23. use Shopware\Core\System\StateMachine\Exception\StateMachineInvalidEntityIdException;
  24. use Shopware\Core\System\StateMachine\Exception\StateMachineInvalidStateFieldException;
  25. use Shopware\Core\System\StateMachine\Exception\StateMachineNotFoundException;
  26. use Shopware\Core\System\StateMachine\Exception\StateMachineWithoutInitialStateException;
  27. use Shopware\Core\System\StateMachine\Exception\UnnecessaryTransitionException;
  28. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  29. #[Package('checkout')]
  30. class StateMachineRegistry
  31. {
  32.     private EntityRepositoryInterface $stateMachineRepository;
  33.     private EntityRepositoryInterface $stateMachineStateRepository;
  34.     private EntityRepositoryInterface $stateMachineHistoryRepository;
  35.     /**
  36.      * @var StateMachineEntity[]
  37.      */
  38.     private array $stateMachines;
  39.     private EventDispatcherInterface $eventDispatcher;
  40.     private DefinitionInstanceRegistry $definitionRegistry;
  41.     /**
  42.      * @var StateMachineStateEntity[]
  43.      */
  44.     private array $initialStates = [];
  45.     /**
  46.      * @internal
  47.      */
  48.     public function __construct(
  49.         EntityRepositoryInterface $stateMachineRepository,
  50.         EntityRepositoryInterface $stateMachineStateRepository,
  51.         EntityRepositoryInterface $stateMachineHistoryRepository,
  52.         EventDispatcherInterface $eventDispatcher,
  53.         DefinitionInstanceRegistry $definitionRegistry
  54.     ) {
  55.         $this->stateMachineRepository $stateMachineRepository;
  56.         $this->stateMachineStateRepository $stateMachineStateRepository;
  57.         $this->stateMachineHistoryRepository $stateMachineHistoryRepository;
  58.         $this->eventDispatcher $eventDispatcher;
  59.         $this->definitionRegistry $definitionRegistry;
  60.     }
  61.     /**
  62.      * @throws StateMachineNotFoundException
  63.      * @throws InconsistentCriteriaIdsException
  64.      */
  65.     public function getStateMachine(string $nameContext $context): StateMachineEntity
  66.     {
  67.         if (isset($this->stateMachines[$name])) {
  68.             return $this->stateMachines[$name];
  69.         }
  70.         $criteria = new Criteria();
  71.         $criteria
  72.             ->addFilter(new EqualsFilter('state_machine.technicalName'$name))
  73.             ->setLimit(1);
  74.         $criteria->getAssociation('transitions')
  75.             ->addSorting(new FieldSorting('state_machine_transition.actionName'))
  76.             ->addAssociation('fromStateMachineState')
  77.             ->addAssociation('toStateMachineState');
  78.         $criteria->getAssociation('states')
  79.             ->addSorting(new FieldSorting('state_machine_state.technicalName'));
  80.         $results $this->stateMachineRepository->search($criteria$context);
  81.         if ($results->count() === 0) {
  82.             throw new StateMachineNotFoundException($name);
  83.         }
  84.         return $this->stateMachines[$name] = $results->first();
  85.     }
  86.     /**
  87.      * @deprecated tag:v6.5.0 - Use `\Shopware\Core\System\StateMachine\Loader\InitialStateIdLoader::get` instead
  88.      *
  89.      * @throws InconsistentCriteriaIdsException
  90.      * @throws StateMachineNotFoundException
  91.      * @throws StateMachineWithoutInitialStateException
  92.      */
  93.     public function getInitialState(string $stateMachineNameContext $context): StateMachineStateEntity
  94.     {
  95.         Feature::triggerDeprecationOrThrow(
  96.             'v6.5.0.0',
  97.             Feature::deprecatedMethodMessage(__CLASS____METHOD__'v6.5.0.0''InitialStateIdLoader::get()')
  98.         );
  99.         if (isset($this->initialStates[$stateMachineName])) {
  100.             return $this->initialStates[$stateMachineName];
  101.         }
  102.         /** @var StateMachineEntity|null $stateMachine */
  103.         $stateMachine $this->getStateMachine($stateMachineName$context);
  104.         if ($stateMachine === null) {
  105.             throw new StateMachineNotFoundException($stateMachineName);
  106.         }
  107.         $initialState $stateMachine->getInitialState();
  108.         if ($initialState === null) {
  109.             throw new StateMachineWithoutInitialStateException($stateMachineName);
  110.         }
  111.         return $this->initialStates[$stateMachineName] = $initialState;
  112.     }
  113.     /**
  114.      * @throws DefinitionNotFoundException
  115.      * @throws InconsistentCriteriaIdsException
  116.      * @throws StateMachineInvalidEntityIdException
  117.      * @throws StateMachineInvalidStateFieldException
  118.      * @throws StateMachineNotFoundException
  119.      */
  120.     public function getAvailableTransitions(
  121.         string $entityName,
  122.         string $entityId,
  123.         string $stateFieldName,
  124.         Context $context
  125.     ): array {
  126.         $stateMachineName $this->getStateField($stateFieldName$entityName)->getStateMachineName();
  127.         $repository $this->definitionRegistry->getRepository($entityName);
  128.         $fromPlace $this->getFromPlace($entityName$entityId$stateFieldName$context$repository);
  129.         return $this->getAvailableTransitionsById($stateMachineName$fromPlace->getId(), $context);
  130.     }
  131.     /**
  132.      * @throws IllegalTransitionException
  133.      * @throws InconsistentCriteriaIdsException
  134.      * @throws StateMachineNotFoundException
  135.      * @throws StateMachineInvalidStateFieldException
  136.      * @throws StateMachineInvalidEntityIdException
  137.      * @throws DefinitionNotFoundException
  138.      */
  139.     public function transition(Transition $transitionContext $context): StateMachineStateCollection
  140.     {
  141.         $stateField $this->getStateField($transition->getStateFieldName(), $transition->getEntityName());
  142.         $stateMachine $this->getStateMachine($stateField->getStateMachineName(), $context);
  143.         $repository $this->definitionRegistry->getRepository($transition->getEntityName());
  144.         $fromPlace $this->getFromPlace(
  145.             $transition->getEntityName(),
  146.             $transition->getEntityId(),
  147.             $transition->getStateFieldName(),
  148.             $context,
  149.             $repository
  150.         );
  151.         if (empty($transition->getTransitionName())) {
  152.             $transitions $this->getAvailableTransitionsById($stateMachine->getTechnicalName(), $fromPlace->getId(), $context);
  153.             $transitionNames array_map(function (StateMachineTransitionEntity $transition) {
  154.                 return $transition->getActionName();
  155.             }, $transitions);
  156.             throw new IllegalTransitionException($fromPlace->getId(), ''$transitionNames);
  157.         }
  158.         try {
  159.             $toPlace $this->getTransitionDestinationById(
  160.                 $stateMachine->getTechnicalName(),
  161.                 $fromPlace->getId(),
  162.                 $transition->getTransitionName(),
  163.                 $context
  164.             );
  165.         } catch (UnnecessaryTransitionException $e) {
  166.             // No transition needed, therefore don't create a history entry and return
  167.             $stateMachineStateCollection = new StateMachineStateCollection();
  168.             $stateMachineStateCollection->set('fromPlace'$fromPlace);
  169.             $stateMachineStateCollection->set('toPlace'$fromPlace);
  170.             return $stateMachineStateCollection;
  171.         }
  172.         $this->stateMachineHistoryRepository->create([
  173.             [
  174.                 'stateMachineId' => $toPlace->getStateMachineId(),
  175.                 'entityName' => $transition->getEntityName(),
  176.                 'entityId' => ['id' => $transition->getEntityId(), 'version_id' => $context->getVersionId()],
  177.                 'fromStateId' => $fromPlace->getId(),
  178.                 'toStateId' => $toPlace->getId(),
  179.                 'transitionActionName' => $transition->getTransitionName(),
  180.                 'userId' => $context->getSource() instanceof AdminApiSource $context->getSource()->getUserId() : null,
  181.             ],
  182.         ], $context);
  183.         $data = [['id' => $transition->getEntityId(), $transition->getStateFieldName() => $toPlace->getId()]];
  184.         $context->scope(Context::SYSTEM_SCOPE, function (Context $context) use ($repository$data): void {
  185.             $repository->upsert($data$context);
  186.         });
  187.         $this->eventDispatcher->dispatch(
  188.             new StateMachineTransitionEvent(
  189.                 $transition->getEntityName(),
  190.                 $transition->getEntityId(),
  191.                 $fromPlace,
  192.                 $toPlace,
  193.                 $context
  194.             )
  195.         );
  196.         $leaveEvent = new StateMachineStateChangeEvent(
  197.             $context,
  198.             StateMachineStateChangeEvent::STATE_MACHINE_TRANSITION_SIDE_LEAVE,
  199.             $transition,
  200.             $stateMachine,
  201.             $fromPlace,
  202.             $toPlace
  203.         );
  204.         $this->eventDispatcher->dispatch(
  205.             $leaveEvent,
  206.             $leaveEvent->getName()
  207.         );
  208.         $enterEvent = new StateMachineStateChangeEvent(
  209.             $context,
  210.             StateMachineStateChangeEvent::STATE_MACHINE_TRANSITION_SIDE_ENTER,
  211.             $transition,
  212.             $stateMachine,
  213.             $fromPlace,
  214.             $toPlace
  215.         );
  216.         $this->eventDispatcher->dispatch(
  217.             $enterEvent,
  218.             $enterEvent->getName()
  219.         );
  220.         $stateMachineStateCollection = new StateMachineStateCollection();
  221.         $stateMachineStateCollection->set('fromPlace'$fromPlace);
  222.         $stateMachineStateCollection->set('toPlace'$toPlace);
  223.         return $stateMachineStateCollection;
  224.     }
  225.     /**
  226.      * @throws StateMachineNotFoundException
  227.      * @throws InconsistentCriteriaIdsException
  228.      */
  229.     private function getAvailableTransitionsById(string $stateMachineNamestring $fromStateIdContext $context): array
  230.     {
  231.         $stateMachine $this->getStateMachine($stateMachineName$context);
  232.         $transitions = [];
  233.         foreach ($stateMachine->getTransitions() as $transition) {
  234.             if ($transition->getFromStateMachineState()->getId() === $fromStateId) {
  235.                 $transitions[] = $transition;
  236.             }
  237.         }
  238.         return $transitions;
  239.     }
  240.     /**
  241.      * @throws IllegalTransitionException
  242.      * @throws InconsistentCriteriaIdsException
  243.      * @throws StateMachineNotFoundException
  244.      */
  245.     private function getTransitionDestinationById(string $stateMachineNamestring $fromStateIdstring $transitionNameContext $context): StateMachineStateEntity
  246.     {
  247.         $stateMachine $this->getStateMachine($stateMachineName$context);
  248.         foreach ($stateMachine->getTransitions() as $transition) {
  249.             // Always allow to cancel a payment whether its a valid transition or not
  250.             if ($transition->getActionName() === 'cancel' && $transitionName === 'cancel') {
  251.                 return $transition->getToStateMachineState();
  252.             }
  253.             // Not the transition that was requested step over
  254.             if ($transition->getActionName() !== $transitionName) {
  255.                 continue;
  256.             }
  257.             // Already transitioned, this exception is handled by StateMachineRegistry::transition
  258.             if ($transition->getToStateMachineState()->getId() === $fromStateId) {
  259.                 throw new UnnecessaryTransitionException($transitionName);
  260.             }
  261.             // Desired transition found
  262.             if ($transition->getFromStateMachineState()->getId() === $fromStateId) {
  263.                 return $transition->getToStateMachineState();
  264.             }
  265.         }
  266.         if ($context->hasState(SetOrderStateAction::FORCE_TRANSITION)) {
  267.             $criteria = new Criteria();
  268.             $criteria->addFilter(new EqualsFilter('technicalName'$transitionName));
  269.             $criteria->addFilter(new EqualsFilter('stateMachineId'$stateMachine->getId()));
  270.             if ($toPlace $this->stateMachineStateRepository->search($criteria$context)->first()) {
  271.                 return $toPlace;
  272.             }
  273.         }
  274.         $transitions $this->getAvailableTransitionsById($stateMachineName$fromStateId$context);
  275.         $transitionNames array_map(function (StateMachineTransitionEntity $transition) {
  276.             return $transition->getActionName();
  277.         }, $transitions);
  278.         throw new IllegalTransitionException(
  279.             $fromStateId,
  280.             $transitionName,
  281.             $transitionNames
  282.         );
  283.     }
  284.     /**
  285.      * @throws StateMachineInvalidStateFieldException
  286.      * @throws DefinitionNotFoundException
  287.      */
  288.     private function getStateField(string $stateFieldNamestring $entityName): StateMachineStateField
  289.     {
  290.         $definition $this->definitionRegistry->getByEntityName($entityName);
  291.         $stateField $definition->getFields()->get($stateFieldName);
  292.         if (!$stateField || !$stateField instanceof StateMachineStateField) {
  293.             throw new StateMachineInvalidStateFieldException($stateFieldName);
  294.         }
  295.         return $stateField;
  296.     }
  297.     /**
  298.      * @throws InconsistentCriteriaIdsException
  299.      * @throws StateMachineInvalidEntityIdException
  300.      * @throws StateMachineInvalidStateFieldException
  301.      */
  302.     private function getFromPlace(
  303.         string $entityName,
  304.         string $entityId,
  305.         string $stateFieldName,
  306.         Context $context,
  307.         EntityRepositoryInterface $repository
  308.     ): StateMachineStateEntity {
  309.         $entity $repository->search(new Criteria([$entityId]), $context)->get($entityId);
  310.         if (!$entity) {
  311.             throw new StateMachineInvalidEntityIdException($entityName$entityId);
  312.         }
  313.         $fromPlaceId $entity->get($stateFieldName);
  314.         if (!$fromPlaceId || !Uuid::isValid($fromPlaceId)) {
  315.             throw new StateMachineInvalidStateFieldException($stateFieldName);
  316.         }
  317.         /** @var StateMachineStateEntity|null $fromPlace */
  318.         $fromPlace $this->stateMachineStateRepository->search(new Criteria([$fromPlaceId]), $context)->get($fromPlaceId);
  319.         if (!$fromPlace) {
  320.             throw new StateMachineInvalidStateFieldException($stateFieldName);
  321.         }
  322.         return $fromPlace;
  323.     }
  324. }