mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 01:08:26 +00:00 
			
		
		
		
	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
```
			
			
This commit is contained in:
		| @@ -121,9 +121,4 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler | ||||
|     { | ||||
|         return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass(); | ||||
|     } | ||||
|  | ||||
|     public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool | ||||
|     { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
| @@ -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<EntityWorkflowComment> | ||||
|      */ | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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(); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 ), | ||||
|             ]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -58,17 +58,15 @@ | ||||
|         {{ form_row(transition_form.transition) }} | ||||
|     </div> | ||||
|  | ||||
|     {% if transition_form.freezeAfter is defined %} | ||||
|         {{ form_row(transition_form.freezeAfter) }} | ||||
|     {% endif %} | ||||
|  | ||||
|     <div id="futureDests"> | ||||
|         {{ 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) }} | ||||
|     </div> | ||||
|  | ||||
|     <p>{{ form_label(transition_form.comment) }}</p> | ||||
|   | ||||
| @@ -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()); | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,61 @@ | ||||
| <?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\Tests\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore; | ||||
| use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Symfony\Component\Workflow\Marking; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class EntityWorkflowMarkingStoreTest extends TestCase | ||||
| { | ||||
|     public function testGetMarking(): void | ||||
|     { | ||||
|         $markingStore = $this->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(); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,49 @@ | ||||
| <?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\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Symfony\Component\Workflow\Marking; | ||||
| use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface; | ||||
|  | ||||
| final readonly class EntityWorkflowMarkingStore implements MarkingStoreInterface | ||||
| { | ||||
|     public function getMarking(object $subject): Marking | ||||
|     { | ||||
|         if (!$subject instanceof EntityWorkflow) { | ||||
|             throw new \UnexpectedValueException('Expected instance of EntityWorkflow'); | ||||
|         } | ||||
|         $step = $subject->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); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|         } | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -0,0 +1,74 @@ | ||||
| <?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\Workflow; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Symfony\Component\Validator\Constraints as Assert; | ||||
| use Symfony\Component\Validator\Context\ExecutionContextInterface; | ||||
| use Symfony\Component\Workflow\Transition; | ||||
|  | ||||
| /** | ||||
|  * Context for a transition on an workflow entity. | ||||
|  */ | ||||
| class WorkflowTransitionContextDTO | ||||
| { | ||||
|     /** | ||||
|      * 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 = []; | ||||
|  | ||||
|     /** | ||||
|      * 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 = []; | ||||
|  | ||||
|     public ?Transition $transition = null; | ||||
|  | ||||
|     public string $comment = ''; | ||||
|  | ||||
|     public function __construct( | ||||
|         public EntityWorkflow $entityWorkflow | ||||
|     ) {} | ||||
|  | ||||
|     #[Assert\Callback()] | ||||
|     public function validateCCUserIsNotInDest(ExecutionContextInterface $context, $payload): void | ||||
|     { | ||||
|         foreach ($this->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(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -123,9 +123,4 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW | ||||
|     { | ||||
|         return AccompanyingPeriodWorkEvaluationDocument::class === $entityWorkflow->getRelatedEntityClass(); | ||||
|     } | ||||
|  | ||||
|     public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool | ||||
|     { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -109,9 +109,4 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH | ||||
|     { | ||||
|         return AccompanyingPeriodWorkEvaluation::class === $entityWorkflow->getRelatedEntityClass(); | ||||
|     } | ||||
|  | ||||
|     public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool | ||||
|     { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -116,9 +116,4 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte | ||||
|     { | ||||
|         return AccompanyingPeriodWork::class === $entityWorkflow->getRelatedEntityClass(); | ||||
|     } | ||||
|  | ||||
|     public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool | ||||
|     { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user