ThemeService::getThemeConfiguration()   F
last analyzed

Complexity

Conditions 19
Paths 242

Size

Total Lines 83
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 51
nc 242
nop 3
dl 0
loc 83
rs 3.1583
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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