vendor/shopware/core/System/SystemConfig/SystemConfigService.php line 72

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\System\SystemConfig;
  3. use Doctrine\DBAL\ArrayParameterType;
  4. use Doctrine\DBAL\Connection;
  5. use Shopware\Core\Defaults;
  6. use Shopware\Core\Framework\Bundle;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\MultiInsertQueryQueue;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Field\ConfigJsonField;
  9. use Shopware\Core\Framework\Log\Package;
  10. use Shopware\Core\Framework\Util\Json;
  11. use Shopware\Core\Framework\Util\XmlReader;
  12. use Shopware\Core\Framework\Uuid\Exception\InvalidUuidException;
  13. use Shopware\Core\Framework\Uuid\Uuid;
  14. use Shopware\Core\System\SystemConfig\Event\BeforeSystemConfigChangedEvent;
  15. use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedEvent;
  16. use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedHook;
  17. use Shopware\Core\System\SystemConfig\Event\SystemConfigDomainLoadedEvent;
  18. use Shopware\Core\System\SystemConfig\Exception\BundleConfigNotFoundException;
  19. use Shopware\Core\System\SystemConfig\Exception\InvalidDomainException;
  20. use Shopware\Core\System\SystemConfig\Exception\InvalidKeyException;
  21. use Shopware\Core\System\SystemConfig\Exception\InvalidSettingValueException;
  22. use Shopware\Core\System\SystemConfig\Util\ConfigReader;
  23. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  24. use Symfony\Contracts\Service\ResetInterface;
  25. use function json_decode;
  26. #[Package('system-settings')]
  27. class SystemConfigService implements ResetInterface
  28. {
  29.     /**
  30.      * @var array<string, bool>
  31.      */
  32.     private array $keys = ['all' => true];
  33.     /**
  34.      * @var array<mixed>
  35.      */
  36.     private array $traces = [];
  37.     /**
  38.      * @var array<string, string>|null
  39.      */
  40.     private ?array $appMapping null;
  41.     /**
  42.      * @internal
  43.      */
  44.     public function __construct(
  45.         private readonly Connection $connection,
  46.         private readonly ConfigReader $configReader,
  47.         private readonly AbstractSystemConfigLoader $loader,
  48.         private readonly EventDispatcherInterface $eventDispatcher
  49.     ) {
  50.     }
  51.     public static function buildName(string $key): string
  52.     {
  53.         return 'config.' $key;
  54.     }
  55.     /**
  56.      * @return array<mixed>|bool|float|int|string|null
  57.      */
  58.     public function get(string $key, ?string $salesChannelId null)
  59.     {
  60.         foreach (array_keys($this->keys) as $trace) {
  61.             $this->traces[$trace][self::buildName($key)] = true;
  62.         }
  63.         $config $this->loader->load($salesChannelId);
  64.         $parts explode('.'$key);
  65.         $pointer $config;
  66.         foreach ($parts as $part) {
  67.             if (!\is_array($pointer)) {
  68.                 return null;
  69.             }
  70.             if (\array_key_exists($part$pointer)) {
  71.                 $pointer $pointer[$part];
  72.                 continue;
  73.             }
  74.             return null;
  75.         }
  76.         return $pointer;
  77.     }
  78.     public function getString(string $key, ?string $salesChannelId null): string
  79.     {
  80.         $value $this->get($key$salesChannelId);
  81.         if (!\is_array($value)) {
  82.             return (string) $value;
  83.         }
  84.         throw new InvalidSettingValueException($key'string'\gettype($value));
  85.     }
  86.     public function getInt(string $key, ?string $salesChannelId null): int
  87.     {
  88.         $value $this->get($key$salesChannelId);
  89.         if (!\is_array($value)) {
  90.             return (int) $value;
  91.         }
  92.         throw new InvalidSettingValueException($key'int'\gettype($value));
  93.     }
  94.     public function getFloat(string $key, ?string $salesChannelId null): float
  95.     {
  96.         $value $this->get($key$salesChannelId);
  97.         if (!\is_array($value)) {
  98.             return (float) $value;
  99.         }
  100.         throw new InvalidSettingValueException($key'float'\gettype($value));
  101.     }
  102.     public function getBool(string $key, ?string $salesChannelId null): bool
  103.     {
  104.         return (bool) $this->get($key$salesChannelId);
  105.     }
  106.     /**
  107.      * @internal should not be used in storefront or store api. The cache layer caches all accessed config keys and use them as cache tag.
  108.      *
  109.      * gets all available shop configs and returns them as an array
  110.      *
  111.      * @return array<mixed>
  112.      */
  113.     public function all(?string $salesChannelId null): array
  114.     {
  115.         return $this->loader->load($salesChannelId);
  116.     }
  117.     /**
  118.      * @internal should not be used in storefront or store api. The cache layer caches all accessed config keys and use them as cache tag.
  119.      *
  120.      * @throws InvalidDomainException
  121.      *
  122.      * @return array<mixed>
  123.      */
  124.     public function getDomain(string $domain, ?string $salesChannelId nullbool $inherit false): array
  125.     {
  126.         $domain trim($domain);
  127.         if ($domain === '') {
  128.             throw new InvalidDomainException('Empty domain');
  129.         }
  130.         $queryBuilder $this->connection->createQueryBuilder()
  131.             ->select(['configuration_key''configuration_value'])
  132.             ->from('system_config');
  133.         if ($inherit) {
  134.             $queryBuilder->where('sales_channel_id IS NULL OR sales_channel_id = :salesChannelId');
  135.         } elseif ($salesChannelId === null) {
  136.             $queryBuilder->where('sales_channel_id IS NULL');
  137.         } else {
  138.             $queryBuilder->where('sales_channel_id = :salesChannelId');
  139.         }
  140.         $domain rtrim($domain'.') . '.';
  141.         $escapedDomain str_replace('%''\\%'$domain);
  142.         $salesChannelId $salesChannelId Uuid::fromHexToBytes($salesChannelId) : null;
  143.         $queryBuilder->andWhere('configuration_key LIKE :prefix')
  144.             ->addOrderBy('sales_channel_id''ASC')
  145.             ->setParameter('prefix'$escapedDomain '%')
  146.             ->setParameter('salesChannelId'$salesChannelId);
  147.         $configs $queryBuilder->executeQuery()->fetchAllNumeric();
  148.         if ($configs === []) {
  149.             return [];
  150.         }
  151.         $merged = [];
  152.         foreach ($configs as [$key$value]) {
  153.             if ($value !== null) {
  154.                 $value json_decode((string) $valuetrue512\JSON_THROW_ON_ERROR);
  155.                 if ($value === false || !isset($value[ConfigJsonField::STORAGE_KEY])) {
  156.                     $value null;
  157.                 } else {
  158.                     $value $value[ConfigJsonField::STORAGE_KEY];
  159.                 }
  160.             }
  161.             $inheritedValuePresent \array_key_exists($key$merged);
  162.             $valueConsideredEmpty = !\is_bool($value) && empty($value);
  163.             if ($inheritedValuePresent && $valueConsideredEmpty) {
  164.                 continue;
  165.             }
  166.             $merged[$key] = $value;
  167.         }
  168.         $event = new SystemConfigDomainLoadedEvent($domain$merged$inherit$salesChannelId);
  169.         $this->eventDispatcher->dispatch($event);
  170.         return $event->getConfig();
  171.     }
  172.     /**
  173.      * @param array<mixed>|bool|float|int|string|null $value
  174.      */
  175.     public function set(string $key$value, ?string $salesChannelId null): void
  176.     {
  177.         $this->setMultiple([$key => $value], $salesChannelId);
  178.     }
  179.     /**
  180.      * @param array<string, array<mixed>|bool|float|int|string|null> $values
  181.      */
  182.     public function setMultiple(array $values, ?string $salesChannelId null): void
  183.     {
  184.         $where $salesChannelId 'sales_channel_id = :salesChannelId' 'sales_channel_id IS NULL';
  185.         $existingIds $this->connection
  186.             ->fetchAllKeyValue(
  187.                 'SELECT configuration_key, id FROM system_config WHERE ' $where ' and configuration_key IN (:configurationKeys)',
  188.                 [
  189.                     'salesChannelId' => $salesChannelId Uuid::fromHexToBytes($salesChannelId) : null,
  190.                     'configurationKeys' => array_keys($values),
  191.                 ],
  192.                 [
  193.                     'configurationKeys' => ArrayParameterType::STRING,
  194.                 ]
  195.             );
  196.         $toBeDeleted = [];
  197.         $insertQueue = new MultiInsertQueryQueue($this->connection100falsetrue);
  198.         $events = [];
  199.         foreach ($values as $key => $value) {
  200.             $key trim($key);
  201.             $this->validate($key$salesChannelId);
  202.             $event = new BeforeSystemConfigChangedEvent($key$value$salesChannelId);
  203.             $this->eventDispatcher->dispatch($event);
  204.             // On null value, delete the config
  205.             if ($value === null) {
  206.                 $toBeDeleted[] = $key;
  207.                 $events[] = new SystemConfigChangedEvent($key$value$salesChannelId);
  208.                 continue;
  209.             }
  210.             if (isset($existingIds[$key])) {
  211.                 $this->connection->update(
  212.                     'system_config',
  213.                     [
  214.                         'configuration_value' => Json::encode(['_value' => $value]),
  215.                         'updated_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  216.                     ],
  217.                     [
  218.                         'id' => $existingIds[$key],
  219.                     ]
  220.                 );
  221.                 $events[] = new SystemConfigChangedEvent($key$value$salesChannelId);
  222.                 continue;
  223.             }
  224.             $insertQueue->addInsert(
  225.                 'system_config',
  226.                 [
  227.                     'id' => Uuid::randomBytes(),
  228.                     'configuration_key' => $key,
  229.                     'configuration_value' => Json::encode(['_value' => $value]),
  230.                     'sales_channel_id' => $salesChannelId Uuid::fromHexToBytes($salesChannelId) : null,
  231.                     'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  232.                 ],
  233.             );
  234.             $events[] = new SystemConfigChangedEvent($key$value$salesChannelId);
  235.         }
  236.         // Delete all null values
  237.         if (!empty($toBeDeleted)) {
  238.             $qb $this->connection
  239.                 ->createQueryBuilder()
  240.                 ->where('configuration_key IN (:keys)')
  241.                 ->setParameter('keys'$toBeDeletedArrayParameterType::STRING);
  242.             if ($salesChannelId) {
  243.                 $qb->andWhere('sales_channel_id = :salesChannelId')
  244.                     ->setParameter('salesChannelId'Uuid::fromHexToBytes($salesChannelId));
  245.             } else {
  246.                 $qb->andWhere('sales_channel_id IS NULL');
  247.             }
  248.             $qb->delete('system_config')
  249.                 ->executeStatement();
  250.         }
  251.         $insertQueue->execute();
  252.         // Dispatch events that the given values have been changed
  253.         foreach ($events as $event) {
  254.             $this->eventDispatcher->dispatch($event);
  255.         }
  256.         $this->eventDispatcher->dispatch(new SystemConfigChangedHook($values$this->getAppMapping()));
  257.     }
  258.     public function delete(string $key, ?string $salesChannel null): void
  259.     {
  260.         $this->setMultiple([$key => null], $salesChannel);
  261.     }
  262.     /**
  263.      * Fetches default values from bundle configuration and saves it to database
  264.      */
  265.     public function savePluginConfiguration(Bundle $bundlebool $override false): void
  266.     {
  267.         try {
  268.             $config $this->configReader->getConfigFromBundle($bundle);
  269.         } catch (BundleConfigNotFoundException) {
  270.             return;
  271.         }
  272.         $prefix $bundle->getName() . '.config.';
  273.         $this->saveConfig($config$prefix$override);
  274.     }
  275.     /**
  276.      * @param array<mixed> $config
  277.      */
  278.     public function saveConfig(array $configstring $prefixbool $override): void
  279.     {
  280.         $relevantSettings $this->getDomain($prefix);
  281.         foreach ($config as $card) {
  282.             foreach ($card['elements'] as $element) {
  283.                 $key $prefix $element['name'];
  284.                 if (!isset($element['defaultValue'])) {
  285.                     continue;
  286.                 }
  287.                 $value XmlReader::phpize($element['defaultValue']);
  288.                 if ($override || !isset($relevantSettings[$key])) {
  289.                     $this->set($key$value);
  290.                 }
  291.             }
  292.         }
  293.     }
  294.     public function deletePluginConfiguration(Bundle $bundle): void
  295.     {
  296.         try {
  297.             $config $this->configReader->getConfigFromBundle($bundle);
  298.         } catch (BundleConfigNotFoundException) {
  299.             return;
  300.         }
  301.         $this->deleteExtensionConfiguration($bundle->getName(), $config);
  302.     }
  303.     /**
  304.      * @param array<mixed> $config
  305.      */
  306.     public function deleteExtensionConfiguration(string $extensionName, array $config): void
  307.     {
  308.         $prefix $extensionName '.config.';
  309.         $configKeys = [];
  310.         foreach ($config as $card) {
  311.             foreach ($card['elements'] as $element) {
  312.                 $configKeys[] = $prefix $element['name'];
  313.             }
  314.         }
  315.         if (empty($configKeys)) {
  316.             return;
  317.         }
  318.         $this->setMultiple(array_fill_keys($configKeysnull));
  319.     }
  320.     /**
  321.      * @return mixed|null All kind of data could be cached
  322.      */
  323.     public function trace(string $key\Closure $param)
  324.     {
  325.         $this->traces[$key] = [];
  326.         $this->keys[$key] = true;
  327.         $result $param();
  328.         unset($this->keys[$key]);
  329.         return $result;
  330.     }
  331.     /**
  332.      * @return array<mixed>
  333.      */
  334.     public function getTrace(string $key): array
  335.     {
  336.         $trace = isset($this->traces[$key]) ? array_keys($this->traces[$key]) : [];
  337.         unset($this->traces[$key]);
  338.         return $trace;
  339.     }
  340.     public function reset(): void
  341.     {
  342.         $this->appMapping null;
  343.     }
  344.     /**
  345.      * @throws InvalidKeyException
  346.      * @throws InvalidUuidException
  347.      */
  348.     private function validate(string $key, ?string $salesChannelId): void
  349.     {
  350.         $key trim($key);
  351.         if ($key === '') {
  352.             throw new InvalidKeyException('key may not be empty');
  353.         }
  354.         if ($salesChannelId && !Uuid::isValid($salesChannelId)) {
  355.             throw new InvalidUuidException($salesChannelId);
  356.         }
  357.     }
  358.     /**
  359.      * @return array<string, string>
  360.      */
  361.     private function getAppMapping(): array
  362.     {
  363.         if ($this->appMapping !== null) {
  364.             return $this->appMapping;
  365.         }
  366.         /** @var array<string, string> $allKeyValue */
  367.         $allKeyValue $this->connection->fetchAllKeyValue('SELECT LOWER(HEX(id)), name FROM app');
  368.         return $this->appMapping $allKeyValue;
  369.     }
  370. }