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

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\LockException;
  20. use Sonata\AdminBundle\Exception\ModelManagerException;
  21. use Sonata\AdminBundle\Exception\ModelManagerThrowable;
  22. use Sonata\AdminBundle\Model\AuditManagerInterface;
  23. use Sonata\AdminBundle\Request\AdminFetcherInterface;
  24. use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
  25. use Sonata\AdminBundle\Util\AdminAclUserManagerInterface;
  26. use Sonata\AdminBundle\Util\AdminObjectAclData;
  27. use Sonata\AdminBundle\Util\AdminObjectAclManipulator;
  28. use Sonata\Exporter\Exporter;
  29. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  30. use Symfony\Component\Form\FormError;
  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\HttpException;
  42. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  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\Contracts\Translation\TranslatorInterface;
  50. use Twig\Environment;
  51. /**
  52.  * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
  53.  *
  54.  * @phpstan-template T of object
  55.  *
  56.  * @psalm-suppress MissingConstructor
  57.  *
  58.  * @see ConfigureCRUDControllerListener
  59.  */
  60. class CRUDController extends AbstractController
  61. {
  62.     /**
  63.      * The related Admin class.
  64.      *
  65.      * @var AdminInterface<object>
  66.      * @phpstan-var AdminInterface<T>
  67.      *
  68.      * @psalm-suppress PropertyNotSetInConstructor
  69.      */
  70.     protected $admin;
  71.     /**
  72.      * The template registry of the related Admin class.
  73.      *
  74.      * @var TemplateRegistryInterface
  75.      *
  76.      * @psalm-suppress PropertyNotSetInConstructor
  77.      */
  78.     private $templateRegistry;
  79.     public static function getSubscribedServices(): array
  80.     {
  81.         return [
  82.             'sonata.admin.pool' => Pool::class,
  83.             'sonata.admin.audit.manager' => AuditManagerInterface::class,
  84.             'sonata.admin.object.manipulator.acl.admin' => AdminObjectAclManipulator::class,
  85.             'sonata.admin.request.fetcher' => AdminFetcherInterface::class,
  86.             'sonata.exporter.exporter' => '?'.Exporter::class,
  87.             'sonata.admin.admin_exporter' => '?'.AdminExporter::class,
  88.             'sonata.admin.security.acl_user_manager' => '?'.AdminAclUserManagerInterface::class,
  89.             'logger' => '?'.LoggerInterface::class,
  90.             'translator' => TranslatorInterface::class,
  91.         ] + parent::getSubscribedServices();
  92.     }
  93.     /**
  94.      * @throws AccessDeniedException If access is not granted
  95.      */
  96.     public function listAction(Request $request): Response
  97.     {
  98.         $this->assertObjectExists($request);
  99.         $this->admin->checkAccess('list');
  100.         $preResponse $this->preList($request);
  101.         if (null !== $preResponse) {
  102.             return $preResponse;
  103.         }
  104.         $listMode $request->get('_list_mode');
  105.         if (null !== $listMode) {
  106.             $this->admin->setListMode($listMode);
  107.         }
  108.         $datagrid $this->admin->getDatagrid();
  109.         $formView $datagrid->getForm()->createView();
  110.         // set the theme for the current Admin Form
  111.         $this->setFormTheme($formView$this->admin->getFilterTheme());
  112.         $template $this->templateRegistry->getTemplate('list');
  113.         if ($this->container->has('sonata.admin.admin_exporter')) {
  114.             $exporter $this->container->get('sonata.admin.admin_exporter');
  115.             \assert($exporter instanceof AdminExporter);
  116.             $exportFormats $exporter->getAvailableFormats($this->admin);
  117.         }
  118.         return $this->renderWithExtraParams($template, [
  119.             'action' => 'list',
  120.             'form' => $formView,
  121.             'datagrid' => $datagrid,
  122.             'csrf_token' => $this->getCsrfToken('sonata.batch'),
  123.             'export_formats' => $exportFormats ?? $this->admin->getExportFormats(),
  124.         ]);
  125.     }
  126.     /**
  127.      * NEXT_MAJOR: Change signature to `(ProxyQueryInterface $query, Request $request).
  128.      *
  129.      * Execute a batch delete.
  130.      *
  131.      * @throws AccessDeniedException If access is not granted
  132.      */
  133.     public function batchActionDelete(ProxyQueryInterface $query): Response
  134.     {
  135.         $this->admin->checkAccess('batchDelete');
  136.         $modelManager $this->admin->getModelManager();
  137.         try {
  138.             $modelManager->batchDelete($this->admin->getClass(), $query);
  139.             $this->addFlash(
  140.                 'sonata_flash_success',
  141.                 $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
  142.             );
  143.         } catch (ModelManagerException $e) {
  144.             // NEXT_MAJOR: Remove this catch.
  145.             $this->handleModelManagerException($e);
  146.             $this->addFlash(
  147.                 'sonata_flash_error',
  148.                 $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
  149.             );
  150.         } catch (ModelManagerThrowable $e) {
  151.             $this->handleModelManagerThrowable($e);
  152.             $this->addFlash(
  153.                 'sonata_flash_error',
  154.                 $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
  155.             );
  156.         }
  157.         return $this->redirectToList();
  158.     }
  159.     /**
  160.      * @throws NotFoundHttpException If the object does not exist
  161.      * @throws AccessDeniedException If access is not granted
  162.      */
  163.     public function deleteAction(Request $request): Response
  164.     {
  165.         $this->assertObjectExists($requesttrue);
  166.         $id $request->get($this->admin->getIdParameter());
  167.         \assert(null !== $id);
  168.         $object $this->admin->getObject($id);
  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.                 $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.                     $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.         $this->assertObjectExists($requesttrue);
  239.         $id $request->get($this->admin->getIdParameter());
  240.         \assert(null !== $id);
  241.         $existingObject $this->admin->getObject($id);
  242.         \assert(null !== $existingObject);
  243.         $this->checkParentChildAssociation($request$existingObject);
  244.         $this->admin->checkAccess('edit'$existingObject);
  245.         $preResponse $this->preEdit($request$existingObject);
  246.         if (null !== $preResponse) {
  247.             return $preResponse;
  248.         }
  249.         $this->admin->setSubject($existingObject);
  250.         $objectId $this->admin->getNormalizedIdentifier($existingObject);
  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.                     $this->handleModelManagerThrowable($e);
  282.                     $isFormValid false;
  283.                 } catch (LockException $e) {
  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.                     $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.         $data json_decode((string) $request->get('data'''), true);
  340.         if (null !== $data) {
  341.             $action $data['action'];
  342.             $idx = (array) ($data['idx'] ?? []);
  343.             $allElements = (bool) ($data['all_elements'] ?? false);
  344.             $forwardedRequest->request->replace(array_merge($forwardedRequest->request->all(), $data));
  345.         } else {
  346.             $action $forwardedRequest->request->get('action');
  347.             /** @var InputBag|ParameterBag $bag */
  348.             $bag $request->request;
  349.             if ($bag instanceof InputBag) {
  350.                 // symfony 5.1+
  351.                 $idx $bag->all('idx');
  352.             } else {
  353.                 $idx = (array) $bag->get('idx', []);
  354.             }
  355.             $allElements $forwardedRequest->request->getBoolean('all_elements');
  356.             $forwardedRequest->request->set('idx'$idx);
  357.             $forwardedRequest->request->set('all_elements', (string) $allElements);
  358.             $data $forwardedRequest->request->all();
  359.             $data['all_elements'] = $allElements;
  360.             unset($data['_sonata_csrf_token']);
  361.         }
  362.         if (null === $action) {
  363.             throw new \RuntimeException('The action is not defined');
  364.         }
  365.         $batchActions $this->admin->getBatchActions();
  366.         if (!\array_key_exists($action$batchActions)) {
  367.             throw new \RuntimeException(sprintf('The `%s` batch action is not defined'$action));
  368.         }
  369.         $camelizedAction InflectorFactory::create()->build()->classify($action);
  370.         $isRelevantAction sprintf('batchAction%sIsRelevant'$camelizedAction);
  371.         if (method_exists($this$isRelevantAction)) {
  372.             $nonRelevantMessage $this->$isRelevantAction($idx$allElements$forwardedRequest);
  373.         } else {
  374.             $nonRelevantMessage !== \count($idx) || $allElements// at least one item is selected
  375.         }
  376.         if (!$nonRelevantMessage) { // default non relevant message (if false of null)
  377.             $nonRelevantMessage 'flash_batch_empty';
  378.         }
  379.         $datagrid $this->admin->getDatagrid();
  380.         $datagrid->buildPager();
  381.         if (true !== $nonRelevantMessage) {
  382.             $this->addFlash(
  383.                 'sonata_flash_info',
  384.                 $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
  385.             );
  386.             return $this->redirectToList();
  387.         }
  388.         $askConfirmation $batchActions[$action]['ask_confirmation'] ?? true;
  389.         if (true === $askConfirmation && 'ok' !== $confirmation) {
  390.             $actionLabel $batchActions[$action]['label'];
  391.             $batchTranslationDomain $batchActions[$action]['translation_domain'] ??
  392.                 $this->admin->getTranslationDomain();
  393.             $formView $datagrid->getForm()->createView();
  394.             $this->setFormTheme($formView$this->admin->getFilterTheme());
  395.             $template $batchActions[$action]['template']
  396.                 ?? $this->templateRegistry->getTemplate('batch_confirmation');
  397.             return $this->renderWithExtraParams($template, [
  398.                 'action' => 'list',
  399.                 'action_label' => $actionLabel,
  400.                 'batch_translation_domain' => $batchTranslationDomain,
  401.                 'datagrid' => $datagrid,
  402.                 'form' => $formView,
  403.                 'data' => $data,
  404.                 'csrf_token' => $this->getCsrfToken('sonata.batch'),
  405.             ]);
  406.         }
  407.         // execute the action, batchActionXxxxx
  408.         $finalAction sprintf('batchAction%s'$camelizedAction);
  409.         if (!method_exists($this$finalAction)) {
  410.             throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', static::class, $finalAction));
  411.         }
  412.         $query $datagrid->getQuery();
  413.         $query->setFirstResult(null);
  414.         $query->setMaxResults(null);
  415.         $this->admin->preBatchAction($action$query$idx$allElements);
  416.         foreach ($this->admin->getExtensions() as $extension) {
  417.             // NEXT_MAJOR: Remove the if-statement around the call to `$extension->preBatchAction()`
  418.             // @phpstan-ignore-next-line
  419.             if (method_exists($extension'preBatchAction')) {
  420.                 $extension->preBatchAction($this->admin$action$query$idx$allElements);
  421.             }
  422.         }
  423.         if (\count($idx) > 0) {
  424.             $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query$idx);
  425.         } elseif (!$allElements) {
  426.             $this->addFlash(
  427.                 'sonata_flash_info',
  428.                 $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
  429.             );
  430.             return $this->redirectToList();
  431.         }
  432.         return $this->$finalAction($query$forwardedRequest);
  433.     }
  434.     /**
  435.      * @throws AccessDeniedException If access is not granted
  436.      */
  437.     public function createAction(Request $request): Response
  438.     {
  439.         $this->assertObjectExists($request);
  440.         $this->admin->checkAccess('create');
  441.         // the key used to lookup the template
  442.         $templateKey 'edit';
  443.         $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
  444.         if ($class->isAbstract()) {
  445.             return $this->renderWithExtraParams(
  446.                 '@SonataAdmin/CRUD/select_subclass.html.twig',
  447.                 [
  448.                     'action' => 'create',
  449.                 ],
  450.             );
  451.         }
  452.         $newObject $this->admin->getNewInstance();
  453.         $preResponse $this->preCreate($request$newObject);
  454.         if (null !== $preResponse) {
  455.             return $preResponse;
  456.         }
  457.         $this->admin->setSubject($newObject);
  458.         $form $this->admin->getForm();
  459.         $form->setData($newObject);
  460.         $form->handleRequest($request);
  461.         if ($form->isSubmitted()) {
  462.             $isFormValid $form->isValid();
  463.             // persist if the form was valid and if in preview mode the preview was approved
  464.             if ($isFormValid && (!$this->isInPreviewMode($request) || $this->isPreviewApproved($request))) {
  465.                 /** @phpstan-var T $submittedObject */
  466.                 $submittedObject $form->getData();
  467.                 $this->admin->setSubject($submittedObject);
  468.                 $this->admin->checkAccess('create'$submittedObject);
  469.                 try {
  470.                     $newObject $this->admin->create($submittedObject);
  471.                     if ($this->isXmlHttpRequest($request)) {
  472.                         return $this->handleXmlHttpRequestSuccessResponse($request$newObject);
  473.                     }
  474.                     $this->addFlash(
  475.                         'sonata_flash_success',
  476.                         $this->trans(
  477.                             'flash_create_success',
  478.                             ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
  479.                             'SonataAdminBundle'
  480.                         )
  481.                     );
  482.                     // redirect to edit mode
  483.                     return $this->redirectTo($request$newObject);
  484.                 } catch (ModelManagerException $e) {
  485.                     // NEXT_MAJOR: Remove this catch.
  486.                     $this->handleModelManagerException($e);
  487.                     $isFormValid false;
  488.                 } catch (ModelManagerThrowable $e) {
  489.                     $this->handleModelManagerThrowable($e);
  490.                     $isFormValid false;
  491.                 }
  492.             }
  493.             // show an error message if the form failed validation
  494.             if (!$isFormValid) {
  495.                 if ($this->isXmlHttpRequest($request) && null !== ($response $this->handleXmlHttpRequestErrorResponse($request$form))) {
  496.                     return $response;
  497.                 }
  498.                 $this->addFlash(
  499.                     'sonata_flash_error',
  500.                     $this->trans(
  501.                         'flash_create_error',
  502.                         ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
  503.                         'SonataAdminBundle'
  504.                     )
  505.                 );
  506.             } elseif ($this->isPreviewRequested($request)) {
  507.                 // pick the preview template if the form was valid and preview was requested
  508.                 $templateKey 'preview';
  509.                 $this->admin->getShow();
  510.             }
  511.         }
  512.         $formView $form->createView();
  513.         // set the theme for the current Admin Form
  514.         $this->setFormTheme($formView$this->admin->getFormTheme());
  515.         $template $this->templateRegistry->getTemplate($templateKey);
  516.         return $this->renderWithExtraParams($template, [
  517.             'action' => 'create',
  518.             'form' => $formView,
  519.             'object' => $newObject,
  520.             'objectId' => null,
  521.         ]);
  522.     }
  523.     /**
  524.      * @throws NotFoundHttpException If the object does not exist
  525.      * @throws AccessDeniedException If access is not granted
  526.      */
  527.     public function showAction(Request $request): Response
  528.     {
  529.         $this->assertObjectExists($requesttrue);
  530.         $id $request->get($this->admin->getIdParameter());
  531.         \assert(null !== $id);
  532.         $object $this->admin->getObject($id);
  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.         $this->assertObjectExists($requesttrue);
  558.         $id $request->get($this->admin->getIdParameter());
  559.         \assert(null !== $id);
  560.         $object $this->admin->getObject($id);
  561.         \assert(null !== $object);
  562.         $this->admin->checkAccess('history'$object);
  563.         $manager $this->container->get('sonata.admin.audit.manager');
  564.         \assert($manager instanceof AuditManagerInterface);
  565.         if (!$manager->hasReader($this->admin->getClass())) {
  566.             throw $this->createNotFoundException(sprintf(
  567.                 'unable to find the audit reader for class : %s',
  568.                 $this->admin->getClass()
  569.             ));
  570.         }
  571.         $reader $manager->getReader($this->admin->getClass());
  572.         $revisions $reader->findRevisions($this->admin->getClass(), $id);
  573.         $template $this->templateRegistry->getTemplate('history');
  574.         return $this->renderWithExtraParams($template, [
  575.             'action' => 'history',
  576.             'object' => $object,
  577.             'revisions' => $revisions,
  578.             'currentRevision' => current($revisions),
  579.         ]);
  580.     }
  581.     /**
  582.      * View history revision of object.
  583.      *
  584.      * @throws AccessDeniedException If access is not granted
  585.      * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
  586.      */
  587.     public function historyViewRevisionAction(Request $requeststring $revision): Response
  588.     {
  589.         $this->assertObjectExists($requesttrue);
  590.         $id $request->get($this->admin->getIdParameter());
  591.         \assert(null !== $id);
  592.         $object $this->admin->getObject($id);
  593.         \assert(null !== $object);
  594.         $this->admin->checkAccess('historyViewRevision'$object);
  595.         $manager $this->container->get('sonata.admin.audit.manager');
  596.         \assert($manager instanceof AuditManagerInterface);
  597.         if (!$manager->hasReader($this->admin->getClass())) {
  598.             throw $this->createNotFoundException(sprintf(
  599.                 'unable to find the audit reader for class : %s',
  600.                 $this->admin->getClass()
  601.             ));
  602.         }
  603.         $reader $manager->getReader($this->admin->getClass());
  604.         // retrieve the revisioned object
  605.         $object $reader->find($this->admin->getClass(), $id$revision);
  606.         if (null === $object) {
  607.             throw $this->createNotFoundException(sprintf(
  608.                 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  609.                 $id,
  610.                 $revision,
  611.                 $this->admin->getClass()
  612.             ));
  613.         }
  614.         $this->admin->setSubject($object);
  615.         $template $this->templateRegistry->getTemplate('show');
  616.         return $this->renderWithExtraParams($template, [
  617.             'action' => 'show',
  618.             'object' => $object,
  619.             'elements' => $this->admin->getShow(),
  620.         ]);
  621.     }
  622.     /**
  623.      * Compare history revisions of object.
  624.      *
  625.      * @throws AccessDeniedException If access is not granted
  626.      * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
  627.      */
  628.     public function historyCompareRevisionsAction(Request $requeststring $baseRevisionstring $compareRevision): Response
  629.     {
  630.         $this->admin->checkAccess('historyCompareRevisions');
  631.         $this->assertObjectExists($requesttrue);
  632.         $id $request->get($this->admin->getIdParameter());
  633.         \assert(null !== $id);
  634.         $manager $this->container->get('sonata.admin.audit.manager');
  635.         \assert($manager instanceof AuditManagerInterface);
  636.         if (!$manager->hasReader($this->admin->getClass())) {
  637.             throw $this->createNotFoundException(sprintf(
  638.                 'unable to find the audit reader for class : %s',
  639.                 $this->admin->getClass()
  640.             ));
  641.         }
  642.         $reader $manager->getReader($this->admin->getClass());
  643.         // retrieve the base revision
  644.         $baseObject $reader->find($this->admin->getClass(), $id$baseRevision);
  645.         if (null === $baseObject) {
  646.             throw $this->createNotFoundException(sprintf(
  647.                 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  648.                 $id,
  649.                 $baseRevision,
  650.                 $this->admin->getClass()
  651.             ));
  652.         }
  653.         // retrieve the compare revision
  654.         $compareObject $reader->find($this->admin->getClass(), $id$compareRevision);
  655.         if (null === $compareObject) {
  656.             throw $this->createNotFoundException(sprintf(
  657.                 'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
  658.                 $id,
  659.                 $compareRevision,
  660.                 $this->admin->getClass()
  661.             ));
  662.         }
  663.         $this->admin->setSubject($baseObject);
  664.         $template $this->templateRegistry->getTemplate('show_compare');
  665.         return $this->renderWithExtraParams($template, [
  666.             'action' => 'show',
  667.             'object' => $baseObject,
  668.             'object_compare' => $compareObject,
  669.             'elements' => $this->admin->getShow(),
  670.         ]);
  671.     }
  672.     /**
  673.      * Export data to specified format.
  674.      *
  675.      * @throws AccessDeniedException If access is not granted
  676.      * @throws \RuntimeException     If the export format is invalid
  677.      */
  678.     public function exportAction(Request $request): Response
  679.     {
  680.         $this->admin->checkAccess('export');
  681.         $format $request->get('format');
  682.         $adminExporter $this->container->get('sonata.admin.admin_exporter');
  683.         \assert($adminExporter instanceof AdminExporter);
  684.         $allowedExportFormats $adminExporter->getAvailableFormats($this->admin);
  685.         $filename $adminExporter->getExportFilename($this->admin$format);
  686.         $exporter $this->container->get('sonata.exporter.exporter');
  687.         \assert($exporter instanceof Exporter);
  688.         if (!\in_array($format$allowedExportFormatstrue)) {
  689.             throw new \RuntimeException(sprintf(
  690.                 'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
  691.                 $format,
  692.                 $this->admin->getClass(),
  693.                 implode(', '$allowedExportFormats)
  694.             ));
  695.         }
  696.         return $exporter->getResponse(
  697.             $format,
  698.             $filename,
  699.             $this->admin->getDataSourceIterator()
  700.         );
  701.     }
  702.     /**
  703.      * Returns the Response object associated to the acl action.
  704.      *
  705.      * @throws AccessDeniedException If access is not granted
  706.      * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
  707.      */
  708.     public function aclAction(Request $request): Response
  709.     {
  710.         if (!$this->admin->isAclEnabled()) {
  711.             throw $this->createNotFoundException('ACL are not enabled for this admin');
  712.         }
  713.         $this->assertObjectExists($requesttrue);
  714.         $id $request->get($this->admin->getIdParameter());
  715.         \assert(null !== $id);
  716.         $object $this->admin->getObject($id);
  717.         \assert(null !== $object);
  718.         $this->admin->checkAccess('acl'$object);
  719.         $this->admin->setSubject($object);
  720.         $aclUsers $this->getAclUsers();
  721.         $aclRoles $this->getAclRoles();
  722.         $adminObjectAclManipulator $this->container->get('sonata.admin.object.manipulator.acl.admin');
  723.         \assert($adminObjectAclManipulator instanceof AdminObjectAclManipulator);
  724.         $adminObjectAclData = new AdminObjectAclData(
  725.             $this->admin,
  726.             $object,
  727.             $aclUsers,
  728.             $adminObjectAclManipulator->getMaskBuilderClass(),
  729.             $aclRoles
  730.         );
  731.         $aclUsersForm $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
  732.         $aclRolesForm $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
  733.         if (Request::METHOD_POST === $request->getMethod()) {
  734.             if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
  735.                 $form $aclUsersForm;
  736.                 $updateMethod 'updateAclUsers';
  737.             } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
  738.                 $form $aclRolesForm;
  739.                 $updateMethod 'updateAclRoles';
  740.             }
  741.             if (isset($form$updateMethod)) {
  742.                 $form->handleRequest($request);
  743.                 if ($form->isValid()) {
  744.                     $adminObjectAclManipulator->$updateMethod($adminObjectAclData);
  745.                     $this->addFlash(
  746.                         'sonata_flash_success',
  747.                         $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
  748.                     );
  749.                     return new RedirectResponse($this->admin->generateObjectUrl('acl'$object));
  750.                 }
  751.             }
  752.         }
  753.         $template $this->templateRegistry->getTemplate('acl');
  754.         return $this->renderWithExtraParams($template, [
  755.             'action' => 'acl',
  756.             'permissions' => $adminObjectAclData->getUserPermissions(),
  757.             'object' => $object,
  758.             'users' => $aclUsers,
  759.             'roles' => $aclRoles,
  760.             'aclUsersForm' => $aclUsersForm->createView(),
  761.             'aclRolesForm' => $aclRolesForm->createView(),
  762.         ]);
  763.     }
  764.     /**
  765.      * Contextualize the admin class depends on the current request.
  766.      *
  767.      * @throws \InvalidArgumentException
  768.      */
  769.     final public function configureAdmin(Request $request): void
  770.     {
  771.         $adminFetcher $this->container->get('sonata.admin.request.fetcher');
  772.         $this->admin $adminFetcher->get($request);
  773.         if (!$this->admin->hasTemplateRegistry()) {
  774.             throw new \RuntimeException(sprintf(
  775.                 'Unable to find the template registry related to the current admin (%s).',
  776.                 $this->admin->getCode()
  777.             ));
  778.         }
  779.         $this->templateRegistry $this->admin->getTemplateRegistry();
  780.     }
  781.     /**
  782.      * Renders a view while passing mandatory parameters on to the template.
  783.      *
  784.      * @param string               $view       The view name
  785.      * @param array<string, mixed> $parameters An array of parameters to pass to the view
  786.      */
  787.     final protected function renderWithExtraParams(string $view, array $parameters = [], ?Response $response null): Response
  788.     {
  789.         return $this->render($view$this->addRenderExtraParams($parameters), $response);
  790.     }
  791.     /**
  792.      * @param array<string, mixed> $parameters
  793.      *
  794.      * @return array<string, mixed>
  795.      */
  796.     protected function addRenderExtraParams(array $parameters = []): array
  797.     {
  798.         $parameters['admin'] = $parameters['admin'] ?? $this->admin;
  799.         $parameters['base_template'] = $parameters['base_template'] ?? $this->getBaseTemplate();
  800.         return $parameters;
  801.     }
  802.     /**
  803.      * @param mixed   $data
  804.      * @param mixed[] $headers
  805.      */
  806.     final protected function renderJson($dataint $status Response::HTTP_OK, array $headers = []): JsonResponse
  807.     {
  808.         return new JsonResponse($data$status$headers);
  809.     }
  810.     /**
  811.      * Returns true if the request is a XMLHttpRequest.
  812.      *
  813.      * @return bool True if the request is an XMLHttpRequest, false otherwise
  814.      */
  815.     final protected function isXmlHttpRequest(Request $request): bool
  816.     {
  817.         return $request->isXmlHttpRequest()
  818.             || $request->request->getBoolean('_xml_http_request')
  819.             || $request->query->getBoolean('_xml_http_request');
  820.     }
  821.     /**
  822.      * Proxy for the logger service of the container.
  823.      * If no such service is found, a NullLogger is returned.
  824.      */
  825.     protected function getLogger(): LoggerInterface
  826.     {
  827.         if ($this->container->has('logger')) {
  828.             $logger $this->container->get('logger');
  829.             \assert($logger instanceof LoggerInterface);
  830.             return $logger;
  831.         }
  832.         return new NullLogger();
  833.     }
  834.     /**
  835.      * Returns the base template name.
  836.      *
  837.      * @return string The template name
  838.      */
  839.     protected function getBaseTemplate(): string
  840.     {
  841.         $requestStack $this->container->get('request_stack');
  842.         \assert($requestStack instanceof RequestStack);
  843.         $request $requestStack->getCurrentRequest();
  844.         \assert(null !== $request);
  845.         if ($this->isXmlHttpRequest($request)) {
  846.             return $this->templateRegistry->getTemplate('ajax');
  847.         }
  848.         return $this->templateRegistry->getTemplate('layout');
  849.     }
  850.     /**
  851.      * @throws \Exception
  852.      */
  853.     protected function handleModelManagerException(\Exception $exception): void
  854.     {
  855.         if ($exception instanceof ModelManagerThrowable) {
  856.             $this->handleModelManagerThrowable($exception);
  857.             return;
  858.         }
  859.         @trigger_error(sprintf(
  860.             'The method "%s()" is deprecated since sonata-project/admin-bundle 3.107 and will be removed in 5.0.',
  861.             __METHOD__
  862.         ), \E_USER_DEPRECATED);
  863.         $debug $this->getParameter('kernel.debug');
  864.         \assert(\is_bool($debug));
  865.         if ($debug) {
  866.             throw $exception;
  867.         }
  868.         $context = ['exception' => $exception];
  869.         if (null !== $exception->getPrevious()) {
  870.             $context['previous_exception_message'] = $exception->getPrevious()->getMessage();
  871.         }
  872.         $this->getLogger()->error($exception->getMessage(), $context);
  873.     }
  874.     /**
  875.      * @throws ModelManagerThrowable
  876.      */
  877.     protected function handleModelManagerThrowable(ModelManagerThrowable $exception): void
  878.     {
  879.         $debug $this->getParameter('kernel.debug');
  880.         \assert(\is_bool($debug));
  881.         if ($debug) {
  882.             throw $exception;
  883.         }
  884.         $context = ['exception' => $exception];
  885.         if (null !== $exception->getPrevious()) {
  886.             $context['previous_exception_message'] = $exception->getPrevious()->getMessage();
  887.         }
  888.         $this->getLogger()->error($exception->getMessage(), $context);
  889.     }
  890.     /**
  891.      * Redirect the user depend on this choice.
  892.      *
  893.      * @phpstan-param T $object
  894.      */
  895.     protected function redirectTo(Request $requestobject $object): RedirectResponse
  896.     {
  897.         if (null !== $request->get('btn_update_and_list')) {
  898.             return $this->redirectToList();
  899.         }
  900.         if (null !== $request->get('btn_create_and_list')) {
  901.             return $this->redirectToList();
  902.         }
  903.         if (null !== $request->get('btn_create_and_create')) {
  904.             $params = [];
  905.             if ($this->admin->hasActiveSubClass()) {
  906.                 $params['subclass'] = $request->get('subclass');
  907.             }
  908.             return new RedirectResponse($this->admin->generateUrl('create'$params));
  909.         }
  910.         if (null !== $request->get('btn_delete')) {
  911.             return $this->redirectToList();
  912.         }
  913.         foreach (['edit''show'] as $route) {
  914.             if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route$object)) {
  915.                 $url $this->admin->generateObjectUrl(
  916.                     $route,
  917.                     $object,
  918.                     $this->getSelectedTab($request)
  919.                 );
  920.                 return new RedirectResponse($url);
  921.             }
  922.         }
  923.         return $this->redirectToList();
  924.     }
  925.     /**
  926.      * Redirects the user to the list view.
  927.      */
  928.     final protected function redirectToList(): RedirectResponse
  929.     {
  930.         $parameters = [];
  931.         $filter $this->admin->getFilterParameters();
  932.         if ([] !== $filter) {
  933.             $parameters['filter'] = $filter;
  934.         }
  935.         return $this->redirect($this->admin->generateUrl('list'$parameters));
  936.     }
  937.     /**
  938.      * Returns true if the preview is requested to be shown.
  939.      */
  940.     final protected function isPreviewRequested(Request $request): bool
  941.     {
  942.         return null !== $request->get('btn_preview');
  943.     }
  944.     /**
  945.      * Returns true if the preview has been approved.
  946.      */
  947.     final protected function isPreviewApproved(Request $request): bool
  948.     {
  949.         return null !== $request->get('btn_preview_approve');
  950.     }
  951.     /**
  952.      * Returns true if the request is in the preview workflow.
  953.      *
  954.      * That means either a preview is requested or the preview has already been shown
  955.      * and it got approved/declined.
  956.      */
  957.     final protected function isInPreviewMode(Request $request): bool
  958.     {
  959.         return $this->admin->supportsPreviewMode()
  960.         && ($this->isPreviewRequested($request)
  961.             || $this->isPreviewApproved($request)
  962.             || $this->isPreviewDeclined($request));
  963.     }
  964.     /**
  965.      * Returns true if the preview has been declined.
  966.      */
  967.     final protected function isPreviewDeclined(Request $request): bool
  968.     {
  969.         return null !== $request->get('btn_preview_decline');
  970.     }
  971.     /**
  972.      * @return \Traversable<UserInterface|string>
  973.      */
  974.     protected function getAclUsers(): \Traversable
  975.     {
  976.         if (!$this->container->has('sonata.admin.security.acl_user_manager')) {
  977.             return new \ArrayIterator([]);
  978.         }
  979.         $aclUserManager $this->container->get('sonata.admin.security.acl_user_manager');
  980.         \assert($aclUserManager instanceof AdminAclUserManagerInterface);
  981.         $aclUsers $aclUserManager->findUsers();
  982.         return \is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
  983.     }
  984.     /**
  985.      * @return \Traversable<string>
  986.      */
  987.     protected function getAclRoles(): \Traversable
  988.     {
  989.         $aclRoles = [];
  990.         $roleHierarchy $this->getParameter('security.role_hierarchy.roles');
  991.         \assert(\is_array($roleHierarchy));
  992.         $pool $this->container->get('sonata.admin.pool');
  993.         \assert($pool instanceof Pool);
  994.         foreach ($pool->getAdminServiceIds() as $id) {
  995.             try {
  996.                 $admin $pool->getInstance($id);
  997.             } catch (\Exception $e) {
  998.                 continue;
  999.             }
  1000.             $baseRole $admin->getSecurityHandler()->getBaseRole($admin);
  1001.             foreach ($admin->getSecurityInformation() as $role => $_permissions) {
  1002.                 $role sprintf($baseRole$role);
  1003.                 $aclRoles[] = $role;
  1004.             }
  1005.         }
  1006.         foreach ($roleHierarchy as $name => $roles) {
  1007.             $aclRoles[] = $name;
  1008.             $aclRoles array_merge($aclRoles$roles);
  1009.         }
  1010.         $aclRoles array_unique($aclRoles);
  1011.         return new \ArrayIterator($aclRoles);
  1012.     }
  1013.     /**
  1014.      * Validate CSRF token for action without form.
  1015.      *
  1016.      * @throws HttpException
  1017.      */
  1018.     final protected function validateCsrfToken(Request $requeststring $intention): void
  1019.     {
  1020.         if (!$this->container->has('security.csrf.token_manager')) {
  1021.             return;
  1022.         }
  1023.         $token $request->get('_sonata_csrf_token');
  1024.         $tokenManager $this->container->get('security.csrf.token_manager');
  1025.         \assert($tokenManager instanceof CsrfTokenManagerInterface);
  1026.         if (!$tokenManager->isTokenValid(new CsrfToken($intention$token))) {
  1027.             throw new HttpException(Response::HTTP_BAD_REQUEST'The csrf token is not valid, CSRF attack?');
  1028.         }
  1029.     }
  1030.     /**
  1031.      * Escape string for html output.
  1032.      */
  1033.     final protected function escapeHtml(string $s): string
  1034.     {
  1035.         return htmlspecialchars($s, \ENT_QUOTES | \ENT_SUBSTITUTE);
  1036.     }
  1037.     /**
  1038.      * Get CSRF token.
  1039.      */
  1040.     final protected function getCsrfToken(string $intention): ?string
  1041.     {
  1042.         if (!$this->container->has('security.csrf.token_manager')) {
  1043.             return null;
  1044.         }
  1045.         $tokenManager $this->container->get('security.csrf.token_manager');
  1046.         \assert($tokenManager instanceof CsrfTokenManagerInterface);
  1047.         return $tokenManager->getToken($intention)->getValue();
  1048.     }
  1049.     /**
  1050.      * This method can be overloaded in your custom CRUD controller.
  1051.      * It's called from createAction.
  1052.      *
  1053.      * @phpstan-param T $object
  1054.      */
  1055.     protected function preCreate(Request $requestobject $object): ?Response
  1056.     {
  1057.         return null;
  1058.     }
  1059.     /**
  1060.      * This method can be overloaded in your custom CRUD controller.
  1061.      * It's called from editAction.
  1062.      *
  1063.      * @phpstan-param T $object
  1064.      */
  1065.     protected function preEdit(Request $requestobject $object): ?Response
  1066.     {
  1067.         return null;
  1068.     }
  1069.     /**
  1070.      * This method can be overloaded in your custom CRUD controller.
  1071.      * It's called from deleteAction.
  1072.      *
  1073.      * @phpstan-param T $object
  1074.      */
  1075.     protected function preDelete(Request $requestobject $object): ?Response
  1076.     {
  1077.         return null;
  1078.     }
  1079.     /**
  1080.      * This method can be overloaded in your custom CRUD controller.
  1081.      * It's called from showAction.
  1082.      *
  1083.      * @phpstan-param T $object
  1084.      */
  1085.     protected function preShow(Request $requestobject $object): ?Response
  1086.     {
  1087.         return null;
  1088.     }
  1089.     /**
  1090.      * This method can be overloaded in your custom CRUD controller.
  1091.      * It's called from listAction.
  1092.      */
  1093.     protected function preList(Request $request): ?Response
  1094.     {
  1095.         return null;
  1096.     }
  1097.     /**
  1098.      * Translate a message id.
  1099.      *
  1100.      * @param mixed[] $parameters
  1101.      */
  1102.     final protected function trans(string $id, array $parameters = [], ?string $domain null, ?string $locale null): string
  1103.     {
  1104.         $domain $domain ?? $this->admin->getTranslationDomain();
  1105.         $translator $this->container->get('translator');
  1106.         \assert($translator instanceof TranslatorInterface);
  1107.         return $translator->trans($id$parameters$domain$locale);
  1108.     }
  1109.     protected function handleXmlHttpRequestErrorResponse(Request $requestFormInterface $form): ?JsonResponse
  1110.     {
  1111.         if ([] === array_intersect(['application/json''*/*'], $request->getAcceptableContentTypes())) {
  1112.             return $this->renderJson([], Response::HTTP_NOT_ACCEPTABLE);
  1113.         }
  1114.         $errors = [];
  1115.         foreach ($form->getErrors(true) as $error) {
  1116.             \assert($error instanceof FormError);
  1117.             $errors[] = $error->getMessage();
  1118.         }
  1119.         return $this->renderJson([
  1120.             'result' => 'error',
  1121.             'errors' => $errors,
  1122.         ], Response::HTTP_BAD_REQUEST);
  1123.     }
  1124.     /**
  1125.      * @phpstan-param T $object
  1126.      */
  1127.     protected function handleXmlHttpRequestSuccessResponse(Request $requestobject $object): JsonResponse
  1128.     {
  1129.         if ([] === array_intersect(['application/json''*/*'], $request->getAcceptableContentTypes())) {
  1130.             return $this->renderJson([], Response::HTTP_NOT_ACCEPTABLE);
  1131.         }
  1132.         return $this->renderJson([
  1133.             'result' => 'ok',
  1134.             'objectId' => $this->admin->getNormalizedIdentifier($object),
  1135.             'objectName' => $this->escapeHtml($this->admin->toString($object)),
  1136.         ]);
  1137.     }
  1138.     final protected function assertObjectExists(Request $requestbool $strict false): void
  1139.     {
  1140.         $admin $this->admin;
  1141.         while (null !== $admin) {
  1142.             $objectId $request->get($admin->getIdParameter());
  1143.             if (null !== $objectId) {
  1144.                 $adminObject $admin->getObject($objectId);
  1145.                 if (null === $adminObject) {
  1146.                     throw $this->createNotFoundException(sprintf(
  1147.                         'Unable to find %s object with id: %s.',
  1148.                         $admin->getClassnameLabel(),
  1149.                         $objectId
  1150.                     ));
  1151.                 }
  1152.             } elseif ($strict || $admin !== $this->admin) {
  1153.                 throw $this->createNotFoundException(sprintf(
  1154.                     'Unable to find the %s object id of the admin "%s".',
  1155.                     $admin->getClassnameLabel(),
  1156.                     \get_class($admin)
  1157.                 ));
  1158.             }
  1159.             $admin $admin->isChild() ? $admin->getParent() : null;
  1160.         }
  1161.     }
  1162.     /**
  1163.      * @phpstan-return array{_tab?: string}
  1164.      */
  1165.     final protected function getSelectedTab(Request $request): array
  1166.     {
  1167.         return array_filter(['_tab' => (string) $request->request->get('_tab')]);
  1168.     }
  1169.     /**
  1170.      * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
  1171.      *
  1172.      * @param string[]|null $theme
  1173.      */
  1174.     final protected function setFormTheme(FormView $formView, ?array $theme null): void
  1175.     {
  1176.         $twig $this->container->get('twig');
  1177.         \assert($twig instanceof Environment);
  1178.         $formRenderer $twig->getRuntime(FormRenderer::class);
  1179.         $formRenderer->setTheme($formView$theme);
  1180.     }
  1181.     /**
  1182.      * @phpstan-param T $object
  1183.      */
  1184.     final protected function checkParentChildAssociation(Request $requestobject $object): void
  1185.     {
  1186.         if (!$this->admin->isChild()) {
  1187.             return;
  1188.         }
  1189.         $parentAdmin $this->admin->getParent();
  1190.         $parentId $request->get($parentAdmin->getIdParameter());
  1191.         $parentAdminObject $parentAdmin->getObject($parentId);
  1192.         if (null === $parentAdminObject) {
  1193.             throw new \RuntimeException(sprintf(
  1194.                 'No object was found in the admin "%s" for the id "%s".',
  1195.                 \get_class($parentAdmin),
  1196.                 $parentId
  1197.             ));
  1198.         }
  1199.         $parentAssociationMapping $this->admin->getParentAssociationMapping();
  1200.         if (null === $parentAssociationMapping) {
  1201.             throw new \RuntimeException('The admin has no parent association mapping.');
  1202.         }
  1203.         $propertyAccessor PropertyAccess::createPropertyAccessor();
  1204.         $propertyPath = new PropertyPath($parentAssociationMapping);
  1205.         $objectParent $propertyAccessor->getValue($object$propertyPath);
  1206.         // $objectParent may be an array or a Collection when the parent association is many to many.
  1207.         $parentObjectMatches $this->equalsOrContains($objectParent$parentAdminObject);
  1208.         if (!$parentObjectMatches) {
  1209.             throw new \RuntimeException(sprintf(
  1210.                 'There is no association between "%s" and "%s"',
  1211.                 $parentAdmin->toString($parentAdminObject),
  1212.                 $this->admin->toString($object)
  1213.             ));
  1214.         }
  1215.     }
  1216.     /**
  1217.      * Checks whether $needle is equal to $haystack or part of it.
  1218.      *
  1219.      * @param object|iterable<object> $haystack
  1220.      *
  1221.      * @return bool true when $haystack equals $needle or $haystack is iterable and contains $needle
  1222.      */
  1223.     private function equalsOrContains($haystackobject $needle): bool
  1224.     {
  1225.         if ($needle === $haystack) {
  1226.             return true;
  1227.         }
  1228.         if (is_iterable($haystack)) {
  1229.             foreach ($haystack as $haystackItem) {
  1230.                 if ($haystackItem === $needle) {
  1231.                     return true;
  1232.                 }
  1233.             }
  1234.         }
  1235.         return false;
  1236.     }
  1237. }