ThemeService::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 10
dl 0
loc 12
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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