vendor/sonata-project/admin-bundle/src/DependencyInjection/Compiler/ExtensionCompilerPass.php line 138

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\DependencyInjection\Compiler;
  12. use Sonata\AdminBundle\DependencyInjection\Admin\TaggedAdminInterface;
  13. use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
  14. use Symfony\Component\DependencyInjection\ContainerBuilder;
  15. use Symfony\Component\DependencyInjection\Definition;
  16. use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
  17. use Symfony\Component\DependencyInjection\Reference;
  18. /**
  19. * @internal
  20. *
  21. * @phpstan-type ExtensionMap = array<string, array{
  22. * global: bool,
  23. * excludes: array<string, string>,
  24. * admins: array<string, string>,
  25. * implements: array<class-string, string>,
  26. * extends: array<class-string, string>,
  27. * instanceof: array<class-string, string>,
  28. * uses: array<class-string, string>,
  29. * admin_implements: array<class-string, string>,
  30. * admin_extends: array<class-string, string>,
  31. * admin_instanceof: array<class-string, string>,
  32. * admin_uses: array<class-string, string>,
  33. * priority: int,
  34. * }>
  35. * @phpstan-type FlattenExtensionMap = array{
  36. * global: array<string, array<string, array{priority: int}>>,
  37. * excludes: array<string, array<string, array{priority: int}>>,
  38. * admins: array<string, array<string, array{priority: int}>>,
  39. * implements: array<string, array<class-string, array{priority: int}>>,
  40. * extends: array<string, array<class-string, array{priority: int}>>,
  41. * instanceof: array<string, array<class-string, array{priority: int}>>,
  42. * uses: array<string, array<class-string, array{priority: int}>>,
  43. * admin_implements: array<string, array<class-string, array{priority: int}>>,
  44. * admin_extends: array<string, array<class-string, array{priority: int}>>,
  45. * admin_instanceof: array<string, array<class-string, array{priority: int}>>,
  46. * admin_uses: array<string, array<class-string, array{priority: int}>>,
  47. * }
  48. *
  49. * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
  50. */
  51. final class ExtensionCompilerPass implements CompilerPassInterface
  52. {
  53. public function process(ContainerBuilder $container): void
  54. {
  55. $universalExtensions = [];
  56. $targets = [];
  57. foreach ($container->findTaggedServiceIds('sonata.admin.extension') as $id => $tags) {
  58. $adminExtension = $container->getDefinition($id);
  59. // Trim possible parameter delimiters ("%") from the class name.
  60. $adminExtensionClass = trim($adminExtension->getClass() ?? '', '%');
  61. if (!class_exists($adminExtensionClass, false) && $container->hasParameter($adminExtensionClass)) {
  62. $adminExtensionClass = $container->getParameter($adminExtensionClass);
  63. \assert(\is_string($adminExtensionClass));
  64. }
  65. \assert(class_exists($adminExtensionClass));
  66. foreach ($tags as $attributes) {
  67. $target = false;
  68. if (isset($attributes['target'])) {
  69. $target = $attributes['target'];
  70. unset($attributes['target']);
  71. }
  72. if (isset($attributes['global'])) {
  73. if ($attributes['global']) {
  74. $attributes['global'] = $adminExtensionClass;
  75. } else {
  76. unset($attributes['global']);
  77. }
  78. }
  79. $universalExtensions[$id][] = $attributes;
  80. if (false === $target || !$container->hasDefinition($target)) {
  81. continue;
  82. }
  83. $this->addExtension($targets, $target, $id, $attributes);
  84. }
  85. }
  86. /**
  87. * @phpstan-var ExtensionMap $extensionConfig
  88. */
  89. $extensionConfig = $container->getParameter('sonata.admin.extension.map');
  90. $extensionMap = $this->flattenExtensionConfiguration($extensionConfig);
  91. foreach ($container->findTaggedServiceIds(TaggedAdminInterface::ADMIN_TAG) as $id => $tags) {
  92. $admin = $container->getDefinition($id);
  93. // Trim possible parameter delimiters ("%") from the class name.
  94. $adminClass = trim($admin->getClass() ?? '', '%');
  95. if (!class_exists($adminClass, false) && $container->hasParameter($adminClass)) {
  96. $adminClass = $container->getParameter($adminClass);
  97. \assert(\is_string($adminClass));
  98. }
  99. \assert(class_exists($adminClass));
  100. if (!isset($targets[$id])) {
  101. $targets[$id] = new \SplPriorityQueue();
  102. }
  103. // NEXT_MAJOR: Remove this line.
  104. $defaultModelClass = $admin->getArguments()[1] ?? null;
  105. foreach ($tags as $attributes) {
  106. // NEXT_MAJOR: Remove the fallback to $defaultModelClass and use null instead.
  107. $modelClass = $attributes['model_class'] ?? $defaultModelClass;
  108. if (null === $modelClass) {
  109. throw new InvalidArgumentException(\sprintf('Missing tag attribute "model_class" on service "%s".', $id));
  110. }
  111. $class = $container->getParameterBag()->resolveValue($modelClass);
  112. if (!\is_string($class)) {
  113. throw new \TypeError(\sprintf(
  114. 'Tag attribute "model_class" for service "%s" must be of type string, %s given.',
  115. $id,
  116. get_debug_type($class)
  117. ));
  118. }
  119. if (!class_exists($class)) {
  120. continue;
  121. }
  122. foreach ($universalExtensions as $extension => $extensionsAttributes) {
  123. foreach ($extensionsAttributes as $extensionAttributes) {
  124. if (isset($extensionAttributes['excludes'][$id])) {
  125. continue;
  126. }
  127. foreach ($extensionAttributes as $type => $subject) {
  128. if ($this->shouldApplyExtension($type, $subject, $class, $adminClass)) {
  129. $this->addExtension($targets, $id, $extension, $extensionAttributes);
  130. break;
  131. }
  132. }
  133. }
  134. }
  135. }
  136. $extensions = $this->getExtensionsForAdmin($id, $tags, $admin, $container, $extensionMap);
  137. foreach ($extensions as $extension => $attributes) {
  138. if (!$container->has($extension)) {
  139. throw new \InvalidArgumentException(\sprintf(
  140. 'Unable to find extension service for id %s',
  141. $extension
  142. ));
  143. }
  144. $this->addExtension($targets, $id, $extension, $attributes);
  145. }
  146. }
  147. foreach ($targets as $target => $extensions) {
  148. $extensions = iterator_to_array($extensions);
  149. krsort($extensions);
  150. $admin = $container->getDefinition($target);
  151. foreach (array_values($extensions) as $extension) {
  152. $admin->addMethodCall('addExtension', [$extension]);
  153. }
  154. }
  155. }
  156. /**
  157. * @param array<string, mixed> $tags
  158. * @param array<string, array<string, array<string, array<string, mixed>>>> $extensionMap
  159. *
  160. * @return array<string, array<string, mixed>>
  161. *
  162. * @phpstan-param FlattenExtensionMap $extensionMap
  163. */
  164. private function getExtensionsForAdmin(string $id, array $tags, Definition $admin, ContainerBuilder $container, array $extensionMap): array
  165. {
  166. // Trim possible parameter delimiters ("%") from the class name.
  167. $adminClass = trim($admin->getClass() ?? '', '%');
  168. if (!class_exists($adminClass, false) && $container->hasParameter($adminClass)) {
  169. $adminClass = $container->getParameter($adminClass);
  170. \assert(\is_string($adminClass));
  171. }
  172. \assert(class_exists($adminClass));
  173. $extensions = [];
  174. $excludes = $extensionMap['excludes'];
  175. unset($extensionMap['excludes']);
  176. foreach ($extensionMap as $type => $subjects) {
  177. foreach ($subjects as $subject => $extensionList) {
  178. if ('admins' === $type) {
  179. if ($id === $subject) {
  180. $extensions = array_merge($extensions, $extensionList);
  181. }
  182. continue;
  183. }
  184. // NEXT_MAJOR: Remove this line.
  185. $defaultModelClass = $admin->getArguments()[1] ?? null;
  186. foreach ($tags as $attributes) {
  187. // NEXT_MAJOR: Remove the fallback to $defaultModelClass and use null instead.
  188. $modelClass = $attributes['model_class'] ?? $defaultModelClass;
  189. if (null === $modelClass) {
  190. throw new InvalidArgumentException(\sprintf('Missing tag attribute "model_class" on service "%s".', $id));
  191. }
  192. $class = $container->getParameterBag()->resolveValue($modelClass);
  193. if (!\is_string($class)) {
  194. throw new \TypeError(\sprintf(
  195. 'Tag attribute "model_class" for service "%s" must be of type string, %s given.',
  196. $id,
  197. get_debug_type($class)
  198. ));
  199. }
  200. if (!class_exists($class)) {
  201. continue;
  202. }
  203. if ($this->shouldApplyExtension($type, $subject, $class, $adminClass)) {
  204. $extensions = array_merge($extensions, $extensionList);
  205. }
  206. }
  207. }
  208. }
  209. if (isset($excludes[$id])) {
  210. $extensions = array_diff_key($extensions, $excludes[$id]);
  211. }
  212. return $extensions;
  213. }
  214. /**
  215. * @param array<string, array<string, array<string, string>|int|bool>> $config
  216. *
  217. * @return array<string, array<string, array<string, array<string, int>>>> an array with the following structure
  218. *
  219. * @phpstan-param ExtensionMap $config
  220. * @phpstan-return FlattenExtensionMap
  221. */
  222. private function flattenExtensionConfiguration(array $config): array
  223. {
  224. /** @phpstan-var FlattenExtensionMap $extensionMap */
  225. $extensionMap = [
  226. 'global' => [],
  227. 'excludes' => [],
  228. 'admins' => [],
  229. 'implements' => [],
  230. 'extends' => [],
  231. 'instanceof' => [],
  232. 'uses' => [],
  233. 'admin_implements' => [],
  234. 'admin_extends' => [],
  235. 'admin_instanceof' => [],
  236. 'admin_uses' => [],
  237. ];
  238. foreach ($config as $extension => $options) {
  239. if (true === $options['global']) {
  240. $options['global'] = [$extension];
  241. } else {
  242. $options['global'] = [];
  243. }
  244. /**
  245. * @phpstan-var array{
  246. * global: array<string, string>,
  247. * excludes: array<string, string>,
  248. * admins: array<string, string>,
  249. * implements: array<class-string, string>,
  250. * extends: array<class-string, string>,
  251. * instanceof: array<class-string, string>,
  252. * uses: array<class-string, string>,
  253. * admin_implements: array<class-string, string>,
  254. * admin_extends: array<class-string, string>,
  255. * admin_instanceof: array<class-string, string>,
  256. * admin_uses: array<class-string, string>,
  257. * } $optionsMap
  258. */
  259. $optionsMap = array_intersect_key($options, $extensionMap);
  260. foreach ($extensionMap as $key => &$value) {
  261. foreach ($optionsMap[$key] as $source) {
  262. $value[$source][$extension]['priority'] = $options['priority'];
  263. }
  264. }
  265. }
  266. return $extensionMap;
  267. }
  268. /**
  269. * @param \ReflectionClass<object> $class
  270. */
  271. private function hasTrait(\ReflectionClass $class, string $traitName): bool
  272. {
  273. if (\in_array($traitName, $class->getTraitNames(), true)) {
  274. return true;
  275. }
  276. $parentClass = $class->getParentClass();
  277. if (false === $parentClass) {
  278. return false;
  279. }
  280. return $this->hasTrait($parentClass, $traitName);
  281. }
  282. /**
  283. * @phpstan-param class-string $class
  284. * @phpstan-param class-string $adminClass
  285. */
  286. private function shouldApplyExtension(string $type, mixed $subject, string $class, string $adminClass): bool
  287. {
  288. $classReflection = new \ReflectionClass($class);
  289. $adminClassReflection = new \ReflectionClass($adminClass);
  290. switch ($type) {
  291. case 'global':
  292. return true;
  293. case 'instanceof':
  294. if (!\is_string($subject) || !class_exists($subject)) {
  295. return false;
  296. }
  297. $subjectReflection = new \ReflectionClass($subject);
  298. return $classReflection->isSubclassOf($subject) || $subjectReflection->getName() === $classReflection->getName();
  299. case 'implements':
  300. return \is_string($subject) && interface_exists($subject) && $classReflection->implementsInterface($subject);
  301. case 'extends':
  302. return \is_string($subject) && class_exists($subject) && $classReflection->isSubclassOf($subject);
  303. case 'uses':
  304. return \is_string($subject) && trait_exists($subject) && $this->hasTrait($classReflection, $subject);
  305. case 'admin_instanceof':
  306. if (!\is_string($subject) || !class_exists($subject)) {
  307. return false;
  308. }
  309. $subjectReflection = new \ReflectionClass($subject);
  310. return $adminClassReflection->isSubclassOf($subject) || $subjectReflection->getName() === $adminClassReflection->getName();
  311. case 'admin_implements':
  312. return \is_string($subject) && interface_exists($subject) && $adminClassReflection->implementsInterface($subject);
  313. case 'admin_extends':
  314. return \is_string($subject) && class_exists($subject) && $adminClassReflection->isSubclassOf($subject);
  315. case 'admin_uses':
  316. return \is_string($subject) && trait_exists($subject) && $this->hasTrait($adminClassReflection, $subject);
  317. default:
  318. return false;
  319. }
  320. }
  321. /**
  322. * Add extension configuration to the targets array.
  323. *
  324. * @param array<string, \SplPriorityQueue<int, Reference>> $targets
  325. * @param array<string, mixed> $attributes
  326. */
  327. private function addExtension(
  328. array &$targets,
  329. string $target,
  330. string $extension,
  331. array $attributes,
  332. ): void {
  333. if (!isset($targets[$target])) {
  334. /** @phpstan-var \SplPriorityQueue<int, Reference> $queue */
  335. $queue = new \SplPriorityQueue();
  336. $targets[$target] = $queue;
  337. }
  338. $priority = $attributes['priority'] ?? 0;
  339. $targets[$target]->insert(new Reference($extension), $priority);
  340. }
  341. }