Passed
Push — trunk ( 05c644...7dd39b )
by Christian
10:24 queued 14s
created

ThemeLifecycleService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 11
dl 0
loc 13
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 GuzzleHttp\Psr7\MimeType;
7
use Shopware\Core\Content\Media\Exception\DuplicatedMediaFileNameException;
8
use Shopware\Core\Content\Media\File\FileNameProvider;
9
use Shopware\Core\Content\Media\File\FileSaver;
10
use Shopware\Core\Content\Media\File\MediaFile;
11
use Shopware\Core\Defaults;
12
use Shopware\Core\Framework\Context;
13
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
14
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
15
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
16
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\RestrictDeleteViolationException;
17
use Shopware\Core\Framework\Log\Package;
18
use Shopware\Core\Framework\Uuid\Uuid;
19
use Shopware\Core\System\Language\LanguageEntity;
0 ignored issues
show
Bug introduced by
The type Shopware\Core\System\Language\LanguageEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use Shopware\Core\System\Locale\LocaleEntity;
0 ignored issues
show
Bug introduced by
The type Shopware\Core\System\Locale\LocaleEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use Shopware\Storefront\Theme\StorefrontPluginConfiguration\StorefrontPluginConfiguration;
22
use Shopware\Storefront\Theme\StorefrontPluginConfiguration\StorefrontPluginConfigurationCollection;
23
24
#[Package('storefront')]
25
class ThemeLifecycleService
26
{
27
    /**
28
     * @internal
29
     */
30
    public function __construct(
31
        private readonly StorefrontPluginRegistryInterface $pluginRegistry,
32
        private readonly EntityRepository $themeRepository,
33
        private readonly EntityRepository $mediaRepository,
34
        private readonly EntityRepository $mediaFolderRepository,
35
        private readonly EntityRepository $themeMediaRepository,
36
        private readonly FileSaver $fileSaver,
37
        private readonly FileNameProvider $fileNameProvider,
38
        private readonly ThemeFileImporterInterface $themeFileImporter,
39
        private readonly EntityRepository $languageRepository,
40
        private readonly EntityRepository $themeChildRepository,
41
        private readonly Connection $connection
42
    ) {
43
    }
44
45
    public function refreshThemes(
46
        Context $context,
47
        ?StorefrontPluginConfigurationCollection $configurationCollection = null
48
    ): void {
49
        if ($configurationCollection === null) {
50
            $configurationCollection = $this->pluginRegistry->getConfigurations()->getThemes();
51
        }
52
53
        // iterate over all theme configs in the filesystem (plugins/bundles)
54
        foreach ($configurationCollection as $config) {
55
            $this->refreshTheme($config, $context);
0 ignored issues
show
Bug introduced by
$config of type array is incompatible with the type Shopware\Storefront\Them...rontPluginConfiguration expected by parameter $configuration of Shopware\Storefront\Them...Service::refreshTheme(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

55
            $this->refreshTheme(/** @scrutinizer ignore-type */ $config, $context);
Loading history...
56
        }
57
    }
58
59
    public function refreshTheme(StorefrontPluginConfiguration $configuration, Context $context): void
60
    {
61
        $themeData = [];
62
        $themeData['name'] = $configuration->getName();
63
        $themeData['technicalName'] = $configuration->getTechnicalName();
64
        $themeData['author'] = $configuration->getAuthor();
65
66
        // refresh theme after deleting media
67
        $theme = $this->getThemeByTechnicalName($configuration->getTechnicalName(), $context);
68
69
        // check if theme config already exists in the database
70
        if ($theme) {
71
            $themeData['id'] = $theme->getId();
72
        } else {
73
            $themeData['active'] = true;
74
        }
75
76
        $themeData['translations'] = $this->getTranslationsConfiguration($configuration, $context);
77
78
        $updatedData = $this->updateMediaInConfiguration($theme, $configuration, $context);
79
80
        $themeData = array_merge($themeData, $updatedData);
81
82
        if (!empty($configuration->getConfigInheritance())) {
83
            $themeData = $this->addParentTheme($configuration, $themeData, $context);
84
        }
85
86
        $writtenEvent = $this->themeRepository->upsert([$themeData], $context);
87
88
        if (!isset($themeData['id']) || empty($themeData['id'])) {
89
            $themeData['id'] = current($writtenEvent->getPrimaryKeys(ThemeDefinition::ENTITY_NAME));
90
        }
91
92
        $this->themeRepository->upsert([$themeData], $context);
93
94
        $parentThemes = $this->getParentThemes($configuration, $themeData['id']);
95
        $parentCriteria = new Criteria();
96
        $parentCriteria->addFilter(new EqualsFilter('childId', $themeData['id']));
97
        /** @var list<array<string, string>> $toDeleteIds */
98
        $toDeleteIds = $this->themeChildRepository->searchIds($parentCriteria, $context)->getIds();
99
        $this->themeChildRepository->delete($toDeleteIds, $context);
0 ignored issues
show
Bug introduced by
$toDeleteIds of type Shopware\Storefront\Theme\list is incompatible with the type array expected by parameter $ids of Shopware\Core\Framework\...ityRepository::delete(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

99
        $this->themeChildRepository->delete(/** @scrutinizer ignore-type */ $toDeleteIds, $context);
Loading history...
100
        $this->themeChildRepository->upsert($parentThemes, $context);
101
    }
102
103
    public function removeTheme(string $technicalName, Context $context): void
104
    {
105
        $criteria = new Criteria();
106
        $criteria->addAssociation('dependentThemes');
107
        $criteria->addFilter(new EqualsFilter('technicalName', $technicalName));
108
109
        /** @var ThemeEntity|null $theme */
110
        $theme = $this->themeRepository->search($criteria, $context)->first();
111
112
        if ($theme === null) {
113
            return;
114
        }
115
116
        $dependentThemes = $theme->getDependentThemes() ?? new ThemeCollection();
117
        $ids = [...array_values($dependentThemes->getIds()), ...[$theme->getId()]];
118
119
        $this->removeOldMedia($technicalName, $context);
120
        $this->themeRepository->delete(array_map(fn (string $id) => ['id' => $id], $ids), $context);
121
    }
122
123
    private function getThemeByTechnicalName(string $technicalName, Context $context): ?ThemeEntity
0 ignored issues
show
Bug introduced by
The type Shopware\Storefront\Theme\ThemeEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
124
    {
125
        $criteria = new Criteria();
126
        $criteria->addFilter(new EqualsFilter('technicalName', $technicalName));
127
128
        return $this->themeRepository->search($criteria, $context)->first();
129
    }
130
131
    /**
132
     * @return array<string, mixed>|null
133
     */
134
    private function createMediaStruct(string $path, string $mediaId, ?string $themeFolderId): ?array
135
    {
136
        $path = $this->themeFileImporter->getRealPath($path);
137
138
        if (!$this->themeFileImporter->fileExists($path)) {
139
            return null;
140
        }
141
142
        $pathinfo = pathinfo($path);
143
144
        return [
145
            'basename' => $pathinfo['filename'],
146
            'media' => ['id' => $mediaId, 'mediaFolderId' => $themeFolderId],
147
            'mediaFile' => new MediaFile(
148
                $path,
149
                (string) MimeType::fromFilename($pathinfo['basename']),
150
                $pathinfo['extension'] ?? '',
151
                (int) filesize($path)
152
            ),
153
        ];
154
    }
155
156
    private function getMediaDefaultFolderId(Context $context): ?string
157
    {
158
        $criteria = new Criteria();
159
        $criteria->addFilter(new EqualsFilter('media_folder.defaultFolder.entity', 'theme'));
160
        $criteria->addAssociation('defaultFolder');
161
        $criteria->setLimit(1);
162
        $defaultFolder = $this->mediaFolderRepository->search($criteria, $context);
163
        $defaultFolderId = null;
164
        if ($defaultFolder->count() === 1) {
165
            $defaultFolderId = $defaultFolder->first()->getId();
166
        }
167
168
        return $defaultFolderId;
169
    }
170
171
    /**
172
     * @return array<string, array<string, mixed>>
173
     */
174
    private function getTranslationsConfiguration(StorefrontPluginConfiguration $configuration, Context $context): array
175
    {
176
        $systemLanguageLocale = $this->getSystemLanguageLocale($context);
177
178
        $themeConfig = $configuration->getThemeConfig();
179
        if (!$themeConfig) {
180
            return [];
181
        }
182
183
        $labelTranslations = $this->getLabelsFromConfig($themeConfig);
184
        $translations = $this->mapTranslations($labelTranslations, 'labels', $systemLanguageLocale);
185
186
        $helpTextTranslations = $this->getHelpTextsFromConfig($themeConfig);
187
188
        return array_merge_recursive(
189
            $translations,
190
            $this->mapTranslations($helpTextTranslations, 'helpTexts', $systemLanguageLocale)
191
        );
192
    }
193
194
    /**
195
     * @param array<string, mixed> $config
196
     *
197
     * @return array<string, array<string, mixed>>
198
     */
199
    private function getLabelsFromConfig(array $config): array
200
    {
201
        $translations = [];
202
        if (\array_key_exists('blocks', $config)) {
203
            $translations = array_merge_recursive($translations, $this->extractLabels('blocks', $config['blocks']));
204
        }
205
206
        if (\array_key_exists('sections', $config)) {
207
            $translations = array_merge_recursive($translations, $this->extractLabels('sections', $config['sections']));
208
        }
209
210
        if (\array_key_exists('tabs', $config)) {
211
            $translations = array_merge_recursive($translations, $this->extractLabels('tabs', $config['tabs']));
212
        }
213
214
        if (\array_key_exists('fields', $config)) {
215
            $translations = array_merge_recursive($translations, $this->extractLabels('fields', $config['fields']));
216
        }
217
218
        return $translations;
219
    }
220
221
    /**
222
     * @param array<string, mixed> $data
223
     *
224
     * @return array<string, array<string, mixed>>
225
     */
226
    private function extractLabels(string $prefix, array $data): array
227
    {
228
        $labels = [];
229
        foreach ($data as $key => $item) {
230
            if (\array_key_exists('label', $item)) {
231
                /**
232
                 * @var string $locale
233
                 * @var string $label
234
                 */
235
                foreach ($item['label'] as $locale => $label) {
236
                    $labels[$locale][$prefix . '.' . $key] = $label;
237
                }
238
            }
239
        }
240
241
        return $labels;
242
    }
243
244
    /**
245
     * @param array<string, mixed> $config
246
     *
247
     * @return array<string, array<string, mixed>>
248
     */
249
    private function getHelpTextsFromConfig(array $config): array
250
    {
251
        $translations = [];
252
253
        if (\array_key_exists('fields', $config)) {
254
            $translations = array_merge_recursive($translations, $this->extractHelpTexts('fields', $config['fields']));
255
        }
256
257
        return $translations;
258
    }
259
260
    /**
261
     * @param array<string, mixed> $data
262
     *
263
     * @return array<string, array<string, mixed>>
264
     */
265
    private function extractHelpTexts(string $prefix, array $data): array
266
    {
267
        $helpTexts = [];
268
        foreach ($data as $key => $item) {
269
            if (!isset($item['helpText'])) {
270
                continue;
271
            }
272
273
            /**
274
             * @var string $locale
275
             * @var string $label
276
             */
277
            foreach ($item['helpText'] as $locale => $label) {
278
                $helpTexts[$locale][$prefix . '.' . $key] = $label;
279
            }
280
        }
281
282
        return $helpTexts;
283
    }
284
285
    private function removeOldMedia(string $technicalName, Context $context): void
286
    {
287
        $theme = $this->getThemeByTechnicalName($technicalName, $context);
288
289
        if (!$theme) {
290
            return;
291
        }
292
293
        // find all assigned media files
294
        $criteria = new Criteria();
295
        $criteria->addFilter(new EqualsFilter('media.themeMedia.id', $theme->getId()));
296
        $result = $this->mediaRepository->searchIds($criteria, $context);
297
298
        // delete theme media association
299
        $themeMediaData = [];
300
        foreach ($result->getIds() as $id) {
301
            $themeMediaData[] = ['themeId' => $theme->getId(), 'mediaId' => $id];
302
        }
303
304
        if (empty($themeMediaData)) {
305
            return;
306
        }
307
308
        // remove associations between theme and media first
309
        $this->themeMediaRepository->delete($themeMediaData, $context);
310
311
        // delete media associated with theme
312
        foreach ($themeMediaData as $item) {
313
            try {
314
                $this->mediaRepository->delete([['id' => $item['mediaId']]], $context);
315
            } catch (RestrictDeleteViolationException) {
316
                // don't delete files that are associated with other entities.
317
                // This files will be recreated using the file name strategy for duplicated filenames.
318
            }
319
        }
320
    }
321
322
    /**
323
     * @return array<string, mixed>
324
     */
325
    private function updateMediaInConfiguration(
326
        ?ThemeEntity $theme,
327
        StorefrontPluginConfiguration $pluginConfiguration,
328
        Context $context
329
    ): array {
330
        $media = [];
331
        $themeData = [];
332
        $themeFolderId = $this->getMediaDefaultFolderId($context);
333
334
        if ($pluginConfiguration->getPreviewMedia()) {
335
            $mediaId = Uuid::randomHex();
336
            $path = $pluginConfiguration->getPreviewMedia();
337
338
            $mediaItem = $this->createMediaStruct($path, $mediaId, $themeFolderId);
339
340
            if ($mediaItem) {
341
                $themeData['previewMediaId'] = $mediaId;
342
                $media[$path] = $mediaItem;
343
            }
344
345
            // if preview was not deleted because it is not created from theme use current preview id
346
            if ($theme && $theme->getPreviewMediaId() !== null) {
347
                $themeData['previewMediaId'] = $theme->getPreviewMediaId();
348
            }
349
        }
350
351
        $baseConfig = $pluginConfiguration->getThemeConfig() ?? [];
352
353
        if (\array_key_exists('fields', $baseConfig)) {
354
            foreach ($baseConfig['fields'] as $key => $field) {
355
                if (!\array_key_exists('type', $field) || $field['type'] !== 'media') {
356
                    continue;
357
                }
358
359
                $path = $pluginConfiguration->getBasePath() . \DIRECTORY_SEPARATOR . $field['value'];
360
361
                if (!\array_key_exists($path, $media)) {
362
                    $mediaId = Uuid::randomHex();
363
                    $mediaItem = $this->createMediaStruct($path, $mediaId, $themeFolderId);
364
365
                    if (!$mediaItem) {
366
                        continue;
367
                    }
368
369
                    $media[$path] = $mediaItem;
370
371
                    // replace media path with media ids
372
                    $baseConfig['fields'][$key]['value'] = $mediaId;
373
                } else {
374
                    $baseConfig['fields'][$key]['value'] = $media[$path]['media']['id'];
375
                }
376
            }
377
            $themeData['baseConfig'] = $baseConfig;
378
        }
379
380
        $mediaIds = [];
381
382
        if (!empty($media)) {
383
            $mediaIds = array_column($media, 'media');
384
385
            $this->mediaRepository->create($mediaIds, $context);
386
387
            foreach ($media as $item) {
388
                try {
389
                    $this->fileSaver->persistFileToMedia($item['mediaFile'], $item['basename'], $item['media']['id'], $context);
390
                } catch (DuplicatedMediaFileNameException) {
391
                    $newFileName = $this->fileNameProvider->provide(
392
                        $item['basename'],
393
                        $item['mediaFile']->getFileExtension(),
394
                        null,
395
                        $context
396
                    );
397
                    $this->fileSaver->persistFileToMedia($item['mediaFile'], $newFileName, $item['media']['id'], $context);
398
                }
399
            }
400
        }
401
402
        $themeData['media'] = $mediaIds;
403
404
        return $themeData;
405
    }
406
407
    private function getSystemLanguageLocale(Context $context): string
408
    {
409
        $criteria = new Criteria();
410
        $criteria->addAssociation('translationCode');
411
        $criteria->addFilter(new EqualsFilter('id', Defaults::LANGUAGE_SYSTEM));
412
413
        /** @var LanguageEntity $language */
414
        $language = $this->languageRepository->search($criteria, $context)->first();
415
        /** @var LocaleEntity $locale */
416
        $locale = $language->getTranslationCode();
417
418
        return $locale->getCode();
419
    }
420
421
    /**
422
     * @param array<string, mixed> $translations
423
     *
424
     * @return array<string, array<string, mixed>>
425
     */
426
    private function mapTranslations(array $translations, string $property, string $systemLanguageLocale): array
427
    {
428
        $result = [];
429
        $containsSystemLanguage = false;
430
        foreach ($translations as $locale => $translation) {
431
            if ($locale === $systemLanguageLocale) {
432
                $containsSystemLanguage = true;
433
            }
434
            $result[$locale] = [$property => $translation];
435
        }
436
437
        if (!$containsSystemLanguage && \count($translations) > 0) {
438
            $translation = array_shift($translations);
439
            if (\array_key_exists('en-GB', $translations)) {
440
                $translation = $translations['en-GB'];
441
            }
442
            $result[$systemLanguageLocale] = [$property => $translation];
443
        }
444
445
        return $result;
446
    }
447
448
    /**
449
     * @param array<string, mixed> $themeData
450
     *
451
     * @return array<string, mixed>
452
     */
453
    private function addParentTheme(StorefrontPluginConfiguration $configuration, array $themeData, Context $context): array
454
    {
455
        $lastNotSameTheme = null;
456
        foreach (array_reverse($configuration->getConfigInheritance()) as $themeName) {
457
            if (
458
                $themeName === '@' . StorefrontPluginRegistry::BASE_THEME_NAME
459
                || $themeName === '@' . $themeData['technicalName']
460
            ) {
461
                continue;
462
            }
463
            /** @var string $lastNotSameTheme */
464
            $lastNotSameTheme = str_replace('@', '', (string) $themeName);
465
        }
466
467
        if ($lastNotSameTheme !== null) {
468
            $criteria = new Criteria();
469
            $criteria->addFilter(new EqualsFilter('technicalName', $lastNotSameTheme));
470
            /** @var ThemeEntity|null $parentTheme */
471
            $parentTheme = $this->themeRepository->search($criteria, $context)->first();
472
            if ($parentTheme) {
473
                $themeData['parentThemeId'] = $parentTheme->getId();
474
            }
475
        }
476
477
        return $themeData;
478
    }
479
480
    /**
481
     * @return list<array{parentId: string, childId: string}>
0 ignored issues
show
Bug introduced by
The type Shopware\Storefront\Theme\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
482
     */
483
    private function getParentThemes(StorefrontPluginConfiguration $config, string $id): array
484
    {
485
        $allThemeConfigs = $this->pluginRegistry->getConfigurations()->getThemes();
486
487
        $allThemes = $this->getAllThemesPlain();
488
489
        $parentThemeConfigs = $allThemeConfigs->filter(
490
            fn (StorefrontPluginConfiguration $parentConfig) => $this->isDependentTheme($parentConfig, $config)
491
        );
492
493
        $technicalNames = $parentThemeConfigs->map(
494
            fn (StorefrontPluginConfiguration $theme) => $theme->getTechnicalName()
495
        );
496
497
        $parentThemes = array_filter(
498
            $allThemes,
499
            fn (array $theme) => \in_array($theme['technicalName'], $technicalNames, true)
500
        );
501
502
        $updateParents = [];
503
        foreach ($parentThemes as $parentTheme) {
504
            $updateParents[] = [
505
                'parentId' => $parentTheme['parentThemeId'],
506
                'childId' => $id,
507
            ];
508
        }
509
510
        return $updateParents;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $updateParents returns the type array|array<mixed,array<string,mixed|string>> which is incompatible with the documented return type Shopware\Storefront\Theme\list.
Loading history...
511
    }
512
513
    /**
514
     * @return list<array{technicalName: string, parentThemeId: string}>
515
     */
516
    private function getAllThemesPlain(): array
517
    {
518
        /** @var list<array{technicalName: string, parentThemeId: string}> $result */
519
        $result = $this->connection->fetchAllAssociative(
520
            'SELECT theme.technical_name as technicalName, LOWER(HEX(theme.id)) as parentThemeId FROM theme'
521
        );
522
523
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type Shopware\Storefront\Theme\list which is incompatible with the type-hinted return array.
Loading history...
524
    }
525
526
    private function isDependentTheme(
527
        StorefrontPluginConfiguration $parentConfig,
528
        StorefrontPluginConfiguration $currentThemeConfig
529
    ): bool {
530
        return $currentThemeConfig->getTechnicalName() !== $parentConfig->getTechnicalName()
531
            && \in_array('@' . $parentConfig->getTechnicalName(), $currentThemeConfig->getStyleFiles()->getFilepaths(), true)
532
        ;
533
    }
534
}
535