src/Core/Framework/Script/Execution/ScriptLoader.php line 61

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Script\Execution;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\DevOps\Environment\EnvironmentHelper;
  5. use Shopware\Core\Framework\Adapter\Cache\CacheCompressor;
  6. use Shopware\Core\Framework\App\Lifecycle\Persister\ScriptPersister;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  8. use Shopware\Core\Framework\Log\Package;
  9. use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
  10. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  11. use Twig\Cache\FilesystemCache;
  12. /**
  13.  * @internal only for use by the app-system
  14.  *
  15.  * @phpstan-type ScriptInfo = array{app_id: ?string, scriptName: string, script: string, hook: string, appName: ?string, integrationId: ?string, lastModified: string, appVersion: string, active: bool}
  16.  * @phpstan-type IncludesInfo = array{app_id: ?string, name: string, script: string, appName: ?string, integrationId: ?string, lastModified: string}
  17.  */
  18. #[Package('core')]
  19. class ScriptLoader implements EventSubscriberInterface
  20. {
  21.     final public const CACHE_KEY 'shopware-app-scripts';
  22.     private readonly string $cacheDir;
  23.     public function __construct(
  24.         private readonly Connection $connection,
  25.         private readonly ScriptPersister $scriptPersister,
  26.         private readonly TagAwareAdapterInterface $cache,
  27.         string $cacheDir,
  28.         private readonly bool $debug
  29.     ) {
  30.         $this->cacheDir $cacheDir '/scripts';
  31.     }
  32.     public static function getSubscribedEvents(): array
  33.     {
  34.         return ['script.written' => 'invalidateCache'];
  35.     }
  36.     /**
  37.      * @return Script[]
  38.      */
  39.     public function get(string $hook): array
  40.     {
  41.         $cacheItem $this->cache->getItem(self::CACHE_KEY);
  42.         if ($cacheItem->isHit() && $cacheItem->get() && !$this->debug) {
  43.             return CacheCompressor::uncompress($cacheItem)[$hook] ?? [];
  44.         }
  45.         $scripts $this->load();
  46.         $cacheItem CacheCompressor::compress($cacheItem$scripts);
  47.         $this->cache->save($cacheItem);
  48.         return $scripts[$hook] ?? [];
  49.     }
  50.     public function invalidateCache(): void
  51.     {
  52.         $this->cache->deleteItem(self::CACHE_KEY);
  53.     }
  54.     /**
  55.      * @return array<string, list<Script>>
  56.      */
  57.     private function load(): array
  58.     {
  59.         if ($this->debug) {
  60.             $this->scriptPersister->refresh();
  61.         }
  62.         /** @var list<ScriptInfo> $scripts */
  63.         $scripts $this->connection->fetchAllAssociative('
  64.             SELECT LOWER(HEX(`script`.`app_id`)) as `app_id`,
  65.                    `script`.`name` AS scriptName,
  66.                    `script`.`script` AS script,
  67.                    `script`.`hook` AS hook,
  68.                    IFNULL(`script`.`updated_at`, `script`.`created_at`) AS lastModified,
  69.                    `app`.`name` AS appName,
  70.                    LOWER(HEX(`app`.`integration_id`)) AS integrationId,
  71.                    `app`.`version` AS appVersion,
  72.                    `script`.`active` AS active
  73.             FROM `script`
  74.             LEFT JOIN `app` ON `script`.`app_id` = `app`.`id`
  75.             WHERE `script`.`hook` != \'include\'
  76.             ORDER BY `app`.`created_at`, `app`.`id`, `script`.`name`
  77.         ');
  78.         $includes $this->connection->fetchAllAssociative('
  79.             SELECT LOWER(HEX(`script`.`app_id`)) as `app_id`,
  80.                    `script`.`name` AS name,
  81.                    `script`.`script` AS script,
  82.                    `app`.`name` AS appName,
  83.                    LOWER(HEX(`app`.`integration_id`)) AS integrationId,
  84.                    IFNULL(`script`.`updated_at`, `script`.`created_at`) AS lastModified
  85.             FROM `script`
  86.             LEFT JOIN `app` ON `script`.`app_id` = `app`.`id`
  87.             WHERE `script`.`hook` = \'include\'
  88.             ORDER BY `app`.`created_at`, `app`.`id`, `script`.`name`
  89.         ');
  90.         /** @var array<string, list<IncludesInfo>> $allIncludes */
  91.         $allIncludes FetchModeHelper::group($includes);
  92.         $executableScripts = [];
  93.         foreach ($scripts as $script) {
  94.             $appId $script['app_id'];
  95.             $includes $allIncludes[$appId] ?? [];
  96.             $dates = [...[$script['lastModified']], ...array_column($includes'lastModified')];
  97.             /** @var \DateTimeInterface $lastModified */
  98.             $lastModified = new \DateTimeImmutable(max($dates));
  99.             /** @var string $cachePrefix */
  100.             $cachePrefix $script['appName'] ? md5($script['appName'] . $script['appVersion']) : EnvironmentHelper::getVariable('INSTANCE_ID''');
  101.             $includes array_map(function (array $script) use ($appId) {
  102.                 $script['app_id'] = $appId;
  103.                 return new Script(
  104.                     $script['name'],
  105.                     $script['script'],
  106.                     new \DateTimeImmutable($script['lastModified']),
  107.                     $this->getAppInfo($script)
  108.                 );
  109.             }, $includes);
  110.             $options = [];
  111.             if (!$this->debug) {
  112.                 $options['cache'] = new FilesystemCache($this->cacheDir '/' $cachePrefix);
  113.             } else {
  114.                 $options['debug'] = true;
  115.             }
  116.             $executableScripts[$script['hook']][] = new Script(
  117.                 $script['scriptName'],
  118.                 $script['script'],
  119.                 $lastModified,
  120.                 $this->getAppInfo($script),
  121.                 $options,
  122.                 $includes,
  123.                 (bool) $script['active']
  124.             );
  125.         }
  126.         return $executableScripts;
  127.     }
  128.     /**
  129.      * @param ScriptInfo|IncludesInfo $script
  130.      */
  131.     private function getAppInfo(array $script): ?ScriptAppInformation
  132.     {
  133.         if (!$script['app_id'] || !$script['appName'] || !$script['integrationId']) {
  134.             return null;
  135.         }
  136.         return new ScriptAppInformation(
  137.             $script['app_id'],
  138.             $script['appName'],
  139.             $script['integrationId']
  140.         );
  141.     }
  142. }