vendor/sonata-project/admin-bundle/src/Controller/CRUDController.php line 281

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of the Sonata Project package.
  5.  *
  6.  * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Sonata\AdminBundle\Controller;
  12. use Psr\Log\LoggerInterface;
  13. use Psr\Log\NullLogger;
  14. use Sonata\AdminBundle\Admin\AdminInterface;
  15. use Sonata\AdminBundle\Admin\Pool;
  16. use Sonata\AdminBundle\Bridge\Exporter\AdminExporter;
  17. use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
  18. use Sonata\AdminBundle\Exception\BadRequestParamHttpException;
  19. use Sonata\AdminBundle\Exception\LockException;
  20. use Sonata\AdminBundle\Exception\ModelManagerException;
  21. use Sonata\AdminBundle\Exception\ModelManagerThrowable;
  22. use Sonata\AdminBundle\Form\FormErrorIteratorToConstraintViolationList;
  23. use Sonata\AdminBundle\Model\AuditManagerInterface;
  24. use Sonata\AdminBundle\Request\AdminFetcherInterface;
  25. use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
  26. use Sonata\AdminBundle\Util\AdminAclUserManagerInterface;
  27. use Sonata\AdminBundle\Util\AdminObjectAclData;
  28. use Sonata\AdminBundle\Util\AdminObjectAclManipulator;
  29. use Sonata\Exporter\ExporterInterface;
  30. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  31. use Symfony\Component\Form\FormInterface;
  32. use Symfony\Component\Form\FormRenderer;
  33. use Symfony\Component\Form\FormView;
  34. use Symfony\Component\HttpFoundation\JsonResponse;
  35. use Symfony\Component\HttpFoundation\RedirectResponse;
  36. use Symfony\Component\HttpFoundation\Request;
  37. use Symfony\Component\HttpFoundation\RequestStack;
  38. use Symfony\Component\HttpFoundation\Response;
  39. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  40. use Symfony\Component\HttpKernel\Exception\HttpException;
  41. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  42. use Symfony\Component\HttpKernel\HttpKernelInterface;
  43. use Symfony\Component\PropertyAccess\PropertyAccess;
  44. use Symfony\Component\PropertyAccess\PropertyPath;
  45. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  46. use Symfony\Component\Security\Core\User\UserInterface;
  47. use Symfony\Component\Security\Csrf\CsrfToken;
  48. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  49. use Symfony\Component\String\UnicodeString;
  50. use Symfony\Contracts\Translation\TranslatorInterface;
  51. use Twig\Environment;
  52. /**
  53.  * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
  54.  *
  55.  * @phpstan-template T of object
  56.  *
  57.  * @psalm-suppress MissingConstructor
  58.  *
  59.  * @see ConfigureCRUDControllerListener
  60.  */
  61. class CRUDController extends AbstractController
  62. {
  63.     /**
  64.      * The related Admin class.
  65.      *
  66.      * @var AdminInterface<object>
  67.      *
  68.      * @phpstan-var AdminInterface<T>
  69.      *
  70.      * @psalm-suppress PropertyNotSetInConstructor
  71.      */
  72.     protected $admin;
  73.     /**
  74.      * The template registry of the related Admin class.
  75.      *
  76.      * @psalm-suppress PropertyNotSetInConstructor
  77.      * @phpstan-ignore-next-line
  78.      */
  79.     private TemplateRegistryInterface $templateRegistry;
  80.     public static function getSubscribedServices(): array
  81.     {
  82.         return [
  83.             'sonata.admin.pool' => Pool::class,
  84.             'sonata.admin.audit.manager' => AuditManagerInterface::class,
  85.             'sonata.admin.object.manipulator.acl.admin' => AdminObjectAclManipulator::class,
  86.             'sonata.admin.request.fetcher' => AdminFetcherInterface::class,
  87.             'sonata.exporter.exporter' => '?'.ExporterInterface::class,
  88.             'sonata.admin.admin_exporter' => '?'.AdminExporter::class,
  89.             'sonata.admin.security.acl_user_manager' => '?'.AdminAclUserManagerInterface::class,
  90.             'controller_resolver' => 'controller_resolver',
  91.             'http_kernel' => HttpKernelInterface::class,
  92.             'logger' => '?'.LoggerInterface::class,
  93.             'translator' => TranslatorInterface::class,
  94.         ] + parent::getSubscribedServices();
  95.     }
  96.     /**
  97.      * @throws AccessDeniedException If access is not granted
  98.      */
  99.     public function listAction(Request $request): Response
  100.     {
  101.         $this->assertObjectExists($request);
  102.         $this->admin->checkAccess('list');
  103.         $preResponse $this->preList($request);
  104.         if (null !== $preResponse) {
  105.             return $preResponse;
  106.         }
  107.         $listMode $request->get('_list_mode');
  108.         if (\is_string($listMode)) {
  109.             $this->admin->setListMode($listMode);
  110.         }
  111.         $datagrid $this->admin->getDatagrid();
  112.         $formView $datagrid->getForm()->createView();
  113.         // set the theme for the current Admin Form
  114.         $this->setFormTheme($formView$this->admin->getFilterTheme());
  115.         $template $this->templateRegistry->getTemplate('list');
  116.         if ($this->container->has('sonata.admin.admin_exporter')) {
  117.             $exporter $this->container->get('sonata.admin.admin_exporter');
  118.             \assert($exporter instanceof AdminExporter);
  119.             $exportFormats $exporter->getAvailableFormats($this->admin);
  120.         }
  121.         /**
  122.          * @psalm-suppress DeprecatedMethod
  123.          */
  124.         return $this->renderWithExtraParams($template, [
  125.             'action' => 'list',
  126.             'form' => $formView,
  127.             'datagrid' => $datagrid,
  128.             'csrf_token' => $this->getCsrfToken('sonata.batch'),
  129.             'export_formats' => $exportFormats ?? $this->admin->getExportFormats(),
  130.         ]);
  131.     }
  132.     /**
  133.      * NEXT_MAJOR: Change signature to `(ProxyQueryInterface $query, Request $request).
  134.      *
  135.      * Execute a batch delete.
  136.      *
  137.      * @throws AccessDeniedException If access is not granted
  138.      *
  139.      * @phpstan-param ProxyQueryInterface<T> $query
  140.      */
  141.     public function batchActionDelete(ProxyQueryInterface $query): Response
  142.     {
  143.         $this->admin->checkAccess('batchDelete');
  144.         $modelManager $this->admin->getModelManager();
  145.         try {
  146.             $modelManager->batchDelete($this->admin->getClass(), $query);
  147.             $this->addFlash(
  148.                 'sonata_flash_success',
  149.                 $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
  150.             );
  151.         } catch (ModelManagerException $e) {
  152.             // NEXT_MAJOR: Remove this catch.
  153.             $errorMessage $this->handleModelManagerException($e);
  154.             $this->addFlash(
  155.                 'sonata_flash_error',
  156.                 $errorMessage ?? $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
  157.             );
  158.         } catch (ModelManagerThrowable $e) {
  159.             $errorMessage $this->handleModelManagerThrowable($e);
  160.             $this->addFlash(
  161.                 'sonata_flash_error',
  162.                 $errorMessage ?? $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
  163.             );
  164.         }
  165.         return $this->redirectToList();
  166.     }
  167.     /**
  168.      * @throws NotFoundHttpException If the object does not exist
  169.      * @throws AccessDeniedException If access is not granted
  170.      */
  171.     public function deleteAction(Request $request): Response
  172.     {
  173.         $object $this->assertObjectExists($requesttrue);
  174.         \assert(null !== $object);
  175.         $this->checkParentChildAssociation($request$object);
  176.         $this->admin->checkAccess('delete'$object);
  177.         $preResponse $this->preDelete($request$object);
  178.         if (null !== $preResponse) {
  179.             return $preResponse;
  180.         }
  181.         if (\in_array($request->getMethod(), [Request::METHOD_POSTRequest::METHOD_DELETE], true)) {
  182.             // check the csrf token
  183.             $this->validateCsrfToken($request'sonata.delete');
  184.             $objectName $this->admin->toString($object);
  185.             try {
  186.                 $this->admin->delete($object);
  187.                 if ($this->isXmlHttpRequest($request)) {
  188.                     return $this->renderJson(['result' => 'ok']);
  189.                 }
  190.                 $this->addFlash(
  191.                     'sonata_flash_success',
  192.                     $this->trans(
  193.                         'flash_delete_success',
  194.                         ['%name%' => $this->escapeHtml($objectName)],
  195.                         'SonataAdminBundle'
  196.                     )
  197.                 );
  198.             } catch (ModelManagerException $e) {
  199.                 // NEXT_MAJOR: Remove this catch.
  200.                 $errorMessage $this->handleModelManagerException($e);
  201.                 if ($this->isXmlHttpRequest($request)) {
  202.                     return $this->renderJson(['result' => 'error']);
  203.                 }
  204.                 $this->addFlash(
  205.                     'sonata_flash_error',
  206.                     $errorMessage ?? $this->trans(
  207.                         'flash_delete_error',
  208.                         ['%name%' => $this->escapeHtml($objectName)],
  209.                         'SonataAdminBundle'
  210.                     )
  211.                 );
  212.             } catch (ModelManagerThrowable $e) {
  213.                 $errorMessage $this->handleModelManagerThrowable($e);
  214.                 if ($this->isXmlHttpRequest($request)) {
  215.                     return $this->renderJson(['result' => 'error'], Response::HTTP_OK, []);
  216.                 }
  217.                 $this->addFlash(
  218.                     'sonata_flash_error',
  219.                     $errorMessage ?? $this->trans(
  220.                         'flash_delete_error',
  221.                         ['%name%' => $this->escapeHtml($objectName)],
  222.                         'SonataAdminBundle'
  223.                     )
  224.                 );
  225.             }
  226.             return $this->redirectTo($request$object);
  227.         }
  228.         $template $this->templateRegistry->getTemplate('delete');
  229.         /**
  230.          * @psalm-suppress DeprecatedMethod
  231.          */
  232.         return $this->renderWithExtraParams($template, [
  233.             'object' => $object,
  234.             'action' => 'delete',
  235.             'csrf_token' => $this->getCsrfToken('sonata.delete'),
  236.         ]);
  237.     }
  238.     /**
  239.      * @throws NotFoundHttpException If the object does not exist
  240.      * @throws AccessDeniedException If access is not granted
  241.      */
  242.     public function editAction(Request $request): Response
  243.     {
  244.         // the key used to lookup the template
  245.         $templateKey 'edit';
  246.         $existingObject $this->assertObjectExists($requesttrue);
  247.         \assert(null !== $existingObject);
  248.         $this->checkParentChildAssociation($request$existingObject);
  249.         $this->admin->checkAccess('edit'$existingObject);
  250.         $preResponse $this->preEdit($request$existingObject);
  251.         if (null !== $preResponse) {
  252.             return $preResponse;
  253.         }
  254.         $this->admin->setSubject($existingObject);
  255.         $objectId $this->admin->getNormalizedIdentifier($existingObject);
  256.         \assert(null !== $objectId);
  257.         $form $this->admin->getForm();
  258.         $form->setData($existingObject);
  259.         $form->handleRequest($request);
  260.         if ($form->isSubmitted()) {
  261.             $isFormValid $form->isValid();
  262.             // persist if the form was valid and if in preview mode the preview was approved
  263.             if ($isFormValid && (!$this->isInPreviewMode($request) || $this->isPreviewApproved($request))) {
  264.                 /** @phpstan-var T $submittedObject */
  265.                 $submittedObject $form->getData();
  266.                 $this->admin->setSubject($submittedObject);
  267.                 try {
  268.                     $existingObject $this->admin->update($submittedObject);
  269.                     if ($this->isXmlHttpRequest($request)) {
  270.                         return $this->handleXmlHttpRequestSuccessResponse($request$existingObject);
  271.                     }
  272.                     $this->addFlash(
  273.                         'sonata_flash_success',
  274.                         $this->trans(
  275.                             'flash_edit_success',
  276.                             ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
  277.                             'SonataAdminBundle'
  278.                         )
  279.                     );
  280.                     // redirect to edit mode
  281.                     return $this->redirectTo($request$existingObject);
  282.                 } catch (ModelManagerException $e) {
  283.                     // NEXT_MAJOR: Remove this catch.
  284.                     $errorMessage $this->handleModelManagerException($e);
  285.                     $isFormValid false;
  286.                 } catch (ModelManagerThrowable $e) {
  287.                     $errorMessage $this->handleModelManagerThrowable($e);
  288.                     $isFormValid false;
  289.                 } catch (LockException) {
  290.                     $this->addFlash('sonata_flash_error'$this->trans('flash_lock_error', [
  291.                         '%name%' => $this->escapeHtml($this->admin->toString($existingObject)),
  292.                         '%link_start%' => \sprintf('<a href="%s">'$this->admin->generateObjectUrl('edit'$existingObject)),
  293.                         '%link_end%' => '</a>',
  294.                     ], 'SonataAdminBundle'));
  295.                 }
  296.             }
  297.             // show an error message if the form failed validation
  298.             if (!$isFormValid) {
  299.                 if ($this->isXmlHttpRequest($request) && null !== ($response $this->handleXmlHttpRequestErrorResponse($request$form))) {
  300.                     return $response;
  301.                 }
  302.                 $this->addFlash(
  303.                     'sonata_flash_error',
  304.                     $errorMessage ?? $this->trans(
  305.                         'flash_edit_error',
  306.                         ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
  307.                         'SonataAdminBundle'
  308.                     )
  309.                 );
  310.             } elseif ($this->isPreviewRequested($request)) {
  311.                 // enable the preview template if the form was valid and preview was requested
  312.                 $templateKey 'preview';
  313.                 $this->admin->getShow();
  314.             }
  315.         }
  316.         $formView $form->createView();
  317.         // set the theme for the current Admin Form
  318.         $this->setFormTheme($formView$this->admin->getFormTheme());
  319.         $template $this->templateRegistry->getTemplate($templateKey);
  320.         /**
  321.          * @psalm-suppress DeprecatedMethod
  322.          */
  323.         return $this->renderWithExtraParams($template, [
  324.             'action' => 'edit',
  325.             'form' => $formView,
  326.             'object' => $existingObject,
  327.             'objectId' => $objectId,
  328.         ]);
  329.     }
  330.     /**
  331.      * @throws NotFoundHttpException If the HTTP method is not POST
  332.      * @throws \RuntimeException     If the batch action is not defined
  333.      */
  334.     public function batchAction(Request $request): Response
  335.     {
  336.         $restMethod $request->getMethod();
  337.         if (Request::METHOD_POST !== $restMethod) {
  338.             throw $this->createNotFoundException(\sprintf(
  339.                 'Invalid request method given "%s", %s expected',
  340.                 $restMethod,
  341.                 Request::METHOD_POST
  342.             ));
  343.         }
  344.         // check the csrf token
  345.         $this->validateCsrfToken($request'sonata.batch');
  346.         $confirmation $request->get('confirmation'false);
  347.         $forwardedRequest $request->duplicate();
  348.         $encodedData $request->get('data');
  349.         if (null === $encodedData) {
  350.             $action $forwardedRequest->request->get('action');
  351.             $bag $request->request;
  352.             $idx $bag->all('idx');
  353.             $allElements $forwardedRequest->request->getBoolean('all_elements');
  354.             $forwardedRequest->request->set('idx'$idx);
  355.             $forwardedRequest->request->set('all_elements', (string) $allElements);
  356.             $data $forwardedRequest->request->all();
  357.             $data['all_elements'] = $allElements;
  358.             unset($data['_sonata_csrf_token']);
  359.         } else {
  360.             if (!\is_string($encodedData)) {
  361.                 throw new BadRequestParamHttpException('data''string'$encodedData);
  362.             }
  363.             try {
  364.                 $data json_decode($encodedDatatrue512\JSON_THROW_ON_ERROR);
  365.             } catch (\JsonException) {
  366.                 throw new BadRequestHttpException('Unable to decode batch data');
  367.             }
  368.             $action $data['action'];
  369.             $idx = (array) ($data['idx'] ?? []);
  370.             $allElements = (bool) ($data['all_elements'] ?? false);
  371.             $forwardedRequest->request->replace(array_merge($forwardedRequest->request->all(), $data));
  372.         }
  373.         if (!\is_string($action)) {
  374.             throw new \RuntimeException('The action is not defined');
  375.         }
  376.         $camelizedAction = (new UnicodeString($action))->camel()->title(true)->toString();
  377.         try {
  378.             $batchActionExecutable $this->getBatchActionExecutable($action);
  379.         } catch (\Throwable $error) {
  380.             $finalAction \sprintf('batchAction%s'$camelizedAction);
  381.             throw new \RuntimeException(\sprintf('A `%s::%s` method must be callable or create a `controller` configuration for your batch action.'$this->admin->getBaseControllerName(), $finalAction), 0$error);
  382.         }
  383.         $batchAction $this->admin->getBatchActions()[$action];
  384.         $isRelevantAction \sprintf('batchAction%sIsRelevant'$camelizedAction);
  385.         if (method_exists($this$isRelevantAction)) {
  386.             // NEXT_MAJOR: Remove if above in sonata-project/admin-bundle 5.0
  387.             @trigger_error(\sprintf(
  388.                 'The is relevant hook via "%s()" is deprecated since sonata-project/admin-bundle 4.12'
  389.                 .' and will not be call in 5.0. Move the logic to your controller.',
  390.                 $isRelevantAction,
  391.             ), \E_USER_DEPRECATED);
  392.             $nonRelevantMessage $this->$isRelevantAction($idx$allElements$forwardedRequest);
  393.         } else {
  394.             $nonRelevantMessage !== \count($idx) || $allElements// at least one item is selected
  395.         }
  396.         if (!\is_string($nonRelevantMessage) && true !== $nonRelevantMessage) { // default non relevant message
  397.             $nonRelevantMessage 'flash_batch_empty';
  398.         }
  399.         $datagrid $this->admin->getDatagrid();
  400.         $datagrid->buildPager();
  401.         if (\is_string($nonRelevantMessage)) {
  402.             $this->addFlash(
  403.                 'sonata_flash_info',
  404.                 $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
  405.             );
  406.             return $this->redirectToList();
  407.         }
  408.         $askConfirmation $batchAction['ask_confirmation'] ?? true;
  409.         if (true === $askConfirmation && 'ok' !== $confirmation) {
  410.             $actionLabel $batchAction['label'];
  411.             $batchTranslationDomain $batchAction['translation_domain'] ??
  412.                 $this->admin->getTranslationDomain();
  413.             $formView $datagrid->getForm()->createView();
  414.             $this->setFormTheme($formView$this->admin->getFilterTheme());
  415.             $template $batchAction['template'] ?? $this->templateRegistry->getTemplate('batch_confirmation');
  416.             /**
  417.              * @psalm-suppress DeprecatedMethod
  418.              */
  419.             return $this->renderWithExtraParams($template, [
  420.                 'action' => 'list',
  421.                 'action_label' => $actionLabel,
  422.                 'batch_translation_domain' => $batchTranslationDomain,
  423.                 'datagrid' => $datagrid,
  424.                 'form' => $formView,
  425.                 'data' => $data,
  426.                 'csrf_token' => $this->getCsrfToken('sonata.batch'),
  427.             ]);
  428.         }
  429.         $query $datagrid->getQuery();
  430.         $query->setFirstResult(null);
  431.         $query->setMaxResults(null);
  432.         $this->admin->preBatchAction($action$query$idx$allElements);
  433.         foreach ($this->admin->getExtensions() as $extension) {
  434.             // NEXT_MAJOR: Remove the if-statement around the call to `$extension->preBatchAction()`
  435.             if (method_exists($extension'preBatchAction')) {
  436.                 $extension->preBatchAction($this->admin$action$query$idx$allElements);
  437.             }
  438.         }
  439.         if (!$allElements) {
  440.             if (\count($idx) > 0) {
  441.                 $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query$idx);
  442.             } else {
  443.                 $this->addFlash(
  444.                     'sonata_flash_info',
  445.                     $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
  446.                 );
  447.                 return $this->redirectToList();
  448.             }
  449.         }
  450.         return \call_user_func($batchActionExecutable$query$forwardedRequest);
  451.     }
  452.     /**
  453.      * @throws AccessDeniedException If access is not granted
  454.      */
  455.     public function createAction(Request $request): Response
  456.     {
  457.         $this->assertObjectExists($request);
  458.         $this->admin->checkAccess('create');
  459.         // the key used to lookup the template
  460.         $templateKey 'edit';
  461.         $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
  462.         if ($class->isAbstract()) {
  463.             /**
  464.              * @psalm-suppress DeprecatedMethod
  465.              */
  466.             return $this->renderWithExtraParams(
  467.                 '@SonataAdmin/CRUD/select_subclass.html.twig',
  468.                 [
  469.                     'action' => 'create',
  470.                 ],
  471.             );
  472.         }
  473.         $newObject $this->admin->getNewInstance();
  474.         $preResponse $this->preCreate($request$newObject);
  475.         if (null !== $preResponse) {
  476.             return $preResponse;
  477.         }
  478.         $this->admin->setSubject($newObject);
  479.         $form $this->admin->getForm();
  480.         $form->setData($newObject);
  481.         $form->handleRequest($request);
  482.         if ($form->isSubmitted()) {
  483.             $isFormValid $form->isValid();
  484.             // persist if the form was valid and if in preview mode the preview was approved
  485.             if ($isFormValid && (!$this->isInPreviewMode($request) || $this->isPreviewApproved($request))) {
  486.                 /** @phpstan-var T $submittedObject */
  487.                 $submittedObject $form->getData();
  488.                 $this->admin->setSubject($submittedObject);
  489.                 try {
  490.                     $newObject $this->admin->create($submittedObject);
  491.                     if ($this->isXmlHttpRequest($request)) {
  492.                         return $this->handleXmlHttpRequestSuccessResponse($request$newObject);
  493.                     }
  494.                     $this->addFlash(
  495.                         'sonata_flash_success',
  496.                         $this->trans(
  497.                             'flash_create_success',
  498.                             ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
  499.                             'SonataAdminBundle'
  500.                         )
  501.                     );
  502.                     // redirect to edit mode
  503.                     return $this->redirectTo($request$newObject);
  504.                 } catch (ModelManagerException $e) {
  505.                     // NEXT_MAJOR: Remove this catch.
  506.                     $errorMessage $this->handleModelManagerException($e);
  507.                     $isFormValid false;
  508.                 } catch (ModelManagerThrowable $e) {
  509.                     $errorMessage $this->handleModelManagerThrowable($e);
  510.                     $isFormValid false;
  511.                 }
  512.             }
  513.             // show an error message if the form failed validation
  514.             if (!$isFormValid) {
  515.                 if ($this->isXmlHttpRequest($request) && null !== ($response $this->handleXmlHttpRequestErrorResponse($request$form))) {
  516.                     return $response;
  517.                 }
  518.                 $this->addFlash(
  519.                     'sonata_flash_error',
  520.                     $errorMessage ?? $this->trans(
  521.                         'flash_create_error',
  522.                         ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
  523.                         'SonataAdminBundle'
  524.                     )
  525.                 );
  526.             } elseif ($this->isPreviewRequested($request)) {
  527.                 // pick the preview template if the form was valid and preview was requested
  528.                 $templateKey 'preview';
  529.                 $this->admin->getShow();
  530.             }
  531.         }
  532.         $formView $form->createView();
  533.         // set the theme for the current Admin Form
  534.         $this->setFormTheme($formView$this->admin->getFormTheme());
  535.         $template $this->templateRegistry->getTemplate($templateKey);
  536.         /**
  537.          * @psalm-suppress DeprecatedMethod
  538.          */
  539.         return $this->renderWithExtraParams($template, [
  540.             'action' => 'create',
  541.             'form' => $formView,
  542.             'object' => $newObject,
  543.             'objectId' => null,
  544.         ]);
  545.     }
  546.     /**
  547.      * @throws NotFoundHttpException If the object does not exist
  548.      * @throws AccessDeniedException If access is not granted
  549.      */
  550.     public function showAction(Request $request): Response
  551.     {
  552.         $object $this->assertObjectExists($requesttrue);
  553.         \assert(null !== $object);
  554.         $this->checkParentChildAssociation($request$object);
  555.         $this->admin->checkAccess('show'$object);
  556.         $preResponse $this->preShow($request$object);
  557.         if (null !== $preResponse) {
  558.             return $preResponse;
  559.         }
  560.         $this->admin->setSubject($object);
  561.         $fields $this->admin->getShow();
  562.         $template $this->templateRegistry->getTemplate('show');
  563.         /**
  564.          * @psalm-suppress DeprecatedMethod
  565.          */
  566.         return $this->renderWithExtraParams($template, [
  567.             'action' => 'show',
  568.             'object' => $object,
  569.             'elements' => $fields,
  570.         ]);
  571.     }
  572.     /**
  573.      * Show history revisions for object.
  574.      *
  575.      * @throws AccessDeniedException If access is not granted
  576.      * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
  577.      */
  578.     public function historyAction(Request $request): Response
  579.     {
  580.         $object $this->assertObjectExists($requesttrue);
  581.         \assert(null !== $object);
  582.         $this->admin->checkAccess('history'$object);
  583.         $objectId $this->admin->getNormalizedIdentifier($object);
  584.         \assert(null !== $objectId);
  585.         $manager $this->container->get('sonata.admin.audit.manager');
  586.         \assert($manager instanceof AuditManagerInterface);
  587.         if (!$manager->hasReader($this->admin->getClass())) {
  588.             throw $this->createNotFoundException(\sprintf(
  589.                 'unable to find the audit reader for class : %s',
  590.                 $this->admin->getClass()
  591.             ));
  592.         }
  593.         $reader $manager->getReader($this->admin->getClass());
  594.         $revisions $reader->findRevisions($this->admin->getClass(), $objectId);
  595.         $template $this->templateRegistry->getTemplate('history');
  596.         /**
  597.          * @psalm-suppress DeprecatedMethod
  598.          */
  599.         return $this->renderWithExtraParams($template, [
  600.             'action' => 'history',
  601.             'object' => $object,
  602.             'revisions' => $revisions,
  603.             'currentRevision' => current($revisions),
  604.         ]);
  605.     }
  606.     /**
  607.      * View history revision of object.
  608.      *
  609.      * @throws AccessDeniedException If access is not granted
  610.      * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
  611.      */
  612.     public function historyViewRevisionAction(Request $requeststring $revision): Response
  613.     {
  614.         $object $this->assertObjectExists($requesttrue);
  615.         \assert(null !== $object);
  616.         $this->admin->checkAccess('historyViewRevision'$object);
  617.         $objectId $this->admin->getNormalizedIdentifier($object);
  618.         \assert(null !== $objectId);
  619.         $manager $this->container->get('sonata.admin.audit.manager');
  620.         \assert($manager instanceof AuditManagerInterface);
  621.         if (!$manager->hasReader($this->admin->getClass())) {
  622.             throw $this->createNotFoundException(\sprintf(
  623.                 'unable to find the audit reader for class : %s',
  624.                 $this->admin->getClass()
  625.             ));
  626.         }
  627.         $reader $manager->getReader($this->admin->getClass());
  628.         // retrieve the revisioned object
  629.         $object $reader->find($this->admin->getClass(), $objectId$revision);
  630.         if (null === $object) {
  631.             throw $this->createNotFoundException(\sprintf(
  632.                 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  633.                 $objectId,
  634.                 $revision,
  635.                 $this->admin->getClass()
  636.             ));
  637.         }
  638.         $this->admin->setSubject($object);
  639.         $template $this->templateRegistry->getTemplate('show');
  640.         /**
  641.          * @psalm-suppress DeprecatedMethod
  642.          */
  643.         return $this->renderWithExtraParams($template, [
  644.             'action' => 'show',
  645.             'object' => $object,
  646.             'elements' => $this->admin->getShow(),
  647.         ]);
  648.     }
  649.     /**
  650.      * Compare history revisions of object.
  651.      *
  652.      * @throws AccessDeniedException If access is not granted
  653.      * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
  654.      */
  655.     public function historyCompareRevisionsAction(Request $requeststring $baseRevisionstring $compareRevision): Response
  656.     {
  657.         $this->admin->checkAccess('historyCompareRevisions');
  658.         $object $this->assertObjectExists($requesttrue);
  659.         \assert(null !== $object);
  660.         $objectId $this->admin->getNormalizedIdentifier($object);
  661.         \assert(null !== $objectId);
  662.         $manager $this->container->get('sonata.admin.audit.manager');
  663.         \assert($manager instanceof AuditManagerInterface);
  664.         if (!$manager->hasReader($this->admin->getClass())) {
  665.             throw $this->createNotFoundException(\sprintf(
  666.                 'unable to find the audit reader for class : %s',
  667.                 $this->admin->getClass()
  668.             ));
  669.         }
  670.         $reader $manager->getReader($this->admin->getClass());
  671.         // retrieve the base revision
  672.         $baseObject $reader->find($this->admin->getClass(), $objectId$baseRevision);
  673.         if (null === $baseObject) {
  674.             throw $this->createNotFoundException(\sprintf(
  675.                 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  676.                 $objectId,
  677.                 $baseRevision,
  678.                 $this->admin->getClass()
  679.             ));
  680.         }
  681.         // retrieve the compare revision
  682.         $compareObject $reader->find($this->admin->getClass(), $objectId$compareRevision);
  683.         if (null === $compareObject) {
  684.             throw $this->createNotFoundException(\sprintf(
  685.                 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  686.                 $objectId,
  687.                 $compareRevision,
  688.                 $this->admin->getClass()
  689.             ));
  690.         }
  691.         $this->admin->setSubject($baseObject);
  692.         $template $this->templateRegistry->getTemplate('show_compare');
  693.         /**
  694.          * @psalm-suppress DeprecatedMethod
  695.          */
  696.         return $this->renderWithExtraParams($template, [
  697.             'action' => 'show',
  698.             'object' => $baseObject,
  699.             'object_compare' => $compareObject,
  700.             'elements' => $this->admin->getShow(),
  701.         ]);
  702.     }
  703.     /**
  704.      * Export data to specified format.
  705.      *
  706.      * @throws AccessDeniedException If access is not granted
  707.      * @throws \RuntimeException     If the export format is invalid
  708.      */
  709.     public function exportAction(Request $request): Response
  710.     {
  711.         $this->admin->checkAccess('export');
  712.         $format $request->get('format');
  713.         if (!\is_string($format)) {
  714.             throw new BadRequestParamHttpException('format''string'$format);
  715.         }
  716.         $adminExporter $this->container->get('sonata.admin.admin_exporter');
  717.         \assert($adminExporter instanceof AdminExporter);
  718.         $allowedExportFormats $adminExporter->getAvailableFormats($this->admin);
  719.         $filename $adminExporter->getExportFilename($this->admin$format);
  720.         $exporter $this->container->get('sonata.exporter.exporter');
  721.         \assert($exporter instanceof ExporterInterface);
  722.         if (!\in_array($format$allowedExportFormatstrue)) {
  723.             throw new \RuntimeException(\sprintf(
  724.                 'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
  725.                 $format,
  726.                 $this->admin->getClass(),
  727.                 implode(', '$allowedExportFormats)
  728.             ));
  729.         }
  730.         return $exporter->getResponse(
  731.             $format,
  732.             $filename,
  733.             $this->admin->getDataSourceIterator()
  734.         );
  735.     }
  736.     /**
  737.      * Returns the Response object associated to the acl action.
  738.      *
  739.      * @throws AccessDeniedException If access is not granted
  740.      * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
  741.      */
  742.     public function aclAction(Request $request): Response
  743.     {
  744.         if (!$this->admin->isAclEnabled()) {
  745.             throw $this->createNotFoundException('ACL are not enabled for this admin');
  746.         }
  747.         $object $this->assertObjectExists($requesttrue);
  748.         \assert(null !== $object);
  749.         $this->admin->checkAccess('acl'$object);
  750.         $this->admin->setSubject($object);
  751.         $aclUsers $this->getAclUsers();
  752.         $aclRoles $this->getAclRoles();
  753.         $adminObjectAclManipulator $this->container->get('sonata.admin.object.manipulator.acl.admin');
  754.         \assert($adminObjectAclManipulator instanceof AdminObjectAclManipulator);
  755.         $adminObjectAclData = new AdminObjectAclData(
  756.             $this->admin,
  757.             $object,
  758.             $aclUsers,
  759.             $adminObjectAclManipulator->getMaskBuilderClass(),
  760.             $aclRoles
  761.         );
  762.         $aclUsersForm $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
  763.         $aclRolesForm $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
  764.         if (Request::METHOD_POST === $request->getMethod()) {
  765.             if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
  766.                 $form $aclUsersForm;
  767.                 $updateMethod 'updateAclUsers';
  768.             } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
  769.                 $form $aclRolesForm;
  770.                 $updateMethod 'updateAclRoles';
  771.             }
  772.             if (isset($form$updateMethod)) {
  773.                 $form->handleRequest($request);
  774.                 if ($form->isValid()) {
  775.                     $adminObjectAclManipulator->$updateMethod($adminObjectAclData);
  776.                     $this->addFlash(
  777.                         'sonata_flash_success',
  778.                         $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
  779.                     );
  780.                     return new RedirectResponse($this->admin->generateObjectUrl('acl'$object));
  781.                 }
  782.             }
  783.         }
  784.         $template $this->templateRegistry->getTemplate('acl');
  785.         /**
  786.          * @psalm-suppress DeprecatedMethod
  787.          */
  788.         return $this->renderWithExtraParams($template, [
  789.             'action' => 'acl',
  790.             'permissions' => $adminObjectAclData->getUserPermissions(),
  791.             'object' => $object,
  792.             'users' => $aclUsers,
  793.             'roles' => $aclRoles,
  794.             'aclUsersForm' => $aclUsersForm->createView(),
  795.             'aclRolesForm' => $aclRolesForm->createView(),
  796.         ]);
  797.     }
  798.     /**
  799.      * Contextualize the admin class depends on the current request.
  800.      *
  801.      * @throws \InvalidArgumentException
  802.      */
  803.     final public function configureAdmin(Request $request): void
  804.     {
  805.         $adminFetcher $this->container->get('sonata.admin.request.fetcher');
  806.         \assert($adminFetcher instanceof AdminFetcherInterface);
  807.         /** @var AdminInterface<T> $admin */
  808.         $admin $adminFetcher->get($request);
  809.         $this->admin $admin;
  810.         if (!$this->admin->hasTemplateRegistry()) {
  811.             throw new \RuntimeException(\sprintf(
  812.                 'Unable to find the template registry related to the current admin (%s).',
  813.                 $this->admin->getCode()
  814.             ));
  815.         }
  816.         $this->templateRegistry $this->admin->getTemplateRegistry();
  817.     }
  818.     /**
  819.      * Add twig globals which are used in every template.
  820.      */
  821.     final public function setTwigGlobals(Request $request): void
  822.     {
  823.         $this->setTwigGlobal('admin'$this->admin);
  824.         if ($this->isXmlHttpRequest($request)) {
  825.             $baseTemplate $this->templateRegistry->getTemplate('ajax');
  826.         } else {
  827.             $baseTemplate $this->templateRegistry->getTemplate('layout');
  828.         }
  829.         $this->setTwigGlobal('base_template'$baseTemplate);
  830.     }
  831.     /**
  832.      * Renders a view while passing mandatory parameters on to the template.
  833.      *
  834.      * @param string               $view       The view name
  835.      * @param array<string, mixed> $parameters An array of parameters to pass to the view
  836.      *
  837.      * @deprecated since sonata-project/admin-bundle version 4.x
  838.      *
  839.      *  NEXT_MAJOR: Remove this method
  840.      */
  841.     final protected function renderWithExtraParams(string $view, array $parameters = [], ?Response $response null): Response
  842.     {
  843.         /**
  844.          * @psalm-suppress DeprecatedMethod
  845.          */
  846.         return $this->render($view$this->addRenderExtraParams($parameters), $response);
  847.     }
  848.     /**
  849.      * @param array<string, mixed> $parameters
  850.      *
  851.      * @return array<string, mixed>
  852.      *
  853.      * @deprecated since sonata-project/admin-bundle version 4.x
  854.      *
  855.      * NEXT_MAJOR: Remove this method
  856.      */
  857.     protected function addRenderExtraParams(array $parameters = []): array
  858.     {
  859.         $parameters['admin'] ??= $this->admin;
  860.         /**
  861.          * @psalm-suppress DeprecatedMethod
  862.          */
  863.         $parameters['base_template'] ??= $this->getBaseTemplate();
  864.         return $parameters;
  865.     }
  866.     /**
  867.      * @param mixed[] $headers
  868.      */
  869.     final protected function renderJson(mixed $dataint $status Response::HTTP_OK, array $headers = []): JsonResponse
  870.     {
  871.         return new JsonResponse($data$status$headers);
  872.     }
  873.     /**
  874.      * Returns true if the request is a XMLHttpRequest.
  875.      *
  876.      * @return bool True if the request is an XMLHttpRequest, false otherwise
  877.      */
  878.     final protected function isXmlHttpRequest(Request $request): bool
  879.     {
  880.         return $request->isXmlHttpRequest()
  881.             || $request->request->getBoolean('_xml_http_request')
  882.             || $request->query->getBoolean('_xml_http_request');
  883.     }
  884.     /**
  885.      * Proxy for the logger service of the container.
  886.      * If no such service is found, a NullLogger is returned.
  887.      */
  888.     protected function getLogger(): LoggerInterface
  889.     {
  890.         if ($this->container->has('logger')) {
  891.             $logger $this->container->get('logger');
  892.             \assert($logger instanceof LoggerInterface);
  893.             return $logger;
  894.         }
  895.         return new NullLogger();
  896.     }
  897.     /**
  898.      * Returns the base template name.
  899.      *
  900.      * @return string The template name
  901.      *
  902.      * @deprecated since sonata-project/admin-bundle version 4.x
  903.      *
  904.      *  NEXT_MAJOR: Remove this method
  905.      */
  906.     protected function getBaseTemplate(): string
  907.     {
  908.         $requestStack $this->container->get('request_stack');
  909.         \assert($requestStack instanceof RequestStack);
  910.         $request $requestStack->getCurrentRequest();
  911.         \assert(null !== $request);
  912.         if ($this->isXmlHttpRequest($request)) {
  913.             return $this->templateRegistry->getTemplate('ajax');
  914.         }
  915.         return $this->templateRegistry->getTemplate('layout');
  916.     }
  917.     /**
  918.      * @throws \Exception
  919.      *
  920.      * @return string|null A custom error message to display in the flag bag instead of the generic one
  921.      */
  922.     protected function handleModelManagerException(\Exception $exception)
  923.     {
  924.         if ($exception instanceof ModelManagerThrowable) {
  925.             return $this->handleModelManagerThrowable($exception);
  926.         }
  927.         @trigger_error(\sprintf(
  928.             'The method "%s()" is deprecated since sonata-project/admin-bundle 3.107 and will be removed in 5.0.',
  929.             __METHOD__
  930.         ), \E_USER_DEPRECATED);
  931.         $debug $this->getParameter('kernel.debug');
  932.         \assert(\is_bool($debug));
  933.         if ($debug) {
  934.             throw $exception;
  935.         }
  936.         $context = ['exception' => $exception];
  937.         if (null !== $exception->getPrevious()) {
  938.             $context['previous_exception_message'] = $exception->getPrevious()->getMessage();
  939.         }
  940.         $this->getLogger()->error($exception->getMessage(), $context);
  941.         return null;
  942.     }
  943.     /**
  944.      * NEXT_MAJOR: Add typehint.
  945.      *
  946.      * @throws ModelManagerThrowable
  947.      *
  948.      * @return string|null A custom error message to display in the flag bag instead of the generic one
  949.      */
  950.     protected function handleModelManagerThrowable(ModelManagerThrowable $exception)
  951.     {
  952.         $debug $this->getParameter('kernel.debug');
  953.         \assert(\is_bool($debug));
  954.         if ($debug) {
  955.             throw $exception;
  956.         }
  957.         $context = ['exception' => $exception];
  958.         if (null !== $exception->getPrevious()) {
  959.             $context['previous_exception_message'] = $exception->getPrevious()->getMessage();
  960.         }
  961.         $this->getLogger()->error($exception->getMessage(), $context);
  962.         return null;
  963.     }
  964.     /**
  965.      * Redirect the user depend on this choice.
  966.      *
  967.      * @phpstan-param T $object
  968.      */
  969.     protected function redirectTo(Request $requestobject $object): RedirectResponse
  970.     {
  971.         if (null !== $request->get('btn_update_and_list')) {
  972.             return $this->redirectToList();
  973.         }
  974.         if (null !== $request->get('btn_create_and_list')) {
  975.             return $this->redirectToList();
  976.         }
  977.         if (null !== $request->get('btn_create_and_create')) {
  978.             $params = [];
  979.             if ($this->admin->hasActiveSubClass()) {
  980.                 $params['subclass'] = $request->get('subclass');
  981.             }
  982.             return new RedirectResponse($this->admin->generateUrl('create'$params));
  983.         }
  984.         if (null !== $request->get('btn_delete')) {
  985.             return $this->redirectToList();
  986.         }
  987.         foreach (['edit''show'] as $route) {
  988.             if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route$object)) {
  989.                 $url $this->admin->generateObjectUrl(
  990.                     $route,
  991.                     $object,
  992.                     $this->getSelectedTab($request)
  993.                 );
  994.                 return new RedirectResponse($url);
  995.             }
  996.         }
  997.         return $this->redirectToList();
  998.     }
  999.     /**
  1000.      * Redirects the user to the list view.
  1001.      */
  1002.     final protected function redirectToList(): RedirectResponse
  1003.     {
  1004.         $parameters = [];
  1005.         $filter $this->admin->getFilterParameters();
  1006.         if ([] !== $filter) {
  1007.             $parameters['filter'] = $filter;
  1008.         }
  1009.         return $this->redirect($this->admin->generateUrl('list'$parameters));
  1010.     }
  1011.     /**
  1012.      * Returns true if the preview is requested to be shown.
  1013.      */
  1014.     final protected function isPreviewRequested(Request $request): bool
  1015.     {
  1016.         return null !== $request->get('btn_preview');
  1017.     }
  1018.     /**
  1019.      * Returns true if the preview has been approved.
  1020.      */
  1021.     final protected function isPreviewApproved(Request $request): bool
  1022.     {
  1023.         return null !== $request->get('btn_preview_approve');
  1024.     }
  1025.     /**
  1026.      * Returns true if the request is in the preview workflow.
  1027.      *
  1028.      * That means either a preview is requested or the preview has already been shown
  1029.      * and it got approved/declined.
  1030.      */
  1031.     final protected function isInPreviewMode(Request $request): bool
  1032.     {
  1033.         return $this->admin->supportsPreviewMode()
  1034.         && ($this->isPreviewRequested($request)
  1035.             || $this->isPreviewApproved($request)
  1036.             || $this->isPreviewDeclined($request));
  1037.     }
  1038.     /**
  1039.      * Returns true if the preview has been declined.
  1040.      */
  1041.     final protected function isPreviewDeclined(Request $request): bool
  1042.     {
  1043.         return null !== $request->get('btn_preview_decline');
  1044.     }
  1045.     /**
  1046.      * @return \Traversable<UserInterface|string>
  1047.      */
  1048.     protected function getAclUsers(): \Traversable
  1049.     {
  1050.         if (!$this->container->has('sonata.admin.security.acl_user_manager')) {
  1051.             return new \ArrayIterator([]);
  1052.         }
  1053.         $aclUserManager $this->container->get('sonata.admin.security.acl_user_manager');
  1054.         \assert($aclUserManager instanceof AdminAclUserManagerInterface);
  1055.         $aclUsers $aclUserManager->findUsers();
  1056.         return \is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
  1057.     }
  1058.     /**
  1059.      * @return \Traversable<string>
  1060.      */
  1061.     protected function getAclRoles(): \Traversable
  1062.     {
  1063.         $aclRoles = [];
  1064.         $roleHierarchy $this->getParameter('security.role_hierarchy.roles');
  1065.         \assert(\is_array($roleHierarchy));
  1066.         $pool $this->container->get('sonata.admin.pool');
  1067.         \assert($pool instanceof Pool);
  1068.         foreach ($pool->getAdminServiceCodes() as $code) {
  1069.             try {
  1070.                 $admin $pool->getInstance($code);
  1071.             } catch (\Exception) {
  1072.                 continue;
  1073.             }
  1074.             $baseRole $admin->getSecurityHandler()->getBaseRole($admin);
  1075.             foreach ($admin->getSecurityInformation() as $role => $_permissions) {
  1076.                 $role \sprintf($baseRole$role);
  1077.                 $aclRoles[] = $role;
  1078.             }
  1079.         }
  1080.         foreach ($roleHierarchy as $name => $roles) {
  1081.             $aclRoles[] = $name;
  1082.             $aclRoles array_merge($aclRoles$roles);
  1083.         }
  1084.         $aclRoles array_unique($aclRoles);
  1085.         return new \ArrayIterator($aclRoles);
  1086.     }
  1087.     /**
  1088.      * Validate CSRF token for action without form.
  1089.      *
  1090.      * @throws HttpException
  1091.      */
  1092.     final protected function validateCsrfToken(Request $requeststring $intention): void
  1093.     {
  1094.         if (!$this->container->has('security.csrf.token_manager')) {
  1095.             return;
  1096.         }
  1097.         $token $request->get('_sonata_csrf_token');
  1098.         $tokenManager $this->container->get('security.csrf.token_manager');
  1099.         \assert($tokenManager instanceof CsrfTokenManagerInterface);
  1100.         if (!$tokenManager->isTokenValid(new CsrfToken($intention$token))) {
  1101.             throw new HttpException(Response::HTTP_BAD_REQUEST'The csrf token is not valid, CSRF attack?');
  1102.         }
  1103.     }
  1104.     /**
  1105.      * Escape string for html output.
  1106.      */
  1107.     final protected function escapeHtml(string $s): string
  1108.     {
  1109.         return htmlspecialchars($s\ENT_QUOTES \ENT_SUBSTITUTE);
  1110.     }
  1111.     /**
  1112.      * Get CSRF token.
  1113.      */
  1114.     final protected function getCsrfToken(string $intention): ?string
  1115.     {
  1116.         if (!$this->container->has('security.csrf.token_manager')) {
  1117.             return null;
  1118.         }
  1119.         $tokenManager $this->container->get('security.csrf.token_manager');
  1120.         \assert($tokenManager instanceof CsrfTokenManagerInterface);
  1121.         return $tokenManager->getToken($intention)->getValue();
  1122.     }
  1123.     /**
  1124.      * This method can be overloaded in your custom CRUD controller.
  1125.      * It's called from createAction.
  1126.      *
  1127.      * @phpstan-param T $object
  1128.      */
  1129.     protected function preCreate(Request $requestobject $object): ?Response
  1130.     {
  1131.         return null;
  1132.     }
  1133.     /**
  1134.      * This method can be overloaded in your custom CRUD controller.
  1135.      * It's called from editAction.
  1136.      *
  1137.      * @phpstan-param T $object
  1138.      */
  1139.     protected function preEdit(Request $requestobject $object): ?Response
  1140.     {
  1141.         return null;
  1142.     }
  1143.     /**
  1144.      * This method can be overloaded in your custom CRUD controller.
  1145.      * It's called from deleteAction.
  1146.      *
  1147.      * @phpstan-param T $object
  1148.      */
  1149.     protected function preDelete(Request $requestobject $object): ?Response
  1150.     {
  1151.         return null;
  1152.     }
  1153.     /**
  1154.      * This method can be overloaded in your custom CRUD controller.
  1155.      * It's called from showAction.
  1156.      *
  1157.      * @phpstan-param T $object
  1158.      */
  1159.     protected function preShow(Request $requestobject $object): ?Response
  1160.     {
  1161.         return null;
  1162.     }
  1163.     /**
  1164.      * This method can be overloaded in your custom CRUD controller.
  1165.      * It's called from listAction.
  1166.      */
  1167.     protected function preList(Request $request): ?Response
  1168.     {
  1169.         return null;
  1170.     }
  1171.     /**
  1172.      * Translate a message id.
  1173.      *
  1174.      * @param mixed[] $parameters
  1175.      */
  1176.     final protected function trans(string $id, array $parameters = [], ?string $domain null, ?string $locale null): string
  1177.     {
  1178.         $domain ??= $this->admin->getTranslationDomain();
  1179.         $translator $this->container->get('translator');
  1180.         \assert($translator instanceof TranslatorInterface);
  1181.         return $translator->trans($id$parameters$domain$locale);
  1182.     }
  1183.     protected function handleXmlHttpRequestErrorResponse(Request $requestFormInterface $form): ?JsonResponse
  1184.     {
  1185.         if ([] === array_intersect(['application/json''*/*'], $request->getAcceptableContentTypes())) {
  1186.             return $this->renderJson([], Response::HTTP_NOT_ACCEPTABLE);
  1187.         }
  1188.         return $this->json(
  1189.             FormErrorIteratorToConstraintViolationList::transform($form->getErrors(true)),
  1190.             Response::HTTP_BAD_REQUEST
  1191.         );
  1192.     }
  1193.     /**
  1194.      * @phpstan-param T $object
  1195.      */
  1196.     protected function handleXmlHttpRequestSuccessResponse(Request $requestobject $object): JsonResponse
  1197.     {
  1198.         if ([] === array_intersect(['application/json''*/*'], $request->getAcceptableContentTypes())) {
  1199.             return $this->renderJson([], Response::HTTP_NOT_ACCEPTABLE);
  1200.         }
  1201.         return $this->renderJson([
  1202.             'result' => 'ok',
  1203.             'objectId' => $this->admin->getNormalizedIdentifier($object),
  1204.             'objectName' => $this->escapeHtml($this->admin->toString($object)),
  1205.         ]);
  1206.     }
  1207.     /**
  1208.      * @phpstan-return T|null
  1209.      */
  1210.     final protected function assertObjectExists(Request $requestbool $strict false): ?object
  1211.     {
  1212.         $admin $this->admin;
  1213.         $object null;
  1214.         while (null !== $admin) {
  1215.             $objectId $request->get($admin->getIdParameter());
  1216.             if (\is_string($objectId) || \is_int($objectId)) {
  1217.                 $adminObject $admin->getObject($objectId);
  1218.                 if (null === $adminObject) {
  1219.                     throw $this->createNotFoundException(\sprintf(
  1220.                         'Unable to find %s object with id: %s.',
  1221.                         $admin->getClassnameLabel(),
  1222.                         $objectId
  1223.                     ));
  1224.                 }
  1225.                 if (null === $object) {
  1226.                     /** @phpstan-var T $object */
  1227.                     $object $adminObject;
  1228.                 }
  1229.             } elseif ($strict || $admin !== $this->admin) {
  1230.                 throw $this->createNotFoundException(\sprintf(
  1231.                     'Unable to find the %s object id of the admin "%s".',
  1232.                     $admin->getClassnameLabel(),
  1233.                     $admin::class
  1234.                 ));
  1235.             }
  1236.             $admin $admin->isChild() ? $admin->getParent() : null;
  1237.         }
  1238.         return $object;
  1239.     }
  1240.     /**
  1241.      * @return array{_tab?: string}
  1242.      */
  1243.     final protected function getSelectedTab(Request $request): array
  1244.     {
  1245.         $tab = (string) $request->request->get('_tab');
  1246.         if ('' === $tab) {
  1247.             return [];
  1248.         }
  1249.         return ['_tab' => $tab];
  1250.     }
  1251.     /**
  1252.      * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
  1253.      *
  1254.      * @param string[]|null $theme
  1255.      */
  1256.     final protected function setFormTheme(FormView $formView, ?array $theme null): void
  1257.     {
  1258.         $twig $this->container->get('twig');
  1259.         \assert($twig instanceof Environment);
  1260.         $formRenderer $twig->getRuntime(FormRenderer::class);
  1261.         $formRenderer->setTheme($formView$theme);
  1262.     }
  1263.     /**
  1264.      * @phpstan-param T $object
  1265.      */
  1266.     final protected function checkParentChildAssociation(Request $requestobject $object): void
  1267.     {
  1268.         if (!$this->admin->isChild()) {
  1269.             return;
  1270.         }
  1271.         $parentAdmin $this->admin->getParent();
  1272.         $parentId $request->get($parentAdmin->getIdParameter());
  1273.         \assert(\is_string($parentId) || \is_int($parentId));
  1274.         $parentAdminObject $parentAdmin->getObject($parentId);
  1275.         if (null === $parentAdminObject) {
  1276.             throw new \RuntimeException(\sprintf(
  1277.                 'No object was found in the admin "%s" for the id "%s".',
  1278.                 $parentAdmin::class,
  1279.                 $parentId
  1280.             ));
  1281.         }
  1282.         $parentAssociationMapping $this->admin->getParentAssociationMapping();
  1283.         if (null === $parentAssociationMapping) {
  1284.             return;
  1285.         }
  1286.         $propertyAccessor PropertyAccess::createPropertyAccessor();
  1287.         $propertyPath = new PropertyPath($parentAssociationMapping);
  1288.         $objectParent $propertyAccessor->getValue($object$propertyPath);
  1289.         // $objectParent may be an array or a Collection when the parent association is many to many.
  1290.         $parentObjectMatches $this->equalsOrContains($objectParent$parentAdminObject);
  1291.         if (!$parentObjectMatches) {
  1292.             throw new \RuntimeException(\sprintf(
  1293.                 'There is no association between "%s" and "%s"',
  1294.                 $parentAdmin->toString($parentAdminObject),
  1295.                 $this->admin->toString($object)
  1296.             ));
  1297.         }
  1298.     }
  1299.     private function setTwigGlobal(string $namemixed $value): void
  1300.     {
  1301.         $twig $this->container->get('twig');
  1302.         \assert($twig instanceof Environment);
  1303.         try {
  1304.             $twig->addGlobal($name$value);
  1305.         } catch (\LogicException) {
  1306.             // Variable already set
  1307.         }
  1308.     }
  1309.     private function getBatchActionExecutable(string $action): callable
  1310.     {
  1311.         $batchActions $this->admin->getBatchActions();
  1312.         if (!\array_key_exists($action$batchActions)) {
  1313.             throw new \RuntimeException(\sprintf('The `%s` batch action is not defined'$action));
  1314.         }
  1315.         $controller $batchActions[$action]['controller'] ?? \sprintf(
  1316.             '%s::%s',
  1317.             $this->admin->getBaseControllerName(),
  1318.             \sprintf('batchAction%s', (new UnicodeString($action))->camel()->title(true)->toString())
  1319.         );
  1320.         // This will throw an exception when called so we know if it's possible or not to call the controller.
  1321.         $exists false !== $this->container
  1322.             ->get('controller_resolver')
  1323.             ->getController(new Request([], [], ['_controller' => $controller]));
  1324.         if (!$exists) {
  1325.             throw new \RuntimeException(\sprintf('Controller for action `%s` cannot be resolved'$action));
  1326.         }
  1327.         return function (ProxyQueryInterface $queryRequest $request) use ($controller): Response {
  1328.             $request->attributes->set('_controller'$controller);
  1329.             $request->attributes->set('query'$query);
  1330.             return $this->container->get('http_kernel')->handle($requestHttpKernelInterface::SUB_REQUEST);
  1331.         };
  1332.     }
  1333.     /**
  1334.      * Checks whether $needle is equal to $haystack or part of it.
  1335.      *
  1336.      * @param object|iterable<object> $haystack
  1337.      *
  1338.      * @return bool true when $haystack equals $needle or $haystack is iterable and contains $needle
  1339.      */
  1340.     private function equalsOrContains($haystackobject $needle): bool
  1341.     {
  1342.         if ($needle === $haystack) {
  1343.             return true;
  1344.         }
  1345.         if (is_iterable($haystack)) {
  1346.             foreach ($haystack as $haystackItem) {
  1347.                 if ($haystackItem === $needle) {
  1348.                     return true;
  1349.                 }
  1350.             }
  1351.         }
  1352.         return false;
  1353.     }
  1354. }