Passed
Push — trunk ( 5f7c6d...3ce3ae )
by Christian
18:14 queued 04:54
created

getThemeConfigurationStructuredFields()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 24
nc 4
nop 3
dl 0
loc 39
rs 9.536
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Storefront\Theme;
4
5
use Doctrine\DBAL\Connection;
6
use Shopware\Core\Framework\Context;
7
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
8
use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
9
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
10
use Shopware\Core\Framework\Log\Package;
11
use Shopware\Core\Framework\Uuid\Uuid;
12
use Shopware\Storefront\Theme\ConfigLoader\AbstractConfigLoader;
13
use Shopware\Storefront\Theme\Event\ThemeAssignedEvent;
14
use Shopware\Storefront\Theme\Event\ThemeConfigChangedEvent;
15
use Shopware\Storefront\Theme\Event\ThemeConfigResetEvent;
16
use Shopware\Storefront\Theme\Exception\InvalidThemeConfigException;
17
use Shopware\Storefront\Theme\Exception\InvalidThemeException;
18
use Shopware\Storefront\Theme\StorefrontPluginConfiguration\StorefrontPluginConfigurationCollection;
19
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
20
21
#[Package('storefront')]
22
class ThemeService
23
{
24
    /**
25
     * @internal
26
     *
27
     * @param EntityRepository<ThemeCollection> $themeRepository
28
     */
29
    public function __construct(
30
        private readonly StorefrontPluginRegistryInterface $extensionRegistry,
31
        private readonly EntityRepository $themeRepository,
32
        private readonly EntityRepository $themeSalesChannelRepository,
33
        private readonly ThemeCompilerInterface $themeCompiler,
34
        private readonly EventDispatcherInterface $dispatcher,
35
        private readonly AbstractConfigLoader $configLoader,
36
        private readonly Connection $connection
37
    ) {
38
    }
39
40
    /**
41
     * Only compiles a single theme/saleschannel combination.
42
     * Use `compileThemeById` to compile all dependend saleschannels
43
     */
44
    public function compileTheme(
45
        string $salesChannelId,
46
        string $themeId,
47
        Context $context,
48
        ?StorefrontPluginConfigurationCollection $configurationCollection = null,
49
        bool $withAssets = true
50
    ): void {
51
        $this->themeCompiler->compileTheme(
52
            $salesChannelId,
53
            $themeId,
54
            $this->configLoader->load($themeId, $context),
55
            $configurationCollection ?? $this->extensionRegistry->getConfigurations(),
56
            $withAssets,
57
            $context
58
        );
59
    }
60
61
    /**
62
     * Compiles all dependend saleschannel/Theme combinations
63
     *
64
     * @return array<int, string>
65
     */
66
    public function compileThemeById(
67
        string $themeId,
68
        Context $context,
69
        ?StorefrontPluginConfigurationCollection $configurationCollection = null,
70
        bool $withAssets = true
71
    ): array {
72
        $mappings = $this->getThemeDependencyMapping($themeId);
73
74
        $compiledThemeIds = [];
75
        foreach ($mappings as $mapping) {
76
            $this->themeCompiler->compileTheme(
77
                $mapping->getSalesChannelId(),
78
                $mapping->getThemeId(),
79
                $this->configLoader->load($mapping->getThemeId(), $context),
80
                $configurationCollection ?? $this->extensionRegistry->getConfigurations(),
81
                $withAssets,
82
                $context
83
            );
84
85
            $compiledThemeIds[] = $mapping->getThemeId();
86
        }
87
88
        return $compiledThemeIds;
89
    }
90
91
    /**
92
     * @param array<string, mixed>|null $config
93
     */
94
    public function updateTheme(string $themeId, ?array $config, ?string $parentThemeId, Context $context): void
95
    {
96
        $criteria = new Criteria([$themeId]);
97
        $criteria->addAssociation('salesChannels');
98
        $theme = $this->themeRepository->search($criteria, $context)->getEntities()->get($themeId);
99
100
        if ($theme === null) {
101
            throw new InvalidThemeException($themeId);
102
        }
103
104
        $data = ['id' => $themeId];
105
        if ($config) {
106
            foreach ($config as $key => $value) {
107
                $data['configValues'][$key] = $value;
108
            }
109
        }
110
111
        if ($parentThemeId) {
112
            $data['parentThemeId'] = $parentThemeId;
113
        }
114
115
        if (\array_key_exists('configValues', $data)) {
116
            $this->dispatcher->dispatch(new ThemeConfigChangedEvent($themeId, $data['configValues']));
117
        }
118
119
        if (\array_key_exists('configValues', $data) && $theme->getConfigValues()) {
120
            $submittedChanges = $data['configValues'];
121
            $currentConfig = $theme->getConfigValues();
122
            $data['configValues'] = array_replace_recursive($currentConfig, $data['configValues']);
123
124
            foreach ($submittedChanges as $key => $changes) {
125
                if (isset($changes['value']) && \is_array($changes['value']) && isset($currentConfig[(string) $key]) && \is_array($currentConfig[(string) $key])) {
126
                    $data['configValues'][$key]['value'] = array_unique($changes['value']);
127
                }
128
            }
129
        }
130
131
        $this->themeRepository->update([$data], $context);
132
133
        if ($theme->getSalesChannels() === null) {
134
            return;
135
        }
136
137
        $this->compileThemeById($themeId, $context, null, false);
138
    }
139
140
    public function assignTheme(string $themeId, string $salesChannelId, Context $context, bool $skipCompile = false): bool
141
    {
142
        if (!$skipCompile) {
143
            $this->compileTheme($salesChannelId, $themeId, $context);
144
        }
145
146
        $this->themeSalesChannelRepository->upsert([[
147
            'themeId' => $themeId,
148
            'salesChannelId' => $salesChannelId,
149
        ]], $context);
150
151
        $this->dispatcher->dispatch(new ThemeAssignedEvent($themeId, $salesChannelId));
152
153
        return true;
154
    }
155
156
    public function resetTheme(string $themeId, Context $context): void
157
    {
158
        $criteria = new Criteria([$themeId]);
159
        $theme = $this->themeRepository->search($criteria, $context)->get($themeId);
160
161
        if (!$theme) {
162
            throw new InvalidThemeException($themeId);
163
        }
164
165
        $data = ['id' => $themeId];
166
        $data['configValues'] = null;
167
168
        $this->dispatcher->dispatch(new ThemeConfigResetEvent($themeId));
169
170
        $this->themeRepository->update([$data], $context);
171
    }
172
173
    /**
174
     * @throws InvalidThemeConfigException
175
     * @throws InvalidThemeException
176
     * @throws InconsistentCriteriaIdsException
177
     *
178
     * @return array<string, mixed>
179
     */
180
    public function getThemeConfiguration(string $themeId, bool $translate, Context $context): array
181
    {
182
        $criteria = new Criteria();
183
        $criteria->setTitle('theme-service::load-config');
184
185
        $themes = $this->themeRepository->search($criteria, $context)->getEntities();
186
187
        $theme = $themes->get($themeId);
188
189
        if ($theme === null) {
190
            throw new InvalidThemeException($themeId);
191
        }
192
193
        $baseTheme = $themes->filter(fn (ThemeEntity $themeEntry) => $themeEntry->getTechnicalName() === StorefrontPluginRegistry::BASE_THEME_NAME)->first();
194
        if ($baseTheme === null) {
195
            throw new InvalidThemeException(StorefrontPluginRegistry::BASE_THEME_NAME);
196
        }
197
198
        $baseThemeConfig = $this->mergeStaticConfig($baseTheme);
199
200
        $themeConfigFieldFactory = new ThemeConfigFieldFactory();
201
        $configFields = [];
202
        $labels = array_replace_recursive($baseTheme->getLabels() ?? [], $theme->getLabels() ?? []);
203
        $helpTexts = array_replace_recursive($baseTheme->getHelpTexts() ?? [], $theme->getHelpTexts() ?? []);
204
205
        if ($theme->getParentThemeId()) {
206
            foreach ($this->getParentThemes($themes, $theme) as $parentTheme) {
207
                $configuredParentTheme = $this->mergeStaticConfig($parentTheme);
208
                $baseThemeConfig = array_replace_recursive($baseThemeConfig, $configuredParentTheme);
209
                $labels = array_replace_recursive($labels, $parentTheme->getLabels() ?? []);
210
                $helpTexts = array_replace_recursive($helpTexts, $parentTheme->getHelpTexts() ?? []);
211
            }
212
        }
213
214
        $configuredTheme = $this->mergeStaticConfig($theme);
215
        $themeConfig = array_replace_recursive($baseThemeConfig, $configuredTheme);
216
217
        foreach ($themeConfig['fields'] ?? [] as $name => $item) {
218
            $configFields[$name] = $themeConfigFieldFactory->create($name, $item);
219
            if (
220
                isset($item['value'], $configuredTheme['fields'])
221
                && \is_array($item['value'])
222
                && \array_key_exists($name, $configuredTheme['fields'])
223
            ) {
224
                $configFields[$name]->setValue($configuredTheme['fields'][$name]['value']);
225
            }
226
        }
227
228
        $configFields = json_decode((string) json_encode($configFields, \JSON_THROW_ON_ERROR), true, 512, \JSON_THROW_ON_ERROR);
229
230
        if ($translate && !empty($labels)) {
231
            $configFields = $this->translateLabels($configFields, $labels);
232
        }
233
234
        if ($translate && !empty($helpTexts)) {
235
            $configFields = $this->translateHelpTexts($configFields, $helpTexts);
236
        }
237
238
        $themeConfig['fields'] = $configFields;
239
        $themeConfig['currentFields'] = [];
240
        $themeConfig['baseThemeFields'] = [];
241
242
        foreach ($themeConfig['fields'] as $field => $fieldItem) {
243
            $isInherited = $this->fieldIsInherited($field, $configuredTheme);
244
            $themeConfig['currentFields'][$field]['isInherited'] = $isInherited;
245
246
            if ($isInherited) {
247
                $themeConfig['currentFields'][$field]['value'] = null;
248
            } elseif (\array_key_exists('value', $fieldItem)) {
249
                $themeConfig['currentFields'][$field]['value'] = $fieldItem['value'];
250
            }
251
252
            $isInherited = $this->fieldIsInherited($field, $baseThemeConfig);
253
            $themeConfig['baseThemeFields'][$field]['isInherited'] = $isInherited;
254
255
            if ($isInherited) {
256
                $themeConfig['baseThemeFields'][$field]['value'] = null;
257
            } elseif (\array_key_exists('value', $fieldItem) && isset($baseThemeConfig['fields'][$field]['value'])) {
258
                $themeConfig['baseThemeFields'][$field]['value'] = $baseThemeConfig['fields'][$field]['value'];
259
            }
260
        }
261
262
        return $themeConfig;
263
    }
264
265
    /**
266
     * @return array<string, mixed>
267
     */
268
    public function getThemeConfigurationStructuredFields(string $themeId, bool $translate, Context $context): array
269
    {
270
        $mergedConfig = $this->getThemeConfiguration($themeId, $translate, $context)['fields'];
271
272
        $translations = [];
273
        if ($translate) {
274
            $translations = $this->getTranslations($themeId, $context);
275
            $mergedConfig = $this->translateLabels($mergedConfig, $translations);
276
        }
277
278
        $outputStructure = [];
279
280
        foreach ($mergedConfig as $fieldName => $fieldConfig) {
281
            $tab = $this->getTab($fieldConfig);
282
            $tabLabel = $this->getTabLabel($tab, $translations);
283
            $block = $this->getBlock($fieldConfig);
284
            $blockLabel = $this->getBlockLabel($block, $translations);
285
            $section = $this->getSection($fieldConfig);
286
            $sectionLabel = $this->getSectionLabel($section, $translations);
287
288
            // set default tab
289
            $outputStructure['tabs']['default']['label'] = '';
290
291
            // set labels
292
            $outputStructure['tabs'][$tab]['label'] = $tabLabel;
293
            $outputStructure['tabs'][$tab]['blocks'][$block]['label'] = $blockLabel;
294
            $outputStructure['tabs'][$tab]['blocks'][$block]['sections'][$section]['label'] = $sectionLabel;
295
296
            // add fields to sections
297
            $outputStructure['tabs'][$tab]['blocks'][$block]['sections'][$section]['fields'][$fieldName] = [
298
                'label' => $fieldConfig['label'],
299
                'helpText' => $fieldConfig['helpText'] ?? null,
300
                'type' => $fieldConfig['type'],
301
                'custom' => $fieldConfig['custom'],
302
                'fullWidth' => $fieldConfig['fullWidth'],
303
            ];
304
        }
305
306
        return $outputStructure;
307
    }
308
309
    public function getThemeDependencyMapping(string $themeId): ThemeSalesChannelCollection
310
    {
311
        $mappings = new ThemeSalesChannelCollection();
312
        $themeData = $this->connection->fetchAllAssociative(
313
            'SELECT LOWER(HEX(theme.id)) as id, LOWER(HEX(childTheme.id)) as dependentId,
314
            LOWER(HEX(tsc.sales_channel_id)) as saleschannelId,
315
            LOWER(HEX(dtsc.sales_channel_id)) as dsaleschannelId
316
            FROM theme
317
            LEFT JOIN theme as childTheme ON childTheme.parent_theme_id = theme.id
318
            LEFT JOIN theme_sales_channel as tsc ON theme.id = tsc.theme_id
319
            LEFT JOIN theme_sales_channel as dtsc ON childTheme.id = dtsc.theme_id
320
            WHERE theme.id = :id',
321
            ['id' => Uuid::fromHexToBytes($themeId)]
322
        );
323
324
        foreach ($themeData as $data) {
325
            if (isset($data['id']) && isset($data['saleschannelId']) && $data['id'] === $themeId) {
326
                $mappings->add(new ThemeSalesChannel($data['id'], $data['saleschannelId']));
327
            }
328
            if (isset($data['dependentId']) && isset($data['dsaleschannelId'])) {
329
                $mappings->add(new ThemeSalesChannel($data['dependentId'], $data['dsaleschannelId']));
330
            }
331
        }
332
333
        return $mappings;
334
    }
335
336
    /**
337
     * @param array<string, ThemeEntity> $parentThemes
338
     *
339
     * @return array<string, ThemeEntity>
340
     */
341
    private function getParentThemes(ThemeCollection $themes, ThemeEntity $mainTheme, array $parentThemes = []): array
342
    {
343
        foreach ($this->getConfigInheritance($mainTheme) as $parentThemeName) {
344
            $parentTheme = $themes->filter(fn (ThemeEntity $themeEntry) => $themeEntry->getTechnicalName() === str_replace('@', '', (string) $parentThemeName))->first();
345
346
            if ($parentTheme instanceof ThemeEntity && !\array_key_exists($parentTheme->getId(), $parentThemes)) {
347
                $parentThemes[$parentTheme->getId()] = $parentTheme;
348
349
                if ($parentTheme->getParentThemeId()) {
350
                    $parentThemes = $this->getParentThemes($themes, $mainTheme, $parentThemes);
351
                }
352
            }
353
        }
354
355
        if ($mainTheme->getParentThemeId()) {
356
            $parentTheme = $themes->filter(fn (ThemeEntity $themeEntry) => $themeEntry->getId() === $mainTheme->getParentThemeId())->first();
357
358
            if ($parentTheme instanceof ThemeEntity && !\array_key_exists($parentTheme->getId(), $parentThemes)) {
359
                $parentThemes[$parentTheme->getId()] = $parentTheme;
360
                if ($parentTheme->getParentThemeId()) {
361
                    $parentThemes = $this->getParentThemes($themes, $mainTheme, $parentThemes);
362
                }
363
            }
364
        }
365
366
        return $parentThemes;
367
    }
368
369
    /**
370
     * @return array<string, mixed>
371
     */
372
    private function getConfigInheritance(ThemeEntity $mainTheme): array
373
    {
374
        if (\is_array($mainTheme->getBaseConfig())
375
            && \array_key_exists('configInheritance', $mainTheme->getBaseConfig())
376
            && \is_array($mainTheme->getBaseConfig()['configInheritance'])
377
            && !empty($mainTheme->getBaseConfig()['configInheritance'])
378
        ) {
379
            return $mainTheme->getBaseConfig()['configInheritance'];
380
        }
381
382
        return [];
383
    }
384
385
    /**
386
     * @return array<string, mixed>
387
     */
388
    private function mergeStaticConfig(ThemeEntity $theme): array
389
    {
390
        $configuredTheme = [];
391
392
        $pluginConfig = null;
393
        if ($theme->getTechnicalName()) {
394
            $pluginConfig = $this->extensionRegistry->getConfigurations()->getByTechnicalName($theme->getTechnicalName());
395
        }
396
397
        if ($pluginConfig !== null) {
398
            $configuredTheme = $pluginConfig->getThemeConfig();
399
        }
400
401
        if ($theme->getBaseConfig() !== null) {
402
            $configuredTheme = array_replace_recursive($configuredTheme ?? [], $theme->getBaseConfig());
403
        }
404
405
        if ($theme->getConfigValues() !== null) {
406
            foreach ($theme->getConfigValues() as $fieldName => $configValue) {
407
                if (\array_key_exists('value', $configValue)) {
408
                    $configuredTheme['fields'][$fieldName]['value'] = $configValue['value'];
409
                }
410
            }
411
        }
412
413
        return $configuredTheme ?: [];
414
    }
415
416
    /**
417
     * @param array<string, mixed> $fieldConfig
418
     */
419
    private function getTab(array $fieldConfig): string
420
    {
421
        $tab = 'default';
422
423
        if (isset($fieldConfig['tab'])) {
424
            $tab = $fieldConfig['tab'];
425
        }
426
427
        return $tab;
428
    }
429
430
    /**
431
     * @param array<string, mixed> $fieldConfig
432
     */
433
    private function getBlock(array $fieldConfig): string
434
    {
435
        $block = 'default';
436
437
        if (isset($fieldConfig['block'])) {
438
            $block = $fieldConfig['block'];
439
        }
440
441
        return $block;
442
    }
443
444
    /**
445
     * @param array<string, mixed> $fieldConfig
446
     */
447
    private function getSection(array $fieldConfig): string
448
    {
449
        $section = 'default';
450
451
        if (isset($fieldConfig['section'])) {
452
            $section = $fieldConfig['section'];
453
        }
454
455
        return $section;
456
    }
457
458
    /**
459
     * @param array<string, mixed> $translations
460
     */
461
    private function getTabLabel(string $tabName, array $translations): string
462
    {
463
        if ($tabName === 'default') {
464
            return '';
465
        }
466
467
        return $translations['tabs.' . $tabName] ?? $tabName;
468
    }
469
470
    /**
471
     * @param array<string, mixed> $translations
472
     */
473
    private function getBlockLabel(string $blockName, array $translations): string
474
    {
475
        if ($blockName === 'default') {
476
            return '';
477
        }
478
479
        return $translations['blocks.' . $blockName] ?? $blockName;
480
    }
481
482
    /**
483
     * @param array<string, mixed> $translations
484
     */
485
    private function getSectionLabel(string $sectionName, array $translations): string
486
    {
487
        if ($sectionName === 'default') {
488
            return '';
489
        }
490
491
        return $translations['sections.' . $sectionName] ?? $sectionName;
492
    }
493
494
    /**
495
     * @param array<string, mixed> $themeConfiguration
496
     * @param array<string, mixed> $translations
497
     *
498
     * @return array<string, mixed>
499
     */
500
    private function translateLabels(array $themeConfiguration, array $translations): array
501
    {
502
        foreach ($themeConfiguration as $key => &$value) {
503
            $value['label'] = $translations['fields.' . $key] ?? $key;
504
        }
505
506
        return $themeConfiguration;
507
    }
508
509
    /**
510
     * @param array<string, mixed> $themeConfiguration
511
     * @param array<string, mixed> $translations
512
     *
513
     * @return array<string, mixed>
514
     */
515
    private function translateHelpTexts(array $themeConfiguration, array $translations): array
516
    {
517
        foreach ($themeConfiguration as $key => &$value) {
518
            $value['helpText'] = $translations['fields.' . $key] ?? null;
519
        }
520
521
        return $themeConfiguration;
522
    }
523
524
    /**
525
     * @return array<string, mixed>
526
     */
527
    private function getTranslations(string $themeId, Context $context): array
528
    {
529
        $theme = $this->themeRepository->search(new Criteria([$themeId]), $context)->getEntities()->get($themeId);
530
        if ($theme === null) {
531
            throw new InvalidThemeException($themeId);
532
        }
533
534
        $translations = $theme->getLabels() ?: [];
535
536
        if ($theme->getParentThemeId()) {
537
            $criteria = new Criteria();
538
            $criteria->setTitle('theme-service::load-translations');
539
540
            $themes = $this->themeRepository->search($criteria, $context)->getEntities();
541
            foreach ($this->getParentThemes($themes, $theme) as $parentTheme) {
542
                $parentTranslations = $parentTheme->getLabels() ?: [];
543
                $translations = array_replace_recursive($parentTranslations, $translations);
544
            }
545
        }
546
547
        return $translations;
548
    }
549
550
    /**
551
     * @param array<string, mixed> $configuration
552
     */
553
    private function fieldIsInherited(string $fieldName, array $configuration): bool
554
    {
555
        if (!isset($configuration['fields'])) {
556
            return true;
557
        }
558
559
        if (!\is_array($configuration['fields'])) {
560
            return true;
561
        }
562
563
        if (!\array_key_exists($fieldName, $configuration['fields'])) {
564
            return true;
565
        }
566
567
        return false;
568
    }
569
}
570