filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; } /** * handle the step to build a query for an export. * * This action has three steps : * * 1.'export', the export form. When the form is posted, the data is stored * in the session (if valid), and then a redirection is done to next step. * 2. 'formatter', the formatter form. When the form is posted, the data is * stored in the session (if valid), and then a redirection is done to next step. * 3. 'generate': gather data from session from the previous steps, and * make a redirection to the "generate" action with data in query (HTTP GET) */ #[Route(path: '/{_locale}/exports/new/{alias}', name: 'chill_main_export_new')] public function newAction(Request $request, string $alias): Response { // first check for ACL $exportManager = $this->exportManager; $export = $exportManager->getExport($alias); if (false === $exportManager->isGrantedForElement($export)) { throw $this->createAccessDeniedException('The user does not have access to this export'); } $savedExport = $this->getSavedExportFromRequest($request); $step = $request->query->getAlpha('step', 'centers'); return match ($step) { 'centers' => $this->selectCentersStep($request, $export, $alias, $savedExport), 'export' => $this->exportFormStep($request, $export, $alias, $savedExport), 'formatter' => $this->formatterFormStep($request, $export, $alias, $savedExport), 'generate' => $this->forwardToGenerate($request, $export, $alias, $savedExport), default => throw $this->createNotFoundException("The given step '{$step}' is invalid"), }; } /** * create a form to show on different steps. * * @param array $data the data from previous step. Required for steps 'formatter' and 'generate_formatter' */ protected function createCreateFormExport(string $alias, string $step, array $data, ?SavedExport $savedExport): FormInterface { $exportManager = $this->exportManager; $isGenerate = str_starts_with($step, 'generate_'); $canEditFull = $this->security->isGranted(ChillExportVoter::COMPOSE_EXPORT); if (!$canEditFull && null === $savedExport) { throw new AccessDeniedHttpException('The user is not allowed to edit all filter, it should edit only SavedExport'); } $options = match ($step) { 'export', 'generate_export' => [ 'export_alias' => $alias, 'picked_centers' => $this->filterStatsByCenters ? $this->exportFormHelper->getPickedCenters($data) : [], 'can_edit_full' => $canEditFull, 'allowed_filters' => $canEditFull ? null : $this->exportConfigProcessor->retrieveUsedFilters($savedExport->getOptions()['filters']), 'allowed_aggregators' => $canEditFull ? null : $this->exportConfigProcessor->retrieveUsedAggregators($savedExport->getOptions()['aggregators']), ], 'formatter', 'generate_formatter' => [ 'export_alias' => $alias, 'formatter_alias' => $exportManager->getFormatterAlias($data['export']), 'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']['aggregators']), ], default => [ 'export_alias' => $alias, ], }; $defaultFormData = match ($savedExport) { null => $this->exportFormHelper->getDefaultData($step, $exportManager->getExport($alias), $options), default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step), }; $builder = $this->formFactory ->createNamedBuilder( '', FormType::class, 'centers' === $step ? ['centers' => $defaultFormData] : $defaultFormData, [ 'method' => $isGenerate ? Request::METHOD_GET : Request::METHOD_POST, 'csrf_protection' => !$isGenerate, ] ); if ('centers' === $step || 'generate_centers' === $step) { $builder->add('centers', PickCenterType::class, $options); } if ('export' === $step || 'generate_export' === $step) { $builder->add('export', ExportType::class, $options); } if ('formatter' === $step || 'generate_formatter' === $step) { $builder->add('formatter', FormatterType::class, $options); } $builder->add('submit', SubmitType::class, [ 'label' => 'Generate', ]); return $builder->getForm(); } /** * Render the export form. * * When the method is POST, the form is stored if valid, and a redirection * is done to next step. */ private function exportFormStep(Request $request, DirectExportInterface|ExportInterface $export, string $alias, ?SavedExport $savedExport = null): Response { $exportManager = $this->exportManager; // check we have data from the previous step (export step) $data = $this->session->get('centers_step', []); if (null === $data && true === $this->filterStatsByCenters) { return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('export', $export, true), 'alias' => $alias, ]); } $export = $exportManager->getExport($alias); $form = $this->createCreateFormExport($alias, 'export', $data, $savedExport); if (Request::METHOD_POST === $request->getMethod()) { $form->handleRequest($request); if ($form->isValid()) { $this->logger->debug('form export is valid', [ 'location' => __METHOD__, ]); // store data for reusing in next steps $data = $form->getData(); $this->session->set( 'export_step_raw', $request->request->all() ); $this->session->set('export_step', $data); // redirect to next step return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('export', $export), 'alias' => $alias, 'from_saved' => $request->get('from_saved', ''), ]); } $this->logger->debug('form export is invalid', [ 'location' => __METHOD__, ]); } return $this->render('@ChillMain/Export/new.html.twig', [ 'form' => $form->createView(), 'export_alias' => $alias, 'export' => $export, 'export_group' => $this->getExportGroup($export), ]); } /** * Render the form for formatter. * * If the form is posted and valid, store the data in session and * redirect to the next step. */ private function formatterFormStep(Request $request, DirectExportInterface|ExportInterface $export, string $alias, ?SavedExport $savedExport = null): Response { // check we have data from the previous step (export step) $data = $this->session->get('export_step', null); if (null === $data) { return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('formatter', $export, true), 'alias' => $alias, ]); } $form = $this->createCreateFormExport($alias, 'formatter', $data, $savedExport); if (Request::METHOD_POST === $request->getMethod()) { $form->handleRequest($request); if ($form->isValid()) { $dataFormatter = $form->getData(); $this->session->set('formatter_step', $dataFormatter); $this->session->set( 'formatter_step_raw', $request->request->all() ); // redirect to next step return $this->redirectToRoute('chill_main_export_new', [ 'alias' => $alias, 'step' => $this->getNextStep('formatter', $export), 'from_saved' => $request->get('from_saved', ''), ]); } } return $this->render( '@ChillMain/Export/new_formatter_step.html.twig', [ 'form' => $form->createView(), 'export' => $export, 'export_group' => $this->getExportGroup($export), ] ); } /** * Gather data stored in session from previous steps, store it inside redis * and redirect to the `generate` action. * * The data from previous steps is removed from session. */ private function forwardToGenerate(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport): Response { $user = $this->getUser(); if (!$user instanceof User) { throw new AccessDeniedHttpException('only regular users can generate export'); } $dataCenters = $this->session->get('centers_step_raw', null); $dataFormatter = $this->session->get('formatter_step_raw', null); $dataExport = $this->session->get('export_step_raw', null); if (null === $dataFormatter && $export instanceof ExportInterface) { return $this->redirectToRoute('chill_main_export_new', [ 'alias' => $alias, 'step' => $this->getNextStep('generate', $export, true), 'from_saved' => $savedExport?->getId() ?? '', ]); } $dataToNormalize = $this->buildExportDataForNormalization( $alias, $dataCenters, $dataExport, $dataFormatter, $savedExport, ); $deleteAt = $this->clock->now()->add(new \DateInterval('P6M')); $options = $this->exportConfigNormalizer->normalizeConfig($alias, $dataToNormalize); $exportGeneration = match (null === $savedExport) { true => new ExportGeneration($alias, $options, $deleteAt), false => ExportGeneration::fromSavedExport($savedExport, $deleteAt, $options), }; $this->entityManager->persist($exportGeneration); $this->entityManager->flush(); $this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user)); // remove data from session $this->session->remove('centers_step_raw'); $this->session->remove('export_step_raw'); $this->session->remove('export_step'); $this->session->remove('formatter_step_raw'); $this->session->remove('formatter_step'); return $this->redirectToRoute('chill_main_export-generation_wait', ['id' => $exportGeneration->getId()]); } /** * Build the export form data into a way suitable for normalization. * * @param string $alias the export alias * @param array $dataCenters Raw data from center step * @param array $dataExport Raw data from export step * @param array $dataFormatter Raw data from formatter step */ private function buildExportDataForNormalization(string $alias, ?array $dataCenters, array $dataExport, array $dataFormatter, ?SavedExport $savedExport): array { if ($this->filterStatsByCenters) { $formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null); $formCenters->submit($dataCenters); $dataAsCollection = $formCenters->getData()['centers']; $centers = $dataAsCollection['centers']; $regroupments = $dataAsCollection['regroupments'] ?? []; $dataCenters = [ 'centers' => $centers instanceof Collection ? $centers->toArray() : $centers, 'regroupments' => $regroupments instanceof Collection ? $regroupments->toArray() : $regroupments, ]; } else { $dataCenters = ['centers' => [], 'regroupments' => []]; } $formExport = $this->createCreateFormExport($alias, 'generate_export', $dataCenters, $savedExport); $formExport->submit($dataExport); $dataExport = $formExport->getData(); if (\count($dataFormatter) > 0) { $formFormatter = $this->createCreateFormExport( $alias, 'generate_formatter', $dataExport, $savedExport ); $formFormatter->submit($dataFormatter); $dataFormatter = $formFormatter->getData(); } return [ 'centers' => ['centers' => $dataCenters['centers'], 'regroupments' => $dataCenters['regroupments']], 'export' => $dataExport['export']['export'] ?? [], 'filters' => $dataExport['export']['filters'] ?? [], 'aggregators' => $dataExport['export']['aggregators'] ?? [], 'pick_formatter' => $dataExport['export']['pick_formatter']['alias'], 'formatter' => $dataFormatter['formatter'] ?? [], ]; } /** * @param string $alias * * @return Response */ private function selectCentersStep(Request $request, DirectExportInterface|ExportInterface $export, $alias, ExportGeneration|SavedExport|null $savedExport = null) { if (!$this->filterStatsByCenters) { return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('centers', $export), 'alias' => $alias, 'from_saved' => $request->get('from_saved', ''), ]); } /** @var ExportManager $exportManager */ $exportManager = $this->exportManager; $form = $this->createCreateFormExport( $alias, 'centers', $this->exportFormHelper->getDefaultData('centers', $export, []), $savedExport ); if (Request::METHOD_POST === $request->getMethod()) { $form->handleRequest($request); if ($form->isValid()) { $this->logger->debug('form centers is valid', [ 'location' => __METHOD__, ]); $data = $form->getData(); // check ACL if ( false === $exportManager->isGrantedForElement( $export, null, $this->exportFormHelper->getPickedCenters($data['centers']), ) ) { throw $this->createAccessDeniedException('you do not have access to this export for those centers'); } $this->session->set( 'centers_step_raw', $request->request->all() ); $this->session->set('centers_step', $data['centers']); return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('centers', $export), 'alias' => $alias, 'from_saved' => $request->get('from_saved', ''), ]); } } return $this->render( '@ChillMain/Export/new_centers_step.html.twig', [ 'form' => $form->createView(), 'export' => $export, 'export_group' => $this->getExportGroup($export), ] ); } private function getExportGroup($target): string { $exportManager = $this->exportManager; $groups = $exportManager->getExportsGrouped(true); foreach ($groups as $group => $array) { foreach ($array as $alias => $export) { if ($export === $target) { return $group; } } } return ''; } /** * get the next step. If $reverse === true, the previous step is returned. * * This method provides a centralized way of handling next/previous step. * * @param string $step the current step * @param bool $reverse set to true to get the previous step * * @return string the next/current step * * @throws \LogicException if there is no step before or after the given step */ private function getNextStep($step, DirectExportInterface|ExportInterface $export, $reverse = false) { switch ($step) { case 'centers': if (false !== $reverse) { throw new \LogicException("there is no step before 'export'"); } return 'export'; case 'export': if ($export instanceof ExportInterface) { return $reverse ? 'centers' : 'formatter'; } if ($export instanceof DirectExportInterface) { return $reverse ? 'centers' : 'generate'; } // no break case 'formatter': return $reverse ? 'export' : 'generate'; case 'generate': if (false === $reverse) { throw new \LogicException("there is no step after 'generate'"); } return 'formatter'; default: throw new \LogicException("the step {$step} is not defined."); } } private function getSavedExportFromRequest(Request $request): SavedExport|ExportGeneration|null { $savedExport = match ($savedExportId = $request->query->get('from_saved', '')) { '' => null, default => $this->savedExportOrExportGenerationRepository->findById($savedExportId), }; if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::GENERATE, $savedExport)) { throw new AccessDeniedHttpException('saved export generation not allowed'); } return $savedExport; } }