src/Core/System/SystemConfig/SystemConfigService.php line 327

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\System\SystemConfig;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Framework\Bundle;
  5. use Shopware\Core\Framework\Context;
  6. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Field\ConfigJsonField;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  11. use Shopware\Core\Framework\Log\Package;
  12. use Shopware\Core\Framework\Util\XmlReader;
  13. use Shopware\Core\Framework\Uuid\Exception\InvalidUuidException;
  14. use Shopware\Core\Framework\Uuid\Uuid;
  15. use Shopware\Core\System\SystemConfig\Event\BeforeSystemConfigChangedEvent;
  16. use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedEvent;
  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 function json_decode;
  25. #[Package('system-settings')]
  26. class SystemConfigService
  27. {
  28.     /**
  29.      * @var array<string, bool>
  30.      */
  31.     private array $keys = ['all' => true];
  32.     /**
  33.      * @var array<mixed>
  34.      */
  35.     private array $traces = [];
  36.     /**
  37.      * @internal
  38.      */
  39.     public function __construct(private readonly Connection $connection, private readonly EntityRepository $systemConfigRepository, private readonly ConfigReader $configReader, private readonly AbstractSystemConfigLoader $loader, private readonly EventDispatcherInterface $eventDispatcher)
  40.     {
  41.     }
  42.     public static function buildName(string $key): string
  43.     {
  44.         return 'config.' $key;
  45.     }
  46.     /**
  47.      * @return array<mixed>|bool|float|int|string|null
  48.      */
  49.     public function get(string $key, ?string $salesChannelId null)
  50.     {
  51.         foreach (array_keys($this->keys) as $trace) {
  52.             $this->traces[$trace][self::buildName($key)] = true;
  53.         }
  54.         $config $this->loader->load($salesChannelId);
  55.         $parts explode('.'$key);
  56.         $pointer $config;
  57.         foreach ($parts as $part) {
  58.             if (!\is_array($pointer)) {
  59.                 return null;
  60.             }
  61.             if (\array_key_exists($part$pointer)) {
  62.                 $pointer $pointer[$part];
  63.                 continue;
  64.             }
  65.             return null;
  66.         }
  67.         return $pointer;
  68.     }
  69.     public function getString(string $key, ?string $salesChannelId null): string
  70.     {
  71.         $value $this->get($key$salesChannelId);
  72.         if (!\is_array($value)) {
  73.             return (string) $value;
  74.         }
  75.         throw new InvalidSettingValueException($key'string'\gettype($value));
  76.     }
  77.     public function getInt(string $key, ?string $salesChannelId null): int
  78.     {
  79.         $value $this->get($key$salesChannelId);
  80.         if (!\is_array($value)) {
  81.             return (int) $value;
  82.         }
  83.         throw new InvalidSettingValueException($key'int'\gettype($value));
  84.     }
  85.     public function getFloat(string $key, ?string $salesChannelId null): float
  86.     {
  87.         $value $this->get($key$salesChannelId);
  88.         if (!\is_array($value)) {
  89.             return (float) $value;
  90.         }
  91.         throw new InvalidSettingValueException($key'float'\gettype($value));
  92.     }
  93.     public function getBool(string $key, ?string $salesChannelId null): bool
  94.     {
  95.         return (bool) $this->get($key$salesChannelId);
  96.     }
  97.     /**
  98.      * @internal should not be used in storefront or store api. The cache layer caches all accessed config keys and use them as cache tag.
  99.      *
  100.      * gets all available shop configs and returns them as an array
  101.      *
  102.      * @return array<mixed>
  103.      */
  104.     public function all(?string $salesChannelId null): array
  105.     {
  106.         return $this->loader->load($salesChannelId);
  107.     }
  108.     /**
  109.      * @internal should not be used in storefront or store api. The cache layer caches all accessed config keys and use them as cache tag.
  110.      *
  111.      * @throws InvalidDomainException
  112.      *
  113.      * @return array<mixed>
  114.      */
  115.     public function getDomain(string $domain, ?string $salesChannelId nullbool $inherit false): array
  116.     {
  117.         $domain trim($domain);
  118.         if ($domain === '') {
  119.             throw new InvalidDomainException('Empty domain');
  120.         }
  121.         $queryBuilder $this->connection->createQueryBuilder()
  122.             ->select(['configuration_key''configuration_value'])
  123.             ->from('system_config');
  124.         if ($inherit) {
  125.             $queryBuilder->where('sales_channel_id IS NULL OR sales_channel_id = :salesChannelId');
  126.         } elseif ($salesChannelId === null) {
  127.             $queryBuilder->where('sales_channel_id IS NULL');
  128.         } else {
  129.             $queryBuilder->where('sales_channel_id = :salesChannelId');
  130.         }
  131.         $domain rtrim($domain'.') . '.';
  132.         $escapedDomain str_replace('%''\\%'$domain);
  133.         $salesChannelId $salesChannelId Uuid::fromHexToBytes($salesChannelId) : null;
  134.         $queryBuilder->andWhere('configuration_key LIKE :prefix')
  135.             ->addOrderBy('sales_channel_id''ASC')
  136.             ->setParameter('prefix'$escapedDomain '%')
  137.             ->setParameter('salesChannelId'$salesChannelId);
  138.         $configs $queryBuilder->executeQuery()->fetchAllNumeric();
  139.         if ($configs === []) {
  140.             return [];
  141.         }
  142.         $merged = [];
  143.         foreach ($configs as [$key$value]) {
  144.             if ($value !== null) {
  145.                 $value json_decode((string) $valuetrue512\JSON_THROW_ON_ERROR);
  146.                 if ($value === false || !isset($value[ConfigJsonField::STORAGE_KEY])) {
  147.                     $value null;
  148.                 } else {
  149.                     $value $value[ConfigJsonField::STORAGE_KEY];
  150.                 }
  151.             }
  152.             $inheritedValuePresent \array_key_exists($key$merged);
  153.             $valueConsideredEmpty = !\is_bool($value) && empty($value);
  154.             if ($inheritedValuePresent && $valueConsideredEmpty) {
  155.                 continue;
  156.             }
  157.             $merged[$key] = $value;
  158.         }
  159.         $event = new SystemConfigDomainLoadedEvent($domain$merged$inherit$salesChannelId);
  160.         $this->eventDispatcher->dispatch($event);
  161.         return $event->getConfig();
  162.     }
  163.     /**
  164.      * @param array<mixed>|bool|float|int|string|null $value
  165.      */
  166.     public function set(string $key$value, ?string $salesChannelId null): void
  167.     {
  168.         $key trim($key);
  169.         $this->validate($key$salesChannelId);
  170.         $event = new BeforeSystemConfigChangedEvent($key$value$salesChannelId);
  171.         $this->eventDispatcher->dispatch($event);
  172.         $id $this->getId($key$salesChannelId);
  173.         if ($value === null) {
  174.             if ($id) {
  175.                 $this->systemConfigRepository->delete([['id' => $id]], Context::createDefaultContext());
  176.             }
  177.             $this->eventDispatcher->dispatch(new SystemConfigChangedEvent($key$value$salesChannelId));
  178.             return;
  179.         }
  180.         $data = [
  181.             'id' => $id ?? Uuid::randomHex(),
  182.             'configurationKey' => $key,
  183.             'configurationValue' => $event->getValue(),
  184.             'salesChannelId' => $salesChannelId,
  185.         ];
  186.         $this->systemConfigRepository->upsert([$data], Context::createDefaultContext());
  187.         $this->eventDispatcher->dispatch(new SystemConfigChangedEvent($key$event->getValue(), $salesChannelId));
  188.     }
  189.     public function delete(string $key, ?string $salesChannel null): void
  190.     {
  191.         $this->set($keynull$salesChannel);
  192.     }
  193.     /**
  194.      * Fetches default values from bundle configuration and saves it to database
  195.      */
  196.     public function savePluginConfiguration(Bundle $bundlebool $override false): void
  197.     {
  198.         try {
  199.             $config $this->configReader->getConfigFromBundle($bundle);
  200.         } catch (BundleConfigNotFoundException) {
  201.             return;
  202.         }
  203.         $prefix $bundle->getName() . '.config.';
  204.         $this->saveConfig($config$prefix$override);
  205.     }
  206.     /**
  207.      * @param array<mixed> $config
  208.      */
  209.     public function saveConfig(array $configstring $prefixbool $override): void
  210.     {
  211.         $relevantSettings $this->getDomain($prefix);
  212.         foreach ($config as $card) {
  213.             foreach ($card['elements'] as $element) {
  214.                 $key $prefix $element['name'];
  215.                 if (!isset($element['defaultValue'])) {
  216.                     continue;
  217.                 }
  218.                 $value XmlReader::phpize($element['defaultValue']);
  219.                 if ($override || !isset($relevantSettings[$key]) || $relevantSettings[$key] === null) {
  220.                     $this->set($key$value);
  221.                 }
  222.             }
  223.         }
  224.     }
  225.     public function deletePluginConfiguration(Bundle $bundle): void
  226.     {
  227.         try {
  228.             $config $this->configReader->getConfigFromBundle($bundle);
  229.         } catch (BundleConfigNotFoundException) {
  230.             return;
  231.         }
  232.         $this->deleteExtensionConfiguration($bundle->getName(), $config);
  233.     }
  234.     /**
  235.      * @param array<mixed> $config
  236.      */
  237.     public function deleteExtensionConfiguration(string $extensionName, array $config): void
  238.     {
  239.         $prefix $extensionName '.config.';
  240.         $configKeys = [];
  241.         foreach ($config as $card) {
  242.             foreach ($card['elements'] as $element) {
  243.                 $configKeys[] = $prefix $element['name'];
  244.             }
  245.         }
  246.         if (empty($configKeys)) {
  247.             return;
  248.         }
  249.         $criteria = new Criteria();
  250.         $criteria->addFilter(new EqualsAnyFilter('configurationKey'$configKeys));
  251.         $systemConfigIds $this->systemConfigRepository->searchIds($criteriaContext::createDefaultContext())->getIds();
  252.         if (empty($systemConfigIds)) {
  253.             return;
  254.         }
  255.         $ids array_map(static fn ($id) => ['id' => $id], $systemConfigIds);
  256.         $this->systemConfigRepository->delete($idsContext::createDefaultContext());
  257.     }
  258.     /**
  259.      * @return mixed|null All kind of data could be cached
  260.      */
  261.     public function trace(string $key\Closure $param)
  262.     {
  263.         $this->traces[$key] = [];
  264.         $this->keys[$key] = true;
  265.         $result $param();
  266.         unset($this->keys[$key]);
  267.         return $result;
  268.     }
  269.     /**
  270.      * @return array<mixed>
  271.      */
  272.     public function getTrace(string $key): array
  273.     {
  274.         $trace = isset($this->traces[$key]) ? array_keys($this->traces[$key]) : [];
  275.         unset($this->traces[$key]);
  276.         return $trace;
  277.     }
  278.     /**
  279.      * @throws InvalidKeyException
  280.      * @throws InvalidUuidException
  281.      */
  282.     private function validate(string $key, ?string $salesChannelId): void
  283.     {
  284.         $key trim($key);
  285.         if ($key === '') {
  286.             throw new InvalidKeyException('key may not be empty');
  287.         }
  288.         if ($salesChannelId && !Uuid::isValid($salesChannelId)) {
  289.             throw new InvalidUuidException($salesChannelId);
  290.         }
  291.     }
  292.     private function getId(string $key, ?string $salesChannelId null): ?string
  293.     {
  294.         $criteria = new Criteria();
  295.         $criteria->addFilter(
  296.             new EqualsFilter('configurationKey'$key),
  297.             new EqualsFilter('salesChannelId'$salesChannelId)
  298.         );
  299.         /** @var array<string> $ids */
  300.         $ids $this->systemConfigRepository->searchIds($criteriaContext::createDefaultContext())->getIds();
  301.         /** @var string|null $id */
  302.         $id array_shift($ids);
  303.         return $id;
  304.     }
  305. }