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

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