vendor/sonata-project/admin-bundle/src/Admin/Pool.php line 292

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 Psr\Container\ContainerInterface;
  13. use Sonata\AdminBundle\Exception\AdminClassNotFoundException;
  14. use Sonata\AdminBundle\Exception\AdminCodeNotFoundException;
  15. use Sonata\AdminBundle\Exception\TooManyAdminClassException;
  16. use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface;
  17. /**
  18. * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
  19. *
  20. * @phpstan-type Item = array{
  21. * label: string,
  22. * roles: list<string>,
  23. * route: string,
  24. * route_absolute: bool,
  25. * route_params: array<string, string>
  26. * }|array{
  27. * admin: string,
  28. * roles: list<string>,
  29. * route_absolute: bool,
  30. * route_params: array<string, string>
  31. * }
  32. * NEXT_MAJOR: Remove the label_catalogue key.
  33. * @phpstan-type Group = array{
  34. * label: string,
  35. * translation_domain: string,
  36. * label_catalogue?: string,
  37. * icon: string,
  38. * items: list<Item>,
  39. * keep_open: bool,
  40. * on_top: bool,
  41. * provider?: string,
  42. * roles: list<string>
  43. * }
  44. */
  45. final class Pool
  46. {
  47. public const DEFAULT_ADMIN_KEY = 'default';
  48. /**
  49. * @param string[] $adminServiceCodes
  50. * @param array<string, array<string, mixed>> $adminGroups
  51. * @param array<class-string, string[]> $adminClasses
  52. *
  53. * @phpstan-param array<string, Group> $adminGroups
  54. */
  55. public function __construct(
  56. private ContainerInterface $container,
  57. private array $adminServiceCodes = [],
  58. private array $adminGroups = [],
  59. private array $adminClasses = [],
  60. ) {
  61. }
  62. /**
  63. * NEXT_MAJOR: Remove the label_catalogue key.
  64. *
  65. * @phpstan-return array<string, array{
  66. * label: string,
  67. * label_catalogue?: string,
  68. * translation_domain: string,
  69. * icon: string,
  70. * items: list<AdminInterface<object>>,
  71. * keep_open: bool,
  72. * on_top: bool,
  73. * provider?: string,
  74. * roles: list<string>
  75. * }>
  76. */
  77. public function getDashboardGroups(): array
  78. {
  79. $groups = [];
  80. foreach ($this->adminGroups as $name => $adminGroup) {
  81. $items = [];
  82. foreach ($adminGroup['items'] as $item) {
  83. // NEXT_MAJOR: Remove the '' check
  84. if (!isset($item['admin']) || '' === $item['admin']) {
  85. continue;
  86. }
  87. $admin = $this->getInstance($item['admin']);
  88. // NEXT_MAJOR: Keep the "if" part.
  89. // @phpstan-ignore-next-line
  90. if (method_exists($admin, 'showInDashboard')) {
  91. if (!$admin->showInDashboard()) {
  92. continue;
  93. }
  94. } else {
  95. @trigger_error(\sprintf(
  96. 'Not implementing "%s::showInDashboard()" is deprecated since sonata-project/admin-bundle 4.7'
  97. .' and will fail in 5.0.',
  98. AdminInterface::class
  99. ), \E_USER_DEPRECATED);
  100. /**
  101. * @psalm-suppress DeprecatedMethod, DeprecatedConstant
  102. */
  103. if (!$admin->showIn(AbstractAdmin::CONTEXT_DASHBOARD)) {
  104. continue;
  105. }
  106. }
  107. $items[] = $admin;
  108. }
  109. if ([] !== $items) {
  110. $groups[$name] = ['items' => $items] + $adminGroup;
  111. }
  112. }
  113. return $groups;
  114. }
  115. /**
  116. * Return the admin related to the given $class.
  117. *
  118. * @throws AdminClassNotFoundException if there is no admin class for the class provided
  119. * @throws TooManyAdminClassException if there is multiple admin class for the class provided
  120. *
  121. * @phpstan-param class-string $class
  122. * @phpstan-return AdminInterface<object>
  123. */
  124. public function getAdminByClass(string $class): AdminInterface
  125. {
  126. if (!$this->hasAdminByClass($class)) {
  127. throw new AdminClassNotFoundException(\sprintf('Pool has no admin for the class %s.', $class));
  128. }
  129. if (isset($this->adminClasses[$class][self::DEFAULT_ADMIN_KEY])) {
  130. return $this->getInstance($this->adminClasses[$class][self::DEFAULT_ADMIN_KEY]);
  131. }
  132. if (1 !== \count($this->adminClasses[$class])) {
  133. throw new TooManyAdminClassException(\sprintf(
  134. 'Unable to find a valid admin for the class: %s, there are too many registered: %s.'
  135. .' Please define a default one with the tag attribute `default: true` in your admin configuration.',
  136. $class,
  137. implode(', ', $this->adminClasses[$class])
  138. ));
  139. }
  140. return $this->getInstance(reset($this->adminClasses[$class]));
  141. }
  142. /**
  143. * @phpstan-param class-string $class
  144. */
  145. public function hasAdminByClass(string $class): bool
  146. {
  147. return isset($this->adminClasses[$class]) && \count($this->adminClasses[$class]) > 0;
  148. }
  149. /**
  150. * Returns an admin class by its Admin code
  151. * ie : sonata.news.admin.post|sonata.news.admin.comment => return the child class of post.
  152. *
  153. * @throws AdminCodeNotFoundException
  154. *
  155. * @return AdminInterface<object>
  156. */
  157. public function getAdminByAdminCode(string $adminCode): AdminInterface
  158. {
  159. $codes = explode('|', $adminCode);
  160. $rootCode = trim(array_shift($codes));
  161. $admin = $this->getInstance($rootCode);
  162. foreach ($codes as $code) {
  163. if (!\in_array($code, $this->adminServiceCodes, true)) {
  164. throw new AdminCodeNotFoundException(\sprintf(
  165. 'Argument 1 passed to %s() must contain a valid admin reference, "%s" found at "%s".',
  166. __METHOD__,
  167. $code,
  168. $adminCode
  169. ));
  170. }
  171. if (!$admin->hasChild($code)) {
  172. throw new AdminCodeNotFoundException(\sprintf(
  173. 'Argument 1 passed to %s() must contain a valid admin hierarchy,'
  174. .' "%s" is not a valid child for "%s"',
  175. __METHOD__,
  176. $code,
  177. $admin->getCode()
  178. ));
  179. }
  180. $admin = $admin->getChild($code);
  181. }
  182. return $admin;
  183. }
  184. /**
  185. * Checks if an admin with a certain admin code exists.
  186. */
  187. public function hasAdminByAdminCode(string $adminCode): bool
  188. {
  189. try {
  190. $this->getAdminByAdminCode($adminCode);
  191. } catch (\InvalidArgumentException) {
  192. return false;
  193. }
  194. return true;
  195. }
  196. /**
  197. * @throws AdminClassNotFoundException if there is no admin for the field description target model
  198. * @throws TooManyAdminClassException if there is too many admin for the field description target model
  199. * @throws AdminCodeNotFoundException if the admin_code option is invalid
  200. *
  201. * @return AdminInterface<object>
  202. */
  203. public function getAdminByFieldDescription(FieldDescriptionInterface $fieldDescription): AdminInterface
  204. {
  205. $adminCode = $fieldDescription->getOption('admin_code');
  206. if (\is_string($adminCode)) {
  207. return $this->getAdminByAdminCode($adminCode);
  208. }
  209. $targetModel = $fieldDescription->getTargetModel();
  210. if (null === $targetModel) {
  211. throw new \InvalidArgumentException('The field description has no target model.');
  212. }
  213. return $this->getAdminByClass($targetModel);
  214. }
  215. /**
  216. * Returns a new admin instance depends on the given code.
  217. *
  218. * @throws AdminCodeNotFoundException if the code is not found in admin pool
  219. *
  220. * @return AdminInterface<object>
  221. */
  222. public function getInstance(string $code): AdminInterface
  223. {
  224. if ('' === $code) {
  225. throw new \InvalidArgumentException(
  226. 'Admin code must contain a valid admin reference, empty string given.'
  227. );
  228. }
  229. if (!\in_array($code, $this->adminServiceCodes, true)) {
  230. $msg = \sprintf('Admin service "%s" not found in admin pool.', $code);
  231. $shortest = -1;
  232. $closest = null;
  233. $alternatives = [];
  234. foreach ($this->adminServiceCodes as $adminServiceCode) {
  235. $lev = levenshtein($code, $adminServiceCode);
  236. if ($lev <= $shortest || $shortest < 0) {
  237. $closest = $adminServiceCode;
  238. $shortest = $lev;
  239. }
  240. if ($lev <= \strlen($adminServiceCode) / 3 || str_contains($adminServiceCode, $code)) {
  241. $alternatives[$adminServiceCode] = $lev;
  242. }
  243. }
  244. if (null !== $closest) {
  245. asort($alternatives);
  246. unset($alternatives[$closest]);
  247. $msg = \sprintf(
  248. 'Admin service "%s" not found in admin pool. Did you mean "%s" or one of those: [%s]?',
  249. $code,
  250. $closest,
  251. implode(', ', array_keys($alternatives))
  252. );
  253. }
  254. throw new AdminCodeNotFoundException($msg);
  255. }
  256. $admin = $this->container->get($code);
  257. if (!$admin instanceof AdminInterface) {
  258. throw new \InvalidArgumentException(\sprintf('Found service "%s" is not a valid admin service', $code));
  259. }
  260. return $admin;
  261. }
  262. /**
  263. * @return array<string, array<string, mixed>>
  264. *
  265. * @phpstan-return array<string, Group>
  266. */
  267. public function getAdminGroups(): array
  268. {
  269. return $this->adminGroups;
  270. }
  271. /**
  272. * @return string[]
  273. */
  274. public function getAdminServiceCodes(): array
  275. {
  276. return $this->adminServiceCodes;
  277. }
  278. /**
  279. * NEXT_MAJOR: Remove this method.
  280. *
  281. * @deprecated since sonata-project/admin-bundle 4.20 will be removed in 5.0 use getAdminServiceCodes() instead.
  282. *
  283. * @return string[]
  284. */
  285. public function getAdminServiceIds(): array
  286. {
  287. return $this->adminServiceCodes;
  288. }
  289. /**
  290. * @return array<string, string[]>
  291. *
  292. * @phpstan-return array<class-string, string[]>
  293. */
  294. public function getAdminClasses(): array
  295. {
  296. return $this->adminClasses;
  297. }
  298. }