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

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