mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
586 lines
16 KiB
PHP
586 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* Chill is a software for social workers
|
|
*
|
|
* For the full copyright and license information, please view
|
|
* the LICENSE file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Chill\MainBundle\Entity\Workflow;
|
|
|
|
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
|
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
|
use Chill\MainBundle\Entity\User;
|
|
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
|
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
use Doctrine\Common\Collections\Collection;
|
|
use Doctrine\Common\Collections\Selectable;
|
|
use Doctrine\ORM\Mapping as ORM;
|
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
|
|
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['entity_workflow' => EntityWorkflow::class])]
|
|
#[ORM\Entity]
|
|
#[ORM\Table('chill_main_workflow_entity')]
|
|
#[EntityWorkflowCreation(groups: ['creation'])]
|
|
class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|
{
|
|
use TrackCreationTrait;
|
|
|
|
use TrackUpdateTrait;
|
|
|
|
/**
|
|
* @var Collection<int, EntityWorkflowComment>
|
|
*/
|
|
#[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<int, EntityWorkflowStep>&Selectable<int, EntityWorkflowStep>
|
|
*/
|
|
#[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<int, User>
|
|
*/
|
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
|
#[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_final')]
|
|
private Collection $subscriberToFinal;
|
|
|
|
/**
|
|
* @var Collection<int, User>
|
|
*/
|
|
#[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<int, EntityWorkflowAttachment>
|
|
*/
|
|
#[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<int, EntityWorkflowAttachment>
|
|
*/
|
|
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<int, EntityWorkflowStep>&Collection<int, EntityWorkflowStep>
|
|
*/
|
|
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;
|
|
}
|
|
}
|