Passed
Push — trunk ( c56b80...32ae88 )
by Christian
11:56 queued 12s
created

SystemConfigService::all()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
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;
0 ignored issues
show
Bug introduced by
The type Shopware\Core\Framework\...e\MultiInsertQueryQueue was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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