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

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