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

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