Passed
Push — master ( 57b5b6...1bd009 )
by
unknown
47:04 queued 32:28
created

getBasePropertyPathFromMultiValuePropertyCollectionElement()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 17
rs 9.9666
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Form\Domain\Configuration;
19
20
use TYPO3\CMS\Core\Cache\CacheManager;
21
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
22
use TYPO3\CMS\Core\SingletonInterface;
23
use TYPO3\CMS\Core\Utility\ArrayUtility;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
use TYPO3\CMS\Extbase\Object\ObjectManager;
26
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
27
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
28
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
29
use TYPO3\CMS\Form\Domain\Configuration\Exception\PrototypeNotFoundException;
30
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
31
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AdditionalElementPropertyPathsExtractor;
32
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\ExtractorDto;
33
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\IsCreatableFormElementExtractor;
34
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\MultiValuePropertiesExtractor;
35
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PredefinedDefaultsExtractor;
36
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PropertyPathsExtractor;
37
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\SelectOptionsExtractor;
38
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\IsCreatablePropertyCollectionElementExtractor;
39
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\MultiValuePropertiesExtractor as CollectionMultiValuePropertiesExtractor;
40
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PredefinedDefaultsExtractor as CollectionPredefinedDefaultsExtractor;
41
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PropertyPathsExtractor as CollectionPropertyPathsExtractor;
42
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\SelectOptionsExtractor as CollectionSelectOptionsExtractor;
43
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
44
use TYPO3\CMS\Form\Service\TranslationService;
45
46
/**
47
 * Helper for configuration settings
48
 * Scope: frontend / backend
49
 */
50
class ConfigurationService implements SingletonInterface
51
{
52
53
    /**
54
     * @var array
55
     */
56
    protected $formSettings;
57
58
    /**
59
     * @var array
60
     */
61
    protected $firstLevelCache = [];
62
63
    /**
64
     * @var TranslationService
65
     */
66
    protected $translationService;
67
68
    /**
69
     * @internal
70
     */
71
    public function initializeObject(): void
72
    {
73
        $this->formSettings = GeneralUtility::makeInstance(ObjectManager::class)
74
            ->get(ConfigurationManagerInterface::class)
75
            ->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form');
76
    }
77
78
    /**
79
     * Get the prototype configuration
80
     *
81
     * @param string $prototypeName name of the prototype to get the configuration for
82
     * @return array the prototype configuration
83
     * @throws PrototypeNotFoundException if prototype with the name $prototypeName was not found
84
     */
85
    public function getPrototypeConfiguration(string $prototypeName): array
86
    {
87
        if (!isset($this->formSettings['prototypes'][$prototypeName])) {
88
            throw new PrototypeNotFoundException(
89
                sprintf('The Prototype "%s" was not found.', $prototypeName),
90
                1475924277
91
            );
92
        }
93
        return $this->formSettings['prototypes'][$prototypeName];
94
    }
95
96
    /**
97
     * Return all prototype names which are defined within "formManager.selectablePrototypesConfiguration.*.identifier"
98
     *
99
     * @return array
100
     * @internal
101
     */
102
    public function getSelectablePrototypeNamesDefinedInFormEditorSetup(): array
103
    {
104
        $returnValue = GeneralUtility::makeInstance(
105
            ArrayProcessor::class,
106
            $this->formSettings['formManager']['selectablePrototypesConfiguration'] ?? []
107
        )->forEach(
108
            GeneralUtility::makeInstance(
109
                ArrayProcessing::class,
110
                'selectablePrototypeNames',
111
                '^([\d]+)\.identifier$',
112
                function ($_, $value) {
113
                    return $value;
114
                }
115
            )
116
        );
117
118
        return array_values($returnValue['selectablePrototypeNames'] ?? []);
119
    }
120
121
    /**
122
     * Check if a form element property is defined in the form setup.
123
     * If a form element property is defined in the form setup then it
124
     * means that the form element property can be written by the form editor.
125
     * A form element property can be written if the property path is defined within
126
     * the following form editor properties:
127
     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath
128
     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.*.propertyPath
129
     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.additionalElementPropertyPaths
130
     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.additionalElementPropertyPaths
131
     * If a form editor property "templateName" is
132
     * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
133
     * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty".
134
     * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid.
135
     * If "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-PropertyGridEditor"
136
     * and
137
     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath = options.xxx"
138
     * then (for example) "options.xxx.yyy" is a valid property path to write.
139
     * If you use a custom form editor "inspector editor" implementation which does not define the writable
140
     * property paths by one of the above described inspector editor properties (e.g "propertyPath") within
141
     * the form setup, you must provide the writable property paths with a hook.
142
     *
143
     * @see executeBuildFormDefinitionValidationConfigurationHooks()
144
     * @param ValidationDto $dto
145
     * @return bool
146
     * @internal
147
     */
148
    public function isFormElementPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
149
    {
150
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
151
            $dto->getPrototypeName()
152
        );
153
154
        $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()] ?? [];
155
        return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
156
    }
157
158
    /**
159
     * Check if a form elements finisher|validator property is defined in the form setup.
160
     * If a form elements finisher|validator property is defined in the form setup then it
161
     * means that the form elements finisher|validator property can be written by the form editor.
162
     * A form elements finisher|validator property can be written if the property path is defined within
163
     * the following form editor properties:
164
     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath
165
     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.*.propertyPath
166
     * If a form elements finisher|validator property "templateName" is
167
     * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
168
     * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty".
169
     * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid.
170
     * If "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.templateName = Inspector-PropertyGridEditor"
171
     * and
172
     * "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath = options.xxx"
173
     * that (for example) "options.xxx.yyy" is a valid property path to write.
174
     * If you use a custom form editor "inspector editor" implementation which not defines the writable
175
     * property paths by one of the above described inspector editor properties (e.g "propertyPath") within
176
     * the form setup, you must provide the writable property paths with a hook.
177
     *
178
     * @see executeBuildFormDefinitionValidationConfigurationHooks()
179
     * @param ValidationDto $dto
180
     * @return bool
181
     * @internal
182
     */
183
    public function isPropertyCollectionPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
184
    {
185
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
186
            $dto->getPrototypeName()
187
        );
188
        $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()] ?? [];
189
190
        return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
191
    }
192
193
    /**
194
     * If a form element editor has a property called "selectOptions"
195
     * (e.g. editors with templateName "Inspector-SingleSelectEditor" or "Inspector-MultiSelectEditor")
196
     * then only the defined values within the selectOptions are allowed to be written
197
     * by the form editor.
198
     *
199
     * @param ValidationDto $dto
200
     * @return bool
201
     * @internal
202
     */
203
    public function formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup(
204
        ValidationDto $dto
205
    ): bool {
206
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
207
            $dto->getPrototypeName()
208
        );
209
210
        $propertyPath = $this->getBasePropertyPathFromMultiValueFormElementProperty($dto);
211
        return isset(
212
            $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['selectOptions'][$propertyPath]
213
        );
214
    }
215
216
    /**
217
     * Get the "selectOptions" value for a form element property from the form setup.
218
     *
219
     * @param ValidationDto $dto
220
     * @return array
221
     * @param bool $translated
222
     * @throws PropertyException
223
     * @internal
224
     */
225
    public function getAllowedValuesForFormElementPropertyFromFormEditorSetup(
226
        ValidationDto $dto,
227
        bool $translated = true
228
    ): array {
229
        if (!$this->formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) {
230
            throw new PropertyException(
231
                sprintf(
232
                    'No selectOptions found for form element type "%s" and property path "%s"',
233
                    $dto->getFormElementType(),
234
                    $dto->getPropertyPath()
235
                ),
236
                1614264312
237
            );
238
        }
239
240
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
241
            $dto->getPrototypeName()
242
        );
243
244
        $property = $translated ? 'selectOptions' : 'untranslatedSelectOptions';
245
        $propertyPath = $this->getBasePropertyPathFromMultiValueFormElementProperty($dto);
246
        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$propertyPath];
247
    }
248
249
    /**
250
     * If a form elements finisher|validator editor has a property called "selectOptions"
251
     * (e.g. editors with templateName "Inspector-SingleSelectEditor" or "Inspector-MultiSelectEditor")
252
     * then only the defined values within the selectOptions are allowed to be written
253
     * by the form editor.
254
     *
255
     * @param ValidationDto $dto
256
     * @return bool
257
     * @internal
258
     */
259
    public function propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup(
260
        ValidationDto $dto
261
    ): bool {
262
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
263
            $dto->getPrototypeName()
264
        );
265
266
        $propertyPath = $this->getBasePropertyPathFromMultiValuePropertyCollectionElement($dto);
267
        return isset(
268
            $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['selectOptions'][$propertyPath]
269
        );
270
    }
271
272
    /**
273
     * Get the "selectOptions" value for a form elements finisher|validator property from the form setup.
274
     *
275
     * @param ValidationDto $dto
276
     * @param bool $translated
277
     * @return array
278
     * @throws PropertyException
279
     * @internal
280
     */
281
    public function getAllowedValuesForPropertyCollectionPropertyFromFormEditorSetup(
282
        ValidationDto $dto,
283
        bool $translated = true
284
    ): array {
285
        if (!$this->propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) {
286
            throw new PropertyException(
287
                sprintf(
288
                    'No selectOptions found for property collection "%s" and identifier "%s" and property path "%s"',
289
                    $dto->getPropertyCollectionName(),
290
                    $dto->getPropertyCollectionElementIdentifier(),
291
                    $dto->getPropertyPath()
292
                ),
293
                1614264313
294
            );
295
        }
296
297
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
298
            $dto->getPrototypeName()
299
        );
300
301
        $property = $translated ? 'selectOptions' : 'untranslatedSelectOptions';
302
        $propertyPath = $this->getBasePropertyPathFromMultiValuePropertyCollectionElement($dto);
303
        return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$propertyPath];
304
    }
305
306
    /**
307
     * @param ValidationDto $dto
308
     * @return string
309
     */
310
    protected function getBasePropertyPathFromMultiValueFormElementProperty(
311
        ValidationDto $dto
312
    ): string {
313
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
314
            $dto->getPrototypeName()
315
        );
316
317
        $propertyPath = $dto->getPropertyPath();
318
        $multiValueProperties = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['multiValueProperties'] ?? [];
319
        foreach ($multiValueProperties as $multiValueProperty) {
320
            if (strpos($propertyPath, $multiValueProperty) === 0) {
321
                $propertyPath = $multiValueProperty;
322
                continue;
323
            }
324
        }
325
326
        return $propertyPath;
327
    }
328
329
    /**
330
     * @param ValidationDto $dto
331
     * @return string
332
     */
333
    protected function getBasePropertyPathFromMultiValuePropertyCollectionElement(
334
        ValidationDto $dto
335
    ): string {
336
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
337
            $dto->getPrototypeName()
338
        );
339
340
        $propertyPath = $dto->getPropertyPath();
341
        $multiValueProperties = $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['multiValueProperties'] ?? [];
342
        foreach ($multiValueProperties as $multiValueProperty) {
343
            if (strpos($propertyPath, $multiValueProperty) === 0) {
344
                $propertyPath = $multiValueProperty;
345
                continue;
346
            }
347
        }
348
349
        return $propertyPath;
350
    }
351
352
    /**
353
     * Check if a form element property is defined in "predefinedDefaults" in the form setup.
354
     * If a form element property is defined in the "predefinedDefaults" in the form setup then it
355
     * means that the form element property can be written by the form editor.
356
     * A form element default property is defined within the following form editor properties:
357
     * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
358
     *
359
     * @param ValidationDto $dto
360
     * @return bool
361
     * @internal
362
     */
363
    public function isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
364
        ValidationDto $dto
365
    ): bool {
366
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
367
            $dto->getPrototypeName()
368
        );
369
        return isset(
370
            $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()]
371
        );
372
    }
373
374
    /**
375
     * Get the "predefinedDefaults" value for a form element property from the form setup.
376
     * A form element default property is defined within the following form editor properties:
377
     * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
378
     *
379
     * @param ValidationDto $dto
380
     * @param bool $translated
381
     * @return mixed
382
     * @throws PropertyException
383
     * @internal
384
     */
385
    public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true)
386
    {
387
        if (!$this->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
388
            throw new PropertyException(
389
                sprintf(
390
                    'No predefinedDefaults found for form element type "%s" and property path "%s"',
391
                    $dto->getFormElementType(),
392
                    $dto->getPropertyPath()
393
                ),
394
                1528578401
395
            );
396
        }
397
398
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
399
            $dto->getPrototypeName()
400
        );
401
402
        $property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults';
403
        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$dto->getPropertyPath()];
404
    }
405
406
    /**
407
     * Check if a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup.
408
     * If a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup then it
409
     * means that the form elements finisher|validator property can be written by the form editor.
410
     * A form elements finisher|validator default property is defined within the following form editor properties:
411
     * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
412
     *
413
     * @param ValidationDto $dto
414
     * @return bool
415
     * @internal
416
     */
417
    public function isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
418
        ValidationDto $dto
419
    ): bool {
420
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
421
            $dto->getPrototypeName()
422
        );
423
        return isset(
424
            $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()]
425
        );
426
    }
427
428
    /**
429
     * Get the "predefinedDefaults" value for a form elements finisher|validator property from the form setup.
430
     * A form elements finisher|validator default property is defined within the following form editor properties:
431
     * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
432
     *
433
     * @param ValidationDto $dto
434
     * @param bool $translated
435
     * @return mixed
436
     * @throws PropertyException
437
     * @internal
438
     */
439
    public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true)
440
    {
441
        if (!$this->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
442
            throw new PropertyException(
443
                sprintf(
444
                    'No predefinedDefaults found for property collection "%s" and identifier "%s" and property path "%s"',
445
                    $dto->getPropertyCollectionName(),
446
                    $dto->getPropertyCollectionElementIdentifier(),
447
                    $dto->getPropertyPath()
448
                ),
449
                1528578402
450
            );
451
        }
452
453
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
454
            $dto->getPrototypeName()
455
        );
456
457
        $property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults';
458
        return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$dto->getPropertyPath()];
459
    }
460
461
    /**
462
     * Check if the form element is creatable through the form editor.
463
     * A form element is creatable if the following properties are set:
464
     *  * formElementsDefinition.<formElementType>.formEditor.group
465
     *  * formElementsDefinition.<formElementType>.formEditor.groupSorting
466
     * And the value from "formElementsDefinition.<formElementType>.formEditor.group" is
467
     * one of the keys within "formEditor.formElementGroups"
468
     *
469
     * @param ValidationDto $dto
470
     * @return bool
471
     * @internal
472
     */
473
    public function isFormElementTypeCreatableByFormEditor(ValidationDto $dto): bool
474
    {
475
        if ($dto->getFormElementType() === 'Form') {
476
            return true;
477
        }
478
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
479
            $dto->getPrototypeName()
480
        );
481
        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['creatable'] ?? false;
482
    }
483
484
    /**
485
     * Check if the form elements finisher|validator is creatable through the form editor.
486
     * A form elements finisher|validator is creatable if the following conditions are true:
487
     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-FinishersEditor"
488
     * or
489
     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-ValidatorsEditor"
490
     * and
491
     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.selectOptions.<index>.value = <finisherIdentifier|validatorIdentifier>"
492
     *
493
     * @param ValidationDto $dto
494
     * @return bool
495
     * @internal
496
     */
497
    public function isPropertyCollectionElementIdentifierCreatableByFormEditor(ValidationDto $dto): bool
498
    {
499
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
500
            $dto->getPrototypeName()
501
        );
502
        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['creatable'] ?? false;
503
    }
504
505
    /**
506
     * Check if the form elements type is defined within the form setup.
507
     *
508
     * @param ValidationDto $dto
509
     * @return bool
510
     * @internal
511
     */
512
    public function isFormElementTypeDefinedInFormSetup(ValidationDto $dto): bool
513
    {
514
        $prototypeConfiguration = $this->getPrototypeConfiguration($dto->getPrototypeName());
515
        return ArrayUtility::isValidPath(
516
            $prototypeConfiguration,
517
            'formElementsDefinition.' . $dto->getFormElementType(),
518
            '.'
519
        );
520
    }
521
522
    /**
523
     * @param array $keys
524
     * @param string $prototypeName
525
     * @return array
526
     * @internal
527
     */
528
    public function getAllBackendTranslationsForTranslationKeys(array $keys, string $prototypeName): array
529
    {
530
        $translations = [];
531
        foreach ($keys as $key) {
532
            if (!is_string($key)) {
533
                continue;
534
            }
535
536
            $translations[$key] = $this->getAllBackendTranslationsForTranslationKey($key, $prototypeName);
537
        }
538
539
        return $translations;
540
    }
541
542
    /**
543
     * @param string $key
544
     * @param string $prototypeName
545
     * @return array
546
     */
547
    public function getAllBackendTranslationsForTranslationKey(string $key, string $prototypeName): array
548
    {
549
        $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
550
551
        return $this->getTranslationService()->translateToAllBackendLanguages(
552
            $key,
553
            [],
554
            $prototypeConfiguration['formEditor']['translationFiles'] ?? []
555
        );
556
    }
557
558
    /**
559
     * Collect all the form editor configurations which are needed to check if a
560
     * form definition property can be written or not.
561
     *
562
     * @param string $prototypeName
563
     * @return array
564
     */
565
    protected function buildFormDefinitionValidationConfigurationFromFormEditorSetup(string $prototypeName): array
566
    {
567
        $cacheKey = implode('_', ['buildFormDefinitionValidationConfigurationFromFormEditorSetup', $prototypeName]);
568
        $configuration = $this->getCacheEntry($cacheKey);
569
570
        if ($configuration === null) {
571
            $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
572
            $extractorDto = GeneralUtility::makeInstance(ExtractorDto::class, $prototypeConfiguration);
573
574
            GeneralUtility::makeInstance(ArrayProcessor::class, $prototypeConfiguration)->forEach(
575
                GeneralUtility::makeInstance(
576
                    ArrayProcessing::class,
577
                    'formElementPropertyPaths',
578
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
579
                    GeneralUtility::makeInstance(PropertyPathsExtractor::class, $extractorDto)
580
                ),
581
                GeneralUtility::makeInstance(
582
                    ArrayProcessing::class,
583
                    'formElementAdditionalElementPropertyPaths',
584
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
585
                    GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
586
                ),
587
                GeneralUtility::makeInstance(
588
                    ArrayProcessing::class,
589
                    'formElementRelativeMultiValueProperties',
590
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
591
                    GeneralUtility::makeInstance(MultiValuePropertiesExtractor::class, $extractorDto)
592
                ),
593
                GeneralUtility::makeInstance(
594
                    ArrayProcessing::class,
595
                    'formElementSelectOptions',
596
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.selectOptions\.([\d]+)\.value$',
597
                    GeneralUtility::makeInstance(SelectOptionsExtractor::class, $extractorDto)
598
                ),
599
                GeneralUtility::makeInstance(
600
                    ArrayProcessing::class,
601
                    'formElementPredefinedDefaults',
602
                    '^formElementsDefinition\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
603
                    GeneralUtility::makeInstance(PredefinedDefaultsExtractor::class, $extractorDto)
604
                ),
605
                GeneralUtility::makeInstance(
606
                    ArrayProcessing::class,
607
                    'formElementCreatable',
608
                    '^formElementsDefinition\.(.*)\.formEditor.group$',
609
                    GeneralUtility::makeInstance(IsCreatableFormElementExtractor::class, $extractorDto)
610
                ),
611
                GeneralUtility::makeInstance(
612
                    ArrayProcessing::class,
613
                    'propertyCollectionCreatable',
614
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
615
                    GeneralUtility::makeInstance(IsCreatablePropertyCollectionElementExtractor::class, $extractorDto)
616
                ),
617
                GeneralUtility::makeInstance(
618
                    ArrayProcessing::class,
619
                    'propertyCollectionPropertyPaths',
620
                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
621
                    GeneralUtility::makeInstance(CollectionPropertyPathsExtractor::class, $extractorDto)
622
                ),
623
                GeneralUtility::makeInstance(
624
                    ArrayProcessing::class,
625
                    'propertyCollectionAdditionalElementPropertyPaths',
626
                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
627
                    GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
628
                ),
629
                GeneralUtility::makeInstance(
630
                    ArrayProcessing::class,
631
                    'propertyCollectionRelativeMultiValueProperties',
632
                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.templateName$',
633
                    GeneralUtility::makeInstance(CollectionMultiValuePropertiesExtractor::class, $extractorDto)
634
                ),
635
                GeneralUtility::makeInstance(
636
                    ArrayProcessing::class,
637
                    'propertyCollectionSelectOptions',
638
                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.selectOptions\.([\d]+)\.value$',
639
                    GeneralUtility::makeInstance(CollectionSelectOptionsExtractor::class, $extractorDto)
640
                ),
641
                GeneralUtility::makeInstance(
642
                    ArrayProcessing::class,
643
                    'propertyCollectionPredefinedDefaults',
644
                    '^(validatorsDefinition|finishersDefinition)\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
645
                    GeneralUtility::makeInstance(CollectionPredefinedDefaultsExtractor::class, $extractorDto)
646
                )
647
            );
648
            $configuration = $extractorDto->getResult();
649
650
            $configuration = $this->translateValues($prototypeConfiguration, $configuration);
651
652
            $configuration = $this->executeBuildFormDefinitionValidationConfigurationHooks(
653
                $prototypeName,
654
                $configuration
655
            );
656
657
            $this->setCacheEntry($cacheKey, $configuration);
658
        }
659
660
        return $configuration;
661
    }
662
663
    /**
664
     * If you use a custom form editor "inspector editor" implementation which does not define the writable
665
     * property paths by one of the described inspector editor properties (e.g "propertyPath") within
666
     * the form setup, you must provide the writable property paths with a hook.
667
     *
668
     * @see isFormElementPropertyDefinedInFormEditorSetup()
669
     * @see isPropertyCollectionPropertyDefinedInFormEditorSetup()
670
     * Connect to the hook:
671
     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'][] = \Vendor\YourNamespace\YourClass::class;
672
     * Use the hook:
673
     * public function addAdditionalPropertyPaths(\TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto): array
674
     * {
675
     *     $textValidationDto = $validationDto->withFormElementType('Text');
676
     *     $textValidatorsValidationDto = $textValidationDto->withPropertyCollectionName('validators');
677
     *     $dateValidationDto = $validationDto->withFormElementType('Date');
678
     *     $propertyPaths = [
679
     *         $textValidationDto->withPropertyPath('properties.my.custom.property'),
680
     *         $textValidationDto->withPropertyPath('properties.my.other.custom.property'),
681
     *         $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('StringLength')->withPropertyPath('options.custom.property'),
682
     *         $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('CustomValidator')->withPropertyPath('options.other.custom.property'),
683
     *         $dateValidationDto->withPropertyPath('properties.custom.property'),
684
     *         // ..
685
     *     ];
686
     *     return $propertyPaths;
687
     * }
688
     * @param string $prototypeName
689
     * @param array $configuration
690
     * @return array
691
     * @throws PropertyException
692
     */
693
    protected function executeBuildFormDefinitionValidationConfigurationHooks(
694
        string $prototypeName,
695
        array $configuration
696
    ): array {
697
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'] ?? [] as $className) {
698
            $hookObj = GeneralUtility::makeInstance($className);
699
            if (method_exists($hookObj, 'addAdditionalPropertyPaths')) {
700
                $validationDto = GeneralUtility::makeInstance(ValidationDto::class, $prototypeName);
701
                $propertyPathsFromHook = $hookObj->addAdditionalPropertyPaths($validationDto);
702
                if (!is_array($propertyPathsFromHook)) {
703
                    $message = 'Return value of "%s->addAdditionalPropertyPaths() must be type "array"';
704
                    throw new PropertyException(sprintf($message, $className), 1528633965);
705
                }
706
                $configuration = $this->addAdditionalPropertyPathsFromHook(
707
                    $className,
708
                    $prototypeName,
709
                    $propertyPathsFromHook,
710
                    $configuration
711
                );
712
            }
713
        }
714
715
        return $configuration;
716
    }
717
718
    /**
719
     * @param string $hookClassName
720
     * @param string $prototypeName
721
     * @param array $propertyPathsFromHook
722
     * @param array $configuration
723
     * @return array
724
     * @throws PropertyException
725
     */
726
    protected function addAdditionalPropertyPathsFromHook(
727
        string $hookClassName,
728
        string $prototypeName,
729
        array $propertyPathsFromHook,
730
        array $configuration
731
    ): array {
732
        foreach ($propertyPathsFromHook as $index => $validationDto) {
733
            if (!($validationDto instanceof ValidationDto)) {
734
                $message = 'Return value of "%s->addAdditionalPropertyPaths()[%s] must be an instance of "%s"';
735
                throw new PropertyException(
736
                    sprintf($message, $hookClassName, $index, ValidationDto::class),
737
                    1528633966
738
                );
739
            }
740
741
            if ($validationDto->getPrototypeName() !== $prototypeName) {
742
                $message = 'The prototype name "%s" does not match "%s" on "%s->addAdditionalPropertyPaths()[%s]';
743
                throw new PropertyException(
744
                    sprintf(
745
                        $message,
746
                        $validationDto->getPrototypeName(),
747
                        $prototypeName,
748
                        $hookClassName,
749
                        $index,
750
                        ValidationDto::class
751
                    ),
752
                    1528634966
753
                );
754
            }
755
756
            $formElementType = $validationDto->getFormElementType();
757
            if (!$this->isFormElementTypeDefinedInFormSetup($validationDto)) {
758
                $message = 'Form element type "%s" does not exist in prototype configuration "%s"';
759
                throw new PropertyException(
760
                    sprintf($message, $formElementType, $validationDto->getPrototypeName()),
761
                    1528633967
762
                );
763
            }
764
765
            if ($validationDto->hasPropertyCollectionName() &&
766
                $validationDto->hasPropertyCollectionElementIdentifier()) {
767
                $propertyCollectionName = $validationDto->getPropertyCollectionName();
768
                $propertyCollectionElementIdentifier = $validationDto->getPropertyCollectionElementIdentifier();
769
770
                if ($propertyCollectionName !== 'finishers' && $propertyCollectionName !== 'validators') {
771
                    $message = 'The property collection name "%s" for form element "%s" must be "finishers" or "validators"';
772
                    throw new PropertyException(
773
                        sprintf($message, $propertyCollectionName, $formElementType),
774
                        1528636941
775
                    );
776
                }
777
778
                $configuration['formElements'][$formElementType]['collections'][$propertyCollectionName][$propertyCollectionElementIdentifier]['additionalPropertyPaths'][]
779
                    = $validationDto->getPropertyPath();
780
            } else {
781
                $configuration['formElements'][$formElementType]['additionalPropertyPaths'][]
782
                    = $validationDto->getPropertyPath();
783
            }
784
        }
785
786
        return $configuration;
787
    }
788
789
    /**
790
     * @param string $propertyPath
791
     * @param array $subConfig
792
     * @return bool
793
     */
794
    protected function isPropertyDefinedInFormEditorSetup(string $propertyPath, array $subConfig): bool
795
    {
796
        if (empty($subConfig)) {
797
            return false;
798
        }
799
        if (
800
            in_array($propertyPath, $subConfig['propertyPaths'] ?? [], true)
801
            || in_array($propertyPath, $subConfig['additionalElementPropertyPaths'] ?? [], true)
802
            || in_array($propertyPath, $subConfig['additionalPropertyPaths'] ?? [], true)
803
        ) {
804
            return true;
805
        }
806
        foreach ($subConfig['multiValueProperties'] ?? [] as $relativeMultiValueProperty) {
807
            if (strpos($propertyPath, $relativeMultiValueProperty) === 0) {
808
                return true;
809
            }
810
        }
811
812
        return false;
813
    }
814
815
    /**
816
     * @param array $prototypeConfiguration
817
     * @param array $configuration
818
     * @return array
819
     */
820
    protected function translateValues(array $prototypeConfiguration, array $configuration): array
821
    {
822
        if (isset($configuration['formElements'])) {
823
            $configuration['formElements'] = $this->translatePredefinedDefaults(
824
                $prototypeConfiguration,
825
                $configuration['formElements']
826
            );
827
828
            $configuration['formElements'] = $this->translateSelectOptions(
829
                $prototypeConfiguration,
830
                $configuration['formElements']
831
            );
832
        }
833
834
        foreach ($configuration['collections'] ?? [] as $name => $collections) {
835
            $configuration['collections'][$name] = $this->translatePredefinedDefaults($prototypeConfiguration, $collections);
836
            $configuration['collections'][$name] = $this->translateSelectOptions($prototypeConfiguration, $configuration['collections'][$name]);
837
        }
838
        return $configuration;
839
    }
840
841
    /**
842
     * @param array $prototypeConfiguration
843
     * @param array $formElements
844
     * @return array
845
     */
846
    protected function translatePredefinedDefaults(array $prototypeConfiguration, array $formElements): array
847
    {
848
        foreach ($formElements ?? [] as $name => $formElement) {
849
            if (!isset($formElement['predefinedDefaults'])) {
850
                continue;
851
            }
852
            $formElement['untranslatedPredefinedDefaults'] = $formElement['predefinedDefaults'];
853
            $formElement['predefinedDefaults'] = $this->getTranslationService()->translateValuesRecursive(
854
                $formElement['predefinedDefaults'],
855
                $prototypeConfiguration['formEditor']['translationFiles'] ?? []
856
            );
857
            $formElements[$name] = $formElement;
858
        }
859
        return $formElements;
860
    }
861
862
    /**
863
     * @param array $prototypeConfiguration
864
     * @param array $formElements
865
     * @return array
866
     */
867
    protected function translateSelectOptions(array $prototypeConfiguration, array $formElements): array
868
    {
869
        foreach ($formElements ?? [] as $name => $formElement) {
870
            if (empty($formElement['selectOptions']) || !is_array($formElement['selectOptions'])) {
871
                continue;
872
            }
873
            $formElement['untranslatedSelectOptions'] = $formElement['selectOptions'];
874
875
            $formElement['selectOptions'] = $this->getTranslationService()->translateValuesRecursive(
876
                $formElement['selectOptions'],
877
                $prototypeConfiguration['formEditor']['translationFiles'] ?? []
878
            );
879
            $formElements[$name] = $formElement;
880
        }
881
        return $formElements;
882
    }
883
884
    /**
885
     * @param string $cacheKey
886
     * @return mixed
887
     */
888
    protected function getCacheEntry(string $cacheKey)
889
    {
890
        if (isset($this->firstLevelCache[$cacheKey])) {
891
            return $this->firstLevelCache[$cacheKey];
892
        }
893
        $cacheValue = $this->getCacheFrontend()->get('form_' . $cacheKey);
894
        return $cacheValue === false ? null : $cacheValue;
895
    }
896
897
    /**
898
     * @param string $cacheKey
899
     * @param mixed $value
900
     */
901
    protected function setCacheEntry(string $cacheKey, $value): void
902
    {
903
        $this->getCacheFrontend()->set('form_' . $cacheKey, $value);
904
        $this->firstLevelCache[$cacheKey] = $value;
905
    }
906
907
    /**
908
     * @return TranslationService
909
     */
910
    protected function getTranslationService(): TranslationService
911
    {
912
        return $this->translationService instanceof TranslationService
0 ignored issues
show
introduced by
$this->translationService is always a sub-type of TYPO3\CMS\Form\Service\TranslationService.
Loading history...
913
            ? $this->translationService
914
            : TranslationService::getInstance();
915
    }
916
917
    /**
918
     * @return FrontendInterface
919
     */
920
    protected function getCacheFrontend(): FrontendInterface
921
    {
922
        return GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
923
    }
924
}
925