From 3db4fff80df6ce82c97b8c6b00b1cd037a3ccb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 28 Jun 2024 11:58:39 +0200 Subject: [PATCH 1/2] Add signature functionality to workflow entities Created new files to add signature functionality to the workflow entities, including signature state enums and signature metadata. Added these changes to the migration script as well. Updated EntityWorkflowStep to include a collection for signatures. --- .../EntityWorkflowSignatureStateEnum.php | 20 ++ .../Entity/Workflow/EntityWorkflowStep.php | 42 ++- .../Workflow/EntityWorkflowStepSignature.php | 156 ++++++++++ .../EntityWorkflowStepSignatureRepository.php | 54 ++++ .../EntityWorkflowStepSignatureTest.php | 69 ++++ .../migrations/Version20240628095159.php | 51 +++ tests/app/config/packages/workflow_chill.yaml | 294 ++++++++++++++++++ 7 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php create mode 100644 src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20240628095159.php create mode 100644 tests/app/config/packages/workflow_chill.yaml diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php new file mode 100644 index 000000000..2d3e4269f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php @@ -0,0 +1,20 @@ + + * @var Collection */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')] private Collection $destUser; /** - * @var Collection + * @var Collection */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')] private Collection $destUserByAccessKey; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)] + private Collection $signatures; + #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')] private ?EntityWorkflow $entityWorkflow = null; @@ -97,6 +103,7 @@ class EntityWorkflowStep $this->ccUser = new ArrayCollection(); $this->destUser = new ArrayCollection(); $this->destUserByAccessKey = new ArrayCollection(); + $this->signatures = new ArrayCollection(); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32)); } @@ -136,6 +143,29 @@ class EntityWorkflowStep return $this; } + /** + * @internal use @see{EntityWorkflowStepSignature}'s constructor instead + */ + public function addSignature(EntityWorkflowStepSignature $signature): self + { + if (!$this->signatures->contains($signature)) { + $this->signatures[] = $signature; + } + + return $this; + } + + public function removeSignature(EntityWorkflowStepSignature $signature): self + { + if ($this->signatures->contains($signature)) { + $this->signatures->removeElement($signature); + } + + $signature->detachEntityWorkflowStep(); + + return $this; + } + public function getAccessKey(): string { return $this->accessKey; @@ -198,6 +228,14 @@ class EntityWorkflowStep return $this->entityWorkflow; } + /** + * @return Collection + */ + public function getSignatures(): Collection + { + return $this->signatures; + } + public function getId(): ?int { return $this->id; diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php new file mode 100644 index 000000000..25babc3c6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php @@ -0,0 +1,156 @@ + null])] + private ?\DateTimeImmutable $stateDate = null; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] + private array $signatureMetadata = []; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])] + private ?int $zoneSignatureIndex = null; + + #[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')] + #[ORM\JoinColumn(nullable: false)] + private ?EntityWorkflowStep $step = null; + + public function __construct( + EntityWorkflowStep $step, + User|Person $signer, + ) { + $this->step = $step; + $step->addSignature($this); + $this->setSigner($signer); + } + + private function setSigner(User|Person $signer): void + { + if ($signer instanceof User) { + $this->userSigner = $signer; + } else { + $this->personSigner = $signer; + } + } + + public function getId(): ?int + { + return $this->id; + } + + public function getStep(): EntityWorkflowStep + { + return $this->step; + } + + public function getSigner(): User|Person + { + if (null !== $this->userSigner) { + return $this->userSigner; + } + + return $this->personSigner; + } + + public function getSignatureMetadata(): array + { + return $this->signatureMetadata; + } + + public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature + { + $this->signatureMetadata = $signatureMetadata; + + return $this; + } + + public function getState(): EntityWorkflowSignatureStateEnum + { + return $this->state; + } + + public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature + { + $this->state = $state; + + return $this; + } + + public function getStateDate(): ?\DateTimeImmutable + { + return $this->stateDate; + } + + public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature + { + $this->stateDate = $stateDate; + + return $this; + } + + public function getZoneSignatureIndex(): ?int + { + return $this->zoneSignatureIndex; + } + + public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature + { + $this->zoneSignatureIndex = $zoneSignatureIndex; + + return $this; + } + + /** + * Detach from the @see{EntityWorkflowStep}. + * + * @internal used internally to remove the current signature + * + * @return $this + */ + public function detachEntityWorkflowStep(): self + { + $this->step = null; + + return $this; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php new file mode 100644 index 000000000..0e1393242 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php @@ -0,0 +1,54 @@ + + */ +class EntityWorkflowStepSignatureRepository implements ObjectRepository +{ + private \Doctrine\ORM\EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class); + } + + public function find($id): ?EntityWorkflowStepSignature + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?EntityWorkflowStepSignature + { + return $this->findOneBy($criteria); + } + + public function getClassName(): string + { + return EntityWorkflowStepSignature::class; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php new file mode 100644 index 000000000..62b6a7d6a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php @@ -0,0 +1,69 @@ +entityManager = self::getContainer()->get(EntityManagerInterface::class); + } + + public function testConstruct() + { + $workflow = new EntityWorkflow(); + $workflow->setWorkflowName('vendee_internal') + ->setRelatedEntityId(0) + ->setRelatedEntityClass(AccompanyingPeriodWorkEvaluationDocument::class); + + $step = $workflow->getCurrentStep(); + + $person = $this->entityManager->createQuery('SELECT p FROM '.Person::class.' p') + ->setMaxResults(1) + ->getSingleResult(); + + $signature = new EntityWorkflowStepSignature($step, $person); + + self::assertCount(1, $step->getSignatures()); + self::assertSame($signature, $step->getSignatures()->first()); + + $this->entityManager->getConnection()->beginTransaction(); + $this->entityManager->persist($workflow); + $this->entityManager->persist($step); + $this->entityManager->persist($signature); + + $this->entityManager->flush(); + $this->entityManager->getConnection()->commit(); + + $this->entityManager->clear(); + + $signatureBis = $this->entityManager->find(EntityWorkflowStepSignature::class, $signature->getId()); + + self::assertEquals($signature->getId(), $signatureBis->getId()); + self::assertEquals($step->getId(), $signatureBis->getStep()->getId()); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php b/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php new file mode 100644 index 000000000..4452530c6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php @@ -0,0 +1,51 @@ +addSql('CREATE SEQUENCE chill_main_workflow_entity_step_signature_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_workflow_entity_step_signature (id INT NOT NULL, step_id INT NOT NULL, '. + 'state VARCHAR(50) NOT NULL, stateDate TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, signatureMetadata JSON DEFAULT \'[]\' NOT NULL,'. + ' zoneSignatureIndex INT DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,'. + ' userSigner_id INT DEFAULT NULL, personSigner_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C47D4BA3D934E3A4 ON chill_main_workflow_entity_step_signature (userSigner_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA3ADFFA293 ON chill_main_workflow_entity_step_signature (personSigner_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA373B21E9C ON chill_main_workflow_entity_step_signature (step_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA33174800F ON chill_main_workflow_entity_step_signature (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA365FF1AEC ON chill_main_workflow_entity_step_signature (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.stateDate IS \'(DC2Type:datetimetz_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3D934E3A4 FOREIGN KEY (userSigner_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3ADFFA293 FOREIGN KEY (personSigner_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA373B21E9C FOREIGN KEY (step_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA33174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_signature_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_main_workflow_entity_step_signature'); + } +} diff --git a/tests/app/config/packages/workflow_chill.yaml b/tests/app/config/packages/workflow_chill.yaml new file mode 100644 index 000000000..82662461b --- /dev/null +++ b/tests/app/config/packages/workflow_chill.yaml @@ -0,0 +1,294 @@ +framework: + workflows: + vendee_internal: + type: state_machine + metadata: + related_entity: + - Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument + - Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork + - Chill\DocStoreBundle\Entity\AccompanyingCourseDocument + label: + fr: 'Suivi' + support_strategy: Chill\MainBundle\Workflow\RelatedEntityWorkflowSupportsStrategy + initial_marking: 'initial' + marking_store: + property: step + type: method + places: + initial: + metadata: + label: + fr: Étape initiale + attenteModification: + metadata: + label: + fr: En attente de modification du document + validationFilterInputLabels: + forward: {fr: Modification effectuée} + backward: {fr: Pas de modification effectuée} + neutral: {fr: Autre} + attenteMiseEnForme: + metadata: + label: + fr: En attente de mise en forme + validationFilterInputLabels: + forward: {fr: Mise en forme terminée} + backward: {fr: Pas de mise en forme effectuée} + neutral: {fr: Autre} + attenteVisa: + metadata: + label: + fr: En attente de visa + validationFilterInputLabels: + forward: {fr: Visa accordé} + backward: {fr: Visa refusé} + neutral: {fr: Autre} + attenteSignature: + metadata: + label: + fr: En attente de signature + validationFilterInputLabels: + forward: {fr: Signature accordée} + backward: {fr: Signature refusée} + neutral: {fr: Autre} + attenteTraitement: + metadata: + label: + fr: En attente de traitement + validationFilterInputLabels: + forward: {fr: Traitement terminé favorablement} + backward: {fr: Traitement terminé défavorablement} + neutral: {fr: Autre} + attenteEnvoi: + metadata: + label: + fr: En attente d'envoi + validationFilterInputLabels: + forward: {fr: Document envoyé} + backward: {fr: Document non envoyé} + neutral: {fr: Autre} + attenteValidationMiseEnForme: + metadata: + label: + fr: En attente de validation de la mise en forme + validationFilterInputLabels: + forward: {fr: Validation de la mise en forme} + backward: {fr: Refus de validation de la mise en forme} + neutral: {fr: Autre} + annule: + metadata: + isFinal: true + isFinalPositive: false + label: + fr: Annulé + final: + metadata: + isFinal: true + isFinalPositive: true + label: + fr: Finalisé + transitions: + # transition qui avancent + demandeModificationDocument: + from: + - initial + to: attenteModification + metadata: + label: + fr: Demande de modification du document + isForward: true + demandeMiseEnForme: + from: + - initial + - attenteModification + to: attenteMiseEnForme + metadata: + label: + fr: Demande de mise en forme + isForward: true + demandeValidationMiseEnForme: + from: + - attenteMiseEnForme + to: attenteValidationMiseEnForme + metadata: + label: + fr: Demande de validation de la mise en forme + isForward: true + demandeVisa: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: attenteVisa + metadata: + label: + fr: Demande de visa + isForward: true + demandeSignature: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + to: attenteSignature + metadata: + label: {fr: Demande de signature} + isForward: true + demandeTraitement: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + to: attenteTraitement + metadata: + label: {fr: Demande de traitement} + isForward: true + demandeEnvoi: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + - attenteTraitement + to: attenteEnvoi + metadata: + label: {fr: Demande d'envoi} + isForward: true + annulation: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: annule + metadata: + label: {fr: Annulation} + isForward: false + # transitions qui répètent l'étape + demandeMiseEnFormeSupplementaire: + from: + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: attenteMiseEnForme + metadata: + label: {fr: Demande de mise en forme supplémentaire} + demandeVisaSupplementaire: + from: + - attenteVisa + to: attenteVisa + metadata: + label: {fr: Demande de visa supplémentaire} + isForward: true + demandeSignatureSupplementaire: + from: + - attenteSignature + to: attenteSignature + metadata: + label: {fr: Demande de signature supplémentaire} + demandeTraitementSupplementaire: + from: + - attenteTraitement + to: attenteTraitement + metadata: + label: {fr: Demande de traitement supplémentaire} + # transitions qui renvoient vers une étape précédente + refusEtModificationDocument: + from: + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteModification + metadata: + label: + fr: Refus et demande de modification du document + isForward: false + refusEtDemandeMiseEnForme: + from: + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteMiseEnForme + metadata: + label: {fr: Refus et demande de mise en forme} + isForward: false + refusEtDemandeVisa: + from: + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteVisa + metadata: + label: {fr: Refus et demande de visa} + isForward: false + refusEtDemandeSignature: + from: + - attenteTraitement + - attenteEnvoi + to: attenteSignature + metadata: + label: {fr: Refus et demande de signature} + isForward: false + refusEtDemandeTraitement: + from: + - attenteEnvoi + to: attenteTraitement + metadata: + label: {fr: Refus et demande de traitement} + isForward: false + # transition vers final + initialToFinal: + from: + - initial + to: final + metadata: + label: {fr: Clotûre immédiate et cloture positive} + isForward: true + attenteMiseEnFormeToFinal: + from: + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: final + metadata: + label: {fr: Mise en forme terminée et cloture positive} + isForward: true + attenteVisaToFinal: + from: + - attenteVisa + to: final + metadata: + label: {fr: Accorde le visa et cloture positive} + isForward: true + attenteSignatureToFinal: + from: + - attenteSignature + to: final + metadata: + label: {fr: Accorde la signature et cloture positive} + isForward: true + attenteTraitementToFinal: + from: + - attenteTraitement + to: final + metadata: + label: {fr: Traitement terminé et cloture postive} + isForward: true + attenteEnvoiToFinal: + from: + - attenteEnvoi + to: final + metadata: + label: {fr: Envoyé et cloture postive} + isForward: true From a309cc077480c1b2889f630098925c9b90476d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 1 Jul 2024 20:47:15 +0200 Subject: [PATCH 2/2] Refactor workflow classes and forms - the workflow controller add a context to each transition; - the state of the entity workflow is applyied using a dedicated marking store - the method EntityWorkflow::step use the context to associate the new step with the future destination user, cc users and email. This makes the step consistent at every step. - this allow to remove some logic which was processed in eventSubscribers, - as counterpart, each workflow must specify a dedicated marking_store: ```yaml framework: workflows: vendee_internal: # ... marking_store: service: Chill\MainBundle\Workflow\EntityWorkflowMarkingStore ``` --- ...ompanyingCourseDocumentWorkflowHandler.php | 5 - .../Controller/WorkflowController.php | 13 +- .../Entity/Workflow/EntityWorkflow.php | 45 +-- .../ChillMainBundle/Form/WorkflowStepType.php | 266 ++++++++---------- .../views/Workflow/_decision.html.twig | 14 +- .../Entity/Workflow/EntityWorkflowTest.php | 17 +- .../EntityWorkflowMarkingStoreTest.php | 61 ++++ .../EntityWorkflowHandlerInterface.php | 2 - .../Workflow/EntityWorkflowMarkingStore.php | 49 ++++ ...ntityWorkflowTransitionEventSubscriber.php | 35 +-- .../NotificationOnTransition.php | 13 +- .../SendAccessKeyEventSubscriber.php | 10 +- .../Workflow/WorkflowTransitionContextDTO.php | 74 +++++ ...dWorkEvaluationDocumentWorkflowHandler.php | 5 - ...ingPeriodWorkEvaluationWorkflowHandler.php | 5 - .../AccompanyingPeriodWorkWorkflowHandler.php | 5 - 16 files changed, 362 insertions(+), 257 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index 9d1b619f9..55b823dcd 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -121,9 +121,4 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler { return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass(); } - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool - { - return false; - } } diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php index d75e42012..3a8626f26 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php @@ -22,6 +22,7 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter; use Chill\MainBundle\Security\ChillSecurity; use Chill\MainBundle\Workflow\EntityWorkflowManager; +use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\FormType; @@ -279,7 +280,7 @@ class WorkflowController extends AbstractController if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) { // possible transition - + $stepDTO = new WorkflowTransitionContextDTO($entityWorkflow); $usersInvolved = $entityWorkflow->getUsersInvolved(); $currentUserFound = array_search($this->security->getUser(), $usersInvolved, true); @@ -289,9 +290,8 @@ class WorkflowController extends AbstractController $transitionForm = $this->createForm( WorkflowStepType::class, - $entityWorkflow->getCurrentStep(), + $stepDTO, [ - 'transition' => true, 'entity_workflow' => $entityWorkflow, 'suggested_users' => $usersInvolved, ] @@ -310,12 +310,7 @@ class WorkflowController extends AbstractController throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs))); } - // TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context) - $entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? []; - $entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? []; - $entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? []; - - $workflow->apply($entityWorkflow, $transition); + $workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]); $this->entityManager->flush(); diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index 6df054b61..9c314115e 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -17,9 +17,9 @@ 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\Order; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Validator\Constraints as Assert; @@ -34,35 +34,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface 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 */ @@ -442,11 +413,23 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface * * @return $this */ - public function setStep(string $step): self + public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self { $newStep = new EntityWorkflowStep(); $newStep->setCurrentStep($step); + foreach ($transitionContextDTO->futureCcUsers as $user) { + $newStep->addCcUser($user); + } + + foreach ($transitionContextDTO->futureDestUsers as $user) { + $newStep->addDestUser($user); + } + + foreach ($transitionContextDTO->futureDestEmails as $email) { + $newStep->addDestEmail($email); + } + // copy the freeze if ($this->isFreeze()) { $newStep->setFreezeAfter(true); diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php index 3a3f1b8d3..284781d54 100644 --- a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php +++ b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php @@ -12,14 +12,12 @@ declare(strict_types=1); namespace Chill\MainBundle\Form; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; -use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; -use Chill\MainBundle\Workflow\EntityWorkflowManager; +use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\FormBuilderInterface; @@ -34,169 +32,151 @@ use Symfony\Component\Workflow\Transition; class WorkflowStepType extends AbstractType { - public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {} + public function __construct( + private readonly Registry $registry, + private readonly TranslatableStringHelperInterface $translatableStringHelper + ) {} public function buildForm(FormBuilderInterface $builder, array $options) { /** @var EntityWorkflow $entityWorkflow */ $entityWorkflow = $options['entity_workflow']; - $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $place = $workflow->getMarking($entityWorkflow); $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]); - if (true === $options['transition']) { - if (null === $options['entity_workflow']) { - throw new \LogicException('if transition is true, entity_workflow should be defined'); - } + if (null === $options['entity_workflow']) { + throw new \LogicException('if transition is true, entity_workflow should be defined'); + } - $transitions = $this->registry - ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName()) - ->getEnabledTransitions($entityWorkflow); + $transitions = $this->registry + ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName()) + ->getEnabledTransitions($entityWorkflow); - $choices = array_combine( - array_map( - static fn (Transition $transition) => $transition->getName(), - $transitions - ), + $choices = array_combine( + array_map( + static fn (Transition $transition) => $transition->getName(), $transitions - ); + ), + $transitions + ); - if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) { - $inputLabels = $placeMetadata['validationFilterInputLabels']; + if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) { + $inputLabels = $placeMetadata['validationFilterInputLabels']; - $builder->add('transitionFilter', ChoiceType::class, [ - 'multiple' => false, - 'label' => 'workflow.My decision', - 'choices' => [ - 'forward' => 'forward', - 'backward' => 'backward', - 'neutral' => 'neutral', - ], - 'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]), - 'choice_attr' => static fn (string $key) => [ - $key => $key, - ], - 'mapped' => false, - 'expanded' => true, - 'data' => 'forward', - ]); - } - - $builder - ->add('transition', ChoiceType::class, [ - 'label' => 'workflow.Next step', - 'mapped' => false, - 'multiple' => false, - 'expanded' => true, - 'choices' => $choices, - 'constraints' => [new NotNull()], - 'choice_label' => function (Transition $transition) use ($workflow) { - $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition); - - if (\array_key_exists('label', $meta)) { - return $this->translatableStringHelper->localize($meta['label']); - } - - return $transition->getName(); - }, - 'choice_attr' => static function (Transition $transition) use ($workflow) { - $toFinal = true; - $isForward = 'neutral'; - - $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); - - if (\array_key_exists('isForward', $metadata)) { - if ($metadata['isForward']) { - $isForward = 'forward'; - } else { - $isForward = 'backward'; - } - } - - foreach ($transition->getTos() as $to) { - $meta = $workflow->getMetadataStore()->getPlaceMetadata($to); - - if ( - !\array_key_exists('isFinal', $meta) || false === $meta['isFinal'] - ) { - $toFinal = false; - } - } - - return [ - 'data-is-transition' => 'data-is-transition', - 'data-to-final' => $toFinal ? '1' : '0', - 'data-is-forward' => $isForward, - ]; - }, - ]) - ->add('future_dest_users', PickUserDynamicType::class, [ - 'label' => 'workflow.dest for next steps', - 'multiple' => true, - 'mapped' => false, - 'suggested' => $options['suggested_users'], - ]) - ->add('future_cc_users', PickUserDynamicType::class, [ - 'label' => 'workflow.cc for next steps', - 'multiple' => true, - 'mapped' => false, - 'required' => false, - 'suggested' => $options['suggested_users'], - ]) - ->add('future_dest_emails', ChillCollectionType::class, [ - 'label' => 'workflow.dest by email', - 'help' => 'workflow.dest by email help', - 'mapped' => false, - 'allow_add' => true, - 'entry_type' => EmailType::class, - 'button_add_label' => 'workflow.Add an email', - 'button_remove_label' => 'workflow.Remove an email', - 'empty_collection_explain' => 'workflow.Any email', - 'entry_options' => [ - 'constraints' => [ - new NotNull(), new NotBlank(), new Email(), - ], - 'label' => 'Email', - ], - ]); + $builder->add('transitionFilter', ChoiceType::class, [ + 'multiple' => false, + 'label' => 'workflow.My decision', + 'choices' => [ + 'forward' => 'forward', + 'backward' => 'backward', + 'neutral' => 'neutral', + ], + 'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]), + 'choice_attr' => static fn (string $key) => [ + $key => $key, + ], + 'mapped' => false, + 'expanded' => true, + 'data' => 'forward', + ]); } - if ( - $handler->supportsFreeze($entityWorkflow) - && !$entityWorkflow->isFreeze() - ) { - $builder - ->add('freezeAfter', CheckboxType::class, [ - 'required' => false, - 'label' => 'workflow.Freeze', - 'help' => 'workflow.The associated element will be freezed', - ]); - } + $builder + ->add('transition', ChoiceType::class, [ + 'label' => 'workflow.Next step', + 'mapped' => false, + 'multiple' => false, + 'expanded' => true, + 'choices' => $choices, + 'constraints' => [new NotNull()], + 'choice_label' => function (Transition $transition) use ($workflow) { + $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition); + + if (\array_key_exists('label', $meta)) { + return $this->translatableStringHelper->localize($meta['label']); + } + + return $transition->getName(); + }, + 'choice_attr' => static function (Transition $transition) use ($workflow) { + $toFinal = true; + $isForward = 'neutral'; + + $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); + + if (\array_key_exists('isForward', $metadata)) { + if ($metadata['isForward']) { + $isForward = 'forward'; + } else { + $isForward = 'backward'; + } + } + + foreach ($transition->getTos() as $to) { + $meta = $workflow->getMetadataStore()->getPlaceMetadata($to); + + if ( + !\array_key_exists('isFinal', $meta) || false === $meta['isFinal'] + ) { + $toFinal = false; + } + } + + return [ + 'data-is-transition' => 'data-is-transition', + 'data-to-final' => $toFinal ? '1' : '0', + 'data-is-forward' => $isForward, + ]; + }, + ]) + ->add('futureDestUsers', PickUserDynamicType::class, [ + 'label' => 'workflow.dest for next steps', + 'multiple' => true, + 'suggested' => $options['suggested_users'], + ]) + ->add('futureCcUsers', PickUserDynamicType::class, [ + 'label' => 'workflow.cc for next steps', + 'multiple' => true, + 'required' => false, + 'suggested' => $options['suggested_users'], + ]) + ->add('futureDestEmails', ChillCollectionType::class, [ + 'label' => 'workflow.dest by email', + 'help' => 'workflow.dest by email help', + 'allow_add' => true, + 'entry_type' => EmailType::class, + 'button_add_label' => 'workflow.Add an email', + 'button_remove_label' => 'workflow.Remove an email', + 'empty_collection_explain' => 'workflow.Any email', + 'entry_options' => [ + 'constraints' => [ + new NotNull(), new NotBlank(), new Email(), + ], + 'label' => 'Email', + ], + ]); $builder ->add('comment', ChillTextareaType::class, [ 'required' => false, 'label' => 'Comment', + 'empty_data' => '', ]); } public function configureOptions(OptionsResolver $resolver) { $resolver - ->setDefined('class') - ->setRequired('transition') - ->setAllowedTypes('transition', 'bool') + ->setDefault('data_class', WorkflowTransitionContextDTO::class) ->setRequired('entity_workflow') ->setAllowedTypes('entity_workflow', EntityWorkflow::class) ->setDefault('suggested_users', []) ->setDefault('constraints', [ new Callback( - function ($step, ExecutionContextInterface $context, $payload) { - /** @var EntityWorkflowStep $step */ - $form = $context->getObject(); - $workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName()); - $transition = $form['transition']->getData(); + function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) { + $workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName()); + $transition = $step->transition; $toFinal = true; if (null === $transition) { @@ -212,8 +192,8 @@ class WorkflowStepType extends AbstractType $toFinal = false; } } - $destUsers = $form['future_dest_users']->getData(); - $destEmails = $form['future_dest_emails']->getData(); + $destUsers = $step->futureDestUsers; + $destEmails = $step->futureDestEmails; if (!$toFinal && [] === $destUsers && [] === $destEmails) { $context @@ -224,20 +204,6 @@ class WorkflowStepType extends AbstractType } } ), - new Callback( - function ($step, ExecutionContextInterface $context, $payload) { - $form = $context->getObject(); - - foreach ($form->get('future_dest_users')->getData() as $u) { - if (in_array($u, $form->get('future_cc_users')->getData(), true)) { - $context - ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step') - ->atPath('ccUsers') - ->addViolation(); - } - } - } - ), ]); } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index 0056c503d..35f587e38 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -58,17 +58,15 @@ {{ form_row(transition_form.transition) }} - {% if transition_form.freezeAfter is defined %} - {{ form_row(transition_form.freezeAfter) }} - {% endif %} -
- {{ form_row(transition_form.future_dest_users) }} + {{ form_row(transition_form.futureDestUsers) }} + {{ form_errors(transition_form.futureDestUsers) }} - {{ form_row(transition_form.future_cc_users) }} + {{ form_row(transition_form.futureCcUsers) }} + {{ form_errors(transition_form.futureCcUsers) }} - {{ form_row(transition_form.future_dest_emails) }} - {{ form_errors(transition_form.future_dest_users) }} + {{ form_row(transition_form.futureDestEmails) }} + {{ form_errors(transition_form.futureDestEmails) }}

{{ form_label(transition_form.comment) }}

diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php index f515490b8..ec5fa6ae2 100644 --- a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Tests\Entity\Workflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use PHPUnit\Framework\TestCase; /** @@ -25,7 +26,7 @@ final class EntityWorkflowTest extends TestCase { $entityWorkflow = new EntityWorkflow(); - $entityWorkflow->setStep('final'); + $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow)); $entityWorkflow->getCurrentStep()->setIsFinal(true); $this->assertTrue($entityWorkflow->isFinal()); @@ -37,16 +38,16 @@ final class EntityWorkflowTest extends TestCase $this->assertFalse($entityWorkflow->isFinal()); - $entityWorkflow->setStep('two'); + $entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertFalse($entityWorkflow->isFinal()); - $entityWorkflow->setStep('previous_final'); + $entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertFalse($entityWorkflow->isFinal()); $entityWorkflow->getCurrentStep()->setIsFinal(true); - $entityWorkflow->setStep('final'); + $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertTrue($entityWorkflow->isFinal()); } @@ -57,20 +58,20 @@ final class EntityWorkflowTest extends TestCase $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('step_one'); + $entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('step_three'); + $entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('freezed'); + $entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow)); $entityWorkflow->getCurrentStep()->setFreezeAfter(true); $this->assertTrue($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('after_freeze'); + $entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertTrue($entityWorkflow->isFreeze()); diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php new file mode 100644 index 000000000..a32cefb09 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php @@ -0,0 +1,61 @@ +buildMarkingStore(); + $workflow = new EntityWorkflow(); + + $marking = $markingStore->getMarking($workflow); + + self::assertEquals(['initial' => 1], $marking->getPlaces()); + } + + public function testSetMarking(): void + { + $markingStore = $this->buildMarkingStore(); + $workflow = new EntityWorkflow(); + + $dto = new WorkflowTransitionContextDTO($workflow); + $dto->futureCcUsers[] = $user1 = new User(); + $dto->futureDestUsers[] = $user2 = new User(); + $dto->futureDestEmails[] = $email = 'test@example.com'; + + $markingStore->setMarking($workflow, new Marking(['foo' => 1]), ['context' => $dto]); + + $currentStep = $workflow->getCurrentStep(); + self::assertEquals('foo', $currentStep->getCurrentStep()); + self::assertContains($email, $currentStep->getDestEmail()); + self::assertContains($user1, $currentStep->getCcUser()); + self::assertContains($user2, $currentStep->getDestUser()); + } + + private function buildMarkingStore(): EntityWorkflowMarkingStore + { + return new EntityWorkflowMarkingStore(); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php index 1d185ba0e..e79982e1c 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php @@ -49,6 +49,4 @@ interface EntityWorkflowHandlerInterface public function isObjectSupported(object $object): bool; public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool; - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool; } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php new file mode 100644 index 000000000..dca929e86 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php @@ -0,0 +1,49 @@ +getCurrentStep(); + + return new Marking([$step->getCurrentStep() => 1]); + } + + public function setMarking(object $subject, Marking $marking, array $context = []): void + { + if (!$subject instanceof EntityWorkflow) { + throw new \UnexpectedValueException('Expected instance of EntityWorkflow'); + } + + $places = $marking->getPlaces(); + if (1 < count($places)) { + throw new \LogicException('Expected maximum one place'); + } + $next = array_keys($places)[0]; + + $transitionDTO = $context['context'] ?? null; + if (!$transitionDTO instanceof WorkflowTransitionContextDTO) { + throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class)); + } + + $subject->setStep($next, $transitionDTO); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php index b1fa80458..ce33ea6d0 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php @@ -21,31 +21,13 @@ use Symfony\Component\Workflow\Event\Event; use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\TransitionBlocker; -class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface +final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface { - public function __construct(private readonly LoggerInterface $chillLogger, private readonly Security $security, private readonly UserRender $userRender) {} - - public function addDests(Event $event): void - { - if (!$event->getSubject() instanceof EntityWorkflow) { - return; - } - - /** @var EntityWorkflow $entityWorkflow */ - $entityWorkflow = $event->getSubject(); - - foreach ($entityWorkflow->futureCcUsers as $user) { - $entityWorkflow->getCurrentStep()->addCcUser($user); - } - - foreach ($entityWorkflow->futureDestUsers as $user) { - $entityWorkflow->getCurrentStep()->addDestUser($user); - } - - foreach ($entityWorkflow->futureDestEmails as $email) { - $entityWorkflow->getCurrentStep()->addDestEmail($email); - } - } + public function __construct( + private LoggerInterface $chillLogger, + private Security $security, + private UserRender $userRender + ) {} public static function getSubscribedEvents(): array { @@ -53,7 +35,6 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac 'workflow.transition' => 'onTransition', 'workflow.completed' => [ ['markAsFinal', 2048], - ['addDests', 2048], ], 'workflow.guard' => [ ['guardEntityWorkflow', 0], @@ -99,6 +80,10 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac public function markAsFinal(Event $event): void { + // NOTE: it is not possible to move this method to the marking store, because + // there is dependency between the Workflow definition and the MarkingStoreInterface (the workflow + // constructor need a MarkingStoreInterface) + if (!$event->getSubject() instanceof EntityWorkflow) { return; } diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php index e290a567e..386b2eff6 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php @@ -23,7 +23,13 @@ use Symfony\Component\Workflow\Registry; class NotificationOnTransition implements EventSubscriberInterface { - public function __construct(private readonly EntityManagerInterface $entityManager, private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Security $security, private readonly Registry $registry) {} + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly \Twig\Environment $engine, + private readonly MetadataExtractor $metadataExtractor, + private readonly Security $security, + private readonly Registry $registry + ) {} public static function getSubscribedEvents(): array { @@ -85,7 +91,10 @@ class NotificationOnTransition implements EventSubscriberInterface 'dest' => $subscriber, 'place' => $place, 'workflow' => $workflow, - 'is_dest' => \in_array($subscriber->getId(), array_map(static fn (User $u) => $u->getId(), $entityWorkflow->futureDestUsers), true), + 'is_dest' => \in_array($subscriber->getId(), array_map( + static fn (User $u) => $u->getId(), + $entityWorkflow->getCurrentStep()->getDestUser()->toArray() + ), true), ]; $notification = new Notification(); diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php index 90a6e19db..1a4b573d5 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php @@ -20,7 +20,13 @@ use Symfony\Component\Workflow\Registry; class SendAccessKeyEventSubscriber { - public function __construct(private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MailerInterface $mailer) {} + public function __construct( + private readonly \Twig\Environment $engine, + private readonly MetadataExtractor $metadataExtractor, + private readonly Registry $registry, + private readonly EntityWorkflowManager $entityWorkflowManager, + private readonly MailerInterface $mailer + ) {} public function postPersist(EntityWorkflowStep $step): void { @@ -32,7 +38,7 @@ class SendAccessKeyEventSubscriber ); $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); - foreach ($entityWorkflow->futureDestEmails as $emailAddress) { + foreach ($step->getDestEmail() as $emailAddress) { $context = [ 'entity_workflow' => $entityWorkflow, 'dest' => $emailAddress, diff --git a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php new file mode 100644 index 000000000..2a8253523 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php @@ -0,0 +1,74 @@ +futureDestUsers as $u) { + if (in_array($u, $this->futureCcUsers, true)) { + $context + ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step') + ->atPath('ccUsers') + ->addViolation(); + } + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 0fc13224e..2912d1c01 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -123,9 +123,4 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW { return AccompanyingPeriodWorkEvaluationDocument::class === $entityWorkflow->getRelatedEntityClass(); } - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool - { - return false; - } } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php index 63b34d1dc..a041b3c37 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php @@ -109,9 +109,4 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH { return AccompanyingPeriodWorkEvaluation::class === $entityWorkflow->getRelatedEntityClass(); } - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool - { - return false; - } } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php index 5c74e5b17..ce146a887 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -116,9 +116,4 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte { return AccompanyingPeriodWork::class === $entityWorkflow->getRelatedEntityClass(); } - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool - { - return false; - } }