updateMediaInConfiguration()   F
last analyzed

Complexity

Conditions 38
Paths 216

Size

Total Lines 158
Code Lines 94

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 38
eloc 94
nc 216
nop 3
dl 0
loc 158
rs 3.1333
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 GuzzleHttp\Psr7\MimeType;
7
use Shopware\Core\Content\Media\Aggregate\MediaFolder\MediaFolderEntity;
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\Content\Media\MediaCollection;
12
use Shopware\Core\Content\Media\MediaException;
13
use Shopware\Core\Defaults;
14
use Shopware\Core\Framework\Context;
15
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
16
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
17
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
18
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\RestrictDeleteViolationException;
19
use Shopware\Core\Framework\Log\Package;
20
use Shopware\Core\Framework\Uuid\Uuid;
21
use Shopware\Core\System\Language\LanguageEntity;
22
use Shopware\Core\System\Locale\LocaleEntity;
23
use Shopware\Storefront\Theme\StorefrontPluginConfiguration\AbstractStorefrontPluginConfigurationFactory;
24
use Shopware\Storefront\Theme\StorefrontPluginConfiguration\StorefrontPluginConfiguration;
25
use Shopware\Storefront\Theme\StorefrontPluginConfiguration\StorefrontPluginConfigurationCollection;
26
27
#[Package('storefront')]
28
class ThemeLifecycleService
29
{
30
    /**
31
     * @param EntityRepository<MediaCollection> $mediaRepository
32
     *
33
     * @internal
34
     *
35
     * @decrecated tag:v6.6.0 argument $pluginConfigurationFactory will be mandatory
36
     */
37
    public function __construct(
38
        private readonly StorefrontPluginRegistryInterface $pluginRegistry,
39
        private readonly EntityRepository $themeRepository,
40
        private readonly EntityRepository $mediaRepository,
41
        private readonly EntityRepository $mediaFolderRepository,
42
        private readonly EntityRepository $themeMediaRepository,
43
        private readonly FileSaver $fileSaver,
44
        private readonly FileNameProvider $fileNameProvider,
45
        private readonly ThemeFileImporterInterface $themeFileImporter,
46
        private readonly EntityRepository $languageRepository,
47
        private readonly EntityRepository $themeChildRepository,
48
        private readonly Connection $connection,
49
        private readonly ?AbstractStorefrontPluginConfigurationFactory $pluginConfigurationFactory
50
    ) {
51
    }
52
53
    public function refreshThemes(
54
        Context $context,
55
        ?StorefrontPluginConfigurationCollection $configurationCollection = null
56
    ): void {
57
        if ($configurationCollection === null) {
58
            $configurationCollection = $this->pluginRegistry->getConfigurations()->getThemes();
59
        }
60
61
        // iterate over all theme configs in the filesystem (plugins/bundles)
62
        foreach ($configurationCollection as $config) {
63
            $this->refreshTheme($config, $context);
64
        }
65
    }
66
67
    public function refreshTheme(StorefrontPluginConfiguration $configuration, Context $context): void
68
    {
69
        $themeData = [];
70
        $themeData['name'] = $configuration->getName();
71
        $themeData['technicalName'] = $configuration->getTechnicalName();
72
        $themeData['author'] = $configuration->getAuthor();
73
        $themeData['themeJson'] = $configuration->getThemeJson();
74
75
        // refresh theme after deleting media
76
        $theme = $this->getThemeByTechnicalName($configuration->getTechnicalName(), $context);
77
78
        // check if theme config already exists in the database
79
        if ($theme) {
80
            $themeData['id'] = $theme->getId();
81
        } else {
82
            $themeData['active'] = true;
83
        }
84
85
        $themeData['translations'] = $this->getTranslationsConfiguration($configuration, $context);
86
87
        $updatedData = $this->updateMediaInConfiguration($theme, $configuration, $context);
88
89
        $themeData = array_merge($themeData, $updatedData);
90
91
        if (!empty($configuration->getConfigInheritance())) {
92
            $themeData = $this->addParentTheme($configuration, $themeData, $context);
93
        }
94
95
        $writtenEvent = $this->themeRepository->upsert([$themeData], $context);
96
97
        if (empty($themeData['id'])) {
98
            $themeData['id'] = current($writtenEvent->getPrimaryKeys(ThemeDefinition::ENTITY_NAME));
99
        }
100
101
        $this->themeRepository->upsert([$themeData], $context);
102
103
        if (!empty($themeData['toDeleteMedia'])) {
104
            $this->themeMediaRepository->delete($themeData['toDeleteMedia'], $context);
105
        }
106
107
        $parentThemes = $this->getParentThemes($configuration, $themeData['id']);
108
        $parentCriteria = new Criteria();
109
        $parentCriteria->addFilter(new EqualsFilter('childId', $themeData['id']));
110
        /** @var list<array<string, string>> $toDeleteIds */
111
        $toDeleteIds = $this->themeChildRepository->searchIds($parentCriteria, $context)->getIds();
112
        $this->themeChildRepository->delete($toDeleteIds, $context);
113
        $this->themeChildRepository->upsert($parentThemes, $context);
114
    }
115
116
    public function removeTheme(string $technicalName, Context $context): void
117
    {
118
        $criteria = new Criteria();
119
        $criteria->addAssociation('dependentThemes');
120
        $criteria->addFilter(new EqualsFilter('technicalName', $technicalName));
121
122
        /** @var ThemeEntity|null $theme */
123
        $theme = $this->themeRepository->search($criteria, $context)->first();
124
125
        if ($theme === null) {
126
            return;
127
        }
128
129
        $dependentThemes = $theme->getDependentThemes() ?? new ThemeCollection();
130
        $ids = [...array_values($dependentThemes->getIds()), ...[$theme->getId()]];
131
132
        $this->removeOldMedia($technicalName, $context);
133
        $this->themeRepository->delete(array_map(fn (string $id) => ['id' => $id], $ids), $context);
134
    }
135
136
    private function getThemeByTechnicalName(string $technicalName, Context $context): ?ThemeEntity
137
    {
138
        $criteria = new Criteria();
139
        $criteria->addFilter(new EqualsFilter('technicalName', $technicalName));
140
        $criteria->addAssociation('previewMedia');
141
142
        $theme = $this->themeRepository->search($criteria, $context)->first();
143
144
        return $theme instanceof ThemeEntity ? $theme : null;
145
    }
146
147
    /**
148
     * @return array<string, mixed>|null
149
     */
150
    private function createMediaStruct(string $path, string $mediaId, ?string $themeFolderId): ?array
151
    {
152
        $path = $this->themeFileImporter->getRealPath($path);
153
154
        if (!$this->themeFileImporter->fileExists($path)) {
155
            return null;
156
        }
157
158
        $pathinfo = pathinfo($path);
159
160
        return [
161
            'basename' => $pathinfo['filename'],
162
            'media' => ['id' => $mediaId, 'mediaFolderId' => $themeFolderId],
163
            'mediaFile' => new MediaFile(
164
                $path,
165
                (string) MimeType::fromFilename($pathinfo['basename']),
166
                $pathinfo['extension'] ?? '',
167
                (int) filesize($path)
168
            ),
169
        ];
170
    }
171
172
    private function getMediaDefaultFolderId(Context $context): ?string
173
    {
174
        $criteria = new Criteria();
175
        $criteria->addFilter(new EqualsFilter('media_folder.defaultFolder.entity', 'theme'));
176
        $criteria->addAssociation('defaultFolder');
177
        $criteria->setLimit(1);
178
        $defaultFolder = $this->mediaFolderRepository->search($criteria, $context);
179
        $defaultFolderId = null;
180
        if ($defaultFolder->count() === 1) {
181
            $defaultFolder = $defaultFolder->first();
182
183
            if ($defaultFolder instanceof MediaFolderEntity) {
184
                $defaultFolderId = $defaultFolder->getId();
185
            }
186
        }
187
188
        return $defaultFolderId;
189
    }
190
191
    /**
192
     * @return array<string, array<string, mixed>>
193
     */
194
    private function getTranslationsConfiguration(StorefrontPluginConfiguration $configuration, Context $context): array
195
    {
196
        $systemLanguageLocale = $this->getSystemLanguageLocale($context);
197
198
        $themeConfig = $configuration->getThemeConfig();
199
        if (!$themeConfig) {
200
            return [];
201
        }
202
203
        $labelTranslations = $this->getLabelsFromConfig($themeConfig);
204
        $translations = $this->mapTranslations($labelTranslations, 'labels', $systemLanguageLocale);
205
206
        $helpTextTranslations = $this->getHelpTextsFromConfig($themeConfig);
207
208
        return array_merge_recursive(
209
            $translations,
210
            $this->mapTranslations($helpTextTranslations, 'helpTexts', $systemLanguageLocale)
211
        );
212
    }
213
214
    /**
215
     * @param array<string, mixed> $config
216
     *
217
     * @return array<string, array<string, mixed>>
218
     */
219
    private function getLabelsFromConfig(array $config): array
220
    {
221
        $translations = [];
222
        if (\array_key_exists('blocks', $config)) {
223
            $translations = array_merge_recursive($translations, $this->extractLabels('blocks', $config['blocks']));
224
        }
225
226
        if (\array_key_exists('sections', $config)) {
227
            $translations = array_merge_recursive($translations, $this->extractLabels('sections', $config['sections']));
228
        }
229
230
        if (\array_key_exists('tabs', $config)) {
231
            $translations = array_merge_recursive($translations, $this->extractLabels('tabs', $config['tabs']));
232
        }
233
234
        if (\array_key_exists('fields', $config)) {
235
            $translations = array_merge_recursive($translations, $this->extractLabels('fields', $config['fields']));
236
        }
237
238
        return $translations;
239
    }
240
241
    /**
242
     * @param array<string, mixed> $data
243
     *
244
     * @return array<string, array<string, mixed>>
245
     */
246
    private function extractLabels(string $prefix, array $data): array
247
    {
248
        $labels = [];
249
        foreach ($data as $key => $item) {
250
            if (\array_key_exists('label', $item)) {
251
                /**
252
                 * @var string $locale
253
                 * @var string $label
254
                 */
255
                foreach ($item['label'] as $locale => $label) {
256
                    $labels[$locale][$prefix . '.' . $key] = $label;
257
                }
258
            }
259
        }
260
261
        return $labels;
262
    }
263
264
    /**
265
     * @param array<string, mixed> $config
266
     *
267
     * @return array<string, array<string, mixed>>
268
     */
269
    private function getHelpTextsFromConfig(array $config): array
270
    {
271
        $translations = [];
272
273
        if (\array_key_exists('fields', $config)) {
274
            $translations = array_merge_recursive($translations, $this->extractHelpTexts('fields', $config['fields']));
275
        }
276
277
        return $translations;
278
    }
279
280
    /**
281
     * @param array<string, mixed> $data
282
     *
283
     * @return array<string, array<string, mixed>>
284
     */
285
    private function extractHelpTexts(string $prefix, array $data): array
286
    {
287
        $helpTexts = [];
288
        foreach ($data as $key => $item) {
289
            if (!isset($item['helpText'])) {
290
                continue;
291
            }
292
293
            /**
294
             * @var string $locale
295
             * @var string $label
296
             */
297
            foreach ($item['helpText'] as $locale => $label) {
298
                $helpTexts[$locale][$prefix . '.' . $key] = $label;
299
            }
300
        }
301
302
        return $helpTexts;
303
    }
304
305
    private function removeOldMedia(string $technicalName, Context $context): void
306
    {
307
        $theme = $this->getThemeByTechnicalName($technicalName, $context);
308
309
        if (!$theme) {
310
            return;
311
        }
312
313
        // find all assigned media files
314
        $criteria = new Criteria();
315
        $criteria->addFilter(new EqualsFilter('media.themeMedia.id', $theme->getId()));
316
        $result = $this->mediaRepository->searchIds($criteria, $context);
317
318
        // delete theme media association
319
        $themeMediaData = [];
320
        foreach ($result->getIds() as $id) {
321
            $themeMediaData[] = ['themeId' => $theme->getId(), 'mediaId' => $id];
322
        }
323
324
        if (empty($themeMediaData)) {
325
            return;
326
        }
327
328
        // remove associations between theme and media first
329
        $this->themeMediaRepository->delete($themeMediaData, $context);
330
331
        // delete media associated with theme
332
        foreach ($themeMediaData as $item) {
333
            try {
334
                $this->mediaRepository->delete([['id' => $item['mediaId']]], $context);
335
            } catch (RestrictDeleteViolationException) {
336
                // don't delete files that are associated with other entities.
337
                // This files will be recreated using the file name strategy for duplicated filenames.
338
            }
339
        }
340
    }
341
342
    /**
343
     * @return array<string, mixed>
344
     */
345
    private function updateMediaInConfiguration(
346
        ?ThemeEntity $theme,
347
        StorefrontPluginConfiguration $pluginConfiguration,
348
        Context $context
349
    ): array {
350
        $media = [];
351
        $themeData = [];
352
        $themeFolderId = $this->getMediaDefaultFolderId($context);
353
354
        $installedConfiguration = null;
355
        if ($theme && \is_array($theme->getThemeJson()) && $this->pluginConfigurationFactory) {
356
            $installedConfiguration = $this->pluginConfigurationFactory->createFromThemeJson(
357
                $theme->getTechnicalName() ?? 'childTheme',
358
                $theme->getThemeJson(),
359
                $pluginConfiguration->getBasePath(),
360
                false
361
            );
362
        }
363
364
        if (
365
            $pluginConfiguration->getPreviewMedia()
366
            && $pluginConfiguration->getPreviewMedia() !== $installedConfiguration?->getPreviewMedia()
367
            && (
368
                $theme === null
369
                || $theme->getPreviewMedia() === null
370
                || basename($installedConfiguration?->getPreviewMedia() ?? '') !== $theme->getPreviewMedia()->getFileNameIncludingExtension()
371
            )
372
        ) {
373
            $mediaId = Uuid::randomHex();
374
375
            $path = $pluginConfiguration->getPreviewMedia();
376
377
            $mediaItem = $this->createMediaStruct($path, $mediaId, $themeFolderId);
378
379
            if ($mediaItem) {
380
                $themeData['previewMediaId'] = $mediaId;
381
                $media[$path] = $mediaItem;
382
            }
383
        }
384
385
        $baseConfig = $pluginConfiguration->getThemeConfig() ?? [];
386
        $installedBaseConfig = $installedConfiguration?->getThemeConfig() ?? [];
387
388
        $currentThemeMedia = null;
389
        $currentMediaIds = null;
390
        $toDeleteIds = null;
391
        // get existing MediaFiles
392
        if ($theme !== null && \array_key_exists('fields', $theme->getBaseConfig() ?? [])) {
393
            foreach ($theme->getBaseConfig()['fields'] as $key => $field) {
394
                if ($this->hasOldMedia($field) === false) {
395
                    continue;
396
                }
397
                $currentMediaIds[$key] = $field['value'];
398
            }
399
400
            if (!empty($currentMediaIds)) {
401
                $currentThemeMedia = $this->mediaRepository->search(new Criteria($currentMediaIds), $context);
402
            }
403
        }
404
405
        if (\array_key_exists('fields', $baseConfig)) {
406
            foreach ($baseConfig['fields'] as $key => $field) {
407
                if ($this->hasNewMedia($field) === false) {
408
                    continue;
409
                }
410
411
                if (
412
                    isset($installedBaseConfig['fields'][$key]['value'])
413
                    && $field['value'] === $installedBaseConfig['fields'][$key]['value']
414
                ) {
415
                    $baseConfig['fields'][$key]['value'] = $currentMediaIds[$key] ?? $baseConfig['fields'][$key]['value'];
416
417
                    continue;
418
                }
419
420
                $path = $pluginConfiguration->getBasePath() . \DIRECTORY_SEPARATOR . $field['value'];
421
422
                if (!\array_key_exists($path, $media)) {
423
                    if (
424
                        $currentThemeMedia !== null
425
                        && !empty($currentMediaIds)
426
                        && isset($currentMediaIds[$key])
427
                        && $currentThemeMedia->getEntities()->get($currentMediaIds[$key])?->getFileNameIncludingExtension() === basename($path)) {
428
                        continue;
429
                    }
430
431
                    $criteriaMedia = new Criteria();
432
                    $criteriaMedia->addFilter(new EqualsFilter('fileName', basename($path)));
433
                    if ($this->mediaRepository->searchIds($criteriaMedia, $context)->getTotal() > 0) {
434
                        continue;
435
                    }
436
437
                    $mediaId = Uuid::randomHex();
438
                    $mediaItem = $this->createMediaStruct($path, $mediaId, $themeFolderId);
439
440
                    if (!$mediaItem) {
441
                        continue;
442
                    }
443
444
                    $media[$path] = $mediaItem;
445
446
                    // replace media path with media ids
447
                    $baseConfig['fields'][$key]['value'] = $mediaId;
448
                } else {
449
                    $baseConfig['fields'][$key]['value'] = $media[$path]['media']['id'];
450
                }
451
                if ($theme && isset($currentMediaIds[$key])) {
452
                    $toDeleteIds[] = $currentMediaIds[$key];
453
                }
454
            }
455
            $themeData['baseConfig'] = $baseConfig;
456
        }
457
458
        $mediaIds = [];
459
460
        if (!empty($media)) {
461
            $mediaIds = array_column($media, 'media');
462
463
            $this->mediaRepository->create($mediaIds, $context);
464
465
            foreach ($media as $item) {
466
                if (!$item['mediaFile'] instanceof MediaFile) {
467
                    throw MediaException::missingFile($item['media']['id']);
468
                }
469
470
                try {
471
                    $this->fileSaver->persistFileToMedia($item['mediaFile'], $item['basename'], $item['media']['id'], $context);
472
                } catch (MediaException $e) {
473
                    if ($e->getErrorCode() !== MediaException::MEDIA_DUPLICATED_FILE_NAME) {
474
                        throw $e;
475
                    }
476
477
                    $newFileName = $this->fileNameProvider->provide(
478
                        $item['basename'],
479
                        $item['mediaFile']->getFileExtension(),
480
                        null,
481
                        $context
482
                    );
483
                    $this->fileSaver->persistFileToMedia($item['mediaFile'], $newFileName, $item['media']['id'], $context);
484
                }
485
            }
486
        }
487
488
        $themeData['media'] = $mediaIds;
489
490
        if ($theme && \is_array($toDeleteIds)) {
491
            $toDeleteIds = array_unique($toDeleteIds);
492
            foreach ($toDeleteIds as $id) {
493
                if (Uuid::isValid($id)) {
494
                    $themeData['toDeleteMedia'][] = [
495
                        'mediaId' => $id,
496
                        'themeId' => $theme->getId(),
497
                    ];
498
                }
499
            }
500
        }
501
502
        return $themeData;
503
    }
504
505
    private function getSystemLanguageLocale(Context $context): string
506
    {
507
        $criteria = new Criteria();
508
        $criteria->addAssociation('translationCode');
509
        $criteria->addFilter(new EqualsFilter('id', Defaults::LANGUAGE_SYSTEM));
510
511
        /** @var LanguageEntity $language */
512
        $language = $this->languageRepository->search($criteria, $context)->first();
513
        /** @var LocaleEntity $locale */
514
        $locale = $language->getTranslationCode();
515
516
        return $locale->getCode();
517
    }
518
519
    /**
520
     * @param array<string, mixed> $translations
521
     *
522
     * @return array<string, array<string, mixed>>
523
     */
524
    private function mapTranslations(array $translations, string $property, string $systemLanguageLocale): array
525
    {
526
        $result = [];
527
        $containsSystemLanguage = false;
528
        foreach ($translations as $locale => $translation) {
529
            if ($locale === $systemLanguageLocale) {
530
                $containsSystemLanguage = true;
531
            }
532
            $result[$locale] = [$property => $translation];
533
        }
534
535
        if (!$containsSystemLanguage && \count($translations) > 0) {
536
            $translation = array_shift($translations);
537
            if (\array_key_exists('en-GB', $translations)) {
538
                $translation = $translations['en-GB'];
539
            }
540
            $result[$systemLanguageLocale] = [$property => $translation];
541
        }
542
543
        return $result;
544
    }
545
546
    /**
547
     * @param array<string, mixed> $themeData
548
     *
549
     * @return array<string, mixed>
550
     */
551
    private function addParentTheme(StorefrontPluginConfiguration $configuration, array $themeData, Context $context): array
552
    {
553
        $lastNotSameTheme = null;
554
        foreach (array_reverse($configuration->getConfigInheritance()) as $themeName) {
555
            if (
556
                $themeName === '@' . StorefrontPluginRegistry::BASE_THEME_NAME
557
                || $themeName === '@' . $themeData['technicalName']
558
            ) {
559
                continue;
560
            }
561
            /** @var string $lastNotSameTheme */
562
            $lastNotSameTheme = str_replace('@', '', (string) $themeName);
563
        }
564
565
        if ($lastNotSameTheme !== null) {
566
            $criteria = new Criteria();
567
            $criteria->addFilter(new EqualsFilter('technicalName', $lastNotSameTheme));
568
            /** @var ThemeEntity|null $parentTheme */
569
            $parentTheme = $this->themeRepository->search($criteria, $context)->first();
570
            if ($parentTheme) {
571
                $themeData['parentThemeId'] = $parentTheme->getId();
572
            }
573
        }
574
575
        return $themeData;
576
    }
577
578
    /**
579
     * @return list<array{parentId: string, childId: string}>
580
     */
581
    private function getParentThemes(StorefrontPluginConfiguration $config, string $id): array
582
    {
583
        $allThemeConfigs = $this->pluginRegistry->getConfigurations()->getThemes();
584
585
        $allThemes = $this->getAllThemesPlain();
586
587
        $parentThemeConfigs = $allThemeConfigs->filter(
588
            fn (StorefrontPluginConfiguration $parentConfig) => $this->isDependentTheme($parentConfig, $config)
589
        );
590
591
        $technicalNames = $parentThemeConfigs->map(
592
            fn (StorefrontPluginConfiguration $theme) => $theme->getTechnicalName()
593
        );
594
595
        $parentThemes = array_filter(
596
            $allThemes,
597
            fn (array $theme) => \in_array($theme['technicalName'], $technicalNames, true)
598
        );
599
600
        $updateParents = [];
601
        foreach ($parentThemes as $parentTheme) {
602
            $updateParents[] = [
603
                'parentId' => $parentTheme['parentThemeId'],
604
                'childId' => $id,
605
            ];
606
        }
607
608
        return $updateParents;
609
    }
610
611
    /**
612
     * @return list<array{technicalName: string, parentThemeId: string}>
613
     */
614
    private function getAllThemesPlain(): array
615
    {
616
        /** @var list<array{technicalName: string, parentThemeId: string}> $result */
617
        $result = $this->connection->fetchAllAssociative(
618
            'SELECT theme.technical_name as technicalName, LOWER(HEX(theme.id)) as parentThemeId FROM theme'
619
        );
620
621
        return $result;
622
    }
623
624
    private function isDependentTheme(
625
        StorefrontPluginConfiguration $parentConfig,
626
        StorefrontPluginConfiguration $currentThemeConfig
627
    ): bool {
628
        return $currentThemeConfig->getTechnicalName() !== $parentConfig->getTechnicalName()
629
            && \in_array('@' . $parentConfig->getTechnicalName(), $currentThemeConfig->getStyleFiles()->getFilepaths(), true)
630
        ;
631
    }
632
633
    /**
634
     * @param array<int|string, mixed> $field
635
     */
636
    private function hasNewMedia(array $field): bool
637
    {
638
        return \array_key_exists('type', $field) && $field['type'] === 'media'
639
            && \array_key_exists('value', $field) && \is_string($field['value']);
640
    }
641
642
    /**
643
     * @param array<int|string, mixed> $field
644
     */
645
    private function hasOldMedia(array $field): bool
646
    {
647
        return \array_key_exists('type', $field) && $field['type'] === 'media'
648
            && \array_key_exists('value', $field) && \is_string($field['value']) && Uuid::isValid($field['value']);
649
    }
650
}
651