Julien Fastré ad94310981
Refactor workflow hold functionality to avoid relying on database to perform some checks on "user holds entityworkflow"
Simplify the logic in handling workflow on hold status by moving related checks and operations to `EntityWorkflowStep` and `EntityWorkflow` entities. This includes implementing new methods to check if a step or workflow is held by a specific user and refactoring the controller actions to use these methods.
2024-09-05 20:46:29 +02:00

435 lines
17 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
use Chill\MainBundle\Form\WorkflowStepType;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
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 Doctrine\ORM\NonUniqueResultException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\TransitionBlocker;
use Symfony\Contracts\Translation\TranslatorInterface;
class WorkflowController extends AbstractController
{
public function __construct(
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly EntityWorkflowRepository $entityWorkflowRepository,
private readonly ValidatorInterface $validator,
private readonly PaginatorFactory $paginatorFactory,
private readonly Registry $registry,
private readonly EntityManagerInterface $entityManager,
private readonly TranslatorInterface $translator,
private readonly ChillSecurity $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
private readonly ClockInterface $clock,
private readonly EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
) {}
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
public function create(Request $request): Response
{
if (!$request->query->has('entityClass')) {
throw new BadRequestHttpException('Missing entityClass parameter');
}
if (!$request->query->has('entityId')) {
throw new BadRequestHttpException('missing entityId parameter');
}
if (!$request->query->has('workflow')) {
throw new BadRequestHttpException('missing workflow parameter');
}
$entityWorkflow = new EntityWorkflow();
$entityWorkflow
->setRelatedEntityClass($request->query->get('entityClass'))
->setRelatedEntityId($request->query->getInt('entityId'))
->setWorkflowName($request->query->get('workflow'))
->addSubscriberToFinal($this->security->getUser());
$errors = $this->validator->validate($entityWorkflow, null, ['creation']);
if (\count($errors) > 0) {
$msg = [];
foreach ($errors as $error) {
/* @var \Symfony\Component\Validator\ConstraintViolationInterface $error */
$msg[] = $error->getMessage();
}
return new Response(implode("\n", $msg), Response::HTTP_UNPROCESSABLE_ENTITY);
}
$this->denyAccessUnlessGranted(EntityWorkflowVoter::CREATE, $entityWorkflow);
$em = $this->managerRegistry->getManager();
$em->persist($entityWorkflow);
$em->flush();
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
}
#[Route(path: '/{_locale}/main/workflow/{id}/delete', name: 'chill_main_workflow_delete')]
public function delete(EntityWorkflow $entityWorkflow, Request $request): Response
{
$this->denyAccessUnlessGranted(EntityWorkflowVoter::DELETE, $entityWorkflow);
$form = $this->createForm(FormType::class);
$form->add('submit', SubmitType::class, ['label' => 'workflow.Delete workflow']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->remove($entityWorkflow);
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.Workflow deleted with success'));
return $this->redirectToRoute('chill_main_homepage');
}
return $this->render('@ChillMain/Workflow/delete.html.twig', [
'entityWorkflow' => $entityWorkflow,
'delete_form' => $form->createView(),
'handler' => $this->entityWorkflowManager->getHandler($entityWorkflow),
]);
}
#[Route(path: '/{_locale}/main/workflow-step/{id}/access_key', name: 'chill_main_workflow_grant_access_by_key')]
public function getAccessByAccessKey(EntityWorkflowStep $entityWorkflowStep, Request $request): Response
{
if (null === $accessKey = $request->query->get('accessKey', null)) {
throw new BadRequestHttpException('accessKey is missing');
}
if (!$this->getUser() instanceof User) {
throw new AccessDeniedHttpException('Not a valid user');
}
if ($entityWorkflowStep->getAccessKey() !== $accessKey) {
throw new AccessDeniedHttpException('Access key is invalid');
}
if (!$entityWorkflowStep->isWaitingForTransition()) {
$this->addFlash('error', $this->translator->trans('workflow.Steps is not waiting for transition. Maybe someone apply the transition before you ?'));
} else {
$entityWorkflowStep->addDestUserByAccessKey($this->security->getUser());
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.You get access to this step'));
}
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflowStep->getEntityWorkflow()->getId()]);
}
/**
* Previous workflows where the user has applyed a transition.
*/
#[Route(path: '/{_locale}/main/workflow/list/previous_transitionned', name: 'chill_main_workflow_list_previous_transitionned')]
public function myPreviousWorkflowsTransitionned(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByPreviousTransitionned($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByPreviousTransitionned(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'help' => 'workflow.Previous workflow transitionned help',
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'previous_transitionned',
]
);
}
/**
* Previous workflows where the user was mentioned, but did not give any reaction.
*/
#[Route(path: '/{_locale}/main/workflow/list/previous_without_reaction', name: 'chill_main_workflow_list_previous_without_reaction')]
public function myPreviousWorkflowsWithoutReaction(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByPreviousDestWithoutReaction($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByPreviousDestWithoutReaction(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'help' => 'workflow.Previous workflow without reaction help',
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'previous_without_reaction',
]
);
}
#[Route(path: '/{_locale}/main/workflow/list/cc', name: 'chill_main_workflow_list_cc')]
public function myWorkflowsCc(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByDest($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByCc(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'cc',
]
);
}
#[Route(path: '/{_locale}/main/workflow/list/dest', name: 'chill_main_workflow_list_dest')]
public function myWorkflowsDest(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByDest($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByDest(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'dest',
]
);
}
#[Route(path: '/{_locale}/main/workflow/list/subscribed', name: 'chill_main_workflow_list_subscribed')]
public function myWorkflowsSubscribed(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findBySubscriber(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'subscribed',
]
);
}
/**
* @throws NonUniqueResultException
*/
#[Route(path: '/{_locale}/main/workflow/{id}/show', name: 'chill_main_workflow_show')]
public function show(EntityWorkflow $entityWorkflow, Request $request): Response
{
$this->denyAccessUnlessGranted(EntityWorkflowVoter::SEE, $entityWorkflow);
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$errors = [];
$signatures = $entityWorkflow->getCurrentStep()->getSignatures();
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
// possible transition
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
$usersInvolved = $entityWorkflow->getUsersInvolved();
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
if (false !== $currentUserFound) {
unset($usersInvolved[$currentUserFound]);
}
$transitionForm = $this->createForm(
WorkflowStepType::class,
$stepDTO,
[
'entity_workflow' => $entityWorkflow,
'suggested_users' => $usersInvolved,
]
);
$transitionForm->handleRequest($request);
if ($transitionForm->isSubmitted() && $transitionForm->isValid()) {
if (!$workflow->can($entityWorkflow, $transition = $transitionForm['transition']->getData()->getName())) {
$blockers = $workflow->buildTransitionBlockerList($entityWorkflow, $transition);
$msgs = array_map(fn (TransitionBlocker $tb) => $this->translator->trans(
$tb->getMessage(),
$tb->getParameters()
), iterator_to_array($blockers));
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
}
$byUser = $this->security->getUser();
$workflow->apply($entityWorkflow, $transition, [
'context' => $stepDTO,
'byUser' => $byUser,
'transition' => $transition,
'transitionAt' => $this->clock->now(),
]);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
}
if ($transitionForm->isSubmitted() && !$transitionForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
}
return $this->render(
'@ChillMain/Workflow/index.html.twig',
[
'handler' => $handler,
'handler_template' => $handler->getTemplate($entityWorkflow),
'handler_template_data' => $handler->getTemplateData($entityWorkflow),
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
'entity_workflow' => $entityWorkflow,
'transition_form_errors' => $errors,
'signatures' => $signatures,
]
);
}
private function buildHandler(array $workflows): array
{
$lines = [];
foreach ($workflows as $workflow) {
$handler = $this->entityWorkflowManager->getHandler($workflow);
$lines[] = [
'handler' => $handler,
'entity_workflow' => $workflow,
];
}
return $lines;
}
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')]
public function addSignatureMetadata(int $signature_id, Request $request): Response
{
$signature = $this->entityWorkflowStepSignatureRepository->find($signature_id);
if (null === $signature) {
throw new NotFoundHttpException('signature not found');
}
if ($signature->isSigned()) {
$this->addFlash(
'notice',
$this->translator->trans('workflow.signature_zone.already_signed_alert')
);
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]);
}
if ($signature->getSigner() instanceof User) {
return $this->redirectToRoute('chill_main_workflow_signature_add', ['id' => $signature_id]);
}
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
$metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
$metadataForm->handleRequest($request);
if ($metadataForm->isSubmitted() && $metadataForm->isValid()) {
$data = $metadataForm->getData();
$signature->setSignatureMetadata(
[
'base_signer' => [
'document_type' => $data['documentType'],
'document_number' => $data['documentNumber'],
'expiration_date' => $data['expirationDate'],
],
]
);
$this->entityManager->persist($signature);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_workflow_signature_add', ['id' => $signature_id]);
}
return $this->render(
'@ChillMain/Workflow/_signature_metadata.html.twig',
[
'metadata_form' => $metadataForm->createView(),
'person' => $signature->getSigner(),
]
);
}
}