ThemeLifecycleService::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 12
dl 0
loc 14
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\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