Passed
Push — master ( 33b245...1c8e6e )
by Christian
12:09 queued 12s
created

ThemeLifecycleService::extractHelpTexts()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 14
rs 10
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Storefront\Theme;
4
5
use Shopware\Core\Content\Media\Exception\DuplicatedMediaFileNameException;
6
use Shopware\Core\Content\Media\File\FileNameProvider;
7
use Shopware\Core\Content\Media\File\FileSaver;
8
use Shopware\Core\Content\Media\File\MediaFile;
9
use Shopware\Core\Defaults;
10
use Shopware\Core\Framework\Context;
11
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
12
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
13
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
14
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\RestrictDeleteViolationException;
15
use Shopware\Core\Framework\Uuid\Uuid;
16
use Shopware\Core\System\Language\LanguageEntity;
17
use Shopware\Storefront\Theme\StorefrontPluginConfiguration\StorefrontPluginConfiguration;
18
use Shopware\Storefront\Theme\StorefrontPluginConfiguration\StorefrontPluginConfigurationCollection;
19
use function GuzzleHttp\Psr7\mimetype_from_filename;
20
21
class ThemeLifecycleService
22
{
23
    /**
24
     * @var StorefrontPluginRegistryInterface
25
     */
26
    private $pluginRegistry;
27
28
    /**
29
     * @var EntityRepositoryInterface
30
     */
31
    private $themeRepository;
32
33
    /**
34
     * @var EntityRepositoryInterface
35
     */
36
    private $mediaRepository;
37
38
    /**
39
     * @var EntityRepositoryInterface
40
     */
41
    private $mediaFolderRepository;
42
43
    /**
44
     * @var EntityRepositoryInterface
45
     */
46
    private $themeMediaRepository;
47
48
    /**
49
     * @var FileSaver
50
     */
51
    private $fileSaver;
52
53
    /**
54
     * @var ThemeFileImporterInterface
55
     */
56
    private $themeFileImporter;
57
58
    /**
59
     * @var FileNameProvider
60
     */
61
    private $fileNameProvider;
62
63
    /**
64
     * @var EntityRepositoryInterface
65
     */
66
    private $languageRepository;
67
68
    public function __construct(
69
        StorefrontPluginRegistryInterface $pluginRegistry,
70
        EntityRepositoryInterface $themeRepository,
71
        EntityRepositoryInterface $mediaRepository,
72
        EntityRepositoryInterface $mediaFolderRepository,
73
        EntityRepositoryInterface $themeMediaRepository,
74
        FileSaver $fileSaver,
75
        FileNameProvider $fileNameProvider,
76
        ThemeFileImporterInterface $themeFileImporter,
77
        EntityRepositoryInterface $languageRepository
78
    ) {
79
        $this->pluginRegistry = $pluginRegistry;
80
        $this->themeRepository = $themeRepository;
81
        $this->mediaRepository = $mediaRepository;
82
        $this->mediaFolderRepository = $mediaFolderRepository;
83
        $this->themeMediaRepository = $themeMediaRepository;
84
        $this->fileSaver = $fileSaver;
85
        $this->fileNameProvider = $fileNameProvider;
86
        $this->themeFileImporter = $themeFileImporter;
87
        $this->languageRepository = $languageRepository;
88
    }
89
90
    public function refreshThemes(
91
        Context $context,
92
        ?StorefrontPluginConfigurationCollection $configurationCollection = null
93
    ): void {
94
        if ($configurationCollection === null) {
95
            $configurationCollection = $this->pluginRegistry->getConfigurations()->getThemes();
96
        }
97
98
        // iterate over all theme configs in the filesystem (plugins/bundles)
99
        foreach ($configurationCollection as $config) {
100
            $this->refreshTheme($config, $context);
101
        }
102
    }
103
104
    public function refreshTheme(StorefrontPluginConfiguration $configuration, Context $context): void
105
    {
106
        if ($configuration->getTechnicalName() === null) {
107
            throw new \LogicException('Bundle can not exist without technical name');
108
        }
109
110
        $themeData['name'] = $configuration->getName();
0 ignored issues
show
Comprehensibility Best Practice introduced by
$themeData was never initialized. Although not strictly required by PHP, it is generally a good practice to add $themeData = array(); before regardless.
Loading history...
111
        $themeData['technicalName'] = $configuration->getTechnicalName();
112
        $themeData['author'] = $configuration->getAuthor();
113
114
        $this->removeOldMedia($configuration->getTechnicalName(), $context);
115
116
        // refresh theme after deleting media
117
        $theme = $this->getThemeByTechnicalName($configuration->getTechnicalName(), $context);
118
119
        // check if theme config already exists in the database
120
        if ($theme) {
121
            $themeData['id'] = $theme->getId();
122
        } else {
123
            $themeData['active'] = true;
124
        }
125
126
        $themeData['translations'] = $this->getTranslationsConfiguration($configuration, $context);
127
128
        $updatedData = $this->updateMediaInConfiguration($theme, $configuration, $context);
129
130
        $themeData = array_merge($themeData, $updatedData);
131
132
        $this->themeRepository->upsert([$themeData], $context);
133
    }
134
135
    private function getThemeByTechnicalName(string $technicalName, Context $context): ?ThemeEntity
136
    {
137
        $criteria = new Criteria();
138
        $criteria->addFilter(new EqualsFilter('technicalName', $technicalName));
139
140
        return $this->themeRepository->search($criteria, $context)->first();
141
    }
142
143
    private function createMediaStruct(string $path, string $mediaId, string $themeFolderId): ?array
144
    {
145
        if (!$this->fileExists($path)) {
146
            return null;
147
        }
148
149
        $path = $this->themeFileImporter->getRealPath($path);
150
151
        $pathinfo = pathinfo($path);
152
153
        return [
154
            'basename' => $pathinfo['filename'],
155
            'media' => ['id' => $mediaId, 'mediaFolderId' => $themeFolderId],
156
            'mediaFile' => new MediaFile(
157
                $path,
158
                mimetype_from_filename($pathinfo['basename']),
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Psr7\mimetype_from_filename() has been deprecated: mimetype_from_filename will be removed in guzzlehttp/psr7:2.0. Use MimeType::fromFilename instead. ( Ignorable by Annotation )

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

158
                /** @scrutinizer ignore-deprecated */ mimetype_from_filename($pathinfo['basename']),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
Bug introduced by
It seems like mimetype_from_filename($pathinfo['basename']) can also be of type null; however, parameter $mimeType of Shopware\Core\Content\Me...ediaFile::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

158
                /** @scrutinizer ignore-type */ mimetype_from_filename($pathinfo['basename']),
Loading history...
159
                $pathinfo['extension'],
160
                filesize($path)
161
            ),
162
        ];
163
    }
164
165
    private function getMediaDefaultFolderId(string $folder, Context $context): ?string
166
    {
167
        $criteria = new Criteria();
168
        $criteria->addFilter(new EqualsFilter('media_folder.defaultFolder.entity', $folder));
169
        $criteria->addAssociation('defaultFolder');
170
        $criteria->setLimit(1);
171
        $defaultFolder = $this->mediaFolderRepository->search($criteria, $context);
172
        $defaultFolderId = null;
173
        if ($defaultFolder->count() === 1) {
174
            $defaultFolderId = $defaultFolder->first()->getId();
175
        }
176
177
        return $defaultFolderId;
178
    }
179
180
    private function getTranslationsConfiguration(StorefrontPluginConfiguration $configuration, Context $context): array
181
    {
182
        $systemLanguageLocale = $this->getSystemLanguageLocale($context);
183
184
        $labelTranslations = $this->getLabelsFromConfig($configuration->getThemeConfig());
185
        $translations = $this->mapTranslations($labelTranslations, 'labels', $systemLanguageLocale);
186
187
        $helpTextTranslations = $this->getHelpTextsFromConfig($configuration->getThemeConfig());
188
189
        return array_merge_recursive(
190
            $translations,
191
            $this->mapTranslations($helpTextTranslations, 'helpTexts', $systemLanguageLocale)
192
        );
193
    }
194
195
    private function getLabelsFromConfig(array $config): array
196
    {
197
        $translations = [];
198
        if (array_key_exists('blocks', $config)) {
199
            $translations = array_merge_recursive($translations, $this->extractLabels('blocks', $config['blocks']));
200
        }
201
202
        if (array_key_exists('sections', $config)) {
203
            $translations = array_merge_recursive($translations, $this->extractLabels('sections', $config['sections']));
204
        }
205
206
        if (array_key_exists('tabs', $config)) {
207
            $translations = array_merge_recursive($translations, $this->extractLabels('tabs', $config['tabs']));
208
        }
209
210
        if (array_key_exists('fields', $config)) {
211
            $translations = array_merge_recursive($translations, $this->extractLabels('fields', $config['fields']));
212
        }
213
214
        return $translations;
215
    }
216
217
    private function extractLabels(string $prefix, array $data): array
218
    {
219
        $labels = [];
220
        foreach ($data as $key => $item) {
221
            if (array_key_exists('label', $item)) {
222
                foreach ($item['label'] as $locale => $label) {
223
                    $labels[$locale][$prefix . '.' . $key] = $label;
224
                }
225
            }
226
        }
227
228
        return $labels;
229
    }
230
231
    private function getHelpTextsFromConfig(array $config): array
232
    {
233
        $translations = [];
234
235
        if (array_key_exists('fields', $config)) {
236
            $translations = array_merge_recursive($translations, $this->extractHelpTexts('fields', $config['fields']));
237
        }
238
239
        return $translations;
240
    }
241
242
    private function extractHelpTexts(string $prefix, array $data): array
243
    {
244
        $helpTexts = [];
245
        foreach ($data as $key => $item) {
246
            if (!isset($item['helpText'])) {
247
                continue;
248
            }
249
250
            foreach ($item['helpText'] as $locale => $label) {
251
                $helpTexts[$locale][$prefix . '.' . $key] = $label;
252
            }
253
        }
254
255
        return $helpTexts;
256
    }
257
258
    private function fileExists(string $path): bool
259
    {
260
        return $this->themeFileImporter->fileExists($path);
261
    }
262
263
    private function removeOldMedia(string $technicalName, Context $context): void
264
    {
265
        $theme = $this->getThemeByTechnicalName($technicalName, $context);
266
267
        if (!$theme) {
268
            return;
269
        }
270
271
        // find all assigned media files
272
        $criteria = new Criteria();
273
        $criteria->addFilter(new EqualsFilter('media.themeMedia.id', $theme->getId()));
274
        $result = $this->mediaRepository->searchIds($criteria, $context);
275
276
        // delete theme media association
277
        $themeMediaData = [];
278
        foreach ($result->getIds() as $id) {
279
            $themeMediaData[] = ['themeId' => $theme->getId(), 'mediaId' => $id];
280
        }
281
282
        if (empty($themeMediaData)) {
283
            return;
284
        }
285
286
        // remove associations between theme and media first
287
        $this->themeMediaRepository->delete($themeMediaData, $context);
288
289
        // delete media associated with theme
290
        foreach ($themeMediaData as $item) {
291
            try {
292
                $this->mediaRepository->delete([['id' => $item['mediaId']]], $context);
293
            } catch (RestrictDeleteViolationException $e) {
294
                // don't delete files that are associated with other entities.
295
                // This files will be recreated using the file name strategy for duplicated filenames.
296
            }
297
        }
298
    }
299
300
    private function updateMediaInConfiguration(?ThemeEntity $theme, StorefrontPluginConfiguration $pluginConfiguration, Context $context): array
301
    {
302
        $media = [];
303
        $themeData = [];
304
        $themeFolderId = $this->getMediaDefaultFolderId('theme', $context);
305
306
        if ($pluginConfiguration->getPreviewMedia()) {
307
            $mediaId = Uuid::randomHex();
308
            $path = $pluginConfiguration->getPreviewMedia();
309
310
            $mediaItem = $this->createMediaStruct($path, $mediaId, $themeFolderId);
0 ignored issues
show
Bug introduced by
It seems like $themeFolderId can also be of type null; however, parameter $themeFolderId of Shopware\Storefront\Them...ce::createMediaStruct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

310
            $mediaItem = $this->createMediaStruct($path, $mediaId, /** @scrutinizer ignore-type */ $themeFolderId);
Loading history...
311
312
            if ($mediaItem) {
313
                $themeData['previewMediaId'] = $mediaId;
314
                $media[$path] = $mediaItem;
315
            }
316
317
            // if preview was not deleted because it is not created from theme use current preview id
318
            if ($theme && $theme->getPreviewMediaId() !== null) {
319
                $themeData['previewMediaId'] = $theme->getPreviewMediaId();
320
            }
321
        }
322
323
        $baseConfig = $pluginConfiguration->getThemeConfig();
324
325
        if (array_key_exists('fields', $baseConfig)) {
326
            foreach ($baseConfig['fields'] as $key => $field) {
327
                if (!array_key_exists('type', $field) || $field['type'] !== 'media') {
328
                    continue;
329
                }
330
331
                $path = $pluginConfiguration->getBasePath() . DIRECTORY_SEPARATOR . $field['value'];
332
333
                if (!array_key_exists($path, $media)) {
334
                    $mediaId = Uuid::randomHex();
335
                    $mediaItem = $this->createMediaStruct($path, $mediaId, $themeFolderId);
336
337
                    if (!$mediaItem) {
338
                        continue;
339
                    }
340
341
                    $media[$path] = $mediaItem;
342
343
                    // replace media path with media ids
344
                    $baseConfig['fields'][$key]['value'] = $mediaId;
345
                } else {
346
                    $baseConfig['fields'][$key]['value'] = $media[$path]['media']['id'];
347
                }
348
            }
349
            $themeData['baseConfig'] = $baseConfig;
350
        }
351
352
        $mediaIds = [];
353
354
        if (!empty($media)) {
355
            $mediaIds = array_column($media, 'media');
356
357
            $this->mediaRepository->create($mediaIds, $context);
358
359
            foreach ($media as $item) {
360
                try {
361
                    $this->fileSaver->persistFileToMedia($item['mediaFile'], $item['basename'], $item['media']['id'], $context);
362
                } catch (DuplicatedMediaFileNameException $e) {
363
                    $newFileName = $this->fileNameProvider->provide(
364
                        $item['basename'],
365
                        $item['mediaFile']->getFileExtension(),
366
                        null,
367
                        $context
368
                    );
369
                    $this->fileSaver->persistFileToMedia($item['mediaFile'], $newFileName, $item['media']['id'], $context);
370
                }
371
            }
372
        }
373
374
        $themeData['media'] = $mediaIds;
375
376
        return $themeData;
377
    }
378
379
    private function getSystemLanguageLocale(Context $context): string
380
    {
381
        $criteria = new Criteria();
382
        $criteria->addAssociation('translationCode');
383
        $criteria->addFilter(new EqualsFilter('id', Defaults::LANGUAGE_SYSTEM));
384
385
        /** @var LanguageEntity $language */
386
        $language = $this->languageRepository->search($criteria, $context)->first();
387
388
        return $language->getTranslationCode()->getCode();
389
    }
390
391
    private function mapTranslations(array $translations, string $property, string $systemLanguageLocale): array
392
    {
393
        $result = [];
394
        $containsSystemLanguage = false;
395
        foreach ($translations as $locale => $translation) {
396
            if ($locale === $systemLanguageLocale) {
397
                $containsSystemLanguage = true;
398
            }
399
            $result[$locale] = [$property => $translation];
400
        }
401
402
        if (!$containsSystemLanguage && count($translations) > 0) {
403
            $translation = array_shift($translations);
404
            if (array_key_exists('en-GB', $translations)) {
405
                $translation = $translations['en-GB'];
406
            }
407
            $result[$systemLanguageLocale] = [$property => $translation];
408
        }
409
410
        return $result;
411
    }
412
}
413