Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

FormField   F

Complexity

Total Complexity 125

Size/Duplication

Total Lines 1563
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1563
rs 0.6314
c 0
b 0
f 0
wmc 125

84 Methods

Rating   Name   Duplication   Size   Complexity  
A rootFieldList() 0 13 2
A getSchemaComponent() 0 3 1
A setValue() 0 4 1
A _templates() 0 9 2
A FieldHolder() 0 9 2
A getAttributes() 0 23 2
A castingHelper() 0 7 3
A Title() 0 3 1
A getDescription() 0 3 1
A setTemplate() 0 5 1
A setFieldHolderTemplate() 0 5 1
A getSchemaDataDefaults() 0 22 1
A performReadonlyTransformation() 0 13 2
A hasClass() 0 4 1
A getContainerFieldList() 0 3 1
A isAutofocus() 0 3 1
A getSchemaData() 0 4 1
A Type() 0 4 1
A HolderID() 0 3 1
A setLeftTitle() 0 5 1
A getSchemaStateDefaults() 0 11 1
A attrValue() 0 3 1
A getCustomValidationMessage() 0 3 1
A setContainerFieldList() 0 4 1
A Value() 0 3 1
A getFieldHolderTemplates() 0 5 1
A getSmallFieldHolderTemplate() 0 3 1
A isComposite() 0 3 1
A getForm() 0 3 1
A getTemplateHelper() 0 7 2
A addExtraClass() 0 9 2
A setAttribute() 0 5 1
A canSubmitValue() 0 3 3
A setAutofocus() 0 4 1
A getSmallFieldHolderTemplates() 0 5 1
A isDisabled() 0 3 1
A setForm() 0 5 1
A Link() 0 3 1
A setSmallFieldHolderTemplate() 0 5 1
A getFieldHolderTemplate() 0 3 1
A validate() 0 3 1
A debug() 0 9 1
A setCustomValidationMessage() 0 5 1
A __construct() 0 17 3
A transform() 0 3 1
A getAttribute() 0 9 2
A setRightTitle() 0 4 1
A setSchemaState() 0 5 1
A setSchemaData() 0 5 1
A hasData() 0 3 1
A getInputType() 0 3 1
A SmallFieldHolder() 0 9 2
A performDisabledTransformation() 0 13 2
A getSchemaValidation() 0 6 2
A setTitle() 0 4 1
D getAttributesHTML() 0 37 9
A attrTitle() 0 3 1
A getTemplate() 0 3 1
A setDescription() 0 5 1
A setupDefaultClasses() 0 6 3
B castedCopy() 0 24 3
A setDisabled() 0 5 1
A name_to_label() 0 11 2
A setInputType() 0 5 1
A securityTokenEnabled() 0 9 2
A Required() 0 7 3
A ID() 0 3 1
A setSchemaComponent() 0 4 1
A dataValue() 0 3 1
A saveInto() 0 4 2
A getTemplates() 0 3 1
A forTemplate() 0 3 1
A getSchemaState() 0 4 1
A setSubmittedValue() 0 3 1
A setReadonly() 0 4 1
A RightTitle() 0 3 1
A getSchemaDataType() 0 3 1
A setName() 0 5 1
A Field() 0 20 4
A removeExtraClass() 0 9 2
B extraClass() 0 26 4
A getName() 0 3 1
A LeftTitle() 0 3 1
A isReadonly() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like FormField often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FormField, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use ReflectionClass;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\RequestHandler;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\Core\Convert;
10
use SilverStripe\Dev\Deprecation;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\DataObjectInterface;
13
use SilverStripe\ORM\FieldType\DBField;
14
use SilverStripe\ORM\FieldType\DBHTMLText;
15
use SilverStripe\View\SSViewer;
16
17
/**
18
 * Represents a field in a form.
19
 *
20
 * A FieldList contains a number of FormField objects which make up the whole of a form.
21
 *
22
 * In addition to single fields, FormField objects can be "composite", for example, the
23
 * {@link TabSet} field. Composite fields let us define complex forms without having to resort to
24
 * custom HTML.
25
 *
26
 * To subclass:
27
 *
28
 * Define a {@link dataValue()} method that returns a value suitable for inserting into a single
29
 * database field.
30
 *
31
 * For example, you might tidy up the format of a date or currency field. Define {@link saveInto()}
32
 * to totally customise saving.
33
 *
34
 * For example, data might be saved to the filesystem instead of the data record, or saved to a
35
 * component of the data record instead of the data record itself.
36
 *
37
 * A form field can be represented as structured data through {@link FormSchema},
38
 * including both structure (name, id, attributes, etc.) and state (field value).
39
 * Can be used by for JSON data which is consumed by a front-end application.
40
 */
41
class FormField extends RequestHandler
42
{
43
    use FormMessage;
44
45
    /** @see $schemaDataType */
46
    const SCHEMA_DATA_TYPE_STRING = 'String';
47
48
    /** @see $schemaDataType */
49
    const SCHEMA_DATA_TYPE_HIDDEN = 'Hidden';
50
51
    /** @see $schemaDataType */
52
    const SCHEMA_DATA_TYPE_TEXT = 'Text';
53
54
    /** @see $schemaDataType */
55
    const SCHEMA_DATA_TYPE_HTML = 'HTML';
56
57
    /** @see $schemaDataType */
58
    const SCHEMA_DATA_TYPE_INTEGER = 'Integer';
59
60
    /** @see $schemaDataType */
61
    const SCHEMA_DATA_TYPE_DECIMAL = 'Decimal';
62
63
    /** @see $schemaDataType */
64
    const SCHEMA_DATA_TYPE_MULTISELECT = 'MultiSelect';
65
66
    /** @see $schemaDataType */
67
    const SCHEMA_DATA_TYPE_SINGLESELECT = 'SingleSelect';
68
69
    /** @see $schemaDataType */
70
    const SCHEMA_DATA_TYPE_DATE = 'Date';
71
72
    /** @see $schemaDataType */
73
    const SCHEMA_DATA_TYPE_DATETIME = 'Datetime';
74
75
    /** @see $schemaDataType */
76
    const SCHEMA_DATA_TYPE_TIME = 'Time';
77
78
    /** @see $schemaDataType */
79
    const SCHEMA_DATA_TYPE_BOOLEAN = 'Boolean';
80
81
    /** @see $schemaDataType */
82
    const SCHEMA_DATA_TYPE_CUSTOM = 'Custom';
83
84
    /** @see $schemaDataType */
85
    const SCHEMA_DATA_TYPE_STRUCTURAL = 'Structural';
86
87
    /**
88
     * @var Form
89
     */
90
    protected $form;
91
92
    /**
93
     * This is INPUT's type attribute value.
94
     *
95
     * @var string
96
     */
97
    protected $inputType = 'text';
98
99
    /**
100
     * @var string
101
     */
102
    protected $name;
103
104
    /**
105
     * @var null|string
106
     */
107
    protected $title;
108
109
    /**
110
     * @var mixed
111
     */
112
    protected $value;
113
114
    /**
115
     * @var string
116
     */
117
    protected $extraClass;
118
119
    /**
120
     * Adds a title attribute to the markup.
121
     *
122
     * @var string
123
     *
124
     * @todo Implement in all subclasses
125
     */
126
    protected $description;
127
128
    /**
129
     * Extra CSS classes for the FormField container.
130
     *
131
     * @var array
132
     */
133
    protected $extraClasses;
134
135
    /**
136
     * @config
137
     * @var array $default_classes The default classes to apply to the FormField
138
     */
139
    private static $default_classes = [];
0 ignored issues
show
introduced by
The private property $default_classes is not used, and could be removed.
Loading history...
140
141
    /**
142
     * Right-aligned, contextual label for the field.
143
     *
144
     * @var string
145
     */
146
    protected $rightTitle;
147
148
    /**
149
     * Left-aligned, contextual label for the field.
150
     *
151
     * @var string
152
     */
153
    protected $leftTitle;
154
155
    /**
156
     * Stores a reference to the FieldList that contains this object.
157
     *
158
     * @var FieldList
159
     */
160
    protected $containerFieldList;
161
162
    /**
163
     * @var bool
164
     */
165
    protected $readonly = false;
166
167
    /**
168
     * @var bool
169
     */
170
    protected $disabled = false;
171
172
    /**
173
     * @var bool
174
     */
175
    protected $autofocus = false;
176
177
    /**
178
     * Custom validation message for the field.
179
     *
180
     * @var string
181
     */
182
    protected $customValidationMessage = '';
183
184
    /**
185
     * Name of the template used to render this form field. If not set, then will look up the class
186
     * ancestry for the first matching template where the template name equals the class name.
187
     *
188
     * To explicitly use a custom template or one named other than the form field see
189
     * {@link setTemplate()}.
190
     *
191
     * @var string
192
     */
193
    protected $template;
194
195
    /**
196
     * Name of the template used to render this form field. If not set, then will look up the class
197
     * ancestry for the first matching template where the template name equals the class name.
198
     *
199
     * To explicitly use a custom template or one named other than the form field see
200
     * {@link setFieldHolderTemplate()}.
201
     *
202
     * @var string
203
     */
204
    protected $fieldHolderTemplate;
205
206
    /**
207
     * @var string
208
     */
209
    protected $smallFieldHolderTemplate;
210
211
    /**
212
     * All attributes on the form field (not the field holder).
213
     *
214
     * Partially determined based on other instance properties.
215
     *
216
     * @see getAttributes()
217
     *
218
     * @var array
219
     */
220
    protected $attributes = [];
221
222
    /**
223
     * The data type backing the field. Represents the type of value the
224
     * form expects to receive via a postback. Should be set in subclasses.
225
     *
226
     * The values allowed in this list include:
227
     *
228
     *   - String: Single line text
229
     *   - Hidden: Hidden field which is posted back without modification
230
     *   - Text: Multi line text
231
     *   - HTML: Rich html text
232
     *   - Integer: Whole number value
233
     *   - Decimal: Decimal value
234
     *   - MultiSelect: Select many from source
235
     *   - SingleSelect: Select one from source
236
     *   - Date: Date only
237
     *   - DateTime: Date and time
238
     *   - Time: Time only
239
     *   - Boolean: Yes or no
240
     *   - Custom: Custom type declared by the front-end component. For fields with this type,
241
     *     the component property is mandatory, and will determine the posted value for this field.
242
     *   - Structural: Represents a field that is NOT posted back. This may contain other fields,
243
     *     or simply be a block of stand-alone content. As with 'Custom',
244
     *     the component property is mandatory if this is assigned.
245
     *
246
     * Each value has an equivalent constant, e.g. {@link self::SCHEMA_DATA_TYPE_STRING}.
247
     *
248
     * @var string
249
     */
250
    protected $schemaDataType;
251
252
    /**
253
     * The type of front-end component to render the FormField as.
254
     *
255
     * @skipUpgrade
256
     * @var string
257
     */
258
    protected $schemaComponent;
259
260
    /**
261
     * Structured schema data representing the FormField.
262
     * Used to render the FormField as a ReactJS Component on the front-end.
263
     *
264
     * @var array
265
     */
266
    protected $schemaData = [];
267
268
    private static $casting = array(
0 ignored issues
show
introduced by
The private property $casting is not used, and could be removed.
Loading history...
269
        'FieldHolder' => 'HTMLFragment',
270
        'SmallFieldHolder' => 'HTMLFragment',
271
        'Field' => 'HTMLFragment',
272
        'AttributesHTML' => 'HTMLFragment', // property $AttributesHTML version
273
        'getAttributesHTML' => 'HTMLFragment', // method $getAttributesHTML($arg) version
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
274
        'Value' => 'Text',
275
        'extraClass' => 'Text',
276
        'ID' => 'Text',
277
        'isReadOnly' => 'Boolean',
278
        'HolderID' => 'Text',
279
        'Title' => 'Text',
280
        'RightTitle' => 'Text',
281
        'Description' => 'HTMLFragment',
282
    );
283
284
    /**
285
     * Structured schema state representing the FormField's current data and validation.
286
     * Used to render the FormField as a ReactJS Component on the front-end.
287
     *
288
     * @var array
289
     */
290
    protected $schemaState = [];
291
292
    /**
293
     * Takes a field name and converts camelcase to spaced words. Also resolves combined field
294
     * names with dot syntax to spaced words.
295
     *
296
     * Examples:
297
     *
298
     * - 'TotalAmount' will return 'Total Amount'
299
     * - 'Organisation.ZipCode' will return 'Organisation Zip Code'
300
     *
301
     * @param string $fieldName
302
     *
303
     * @return string
304
     */
305
    public static function name_to_label($fieldName)
306
    {
307
        if (strpos($fieldName, '.') !== false) {
308
            $parts = explode('.', $fieldName);
309
310
            $label = $parts[count($parts) - 2] . ' ' . $parts[count($parts) - 1];
311
        } else {
312
            $label = $fieldName;
313
        }
314
315
        return preg_replace('/([a-z]+)([A-Z])/', '$1 $2', $label);
316
    }
317
318
    /**
319
     * Creates a new field.
320
     *
321
     * @param string $name The internal field name, passed to forms.
322
     * @param null|string $title The human-readable field label.
323
     * @param mixed $value The value of the field.
324
     */
325
    public function __construct($name, $title = null, $value = null)
326
    {
327
        $this->setName($name);
328
329
        if ($title === null) {
330
            $this->title = self::name_to_label($name);
331
        } else {
332
            $this->title = $title;
333
        }
334
335
        if ($value !== null) {
336
            $this->setValue($value);
337
        }
338
339
        parent::__construct();
340
341
        $this->setupDefaultClasses();
342
    }
343
344
    /**
345
     * Set up the default classes for the form. This is done on construct so that the default classes can be removed
346
     * after instantiation
347
     */
348
    protected function setupDefaultClasses()
349
    {
350
        $defaultClasses = $this->config()->get('default_classes');
351
        if ($defaultClasses) {
352
            foreach ($defaultClasses as $class) {
353
                $this->addExtraClass($class);
354
            }
355
        }
356
    }
357
358
    /**
359
     * Return a link to this field.
360
     *
361
     * @param string $action
362
     *
363
     * @return string
364
     */
365
    public function Link($action = null)
366
    {
367
        return Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
368
    }
369
370
    /**
371
     * Returns the HTML ID of the field.
372
     *
373
     * The ID is generated as FormName_FieldName. All Field functions should ensure that this ID is
374
     * included in the field.
375
     *
376
     * @return string
377
     */
378
    public function ID()
379
    {
380
        return $this->getTemplateHelper()->generateFieldID($this);
381
    }
382
383
    /**
384
     * Returns the HTML ID for the form field holder element.
385
     *
386
     * @return string
387
     */
388
    public function HolderID()
389
    {
390
        return $this->getTemplateHelper()->generateFieldHolderID($this);
391
    }
392
393
    /**
394
     * Returns the current {@link FormTemplateHelper} on either the parent
395
     * Form or the global helper set through the {@link Injector} layout.
396
     *
397
     * To customize a single {@link FormField}, use {@link setTemplate} and
398
     * provide a custom template name.
399
     *
400
     * @return FormTemplateHelper
401
     */
402
    public function getTemplateHelper()
403
    {
404
        if ($this->form) {
405
            return $this->form->getTemplateHelper();
406
        }
407
408
        return FormTemplateHelper::singleton();
409
    }
410
411
    /**
412
     * Returns the field name.
413
     *
414
     * @return string
415
     */
416
    public function getName()
417
    {
418
        return $this->name;
419
    }
420
421
    /**
422
     * Returns the field input name.
423
     *
424
     * @return string
425
     */
426
    public function getInputType()
427
    {
428
        return $this->inputType;
429
    }
430
431
    /**
432
     * Returns the field value.
433
     *
434
     * @see FormField::setSubmittedValue()
435
     * @return mixed
436
     */
437
    public function Value()
438
    {
439
        return $this->value;
440
    }
441
442
    /**
443
     * Method to save this form field into the given {@link DataObject}.
444
     *
445
     * By default, makes use of $this->dataValue()
446
     *
447
     * @param DataObject|DataObjectInterface $record DataObject to save data into
448
     */
449
    public function saveInto(DataObjectInterface $record)
450
    {
451
        if ($this->name) {
452
            $record->setCastedField($this->name, $this->dataValue());
453
        }
454
    }
455
456
    /**
457
     * Returns the field value suitable for insertion into the data object.
458
     * @see Formfield::setValue()
459
     * @return mixed
460
     */
461
    public function dataValue()
462
    {
463
        return $this->value;
464
    }
465
466
    /**
467
     * Returns the field label - used by templates.
468
     *
469
     * @return string
470
     */
471
    public function Title()
472
    {
473
        return $this->title;
474
    }
475
476
    /**
477
     * Set the title of this formfield.
478
     * Note: This expects escaped HTML.
479
     *
480
     * @param string $title Escaped HTML for title
481
     * @return $this
482
     */
483
    public function setTitle($title)
484
    {
485
        $this->title = $title;
486
        return $this;
487
    }
488
489
    /**
490
     * Gets the contextual label than can be used for additional field description.
491
     * Can be shown to the right or under the field in question.
492
     *
493
     * @return string Contextual label text.
494
     */
495
    public function RightTitle()
496
    {
497
        return $this->rightTitle;
498
    }
499
500
    /**
501
     * Sets the right title for this formfield
502
     * Note: This expects escaped HTML.
503
     *
504
     * @param string $rightTitle Escaped HTML for title
505
     * @return $this
506
     */
507
    public function setRightTitle($rightTitle)
508
    {
509
        $this->rightTitle = $rightTitle;
510
        return $this;
511
    }
512
513
    /**
514
     * @return string
515
     */
516
    public function LeftTitle()
517
    {
518
        return $this->leftTitle;
519
    }
520
521
    /**
522
     * @param string $leftTitle
523
     *
524
     * @return $this
525
     */
526
    public function setLeftTitle($leftTitle)
527
    {
528
        $this->leftTitle = $leftTitle;
529
530
        return $this;
531
    }
532
533
    /**
534
     * Compiles all CSS-classes. Optionally includes a "form-group--no-label" class if no title was set on the
535
     * FormField.
536
     *
537
     * Uses {@link Message()} and {@link MessageType()} to add validation error classes which can
538
     * be used to style the contained tags.
539
     *
540
     * @return string
541
     */
542
    public function extraClass()
543
    {
544
        $classes = array();
545
546
        $classes[] = $this->Type();
547
548
        if ($this->extraClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
549
            $classes = array_merge(
550
                $classes,
551
                array_values($this->extraClasses)
552
            );
553
        }
554
555
        if (!$this->Title()) {
556
            $classes[] = 'form-group--no-label';
557
        }
558
559
        // Allow custom styling of any element in the container based on validation errors,
560
        // e.g. red borders on input tags.
561
        //
562
        // CSS class needs to be different from the one rendered through {@link FieldHolder()}.
563
        if ($this->getMessage()) {
564
            $classes[] .= 'holder-' . $this->getMessageType();
565
        }
566
567
        return implode(' ', $classes);
568
    }
569
570
    /**
571
     * Add one or more CSS-classes to the FormField container.
572
     *
573
     * Multiple class names should be space delimited.
574
     *
575
     * @param string $class
576
     *
577
     * @return $this
578
     */
579
    public function addExtraClass($class)
580
    {
581
        $classes = preg_split('/\s+/', $class);
582
583
        foreach ($classes as $class) {
584
            $this->extraClasses[$class] = $class;
585
        }
586
587
        return $this;
588
    }
589
590
    /**
591
     * Remove one or more CSS-classes from the FormField container.
592
     *
593
     * @param string $class
594
     *
595
     * @return $this
596
     */
597
    public function removeExtraClass($class)
598
    {
599
        $classes = preg_split('/\s+/', $class);
600
601
        foreach ($classes as $class) {
602
            unset($this->extraClasses[$class]);
603
        }
604
605
        return $this;
606
    }
607
608
    /**
609
     * Set an HTML attribute on the field element, mostly an input tag.
610
     *
611
     * Some attributes are best set through more specialized methods, to avoid interfering with
612
     * built-in behaviour:
613
     *
614
     * - 'class': {@link addExtraClass()}
615
     * - 'title': {@link setDescription()}
616
     * - 'value': {@link setValue}
617
     * - 'name': {@link setName}
618
     *
619
     * Caution: this doesn't work on most fields which are composed of more than one HTML form
620
     * field.
621
     *
622
     * @param string $name
623
     * @param string $value
624
     *
625
     * @return $this
626
     */
627
    public function setAttribute($name, $value)
628
    {
629
        $this->attributes[$name] = $value;
630
631
        return $this;
632
    }
633
634
    /**
635
     * Get an HTML attribute defined by the field, or added through {@link setAttribute()}.
636
     *
637
     * Caution: this doesn't work on all fields, see {@link setAttribute()}.
638
     *
639
     * @param string $name
640
     * @return string
641
     */
642
    public function getAttribute($name)
643
    {
644
        $attributes = $this->getAttributes();
645
646
        if (isset($attributes[$name])) {
647
            return $attributes[$name];
648
        }
649
650
        return null;
651
    }
652
653
    /**
654
     * Allows customization through an 'updateAttributes' hook on the base class.
655
     * Existing attributes are passed in as the first argument and can be manipulated,
656
     * but any attributes added through a subclass implementation won't be included.
657
     *
658
     * @return array
659
     */
660
    public function getAttributes()
661
    {
662
        $attributes = array(
663
            'type' => $this->getInputType(),
664
            'name' => $this->getName(),
665
            'value' => $this->Value(),
666
            'class' => $this->extraClass(),
667
            'id' => $this->ID(),
668
            'disabled' => $this->isDisabled(),
669
            'readonly' => $this->isReadonly(),
670
            'autofocus' => $this->isAutofocus()
671
        );
672
673
        if ($this->Required()) {
674
            $attributes['required'] = 'required';
675
            $attributes['aria-required'] = 'true';
676
        }
677
678
        $attributes = array_merge($attributes, $this->attributes);
679
680
        $this->extend('updateAttributes', $attributes);
681
682
        return $attributes;
683
    }
684
685
    /**
686
     * Custom attributes to process. Falls back to {@link getAttributes()}.
687
     *
688
     * If at least one argument is passed as a string, all arguments act as excludes, by name.
689
     *
690
     * @param array $attributes
691
     *
692
     * @return string
693
     */
694
    public function getAttributesHTML($attributes = null)
695
    {
696
        $exclude = null;
697
698
        if (is_string($attributes)) {
699
            $exclude = func_get_args();
700
        }
701
702
        if (!$attributes || is_string($attributes)) {
703
            $attributes = $this->getAttributes();
704
        }
705
706
        $attributes = (array) $attributes;
707
708
        $attributes = array_filter($attributes, function ($v) {
709
            return ($v || $v === 0 || $v === '0');
710
        });
711
712
        if ($exclude) {
713
            $attributes = array_diff_key(
714
                $attributes,
715
                array_flip($exclude)
716
            );
717
        }
718
719
        // Create markup
720
        $parts = array();
721
722
        foreach ($attributes as $name => $value) {
723
            if ($value === true) {
724
                $parts[] = sprintf('%s="%s"', $name, $name);
725
            } else {
726
                $parts[] = sprintf('%s="%s"', $name, Convert::raw2att($value));
0 ignored issues
show
Bug introduced by
It seems like SilverStripe\Core\Convert::raw2att($value) can also be of type array; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

726
                $parts[] = sprintf('%s="%s"', $name, /** @scrutinizer ignore-type */ Convert::raw2att($value));
Loading history...
727
            }
728
        }
729
730
        return implode(' ', $parts);
731
    }
732
733
    /**
734
     * Returns a version of a title suitable for insertion into an HTML attribute.
735
     *
736
     * @return string
737
     */
738
    public function attrTitle()
739
    {
740
        return Convert::raw2att($this->title);
741
    }
742
743
    /**
744
     * Returns a version of a title suitable for insertion into an HTML attribute.
745
     *
746
     * @return string
747
     */
748
    public function attrValue()
749
    {
750
        return Convert::raw2att($this->value);
751
    }
752
753
    /**
754
     * Set the field value.
755
     *
756
     * If a FormField requires specific behaviour for loading content from either the database
757
     * or a submitted form value they should override setSubmittedValue() instead.
758
     *
759
     * @param mixed $value Either the parent object, or array of source data being loaded
760
     * @param array|DataObject $data {@see Form::loadDataFrom}
761
     * @return $this
762
     */
763
    public function setValue($value, $data = null)
764
    {
765
        $this->value = $value;
766
        return $this;
767
    }
768
769
    /**
770
     * Set value assigned from a submitted form postback.
771
     * Can be overridden to handle custom behaviour for user-localised
772
     * data formats.
773
     *
774
     * @param mixed $value
775
     * @param array|DataObject $data
776
     * @return $this
777
     */
778
    public function setSubmittedValue($value, $data = null)
779
    {
780
        return $this->setValue($value, $data);
781
    }
782
783
    /**
784
     * Set the field name.
785
     *
786
     * @param string $name
787
     *
788
     * @return $this
789
     */
790
    public function setName($name)
791
    {
792
        $this->name = $name;
793
794
        return $this;
795
    }
796
797
    /**
798
     * Set the field input type.
799
     *
800
     * @param string $type
801
     *
802
     * @return $this
803
     */
804
    public function setInputType($type)
805
    {
806
        $this->inputType = $type;
807
808
        return $this;
809
    }
810
811
    /**
812
     * Set the container form.
813
     *
814
     * This is called automatically when fields are added to forms.
815
     *
816
     * @param Form $form
817
     *
818
     * @return $this
819
     */
820
    public function setForm($form)
821
    {
822
        $this->form = $form;
823
824
        return $this;
825
    }
826
827
    /**
828
     * Get the currently used form.
829
     *
830
     * @return Form
831
     */
832
    public function getForm()
833
    {
834
        return $this->form;
835
    }
836
837
    /**
838
     * Return true if security token protection is enabled on the parent {@link Form}.
839
     *
840
     * @return bool
841
     */
842
    public function securityTokenEnabled()
843
    {
844
        $form = $this->getForm();
845
846
        if (!$form) {
847
            return false;
848
        }
849
850
        return $form->getSecurityToken()->isEnabled();
851
    }
852
853
    public function castingHelper($field)
854
    {
855
        // Override casting for field message
856
        if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
857
            return $helper;
858
        }
859
        return parent::castingHelper($field);
860
    }
861
862
    /**
863
     * Set the custom error message to show instead of the default format.
864
     *
865
     * Different from setError() as that appends it to the standard error messaging.
866
     *
867
     * @param string $customValidationMessage
868
     *
869
     * @return $this
870
     */
871
    public function setCustomValidationMessage($customValidationMessage)
872
    {
873
        $this->customValidationMessage = $customValidationMessage;
874
875
        return $this;
876
    }
877
878
    /**
879
     * Get the custom error message for this form field. If a custom message has not been defined
880
     * then just return blank. The default error is defined on {@link Validator}.
881
     *
882
     * @return string
883
     */
884
    public function getCustomValidationMessage()
885
    {
886
        return $this->customValidationMessage;
887
    }
888
889
    /**
890
     * Set name of template (without path or extension).
891
     *
892
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
893
     * method on the subclass for support.
894
     *
895
     * @param string $template
896
     *
897
     * @return $this
898
     */
899
    public function setTemplate($template)
900
    {
901
        $this->template = $template;
902
903
        return $this;
904
    }
905
906
    /**
907
     * @return string
908
     */
909
    public function getTemplate()
910
    {
911
        return $this->template;
912
    }
913
914
    /**
915
     * @return string
916
     */
917
    public function getFieldHolderTemplate()
918
    {
919
        return $this->fieldHolderTemplate;
920
    }
921
922
    /**
923
     * Set name of template (without path or extension) for the holder, which in turn is
924
     * responsible for rendering {@link Field()}.
925
     *
926
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
927
     * method on the subclass for support.
928
     *
929
     * @param string $fieldHolderTemplate
930
     *
931
     * @return $this
932
     */
933
    public function setFieldHolderTemplate($fieldHolderTemplate)
934
    {
935
        $this->fieldHolderTemplate = $fieldHolderTemplate;
936
937
        return $this;
938
    }
939
940
    /**
941
     * @return string
942
     */
943
    public function getSmallFieldHolderTemplate()
944
    {
945
        return $this->smallFieldHolderTemplate;
946
    }
947
948
    /**
949
     * Set name of template (without path or extension) for the small holder, which in turn is
950
     * responsible for rendering {@link Field()}.
951
     *
952
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
953
     * method on the subclass for support.
954
     *
955
     * @param string $smallFieldHolderTemplate
956
     *
957
     * @return $this
958
     */
959
    public function setSmallFieldHolderTemplate($smallFieldHolderTemplate)
960
    {
961
        $this->smallFieldHolderTemplate = $smallFieldHolderTemplate;
962
963
        return $this;
964
    }
965
966
    /**
967
     * Returns the form field.
968
     *
969
     * Although FieldHolder is generally what is inserted into templates, all of the field holder
970
     * templates make use of $Field. It's expected that FieldHolder will give you the "complete"
971
     * representation of the field on the form, whereas Field will give you the core editing widget,
972
     * such as an input tag.
973
     *
974
     * @param array $properties
975
     * @return DBHTMLText
976
     */
977
    public function Field($properties = array())
978
    {
979
        $context = $this;
980
981
        $this->extend('onBeforeRender', $context, $properties);
982
983
        if (count($properties)) {
984
            $context = $context->customise($properties);
985
        }
986
987
        $result = $context->renderWith($this->getTemplates());
988
989
        // Trim whitespace from the result, so that trailing newlines are supressed. Works for strings and HTMLText values
990
        if (is_string($result)) {
991
            $result = trim($result);
992
        } elseif ($result instanceof DBField) {
993
            $result->setValue(trim($result->getValue()));
994
        }
995
996
        return $result;
997
    }
998
999
    /**
1000
     * Returns a "field holder" for this field.
1001
     *
1002
     * Forms are constructed by concatenating a number of these field holders.
1003
     *
1004
     * The default field holder is a label and a form field inside a div.
1005
     *
1006
     * @see FieldHolder.ss
1007
     *
1008
     * @param array $properties
1009
     *
1010
     * @return DBHTMLText
1011
     */
1012
    public function FieldHolder($properties = array())
1013
    {
1014
        $context = $this;
1015
1016
        if (count($properties)) {
1017
            $context = $this->customise($properties);
1018
        }
1019
1020
        return $context->renderWith($this->getFieldHolderTemplates());
1021
    }
1022
1023
    /**
1024
     * Returns a restricted field holder used within things like FieldGroups.
1025
     *
1026
     * @param array $properties
1027
     *
1028
     * @return string
1029
     */
1030
    public function SmallFieldHolder($properties = array())
1031
    {
1032
        $context = $this;
1033
1034
        if (count($properties)) {
1035
            $context = $this->customise($properties);
1036
        }
1037
1038
        return $context->renderWith($this->getSmallFieldHolderTemplates());
1039
    }
1040
1041
    /**
1042
     * Returns an array of templates to use for rendering {@link FieldHolder}.
1043
     *
1044
     * @return array
1045
     */
1046
    public function getTemplates()
1047
    {
1048
        return $this->_templates($this->getTemplate());
1049
    }
1050
1051
    /**
1052
     * Returns an array of templates to use for rendering {@link FieldHolder}.
1053
     *
1054
     * @return array
1055
     */
1056
    public function getFieldHolderTemplates()
1057
    {
1058
        return $this->_templates(
1059
            $this->getFieldHolderTemplate(),
1060
            '_holder'
1061
        );
1062
    }
1063
1064
    /**
1065
     * Returns an array of templates to use for rendering {@link SmallFieldHolder}.
1066
     *
1067
     * @return array
1068
     */
1069
    public function getSmallFieldHolderTemplates()
1070
    {
1071
        return $this->_templates(
1072
            $this->getSmallFieldHolderTemplate(),
1073
            '_holder_small'
1074
        );
1075
    }
1076
1077
1078
    /**
1079
     * Generate an array of class name strings to use for rendering this form field into HTML.
1080
     *
1081
     * @param string $customTemplate
1082
     * @param string $customTemplateSuffix
1083
     *
1084
     * @return array
1085
     */
1086
    protected function _templates($customTemplate = null, $customTemplateSuffix = null)
1087
    {
1088
        $templates = SSViewer::get_templates_by_class(static::class, $customTemplateSuffix, __CLASS__);
1089
        // Prefer any custom template
1090
        if ($customTemplate) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $customTemplate of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1091
            // Prioritise direct template
1092
            array_unshift($templates, $customTemplate);
1093
        }
1094
        return $templates;
1095
    }
1096
1097
    /**
1098
     * Returns true if this field is a composite field.
1099
     *
1100
     * To create composite field types, you should subclass {@link CompositeField}.
1101
     *
1102
     * @return bool
1103
     */
1104
    public function isComposite()
1105
    {
1106
        return false;
1107
    }
1108
1109
    /**
1110
     * Returns true if this field has its own data.
1111
     *
1112
     * Some fields, such as titles and composite fields, don't actually have any data. It doesn't
1113
     * make sense for data-focused methods to look at them. By overloading hasData() to return
1114
     * false, you can prevent any data-focused methods from looking at it.
1115
     *
1116
     * @see FieldList::collateDataFields()
1117
     *
1118
     * @return bool
1119
     */
1120
    public function hasData()
1121
    {
1122
        return true;
1123
    }
1124
1125
    /**
1126
     * @return bool
1127
     */
1128
    public function isReadonly()
1129
    {
1130
        return $this->readonly;
1131
    }
1132
1133
    /**
1134
     * Sets a read-only flag on this FormField.
1135
     *
1136
     * Use performReadonlyTransformation() to transform this instance.
1137
     *
1138
     * Setting this to false has no effect on the field.
1139
     *
1140
     * @param bool $readonly
1141
     *
1142
     * @return $this
1143
     */
1144
    public function setReadonly($readonly)
1145
    {
1146
        $this->readonly = $readonly;
1147
        return $this;
1148
    }
1149
1150
    /**
1151
     * @return bool
1152
     */
1153
    public function isDisabled()
1154
    {
1155
        return $this->disabled;
1156
    }
1157
1158
    /**
1159
     * Sets a disabled flag on this FormField.
1160
     *
1161
     * Use performDisabledTransformation() to transform this instance.
1162
     *
1163
     * Setting this to false has no effect on the field.
1164
     *
1165
     * @param bool $disabled
1166
     *
1167
     * @return $this
1168
     */
1169
    public function setDisabled($disabled)
1170
    {
1171
        $this->disabled = $disabled;
1172
1173
        return $this;
1174
    }
1175
1176
    /**
1177
     * @return bool
1178
     */
1179
    public function isAutofocus()
1180
    {
1181
        return $this->autofocus;
1182
    }
1183
1184
    /**
1185
     * Sets a autofocus flag on this FormField.
1186
     *
1187
     * @param bool $autofocus
1188
     * @return $this
1189
     */
1190
    public function setAutofocus($autofocus)
1191
    {
1192
        $this->autofocus = $autofocus;
1193
        return $this;
1194
    }
1195
1196
    /**
1197
     * Returns a read-only version of this field.
1198
     *
1199
     * @return FormField
1200
     */
1201
    public function performReadonlyTransformation()
1202
    {
1203
        $readonlyClassName = static::class . '_Readonly';
1204
1205
        if (ClassInfo::exists($readonlyClassName)) {
1206
            $clone = $this->castedCopy($readonlyClassName);
1207
        } else {
1208
            $clone = $this->castedCopy(ReadonlyField::class);
1209
        }
1210
1211
        $clone->setReadonly(true);
1212
1213
        return $clone;
1214
    }
1215
1216
    /**
1217
     * Return a disabled version of this field.
1218
     *
1219
     * Tries to find a class of the class name of this field suffixed with "_Disabled", failing
1220
     * that, finds a method {@link setDisabled()}.
1221
     *
1222
     * @return FormField
1223
     */
1224
    public function performDisabledTransformation()
1225
    {
1226
        $disabledClassName = static::class . '_Disabled';
1227
1228
        if (ClassInfo::exists($disabledClassName)) {
1229
            $clone = $this->castedCopy($disabledClassName);
1230
        } else {
1231
            $clone = clone $this;
1232
        }
1233
1234
        $clone->setDisabled(true);
1235
1236
        return $clone;
1237
    }
1238
1239
    /**
1240
     * @param FormTransformation $transformation
1241
     *
1242
     * @return mixed
1243
     */
1244
    public function transform(FormTransformation $transformation)
1245
    {
1246
        return $transformation->transform($this);
1247
    }
1248
1249
    /**
1250
     * Returns whether the current field has the given class added
1251
     *
1252
     * @param string $class
1253
     *
1254
     * @return bool
1255
     */
1256
    public function hasClass($class)
1257
    {
1258
        $classes = explode(' ', strtolower($this->extraClass()));
1259
        return in_array(strtolower(trim($class)), $classes);
1260
    }
1261
1262
    /**
1263
     * Returns the field type.
1264
     *
1265
     * The field type is the class name with the word Field dropped off the end, all lowercase.
1266
     *
1267
     * It's handy for assigning HTML classes. Doesn't signify the input type attribute.
1268
     *
1269
     * @see {link getAttributes()}.
1270
     *
1271
     * @return string
1272
     */
1273
    public function Type()
1274
    {
1275
        $type = new ReflectionClass($this);
1276
        return strtolower(preg_replace('/Field$/', '', $type->getShortName()));
1277
    }
1278
1279
    /**
1280
     * Abstract method each {@link FormField} subclass must implement, determines whether the field
1281
     * is valid or not based on the value.
1282
     *
1283
     * @todo Make this abstract.
1284
     *
1285
     * @param Validator $validator
1286
     * @return bool
1287
     */
1288
    public function validate($validator)
0 ignored issues
show
Unused Code introduced by
The parameter $validator is not used and could be removed. ( Ignorable by Annotation )

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

1288
    public function validate(/** @scrutinizer ignore-unused */ $validator)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1289
    {
1290
        return true;
1291
    }
1292
1293
    /**
1294
     * Describe this field, provide help text for it.
1295
     *
1296
     * By default, renders as a span class="description" underneath the form field.
1297
     *
1298
     * @param string $description
1299
     *
1300
     * @return $this
1301
     */
1302
    public function setDescription($description)
1303
    {
1304
        $this->description = $description;
1305
1306
        return $this;
1307
    }
1308
1309
    /**
1310
     * @return string
1311
     */
1312
    public function getDescription()
1313
    {
1314
        return $this->description;
1315
    }
1316
1317
    /**
1318
     * @return string
1319
     */
1320
    public function debug()
1321
    {
1322
        return sprintf(
1323
            '%s (%s: %s : <span style="color:red;">%s</span>) = %s',
1324
            static::class,
1325
            $this->name,
1326
            $this->title,
1327
            $this->message,
1328
            $this->value
1329
        );
1330
    }
1331
1332
    /**
1333
     * This function is used by the template processor. If you refer to a field as a $ variable, it
1334
     * will return the $Field value.
1335
     *
1336
     * @return string
1337
     */
1338
    public function forTemplate()
1339
    {
1340
        return $this->Field();
1341
    }
1342
1343
    /**
1344
     * @return bool
1345
     */
1346
    public function Required()
1347
    {
1348
        if ($this->form && ($validator = $this->form->getValidator())) {
1349
            return $validator->fieldIsRequired($this->name);
1350
        }
1351
1352
        return false;
1353
    }
1354
1355
    /**
1356
     * Set the FieldList that contains this field.
1357
     *
1358
     * @param FieldList $containerFieldList
1359
     * @return $this
1360
     */
1361
    public function setContainerFieldList($containerFieldList)
1362
    {
1363
        $this->containerFieldList = $containerFieldList;
1364
        return $this;
1365
    }
1366
1367
    /**
1368
     * Get the FieldList that contains this field.
1369
     *
1370
     * @return FieldList
1371
     */
1372
    public function getContainerFieldList()
1373
    {
1374
        return $this->containerFieldList;
1375
    }
1376
1377
    /**
1378
     * @return null|FieldList
1379
     */
1380
    public function rootFieldList()
1381
    {
1382
        if ($this->containerFieldList) {
1383
            return $this->containerFieldList->rootFieldList();
1384
        }
1385
1386
        $class = static::class;
1387
        user_error(
1388
            "rootFieldList() called on {$class} object without a containerFieldList",
1389
            E_USER_ERROR
1390
        );
1391
1392
        return null;
1393
    }
1394
1395
    /**
1396
     * Returns another instance of this field, but "cast" to a different class. The logic tries to
1397
     * retain all of the instance properties, and may be overloaded by subclasses to set additional
1398
     * ones.
1399
     *
1400
     * Assumes the standard FormField parameter signature with its name as the only mandatory
1401
     * argument. Mainly geared towards creating *_Readonly or *_Disabled subclasses of the same
1402
     * type, or casting to a {@link ReadonlyField}.
1403
     *
1404
     * Does not copy custom field templates, since they probably won't apply to the new instance.
1405
     *
1406
     * @param mixed $classOrCopy Class name for copy, or existing copy instance to update
1407
     *
1408
     * @return FormField
1409
     */
1410
    public function castedCopy($classOrCopy)
1411
    {
1412
        $field = $classOrCopy;
1413
1414
        if (!is_object($field)) {
1415
            $field = new $classOrCopy($this->name);
1416
        }
1417
1418
        $field
1419
            ->setValue($this->value)
1420
            ->setForm($this->form)
1421
            ->setTitle($this->Title())
1422
            ->setLeftTitle($this->LeftTitle())
1423
            ->setRightTitle($this->RightTitle())
1424
            ->addExtraClass($this->extraClass) // Don't use extraClass(), since this merges calculated values
1425
            ->setDescription($this->getDescription());
1426
1427
        // Only include built-in attributes, ignore anything set through getAttributes().
1428
        // Those might change important characteristics of the field, e.g. its "type" attribute.
1429
        foreach ($this->attributes as $attributeKey => $attributeValue) {
1430
            $field->setAttribute($attributeKey, $attributeValue);
1431
        }
1432
1433
        return $field;
1434
    }
1435
1436
    /**
1437
     * Determine if the value of this formfield accepts front-end submitted values and is saveable.
1438
     *
1439
     * @return bool
1440
     */
1441
    public function canSubmitValue()
1442
    {
1443
        return $this->hasData() && !$this->isReadonly() && !$this->isDisabled();
1444
    }
1445
1446
    /**
1447
     * Sets the component type the FormField will be rendered as on the front-end.
1448
     *
1449
     * @param string $componentType
1450
     * @return FormField
1451
     */
1452
    public function setSchemaComponent($componentType)
1453
    {
1454
        $this->schemaComponent = $componentType;
1455
        return $this;
1456
    }
1457
1458
    /**
1459
     * Gets the type of front-end component the FormField will be rendered as.
1460
     *
1461
     * @return string
1462
     */
1463
    public function getSchemaComponent()
1464
    {
1465
        return $this->schemaComponent;
1466
    }
1467
1468
    /**
1469
     * Sets the schema data used for rendering the field on the front-end.
1470
     * Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
1471
     * Any passed keys that are not defined in {@link getSchemaDataDefaults()} are ignored.
1472
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1473
     *
1474
     * @param array $schemaData - The data to be merged with $this->schemaData.
1475
     * @return FormField
1476
     *
1477
     * @todo Add deep merging of arrays like `data` and `attributes`.
1478
     */
1479
    public function setSchemaData($schemaData = [])
1480
    {
1481
        $defaults = $this->getSchemaData();
1482
        $this->schemaData = array_merge($this->schemaData, array_intersect_key($schemaData, $defaults));
1483
        return $this;
1484
    }
1485
1486
    /**
1487
     * Gets the schema data used to render the FormField on the front-end.
1488
     *
1489
     * @return array
1490
     */
1491
    public function getSchemaData()
1492
    {
1493
        $defaults = $this->getSchemaDataDefaults();
1494
        return array_replace_recursive($defaults, array_intersect_key($this->schemaData, $defaults));
1495
    }
1496
1497
    /**
1498
     * @todo Throw exception if value is missing, once a form field schema is mandatory across the CMS
1499
     *
1500
     * @return string
1501
     */
1502
    public function getSchemaDataType()
1503
    {
1504
        return $this->schemaDataType;
1505
    }
1506
1507
    /**
1508
     * Gets the defaults for $schemaData.
1509
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaData()} are ignored.
1510
     * Instead the `data` array should be used to pass around ad hoc data.
1511
     *
1512
     * @return array
1513
     */
1514
    public function getSchemaDataDefaults()
1515
    {
1516
        return [
1517
            'name' => $this->getName(),
1518
            'id' => $this->ID(),
1519
            'type' => $this->getInputType(),
1520
            'schemaType' => $this->getSchemaDataType(),
1521
            'component' => $this->getSchemaComponent(),
1522
            'holderId' => $this->HolderID(),
1523
            'title' => $this->obj('Title')->getSchemaValue(),
1524
            'source' => null,
1525
            'extraClass' => $this->extraClass(),
1526
            'description' => $this->obj('Description')->getSchemaValue(),
1527
            'rightTitle' => $this->obj('RightTitle')->getSchemaValue(),
1528
            'leftTitle' => $this->obj('LeftTitle')->getSchemaValue(),
1529
            'readOnly' => $this->isReadonly(),
1530
            'disabled' => $this->isDisabled(),
1531
            'customValidationMessage' => $this->getCustomValidationMessage(),
1532
            'validation' => $this->getSchemaValidation(),
1533
            'attributes' => [],
1534
            'autoFocus' => $this->isAutofocus(),
1535
            'data' => [],
1536
        ];
1537
    }
1538
1539
    /**
1540
     * Sets the schema data used for rendering the field on the front-end.
1541
     * Merges the passed array with the current `$schemaState` or {@link getSchemaStateDefaults()}.
1542
     * Any passed keys that are not defined in {@link getSchemaStateDefaults()} are ignored.
1543
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1544
     *
1545
     * @param array $schemaState The data to be merged with $this->schemaData.
1546
     * @return FormField
1547
     *
1548
     * @todo Add deep merging of arrays like `data` and `attributes`.
1549
     */
1550
    public function setSchemaState($schemaState = [])
1551
    {
1552
        $defaults = $this->getSchemaState();
1553
        $this->schemaState = array_merge($this->schemaState, array_intersect_key($schemaState, $defaults));
1554
        return $this;
1555
    }
1556
1557
    /**
1558
     * Gets the schema state used to render the FormField on the front-end.
1559
     *
1560
     * @return array
1561
     */
1562
    public function getSchemaState()
1563
    {
1564
        $defaults = $this->getSchemaStateDefaults();
1565
        return array_merge($defaults, array_intersect_key($this->schemaState, $defaults));
1566
    }
1567
1568
    /**
1569
     * Gets the defaults for $schemaState.
1570
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaState()} are ignored.
1571
     * Instead the `data` array should be used to pass around ad hoc data.
1572
     * Includes validation data if the field is associated to a {@link Form},
1573
     * and {@link Form->validate()} has been called.
1574
     *
1575
     * @todo Make form / field messages not always stored as html; Store value / casting as separate values.
1576
     * @return array
1577
     */
1578
    public function getSchemaStateDefaults()
1579
    {
1580
        $state = [
1581
            'name' => $this->getName(),
1582
            'id' => $this->ID(),
1583
            'value' => $this->Value(),
1584
            'message' => $this->getSchemaMessage(),
1585
            'data' => [],
1586
        ];
1587
1588
        return $state;
1589
    }
1590
1591
    /**
1592
     * Return list of validation rules. Each rule is a key value pair.
1593
     * The key is the rule name. The value is any information the frontend
1594
     * validation handler can understand, or just `true` to enable.
1595
     *
1596
     * @return array
1597
     */
1598
    public function getSchemaValidation()
1599
    {
1600
        if ($this->Required()) {
1601
            return [ 'required' => true ];
1602
        }
1603
        return [];
1604
    }
1605
}
1606