EntityWorkflow::class])] #[ORM\Entity] #[ORM\Table('chill_main_workflow_entity')] #[EntityWorkflowCreation(groups: ['creation'])] class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface { use TrackCreationTrait; use TrackUpdateTrait; /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowComment::class, orphanRemoval: true)] private Collection $comments; #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] private ?int $id = null; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)] private string $relatedEntityClass = ''; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] private int $relatedEntityId; /** * @var Collection&Selectable */ #[Assert\Valid(traverse: true)] #[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist', 'remove'], orphanRemoval: true)] #[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])] private Collection&Selectable $steps; /** * @var array|EntityWorkflowStep[]|null */ private ?array $stepsChainedCache = null; /** * @var Collection */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_final')] private Collection $subscriberToFinal; /** * @var Collection */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_step')] private Collection $subscriberToStep; /** * a step which will store all the transition data. */ private ?EntityWorkflowStep $transitionningStep = null; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)] private string $workflowName; /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowAttachment::class, cascade: ['remove'], orphanRemoval: true)] private Collection $attachments; public function __construct() { $this->subscriberToFinal = new ArrayCollection(); $this->subscriberToStep = new ArrayCollection(); $this->comments = new ArrayCollection(); $this->steps = new ArrayCollection(); $this->attachments = new ArrayCollection(); $initialStep = new EntityWorkflowStep(); $initialStep ->setCurrentStep('initial'); $this->addStep($initialStep); } public function addComment(EntityWorkflowComment $comment): self { if (!$this->comments->contains($comment)) { $this->comments[] = $comment; $comment->setEntityWorkflow($this); } return $this; } /** * @internal You should prepare a step and run a workflow transition instead of manually adding a step */ public function addStep(EntityWorkflowStep $step): self { if (!$this->steps->contains($step)) { $this->steps[] = $step; $step->setEntityWorkflow($this); $this->stepsChainedCache = null; } return $this; } public function addSubscriberToFinal(User $user): self { if (!$this->subscriberToFinal->contains($user)) { $this->subscriberToFinal[] = $user; } return $this; } public function addSubscriberToStep(User $user): self { if (!$this->subscriberToStep->contains($user)) { $this->subscriberToStep[] = $user; } return $this; } /** * @return $this * * @internal use @{EntityWorkflowAttachement::__construct} instead */ public function addAttachment(EntityWorkflowAttachment $attachment): self { if (!$this->attachments->contains($attachment)) { $this->attachments[] = $attachment; } return $this; } /** * @return Collection */ public function getAttachments(): Collection { return $this->attachments; } public function removeAttachment(EntityWorkflowAttachment $attachment): self { $this->attachments->removeElement($attachment); return $this; } public function getComments(): Collection { return $this->comments; } public function getCurrentStep(): ?EntityWorkflowStep { $step = $this->steps->last(); if (false !== $step) { return $step; } return null; } public function getCurrentStepChained(): ?EntityWorkflowStep { $steps = $this->getStepsChained(); $currentStep = $this->getCurrentStep(); foreach ($steps as $step) { if ($step === $currentStep) { return $step; } } return null; } public function getCurrentStepCreatedAt(): ?\DateTimeInterface { if (null !== $previous = $this->getPreviousStepIfAny()) { return $previous->getTransitionAt(); } return null; } public function getCurrentStepCreatedBy(): ?User { if (null !== $previous = $this->getPreviousStepIfAny()) { return $previous->getTransitionBy(); } return null; } public function getId(): ?int { return $this->id; } public function getRelatedEntityClass(): string { return $this->relatedEntityClass; } public function getRelatedEntityId(): int { return $this->relatedEntityId; } /** * Method used by MarkingStore. * * get a string representation of the step */ public function getStep(): string { return $this->getCurrentStep()->getCurrentStep(); } public function getStepAfter(EntityWorkflowStep $step): ?EntityWorkflowStep { $iterator = $this->steps->getIterator(); if ($iterator instanceof \Iterator) { $iterator->rewind(); while ($iterator->valid()) { $curStep = $iterator->current(); if ($curStep === $step) { $iterator->next(); if ($iterator->valid()) { return $iterator->current(); } return null; } $iterator->next(); } return null; } throw new \RuntimeException(); } /** * @return Selectable&Collection */ public function getSteps(): Collection&Selectable { return $this->steps; } /** * @throws \Exception */ public function getStepsChained(): array { if (\is_array($this->stepsChainedCache)) { return $this->stepsChainedCache; } /** @var \ArrayIterator $iterator */ $iterator = $this->steps->getIterator(); $current = null; $steps = []; $iterator->rewind(); do { $previous = $current; $current = $iterator->current(); $steps[] = $current; $current->setPrevious($previous); $iterator->next(); if ($iterator->valid()) { $current->setNext($iterator->current()); } else { $current->setNext(null); } } while ($iterator->valid()); $this->stepsChainedCache = $steps; return $steps; } public function getSubscriberToFinal(): ArrayCollection|Collection { return $this->subscriberToFinal; } public function getSubscriberToStep(): ArrayCollection|Collection { return $this->subscriberToStep; } /** * get the step which is transitionning. Should be called only by event which will * concern the transition. */ public function getTransitionningStep(): ?EntityWorkflowStep { return $this->transitionningStep; } /** * @return User[] */ public function getUsersInvolved(): array { $usersInvolved = []; $usersInvolved[spl_object_hash($this->getCreatedBy())] = $this->getCreatedBy(); foreach ($this->steps as $step) { foreach ($step->getDestUser() as $u) { $usersInvolved[spl_object_hash($u)] = $u; } } return array_values($usersInvolved); } public function getWorkflowName(): string { return $this->workflowName; } public function isFinal(): bool { foreach ($this->getStepsChained() as $step) { if ($step->isFinal()) { return true; } } return false; } public function isFreeze(): bool { foreach ($this->getStepsChained() as $step) { if ($step->isFreezeAfter()) { return true; } } return false; } public function isOnHoldByUser(User $user): bool { return $this->getCurrentStep()->isOnHoldByUser($user); } public function isUserInvolved(User $user): bool { foreach ($this->getSteps() as $step) { if ($step->getAllDestUser()->contains($user)) { return true; } } return false; } public function isUserSubscribedToFinal(User $user): bool { return $this->subscriberToFinal->contains($user); } public function isUserSubscribedToStep(User $user): bool { return $this->subscriberToStep->contains($user); } public function prepareStepBeforeTransition(EntityWorkflowStep $step): self { $this->transitionningStep = $step; return $this; } public function removeComment(EntityWorkflowComment $comment): self { if ($this->comments->removeElement($comment)) { $comment->setEntityWorkflow(null); } return $this; } public function removeStep(EntityWorkflowStep $step): self { if ($this->steps->removeElement($step)) { $step->setEntityWorkflow(null); } return $this; } public function removeSubscriberToFinal(User $user): self { $this->subscriberToFinal->removeElement($user); return $this; } public function removeSubscriberToStep(User $user): self { $this->subscriberToStep->removeElement($user); return $this; } public function setRelatedEntityClass(string $relatedEntityClass): EntityWorkflow { $this->relatedEntityClass = $relatedEntityClass; return $this; } public function setRelatedEntityId(int $relatedEntityId): EntityWorkflow { $this->relatedEntityId = $relatedEntityId; return $this; } /** * Method used by marking store. * * @return $this */ public function setStep( string $step, WorkflowTransitionContextDTO $transitionContextDTO, string $transition, \DateTimeImmutable $transitionAt, ?User $byUser = null, ): self { $previousStep = $this->getCurrentStep(); $previousStep ->setComment($transitionContextDTO->comment) ->setTransitionAfter($transition) ->setTransitionAt($transitionAt) ->setTransitionBy($byUser); $newStep = new EntityWorkflowStep(); $newStep->setCurrentStep($step); foreach ($transitionContextDTO->futureCcUsers as $user) { $newStep->addCcUser($user); } foreach ($transitionContextDTO->getFutureDestUsers() as $user) { $newStep->addDestUser($user); } foreach ($transitionContextDTO->getFutureDestUserGroups() as $userGroup) { $newStep->addDestUserGroup($userGroup); } if (null !== $transitionContextDTO->futureUserSignature) { $newStep->addDestUser($transitionContextDTO->futureUserSignature); } if (null !== $transitionContextDTO->futureUserSignature) { new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature); } else { foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) { new EntityWorkflowStepSignature($newStep, $personSignature); } } foreach ($transitionContextDTO->futureDestineeThirdParties as $thirdParty) { new EntityWorkflowSend($newStep, $thirdParty, $transitionAt->add(new \DateInterval('P30D'))); } foreach ($transitionContextDTO->futureDestineeEmails as $email) { new EntityWorkflowSend($newStep, $email, $transitionAt->add(new \DateInterval('P30D'))); } // copy the freeze if ($this->isFreeze()) { $newStep->setFreezeAfter(true); } $this->addStep($newStep); return $this; } public function setWorkflowName(string $workflowName): EntityWorkflow { $this->workflowName = $workflowName; return $this; } private function getPreviousStepIfAny(): ?EntityWorkflowStep { if (1 === \count($this->steps)) { return null; } return $this->steps->get($this->steps->count() - 2); } public function isOnHoldAtCurrentStep(): bool { return $this->getCurrentStep()->getHoldsOnStep()->count() > 0; } /** * Determines if the workflow has become stale after a given date. * * This function checks the creation date and the transition states of the workflow steps. * A workflow is considered stale if: * - The creation date is before the given date and no transitions have occurred since the creation. * - Or if there are no transitions after the given date. * * @param \DateTimeImmutable $at the date to compare against the workflow's status * * @return bool true if the workflow is stale after the given date, false otherwise */ public function isStaledAt(\DateTimeImmutable $at): bool { // if there is no transition since the creation, then the workflow is staled if ('initial' === $this->getCurrentStep()->getCurrentStep() && null === $this->getCurrentStep()->getTransitionAt() ) { if (null === $this->getCreatedAt()) { return false; } if ($this->getCreatedAt() < $at) { return true; } return false; } return $this->getCurrentStepChained()->getPrevious()->getTransitionAt() < $at; } }