Passed
Push — trunk ( d88a07...fa38cb )
by Christian
13:48 queued 13s
created

SystemConfigService::get()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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