Completed
Push — fix-2494 ( 3153ee...40d9bb )
by Sam
13:43 queued 06:38
created

FormField::transform()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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 = [];
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
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
269
        'FieldHolder' => 'HTMLFragment',
270
        'SmallFieldHolder' => 'HTMLFragment',
271
        'Field' => 'HTMLFragment',
272
        'AttributesHTML' => 'HTMLFragment', // property $AttributesHTML version
273
        'getAttributesHTML' => 'HTMLFragment', // method $getAttributesHTML($arg) version
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
     * Construct and return HTML tag.
320
     *
321
     * @param string $tag
322
     * @param array $attributes
323
     * @param null|string $content
324
     *
325
     * @return string
326
     */
327
    public static function create_tag($tag, $attributes, $content = null)
328
    {
329
        $preparedAttributes = '';
330
331
        foreach ($attributes as $attributeKey => $attributeValue) {
332
            if (!empty($attributeValue) || $attributeValue === '0' || ($attributeKey == 'value' && $attributeValue !== null)) {
333
                $preparedAttributes .= sprintf(
334
                    ' %s="%s"',
335
                    $attributeKey,
336
                    Convert::raw2att($attributeValue)
337
                );
338
            }
339
        }
340
341
        if ($content || !in_array($tag, [
0 ignored issues
show
Bug Best Practice introduced by
The expression $content 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...
342
                'area',
343
                'base',
344
                'br',
345
                'col',
346
                'embed',
347
                'hr',
348
                'img',
349
                'input',
350
                'link',
351
                'meta',
352
                'param',
353
                'source',
354
                'track',
355
                'wbr',
356
        ])) {
357
            return sprintf(
358
                '<%s%s>%s</%s>',
359
                $tag,
360
                $preparedAttributes,
361
                $content,
362
                $tag
363
            );
364
        }
365
366
        return sprintf(
367
            '<%s%s />',
368
            $tag,
369
            $preparedAttributes
370
        );
371
    }
372
373
    /**
374
     * Creates a new field.
375
     *
376
     * @param string $name The internal field name, passed to forms.
377
     * @param null|string $title The human-readable field label.
378
     * @param mixed $value The value of the field.
379
     */
380
    public function __construct($name, $title = null, $value = null)
381
    {
382
        $this->setName($name);
383
384
        if ($title === null) {
385
            $this->title = self::name_to_label($name);
386
        } else {
387
            $this->title = $title;
388
        }
389
390
        if ($value !== null) {
391
            $this->setValue($value);
392
        }
393
394
        parent::__construct();
395
396
        $this->setupDefaultClasses();
397
    }
398
399
    /**
400
     * Set up the default classes for the form. This is done on construct so that the default classes can be removed
401
     * after instantiation
402
     */
403
    protected function setupDefaultClasses()
404
    {
405
        $defaultClasses = $this->config()->get('default_classes');
406
        if ($defaultClasses) {
407
            foreach ($defaultClasses as $class) {
408
                $this->addExtraClass($class);
409
            }
410
        }
411
    }
412
413
    /**
414
     * Return a link to this field.
415
     *
416
     * @param string $action
417
     *
418
     * @return string
419
     */
420
    public function Link($action = null)
421
    {
422
        return Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
423
    }
424
425
    /**
426
     * Returns the HTML ID of the field.
427
     *
428
     * The ID is generated as FormName_FieldName. All Field functions should ensure that this ID is
429
     * included in the field.
430
     *
431
     * @return string
432
     */
433
    public function ID()
434
    {
435
        return $this->getTemplateHelper()->generateFieldID($this);
436
    }
437
438
    /**
439
     * Returns the HTML ID for the form field holder element.
440
     *
441
     * @return string
442
     */
443
    public function HolderID()
444
    {
445
        return $this->getTemplateHelper()->generateFieldHolderID($this);
446
    }
447
448
    /**
449
     * Returns the current {@link FormTemplateHelper} on either the parent
450
     * Form or the global helper set through the {@link Injector} layout.
451
     *
452
     * To customize a single {@link FormField}, use {@link setTemplate} and
453
     * provide a custom template name.
454
     *
455
     * @return FormTemplateHelper
456
     */
457
    public function getTemplateHelper()
458
    {
459
        if ($this->form) {
460
            return $this->form->getTemplateHelper();
461
        }
462
463
        return FormTemplateHelper::singleton();
464
    }
465
466
    /**
467
     * Returns the field name.
468
     *
469
     * @return string
470
     */
471
    public function getName()
472
    {
473
        return $this->name;
474
    }
475
476
    /**
477
     * Returns the field input name.
478
     *
479
     * @return string
480
     */
481
    public function getInputType()
482
    {
483
        return $this->inputType;
484
    }
485
486
    /**
487
     * Returns the field value.
488
     *
489
     * @see FormField::setSubmittedValue()
490
     * @return mixed
491
     */
492
    public function Value()
493
    {
494
        return $this->value;
495
    }
496
497
    /**
498
     * Method to save this form field into the given {@link DataObject}.
499
     *
500
     * By default, makes use of $this->dataValue()
501
     *
502
     * @param DataObject|DataObjectInterface $record DataObject to save data into
503
     */
504
    public function saveInto(DataObjectInterface $record)
505
    {
506
        if ($this->name) {
507
            $record->setCastedField($this->name, $this->dataValue());
508
        }
509
    }
510
511
    /**
512
     * Returns the field value suitable for insertion into the data object.
513
     * @see Formfield::setValue()
514
     * @return mixed
515
     */
516
    public function dataValue()
517
    {
518
        return $this->value;
519
    }
520
521
    /**
522
     * Returns the field label - used by templates.
523
     *
524
     * @return string
525
     */
526
    public function Title()
527
    {
528
        return $this->title;
529
    }
530
531
    /**
532
     * Set the title of this formfield.
533
     * Note: This expects escaped HTML.
534
     *
535
     * @param string $title Escaped HTML for title
536
     * @return $this
537
     */
538
    public function setTitle($title)
539
    {
540
        $this->title = $title;
541
        return $this;
542
    }
543
544
    /**
545
     * Gets the contextual label than can be used for additional field description.
546
     * Can be shown to the right or under the field in question.
547
     *
548
     * @return string Contextual label text.
549
     */
550
    public function RightTitle()
551
    {
552
        return $this->rightTitle;
553
    }
554
555
    /**
556
     * Sets the right title for this formfield
557
     * Note: This expects escaped HTML.
558
     *
559
     * @param string $rightTitle Escaped HTML for title
560
     * @return $this
561
     */
562
    public function setRightTitle($rightTitle)
563
    {
564
        $this->rightTitle = $rightTitle;
565
        return $this;
566
    }
567
568
    /**
569
     * @return string
570
     */
571
    public function LeftTitle()
572
    {
573
        return $this->leftTitle;
574
    }
575
576
    /**
577
     * @param string $leftTitle
578
     *
579
     * @return $this
580
     */
581
    public function setLeftTitle($leftTitle)
582
    {
583
        $this->leftTitle = $leftTitle;
584
585
        return $this;
586
    }
587
588
    /**
589
     * Compiles all CSS-classes. Optionally includes a "form-group--no-label" class if no title was set on the
590
     * FormField.
591
     *
592
     * Uses {@link Message()} and {@link MessageType()} to add validation error classes which can
593
     * be used to style the contained tags.
594
     *
595
     * @return string
596
     */
597
    public function extraClass()
598
    {
599
        $classes = array();
600
601
        $classes[] = $this->Type();
602
603
        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...
604
            $classes = array_merge(
605
                $classes,
606
                array_values($this->extraClasses)
607
            );
608
        }
609
610
        if (!$this->Title()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->Title() of type null|string is loosely compared to false; 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...
611
            $classes[] = 'form-group--no-label';
612
        }
613
614
        // Allow custom styling of any element in the container based on validation errors,
615
        // e.g. red borders on input tags.
616
        //
617
        // CSS class needs to be different from the one rendered through {@link FieldHolder()}.
618
        if ($this->getMessage()) {
619
            $classes[] .= 'holder-' . $this->getMessageType();
620
        }
621
622
        return implode(' ', $classes);
623
    }
624
625
    /**
626
     * Add one or more CSS-classes to the FormField container.
627
     *
628
     * Multiple class names should be space delimited.
629
     *
630
     * @param string $class
631
     *
632
     * @return $this
633
     */
634 View Code Duplication
    public function addExtraClass($class)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
635
    {
636
        $classes = preg_split('/\s+/', $class);
637
638
        foreach ($classes as $class) {
639
            $this->extraClasses[$class] = $class;
640
        }
641
642
        return $this;
643
    }
644
645
    /**
646
     * Remove one or more CSS-classes from the FormField container.
647
     *
648
     * @param string $class
649
     *
650
     * @return $this
651
     */
652 View Code Duplication
    public function removeExtraClass($class)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
653
    {
654
        $classes = preg_split('/\s+/', $class);
655
656
        foreach ($classes as $class) {
657
            unset($this->extraClasses[$class]);
658
        }
659
660
        return $this;
661
    }
662
663
    /**
664
     * Set an HTML attribute on the field element, mostly an <input> tag.
665
     *
666
     * Some attributes are best set through more specialized methods, to avoid interfering with
667
     * built-in behaviour:
668
     *
669
     * - 'class': {@link addExtraClass()}
670
     * - 'title': {@link setDescription()}
671
     * - 'value': {@link setValue}
672
     * - 'name': {@link setName}
673
     *
674
     * Caution: this doesn't work on most fields which are composed of more than one HTML form
675
     * field.
676
     *
677
     * @param string $name
678
     * @param string $value
679
     *
680
     * @return $this
681
     */
682
    public function setAttribute($name, $value)
683
    {
684
        $this->attributes[$name] = $value;
685
686
        return $this;
687
    }
688
689
    /**
690
     * Get an HTML attribute defined by the field, or added through {@link setAttribute()}.
691
     *
692
     * Caution: this doesn't work on all fields, see {@link setAttribute()}.
693
     *
694
     * @param string $name
695
     * @return string
696
     */
697
    public function getAttribute($name)
698
    {
699
        $attributes = $this->getAttributes();
700
701
        if (isset($attributes[$name])) {
702
            return $attributes[$name];
703
        }
704
705
        return null;
706
    }
707
708
    /**
709
     * Allows customization through an 'updateAttributes' hook on the base class.
710
     * Existing attributes are passed in as the first argument and can be manipulated,
711
     * but any attributes added through a subclass implementation won't be included.
712
     *
713
     * @return array
714
     */
715
    public function getAttributes()
716
    {
717
        $attributes = array(
718
            'type' => $this->getInputType(),
719
            'name' => $this->getName(),
720
            'value' => $this->Value(),
721
            'class' => $this->extraClass(),
722
            'id' => $this->ID(),
723
            'disabled' => $this->isDisabled(),
724
            'readonly' => $this->isReadonly(),
725
            'autofocus' => $this->isAutofocus()
726
        );
727
728
        if ($this->Required()) {
729
            $attributes['required'] = 'required';
730
            $attributes['aria-required'] = 'true';
731
        }
732
733
        $attributes = array_merge($attributes, $this->attributes);
734
735
        $this->extend('updateAttributes', $attributes);
736
737
        return $attributes;
738
    }
739
740
    /**
741
     * Custom attributes to process. Falls back to {@link getAttributes()}.
742
     *
743
     * If at least one argument is passed as a string, all arguments act as excludes, by name.
744
     *
745
     * @param array $attributes
746
     *
747
     * @return string
748
     */
749
    public function getAttributesHTML($attributes = null)
750
    {
751
        $exclude = null;
752
753
        if (is_string($attributes)) {
754
            $exclude = func_get_args();
755
        }
756
757
        if (!$attributes || is_string($attributes)) {
758
            $attributes = $this->getAttributes();
759
        }
760
761
        $attributes = (array) $attributes;
762
763
        $attributes = array_filter($attributes, function ($v) {
764
            return ($v || $v === 0 || $v === '0');
765
        });
766
767
        if ($exclude) {
768
            $attributes = array_diff_key(
769
                $attributes,
770
                array_flip($exclude)
771
            );
772
        }
773
774
        // Create markup
775
        $parts = array();
776
777
        foreach ($attributes as $name => $value) {
778
            if ($value === true) {
779
                $parts[] = sprintf('%s="%s"', $name, $name);
780
            } else {
781
                $parts[] = sprintf('%s="%s"', $name, Convert::raw2att($value));
782
            }
783
        }
784
785
        return implode(' ', $parts);
786
    }
787
788
    /**
789
     * Returns a version of a title suitable for insertion into an HTML attribute.
790
     *
791
     * @return string
792
     */
793
    public function attrTitle()
794
    {
795
        return Convert::raw2att($this->title);
796
    }
797
798
    /**
799
     * Returns a version of a title suitable for insertion into an HTML attribute.
800
     *
801
     * @return string
802
     */
803
    public function attrValue()
804
    {
805
        return Convert::raw2att($this->value);
806
    }
807
808
    /**
809
     * Set the field value.
810
     *
811
     * If a FormField requires specific behaviour for loading content from either the database
812
     * or a submitted form value they should override setSubmittedValue() instead.
813
     *
814
     * @param mixed $value Either the parent object, or array of source data being loaded
815
     * @param array|DataObject $data {@see Form::loadDataFrom}
816
     * @return $this
817
     */
818
    public function setValue($value, $data = null)
819
    {
820
        $this->value = $value;
821
        return $this;
822
    }
823
824
    /**
825
     * Set value assigned from a submitted form postback.
826
     * Can be overridden to handle custom behaviour for user-localised
827
     * data formats.
828
     *
829
     * @param mixed $value
830
     * @param array|DataObject $data
831
     * @return $this
832
     */
833
    public function setSubmittedValue($value, $data = null)
834
    {
835
        return $this->setValue($value, $data);
836
    }
837
838
    /**
839
     * Set the field name.
840
     *
841
     * @param string $name
842
     *
843
     * @return $this
844
     */
845
    public function setName($name)
846
    {
847
        $this->name = $name;
848
849
        return $this;
850
    }
851
852
    /**
853
     * Set the field input type.
854
     *
855
     * @param string $type
856
     *
857
     * @return $this
858
     */
859
    public function setInputType($type)
860
    {
861
        $this->inputType = $type;
862
863
        return $this;
864
    }
865
866
    /**
867
     * Set the container form.
868
     *
869
     * This is called automatically when fields are added to forms.
870
     *
871
     * @param Form $form
872
     *
873
     * @return $this
874
     */
875
    public function setForm($form)
876
    {
877
        $this->form = $form;
878
879
        return $this;
880
    }
881
882
    /**
883
     * Get the currently used form.
884
     *
885
     * @return Form
886
     */
887
    public function getForm()
888
    {
889
        return $this->form;
890
    }
891
892
    /**
893
     * Return true if security token protection is enabled on the parent {@link Form}.
894
     *
895
     * @return bool
896
     */
897
    public function securityTokenEnabled()
898
    {
899
        $form = $this->getForm();
900
901
        if (!$form) {
902
            return false;
903
        }
904
905
        return $form->getSecurityToken()->isEnabled();
906
    }
907
908 View Code Duplication
    public function castingHelper($field)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
909
    {
910
        // Override casting for field message
911
        if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
912
            return $helper;
913
        }
914
        return parent::castingHelper($field);
915
    }
916
917
    /**
918
     * Set the custom error message to show instead of the default format.
919
     *
920
     * Different from setError() as that appends it to the standard error messaging.
921
     *
922
     * @param string $customValidationMessage
923
     *
924
     * @return $this
925
     */
926
    public function setCustomValidationMessage($customValidationMessage)
927
    {
928
        $this->customValidationMessage = $customValidationMessage;
929
930
        return $this;
931
    }
932
933
    /**
934
     * Get the custom error message for this form field. If a custom message has not been defined
935
     * then just return blank. The default error is defined on {@link Validator}.
936
     *
937
     * @return string
938
     */
939
    public function getCustomValidationMessage()
940
    {
941
        return $this->customValidationMessage;
942
    }
943
944
    /**
945
     * Set name of template (without path or extension).
946
     *
947
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
948
     * method on the subclass for support.
949
     *
950
     * @param string $template
951
     *
952
     * @return $this
953
     */
954
    public function setTemplate($template)
955
    {
956
        $this->template = $template;
957
958
        return $this;
959
    }
960
961
    /**
962
     * @return string
963
     */
964
    public function getTemplate()
965
    {
966
        return $this->template;
967
    }
968
969
    /**
970
     * @return string
971
     */
972
    public function getFieldHolderTemplate()
973
    {
974
        return $this->fieldHolderTemplate;
975
    }
976
977
    /**
978
     * Set name of template (without path or extension) for the holder, which in turn is
979
     * responsible for rendering {@link Field()}.
980
     *
981
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
982
     * method on the subclass for support.
983
     *
984
     * @param string $fieldHolderTemplate
985
     *
986
     * @return $this
987
     */
988
    public function setFieldHolderTemplate($fieldHolderTemplate)
989
    {
990
        $this->fieldHolderTemplate = $fieldHolderTemplate;
991
992
        return $this;
993
    }
994
995
    /**
996
     * @return string
997
     */
998
    public function getSmallFieldHolderTemplate()
999
    {
1000
        return $this->smallFieldHolderTemplate;
1001
    }
1002
1003
    /**
1004
     * Set name of template (without path or extension) for the small holder, which in turn is
1005
     * responsible for rendering {@link Field()}.
1006
     *
1007
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
1008
     * method on the subclass for support.
1009
     *
1010
     * @param string $smallFieldHolderTemplate
1011
     *
1012
     * @return $this
1013
     */
1014
    public function setSmallFieldHolderTemplate($smallFieldHolderTemplate)
1015
    {
1016
        $this->smallFieldHolderTemplate = $smallFieldHolderTemplate;
1017
1018
        return $this;
1019
    }
1020
1021
    /**
1022
     * Returns the form field.
1023
     *
1024
     * Although FieldHolder is generally what is inserted into templates, all of the field holder
1025
     * templates make use of $Field. It's expected that FieldHolder will give you the "complete"
1026
     * representation of the field on the form, whereas Field will give you the core editing widget,
1027
     * such as an input tag.
1028
     *
1029
     * @param array $properties
1030
     * @return DBHTMLText
1031
     */
1032
    public function Field($properties = array())
1033
    {
1034
        $context = $this;
1035
1036
        if (count($properties)) {
1037
            $context = $context->customise($properties);
1038
        }
1039
1040
        $this->extend('onBeforeRender', $this);
1041
1042
        $result = $context->renderWith($this->getTemplates());
1043
1044
        // Trim whitespace from the result, so that trailing newlines are supressed. Works for strings and HTMLText values
1045
        if (is_string($result)) {
1046
            $result = trim($result);
1047
        } elseif ($result instanceof DBField) {
1048
            $result->setValue(trim($result->getValue()));
1049
        }
1050
1051
        return $result;
1052
    }
1053
1054
    /**
1055
     * Returns a "field holder" for this field.
1056
     *
1057
     * Forms are constructed by concatenating a number of these field holders.
1058
     *
1059
     * The default field holder is a label and a form field inside a div.
1060
     *
1061
     * @see FieldHolder.ss
1062
     *
1063
     * @param array $properties
1064
     *
1065
     * @return DBHTMLText
1066
     */
1067 View Code Duplication
    public function FieldHolder($properties = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1068
    {
1069
        $context = $this;
1070
1071
        if (count($properties)) {
1072
            $context = $this->customise($properties);
1073
        }
1074
1075
        return $context->renderWith($this->getFieldHolderTemplates());
1076
    }
1077
1078
    /**
1079
     * Returns a restricted field holder used within things like FieldGroups.
1080
     *
1081
     * @param array $properties
1082
     *
1083
     * @return string
1084
     */
1085 View Code Duplication
    public function SmallFieldHolder($properties = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1086
    {
1087
        $context = $this;
1088
1089
        if (count($properties)) {
1090
            $context = $this->customise($properties);
1091
        }
1092
1093
        return $context->renderWith($this->getSmallFieldHolderTemplates());
1094
    }
1095
1096
    /**
1097
     * Returns an array of templates to use for rendering {@link FieldHolder}.
1098
     *
1099
     * @return array
1100
     */
1101
    public function getTemplates()
1102
    {
1103
        return $this->_templates($this->getTemplate());
1104
    }
1105
1106
    /**
1107
     * Returns an array of templates to use for rendering {@link FieldHolder}.
1108
     *
1109
     * @return array
1110
     */
1111
    public function getFieldHolderTemplates()
1112
    {
1113
        return $this->_templates(
1114
            $this->getFieldHolderTemplate(),
1115
            '_holder'
1116
        );
1117
    }
1118
1119
    /**
1120
     * Returns an array of templates to use for rendering {@link SmallFieldHolder}.
1121
     *
1122
     * @return array
1123
     */
1124
    public function getSmallFieldHolderTemplates()
1125
    {
1126
        return $this->_templates(
1127
            $this->getSmallFieldHolderTemplate(),
1128
            '_holder_small'
1129
        );
1130
    }
1131
1132
1133
    /**
1134
     * Generate an array of class name strings to use for rendering this form field into HTML.
1135
     *
1136
     * @param string $customTemplate
1137
     * @param string $customTemplateSuffix
1138
     *
1139
     * @return array
1140
     */
1141
    protected function _templates($customTemplate = null, $customTemplateSuffix = null)
1142
    {
1143
        $templates = SSViewer::get_templates_by_class(get_class($this), $customTemplateSuffix, __CLASS__);
1144
        // Prefer any custom template
1145
        if ($customTemplate) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $customTemplate of type string|null 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...
1146
            // Prioritise direct template
1147
            array_unshift($templates, $customTemplate);
1148
        }
1149
        return $templates;
1150
    }
1151
1152
    /**
1153
     * Returns true if this field is a composite field.
1154
     *
1155
     * To create composite field types, you should subclass {@link CompositeField}.
1156
     *
1157
     * @return bool
1158
     */
1159
    public function isComposite()
1160
    {
1161
        return false;
1162
    }
1163
1164
    /**
1165
     * Returns true if this field has its own data.
1166
     *
1167
     * Some fields, such as titles and composite fields, don't actually have any data. It doesn't
1168
     * make sense for data-focused methods to look at them. By overloading hasData() to return
1169
     * false, you can prevent any data-focused methods from looking at it.
1170
     *
1171
     * @see FieldList::collateDataFields()
1172
     *
1173
     * @return bool
1174
     */
1175
    public function hasData()
1176
    {
1177
        return true;
1178
    }
1179
1180
    /**
1181
     * @return bool
1182
     */
1183
    public function isReadonly()
1184
    {
1185
        return $this->readonly;
1186
    }
1187
1188
    /**
1189
     * Sets a read-only flag on this FormField.
1190
     *
1191
     * Use performReadonlyTransformation() to transform this instance.
1192
     *
1193
     * Setting this to false has no effect on the field.
1194
     *
1195
     * @param bool $readonly
1196
     *
1197
     * @return $this
1198
     */
1199
    public function setReadonly($readonly)
1200
    {
1201
        $this->readonly = $readonly;
1202
        return $this;
1203
    }
1204
1205
    /**
1206
     * @return bool
1207
     */
1208
    public function isDisabled()
1209
    {
1210
        return $this->disabled;
1211
    }
1212
1213
    /**
1214
     * Sets a disabled flag on this FormField.
1215
     *
1216
     * Use performDisabledTransformation() to transform this instance.
1217
     *
1218
     * Setting this to false has no effect on the field.
1219
     *
1220
     * @param bool $disabled
1221
     *
1222
     * @return $this
1223
     */
1224
    public function setDisabled($disabled)
1225
    {
1226
        $this->disabled = $disabled;
1227
1228
        return $this;
1229
    }
1230
1231
    /**
1232
     * @return bool
1233
     */
1234
    public function isAutofocus()
1235
    {
1236
        return $this->autofocus;
1237
    }
1238
1239
    /**
1240
     * Sets a autofocus flag on this FormField.
1241
     *
1242
     * @param bool $autofocus
1243
     * @return $this
1244
     */
1245
    public function setAutofocus($autofocus)
1246
    {
1247
        $this->autofocus = $autofocus;
1248
        return $this;
1249
    }
1250
1251
    /**
1252
     * Returns a read-only version of this field.
1253
     *
1254
     * @return FormField
1255
     */
1256
    public function performReadonlyTransformation()
1257
    {
1258
        $readonlyClassName = static::class . '_Readonly';
1259
1260
        if (ClassInfo::exists($readonlyClassName)) {
1261
            $clone = $this->castedCopy($readonlyClassName);
1262
        } else {
1263
            $clone = $this->castedCopy(ReadonlyField::class);
1264
        }
1265
1266
        $clone->setReadonly(true);
1267
1268
        return $clone;
1269
    }
1270
1271
    /**
1272
     * Return a disabled version of this field.
1273
     *
1274
     * Tries to find a class of the class name of this field suffixed with "_Disabled", failing
1275
     * that, finds a method {@link setDisabled()}.
1276
     *
1277
     * @return FormField
1278
     */
1279
    public function performDisabledTransformation()
1280
    {
1281
        $disabledClassName = $this->class . '_Disabled';
1282
1283
        if (ClassInfo::exists($disabledClassName)) {
1284
            $clone = $this->castedCopy($disabledClassName);
1285
        } else {
1286
            $clone = clone $this;
1287
        }
1288
1289
        $clone->setDisabled(true);
1290
1291
        return $clone;
1292
    }
1293
1294
    /**
1295
     * @param FormTransformation $transformation
1296
     *
1297
     * @return mixed
1298
     */
1299
    public function transform(FormTransformation $transformation)
1300
    {
1301
        return $transformation->transform($this);
1302
    }
1303
1304
    /**
1305
     * @param string $class
1306
     *
1307
     * @return int
1308
     */
1309
    public function hasClass($class)
1310
    {
1311
        $patten = '/' . strtolower($class) . '/i';
1312
1313
        $subject = strtolower($this->class . ' ' . $this->extraClass());
1314
1315
        return preg_match($patten, $subject);
1316
    }
1317
1318
    /**
1319
     * Returns the field type.
1320
     *
1321
     * The field type is the class name with the word Field dropped off the end, all lowercase.
1322
     *
1323
     * It's handy for assigning HTML classes. Doesn't signify the <input type> attribute.
1324
     *
1325
     * @see {link getAttributes()}.
1326
     *
1327
     * @return string
1328
     */
1329
    public function Type()
1330
    {
1331
        $type = new ReflectionClass($this);
1332
        return strtolower(preg_replace('/Field$/', '', $type->getShortName()));
1333
    }
1334
1335
    /**
1336
     * Abstract method each {@link FormField} subclass must implement, determines whether the field
1337
     * is valid or not based on the value.
1338
     *
1339
     * @todo Make this abstract.
1340
     *
1341
     * @param Validator $validator
1342
     * @return bool
1343
     */
1344
    public function validate($validator)
1345
    {
1346
        return true;
1347
    }
1348
1349
    /**
1350
     * Describe this field, provide help text for it.
1351
     *
1352
     * By default, renders as a <span class="description"> underneath the form field.
1353
     *
1354
     * @param string $description
1355
     *
1356
     * @return $this
1357
     */
1358
    public function setDescription($description)
1359
    {
1360
        $this->description = $description;
1361
1362
        return $this;
1363
    }
1364
1365
    /**
1366
     * @return string
1367
     */
1368
    public function getDescription()
1369
    {
1370
        return $this->description;
1371
    }
1372
1373
    /**
1374
     * @return string
1375
     */
1376
    public function debug()
1377
    {
1378
        return sprintf(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return sprintf('%s (%s: ...message, $this->value); (string) is incompatible with the return type of the parent method SilverStripe\View\ViewableData::Debug of type SilverStripe\View\ViewableData_Debugger.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1379
            '%s (%s: %s : <span style="color:red;">%s</span>) = %s',
1380
            $this->class,
1381
            $this->name,
1382
            $this->title,
1383
            $this->message,
1384
            $this->value
1385
        );
1386
    }
1387
1388
    /**
1389
     * This function is used by the template processor. If you refer to a field as a $ variable, it
1390
     * will return the $Field value.
1391
     *
1392
     * @return string
1393
     */
1394
    public function forTemplate()
1395
    {
1396
        return $this->Field();
1397
    }
1398
1399
    /**
1400
     * @return bool
1401
     */
1402
    public function Required()
1403
    {
1404
        if ($this->form && ($validator = $this->form->getValidator())) {
1405
            return $validator->fieldIsRequired($this->name);
1406
        }
1407
1408
        return false;
1409
    }
1410
1411
    /**
1412
     * Set the FieldList that contains this field.
1413
     *
1414
     * @param FieldList $containerFieldList
1415
     * @return $this
1416
     */
1417
    public function setContainerFieldList($containerFieldList)
1418
    {
1419
        $this->containerFieldList = $containerFieldList;
1420
        return $this;
1421
    }
1422
1423
    /**
1424
     * Get the FieldList that contains this field.
1425
     *
1426
     * @return FieldList
1427
     */
1428
    public function getContainerFieldList()
1429
    {
1430
        return $this->containerFieldList;
1431
    }
1432
1433
    /**
1434
     * @return null|FieldList
1435
     */
1436
    public function rootFieldList()
1437
    {
1438
        if (is_object($this->containerFieldList)) {
1439
            return $this->containerFieldList->rootFieldList();
1440
        }
1441
1442
        user_error(
1443
            "rootFieldList() called on $this->class object without a containerFieldList",
1444
            E_USER_ERROR
1445
        );
1446
1447
        return null;
1448
    }
1449
1450
    /**
1451
     * Returns another instance of this field, but "cast" to a different class. The logic tries to
1452
     * retain all of the instance properties, and may be overloaded by subclasses to set additional
1453
     * ones.
1454
     *
1455
     * Assumes the standard FormField parameter signature with its name as the only mandatory
1456
     * argument. Mainly geared towards creating *_Readonly or *_Disabled subclasses of the same
1457
     * type, or casting to a {@link ReadonlyField}.
1458
     *
1459
     * Does not copy custom field templates, since they probably won't apply to the new instance.
1460
     *
1461
     * @param mixed $classOrCopy Class name for copy, or existing copy instance to update
1462
     *
1463
     * @return FormField
1464
     */
1465
    public function castedCopy($classOrCopy)
1466
    {
1467
        $field = $classOrCopy;
1468
1469
        if (!is_object($field)) {
1470
            $field = new $classOrCopy($this->name);
1471
        }
1472
1473
        $field
1474
            ->setValue($this->value)
1475
            ->setForm($this->form)
1476
            ->setTitle($this->Title())
1477
            ->setLeftTitle($this->LeftTitle())
1478
            ->setRightTitle($this->RightTitle())
1479
            ->addExtraClass($this->extraClass) // Don't use extraClass(), since this merges calculated values
1480
            ->setDescription($this->getDescription());
1481
1482
        // Only include built-in attributes, ignore anything set through getAttributes().
1483
        // Those might change important characteristics of the field, e.g. its "type" attribute.
1484
        foreach ($this->attributes as $attributeKey => $attributeValue) {
1485
            $field->setAttribute($attributeKey, $attributeValue);
1486
        }
1487
1488
        return $field;
1489
    }
1490
1491
    /**
1492
     * Determine if the value of this formfield accepts front-end submitted values and is saveable.
1493
     *
1494
     * @return bool
1495
     */
1496
    public function canSubmitValue()
1497
    {
1498
        return $this->hasData() && !$this->isReadonly() && !$this->isDisabled();
1499
    }
1500
1501
    /**
1502
     * Sets the component type the FormField will be rendered as on the front-end.
1503
     *
1504
     * @param string $componentType
1505
     * @return FormField
1506
     */
1507
    public function setSchemaComponent($componentType)
1508
    {
1509
        $this->schemaComponent = $componentType;
1510
        return $this;
1511
    }
1512
1513
    /**
1514
     * Gets the type of front-end component the FormField will be rendered as.
1515
     *
1516
     * @return string
1517
     */
1518
    public function getSchemaComponent()
1519
    {
1520
        return $this->schemaComponent;
1521
    }
1522
1523
    /**
1524
     * Sets the schema data used for rendering the field on the front-end.
1525
     * Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
1526
     * Any passed keys that are not defined in {@link getSchemaDataDefaults()} are ignored.
1527
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1528
     *
1529
     * @param array $schemaData - The data to be merged with $this->schemaData.
1530
     * @return FormField
1531
     *
1532
     * @todo Add deep merging of arrays like `data` and `attributes`.
1533
     */
1534
    public function setSchemaData($schemaData = [])
1535
    {
1536
        $defaults = $this->getSchemaData();
1537
        $this->schemaData = array_merge($this->schemaData, array_intersect_key($schemaData, $defaults));
1538
        return $this;
1539
    }
1540
1541
    /**
1542
     * Gets the schema data used to render the FormField on the front-end.
1543
     *
1544
     * @return array
1545
     */
1546
    public function getSchemaData()
1547
    {
1548
        $defaults = $this->getSchemaDataDefaults();
1549
        return array_replace_recursive($defaults, array_intersect_key($this->schemaData, $defaults));
1550
    }
1551
1552
    /**
1553
     * @todo Throw exception if value is missing, once a form field schema is mandatory across the CMS
1554
     *
1555
     * @return string
1556
     */
1557
    public function getSchemaDataType()
1558
    {
1559
        return $this->schemaDataType;
1560
    }
1561
1562
    /**
1563
     * Gets the defaults for $schemaData.
1564
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaData()} are ignored.
1565
     * Instead the `data` array should be used to pass around ad hoc data.
1566
     *
1567
     * @return array
1568
     */
1569
    public function getSchemaDataDefaults()
1570
    {
1571
        return [
1572
            'name' => $this->getName(),
1573
            'id' => $this->ID(),
1574
            'type' => $this->getInputType(),
1575
            'schemaType' => $this->getSchemaDataType(),
1576
            'component' => $this->getSchemaComponent(),
1577
            'holderId' => $this->HolderID(),
1578
            'title' => $this->obj('Title')->getSchemaValue(),
1579
            'source' => null,
1580
            'extraClass' => $this->extraClass(),
1581
            'description' => $this->obj('Description')->getSchemaValue(),
1582
            'rightTitle' => $this->obj('RightTitle')->getSchemaValue(),
1583
            'leftTitle' => $this->obj('LeftTitle')->getSchemaValue(),
1584
            'readOnly' => $this->isReadonly(),
1585
            'disabled' => $this->isDisabled(),
1586
            'customValidationMessage' => $this->getCustomValidationMessage(),
1587
            'validation' => $this->getSchemaValidation(),
1588
            'attributes' => [],
1589
            'autoFocus' => $this->isAutofocus(),
1590
            'data' => [],
1591
        ];
1592
    }
1593
1594
    /**
1595
     * Sets the schema data used for rendering the field on the front-end.
1596
     * Merges the passed array with the current `$schemaState` or {@link getSchemaStateDefaults()}.
1597
     * Any passed keys that are not defined in {@link getSchemaStateDefaults()} are ignored.
1598
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1599
     *
1600
     * @param array $schemaState The data to be merged with $this->schemaData.
1601
     * @return FormField
1602
     *
1603
     * @todo Add deep merging of arrays like `data` and `attributes`.
1604
     */
1605
    public function setSchemaState($schemaState = [])
1606
    {
1607
        $defaults = $this->getSchemaState();
1608
        $this->schemaState = array_merge($this->schemaState, array_intersect_key($schemaState, $defaults));
1609
        return $this;
1610
    }
1611
1612
    /**
1613
     * Gets the schema state used to render the FormField on the front-end.
1614
     *
1615
     * @return array
1616
     */
1617
    public function getSchemaState()
1618
    {
1619
        $defaults = $this->getSchemaStateDefaults();
1620
        return array_merge($defaults, array_intersect_key($this->schemaState, $defaults));
1621
    }
1622
1623
    /**
1624
     * Gets the defaults for $schemaState.
1625
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaState()} are ignored.
1626
     * Instead the `data` array should be used to pass around ad hoc data.
1627
     * Includes validation data if the field is associated to a {@link Form},
1628
     * and {@link Form->validate()} has been called.
1629
     *
1630
     * @todo Make form / field messages not always stored as html; Store value / casting as separate values.
1631
     * @return array
1632
     */
1633
    public function getSchemaStateDefaults()
1634
    {
1635
        $state = [
1636
            'name' => $this->getName(),
1637
            'id' => $this->ID(),
1638
            'value' => $this->Value(),
1639
            'message' => $this->getSchemaMessage(),
1640
            'data' => [],
1641
        ];
1642
1643
        return $state;
1644
    }
1645
1646
    /**
1647
     * Return list of validation rules. Each rule is a key value pair.
1648
     * The key is the rule name. The value is any information the frontend
1649
     * validation handler can understand, or just `true` to enable.
1650
     *
1651
     * @return array
1652
     */
1653
    public function getSchemaValidation()
1654
    {
1655
        if ($this->Required()) {
1656
            return [ 'required' => true ];
1657
        }
1658
        return [];
1659
    }
1660
}
1661