vendor/easycorp/easyadmin-bundle/src/Controller/AbstractCrudController.php line 117

  1. <?php
  2. namespace EasyCorp\Bundle\EasyAdminBundle\Controller;
  3. use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Doctrine\ORM\QueryBuilder;
  6. use Doctrine\Persistence\ManagerRegistry;
  7. use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
  8. use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
  9. use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
  10. use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
  11. use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
  12. use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
  13. use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
  14. use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
  15. use EasyCorp\Bundle\EasyAdminBundle\Config\Option\EA;
  16. use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
  17. use EasyCorp\Bundle\EasyAdminBundle\Contracts\Controller\CrudControllerInterface;
  18. use EasyCorp\Bundle\EasyAdminBundle\Dto\AssetsDto;
  19. use EasyCorp\Bundle\EasyAdminBundle\Dto\BatchActionDto;
  20. use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
  21. use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
  22. use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
  23. use EasyCorp\Bundle\EasyAdminBundle\Event\AfterCrudActionEvent;
  24. use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityDeletedEvent;
  25. use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityPersistedEvent;
  26. use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityUpdatedEvent;
  27. use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeCrudActionEvent;
  28. use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityDeletedEvent;
  29. use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
  30. use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
  31. use EasyCorp\Bundle\EasyAdminBundle\Exception\EntityRemoveException;
  32. use EasyCorp\Bundle\EasyAdminBundle\Exception\ForbiddenActionException;
  33. use EasyCorp\Bundle\EasyAdminBundle\Exception\InsufficientEntityPermissionException;
  34. use EasyCorp\Bundle\EasyAdminBundle\Factory\ActionFactory;
  35. use EasyCorp\Bundle\EasyAdminBundle\Factory\ControllerFactory;
  36. use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
  37. use EasyCorp\Bundle\EasyAdminBundle\Factory\FieldFactory;
  38. use EasyCorp\Bundle\EasyAdminBundle\Factory\FilterFactory;
  39. use EasyCorp\Bundle\EasyAdminBundle\Factory\FormFactory;
  40. use EasyCorp\Bundle\EasyAdminBundle\Factory\PaginatorFactory;
  41. use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
  42. use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
  43. use EasyCorp\Bundle\EasyAdminBundle\Form\Type\FileUploadType;
  44. use EasyCorp\Bundle\EasyAdminBundle\Form\Type\FiltersFormType;
  45. use EasyCorp\Bundle\EasyAdminBundle\Form\Type\Model\FileUploadState;
  46. use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
  47. use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityUpdater;
  48. use EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider;
  49. use EasyCorp\Bundle\EasyAdminBundle\Provider\FieldProvider;
  50. use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
  51. use EasyCorp\Bundle\EasyAdminBundle\Security\Permission;
  52. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  53. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  54. use Symfony\Component\Form\FormBuilderInterface;
  55. use Symfony\Component\Form\FormInterface;
  56. use Symfony\Component\HttpFoundation\JsonResponse;
  57. use Symfony\Component\HttpFoundation\RedirectResponse;
  58. use Symfony\Component\HttpFoundation\Response;
  59. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  60. use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
  61. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  62. use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
  63. use function Symfony\Component\String\u;
  64. /**
  65.  * @author Javier Eguiluz <javier.eguiluz@gmail.com>
  66.  *
  67.  * @template TEntity of object
  68.  *
  69.  * @implements  CrudControllerInterface<TEntity>
  70.  */
  71. abstract class AbstractCrudController extends AbstractController implements CrudControllerInterface
  72. {
  73.     abstract public static function getEntityFqcn(): string;
  74.     public function configureCrud(Crud $crud): Crud
  75.     {
  76.         return $crud;
  77.     }
  78.     public function configureAssets(Assets $assets): Assets
  79.     {
  80.         return $assets;
  81.     }
  82.     public function configureActions(Actions $actions): Actions
  83.     {
  84.         return $actions;
  85.     }
  86.     public function configureFilters(Filters $filters): Filters
  87.     {
  88.         return $filters;
  89.     }
  90.     public function configureFields(string $pageName): iterable
  91.     {
  92.         return $this->container->get(FieldProvider::class)->getDefaultFields($pageName);
  93.     }
  94.     public static function getSubscribedServices(): array
  95.     {
  96.         return array_merge(parent::getSubscribedServices(), [
  97.             'doctrine' => '?'.ManagerRegistry::class,
  98.             'event_dispatcher' => '?'.EventDispatcherInterface::class,
  99.             ActionFactory::class => '?'.ActionFactory::class,
  100.             AdminContextProvider::class => '?'.AdminContextProvider::class,
  101.             AdminUrlGenerator::class => '?'.AdminUrlGenerator::class,
  102.             ControllerFactory::class => '?'.ControllerFactory::class,
  103.             EntityFactory::class => '?'.EntityFactory::class,
  104.             EntityRepository::class => '?'.EntityRepository::class,
  105.             EntityUpdater::class => '?'.EntityUpdater::class,
  106.             FieldProvider::class => '?'.FieldProvider::class,
  107.             FilterFactory::class => '?'.FilterFactory::class,
  108.             FormFactory::class => '?'.FormFactory::class,
  109.             PaginatorFactory::class => '?'.PaginatorFactory::class,
  110.             FieldFactory::class => '?'.FieldFactory::class,
  111.         ]);
  112.     }
  113.     public function index(AdminContext $context): KeyValueStore|Response
  114.     {
  115.         $event = new BeforeCrudActionEvent($context);
  116.         $this->container->get('event_dispatcher')->dispatch($event);
  117.         if ($event->isPropagationStopped()) {
  118.             return $event->getResponse();
  119.         }
  120.         if (!$this->isGranted(Permission::EA_EXECUTE_ACTION, ['action' => Action::INDEX'entity' => null'entityFqcn' => $context->getEntity()->getFqcn()])) {
  121.             throw new ForbiddenActionException($context);
  122.         }
  123.         $fields = new FieldCollection($this->configureFields(Crud::PAGE_INDEX));
  124.         $filters $this->container->get(FilterFactory::class)->create($context->getCrud()->getFiltersConfig(), $fields$context->getEntity());
  125.         $queryBuilder $this->createIndexQueryBuilder($context->getSearch(), $context->getEntity(), $fields$filters);
  126.         $paginator $this->container->get(PaginatorFactory::class)->create($queryBuilder);
  127.         // this can happen after deleting some items and trying to return
  128.         // to a 'index' page that no longer exists. Redirect to the last page instead
  129.         if ($paginator->isOutOfRange()) {
  130.             return $this->redirect($this->container->get(AdminUrlGenerator::class)
  131.                 ->set(EA::PAGE$paginator->getLastPage())
  132.                 ->generateUrl());
  133.         }
  134.         $entities $this->container->get(EntityFactory::class)->createCollection($context->getEntity(), $paginator->getResults());
  135.         $this->container->get(FieldFactory::class)->processFieldsForAll($entities$fieldsCrud::PAGE_INDEX);
  136.         $processedFields $entities->first()?->getFields() ?? new FieldCollection([]);
  137.         $context->getCrud()->setFieldAssets($this->getFieldAssets($processedFields));
  138.         $actions $this->container->get(ActionFactory::class)->processGlobalActionsAndEntityActionsForAll($entities$context->getCrud()->getActionsConfig());
  139.         $responseParameters $this->configureResponseParameters(KeyValueStore::new([
  140.             'pageName' => Crud::PAGE_INDEX,
  141.             'templateName' => 'crud/index',
  142.             'entities' => $entities,
  143.             'paginator' => $paginator,
  144.             'global_actions' => $actions->getGlobalActions(),
  145.             'batch_actions' => $actions->getBatchActions(),
  146.             'filters' => $filters,
  147.         ]));
  148.         $event = new AfterCrudActionEvent($context$responseParameters);
  149.         $this->container->get('event_dispatcher')->dispatch($event);
  150.         if ($event->isPropagationStopped()) {
  151.             return $event->getResponse();
  152.         }
  153.         return $responseParameters;
  154.     }
  155.     public function detail(AdminContext $context): KeyValueStore|Response
  156.     {
  157.         $event = new BeforeCrudActionEvent($context);
  158.         $this->container->get('event_dispatcher')->dispatch($event);
  159.         if ($event->isPropagationStopped()) {
  160.             return $event->getResponse();
  161.         }
  162.         if (!$this->isGranted(Permission::EA_EXECUTE_ACTION, ['action' => Action::DETAIL'entity' => $context->getEntity(), 'entityFqcn' => $context->getEntity()->getFqcn()])) {
  163.             throw new ForbiddenActionException($context);
  164.         }
  165.         if (!$context->getEntity()->isAccessible()) {
  166.             throw new InsufficientEntityPermissionException($context);
  167.         }
  168.         $this->container->get(FieldFactory::class)->processFields($context->getEntity(), new FieldCollection($this->configureFields(Crud::PAGE_DETAIL)), Crud::PAGE_DETAIL);
  169.         $context->getCrud()->setFieldAssets($this->getFieldAssets($context->getEntity()->getFields()));
  170.         $this->container->get(ActionFactory::class)->processEntityActions($context->getEntity(), $context->getCrud()->getActionsConfig());
  171.         $responseParameters $this->configureResponseParameters(KeyValueStore::new([
  172.             'pageName' => Crud::PAGE_DETAIL,
  173.             'templateName' => 'crud/detail',
  174.             'entity' => $context->getEntity(),
  175.         ]));
  176.         $event = new AfterCrudActionEvent($context$responseParameters);
  177.         $this->container->get('event_dispatcher')->dispatch($event);
  178.         if ($event->isPropagationStopped()) {
  179.             return $event->getResponse();
  180.         }
  181.         return $responseParameters;
  182.     }
  183.     public function edit(AdminContext $context): KeyValueStore|Response
  184.     {
  185.         $event = new BeforeCrudActionEvent($context);
  186.         $this->container->get('event_dispatcher')->dispatch($event);
  187.         if ($event->isPropagationStopped()) {
  188.             return $event->getResponse();
  189.         }
  190.         if (!$this->isGranted(Permission::EA_EXECUTE_ACTION, ['action' => Action::EDIT'entity' => $context->getEntity(), 'entityFqcn' => $context->getEntity()->getFqcn()])) {
  191.             throw new ForbiddenActionException($context);
  192.         }
  193.         if (!$context->getEntity()->isAccessible()) {
  194.             throw new InsufficientEntityPermissionException($context);
  195.         }
  196.         $this->container->get(FieldFactory::class)->processFields($context->getEntity(), new FieldCollection($this->configureFields(Crud::PAGE_EDIT)), Crud::PAGE_EDIT);
  197.         $context->getCrud()->setFieldAssets($this->getFieldAssets($context->getEntity()->getFields()));
  198.         $this->container->get(ActionFactory::class)->processEntityActions($context->getEntity(), $context->getCrud()->getActionsConfig());
  199.         /** @var TEntity $entityInstance */
  200.         $entityInstance $context->getEntity()->getInstance();
  201.         if ($context->getRequest()->isXmlHttpRequest()) {
  202.             if ('PATCH' !== $context->getRequest()->getMethod()) {
  203.                 throw new MethodNotAllowedHttpException(['PATCH']);
  204.             }
  205.             if (!$this->isCsrfTokenValid(BooleanField::CSRF_TOKEN_NAME$context->getRequest()->query->get('csrfToken'))) {
  206.                 throw new InvalidCsrfTokenException();
  207.             }
  208.             $fieldName $context->getRequest()->query->get('fieldName');
  209.             $newValue 'true' === mb_strtolower($context->getRequest()->query->get('newValue'));
  210.             try {
  211.                 $event $this->ajaxEdit($context->getEntity(), $fieldName$newValue);
  212.             } catch (\Exception $e) {
  213.                 throw new BadRequestHttpException($e->getMessage());
  214.             }
  215.             if ($event->isPropagationStopped()) {
  216.                 return $event->getResponse();
  217.             }
  218.             return new Response($newValue '1' '0');
  219.         }
  220.         $editForm $this->createEditForm($context->getEntity(), $context->getCrud()->getEditFormOptions(), $context);
  221.         $editForm->handleRequest($context->getRequest());
  222.         if ($editForm->isSubmitted() && $editForm->isValid()) {
  223.             $this->processUploadedFiles($editForm);
  224.             $event = new BeforeEntityUpdatedEvent($entityInstance);
  225.             $this->container->get('event_dispatcher')->dispatch($event);
  226.             $entityInstance $event->getEntityInstance();
  227.             $this->updateEntity($this->container->get('doctrine')->getManagerForClass($context->getEntity()->getFqcn()), $entityInstance);
  228.             $this->container->get('event_dispatcher')->dispatch(new AfterEntityUpdatedEvent($entityInstance));
  229.             return $this->getRedirectResponseAfterSave($contextAction::EDIT);
  230.         }
  231.         $responseParameters $this->configureResponseParameters(KeyValueStore::new([
  232.             'pageName' => Crud::PAGE_EDIT,
  233.             'templateName' => 'crud/edit',
  234.             'edit_form' => $editForm,
  235.             'entity' => $context->getEntity(),
  236.         ]));
  237.         $event = new AfterCrudActionEvent($context$responseParameters);
  238.         $this->container->get('event_dispatcher')->dispatch($event);
  239.         if ($event->isPropagationStopped()) {
  240.             return $event->getResponse();
  241.         }
  242.         return $responseParameters;
  243.     }
  244.     public function new(AdminContext $context): KeyValueStore|Response
  245.     {
  246.         $event = new BeforeCrudActionEvent($context);
  247.         $this->container->get('event_dispatcher')->dispatch($event);
  248.         if ($event->isPropagationStopped()) {
  249.             return $event->getResponse();
  250.         }
  251.         if (!$this->isGranted(Permission::EA_EXECUTE_ACTION, ['action' => Action::NEW, 'entity' => null'entityFqcn' => $context->getEntity()->getFqcn()])) {
  252.             throw new ForbiddenActionException($context);
  253.         }
  254.         if (!$context->getEntity()->isAccessible()) {
  255.             throw new InsufficientEntityPermissionException($context);
  256.         }
  257.         /** @var class-string<TEntity> $entityFqcn */
  258.         $entityFqcn $context->getEntity()->getFqcn();
  259.         $context->getEntity()->setInstance($this->createEntity($entityFqcn));
  260.         $this->container->get(FieldFactory::class)->processFields($context->getEntity(), new FieldCollection($this->configureFields(Crud::PAGE_NEW)), Crud::PAGE_NEW);
  261.         $context->getCrud()->setFieldAssets($this->getFieldAssets($context->getEntity()->getFields()));
  262.         $this->container->get(ActionFactory::class)->processEntityActions($context->getEntity(), $context->getCrud()->getActionsConfig());
  263.         $newForm $this->createNewForm($context->getEntity(), $context->getCrud()->getNewFormOptions(), $context);
  264.         $newForm->handleRequest($context->getRequest());
  265.         /** @var TEntity $entityInstance */
  266.         $entityInstance $newForm->getData();
  267.         $context->getEntity()->setInstance($entityInstance);
  268.         if ($newForm->isSubmitted() && $newForm->isValid()) {
  269.             $this->processUploadedFiles($newForm);
  270.             $event = new BeforeEntityPersistedEvent($entityInstance);
  271.             $this->container->get('event_dispatcher')->dispatch($event);
  272.             $entityInstance $event->getEntityInstance();
  273.             $this->persistEntity($this->container->get('doctrine')->getManagerForClass($context->getEntity()->getFqcn()), $entityInstance);
  274.             $this->container->get('event_dispatcher')->dispatch(new AfterEntityPersistedEvent($entityInstance));
  275.             $context->getEntity()->setInstance($entityInstance);
  276.             return $this->getRedirectResponseAfterSave($contextAction::NEW);
  277.         }
  278.         $responseParameters $this->configureResponseParameters(KeyValueStore::new([
  279.             'pageName' => Crud::PAGE_NEW,
  280.             'templateName' => 'crud/new',
  281.             'entity' => $context->getEntity(),
  282.             'new_form' => $newForm,
  283.         ]));
  284.         $event = new AfterCrudActionEvent($context$responseParameters);
  285.         $this->container->get('event_dispatcher')->dispatch($event);
  286.         if ($event->isPropagationStopped()) {
  287.             return $event->getResponse();
  288.         }
  289.         return $responseParameters;
  290.     }
  291.     public function delete(AdminContext $context): KeyValueStore|Response
  292.     {
  293.         $event = new BeforeCrudActionEvent($context);
  294.         $this->container->get('event_dispatcher')->dispatch($event);
  295.         if ($event->isPropagationStopped()) {
  296.             return $event->getResponse();
  297.         }
  298.         if (!$this->isGranted(Permission::EA_EXECUTE_ACTION, ['action' => Action::DELETE'entity' => $context->getEntity(), 'entityFqcn' => $context->getEntity()->getFqcn()])) {
  299.             throw new ForbiddenActionException($context);
  300.         }
  301.         if (!$context->getEntity()->isAccessible()) {
  302.             throw new InsufficientEntityPermissionException($context);
  303.         }
  304.         $csrfToken $context->getRequest()->request->has('token')
  305.             ? (string) $context->getRequest()->request->get('token')
  306.             : null;
  307.         if ($this->container->has('security.csrf.token_manager') && !$this->isCsrfTokenValid('ea-delete'$csrfToken)) {
  308.             return $this->redirectToRoute($context->getDashboardRouteName());
  309.         }
  310.         /** @var TEntity $entityInstance */
  311.         $entityInstance $context->getEntity()->getInstance();
  312.         $event = new BeforeEntityDeletedEvent($entityInstance);
  313.         $this->container->get('event_dispatcher')->dispatch($event);
  314.         if ($event->isPropagationStopped()) {
  315.             return $event->getResponse();
  316.         }
  317.         $entityInstance $event->getEntityInstance();
  318.         try {
  319.             $this->deleteEntity($this->container->get('doctrine')->getManagerForClass($context->getEntity()->getFqcn()), $entityInstance);
  320.         } catch (ForeignKeyConstraintViolationException $e) {
  321.             throw new EntityRemoveException(['entity_name' => $context->getEntity()->getName(), 'message' => $e->getMessage()], $e);
  322.         }
  323.         $this->container->get('event_dispatcher')->dispatch(new AfterEntityDeletedEvent($entityInstance));
  324.         $responseParameters $this->configureResponseParameters(KeyValueStore::new([
  325.             'entity' => $context->getEntity(),
  326.         ]));
  327.         $event = new AfterCrudActionEvent($context$responseParameters);
  328.         $this->container->get('event_dispatcher')->dispatch($event);
  329.         if ($event->isPropagationStopped()) {
  330.             return $event->getResponse();
  331.         }
  332.         return $this->redirect($this->container->get(AdminUrlGenerator::class)->setController($context->getCrud()->getControllerFqcn())->setAction(Action::INDEX)->unset(EA::ENTITY_ID)->generateUrl());
  333.     }
  334.     /**
  335.      * @param BatchActionDto<TEntity> $batchActionDto
  336.      */
  337.     public function batchDelete(AdminContext $contextBatchActionDto $batchActionDto): Response
  338.     {
  339.         $event = new BeforeCrudActionEvent($context);
  340.         $this->container->get('event_dispatcher')->dispatch($event);
  341.         if ($event->isPropagationStopped()) {
  342.             return $event->getResponse();
  343.         }
  344.         if (!$this->isCsrfTokenValid('ea-batch-action-'.Action::BATCH_DELETE$batchActionDto->getCsrfToken())) {
  345.             return $this->redirectToRoute($context->getDashboardRouteName());
  346.         }
  347.         /** @var EntityManagerInterface $entityManager */
  348.         $entityManager $this->container->get('doctrine')->getManagerForClass($batchActionDto->getEntityFqcn());
  349.         $repository $entityManager->getRepository($batchActionDto->getEntityFqcn());
  350.         foreach ($batchActionDto->getEntityIds() as $entityId) {
  351.             $entityInstance $repository->find($entityId);
  352.             if (null === $entityInstance) {
  353.                 continue;
  354.             }
  355.             $entityDto $context->getEntity()->newWithInstance($entityInstance);
  356.             if (!$this->isGranted(Permission::EA_EXECUTE_ACTION, ['action' => Action::DELETE'entity' => $entityDto'entityFqcn' => $context->getEntity()->getFqcn()])) {
  357.                 throw new ForbiddenActionException($context);
  358.             }
  359.             if (!$entityDto->isAccessible()) {
  360.                 throw new InsufficientEntityPermissionException($context);
  361.             }
  362.             $event = new BeforeEntityDeletedEvent($entityInstance);
  363.             $this->container->get('event_dispatcher')->dispatch($event);
  364.             if ($event->isPropagationStopped()) {
  365.                 return $event->getResponse();
  366.             }
  367.             $entityInstance $event->getEntityInstance();
  368.             try {
  369.                 $this->deleteEntity($entityManager$entityInstance);
  370.             } catch (ForeignKeyConstraintViolationException $e) {
  371.                 throw new EntityRemoveException(['entity_name' => (string) $entityDto'message' => $e->getMessage()], $e);
  372.             }
  373.             $this->container->get('event_dispatcher')->dispatch(new AfterEntityDeletedEvent($entityInstance));
  374.         }
  375.         $responseParameters $this->configureResponseParameters(KeyValueStore::new([
  376.             'entity' => $context->getEntity(),
  377.             'batchActionDto' => $batchActionDto,
  378.         ]));
  379.         $event = new AfterCrudActionEvent($context$responseParameters);
  380.         $this->container->get('event_dispatcher')->dispatch($event);
  381.         if ($event->isPropagationStopped()) {
  382.             return $event->getResponse();
  383.         }
  384.         $redirectUrl $this->container->get(AdminUrlGenerator::class)
  385.             // reset the page number to avoid confusing elements after the page reload
  386.             // (we're deleting items, so the original listing pages will change)
  387.             ->unset(EA::PAGE)
  388.             ->setController($context->getCrud()->getControllerFqcn())
  389.             ->setAction(Action::INDEX)
  390.             ->generateUrl();
  391.         return $this->redirect($redirectUrl);
  392.     }
  393.     public function autocomplete(AdminContext $context): JsonResponse
  394.     {
  395.         $queryBuilder $this->createIndexQueryBuilder($context->getSearch(), $context->getEntity(), new FieldCollection([]), new FilterCollection());
  396.         $autocompleteContext $context->getRequest()->query->all(AssociationField::PARAM_AUTOCOMPLETE_CONTEXT);
  397.         if (!isset($autocompleteContext['originatingPage'], $autocompleteContext['propertyName'])) {
  398.             throw new \RuntimeException('Invalid autocomplete context: missing required parameters "originatingPage" or "propertyName".');
  399.         }
  400.         $crudControllerFqcn $autocompleteContext[EA::CRUD_CONTROLLER_FQCN] ?? $context->getRequest()->attributes->get(EA::CRUD_CONTROLLER_FQCN) ?? $context->getRequest()->query->get(EA::CRUD_CONTROLLER_FQCN);
  401.         /** @var CrudControllerInterface $controller */
  402.         $controller $this->container->get(ControllerFactory::class)->getCrudControllerInstance($crudControllerFqcnAction::INDEX$context->getRequest());
  403.         $originatingPage $autocompleteContext['originatingPage'];
  404.         $propertyName $autocompleteContext['propertyName'];
  405.         $fields = new FieldCollection($controller->configureFields($originatingPage));
  406.         // find the first field with matching property that is displayed on the originating page;
  407.         // this ensures we get the correct field when a controller defines more than one field for the
  408.         // same property, each of them displayed only on certain pages (via hideOnIndex(), onlyOnForms(), etc.)
  409.         /** @var FieldDto|null $field */
  410.         $field null;
  411.         foreach ($fields as $fieldDto) {
  412.             if ($propertyName === $fieldDto->getProperty() && $fieldDto->isDisplayedOn($originatingPage)) {
  413.                 $field $fieldDto;
  414.                 break;
  415.             }
  416.         }
  417.         /** @var \Closure|null $queryBuilderCallable */
  418.         $queryBuilderCallable $field?->getCustomOption(AssociationField::OPTION_QUERY_BUILDER_CALLABLE);
  419.         if (null !== $queryBuilderCallable) {
  420.             $queryBuilder $queryBuilderCallable($queryBuilder) ?? $queryBuilder;
  421.         }
  422.         $callback $field?->getCustomOption(AssociationField::OPTION_AUTOCOMPLETE_CALLBACK)
  423.             ?? $context->getCrud()?->getAutocompleteCallback();
  424.         $template $field?->getCustomOption(AssociationField::OPTION_AUTOCOMPLETE_TEMPLATE)
  425.             ?? $context->getCrud()?->getAutocompleteTemplate();
  426.         // at field-level, the option is OPTION_ESCAPE_HTML_CONTENTS, which is "render as HTML" inverted
  427.         $fieldEscapeHtml $field?->getCustomOption(AssociationField::OPTION_ESCAPE_HTML_CONTENTS);
  428.         $renderAsHtml = (null !== $fieldEscapeHtml && false === $fieldEscapeHtml)
  429.             || (null === $fieldEscapeHtml && $context->getCrud()?->getAutocompleteRenderAsHtml());
  430.         $paginator $this->container->get(PaginatorFactory::class)->create($queryBuilder);
  431.         return JsonResponse::fromJsonString($paginator->getResultsAsJson($callback$template$renderAsHtml));
  432.     }
  433.     public function createIndexQueryBuilder(SearchDto $searchDtoEntityDto $entityDtoFieldCollection $fieldsFilterCollection $filters): QueryBuilder
  434.     {
  435.         return $this->container->get(EntityRepository::class)->createQueryBuilder($searchDto$entityDto$fields$filters);
  436.     }
  437.     public function renderFilters(AdminContext $context): KeyValueStore
  438.     {
  439.         $fields = new FieldCollection($this->configureFields(Crud::PAGE_INDEX));
  440.         $this->container->get(FieldFactory::class)->processFields($context->getEntity(), $fieldsCrud::PAGE_INDEX);
  441.         $filters $this->container->get(FilterFactory::class)->create($context->getCrud()->getFiltersConfig(), $context->getEntity()->getFields(), $context->getEntity());
  442.         /** @var FormInterface&FiltersFormType $filtersForm */
  443.         $filtersForm $this->container->get(FormFactory::class)->createFiltersForm($filters$context->getRequest());
  444.         $formActionParts parse_url($filtersForm->getConfig()->getAction());
  445.         $queryString $formActionParts[EA::QUERY] ?? '';
  446.         parse_str($queryString$queryStringAsArray);
  447.         unset($queryStringAsArray[EA::FILTERS], $queryStringAsArray[EA::PAGE]);
  448.         $responseParameters KeyValueStore::new([
  449.             'templateName' => 'crud/filters',
  450.             'filters_form' => $filtersForm,
  451.             'form_action_query_string_as_array' => $queryStringAsArray,
  452.         ]);
  453.         return $this->configureResponseParameters($responseParameters);
  454.     }
  455.     public function createEntity(string $entityFqcn): object
  456.     {
  457.         return new $entityFqcn();
  458.     }
  459.     public function updateEntity(EntityManagerInterface $entityManagerobject $entityInstance): void
  460.     {
  461.         $entityManager->persist($entityInstance);
  462.         $entityManager->flush();
  463.     }
  464.     public function persistEntity(EntityManagerInterface $entityManagerobject $entityInstance): void
  465.     {
  466.         $entityManager->persist($entityInstance);
  467.         $entityManager->flush();
  468.     }
  469.     public function deleteEntity(EntityManagerInterface $entityManagerobject $entityInstance): void
  470.     {
  471.         $entityManager->remove($entityInstance);
  472.         $entityManager->flush();
  473.     }
  474.     public function createEditForm(EntityDto $entityDtoKeyValueStore $formOptionsAdminContext $context): FormInterface
  475.     {
  476.         return $this->createEditFormBuilder($entityDto$formOptions$context)->getForm();
  477.     }
  478.     public function createEditFormBuilder(EntityDto $entityDtoKeyValueStore $formOptionsAdminContext $context): FormBuilderInterface
  479.     {
  480.         return $this->container->get(FormFactory::class)->createEditFormBuilder($entityDto$formOptions$context);
  481.     }
  482.     public function createNewForm(EntityDto $entityDtoKeyValueStore $formOptionsAdminContext $context): FormInterface
  483.     {
  484.         return $this->createNewFormBuilder($entityDto$formOptions$context)->getForm();
  485.     }
  486.     public function createNewFormBuilder(EntityDto $entityDtoKeyValueStore $formOptionsAdminContext $context): FormBuilderInterface
  487.     {
  488.         return $this->container->get(FormFactory::class)->createNewFormBuilder($entityDto$formOptions$context);
  489.     }
  490.     /**
  491.      * Used to add/modify/remove parameters before passing them to the Twig template.
  492.      */
  493.     public function configureResponseParameters(KeyValueStore $responseParameters): KeyValueStore
  494.     {
  495.         return $responseParameters;
  496.     }
  497.     protected function getContext(): ?AdminContext
  498.     {
  499.         return $this->container->get(AdminContextProvider::class)->getContext();
  500.     }
  501.     /**
  502.      * @param EntityDto<TEntity> $entityDto
  503.      */
  504.     protected function ajaxEdit(EntityDto $entityDto, ?string $propertyNamebool $newValue): AfterCrudActionEvent
  505.     {
  506.         $field $entityDto->getFields()->getByProperty($propertyName);
  507.         if (null === $field || true === $field->getFormTypeOption('disabled')) {
  508.             throw new AccessDeniedException(sprintf('The field "%s" does not exist or it\'s configured as disabled, so it can\'t be modified.'$propertyName));
  509.         }
  510.         $this->container->get(EntityUpdater::class)->updateProperty($entityDto$propertyName$newValue);
  511.         /** @var TEntity $entityInstance */
  512.         $entityInstance $entityDto->getInstance();
  513.         $event = new BeforeEntityUpdatedEvent($entityInstance);
  514.         $this->container->get('event_dispatcher')->dispatch($event);
  515.         $entityInstance $event->getEntityInstance();
  516.         $this->updateEntity($this->container->get('doctrine')->getManagerForClass($entityDto->getFqcn()), $entityInstance);
  517.         $this->container->get('event_dispatcher')->dispatch(new AfterEntityUpdatedEvent($entityInstance));
  518.         $entityDto->setInstance($entityInstance);
  519.         $parameters KeyValueStore::new([
  520.             'action' => Action::EDIT,
  521.             'entity' => $entityDto,
  522.         ]);
  523.         $event = new AfterCrudActionEvent($this->getContext(), $parameters);
  524.         $this->container->get('event_dispatcher')->dispatch($event);
  525.         return $event;
  526.     }
  527.     protected function processUploadedFiles(FormInterface $form): void
  528.     {
  529.         /** @var FormInterface $child */
  530.         foreach ($form as $child) {
  531.             $config $child->getConfig();
  532.             if (!$config->getType()->getInnerType() instanceof FileUploadType) {
  533.                 if ($config->getCompound()) {
  534.                     $this->processUploadedFiles($child);
  535.                 }
  536.                 continue;
  537.             }
  538.             /** @var FileUploadState $state */
  539.             $state $config->getAttribute('state');
  540.             if (!$state->isModified()) {
  541.                 continue;
  542.             }
  543.             $uploadDelete $config->getOption('upload_delete');
  544.             if ($state->hasCurrentFiles() && ($state->isDelete() || (!$state->isAddAllowed() && $state->hasUploadedFiles()))) {
  545.                 foreach ($state->getCurrentFiles() as $file) {
  546.                     $uploadDelete($file);
  547.                 }
  548.                 $state->setCurrentFiles([]);
  549.             }
  550.             $filePaths = (array) $child->getData();
  551.             $uploadDir $config->getOption('upload_dir');
  552.             $uploadNew $config->getOption('upload_new');
  553.             foreach ($state->getUploadedFiles() as $index => $file) {
  554.                 $fileName u($filePaths[$index])->replace($uploadDir'')->toString();
  555.                 $uploadNew($file$uploadDir$fileName);
  556.             }
  557.         }
  558.     }
  559.     protected function getRedirectResponseAfterSave(AdminContext $contextstring $action): RedirectResponse
  560.     {
  561.         $submitButtonName $context->getRequest()->request->all()['ea']['newForm']['btn'] ?? null;
  562.         $url = match ($submitButtonName) {
  563.             Action::SAVE_AND_CONTINUE => $this->container->get(AdminUrlGenerator::class)
  564.                 ->setAction(Action::EDIT)
  565.                 ->setEntityId($context->getEntity()->getPrimaryKeyValue())
  566.                 ->generateUrl(),
  567.             Action::SAVE_AND_RETURN => $this->container->get(AdminUrlGenerator::class)->setAction(Action::INDEX)->generateUrl(),
  568.             Action::SAVE_AND_ADD_ANOTHER => $this->container->get(AdminUrlGenerator::class)->setAction(Action::NEW)->generateUrl(),
  569.             default => $this->generateUrl($context->getDashboardRouteName()),
  570.         };
  571.         return $this->redirect($url);
  572.     }
  573.     protected function getFieldAssets(FieldCollection $fieldDtos): AssetsDto
  574.     {
  575.         $fieldAssetsDto = new AssetsDto();
  576.         $currentPageName $this->getContext()?->getCrud()?->getCurrentPage();
  577.         foreach ($fieldDtos as $fieldDto) {
  578.             $fieldAssetsDto $fieldAssetsDto->mergeWith($fieldDto->getAssets()->loadedOn($currentPageName));
  579.         }
  580.         return $fieldAssetsDto;
  581.     }
  582. }