Passed
Push — master ( 3cfbcc...dd6e64 )
by Christian
16:35 queued 06:50
created

SystemConfigService::savePluginConfiguration()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 11
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\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\EntityRepositoryInterface;
11
use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
12
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
13
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
14
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
15
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
16
use Shopware\Core\Framework\Uuid\Exception\InvalidUuidException;
17
use Shopware\Core\Framework\Uuid\Uuid;
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\Component\Config\Util\XmlUtils;
24
25
class SystemConfigService
26
{
27
    /**
28
     * @var Connection
29
     */
30
    private $connection;
31
32
    /**
33
     * @var EntityRepositoryInterface
34
     */
35
    private $systemConfigRepository;
36
37
    /**
38
     * @var array[]
39
     */
40
    private $configs = [];
41
42
    /**
43
     * @var ConfigReader
44
     */
45
    private $configReader;
46
47
    public function __construct(
48
        Connection $connection,
49
        EntityRepositoryInterface $systemConfigRepository,
50
        ConfigReader $configReader
51
    ) {
52
        $this->connection = $connection;
53
        $this->systemConfigRepository = $systemConfigRepository;
54
        $this->configReader = $configReader;
55
    }
56
57
    /**
58
     * @return array|bool|float|int|string|null
59
     */
60
    public function get(string $key, ?string $salesChannelId = null)
61
    {
62
        $config = $this->load($salesChannelId);
63
64
        $parts = explode('.', $key);
65
66
        $pointer = $config;
67
68
        foreach ($parts as $part) {
69
            if (!\is_array($pointer)) {
70
                return null;
71
            }
72
73
            if (\array_key_exists($part, $pointer)) {
74
                $pointer = $pointer[$part];
75
76
                continue;
77
            }
78
79
            return null;
80
        }
81
82
        return $pointer;
83
    }
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
92
        throw new InvalidSettingValueException($key, 'string', \gettype($value));
93
    }
94
95
    public function getInt(string $key, ?string $salesChannelId = null): int
96
    {
97
        $value = $this->get($key, $salesChannelId);
98
        if (!\is_array($value)) {
99
            return (int) $value;
100
        }
101
102
        throw new InvalidSettingValueException($key, 'int', \gettype($value));
103
    }
104
105
    public function getFloat(string $key, ?string $salesChannelId = null): float
106
    {
107
        $value = $this->get($key, $salesChannelId);
108
        if (!\is_array($value)) {
109
            return (float) $value;
110
        }
111
112
        throw new InvalidSettingValueException($key, 'float', \gettype($value));
113
    }
114
115
    public function getBool(string $key, ?string $salesChannelId = null): bool
116
    {
117
        return (bool) $this->get($key, $salesChannelId);
118
    }
119
120
    /**
121
     * gets all available shop configs and returns them as an array
122
     */
123
    public function all(?string $salesChannelId = null): array
124
    {
125
        return $this->load($salesChannelId);
126
    }
127
128
    /**
129
     * @throws InvalidDomainException
130
     * @throws InvalidUuidException
131
     * @throws InconsistentCriteriaIdsException
132
     */
133
    public function getDomain(string $domain, ?string $salesChannelId = null, bool $inherit = false): array
134
    {
135
        $domain = trim($domain);
136
        if ($domain === '') {
137
            throw new InvalidDomainException('Empty domain');
138
        }
139
140
        $queryBuilder = $this->connection->createQueryBuilder()
141
            ->select('LOWER(HEX(id))')
142
            ->from('system_config');
143
144
        if ($inherit) {
145
            $queryBuilder->where('sales_channel_id IS NULL OR sales_channel_id = :salesChannelId');
146
        } elseif ($salesChannelId === null) {
147
            $queryBuilder->where('sales_channel_id IS NULL');
148
        } else {
149
            $queryBuilder->where('sales_channel_id = :salesChannelId');
150
        }
151
152
        $domain = rtrim($domain, '.') . '.';
153
        $escapedDomain = str_replace('%', '\\%', $domain);
154
155
        $salesChannelId = $salesChannelId ? Uuid::fromHexToBytes($salesChannelId) : null;
156
157
        $queryBuilder->andWhere('configuration_key LIKE :prefix')
158
            ->orderBy('configuration_key', 'ASC')
159
            ->addOrderBy('sales_channel_id', 'ASC')
160
            ->setParameter('prefix', $escapedDomain . '%')
161
            ->setParameter('salesChannelId', $salesChannelId);
162
        $ids = $queryBuilder->execute()->fetchAll(FetchMode::COLUMN);
163
164
        if (empty($ids)) {
165
            return [];
166
        }
167
168
        $criteria = new Criteria($ids);
169
        /** @var SystemConfigCollection $collection */
170
        $collection = $this->systemConfigRepository
171
            ->search($criteria, Context::createDefaultContext())
172
            ->getEntities();
173
174
        $collection->sortByIdArray($ids);
175
        $merged = [];
176
177
        foreach ($collection as $cur) {
178
            $key = $cur->getConfigurationKey();
179
            $value = $cur->getConfigurationValue();
180
181
            $inheritedValuePresent = array_key_exists($key, $merged);
182
            $valueConsideredEmpty = !is_bool($value) && empty($value);
183
184
            if ($inheritedValuePresent && $valueConsideredEmpty) {
185
                continue;
186
            }
187
188
            $merged[$key] = $value;
189
        }
190
191
        return $merged;
192
    }
193
194
    /**
195
     * @param array|bool|float|int|string|null $value
196
     */
197
    public function set(string $key, $value, ?string $salesChannelId = null): void
198
    {
199
        // reset internal cache
200
        $this->configs = [];
201
202
        $key = trim($key);
203
        $this->validate($key, $salesChannelId);
204
205
        $id = $this->getId($key, $salesChannelId);
206
        if ($value === null) {
207
            if ($id) {
208
                $this->systemConfigRepository->delete([['id' => $id]], Context::createDefaultContext());
209
            }
210
211
            return;
212
        }
213
214
        $data = [
215
            'id' => $id ?? Uuid::randomHex(),
216
            'configurationKey' => $key,
217
            'configurationValue' => $value,
218
            'salesChannelId' => $salesChannelId,
219
        ];
220
        $this->systemConfigRepository->upsert([$data], Context::createDefaultContext());
221
    }
222
223
    public function delete(string $key, ?string $salesChannel = null): void
224
    {
225
        $this->set($key, null, $salesChannel);
226
    }
227
228
    /**
229
     * Fetches default values from bundle configuration and saves it to database
230
     */
231
    public function savePluginConfiguration(Bundle $bundle, bool $override = false): void
232
    {
233
        try {
234
            $config = $this->configReader->getConfigFromBundle($bundle);
235
        } catch (BundleConfigNotFoundException $e) {
236
            return;
237
        }
238
239
        $prefix = $bundle->getName() . '.config.';
240
241
        $this->saveConfig($config, $prefix, $override);
242
    }
243
244
    public function saveConfig(array $config, string $prefix, bool $override): void
245
    {
246
        foreach ($config as $card) {
247
            foreach ($card['elements'] as $element) {
248
                $key = $prefix . $element['name'];
249
                if (!isset($element['defaultValue'])) {
250
                    continue;
251
                }
252
253
                $value = XmlUtils::phpize($element['defaultValue']);
254
                if ($override || $this->get($key) === null) {
255
                    $this->set($key, $value);
256
                }
257
            }
258
        }
259
    }
260
261
    private function load(?string $salesChannelId): array
262
    {
263
        $key = $salesChannelId ?? 'global';
264
265
        if (isset($this->configs[$key])) {
266
            return $this->configs[$key];
267
        }
268
269
        $criteria = new Criteria();
270
        $criteria->setTitle('system-config::load');
271
272
        if ($salesChannelId === null) {
273
            $criteria->addFilter(new EqualsFilter('salesChannelId', null));
274
        } else {
275
            $criteria->addFilter(
276
                new MultiFilter(
277
                    MultiFilter::CONNECTION_OR,
278
                    [
279
                        new EqualsFilter('salesChannelId', $salesChannelId),
280
                        new EqualsFilter('salesChannelId', null),
281
                    ]
282
                )
283
            );
284
        }
285
286
        $criteria->addSorting(
287
            new FieldSorting('salesChannelId', FieldSorting::ASCENDING),
288
            new FieldSorting('id', FieldSorting::ASCENDING)
289
        );
290
        $criteria->setLimit(500);
291
292
        $systemConfigs = new SystemConfigCollection();
293
        $iterator = new RepositoryIterator($this->systemConfigRepository, Context::createDefaultContext(), $criteria);
294
295
        while ($chunk = $iterator->fetch()) {
296
            $systemConfigs->merge($chunk->getEntities());
297
        }
298
299
        $this->configs[$key] = $this->buildSystemConfigArray($systemConfigs);
300
301
        return $this->configs[$key];
302
    }
303
304
    /**
305
     * The keys of the system configs look like `core.loginRegistration.showPhoneNumberField`.
306
     * This method splits those strings and builds an array structure
307
     *
308
     * ```
309
     * Array
310
     * (
311
     *     [core] => Array
312
     *         (
313
     *             [loginRegistration] => Array
314
     *                 (
315
     *                     [showPhoneNumberField] => 'someValue'
316
     *                 )
317
     *         )
318
     * )
319
     * ```
320
     */
321
    private function buildSystemConfigArray(SystemConfigCollection $systemConfigs): array
322
    {
323
        $configValues = [];
324
325
        foreach ($systemConfigs as $systemConfig) {
326
            $keys = explode('.', $systemConfig->getConfigurationKey());
327
328
            $configValues = $this->getSubArray($configValues, $keys, $systemConfig->getConfigurationValue());
329
        }
330
331
        return $configValues;
332
    }
333
334
    private function getSubArray(array $configValues, array $keys, $value): array
335
    {
336
        $key = array_shift($keys);
337
338
        if (empty($keys)) {
339
            $configValues[$key] = $value;
340
        } else {
341
            if (!\array_key_exists($key, $configValues)) {
342
                $configValues[$key] = [];
343
            }
344
345
            $configValues[$key] = $this->getSubArray($configValues[$key], $keys, $value);
346
        }
347
348
        return $configValues;
349
    }
350
351
    /**
352
     * @throws InvalidKeyException
353
     * @throws InvalidUuidException
354
     */
355
    private function validate(string $key, ?string $salesChannelId): void
356
    {
357
        $key = trim($key);
358
        if ($key === '') {
359
            throw new InvalidKeyException('key may not be empty');
360
        }
361
        if ($salesChannelId && !Uuid::isValid($salesChannelId)) {
362
            throw new InvalidUuidException($salesChannelId);
363
        }
364
    }
365
366
    private function getId(string $key, ?string $salesChannelId = null): ?string
367
    {
368
        $criteria = new Criteria();
369
        $criteria->addFilter(
370
            new EqualsFilter('configurationKey', $key),
371
            new EqualsFilter('salesChannelId', $salesChannelId)
372
        );
373
374
        $ids = $this->systemConfigRepository->searchIds($criteria, Context::createDefaultContext())->getIds();
375
376
        return array_shift($ids);
377
    }
378
}
379