src/Core/Framework/Plugin/KernelPluginLoader/KernelPluginLoader.php line 108

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Plugin\KernelPluginLoader;
  3. use Composer\Autoload\ClassLoader;
  4. use Composer\Autoload\ClassMapGenerator;
  5. use Shopware\Core\Framework\Log\Package;
  6. use Shopware\Core\Framework\Parameter\AdditionalBundleParameters;
  7. use Shopware\Core\Framework\Plugin;
  8. use Shopware\Core\Framework\Plugin\Exception\KernelPluginLoaderException;
  9. use Shopware\Core\Framework\Plugin\KernelPluginCollection;
  10. use Symfony\Component\DependencyInjection\ContainerBuilder;
  11. use Symfony\Component\DependencyInjection\Definition;
  12. use Symfony\Component\DependencyInjection\Reference;
  13. use Symfony\Component\HttpKernel\Bundle\Bundle;
  14. #[Package('core')]
  15. abstract class KernelPluginLoader extends Bundle
  16. {
  17.     /**
  18.      * @var array<int, mixed>
  19.      */
  20.     protected $pluginInfos = [];
  21.     private readonly KernelPluginCollection $pluginInstances;
  22.     private readonly string $pluginDir;
  23.     private bool $initialized false;
  24.     /**
  25.      * @internal
  26.      */
  27.     public function __construct(private readonly ClassLoader $classLoader, ?string $pluginDir null)
  28.     {
  29.         $this->pluginDir $pluginDir ?? 'custom/plugins';
  30.         $this->pluginInstances = new KernelPluginCollection();
  31.     }
  32.     final public function getPluginDir(string $projectDir): string
  33.     {
  34.         // absolute path
  35.         if (mb_strpos($this->pluginDir'/') === 0) {
  36.             return $this->pluginDir;
  37.         }
  38.         return $projectDir '/' $this->pluginDir;
  39.     }
  40.     /**
  41.      * @return array<int, mixed>
  42.      * Basic information required for instantiating the plugins
  43.      */
  44.     final public function getPluginInfos(): array
  45.     {
  46.         return $this->pluginInfos;
  47.     }
  48.     /**
  49.      * @final
  50.      * Instances of the plugin bundle classes
  51.      */
  52.     public function getPluginInstances(): KernelPluginCollection
  53.     {
  54.         return $this->pluginInstances;
  55.     }
  56.     /**
  57.      * @param array<string, mixed> $kernelParameters
  58.      * @param array<int, string> $loadedBundles
  59.      *
  60.      * @return \Traversable<Bundle>
  61.      */
  62.     final public function getBundles(array $kernelParameters = [], array $loadedBundles = []): iterable
  63.     {
  64.         if (!$this->initialized) {
  65.             return;
  66.         }
  67.         foreach ($this->pluginInstances->getActives() as $plugin) {
  68.             $copy = new KernelPluginCollection($this->getPluginInstances()->all());
  69.             $additionalBundleParameters = new AdditionalBundleParameters($this->classLoader$copy$kernelParameters);
  70.             $additionalBundles $plugin->getAdditionalBundles($additionalBundleParameters);
  71.             [$preLoaded$postLoaded] = $this->splitBundlesIntoPreAndPost($additionalBundles);
  72.             foreach ([...\array_values($preLoaded), $plugin, ...\array_values($postLoaded)] as $bundle) {
  73.                 if (!\in_array($bundle->getName(), $loadedBundlestrue)) {
  74.                     yield $bundle;
  75.                     $loadedBundles[] = $bundle->getName();
  76.                 }
  77.             }
  78.         }
  79.         if (!\in_array($this->getName(), $loadedBundlestrue)) {
  80.             yield $this;
  81.         }
  82.     }
  83.     /**
  84.      * @throws KernelPluginLoaderException
  85.      */
  86.     final public function initializePlugins(string $projectDir): void
  87.     {
  88.         if ($this->initialized) {
  89.             return;
  90.         }
  91.         $this->loadPluginInfos();
  92.         if (empty($this->pluginInfos)) {
  93.             $this->initialized true;
  94.             return;
  95.         }
  96.         $this->registerPluginNamespaces($projectDir);
  97.         $this->instantiatePlugins($projectDir);
  98.         $this->initialized true;
  99.     }
  100.     final public function build(ContainerBuilder $container): void
  101.     {
  102.         if (!$this->initialized) {
  103.             return;
  104.         }
  105.         parent::build($container);
  106.         /*
  107.          * Register every plugin in the di container, enable autowire and set public
  108.          */
  109.         foreach ($this->pluginInstances->getActives() as $plugin) {
  110.             $class $plugin::class;
  111.             $definition = new Definition();
  112.             if ($container->hasDefinition($class)) {
  113.                 $definition $container->getDefinition($class);
  114.             }
  115.             $definition->setFactory([new Reference(self::class), 'getPluginInstance']);
  116.             $definition->addArgument($class);
  117.             $definition->setAutowired(true);
  118.             $definition->setPublic(true);
  119.             $container->setDefinition($class$definition);
  120.         }
  121.     }
  122.     final public function getPluginInstance(string $class): ?Plugin
  123.     {
  124.         $plugin $this->pluginInstances->get($class);
  125.         if (!$plugin || !$plugin->isActive()) {
  126.             return null;
  127.         }
  128.         return $plugin;
  129.     }
  130.     public function getClassLoader(): ClassLoader
  131.     {
  132.         return $this->classLoader;
  133.     }
  134.     abstract protected function loadPluginInfos(): void;
  135.     /**
  136.      * @throws KernelPluginLoaderException
  137.      */
  138.     private function registerPluginNamespaces(string $projectDir): void
  139.     {
  140.         foreach ($this->pluginInfos as $plugin) {
  141.             $pluginName $plugin['name'] ?? $plugin['baseClass'];
  142.             // plugins managed by composer are already in the classMap
  143.             if ($plugin['managedByComposer']) {
  144.                 continue;
  145.             }
  146.             if (!isset($plugin['autoload'])) {
  147.                 $reason sprintf(
  148.                     'Unable to register plugin "%s" in autoload. Required property `autoload` missing.',
  149.                     $plugin['baseClass']
  150.                 );
  151.                 throw new KernelPluginLoaderException($pluginName$reason);
  152.             }
  153.             $psr4 $plugin['autoload']['psr-4'] ?? [];
  154.             $psr0 $plugin['autoload']['psr-0'] ?? [];
  155.             if (empty($psr4) && empty($psr0)) {
  156.                 $reason sprintf(
  157.                     'Unable to register plugin "%s" in autoload. Required property `psr-4` or `psr-0` missing in property autoload.',
  158.                     $plugin['baseClass']
  159.                 );
  160.                 throw new KernelPluginLoaderException($pluginName$reason);
  161.             }
  162.             foreach ($psr4 as $namespace => $paths) {
  163.                 if (\is_string($paths)) {
  164.                     $paths = [$paths];
  165.                 }
  166.                 $mappedPaths $this->mapPsrPaths($pluginName$paths$projectDir$plugin['path']);
  167.                 $this->classLoader->addPsr4($namespace$mappedPaths);
  168.                 if ($this->classLoader->isClassMapAuthoritative()) {
  169.                     foreach ($mappedPaths as $mappedPath) {
  170.                         $this->classLoader->addClassMap(ClassMapGenerator::createMap($mappedPath));
  171.                     }
  172.                 }
  173.             }
  174.             foreach ($psr0 as $namespace => $paths) {
  175.                 if (\is_string($paths)) {
  176.                     $paths = [$paths];
  177.                 }
  178.                 $mappedPaths $this->mapPsrPaths($pluginName$paths$projectDir$plugin['path']);
  179.                 $this->classLoader->add($namespace$mappedPaths);
  180.                 if ($this->classLoader->isClassMapAuthoritative()) {
  181.                     foreach ($mappedPaths as $mappedPath) {
  182.                         $this->classLoader->addClassMap(ClassMapGenerator::createMap($mappedPath));
  183.                     }
  184.                 }
  185.             }
  186.         }
  187.     }
  188.     /**
  189.      * @param array<string> $psr
  190.      *
  191.      * @throws KernelPluginLoaderException
  192.      *
  193.      * @return array<string>
  194.      */
  195.     private function mapPsrPaths(string $plugin, array $psrstring $projectDirstring $pluginRootPath): array
  196.     {
  197.         $mappedPaths = [];
  198.         $absolutePluginRootPath $this->getAbsolutePluginRootPath($projectDir$pluginRootPath);
  199.         if (mb_strpos($absolutePluginRootPath$projectDir) !== 0) {
  200.             throw new KernelPluginLoaderException(
  201.                 $plugin,
  202.                 sprintf('Plugin dir %s needs to be a sub-directory of the project dir %s'$pluginRootPath$projectDir)
  203.             );
  204.         }
  205.         foreach ($psr as $path) {
  206.             $mappedPaths[] = $absolutePluginRootPath '/' $path;
  207.         }
  208.         return $mappedPaths;
  209.     }
  210.     private function getAbsolutePluginRootPath(string $projectDirstring $pluginRootPath): string
  211.     {
  212.         // is relative path
  213.         if (mb_strpos($pluginRootPath'/') !== 0) {
  214.             $pluginRootPath $projectDir '/' $pluginRootPath;
  215.         }
  216.         return $pluginRootPath;
  217.     }
  218.     /**
  219.      * @throws KernelPluginLoaderException
  220.      */
  221.     private function instantiatePlugins(string $projectDir): void
  222.     {
  223.         foreach ($this->pluginInfos as $pluginData) {
  224.             $className $pluginData['baseClass'];
  225.             $pluginClassFilePath $this->classLoader->findFile($className);
  226.             if (!class_exists($className) || !$pluginClassFilePath || !file_exists($pluginClassFilePath)) {
  227.                 continue;
  228.             }
  229.             /** @var Plugin $plugin */
  230.             $plugin = new $className((bool) $pluginData['active'], $pluginData['path'], $projectDir);
  231.             if (!$plugin instanceof Plugin) {
  232.                 $reason sprintf('Plugin class "%s" must extend "%s"'$plugin::class, Plugin::class);
  233.                 throw new KernelPluginLoaderException($pluginData['name'], $reason);
  234.             }
  235.             $this->pluginInstances->add($plugin);
  236.         }
  237.     }
  238.     /**
  239.      * @param Bundle[] $bundles
  240.      *
  241.      * @return array<Bundle[]>
  242.      */
  243.     private function splitBundlesIntoPreAndPost(array $bundles): array
  244.     {
  245.         $pre = [];
  246.         $post = [];
  247.         foreach ($bundles as $index => $bundle) {
  248.             if (\is_int($index) && $index 0) {
  249.                 $pre[$index] = $bundle;
  250.             } else {
  251.                 $post[$index] = $bundle;
  252.             }
  253.         }
  254.         \ksort($pre);
  255.         \ksort($post);
  256.         return [$pre$post];
  257.     }
  258. }