[FEATURE] Introduce conditional variants for form elements
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Domain / Model / Renderable / AbstractRenderable.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Form\Domain\Model\Renderable;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It originated from the Neos.Form package (www.neos.io)
9 *
10 * It is free software; you can redistribute it and/or modify it under
11 * the terms of the GNU General Public License, either version 2
12 * of the License, or any later version.
13 *
14 * For the full copyright and license information, please read the
15 * LICENSE.txt file that was distributed with this source code.
16 *
17 * The TYPO3 project - inspiring people to share!
18 */
19
20 use TYPO3\CMS\Core\Cache\CacheManager;
21 use TYPO3\CMS\Core\Utility\ArrayUtility;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Extbase\Object\ObjectManager;
24 use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
25 use TYPO3\CMS\Form\Domain\Model\Exception\FormDefinitionConsistencyException;
26 use TYPO3\CMS\Form\Domain\Model\Exception\ValidatorPresetNotFoundException;
27 use TYPO3\CMS\Form\Domain\Model\FormDefinition;
28
29 /**
30 * Convenience base class which implements common functionality for most
31 * classes which implement RenderableInterface.
32 *
33 * Scope: frontend
34 * **This class is NOT meant to be sub classed by developers.**
35 * @internal
36 */
37 abstract class AbstractRenderable implements RenderableInterface, VariableRenderableInterface
38 {
39
40 /**
41 * Abstract "type" of this Renderable. Is used during the rendering process
42 * to determine the template file or the View PHP class being used to render
43 * the particular element.
44 *
45 * @var string
46 */
47 protected $type;
48
49 /**
50 * The identifier of this renderable
51 *
52 * @var string
53 */
54 protected $identifier;
55
56 /**
57 * The parent renderable
58 *
59 * @var CompositeRenderableInterface
60 */
61 protected $parentRenderable;
62
63 /**
64 * The label of this renderable
65 *
66 * @var string
67 */
68 protected $label = '';
69
70 /**
71 * associative array of rendering options
72 *
73 * @var array
74 */
75 protected $renderingOptions = [];
76
77 /**
78 * The position of this renderable inside the parent renderable.
79 *
80 * @var int
81 */
82 protected $index = 0;
83
84 /**
85 * The name of the template file of the renderable.
86 *
87 * @var string
88 */
89 protected $templateName = '';
90
91 /**
92 * associative array of rendering variants
93 *
94 * @var array
95 */
96 protected $variants = [];
97
98 /**
99 * Get the type of the renderable
100 *
101 * @return string
102 * @api
103 */
104 public function getType(): string
105 {
106 return $this->type;
107 }
108
109 /**
110 * Get the identifier of the element
111 *
112 * @return string
113 * @api
114 */
115 public function getIdentifier(): string
116 {
117 return $this->identifier;
118 }
119
120 /**
121 * Set the identifier of the element
122 *
123 * @param string $identifier
124 * @api
125 */
126 public function setIdentifier(string $identifier)
127 {
128 $this->identifier = $identifier;
129 }
130
131 /**
132 * Set multiple properties of this object at once.
133 * Every property which has a corresponding set* method can be set using
134 * the passed $options array.
135 *
136 * @param array $options
137 * @param bool $resetValidators
138 * @api
139 */
140 public function setOptions(array $options, bool $resetValidators = false)
141 {
142 if (isset($options['label'])) {
143 $this->setLabel($options['label']);
144 }
145
146 if (isset($options['defaultValue'])) {
147 $this->setDefaultValue($options['defaultValue']);
148 }
149
150 if (isset($options['properties'])) {
151 foreach ($options['properties'] as $key => $value) {
152 $this->setProperty($key, $value);
153 }
154 }
155
156 if (isset($options['renderingOptions'])) {
157 foreach ($options['renderingOptions'] as $key => $value) {
158 $this->setRenderingOption($key, $value);
159 }
160 }
161
162 if (isset($options['validators'])) {
163 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
164 $configurationHashes = $runtimeCache->get('formAbstractRenderableConfigurationHashes') ?: [];
165
166 if ($resetValidators) {
167 $processingRule = $this->getRootForm()->getProcessingRule($this->getIdentifier());
168 foreach ($this->getValidators() as $validator) {
169 $processingRule->removeValidator($validator);
170 }
171 $configurationHashes = [];
172 }
173
174 foreach ($options['validators'] as $validatorConfiguration) {
175 $configurationHash = md5($this->getIdentifier() . json_encode($validatorConfiguration));
176 if (in_array($configurationHash, $configurationHashes)) {
177 continue;
178 }
179 $this->createValidator($validatorConfiguration['identifier'], $validatorConfiguration['options'] ?? []);
180 $configurationHashes[] = $configurationHash;
181 $runtimeCache->set('formAbstractRenderableConfigurationHashes', $configurationHashes);
182 }
183 }
184
185 if (isset($options['variants'])) {
186 foreach ($options['variants'] as $variantConfiguration) {
187 $this->createVariant($variantConfiguration);
188 }
189 }
190
191 ArrayUtility::assertAllArrayKeysAreValid(
192 $options,
193 ['label', 'defaultValue', 'properties', 'renderingOptions', 'validators', 'formEditor', 'variants']
194 );
195 }
196
197 /**
198 * Create a validator for the element
199 *
200 * @param string $validatorIdentifier
201 * @param array $options
202 * @return mixed
203 * @throws ValidatorPresetNotFoundException
204 * @api
205 */
206 public function createValidator(string $validatorIdentifier, array $options = [])
207 {
208 $validatorsDefinition = $this->getRootForm()->getValidatorsDefinition();
209 if (isset($validatorsDefinition[$validatorIdentifier]) && is_array($validatorsDefinition[$validatorIdentifier]) && isset($validatorsDefinition[$validatorIdentifier]['implementationClassName'])) {
210 $implementationClassName = $validatorsDefinition[$validatorIdentifier]['implementationClassName'];
211 $defaultOptions = $validatorsDefinition[$validatorIdentifier]['options'] ?? [];
212
213 ArrayUtility::mergeRecursiveWithOverrule($defaultOptions, $options);
214
215 $validator = GeneralUtility::makeInstance(ObjectManager::class)
216 ->get($implementationClassName, $defaultOptions);
217 $this->addValidator($validator);
218 return $validator;
219 }
220 throw new ValidatorPresetNotFoundException('The validator preset identified by "' . $validatorIdentifier . '" could not be found, or the implementationClassName was not specified.', 1328710202);
221 }
222
223 /**
224 * Add a validator to the element
225 *
226 * @param ValidatorInterface $validator
227 * @api
228 */
229 public function addValidator(ValidatorInterface $validator)
230 {
231 $formDefinition = $this->getRootForm();
232 $formDefinition->getProcessingRule($this->getIdentifier())->addValidator($validator);
233 }
234
235 /**
236 * Get all validators on the element
237 *
238 * @return \SplObjectStorage
239 * @internal
240 */
241 public function getValidators(): \SplObjectStorage
242 {
243 $formDefinition = $this->getRootForm();
244 return $formDefinition->getProcessingRule($this->getIdentifier())->getValidators();
245 }
246
247 /**
248 * Set the datatype
249 *
250 * @param string $dataType
251 * @api
252 */
253 public function setDataType(string $dataType)
254 {
255 $formDefinition = $this->getRootForm();
256 $formDefinition->getProcessingRule($this->getIdentifier())->setDataType($dataType);
257 }
258
259 /**
260 * Get the classname of the renderer
261 *
262 * @return string
263 * @api
264 */
265 public function getRendererClassName(): string
266 {
267 return $this->getRootForm()->getRendererClassName();
268 }
269
270 /**
271 * Get all rendering options
272 *
273 * @return array
274 * @api
275 */
276 public function getRenderingOptions(): array
277 {
278 return $this->renderingOptions;
279 }
280
281 /**
282 * Set the rendering option $key to $value.
283 *
284 * @param string $key
285 * @param mixed $value
286 * @return mixed
287 * @api
288 */
289 public function setRenderingOption(string $key, $value)
290 {
291 if (is_array($value) && isset($this->renderingOptions[$key]) && is_array($this->renderingOptions[$key])) {
292 ArrayUtility::mergeRecursiveWithOverrule($this->renderingOptions[$key], $value);
293 $this->renderingOptions[$key] = ArrayUtility::removeNullValuesRecursive($this->renderingOptions[$key]);
294 } elseif ($value === null) {
295 unset($this->renderingOptions[$key]);
296 } else {
297 $this->renderingOptions[$key] = $value;
298 }
299 }
300
301 /**
302 * Get the parent renderable
303 *
304 * @return CompositeRenderableInterface|null
305 * @api
306 */
307 public function getParentRenderable()
308 {
309 return $this->parentRenderable;
310 }
311
312 /**
313 * Set the parent renderable
314 *
315 * @param CompositeRenderableInterface $parentRenderable
316 * @api
317 */
318 public function setParentRenderable(CompositeRenderableInterface $parentRenderable)
319 {
320 $this->parentRenderable = $parentRenderable;
321 $this->registerInFormIfPossible();
322 }
323
324 /**
325 * Get the root form this element belongs to
326 *
327 * @return FormDefinition
328 * @throws FormDefinitionConsistencyException
329 * @api
330 */
331 public function getRootForm(): FormDefinition
332 {
333 $rootRenderable = $this->parentRenderable;
334 while ($rootRenderable !== null && !($rootRenderable instanceof FormDefinition)) {
335 $rootRenderable = $rootRenderable->getParentRenderable();
336 }
337 if ($rootRenderable === null) {
338 throw new FormDefinitionConsistencyException(sprintf('The form element "%s" is not attached to a parent form.', $this->identifier), 1326803398);
339 }
340
341 return $rootRenderable;
342 }
343
344 /**
345 * Register this element at the parent form, if there is a connection to the parent form.
346 *
347 * @internal
348 */
349 public function registerInFormIfPossible()
350 {
351 try {
352 $rootForm = $this->getRootForm();
353 $rootForm->registerRenderable($this);
354 } catch (FormDefinitionConsistencyException $exception) {
355 }
356 }
357
358 /**
359 * Triggered when the renderable is removed from it's parent
360 *
361 * @internal
362 */
363 public function onRemoveFromParentRenderable()
364 {
365 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRemoveFromParentRenderable'] ?? [] as $className) {
366 $hookObj = GeneralUtility::makeInstance($className);
367 if (method_exists($hookObj, 'beforeRemoveFromParentRenderable')) {
368 $hookObj->beforeRemoveFromParentRenderable(
369 $this
370 );
371 }
372 }
373
374 try {
375 $rootForm = $this->getRootForm();
376 $rootForm->unregisterRenderable($this);
377 } catch (FormDefinitionConsistencyException $exception) {
378 }
379 $this->parentRenderable = null;
380 }
381
382 /**
383 * Get the index of the renderable
384 *
385 * @return int
386 * @internal
387 */
388 public function getIndex(): int
389 {
390 return $this->index;
391 }
392
393 /**
394 * Set the index of the renderable
395 *
396 * @param int $index
397 * @internal
398 */
399 public function setIndex(int $index)
400 {
401 $this->index = $index;
402 }
403
404 /**
405 * Get the label of the renderable
406 *
407 * @return string
408 * @api
409 */
410 public function getLabel(): string
411 {
412 return $this->label;
413 }
414
415 /**
416 * Set the label which shall be displayed next to the form element
417 *
418 * @param string $label
419 * @api
420 */
421 public function setLabel(string $label)
422 {
423 $this->label = $label;
424 }
425
426 /**
427 * Get the templateName name of the renderable
428 *
429 * @return string
430 * @api
431 */
432 public function getTemplateName(): string
433 {
434 return empty($this->renderingOptions['templateName'])
435 ? $this->type
436 : $this->renderingOptions['templateName'];
437 }
438
439 /**
440 * Returns whether this renderable is enabled
441 *
442 * @return bool
443 */
444 public function isEnabled(): bool
445 {
446 return !isset($this->renderingOptions['enabled']) || (bool)$this->renderingOptions['enabled'] === true;
447 }
448
449 /**
450 * Get all rendering variants
451 *
452 * @return RenderableVariantInterface[]
453 * @api
454 */
455 public function getVariants(): array
456 {
457 return $this->variants;
458 }
459
460 /**
461 * @param array $options
462 * @return RenderableVariantInterface
463 * @api
464 */
465 public function createVariant(array $options): RenderableVariantInterface
466 {
467 $identifier = $options['identifier'] ?? '';
468 unset($options['identifier']);
469
470 $variant = GeneralUtility::makeInstance(ObjectManager::class)
471 ->get(RenderableVariant::class, $identifier, $options, $this);
472
473 $this->addVariant($variant);
474 return $variant;
475 }
476
477 /**
478 * Adds the specified variant to this form element
479 *
480 * @param RenderableVariantInterface $variant
481 * @api
482 */
483 public function addVariant(RenderableVariantInterface $variant)
484 {
485 $this->variants[$variant->getIdentifier()] = $variant;
486 }
487
488 /**
489 * Apply the specified variant to this form element
490 * regardless of their conditions
491 *
492 * @param RenderableVariantInterface $variant
493 * @api
494 */
495 public function applyVariant(RenderableVariantInterface $variant)
496 {
497 $variant->apply();
498 }
499 }