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($adminExtensionClassfalse) && $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 (!$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($adminClassfalse) && $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.                         \is_object($class) ? \get_class($class) : \gettype($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 $tagsDefinition $adminContainerBuilder $container, array $extensionMap): array
  165.     {
  166.         // Trim possible parameter delimiters ("%") from the class name.
  167.         $adminClass trim($admin->getClass() ?? '''%');
  168.         if (!class_exists($adminClassfalse) && $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.                             \is_object($class) ? \get_class($class) : \gettype($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 $classstring $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.      * @param mixed $subject
  284.      *
  285.      * @phpstan-param class-string $class
  286.      * @phpstan-param class-string $adminClass
  287.      */
  288.     private function shouldApplyExtension(string $type$subjectstring $classstring $adminClass): bool
  289.     {
  290.         $classReflection = new \ReflectionClass($class);
  291.         $adminClassReflection = new \ReflectionClass($adminClass);
  292.         switch ($type) {
  293.             case 'global':
  294.                 return true;
  295.             case 'instanceof':
  296.                 if (!\is_string($subject) || !class_exists($subject)) {
  297.                     return false;
  298.                 }
  299.                 $subjectReflection = new \ReflectionClass($subject);
  300.                 return $classReflection->isSubclassOf($subject) || $subjectReflection->getName() === $classReflection->getName();
  301.             case 'implements':
  302.                 return \is_string($subject) && interface_exists($subject) && $classReflection->implementsInterface($subject);
  303.             case 'extends':
  304.                 return \is_string($subject) && class_exists($subject) && $classReflection->isSubclassOf($subject);
  305.             case 'uses':
  306.                 return \is_string($subject) && trait_exists($subject) && $this->hasTrait($classReflection$subject);
  307.             case 'admin_instanceof':
  308.                 if (!\is_string($subject) || !class_exists($subject)) {
  309.                     return false;
  310.                 }
  311.                 $subjectReflection = new \ReflectionClass($subject);
  312.                 return $adminClassReflection->isSubclassOf($subject) || $subjectReflection->getName() === $adminClassReflection->getName();
  313.             case 'admin_implements':
  314.                 return \is_string($subject) && interface_exists($subject) && $adminClassReflection->implementsInterface($subject);
  315.             case 'admin_extends':
  316.                 return \is_string($subject) && class_exists($subject) && $adminClassReflection->isSubclassOf($subject);
  317.             case 'admin_uses':
  318.                 return \is_string($subject) && trait_exists($subject) && $this->hasTrait($adminClassReflection$subject);
  319.             default:
  320.                 return false;
  321.         }
  322.     }
  323.     /**
  324.      * Add extension configuration to the targets array.
  325.      *
  326.      * @param array<string, \SplPriorityQueue<int, Reference>> $targets
  327.      * @param array<string, mixed>                             $attributes
  328.      */
  329.     private function addExtension(
  330.         array &$targets,
  331.         string $target,
  332.         string $extension,
  333.         array $attributes
  334.     ): void {
  335.         if (!isset($targets[$target])) {
  336.             /** @phpstan-var \SplPriorityQueue<int, Reference> $queue */
  337.             $queue = new \SplPriorityQueue();
  338.             $targets[$target] = $queue;
  339.         }
  340.         $priority $attributes['priority'] ?? 0;
  341.         $targets[$target]->insert(new Reference($extension), $priority);
  342.     }
  343. }