Passed
Push — master ( ff7745...ed75ff )
by Christian
77:35 queued 65:56
created

SystemConfigService::saveConfig()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 8
nc 5
nop 3
dl 0
loc 12
rs 9.2222
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\System\SystemConfig;
4
5
use Doctrine\DBAL\Connection;
6
use Doctrine\DBAL\FetchMode;
7
use Shopware\Core\Framework\Bundle;
8
use Shopware\Core\Framework\Context;
9
use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\RepositoryIterator;
10
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
11
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
12
use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
13
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
14
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
15
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
16
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
17
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
18
use Shopware\Core\Framework\Plugin\PluginEntity;
19
use Shopware\Core\Framework\Uuid\Exception\InvalidUuidException;
20
use Shopware\Core\Framework\Uuid\Uuid;
21
use Shopware\Core\System\SystemConfig\Exception\BundleConfigNotFoundException;
22
use Shopware\Core\System\SystemConfig\Exception\InvalidDomainException;
23
use Shopware\Core\System\SystemConfig\Exception\InvalidKeyException;
24
use Shopware\Core\System\SystemConfig\Exception\InvalidSettingValueException;
25
use Shopware\Core\System\SystemConfig\Util\ConfigReader;
26
use Symfony\Component\Config\Util\XmlUtils;
27
28
class SystemConfigService
29
{
30
    /**
31
     * @var Connection
32
     */
33
    private $connection;
34
35
    /**
36
     * @var EntityRepositoryInterface
37
     */
38
    private $systemConfigRepository;
39
40
    /**
41
     * @var array[]
42
     */
43
    private $configs = [];
44
45
    /**
46
     * @var ConfigReader
47
     */
48
    private $configReader;
49
50
    /**
51
     * @var EntityRepositoryInterface
52
     */
53
    private $pluginRepository;
54
55
    public function __construct(
56
        Connection $connection,
57
        EntityRepositoryInterface $systemConfigRepository,
58
        ConfigReader $configReader,
59
        EntityRepositoryInterface $pluginRepository
60
    ) {
61
        $this->connection = $connection;
62
        $this->systemConfigRepository = $systemConfigRepository;
63
        $this->configReader = $configReader;
64
        $this->pluginRepository = $pluginRepository;
65
    }
66
67
    /**
68
     * @return array|bool|float|int|string|null
69
     */
70
    public function get(string $key, ?string $salesChannelId = null)
71
    {
72
        $config = $this->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
     * gets all available shop configs and returns them as an array
132
     */
133
    public function all(?string $salesChannelId = null): array
134
    {
135
        return $this->load($salesChannelId);
136
    }
137
138
    /**
139
     * @throws InvalidDomainException
140
     * @throws InvalidUuidException
141
     * @throws InconsistentCriteriaIdsException
142
     */
143
    public function getDomain(string $domain, ?string $salesChannelId = null, bool $inherit = false): array
144
    {
145
        $domain = trim($domain);
146
        if ($domain === '') {
147
            throw new InvalidDomainException('Empty domain');
148
        }
149
150
        $queryBuilder = $this->connection->createQueryBuilder()
151
            ->select('LOWER(HEX(id))')
152
            ->from('system_config');
153
154
        if ($inherit) {
155
            $queryBuilder->where('sales_channel_id IS NULL OR sales_channel_id = :salesChannelId');
156
        } elseif ($salesChannelId === null) {
157
            $queryBuilder->where('sales_channel_id IS NULL');
158
        } else {
159
            $queryBuilder->where('sales_channel_id = :salesChannelId');
160
        }
161
162
        $domain = rtrim($domain, '.') . '.';
163
        $escapedDomain = str_replace('%', '\\%', $domain);
164
165
        $salesChannelId = $salesChannelId ? Uuid::fromHexToBytes($salesChannelId) : null;
166
167
        $queryBuilder->andWhere('configuration_key LIKE :prefix')
168
            ->orderBy('configuration_key', 'ASC')
169
            ->addOrderBy('sales_channel_id', 'ASC')
170
            ->setParameter('prefix', $escapedDomain . '%')
171
            ->setParameter('salesChannelId', $salesChannelId);
172
        $ids = $queryBuilder->execute()->fetchAll(FetchMode::COLUMN);
173
174
        if (empty($ids)) {
175
            return [];
176
        }
177
178
        $criteria = new Criteria($ids);
179
        /** @var SystemConfigCollection $collection */
180
        $collection = $this->systemConfigRepository
181
            ->search($criteria, Context::createDefaultContext())
182
            ->getEntities();
183
184
        $collection->sortByIdArray($ids);
185
        $merged = [];
186
187
        foreach ($collection as $cur) {
188
            $key = $cur->getConfigurationKey();
189
            $value = $cur->getConfigurationValue();
190
191
            $inheritedValuePresent = \array_key_exists($key, $merged);
192
            $valueConsideredEmpty = !\is_bool($value) && empty($value);
193
194
            if ($inheritedValuePresent && $valueConsideredEmpty) {
195
                continue;
196
            }
197
198
            $merged[$key] = $value;
199
        }
200
201
        return $merged;
202
    }
203
204
    /**
205
     * @param array|bool|float|int|string|null $value
206
     */
207
    public function set(string $key, $value, ?string $salesChannelId = null): void
208
    {
209
        // reset internal cache
210
        $this->configs = [];
211
212
        $key = trim($key);
213
        $this->validate($key, $salesChannelId);
214
215
        $id = $this->getId($key, $salesChannelId);
216
        if ($value === null) {
217
            if ($id) {
218
                $this->systemConfigRepository->delete([['id' => $id]], Context::createDefaultContext());
219
            }
220
221
            return;
222
        }
223
224
        $data = [
225
            'id' => $id ?? Uuid::randomHex(),
226
            'configurationKey' => $key,
227
            'configurationValue' => $value,
228
            'salesChannelId' => $salesChannelId,
229
        ];
230
        $this->systemConfigRepository->upsert([$data], Context::createDefaultContext());
231
    }
232
233
    public function delete(string $key, ?string $salesChannel = null): void
234
    {
235
        $this->set($key, null, $salesChannel);
236
    }
237
238
    /**
239
     * Fetches default values from bundle configuration and saves it to database
240
     */
241
    public function savePluginConfiguration(Bundle $bundle, bool $override = false): void
242
    {
243
        try {
244
            $config = $this->configReader->getConfigFromBundle($bundle);
245
        } catch (BundleConfigNotFoundException $e) {
246
            return;
247
        }
248
249
        $prefix = $bundle->getName() . '.config.';
250
251
        $this->saveConfig($config, $prefix, $override);
252
    }
253
254
    public function saveConfig(array $config, string $prefix, bool $override): void
255
    {
256
        foreach ($config as $card) {
257
            foreach ($card['elements'] as $element) {
258
                $key = $prefix . $element['name'];
259
                if (!isset($element['defaultValue'])) {
260
                    continue;
261
                }
262
263
                $value = XmlUtils::phpize($element['defaultValue']);
264
                if ($override || $this->get($key) === null) {
265
                    $this->set($key, $value);
266
                }
267
            }
268
        }
269
    }
270
271
    public function deletePluginConfiguration(Bundle $bundle): void
272
    {
273
        try {
274
            $config = $this->configReader->getConfigFromBundle($bundle);
275
        } catch (BundleConfigNotFoundException $e) {
276
            return;
277
        }
278
279
        $prefix = $bundle->getName() . '.config.';
280
281
        $configKeys = [];
282
        foreach ($config as $card) {
283
            foreach ($card['elements'] as $element) {
284
                $configKeys[] = $prefix . $element['name'];
285
            }
286
        }
287
288
        if (empty($configKeys)) {
289
            return;
290
        }
291
292
        $criteria = new Criteria();
293
        $criteria->addFilter(new EqualsAnyFilter('configurationKey', $configKeys));
294
        $systemConfigIds = $this->systemConfigRepository->searchIds($criteria, Context::createDefaultContext())->getIds();
295
        if (empty($systemConfigIds)) {
296
            return;
297
        }
298
299
        $ids = array_map(static function ($id) {
300
            return ['id' => $id];
301
        }, $systemConfigIds);
302
303
        $this->systemConfigRepository->delete($ids, Context::createDefaultContext());
304
    }
305
306
    private function load(?string $salesChannelId): array
307
    {
308
        $key = $salesChannelId ?? 'global';
309
310
        if (isset($this->configs[$key])) {
311
            return $this->configs[$key];
312
        }
313
314
        $criteria = new Criteria();
315
        $criteria->setTitle('system-config::load');
316
317
        if ($salesChannelId === null) {
318
            $criteria->addFilter(new EqualsFilter('salesChannelId', null));
319
        } else {
320
            $criteria->addFilter(
321
                new MultiFilter(
322
                    MultiFilter::CONNECTION_OR,
323
                    [
324
                        new EqualsFilter('salesChannelId', $salesChannelId),
325
                        new EqualsFilter('salesChannelId', null),
326
                    ]
327
                )
328
            );
329
        }
330
331
        $criteria->addSorting(
332
            new FieldSorting('salesChannelId', FieldSorting::ASCENDING),
333
            new FieldSorting('id', FieldSorting::ASCENDING)
334
        );
335
        $criteria->setLimit(500);
336
337
        $systemConfigs = new SystemConfigCollection();
338
        $iterator = new RepositoryIterator($this->systemConfigRepository, Context::createDefaultContext(), $criteria);
339
340
        while ($chunk = $iterator->fetch()) {
341
            $systemConfigs->merge($chunk->getEntities());
342
        }
343
344
        $this->configs[$key] = $this->buildSystemConfigArray($systemConfigs);
345
346
        return $this->configs[$key];
347
    }
348
349
    /**
350
     * The keys of the system configs look like `core.loginRegistration.showPhoneNumberField`.
351
     * This method splits those strings and builds an array structure
352
     *
353
     * ```
354
     * Array
355
     * (
356
     *     [core] => Array
357
     *         (
358
     *             [loginRegistration] => Array
359
     *                 (
360
     *                     [showPhoneNumberField] => 'someValue'
361
     *                 )
362
     *         )
363
     * )
364
     * ```
365
     */
366
    private function buildSystemConfigArray(SystemConfigCollection $systemConfigs): array
367
    {
368
        $configValues = [];
369
370
        foreach ($systemConfigs as $systemConfig) {
371
            $keys = explode('.', $systemConfig->getConfigurationKey());
372
373
            $configValues = $this->getSubArray($configValues, $keys, $systemConfig->getConfigurationValue());
374
        }
375
376
        return $this->filterNotActivatedPlugins($configValues);
377
    }
378
379
    private function filterNotActivatedPlugins(array $configValues): array
380
    {
381
        $notActivatedPlugins = $this->getNotActivatedPlugins();
382
        foreach (array_keys($configValues) as $key) {
383
            $notActivatedPlugin = $notActivatedPlugins->filter(function (PluginEntity $plugin) use ($key) {
384
                return $plugin->getName() === $key;
385
            })->first();
386
387
            if ($notActivatedPlugin) {
388
                unset($configValues[$key]);
389
            }
390
        }
391
392
        return $configValues;
393
    }
394
395
    private function getSubArray(array $configValues, array $keys, $value): array
396
    {
397
        $key = array_shift($keys);
398
399
        if (empty($keys)) {
400
            $configValues[$key] = $value;
401
        } else {
402
            if (!\array_key_exists($key, $configValues)) {
403
                $configValues[$key] = [];
404
            }
405
406
            $configValues[$key] = $this->getSubArray($configValues[$key], $keys, $value);
407
        }
408
409
        return $configValues;
410
    }
411
412
    /**
413
     * @throws InvalidKeyException
414
     * @throws InvalidUuidException
415
     */
416
    private function validate(string $key, ?string $salesChannelId): void
417
    {
418
        $key = trim($key);
419
        if ($key === '') {
420
            throw new InvalidKeyException('key may not be empty');
421
        }
422
        if ($salesChannelId && !Uuid::isValid($salesChannelId)) {
423
            throw new InvalidUuidException($salesChannelId);
424
        }
425
    }
426
427
    private function getId(string $key, ?string $salesChannelId = null): ?string
428
    {
429
        $criteria = new Criteria();
430
        $criteria->addFilter(
431
            new EqualsFilter('configurationKey', $key),
432
            new EqualsFilter('salesChannelId', $salesChannelId)
433
        );
434
435
        $ids = $this->systemConfigRepository->searchIds($criteria, Context::createDefaultContext())->getIds();
436
437
        return array_shift($ids);
438
    }
439
440
    private function getNotActivatedPlugins(): EntityCollection
441
    {
442
        $criteria = new Criteria();
443
        $criteria->addFilter(new EqualsFilter('active', false));
444
445
        return $this->pluginRepository->search($criteria, Context::createDefaultContext())->getEntities();
446
    }
447
}
448