480 lines
12 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 Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping as ORM;
use Exception;
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;
/**
* a list of future cc users for the next steps.
*
* @var array|User[]
*/
public array $futureCcUsers = [];
/**
* a list of future dest emails for the next steps.
*
* This is in used in order to let controller inform who will be the future emails which will validate
* the next step. This is necessary to perform some computation about the next emails, before they are
* associated to the entity EntityWorkflowStep.
*
* @var array|string[]
*/
public array $futureDestEmails = [];
/**
* a list of future dest users for the next steps.
*
* This is in used in order to let controller inform who will be the future users which will validate
* the next step. This is necessary to perform some computation about the next users, before they are
* associated to the entity EntityWorkflowStep.
*
* @var array|User[]
*/
public array $futureDestUsers = [];
/**
* @var Collection<int, \Chill\MainBundle\Entity\Workflow\EntityWorkflowComment>
*/
#[ORM\OneToMany(targetEntity: EntityWorkflowComment::class, mappedBy: 'entityWorkflow', 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 ArrayCollection<int, EntityWorkflowStep>
*/
#[Assert\Valid(traverse: true)]
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist'], orphanRemoval: true)]
#[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])]
private ArrayCollection $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;
public function __construct()
{
$this->subscriberToFinal = new ArrayCollection();
$this->subscriberToStep = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->steps = 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;
}
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();
}
public function getSteps(): ArrayCollection|Collection
{
return $this->steps;
}
/**
* @throws Exception
*/
public function getStepsChained(): array
{
if (\is_array($this->stepsChainedCache)) {
return $this->stepsChainedCache;
}
$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 $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
{
$steps = $this->getStepsChained();
foreach ($this->getStepsChained() as $step) {
if ($step->isFreezeAfter()) {
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 use by marking store.
*
* @return $this
*/
public function setStep(string $step): self
{
$newStep = new EntityWorkflowStep();
$newStep->setCurrentStep($step);
// 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);
}
}