vendor/sonata-project/admin-bundle/src/Admin/AbstractAdmin.php line 554

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\Admin;
  12. use Knp\Menu\ItemInterface;
  13. use Sonata\AdminBundle\BCLayer\BCHelper;
  14. use Sonata\AdminBundle\Datagrid\DatagridInterface;
  15. use Sonata\AdminBundle\Datagrid\DatagridMapper;
  16. use Sonata\AdminBundle\Datagrid\ListMapper;
  17. use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
  18. use Sonata\AdminBundle\DependencyInjection\Admin\AbstractTaggedAdmin;
  19. use Sonata\AdminBundle\Exception\AdminClassNotFoundException;
  20. use Sonata\AdminBundle\FieldDescription\FieldDescriptionCollection;
  21. use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface;
  22. use Sonata\AdminBundle\Form\FormMapper;
  23. use Sonata\AdminBundle\Form\Type\ModelHiddenType;
  24. use Sonata\AdminBundle\Manipulator\ObjectManipulator;
  25. use Sonata\AdminBundle\Model\ProxyResolverInterface;
  26. use Sonata\AdminBundle\Object\Metadata;
  27. use Sonata\AdminBundle\Object\MetadataInterface;
  28. use Sonata\AdminBundle\Route\RouteCollection;
  29. use Sonata\AdminBundle\Route\RouteCollectionInterface;
  30. use Sonata\AdminBundle\Security\Acl\Permission\AdminPermissionMap;
  31. use Sonata\AdminBundle\Security\Handler\AclSecurityHandlerInterface;
  32. use Sonata\AdminBundle\Show\ShowMapper;
  33. use Sonata\AdminBundle\Util\Instantiator;
  34. use Sonata\AdminBundle\Util\ParametersManipulator;
  35. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  36. use Symfony\Component\Form\FormBuilderInterface;
  37. use Symfony\Component\Form\FormEvent;
  38. use Symfony\Component\Form\FormEvents;
  39. use Symfony\Component\Form\FormInterface;
  40. use Symfony\Component\HttpFoundation\InputBag;
  41. use Symfony\Component\HttpFoundation\ParameterBag;
  42. use Symfony\Component\HttpFoundation\Request;
  43. use Symfony\Component\PropertyAccess\Exception\AccessException;
  44. use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
  45. use Symfony\Component\PropertyAccess\PropertyAccess;
  46. use Symfony\Component\Routing\Generator\UrlGeneratorInterface as RoutingUrlGeneratorInterface;
  47. use Symfony\Component\Security\Acl\Model\DomainObjectInterface;
  48. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  49. /**
  50. * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
  51. *
  52. * @phpstan-template T of object
  53. * @phpstan-extends AbstractTaggedAdmin<T>
  54. * @phpstan-implements AdminInterface<T>
  55. */
  56. abstract class AbstractAdmin extends AbstractTaggedAdmin implements AdminInterface, DomainObjectInterface, AdminTreeInterface
  57. {
  58. // NEXT_MAJOR: Remove the CONTEXT constants.
  59. /** @deprecated */
  60. public const CONTEXT_MENU = 'menu';
  61. /** @deprecated */
  62. public const CONTEXT_DASHBOARD = 'dashboard';
  63. public const CLASS_REGEX =
  64. '@
  65. (?:([A-Za-z0-9]*)\\\)? # vendor name / app name
  66. (Bundle\\\)? # optional bundle directory
  67. ([A-Za-z0-9]+?)(?:Bundle)?\\\ # bundle name, with optional suffix
  68. (
  69. Entity|Document|Model|PHPCR|CouchDocument|Phpcr|
  70. Doctrine\\\Orm|Doctrine\\\Phpcr|Doctrine\\\MongoDB|Doctrine\\\CouchDB
  71. )\\\(.*)@x';
  72. private const ACTION_TREE = 1;
  73. private const ACTION_SHOW = 2;
  74. private const ACTION_EDIT = 4;
  75. private const ACTION_DELETE = 8;
  76. private const ACTION_ACL = 16;
  77. private const ACTION_HISTORY = 32;
  78. private const ACTION_LIST = 64;
  79. private const ACTION_BATCH = 128;
  80. private const INTERNAL_ACTIONS = [
  81. 'tree' => self::ACTION_TREE,
  82. 'show' => self::ACTION_SHOW,
  83. 'edit' => self::ACTION_EDIT,
  84. 'delete' => self::ACTION_DELETE,
  85. 'acl' => self::ACTION_ACL,
  86. 'history' => self::ACTION_HISTORY,
  87. 'list' => self::ACTION_LIST,
  88. 'batch' => self::ACTION_BATCH,
  89. ];
  90. private const MASK_OF_ACTION_CREATE = self::ACTION_TREE | self::ACTION_SHOW | self::ACTION_EDIT | self::ACTION_DELETE | self::ACTION_LIST | self::ACTION_BATCH;
  91. private const MASK_OF_ACTION_SHOW = self::ACTION_EDIT | self::ACTION_HISTORY | self::ACTION_ACL;
  92. private const MASK_OF_ACTION_EDIT = self::ACTION_SHOW | self::ACTION_DELETE | self::ACTION_ACL | self::ACTION_HISTORY;
  93. private const MASK_OF_ACTION_HISTORY = self::ACTION_SHOW | self::ACTION_EDIT | self::ACTION_ACL;
  94. private const MASK_OF_ACTION_ACL = self::ACTION_EDIT | self::ACTION_HISTORY;
  95. private const MASK_OF_ACTION_LIST = self::ACTION_SHOW | self::ACTION_EDIT | self::ACTION_DELETE | self::ACTION_ACL | self::ACTION_BATCH;
  96. private const MASK_OF_ACTIONS_USING_OBJECT = self::MASK_OF_ACTION_SHOW | self::MASK_OF_ACTION_EDIT | self::MASK_OF_ACTION_HISTORY | self::MASK_OF_ACTION_ACL;
  97. private const DEFAULT_LIST_PER_PAGE_RESULTS = 25;
  98. private const DEFAULT_LIST_PER_PAGE_OPTIONS = [10, 25, 50, 100, 250];
  99. /**
  100. * @deprecated since sonata-project/admin-bundle 4.15, will be removed in 5.0.
  101. *
  102. * The base route name used to generate the routing information.
  103. *
  104. * @var string|null
  105. */
  106. protected $baseRouteName;
  107. /**
  108. * @deprecated since sonata-project/admin-bundle 4.15, will be removed in 5.0.
  109. *
  110. * The base route pattern used to generate the routing information.
  111. *
  112. * @var string|null
  113. */
  114. protected $baseRoutePattern;
  115. /**
  116. * The label class name (used in the title/breadcrumb ...).
  117. *
  118. * @var string|null
  119. */
  120. protected $classnameLabel;
  121. /**
  122. * Setting to true will enable preview mode for
  123. * the entity and show a preview button in the
  124. * edit/create forms.
  125. *
  126. * @var bool
  127. */
  128. protected $supportsPreviewMode = false;
  129. /**
  130. * The list FieldDescription constructed from the configureListField method.
  131. *
  132. * @var array<string, FieldDescriptionInterface>
  133. */
  134. private array $listFieldDescriptions = [];
  135. /**
  136. * The show FieldDescription constructed from the configureShowFields method.
  137. *
  138. * @var FieldDescriptionInterface[]
  139. */
  140. private array $showFieldDescriptions = [];
  141. /**
  142. * The list FieldDescription constructed from the configureFormField method.
  143. *
  144. * @var FieldDescriptionInterface[]
  145. */
  146. private array $formFieldDescriptions = [];
  147. /**
  148. * The filter FieldDescription constructed from the configureFilterField method.
  149. *
  150. * @var FieldDescriptionInterface[]
  151. */
  152. private array $filterFieldDescriptions = [];
  153. /**
  154. * The maximum number of page numbers to display in the list.
  155. */
  156. private int $maxPageLinks = 25;
  157. /**
  158. * The translation domain to be used to translate messages.
  159. */
  160. private string $translationDomain = 'messages';
  161. /**
  162. * Array of routes related to this admin.
  163. */
  164. private ?RouteCollectionInterface $routes = null;
  165. /**
  166. * The subject only set in edit/update/create mode.
  167. *
  168. * @phpstan-var T|null
  169. */
  170. private ?object $subject = null;
  171. /**
  172. * Define a Collection of child admin, ie /admin/order/{id}/order-element/{childId}.
  173. *
  174. * @var array<string, AdminInterface<object>>
  175. */
  176. private array $children = [];
  177. /**
  178. * Reference the parent admin.
  179. *
  180. * @var AdminInterface<object>|null
  181. */
  182. private ?AdminInterface $parent = null;
  183. /**
  184. * Reference the parent FieldDescription related to this admin
  185. * only set for FieldDescription which is associated to an Sub Admin instance.
  186. */
  187. private ?FieldDescriptionInterface $parentFieldDescription = null;
  188. /**
  189. * If true then the current admin is part of the nested admin set (from the url).
  190. */
  191. private bool $currentChild = false;
  192. /**
  193. * The uniqId is used to avoid clashing with 2 admin related to the code
  194. * ie: a Block linked to a Block.
  195. */
  196. private ?string $uniqId = null;
  197. /**
  198. * The current request object.
  199. */
  200. private ?Request $request = null;
  201. /**
  202. * @phpstan-var DatagridInterface<ProxyQueryInterface<T>>|null
  203. */
  204. private ?DatagridInterface $datagrid = null;
  205. private ?ItemInterface $menu = null;
  206. /**
  207. * @var string[]
  208. */
  209. private array $formTheme = [];
  210. /**
  211. * @var string[]
  212. */
  213. private array $filterTheme = [];
  214. /**
  215. * @var AdminExtensionInterface[]
  216. *
  217. * @phpstan-var array<AdminExtensionInterface<T>>
  218. */
  219. private array $extensions = [];
  220. /**
  221. * @var array<string, bool>
  222. */
  223. private array $cacheIsGranted = [];
  224. /**
  225. * @var array<string, string|null>
  226. */
  227. private array $parentAssociationMapping = [];
  228. /**
  229. * The subclasses supported by the admin class.
  230. *
  231. * @var string[]
  232. *
  233. * @phpstan-var array<string, class-string<T>>
  234. */
  235. private array $subClasses = [];
  236. /**
  237. * The list collection.
  238. *
  239. * @var FieldDescriptionCollection<FieldDescriptionInterface>|null
  240. */
  241. private ?FieldDescriptionCollection $list = null;
  242. /**
  243. * @var FieldDescriptionCollection<FieldDescriptionInterface>|null
  244. */
  245. private ?FieldDescriptionCollection $show = null;
  246. private ?FormInterface $form = null;
  247. /**
  248. * The cached base route name.
  249. */
  250. private ?string $cachedBaseRouteName = null;
  251. /**
  252. * The cached base route pattern.
  253. */
  254. private ?string $cachedBaseRoutePattern = null;
  255. /**
  256. * The form group disposition.
  257. *
  258. * @var array<string, array<string, mixed>>
  259. */
  260. private array $formGroups = [];
  261. /**
  262. * The form tabs disposition.
  263. *
  264. * @var array<string, array<string, mixed>>
  265. */
  266. private array $formTabs = [];
  267. /**
  268. * The view group disposition.
  269. *
  270. * @var array<string, array<string, mixed>>
  271. */
  272. private array $showGroups = [];
  273. /**
  274. * The view tab disposition.
  275. *
  276. * @var array<string, array<string, mixed>>
  277. */
  278. private array $showTabs = [];
  279. /**
  280. * @var array<string, bool>
  281. */
  282. private array $loaded = [
  283. 'routes' => false,
  284. 'tab_menu' => false,
  285. 'show' => false,
  286. 'list' => false,
  287. 'form' => false,
  288. 'datagrid' => false,
  289. ];
  290. public function getExportFormats(): array
  291. {
  292. return [];
  293. }
  294. final public function getExportFields(): array
  295. {
  296. $fields = $this->configureExportFields();
  297. foreach ($this->getExtensions() as $extension) {
  298. $fields = $extension->configureExportFields($this, $fields);
  299. }
  300. return $fields;
  301. }
  302. final public function getDataSourceIterator(): \Iterator
  303. {
  304. $datagrid = $this->getDatagrid();
  305. $datagrid->buildPager();
  306. $fields = [];
  307. foreach ($this->getExportFields() as $key => $field) {
  308. if (!\is_string($key)) {
  309. $label = $this->getTranslationLabel($field, 'export', 'label');
  310. $key = $this->getTranslator()->trans($label, [], $this->getTranslationDomain());
  311. }
  312. $fields[$key] = $field;
  313. }
  314. $query = $datagrid->getQuery();
  315. return $this->getDataSource()->createIterator($query, $fields);
  316. }
  317. final public function initialize(): void
  318. {
  319. if (null === $this->classnameLabel) {
  320. $namespaceSeparatorPos = strrpos($this->getClass(), '\\');
  321. $this->classnameLabel = false !== $namespaceSeparatorPos
  322. ? substr($this->getClass(), $namespaceSeparatorPos + 1)
  323. : $this->getClass();
  324. }
  325. $this->configure();
  326. foreach ($this->getExtensions() as $extension) {
  327. $extension->configure($this);
  328. }
  329. }
  330. final public function update(object $object): object
  331. {
  332. $this->preUpdate($object);
  333. foreach ($this->getExtensions() as $extension) {
  334. $extension->preUpdate($this, $object);
  335. }
  336. $this->getModelManager()->update($object);
  337. $this->postUpdate($object);
  338. foreach ($this->getExtensions() as $extension) {
  339. $extension->postUpdate($this, $object);
  340. }
  341. return $object;
  342. }
  343. final public function create(object $object): object
  344. {
  345. $this->prePersist($object);
  346. foreach ($this->getExtensions() as $extension) {
  347. $extension->prePersist($this, $object);
  348. }
  349. $this->getModelManager()->create($object);
  350. $this->postPersist($object);
  351. foreach ($this->getExtensions() as $extension) {
  352. $extension->postPersist($this, $object);
  353. }
  354. $this->createObjectSecurity($object);
  355. return $object;
  356. }
  357. final public function delete(object $object): void
  358. {
  359. $this->preRemove($object);
  360. foreach ($this->getExtensions() as $extension) {
  361. $extension->preRemove($this, $object);
  362. }
  363. $this->getSecurityHandler()->deleteObjectSecurity($this, $object);
  364. $this->getModelManager()->delete($object);
  365. $this->postRemove($object);
  366. foreach ($this->getExtensions() as $extension) {
  367. $extension->postRemove($this, $object);
  368. }
  369. }
  370. public function preBatchAction(string $actionName, ProxyQueryInterface $query, array &$idx, bool $allElements = false): void
  371. {
  372. }
  373. final public function getDefaultFilterParameters(): array
  374. {
  375. return array_merge(
  376. $this->getDefaultSortValues(),
  377. $this->getDefaultFilterValues()
  378. );
  379. }
  380. final public function getFilterParameters(): array
  381. {
  382. $parameters = $this->getDefaultFilterParameters();
  383. // build the values array
  384. if ($this->hasRequest()) {
  385. /** @var InputBag|ParameterBag $bag */
  386. $bag = $this->getRequest()->query;
  387. if ($bag instanceof InputBag) {
  388. // symfony 5.1+
  389. $filters = $bag->all('filter');
  390. } else {
  391. $filters = $bag->get('filter', []);
  392. \assert(\is_array($filters));
  393. }
  394. if (isset($filters[DatagridInterface::PAGE])) {
  395. $filters[DatagridInterface::PAGE] = (int) $filters[DatagridInterface::PAGE];
  396. }
  397. if (isset($filters[DatagridInterface::PER_PAGE])) {
  398. $filters[DatagridInterface::PER_PAGE] = (int) $filters[DatagridInterface::PER_PAGE];
  399. }
  400. // if filter persistence is configured
  401. if ($this->hasFilterPersister()) {
  402. // if reset filters is asked, remove from storage
  403. if ('reset' === $this->getRequest()->query->get('filters')) {
  404. $this->getFilterPersister()->reset($this->getCode());
  405. }
  406. // if no filters, fetch from storage
  407. // otherwise save to storage
  408. if ([] === $filters) {
  409. $filters = $this->getFilterPersister()->get($this->getCode());
  410. } else {
  411. $this->getFilterPersister()->set($this->getCode(), $filters);
  412. }
  413. }
  414. $parameters = ParametersManipulator::merge($parameters, $filters);
  415. // always force the parent value
  416. if ($this->isChild()) {
  417. $parentAssociationMapping = $this->getParentAssociationMapping();
  418. if (null !== $parentAssociationMapping) {
  419. $name = str_replace('.', '__', $parentAssociationMapping);
  420. $parameters[$name] = ['value' => $this->getRequest()->get($this->getParent()->getIdParameter())];
  421. }
  422. }
  423. }
  424. if (
  425. !isset($parameters[DatagridInterface::PER_PAGE])
  426. || !\is_int($parameters[DatagridInterface::PER_PAGE])
  427. || !$this->determinedPerPageValue($parameters[DatagridInterface::PER_PAGE])
  428. ) {
  429. $parameters[DatagridInterface::PER_PAGE] = $this->getMaxPerPage();
  430. }
  431. $parameters = $this->configureFilterParameters($parameters);
  432. foreach ($this->getExtensions() as $extension) {
  433. $parameters = $extension->configureFilterParameters($this, $parameters);
  434. }
  435. return $parameters;
  436. }
  437. /**
  438. * Returns the name of the parent related field, so the field can be use to set the default
  439. * value (ie the parent object) or to filter the object.
  440. *
  441. * @throws \LogicException
  442. */
  443. final public function getParentAssociationMapping(): ?string
  444. {
  445. if (!$this->isChild()) {
  446. throw new \LogicException(sprintf(
  447. 'Admin "%s" has no parent.',
  448. static::class
  449. ));
  450. }
  451. $parent = $this->getParent()->getCode();
  452. return $this->parentAssociationMapping[$parent];
  453. }
  454. final public function getBaseRoutePattern(): string
  455. {
  456. if (null !== $this->cachedBaseRoutePattern) {
  457. return $this->cachedBaseRoutePattern;
  458. }
  459. if ($this->isChild()) { // the admin class is a child, prefix it with the parent route pattern
  460. $this->cachedBaseRoutePattern = sprintf(
  461. '%s/%s/%s',
  462. $this->getParent()->getBaseRoutePattern(),
  463. $this->getParent()->getRouterIdParameter(),
  464. $this->generateBaseRoutePattern(true)
  465. );
  466. } else {
  467. $this->cachedBaseRoutePattern = $this->generateBaseRoutePattern();
  468. }
  469. return $this->cachedBaseRoutePattern;
  470. }
  471. /**
  472. * Returns the baseRouteName used to generate the routing information.
  473. *
  474. * @return string the baseRouteName used to generate the routing information
  475. */
  476. final public function getBaseRouteName(): string
  477. {
  478. if (null !== $this->cachedBaseRouteName) {
  479. return $this->cachedBaseRouteName;
  480. }
  481. if ($this->isChild()) { // the admin class is a child, prefix it with the parent route name
  482. $this->cachedBaseRouteName = sprintf(
  483. '%s_%s',
  484. $this->getParent()->getBaseRouteName(),
  485. $this->generateBaseRouteName(true)
  486. );
  487. } else {
  488. $this->cachedBaseRouteName = $this->generateBaseRouteName();
  489. }
  490. return $this->cachedBaseRouteName;
  491. }
  492. final public function getClass(): string
  493. {
  494. if ($this->hasActiveSubClass()) {
  495. if ($this->hasParentFieldDescription()) {
  496. throw new \LogicException('Feature not implemented: an embedded admin cannot have subclass');
  497. }
  498. $subClass = $this->getRequest()->query->get('subclass');
  499. \assert(\is_string($subClass));
  500. if (!$this->hasSubClass($subClass)) {
  501. throw new \LogicException(sprintf('Subclass "%s" is not defined.', $subClass));
  502. }
  503. return $this->getSubClass($subClass);
  504. }
  505. // Do not use `$this->hasSubject()` and `$this->getSubject()` here to avoid infinite loop.
  506. // `getSubject` use `hasSubject()` which use `getObject()` which use `getClass()`.
  507. if (null !== $this->subject) {
  508. $modelManager = $this->getModelManager();
  509. /** @phpstan-var class-string<T> $class */
  510. $class = $modelManager instanceof ProxyResolverInterface
  511. ? $modelManager->getRealClass($this->subject)
  512. // NEXT_MAJOR: Change to `\get_class($this->subject)` instead
  513. : BCHelper::getClass($this->subject);
  514. return $class;
  515. }
  516. return $this->getModelClass();
  517. }
  518. final public function getSubClasses(): array
  519. {
  520. return $this->subClasses;
  521. }
  522. final public function setSubClasses(array $subClasses): void
  523. {
  524. $this->subClasses = $subClasses;
  525. }
  526. final public function hasSubClass(string $name): bool
  527. {
  528. return isset($this->subClasses[$name]);
  529. }
  530. final public function hasActiveSubClass(): bool
  531. {
  532. if (\count($this->subClasses) > 0 && $this->hasRequest()) {
  533. return \is_string($this->getRequest()->query->get('subclass'));
  534. }
  535. return false;
  536. }
  537. final public function getActiveSubClass(): string
  538. {
  539. if (!$this->hasActiveSubClass()) {
  540. throw new \LogicException(sprintf(
  541. 'Admin "%s" has no active subclass.',
  542. static::class
  543. ));
  544. }
  545. return $this->getSubClass($this->getActiveSubclassCode());
  546. }
  547. final public function getActiveSubclassCode(): string
  548. {
  549. if (!$this->hasActiveSubClass()) {
  550. throw new \LogicException(sprintf(
  551. 'Admin "%s" has no active subclass.',
  552. static::class
  553. ));
  554. }
  555. $subClass = (string) $this->getRequest()->query->get('subclass');
  556. if (!$this->hasSubClass($subClass)) {
  557. throw new \LogicException(sprintf(
  558. 'Admin "%s" has no active subclass.',
  559. static::class
  560. ));
  561. }
  562. return $subClass;
  563. }
  564. final public function getBatchActions(): array
  565. {
  566. if (!$this->hasRoute('batch')) {
  567. return [];
  568. }
  569. $actions = [];
  570. if ($this->hasRoute('delete') && $this->hasAccess('delete')) {
  571. $actions['delete'] = [
  572. 'label' => 'action_delete',
  573. 'translation_domain' => 'SonataAdminBundle',
  574. 'ask_confirmation' => true, // by default always true
  575. ];
  576. }
  577. $actions = $this->configureBatchActions($actions);
  578. foreach ($this->getExtensions() as $extension) {
  579. $actions = $extension->configureBatchActions($this, $actions);
  580. }
  581. foreach ($actions as $name => &$action) {
  582. if (!\array_key_exists('label', $action)) {
  583. $action['label'] = $this->getTranslationLabel($name, 'batch', 'label');
  584. }
  585. if (!\array_key_exists('translation_domain', $action)) {
  586. $action['translation_domain'] = $this->getTranslationDomain();
  587. }
  588. }
  589. return $actions;
  590. }
  591. final public function getRoutes(): RouteCollectionInterface
  592. {
  593. $routes = $this->buildRoutes();
  594. if (null === $routes) {
  595. throw new \LogicException('Cannot access routes during the building process.');
  596. }
  597. return $routes;
  598. }
  599. public function getRouterIdParameter(): string
  600. {
  601. return sprintf('{%s}', $this->getIdParameter());
  602. }
  603. public function getIdParameter(): string
  604. {
  605. $parameter = 'id';
  606. for ($i = 0; $i < $this->getChildDepth(); ++$i) {
  607. $parameter = sprintf('child%s', ucfirst($parameter));
  608. }
  609. return $parameter;
  610. }
  611. final public function hasRoute(string $name): bool
  612. {
  613. return $this->getRouteGenerator()->hasAdminRoute($this, $name);
  614. }
  615. final public function isCurrentRoute(string $name, ?string $adminCode = null): bool
  616. {
  617. if (!$this->hasRequest()) {
  618. return false;
  619. }
  620. $request = $this->getRequest();
  621. $route = $request->get('_route');
  622. if (null !== $adminCode) {
  623. $pool = $this->getConfigurationPool();
  624. if ($pool->hasAdminByAdminCode($adminCode)) {
  625. $admin = $pool->getAdminByAdminCode($adminCode);
  626. } else {
  627. return false;
  628. }
  629. } else {
  630. $admin = $this;
  631. }
  632. return $admin->getRoutes()->getRouteName($name) === $route;
  633. }
  634. final public function generateObjectUrl(string $name, object $object, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): string
  635. {
  636. $parameters[$this->getIdParameter()] = $this->getUrlSafeIdentifier($object);
  637. return $this->generateUrl($name, $parameters, $referenceType);
  638. }
  639. final public function generateUrl(string $name, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): string
  640. {
  641. return $this->getRouteGenerator()->generateUrl($this, $name, $parameters, $referenceType);
  642. }
  643. final public function generateMenuUrl(string $name, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): array
  644. {
  645. return $this->getRouteGenerator()->generateMenuUrl($this, $name, $parameters, $referenceType);
  646. }
  647. final public function getNewInstance(): object
  648. {
  649. $object = $this->createNewInstance();
  650. $this->alterNewInstance($object);
  651. foreach ($this->getExtensions() as $extension) {
  652. $extension->alterNewInstance($this, $object);
  653. }
  654. return $object;
  655. }
  656. final public function getFormBuilder(): FormBuilderInterface
  657. {
  658. $formBuilder = $this->getFormContractor()->getFormBuilder(
  659. $this->getUniqId(),
  660. ['data_class' => $this->getClass()] + $this->getFormOptions(),
  661. );
  662. $this->defineFormBuilder($formBuilder);
  663. return $formBuilder;
  664. }
  665. /**
  666. * This method is being called by the main admin class and the child class,
  667. * the getFormBuilder is only call by the main admin class.
  668. */
  669. final public function defineFormBuilder(FormBuilderInterface $formBuilder): void
  670. {
  671. if (!$this->hasSubject()) {
  672. throw new \LogicException(sprintf(
  673. 'Admin "%s" has no subject.',
  674. static::class
  675. ));
  676. }
  677. $mapper = new FormMapper($this->getFormContractor(), $formBuilder, $this);
  678. $this->configureFormFields($mapper);
  679. foreach ($this->getExtensions() as $extension) {
  680. $extension->configureFormFields($mapper);
  681. }
  682. }
  683. final public function attachAdminClass(FieldDescriptionInterface $fieldDescription): void
  684. {
  685. $pool = $this->getConfigurationPool();
  686. try {
  687. $admin = $pool->getAdminByFieldDescription($fieldDescription);
  688. } catch (AdminClassNotFoundException) {
  689. // Using a fieldDescription with no admin class for the target model is a valid case.
  690. // Since there is no easy way to check for this case, we catch the exception instead.
  691. return;
  692. }
  693. if ($this->hasRequest()) {
  694. $admin->setRequest($this->getRequest());
  695. }
  696. $fieldDescription->setAssociationAdmin($admin);
  697. }
  698. /**
  699. * @param string|int|null $id
  700. *
  701. * @phpstan-return T|null
  702. */
  703. final public function getObject($id): ?object
  704. {
  705. if (null === $id) {
  706. return null;
  707. }
  708. $object = $this->getModelManager()->find($this->getClass(), $id);
  709. if (null === $object) {
  710. return null;
  711. }
  712. $this->alterObject($object);
  713. foreach ($this->getExtensions() as $extension) {
  714. $extension->alterObject($this, $object);
  715. }
  716. return $object;
  717. }
  718. final public function getForm(): FormInterface
  719. {
  720. $form = $this->buildForm();
  721. if (null === $form) {
  722. throw new \LogicException('Cannot access form during the building process.');
  723. }
  724. return $form;
  725. }
  726. final public function getList(): FieldDescriptionCollection
  727. {
  728. $list = $this->buildList();
  729. if (null === $list) {
  730. throw new \LogicException('Cannot access list during the building process.');
  731. }
  732. return $list;
  733. }
  734. final public function createQuery(): ProxyQueryInterface
  735. {
  736. $query = $this->getModelManager()->createQuery($this->getClass());
  737. $query = $this->configureQuery($query);
  738. foreach ($this->getExtensions() as $extension) {
  739. $extension->configureQuery($this, $query);
  740. }
  741. return $query;
  742. }
  743. final public function getDatagrid(): DatagridInterface
  744. {
  745. $datagrid = $this->buildDatagrid();
  746. if (null === $datagrid) {
  747. throw new \LogicException('Cannot access datagrid during the building process.');
  748. }
  749. return $datagrid;
  750. }
  751. final public function getSideMenu(string $action, ?AdminInterface $childAdmin = null): ItemInterface
  752. {
  753. if ($this->isChild()) {
  754. return $this->getParent()->getSideMenu($action, $this);
  755. }
  756. $menu = $this->buildTabMenu($action, $childAdmin);
  757. if (null === $menu) {
  758. throw new \LogicException('Cannot access menu during the building process.');
  759. }
  760. return $menu;
  761. }
  762. final public function getRootCode(): string
  763. {
  764. return $this->getRoot()->getCode();
  765. }
  766. final public function getRoot(): AdminInterface
  767. {
  768. if (!$this->hasParentFieldDescription()) {
  769. return $this;
  770. }
  771. return $this->getParentFieldDescription()->getAdmin()->getRoot();
  772. }
  773. final public function getMaxPerPage(): int
  774. {
  775. $sortValues = $this->getDefaultSortValues();
  776. return $sortValues[DatagridInterface::PER_PAGE] ?? self::DEFAULT_LIST_PER_PAGE_RESULTS;
  777. }
  778. final public function setMaxPageLinks(int $maxPageLinks): void
  779. {
  780. $this->maxPageLinks = $maxPageLinks;
  781. }
  782. final public function getMaxPageLinks(): int
  783. {
  784. return $this->maxPageLinks;
  785. }
  786. final public function getFormGroups(): array
  787. {
  788. return $this->formGroups;
  789. }
  790. final public function setFormGroups(array $formGroups): void
  791. {
  792. $this->formGroups = $formGroups;
  793. }
  794. final public function removeFieldFromFormGroup(string $key): void
  795. {
  796. foreach ($this->formGroups as $name => $_formGroup) {
  797. unset($this->formGroups[$name]['fields'][$key]);
  798. if ([] === $this->formGroups[$name]['fields']) {
  799. unset($this->formGroups[$name]);
  800. }
  801. }
  802. }
  803. final public function reorderFormGroup(string $group, array $keys): void
  804. {
  805. $formGroups = $this->getFormGroups();
  806. $formGroups[$group]['fields'] = array_merge(array_flip($keys), $formGroups[$group]['fields']);
  807. $this->setFormGroups($formGroups);
  808. }
  809. final public function getFormTabs(): array
  810. {
  811. return $this->formTabs;
  812. }
  813. final public function setFormTabs(array $formTabs): void
  814. {
  815. $this->formTabs = $formTabs;
  816. }
  817. final public function getShowTabs(): array
  818. {
  819. return $this->showTabs;
  820. }
  821. final public function setShowTabs(array $showTabs): void
  822. {
  823. $this->showTabs = $showTabs;
  824. }
  825. final public function getShowGroups(): array
  826. {
  827. return $this->showGroups;
  828. }
  829. final public function setShowGroups(array $showGroups): void
  830. {
  831. $this->showGroups = $showGroups;
  832. }
  833. final public function removeFieldFromShowGroup(string $key): void
  834. {
  835. foreach ($this->showGroups as $name => $_showGroup) {
  836. unset($this->showGroups[$name]['fields'][$key]);
  837. if ([] === $this->showGroups[$name]['fields']) {
  838. unset($this->showGroups[$name]);
  839. }
  840. }
  841. }
  842. final public function reorderShowGroup(string $group, array $keys): void
  843. {
  844. $showGroups = $this->getShowGroups();
  845. $showGroups[$group]['fields'] = array_merge(array_flip($keys), $showGroups[$group]['fields']);
  846. $this->setShowGroups($showGroups);
  847. }
  848. final public function setParentFieldDescription(FieldDescriptionInterface $parentFieldDescription): void
  849. {
  850. $this->parentFieldDescription = $parentFieldDescription;
  851. }
  852. final public function getParentFieldDescription(): FieldDescriptionInterface
  853. {
  854. if (!$this->hasParentFieldDescription()) {
  855. throw new \LogicException(sprintf(
  856. 'Admin "%s" has no parent field description.',
  857. static::class
  858. ));
  859. }
  860. return $this->parentFieldDescription;
  861. }
  862. /**
  863. * @phpstan-assert-if-true !null $this->parentFieldDescription
  864. */
  865. final public function hasParentFieldDescription(): bool
  866. {
  867. return null !== $this->parentFieldDescription;
  868. }
  869. final public function setSubject(?object $subject): void
  870. {
  871. if (null !== $subject && !is_a($subject, $this->getModelClass(), true)) {
  872. throw new \LogicException(sprintf(
  873. 'Admin "%s" does not allow this subject: %s, use the one register with this admin class %s',
  874. static::class,
  875. $subject::class,
  876. $this->getModelClass()
  877. ));
  878. }
  879. $this->subject = $subject;
  880. }
  881. final public function getSubject(): object
  882. {
  883. if (!$this->hasSubject()) {
  884. throw new \LogicException(sprintf(
  885. 'Admin "%s" has no subject.',
  886. static::class
  887. ));
  888. }
  889. return $this->subject;
  890. }
  891. /**
  892. * @phpstan-assert-if-true !null $this->subject
  893. */
  894. final public function hasSubject(): bool
  895. {
  896. if (null === $this->subject && $this->hasRequest() && !$this->hasParentFieldDescription()) {
  897. $id = $this->getRequest()->get($this->getIdParameter());
  898. if (null !== $id) {
  899. $this->subject = $this->getObject($id);
  900. }
  901. }
  902. return null !== $this->subject;
  903. }
  904. final public function getFormFieldDescriptions(): array
  905. {
  906. $this->buildForm();
  907. return $this->formFieldDescriptions;
  908. }
  909. final public function getFormFieldDescription(string $name): FieldDescriptionInterface
  910. {
  911. $this->buildForm();
  912. if (!$this->hasFormFieldDescription($name)) {
  913. throw new \LogicException(sprintf(
  914. 'Admin "%s" has no form field description for the field %s.',
  915. static::class,
  916. $name
  917. ));
  918. }
  919. return $this->formFieldDescriptions[$name];
  920. }
  921. /**
  922. * Returns true if the admin has a FieldDescription with the given $name.
  923. */
  924. final public function hasFormFieldDescription(string $name): bool
  925. {
  926. $this->buildForm();
  927. return \array_key_exists($name, $this->formFieldDescriptions);
  928. }
  929. final public function addFormFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
  930. {
  931. $this->formFieldDescriptions[$name] = $fieldDescription;
  932. }
  933. /**
  934. * remove a FieldDescription.
  935. */
  936. final public function removeFormFieldDescription(string $name): void
  937. {
  938. unset($this->formFieldDescriptions[$name]);
  939. }
  940. /**
  941. * build and return the collection of form FieldDescription.
  942. *
  943. * @return FieldDescriptionInterface[] collection of form FieldDescription
  944. */
  945. final public function getShowFieldDescriptions(): array
  946. {
  947. $this->buildShow();
  948. return $this->showFieldDescriptions;
  949. }
  950. /**
  951. * Returns the form FieldDescription with the given $name.
  952. */
  953. final public function getShowFieldDescription(string $name): FieldDescriptionInterface
  954. {
  955. $this->buildShow();
  956. if (!$this->hasShowFieldDescription($name)) {
  957. throw new \LogicException(sprintf(
  958. 'Admin "%s" has no show field description for the field %s.',
  959. static::class,
  960. $name
  961. ));
  962. }
  963. return $this->showFieldDescriptions[$name];
  964. }
  965. final public function hasShowFieldDescription(string $name): bool
  966. {
  967. $this->buildShow();
  968. return \array_key_exists($name, $this->showFieldDescriptions);
  969. }
  970. final public function addShowFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
  971. {
  972. $this->showFieldDescriptions[$name] = $fieldDescription;
  973. }
  974. final public function removeShowFieldDescription(string $name): void
  975. {
  976. unset($this->showFieldDescriptions[$name]);
  977. }
  978. final public function getListFieldDescriptions(): array
  979. {
  980. $this->buildList();
  981. return $this->listFieldDescriptions;
  982. }
  983. final public function getListFieldDescription(string $name): FieldDescriptionInterface
  984. {
  985. $this->buildList();
  986. if (!$this->hasListFieldDescription($name)) {
  987. throw new \LogicException(sprintf(
  988. 'Admin "%s" has no list field description for %s.',
  989. static::class,
  990. $name
  991. ));
  992. }
  993. return $this->listFieldDescriptions[$name];
  994. }
  995. final public function hasListFieldDescription(string $name): bool
  996. {
  997. $this->buildList();
  998. return \array_key_exists($name, $this->listFieldDescriptions);
  999. }
  1000. final public function addListFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
  1001. {
  1002. $this->listFieldDescriptions[$name] = $fieldDescription;
  1003. }
  1004. final public function removeListFieldDescription(string $name): void
  1005. {
  1006. unset($this->listFieldDescriptions[$name]);
  1007. }
  1008. final public function getFilterFieldDescription(string $name): FieldDescriptionInterface
  1009. {
  1010. $this->buildDatagrid();
  1011. if (!$this->hasFilterFieldDescription($name)) {
  1012. throw new \LogicException(sprintf(
  1013. 'Admin "%s" has no filter field description for the field %s.',
  1014. static::class,
  1015. $name
  1016. ));
  1017. }
  1018. return $this->filterFieldDescriptions[$name];
  1019. }
  1020. final public function hasFilterFieldDescription(string $name): bool
  1021. {
  1022. $this->buildDatagrid();
  1023. return \array_key_exists($name, $this->filterFieldDescriptions);
  1024. }
  1025. final public function addFilterFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
  1026. {
  1027. $this->filterFieldDescriptions[$name] = $fieldDescription;
  1028. }
  1029. final public function removeFilterFieldDescription(string $name): void
  1030. {
  1031. unset($this->filterFieldDescriptions[$name]);
  1032. }
  1033. final public function getFilterFieldDescriptions(): array
  1034. {
  1035. $this->buildDatagrid();
  1036. return $this->filterFieldDescriptions;
  1037. }
  1038. /**
  1039. * @psalm-suppress PossiblyNullArgument Will be solved in NEXT_MAJOR
  1040. */
  1041. final public function addChild(AdminInterface $child, ?string $field = null): void
  1042. {
  1043. $parentAdmin = $this;
  1044. while ($parentAdmin->isChild() && $parentAdmin->getCode() !== $child->getCode()) {
  1045. $parentAdmin = $parentAdmin->getParent();
  1046. }
  1047. if ($parentAdmin->getCode() === $child->getCode()) {
  1048. throw new \LogicException(sprintf(
  1049. 'Circular reference detected! The child admin `%s` is already in the parent tree of the `%s` admin.',
  1050. $child->getCode(),
  1051. $this->getCode()
  1052. ));
  1053. }
  1054. $this->children[$child->getCode()] = $child;
  1055. // @phpstan-ignore-next-line Will be solved in NEXT_MAJOR
  1056. $child->setParent($this, $field);
  1057. }
  1058. final public function hasChild(string $code): bool
  1059. {
  1060. return isset($this->children[$code]);
  1061. }
  1062. final public function getChildren(): array
  1063. {
  1064. return $this->children;
  1065. }
  1066. final public function getChild(string $code): AdminInterface
  1067. {
  1068. if (!$this->hasChild($code)) {
  1069. throw new \LogicException(sprintf(
  1070. 'Admin "%s" has no child for the code %s.',
  1071. static::class,
  1072. $code
  1073. ));
  1074. }
  1075. return $this->getChildren()[$code];
  1076. }
  1077. final public function setParent(AdminInterface $parent, ?string $parentAssociationMapping = null): void
  1078. {
  1079. $this->parent = $parent;
  1080. $this->parentAssociationMapping[$parent->getCode()] = $parentAssociationMapping;
  1081. }
  1082. final public function getParent(): AdminInterface
  1083. {
  1084. if (null === $this->parent) {
  1085. throw new \LogicException(sprintf(
  1086. 'Admin "%s" has no parent.',
  1087. static::class
  1088. ));
  1089. }
  1090. return $this->parent;
  1091. }
  1092. final public function getRootAncestor(): AdminInterface
  1093. {
  1094. $parent = $this;
  1095. while ($parent->isChild()) {
  1096. $parent = $parent->getParent();
  1097. }
  1098. return $parent;
  1099. }
  1100. final public function getChildDepth(): int
  1101. {
  1102. $parent = $this;
  1103. $depth = 0;
  1104. while ($parent->isChild()) {
  1105. $parent = $parent->getParent();
  1106. ++$depth;
  1107. }
  1108. return $depth;
  1109. }
  1110. final public function getCurrentLeafChildAdmin(): ?AdminInterface
  1111. {
  1112. $child = $this->getCurrentChildAdmin();
  1113. if (null === $child) {
  1114. return null;
  1115. }
  1116. for ($c = $child; null !== $c; $c = $child->getCurrentChildAdmin()) {
  1117. $child = $c;
  1118. }
  1119. return $child;
  1120. }
  1121. final public function isChild(): bool
  1122. {
  1123. return $this->parent instanceof AdminInterface;
  1124. }
  1125. /**
  1126. * Returns true if the admin has children, false otherwise.
  1127. *
  1128. * @phpstan-assert-if-true non-empty-array $this->children
  1129. */
  1130. final public function hasChildren(): bool
  1131. {
  1132. return \count($this->children) > 0;
  1133. }
  1134. final public function setUniqId(string $uniqId): void
  1135. {
  1136. $this->uniqId = $uniqId;
  1137. }
  1138. final public function getUniqId(): string
  1139. {
  1140. if (null === $this->uniqId) {
  1141. $this->uniqId = sprintf('s%s', uniqid());
  1142. }
  1143. return $this->uniqId;
  1144. }
  1145. final public function getClassnameLabel(): string
  1146. {
  1147. if (null === $this->classnameLabel) {
  1148. throw new \LogicException(sprintf(
  1149. 'Admin "%s" has no classname label. Did you forgot to initialize the admin ?',
  1150. static::class
  1151. ));
  1152. }
  1153. return $this->classnameLabel;
  1154. }
  1155. final public function getPersistentParameters(): array
  1156. {
  1157. $parameters = $this->configurePersistentParameters();
  1158. foreach ($this->getExtensions() as $extension) {
  1159. $parameters = $extension->configurePersistentParameters($this, $parameters);
  1160. }
  1161. return $parameters;
  1162. }
  1163. final public function getPersistentParameter(string $name, $default = null)
  1164. {
  1165. $parameters = $this->getPersistentParameters();
  1166. return $parameters[$name] ?? $default;
  1167. }
  1168. final public function setCurrentChild(bool $currentChild): void
  1169. {
  1170. $this->currentChild = $currentChild;
  1171. }
  1172. final public function isCurrentChild(): bool
  1173. {
  1174. return $this->currentChild;
  1175. }
  1176. final public function getCurrentChildAdmin(): ?AdminInterface
  1177. {
  1178. foreach ($this->getChildren() as $child) {
  1179. if ($child->isCurrentChild()) {
  1180. return $child;
  1181. }
  1182. }
  1183. return null;
  1184. }
  1185. final public function setTranslationDomain(string $translationDomain): void
  1186. {
  1187. $this->translationDomain = $translationDomain;
  1188. }
  1189. final public function getTranslationDomain(): string
  1190. {
  1191. return $this->translationDomain;
  1192. }
  1193. final public function getTranslationLabel(string $label, string $context = '', string $type = ''): string
  1194. {
  1195. return $this->getLabelTranslatorStrategy()->getLabel($label, $context, $type);
  1196. }
  1197. final public function setRequest(Request $request): void
  1198. {
  1199. $this->request = $request;
  1200. foreach ($this->getChildren() as $children) {
  1201. $children->setRequest($request);
  1202. }
  1203. }
  1204. final public function getRequest(): Request
  1205. {
  1206. if (!$this->hasRequest()) {
  1207. throw new \LogicException('The Request object has not been set');
  1208. }
  1209. return $this->request;
  1210. }
  1211. /**
  1212. * @phpstan-assert-if-true !null $this->request
  1213. */
  1214. final public function hasRequest(): bool
  1215. {
  1216. return null !== $this->request;
  1217. }
  1218. final public function getBaseCodeRoute(): string
  1219. {
  1220. if ($this->isChild()) {
  1221. return $this->getParent()->getBaseCodeRoute().'|'.$this->getCode();
  1222. }
  1223. return $this->getCode();
  1224. }
  1225. /**
  1226. * @return string
  1227. */
  1228. public function getObjectIdentifier()
  1229. {
  1230. return $this->getCode();
  1231. }
  1232. public function showInDashboard(): bool
  1233. {
  1234. // NEXT_MAJOR: Remove those lines and uncomment the last one.
  1235. $permissionShow = $this->getPermissionsShow(self::CONTEXT_DASHBOARD, 'sonata_deprecation_mute');
  1236. $permission = 1 === \count($permissionShow) ? reset($permissionShow) : $permissionShow;
  1237. return $this->isGranted($permission);
  1238. // return $this->isGranted('LIST');
  1239. }
  1240. /**
  1241. * NEXT_MAJOR: Remove this method.
  1242. *
  1243. * @deprecated since sonata-project/admin-bundle version 4.7 use showInDashboard instead
  1244. */
  1245. final public function showIn(string $context): bool
  1246. {
  1247. if ('sonata_deprecation_mute' !== (\func_get_args()[1] ?? null)) {
  1248. @trigger_error(sprintf(
  1249. 'The "%s()" method is deprecated since sonata-project/admin-bundle version 4.7 and will be'
  1250. .' removed in 5.0 version. Use showInDashboard() instead.',
  1251. __METHOD__
  1252. ), \E_USER_DEPRECATED);
  1253. }
  1254. $permissionShow = $this->getPermissionsShow($context, 'sonata_deprecation_mute');
  1255. // Avoid isGranted deprecation if there is only one permission show.
  1256. $permission = 1 === \count($permissionShow) ? reset($permissionShow) : $permissionShow;
  1257. return $this->isGranted($permission);
  1258. }
  1259. final public function createObjectSecurity(object $object): void
  1260. {
  1261. $this->getSecurityHandler()->createObjectSecurity($this, $object);
  1262. }
  1263. final public function isGranted($name, ?object $object = null): bool
  1264. {
  1265. if (\is_array($name)) {
  1266. @trigger_error(
  1267. sprintf(
  1268. 'Passing an array as argument 1 of "%s()" is deprecated since sonata-project/admin-bundle 4.6'
  1269. .' and will throw an error in 5.0. You MUST pass a string instead.',
  1270. __METHOD__
  1271. ),
  1272. \E_USER_DEPRECATED
  1273. );
  1274. }
  1275. $objectRef = null !== $object ? sprintf('/%s#%s', spl_object_hash($object), $this->id($object) ?? '') : '';
  1276. $key = md5(json_encode($name, \JSON_THROW_ON_ERROR).$objectRef);
  1277. if (!\array_key_exists($key, $this->cacheIsGranted)) {
  1278. $this->cacheIsGranted[$key] = $this->getSecurityHandler()->isGranted($this, $name, $object ?? $this);
  1279. }
  1280. return $this->cacheIsGranted[$key];
  1281. }
  1282. final public function getUrlSafeIdentifier(object $model): ?string
  1283. {
  1284. return $this->getModelManager()->getUrlSafeIdentifier($model);
  1285. }
  1286. final public function getNormalizedIdentifier(object $model): ?string
  1287. {
  1288. return $this->getModelManager()->getNormalizedIdentifier($model);
  1289. }
  1290. public function id(object $model): ?string
  1291. {
  1292. return $this->getNormalizedIdentifier($model);
  1293. }
  1294. final public function getShow(): FieldDescriptionCollection
  1295. {
  1296. $show = $this->buildShow();
  1297. if (null === $show) {
  1298. throw new \LogicException('Cannot access show during the building process.');
  1299. }
  1300. return $show;
  1301. }
  1302. final public function setFormTheme(array $formTheme): void
  1303. {
  1304. $this->formTheme = $formTheme;
  1305. }
  1306. final public function getFormTheme(): array
  1307. {
  1308. return $this->formTheme;
  1309. }
  1310. final public function setFilterTheme(array $filterTheme): void
  1311. {
  1312. $this->filterTheme = $filterTheme;
  1313. }
  1314. final public function getFilterTheme(): array
  1315. {
  1316. return $this->filterTheme;
  1317. }
  1318. final public function addExtension(AdminExtensionInterface $extension): void
  1319. {
  1320. $this->extensions[] = $extension;
  1321. }
  1322. /**
  1323. * @phpstan-param AdminExtensionInterface<T> $extension
  1324. */
  1325. final public function removeExtension(AdminExtensionInterface $extension): void
  1326. {
  1327. $key = array_search($extension, $this->extensions, true);
  1328. if (false === $key) {
  1329. throw new \InvalidArgumentException(
  1330. sprintf('The extension "%s" was not set to the "%s" admin.', $extension::class, self::class)
  1331. );
  1332. }
  1333. unset($this->extensions[$key]);
  1334. }
  1335. final public function getExtensions(): array
  1336. {
  1337. return $this->extensions;
  1338. }
  1339. public function toString(object $object): string
  1340. {
  1341. if (method_exists($object, '__toString') && null !== $object->__toString()) {
  1342. return $object->__toString();
  1343. }
  1344. $modelManager = $this->getModelManager();
  1345. if ($modelManager instanceof ProxyResolverInterface) {
  1346. $class = $modelManager->getRealClass($object);
  1347. } else {
  1348. // NEXT_MAJOR: Change to `\get_class($object)`
  1349. $class = BCHelper::getClass($object);
  1350. }
  1351. return sprintf('%s:%s', $class, spl_object_hash($object));
  1352. }
  1353. final public function supportsPreviewMode(): bool
  1354. {
  1355. return $this->supportsPreviewMode;
  1356. }
  1357. /**
  1358. * Returns predefined per page options.
  1359. *
  1360. * @return array<int>
  1361. */
  1362. public function getPerPageOptions(): array
  1363. {
  1364. $perPageOptions = self::DEFAULT_LIST_PER_PAGE_OPTIONS;
  1365. $perPageOptions[] = $this->getMaxPerPage();
  1366. $perPageOptions = array_unique($perPageOptions);
  1367. sort($perPageOptions);
  1368. return $perPageOptions;
  1369. }
  1370. /**
  1371. * Returns true if the per page value is allowed, false otherwise.
  1372. */
  1373. final public function determinedPerPageValue(int $perPage): bool
  1374. {
  1375. return \in_array($perPage, $this->getPerPageOptions(), true);
  1376. }
  1377. final public function isAclEnabled(): bool
  1378. {
  1379. return $this->getSecurityHandler() instanceof AclSecurityHandlerInterface;
  1380. }
  1381. public function getObjectMetadata(object $object): MetadataInterface
  1382. {
  1383. return new Metadata($this->toString($object));
  1384. }
  1385. final public function setListMode(string $mode): void
  1386. {
  1387. $this->getRequest()->getSession()->set(sprintf('%s.list_mode', $this->getCode()), $mode);
  1388. }
  1389. final public function getListMode(): string
  1390. {
  1391. $defaultListMode = array_keys($this->getListModes())[0];
  1392. if (!$this->hasRequest() || !$this->getRequest()->hasSession()) {
  1393. return $defaultListMode;
  1394. }
  1395. return $this->getRequest()->getSession()->get(sprintf('%s.list_mode', $this->getCode()), $defaultListMode);
  1396. }
  1397. final public function checkAccess(string $action, ?object $object = null): void
  1398. {
  1399. $access = $this->getAccess();
  1400. if (!\array_key_exists($action, $access)) {
  1401. throw new \InvalidArgumentException(sprintf(
  1402. 'Action "%s" could not be found in access mapping.'
  1403. .' Please make sure your action is defined into your admin class accessMapping property.',
  1404. $action
  1405. ));
  1406. }
  1407. if (!\is_array($access[$action])) {
  1408. $access[$action] = [$access[$action]];
  1409. }
  1410. foreach ($access[$action] as $role) {
  1411. if (false === $this->isGranted($role, $object)) {
  1412. throw new AccessDeniedException(sprintf('Access Denied to the action %s and role %s', $action, $role));
  1413. }
  1414. }
  1415. }
  1416. final public function hasAccess(string $action, ?object $object = null): bool
  1417. {
  1418. $access = $this->getAccess();
  1419. if (!\array_key_exists($action, $access)) {
  1420. return false;
  1421. }
  1422. if (!\is_array($access[$action])) {
  1423. $access[$action] = [$access[$action]];
  1424. }
  1425. foreach ($access[$action] as $role) {
  1426. if (false === $this->isGranted($role, $object)) {
  1427. return false;
  1428. }
  1429. }
  1430. return true;
  1431. }
  1432. /**
  1433. * @return array<string, array<string, mixed>>
  1434. *
  1435. * @phpstan-param T|null $object
  1436. */
  1437. final public function getActionButtons(string $action, ?object $object = null): array
  1438. {
  1439. $defaultButtonList = $this->getDefaultActionButtons($action, $object);
  1440. $buttonList = $this->configureActionButtons($defaultButtonList, $action, $object);
  1441. foreach ($this->getExtensions() as $extension) {
  1442. $buttonList = $extension->configureActionButtons($this, $buttonList, $action, $object);
  1443. }
  1444. return $buttonList;
  1445. }
  1446. /**
  1447. * Get the list of actions that can be accessed directly from the dashboard.
  1448. *
  1449. * @return array<string, array<string, mixed>>
  1450. */
  1451. final public function getDashboardActions(): array
  1452. {
  1453. $actions = [];
  1454. if ($this->hasRoute('create') && $this->hasAccess('create')) {
  1455. $actions['create'] = [
  1456. 'label' => 'link_add',
  1457. 'translation_domain' => 'SonataAdminBundle',
  1458. 'template' => $this->getTemplateRegistry()->getTemplate('action_create'),
  1459. 'url' => $this->generateUrl('create'),
  1460. 'icon' => 'fas fa-plus-circle',
  1461. ];
  1462. }
  1463. if ($this->hasRoute('list') && $this->hasAccess('list')) {
  1464. $actions['list'] = [
  1465. 'label' => 'link_list',
  1466. 'translation_domain' => 'SonataAdminBundle',
  1467. 'url' => $this->generateUrl('list'),
  1468. 'icon' => 'fas fa-list',
  1469. ];
  1470. }
  1471. $actions = $this->configureDashboardActions($actions);
  1472. foreach ($this->getExtensions() as $extension) {
  1473. $actions = $extension->configureDashboardActions($this, $actions);
  1474. }
  1475. return $actions;
  1476. }
  1477. final public function createFieldDescription(string $propertyName, array $options = []): FieldDescriptionInterface
  1478. {
  1479. $fieldDescriptionFactory = $this->getFieldDescriptionFactory();
  1480. $fieldDescription = $fieldDescriptionFactory->create($this->getClass(), $propertyName, $options);
  1481. $fieldDescription->setAdmin($this);
  1482. return $fieldDescription;
  1483. }
  1484. /**
  1485. * Hook to run after initialization.
  1486. */
  1487. protected function configure(): void
  1488. {
  1489. }
  1490. protected function generateBaseRoutePattern(bool $isChildAdmin = false): string
  1491. {
  1492. // NEXT_MAJOR: Remove this code
  1493. if (null !== $this->baseRoutePattern) {
  1494. @trigger_error(sprintf(
  1495. 'Overriding the baseRoutePattern property is deprecated since sonata-project/admin-bundle 4.15.'
  1496. .' You MUST override the method %s() instead.',
  1497. __METHOD__
  1498. ), \E_USER_DEPRECATED);
  1499. return $this->baseRoutePattern;
  1500. }
  1501. preg_match(self::CLASS_REGEX, $this->getModelClass(), $matches);
  1502. if ([] === $matches) {
  1503. throw new \LogicException(sprintf(
  1504. 'Please define a default `baseRoutePattern` value for the admin class `%s`',
  1505. static::class
  1506. ));
  1507. }
  1508. if ($isChildAdmin) {
  1509. return $this->urlize($matches[5], '-');
  1510. }
  1511. return sprintf(
  1512. '/%s%s/%s',
  1513. '' === $matches[1] ? '' : $this->urlize($matches[1], '-').'/',
  1514. $this->urlize($matches[3], '-'),
  1515. $this->urlize($matches[5], '-')
  1516. );
  1517. }
  1518. protected function generateBaseRouteName(bool $isChildAdmin = false): string
  1519. {
  1520. // NEXT_MAJOR: Remove this code
  1521. if (null !== $this->baseRouteName) {
  1522. @trigger_error(sprintf(
  1523. 'Overriding the baseRouteName property is deprecated since sonata-project/admin-bundle 4.15.'
  1524. .' You MUST override the method %s() instead.',
  1525. __METHOD__
  1526. ), \E_USER_DEPRECATED);
  1527. return $this->baseRouteName;
  1528. }
  1529. preg_match(self::CLASS_REGEX, $this->getModelClass(), $matches);
  1530. if ([] === $matches) {
  1531. throw new \LogicException(sprintf(
  1532. 'Cannot automatically determine base route name,'
  1533. .' please define a default `baseRouteName` value for the admin class `%s`',
  1534. static::class
  1535. ));
  1536. }
  1537. if ($isChildAdmin) {
  1538. return $this->urlize($matches[5]);
  1539. }
  1540. return sprintf(
  1541. 'admin_%s%s_%s',
  1542. '' === $matches[1] ? '' : $this->urlize($matches[1]).'_',
  1543. $this->urlize($matches[3]),
  1544. $this->urlize($matches[5])
  1545. );
  1546. }
  1547. /**
  1548. * @phpstan-return T
  1549. */
  1550. protected function createNewInstance(): object
  1551. {
  1552. $object = Instantiator::instantiate($this->getClass());
  1553. $this->appendParentObject($object);
  1554. return $object;
  1555. }
  1556. /**
  1557. * @phpstan-param T $object
  1558. */
  1559. protected function alterNewInstance(object $object): void
  1560. {
  1561. }
  1562. /**
  1563. * @phpstan-param T $object
  1564. */
  1565. protected function alterObject(object $object): void
  1566. {
  1567. }
  1568. /**
  1569. * @phpstan-param T $object
  1570. */
  1571. protected function preValidate(object $object): void
  1572. {
  1573. }
  1574. /**
  1575. * @phpstan-param T $object
  1576. */
  1577. protected function preUpdate(object $object): void
  1578. {
  1579. }
  1580. /**
  1581. * @phpstan-param T $object
  1582. */
  1583. protected function postUpdate(object $object): void
  1584. {
  1585. }
  1586. /**
  1587. * @phpstan-param T $object
  1588. */
  1589. protected function prePersist(object $object): void
  1590. {
  1591. }
  1592. /**
  1593. * @phpstan-param T $object
  1594. */
  1595. protected function postPersist(object $object): void
  1596. {
  1597. }
  1598. /**
  1599. * @phpstan-param T $object
  1600. */
  1601. protected function preRemove(object $object): void
  1602. {
  1603. }
  1604. /**
  1605. * @phpstan-param T $object
  1606. */
  1607. protected function postRemove(object $object): void
  1608. {
  1609. }
  1610. /**
  1611. * @return array<string, mixed>
  1612. */
  1613. protected function configurePersistentParameters(): array
  1614. {
  1615. return [];
  1616. }
  1617. /**
  1618. * @return string[]
  1619. */
  1620. protected function configureExportFields(): array
  1621. {
  1622. return $this->getModelManager()->getExportFields($this->getClass());
  1623. }
  1624. /**
  1625. * @param ProxyQueryInterface<T> $query
  1626. *
  1627. * @return ProxyQueryInterface<T>
  1628. */
  1629. protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
  1630. {
  1631. return $query;
  1632. }
  1633. /**
  1634. * urlize the given word.
  1635. *
  1636. * @param string $sep the separator
  1637. */
  1638. final protected function urlize(string $word, string $sep = '_'): string
  1639. {
  1640. return strtolower(preg_replace('/[^a-z0-9_]/i', $sep.'$1', $word) ?? '');
  1641. }
  1642. /**
  1643. * @param array<string, mixed> $parameters
  1644. *
  1645. * @return array<string, mixed>
  1646. */
  1647. protected function configureFilterParameters(array $parameters): array
  1648. {
  1649. return $parameters;
  1650. }
  1651. /**
  1652. * Returns a list of default sort values.
  1653. *
  1654. * @phpstan-return array{
  1655. * _page?: int,
  1656. * _per_page?: int,
  1657. * _sort_by?: string,
  1658. * _sort_order?: string
  1659. * }
  1660. */
  1661. final protected function getDefaultSortValues(): array
  1662. {
  1663. $defaultSortValues = [DatagridInterface::PAGE => 1, DatagridInterface::PER_PAGE => self::DEFAULT_LIST_PER_PAGE_RESULTS];
  1664. $this->configureDefaultSortValues($defaultSortValues);
  1665. foreach ($this->getExtensions() as $extension) {
  1666. $extension->configureDefaultSortValues($this, $defaultSortValues);
  1667. }
  1668. return $defaultSortValues;
  1669. }
  1670. /**
  1671. * Returns a list of default filters.
  1672. *
  1673. * @return array<string, array<string, mixed>>
  1674. */
  1675. final protected function getDefaultFilterValues(): array
  1676. {
  1677. $defaultFilterValues = [];
  1678. $this->configureDefaultFilterValues($defaultFilterValues);
  1679. foreach ($this->getExtensions() as $extension) {
  1680. $extension->configureDefaultFilterValues($this, $defaultFilterValues);
  1681. }
  1682. return $defaultFilterValues;
  1683. }
  1684. /**
  1685. * @return array<string, mixed>
  1686. */
  1687. final protected function getFormOptions(): array
  1688. {
  1689. $formOptions = [];
  1690. $this->configureFormOptions($formOptions);
  1691. foreach ($this->getExtensions() as $extension) {
  1692. $extension->configureFormOptions($this, $formOptions);
  1693. }
  1694. return $formOptions;
  1695. }
  1696. /**
  1697. * @phpstan-param FormMapper<T> $form
  1698. */
  1699. protected function configureFormFields(FormMapper $form): void
  1700. {
  1701. }
  1702. /**
  1703. * @phpstan-param ListMapper<T> $list
  1704. */
  1705. protected function configureListFields(ListMapper $list): void
  1706. {
  1707. }
  1708. /**
  1709. * @phpstan-param DatagridMapper<T> $filter
  1710. */
  1711. protected function configureDatagridFilters(DatagridMapper $filter): void
  1712. {
  1713. }
  1714. /**
  1715. * @phpstan-param ShowMapper<T> $show
  1716. */
  1717. protected function configureShowFields(ShowMapper $show): void
  1718. {
  1719. }
  1720. protected function configureRoutes(RouteCollectionInterface $collection): void
  1721. {
  1722. }
  1723. /**
  1724. * @param array<string, array<string, mixed>> $buttonList
  1725. *
  1726. * @return array<string, array<string, mixed>>
  1727. *
  1728. * @phpstan-param T|null $object
  1729. */
  1730. protected function configureActionButtons(array $buttonList, string $action, ?object $object = null): array
  1731. {
  1732. return $buttonList;
  1733. }
  1734. /**
  1735. * @param array<string, array<string, mixed>> $actions
  1736. *
  1737. * @return array<string, array<string, mixed>>
  1738. */
  1739. protected function configureDashboardActions(array $actions): array
  1740. {
  1741. return $actions;
  1742. }
  1743. /**
  1744. * Allows you to customize batch actions.
  1745. *
  1746. * @param array<string, array<string, mixed>> $actions
  1747. *
  1748. * @return array<string, array<string, mixed>>
  1749. */
  1750. protected function configureBatchActions(array $actions): array
  1751. {
  1752. return $actions;
  1753. }
  1754. /**
  1755. * Configures the tab menu in your admin.
  1756. *
  1757. * @phpstan-template TChild of object
  1758. * @phpstan-param AdminInterface<TChild>|null $childAdmin
  1759. */
  1760. protected function configureTabMenu(ItemInterface $menu, string $action, ?AdminInterface $childAdmin = null): void
  1761. {
  1762. }
  1763. /**
  1764. * Gets the subclass corresponding to the given name.
  1765. *
  1766. * @phpstan-return class-string<T>
  1767. */
  1768. protected function getSubClass(string $name): string
  1769. {
  1770. if ($this->hasSubClass($name)) {
  1771. return $this->subClasses[$name];
  1772. }
  1773. throw new \LogicException(sprintf('Unable to find the subclass `%s` for admin `%s`', $name, static::class));
  1774. }
  1775. /**
  1776. * Return list routes with permissions name.
  1777. *
  1778. * @return array<string, string|string[]>
  1779. */
  1780. final protected function getAccess(): array
  1781. {
  1782. $access = array_merge([
  1783. 'acl' => AdminPermissionMap::PERMISSION_MASTER,
  1784. 'export' => AdminPermissionMap::PERMISSION_EXPORT,
  1785. 'historyCompareRevisions' => AdminPermissionMap::PERMISSION_HISTORY,
  1786. 'historyViewRevision' => AdminPermissionMap::PERMISSION_HISTORY,
  1787. 'history' => AdminPermissionMap::PERMISSION_HISTORY,
  1788. 'edit' => AdminPermissionMap::PERMISSION_EDIT,
  1789. 'show' => AdminPermissionMap::PERMISSION_VIEW,
  1790. 'create' => AdminPermissionMap::PERMISSION_CREATE,
  1791. 'delete' => AdminPermissionMap::PERMISSION_DELETE,
  1792. 'batchDelete' => AdminPermissionMap::PERMISSION_DELETE,
  1793. 'list' => AdminPermissionMap::PERMISSION_LIST,
  1794. ], $this->getAccessMapping());
  1795. foreach ($this->getExtensions() as $extension) {
  1796. $access = array_merge($access, $extension->getAccessMapping($this));
  1797. }
  1798. return $access;
  1799. }
  1800. /**
  1801. * @return array<string, string|string[]> [action1 => requiredRole1, action2 => [requiredRole2, requiredRole3]]
  1802. */
  1803. protected function getAccessMapping(): array
  1804. {
  1805. return [];
  1806. }
  1807. /**
  1808. * Return the list of permissions the user should have in order to display the admin.
  1809. *
  1810. * NEXT_MAJOR: Remove this method.
  1811. *
  1812. * @deprecated since sonata-project/admin-bundle version 4.7
  1813. *
  1814. * @return string[]
  1815. */
  1816. protected function getPermissionsShow(string $context): array
  1817. {
  1818. if ('sonata_deprecation_mute' !== (\func_get_args()[1] ?? null)) {
  1819. @trigger_error(sprintf(
  1820. 'The "%s()" method is deprecated since sonata-project/admin-bundle version 4.7 and will be'
  1821. .' removed in 5.0 version.',
  1822. __METHOD__
  1823. ), \E_USER_DEPRECATED);
  1824. }
  1825. return ['LIST'];
  1826. }
  1827. /**
  1828. * Configures a list of default filters.
  1829. *
  1830. * @param array<string, array<string, mixed>> $filterValues
  1831. */
  1832. protected function configureDefaultFilterValues(array &$filterValues): void
  1833. {
  1834. }
  1835. /**
  1836. * Configures a list of form options.
  1837. *
  1838. * @param array<string, mixed> $formOptions
  1839. */
  1840. protected function configureFormOptions(array &$formOptions): void
  1841. {
  1842. }
  1843. /**
  1844. * Configures a list of default sort values.
  1845. *
  1846. * Example:
  1847. * $sortValues[DatagridInterface::SORT_BY] = 'foo'
  1848. * $sortValues[DatagridInterface::SORT_ORDER] = 'DESC'
  1849. *
  1850. * @param array<string, string|int> $sortValues
  1851. *
  1852. * @phpstan-param array{
  1853. * _page?: int,
  1854. * _per_page?: int,
  1855. * _sort_by?: string,
  1856. * _sort_order?: string
  1857. * } $sortValues
  1858. */
  1859. protected function configureDefaultSortValues(array &$sortValues): void
  1860. {
  1861. }
  1862. /**
  1863. * Set the parent object, if any, to the provided object.
  1864. *
  1865. * @phpstan-param T $object
  1866. */
  1867. final protected function appendParentObject(object $object): void
  1868. {
  1869. if ($this->isChild()) {
  1870. $parentAssociationMapping = $this->getParentAssociationMapping();
  1871. if (null !== $parentAssociationMapping) {
  1872. $parentAdmin = $this->getParent();
  1873. $parentObject = $parentAdmin->getObject($this->getRequest()->get($parentAdmin->getIdParameter()));
  1874. if (null !== $parentObject) {
  1875. $propertyAccessor = PropertyAccess::createPropertyAccessor();
  1876. try {
  1877. $value = $propertyAccessor->getValue($object, $parentAssociationMapping);
  1878. } catch (AccessException $e) {
  1879. // @todo: Catching and checking AccessException here as BC for symfony/property-access < 5.1.
  1880. // Catch UninitializedPropertyException and remove the check when dropping support < 5.1
  1881. if (AccessException::class !== $e::class && !$e instanceof UninitializedPropertyException) {
  1882. throw $e; // Re-throw. We only want to "ignore" pure AccessException (Sf < 5.1) and UninitializedPropertyException (Sf >= 5.1)
  1883. }
  1884. $value = null;
  1885. }
  1886. if (\is_array($value) || $value instanceof \ArrayAccess) {
  1887. $value[] = $parentObject;
  1888. $propertyAccessor->setValue($object, $parentAssociationMapping, $value);
  1889. } else {
  1890. $propertyAccessor->setValue($object, $parentAssociationMapping, $parentObject);
  1891. }
  1892. }
  1893. return;
  1894. }
  1895. }
  1896. if ($this->hasParentFieldDescription()) {
  1897. $parentAdmin = $this->getParentFieldDescription()->getAdmin();
  1898. $parentObject = $parentAdmin->getObject($this->getRequest()->get($parentAdmin->getIdParameter()));
  1899. if (null !== $parentObject) {
  1900. ObjectManipulator::setObject($object, $parentObject, $this->getParentFieldDescription());
  1901. }
  1902. }
  1903. }
  1904. /**
  1905. * @return array<string, array<string, mixed>>
  1906. *
  1907. * @phpstan-param T|null $object
  1908. */
  1909. private function getDefaultActionButtons(string $action, ?object $object = null): array
  1910. {
  1911. // nothing to do for non-internal actions
  1912. if (!isset(self::INTERNAL_ACTIONS[$action])) {
  1913. return [];
  1914. }
  1915. $buttonList = [];
  1916. $actionBit = self::INTERNAL_ACTIONS[$action];
  1917. if (0 !== (self::MASK_OF_ACTION_CREATE & $actionBit)
  1918. && $this->hasRoute('create')
  1919. && $this->hasAccess('create')
  1920. ) {
  1921. $buttonList['create'] = [
  1922. 'template' => $this->getTemplateRegistry()->getTemplate('button_create'),
  1923. ];
  1924. }
  1925. $canAccessObject = 0 !== (self::MASK_OF_ACTIONS_USING_OBJECT & $actionBit)
  1926. && null !== $object
  1927. && null !== $this->id($object);
  1928. if ($canAccessObject
  1929. && 0 !== (self::MASK_OF_ACTION_EDIT & $actionBit)
  1930. && $this->hasRoute('edit')
  1931. && $this->hasAccess('edit', $object)
  1932. ) {
  1933. $buttonList['edit'] = [
  1934. 'template' => $this->getTemplateRegistry()->getTemplate('button_edit'),
  1935. ];
  1936. }
  1937. if ($canAccessObject
  1938. && 0 !== (self::MASK_OF_ACTION_HISTORY & $actionBit)
  1939. && $this->hasRoute('history')
  1940. && $this->hasAccess('history', $object)
  1941. ) {
  1942. $buttonList['history'] = [
  1943. 'template' => $this->getTemplateRegistry()->getTemplate('button_history'),
  1944. ];
  1945. }
  1946. if ($canAccessObject
  1947. && 0 !== (self::MASK_OF_ACTION_ACL & $actionBit)
  1948. && $this->isAclEnabled()
  1949. && $this->hasRoute('acl')
  1950. && $this->hasAccess('acl', $object)
  1951. ) {
  1952. $buttonList['acl'] = [
  1953. 'template' => $this->getTemplateRegistry()->getTemplate('button_acl'),
  1954. ];
  1955. }
  1956. if ($canAccessObject
  1957. && 0 !== (self::MASK_OF_ACTION_SHOW & $actionBit)
  1958. && $this->hasRoute('show')
  1959. && $this->hasAccess('show', $object)
  1960. && \count($this->getShow()) > 0
  1961. ) {
  1962. $buttonList['show'] = [
  1963. 'template' => $this->getTemplateRegistry()->getTemplate('button_show'),
  1964. ];
  1965. }
  1966. if (0 !== (self::MASK_OF_ACTION_LIST & $actionBit)
  1967. && $this->hasRoute('list')
  1968. && $this->hasAccess('list')
  1969. ) {
  1970. $buttonList['list'] = [
  1971. 'template' => $this->getTemplateRegistry()->getTemplate('button_list'),
  1972. ];
  1973. }
  1974. return $buttonList;
  1975. }
  1976. /**
  1977. * @return DatagridInterface<ProxyQueryInterface<T>>|null
  1978. */
  1979. private function buildDatagrid(): ?DatagridInterface
  1980. {
  1981. if ($this->loaded['datagrid']) {
  1982. return $this->datagrid;
  1983. }
  1984. $this->loaded['datagrid'] = true;
  1985. $filterParameters = $this->getFilterParameters();
  1986. // transform DatagridInterface::SORT_BY filter parameter from a string to a FieldDescriptionInterface for the datagrid.
  1987. if (isset($filterParameters[DatagridInterface::SORT_BY]) && \is_string($filterParameters[DatagridInterface::SORT_BY])) {
  1988. if ($this->hasListFieldDescription($filterParameters[DatagridInterface::SORT_BY])) {
  1989. $filterParameters[DatagridInterface::SORT_BY] = $this->getListFieldDescription($filterParameters[DatagridInterface::SORT_BY]);
  1990. } else {
  1991. $filterParameters[DatagridInterface::SORT_BY] = $this->createFieldDescription(
  1992. $filterParameters[DatagridInterface::SORT_BY]
  1993. );
  1994. $this->getListBuilder()->buildField(null, $filterParameters[DatagridInterface::SORT_BY]);
  1995. }
  1996. }
  1997. // initialize the datagrid
  1998. $this->datagrid = $this->getDatagridBuilder()->getBaseDatagrid($this, $filterParameters);
  1999. $this->datagrid->getPager()->setMaxPageLinks($this->getMaxPageLinks());
  2000. /** @psalm-suppress InvalidArgument https://github.com/vimeo/psalm/issues/8423 */
  2001. $mapper = new DatagridMapper($this->getDatagridBuilder(), $this->datagrid, $this);
  2002. // build the datagrid filter
  2003. $this->configureDatagridFilters($mapper);
  2004. // ok, try to limit to add parent filter
  2005. if ($this->isChild()) {
  2006. $parentAssociationMapping = $this->getParentAssociationMapping();
  2007. if (null !== $parentAssociationMapping && !$mapper->has($parentAssociationMapping)) {
  2008. $mapper->add($parentAssociationMapping, null, [
  2009. 'show_filter' => false,
  2010. 'label' => false,
  2011. 'field_type' => ModelHiddenType::class,
  2012. 'field_options' => [
  2013. 'model_manager' => $this->getParent()->getModelManager(),
  2014. 'class' => $this->getParent()->getClass(),
  2015. ],
  2016. 'operator_type' => HiddenType::class,
  2017. ], [
  2018. 'admin_code' => $this->getParent()->getCode(),
  2019. ]);
  2020. }
  2021. }
  2022. foreach ($this->getExtensions() as $extension) {
  2023. $extension->configureDatagridFilters($mapper);
  2024. }
  2025. return $this->datagrid;
  2026. }
  2027. /**
  2028. * @return FieldDescriptionCollection<FieldDescriptionInterface>|null
  2029. */
  2030. private function buildShow(): ?FieldDescriptionCollection
  2031. {
  2032. if ($this->loaded['show']) {
  2033. return $this->show;
  2034. }
  2035. $this->loaded['show'] = true;
  2036. $this->show = $this->getShowBuilder()->getBaseList();
  2037. $mapper = new ShowMapper($this->getShowBuilder(), $this->show, $this);
  2038. $this->configureShowFields($mapper);
  2039. foreach ($this->getExtensions() as $extension) {
  2040. $extension->configureShowFields($mapper);
  2041. }
  2042. return $this->show;
  2043. }
  2044. /**
  2045. * @return FieldDescriptionCollection<FieldDescriptionInterface>|null
  2046. */
  2047. private function buildList(): ?FieldDescriptionCollection
  2048. {
  2049. if ($this->loaded['list']) {
  2050. return $this->list;
  2051. }
  2052. $this->loaded['list'] = true;
  2053. $this->list = $this->getListBuilder()->getBaseList();
  2054. $mapper = new ListMapper($this->getListBuilder(), $this->list, $this);
  2055. if (\count($this->getBatchActions()) > 0 && $this->hasRequest() && !$this->getRequest()->isXmlHttpRequest()) {
  2056. $mapper->add(ListMapper::NAME_BATCH, ListMapper::TYPE_BATCH, [
  2057. 'label' => 'batch',
  2058. 'sortable' => false,
  2059. 'virtual_field' => true,
  2060. 'template' => $this->getTemplateRegistry()->getTemplate('batch'),
  2061. ]);
  2062. }
  2063. $this->configureListFields($mapper);
  2064. foreach ($this->getExtensions() as $extension) {
  2065. $extension->configureListFields($mapper);
  2066. }
  2067. if ($this->hasRequest()
  2068. && $this->getRequest()->isXmlHttpRequest()
  2069. && $this->getRequest()->query->getBoolean('select', true) // NEXT_MAJOR: Change the default value to `false` in version 5
  2070. ) {
  2071. $mapper->add(ListMapper::NAME_SELECT, ListMapper::TYPE_SELECT, [
  2072. 'label' => false,
  2073. 'sortable' => false,
  2074. 'virtual_field' => false,
  2075. 'template' => $this->getTemplateRegistry()->getTemplate('select'),
  2076. ]);
  2077. }
  2078. return $this->list;
  2079. }
  2080. private function buildForm(): ?FormInterface
  2081. {
  2082. if ($this->loaded['form']) {
  2083. return $this->form;
  2084. }
  2085. $this->loaded['form'] = true;
  2086. $formBuilder = $this->getFormBuilder();
  2087. $formBuilder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
  2088. /** @phpstan-var T $data */
  2089. $data = $event->getData();
  2090. $this->preValidate($data);
  2091. }, 100);
  2092. $this->form = $formBuilder->getForm();
  2093. return $this->form;
  2094. }
  2095. private function buildRoutes(): ?RouteCollectionInterface
  2096. {
  2097. if ($this->loaded['routes']) {
  2098. return $this->routes;
  2099. }
  2100. $this->loaded['routes'] = true;
  2101. $routes = new RouteCollection(
  2102. $this->getBaseCodeRoute(),
  2103. $this->getBaseRouteName(),
  2104. $this->getBaseRoutePattern(),
  2105. $this->getBaseControllerName()
  2106. );
  2107. $this->getRouteBuilder()->build($this, $routes);
  2108. $this->configureRoutes($routes);
  2109. foreach ($this->getExtensions() as $extension) {
  2110. $extension->configureRoutes($this, $routes);
  2111. }
  2112. $this->routes = $routes;
  2113. return $this->routes;
  2114. }
  2115. /**
  2116. * @phpstan-template TChild of object
  2117. * @phpstan-param AdminInterface<TChild>|null $childAdmin
  2118. */
  2119. private function buildTabMenu(string $action, ?AdminInterface $childAdmin = null): ?ItemInterface
  2120. {
  2121. if ($this->loaded['tab_menu']) {
  2122. return $this->menu;
  2123. }
  2124. $this->loaded['tab_menu'] = true;
  2125. $menu = $this->getMenuFactory()->createItem('root');
  2126. $menu->setChildrenAttribute('class', 'nav navbar-nav');
  2127. $menu->setExtra('translation_domain', $this->getTranslationDomain());
  2128. $this->configureTabMenu($menu, $action, $childAdmin);
  2129. foreach ($this->getExtensions() as $extension) {
  2130. $extension->configureTabMenu($this, $menu, $action, $childAdmin);
  2131. }
  2132. $this->menu = $menu;
  2133. return $this->menu;
  2134. }
  2135. }