Passed
Pull Request — 4 (#10033)
by Maxime
07:40
created

FormField::getDefaultAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 0
dl 0
loc 19
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use LogicException;
6
use ReflectionClass;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Control\RequestHandler;
9
use SilverStripe\Core\ClassInfo;
10
use SilverStripe\Core\Convert;
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\ORM\ValidationResult;
16
use SilverStripe\View\AttributesHTML;
17
use SilverStripe\View\SSViewer;
18
19
/**
20
 * Represents a field in a form.
21
 *
22
 * A FieldList contains a number of FormField objects which make up the whole of a form.
23
 *
24
 * In addition to single fields, FormField objects can be "composite", for example, the
25
 * {@link TabSet} field. Composite fields let us define complex forms without having to resort to
26
 * custom HTML.
27
 *
28
 * To subclass:
29
 *
30
 * Define a {@link dataValue()} method that returns a value suitable for inserting into a single
31
 * database field.
32
 *
33
 * For example, you might tidy up the format of a date or currency field. Define {@link saveInto()}
34
 * to totally customise saving.
35
 *
36
 * For example, data might be saved to the filesystem instead of the data record, or saved to a
37
 * component of the data record instead of the data record itself.
38
 *
39
 * A form field can be represented as structured data through {@link FormSchema},
40
 * including both structure (name, id, attributes, etc.) and state (field value).
41
 * Can be used by for JSON data which is consumed by a front-end application.
42
 */
43
class FormField extends RequestHandler
44
{
45
    use AttributesHTML;
46
    use FormMessage;
47
48
    /** @see $schemaDataType */
49
    const SCHEMA_DATA_TYPE_STRING = 'String';
50
51
    /** @see $schemaDataType */
52
    const SCHEMA_DATA_TYPE_HIDDEN = 'Hidden';
53
54
    /** @see $schemaDataType */
55
    const SCHEMA_DATA_TYPE_TEXT = 'Text';
56
57
    /** @see $schemaDataType */
58
    const SCHEMA_DATA_TYPE_HTML = 'HTML';
59
60
    /** @see $schemaDataType */
61
    const SCHEMA_DATA_TYPE_INTEGER = 'Integer';
62
63
    /** @see $schemaDataType */
64
    const SCHEMA_DATA_TYPE_DECIMAL = 'Decimal';
65
66
    /** @see $schemaDataType */
67
    const SCHEMA_DATA_TYPE_MULTISELECT = 'MultiSelect';
68
69
    /** @see $schemaDataType */
70
    const SCHEMA_DATA_TYPE_SINGLESELECT = 'SingleSelect';
71
72
    /** @see $schemaDataType */
73
    const SCHEMA_DATA_TYPE_DATE = 'Date';
74
75
    /** @see $schemaDataType */
76
    const SCHEMA_DATA_TYPE_DATETIME = 'Datetime';
77
78
    /** @see $schemaDataType */
79
    const SCHEMA_DATA_TYPE_TIME = 'Time';
80
81
    /** @see $schemaDataType */
82
    const SCHEMA_DATA_TYPE_BOOLEAN = 'Boolean';
83
84
    /** @see $schemaDataType */
85
    const SCHEMA_DATA_TYPE_CUSTOM = 'Custom';
86
87
    /** @see $schemaDataType */
88
    const SCHEMA_DATA_TYPE_STRUCTURAL = 'Structural';
89
90
    /**
91
     * @var Form
92
     */
93
    protected $form;
94
95
    /**
96
     * This is INPUT's type attribute value.
97
     *
98
     * @var string
99
     */
100
    protected $inputType = 'text';
101
102
    /**
103
     * @var string
104
     */
105
    protected $name;
106
107
    /**
108
     * @var null|string
109
     */
110
    protected $title;
111
112
    /**
113
     * @var mixed
114
     */
115
    protected $value;
116
117
    /**
118
     * @var string
119
     */
120
    protected $extraClass;
121
122
    /**
123
     * Adds a title attribute to the markup.
124
     *
125
     * @var string
126
     *
127
     * @todo Implement in all subclasses
128
     */
129
    protected $description;
130
131
    /**
132
     * Extra CSS classes for the FormField container.
133
     *
134
     * @var array
135
     */
136
    protected $extraClasses;
137
138
    /**
139
     * @config
140
     * @var array $default_classes The default classes to apply to the FormField
141
     */
142
    private static $default_classes = [];
0 ignored issues
show
introduced by
The private property $default_classes is not used, and could be removed.
Loading history...
143
144
    /**
145
     * Right-aligned, contextual label for the field.
146
     *
147
     * @var string
148
     */
149
    protected $rightTitle;
150
151
    /**
152
     * Left-aligned, contextual label for the field.
153
     *
154
     * @var string
155
     */
156
    protected $leftTitle;
157
158
    /**
159
     * Stores a reference to the FieldList that contains this object.
160
     *
161
     * @var FieldList
162
     */
163
    protected $containerFieldList;
164
165
    /**
166
     * @var bool
167
     */
168
    protected $readonly = false;
169
170
    /**
171
     * @var bool
172
     */
173
    protected $disabled = false;
174
175
    /**
176
     * @var bool
177
     */
178
    protected $autofocus = false;
179
180
    /**
181
     * Custom validation message for the field.
182
     *
183
     * @var string
184
     */
185
    protected $customValidationMessage = '';
186
187
    /**
188
     * Name of the template used to render this form field. If not set, then will look up the class
189
     * ancestry for the first matching template where the template name equals the class name.
190
     *
191
     * To explicitly use a custom template or one named other than the form field see
192
     * {@link setTemplate()}.
193
     *
194
     * @var string
195
     */
196
    protected $template;
197
198
    /**
199
     * Name of the template used to render this form field. If not set, then will look up the class
200
     * ancestry for the first matching template where the template name equals the class name.
201
     *
202
     * To explicitly use a custom template or one named other than the form field see
203
     * {@link setFieldHolderTemplate()}.
204
     *
205
     * @var string
206
     */
207
    protected $fieldHolderTemplate;
208
209
    /**
210
     * @var string
211
     */
212
    protected $smallFieldHolderTemplate;
213
214
    /**
215
     * The data type backing the field. Represents the type of value the
216
     * form expects to receive via a postback. Should be set in subclasses.
217
     *
218
     * The values allowed in this list include:
219
     *
220
     *   - String: Single line text
221
     *   - Hidden: Hidden field which is posted back without modification
222
     *   - Text: Multi line text
223
     *   - HTML: Rich html text
224
     *   - Integer: Whole number value
225
     *   - Decimal: Decimal value
226
     *   - MultiSelect: Select many from source
227
     *   - SingleSelect: Select one from source
228
     *   - Date: Date only
229
     *   - DateTime: Date and time
230
     *   - Time: Time only
231
     *   - Boolean: Yes or no
232
     *   - Custom: Custom type declared by the front-end component. For fields with this type,
233
     *     the component property is mandatory, and will determine the posted value for this field.
234
     *   - Structural: Represents a field that is NOT posted back. This may contain other fields,
235
     *     or simply be a block of stand-alone content. As with 'Custom',
236
     *     the component property is mandatory if this is assigned.
237
     *
238
     * Each value has an equivalent constant, e.g. {@link self::SCHEMA_DATA_TYPE_STRING}.
239
     *
240
     * @var string
241
     */
242
    protected $schemaDataType;
243
244
    /**
245
     * The type of front-end component to render the FormField as.
246
     *
247
     * @skipUpgrade
248
     * @var string
249
     */
250
    protected $schemaComponent;
251
252
    /**
253
     * Structured schema data representing the FormField.
254
     * Used to render the FormField as a ReactJS Component on the front-end.
255
     *
256
     * @var array
257
     */
258
    protected $schemaData = [];
259
260
    private static $casting = [
0 ignored issues
show
introduced by
The private property $casting is not used, and could be removed.
Loading history...
261
        'FieldHolder' => 'HTMLFragment',
262
        'SmallFieldHolder' => 'HTMLFragment',
263
        'Field' => 'HTMLFragment',
264
        'AttributesHTML' => 'HTMLFragment', // property $AttributesHTML version
265
        'getAttributesHTML' => 'HTMLFragment', // method $getAttributesHTML($arg) version
266
        'Value' => 'Text',
267
        'extraClass' => 'Text',
268
        'ID' => 'Text',
269
        'isReadOnly' => 'Boolean',
270
        'HolderID' => 'Text',
271
        'Title' => 'Text',
272
        'RightTitle' => 'Text',
273
        'Description' => 'HTMLFragment',
274
    ];
275
276
    /**
277
     * Structured schema state representing the FormField's current data and validation.
278
     * Used to render the FormField as a ReactJS Component on the front-end.
279
     *
280
     * @var array
281
     */
282
    protected $schemaState = [];
283
284
    /**
285
     * Takes a field name and converts camelcase to spaced words. Also resolves combined field
286
     * names with dot syntax to spaced words.
287
     *
288
     * Examples:
289
     *
290
     * - 'TotalAmount' will return 'Total amount'
291
     * - 'Organisation.ZipCode' will return 'Organisation zip code'
292
     *
293
     * @param string $fieldName
294
     *
295
     * @return string
296
     */
297
    public static function name_to_label($fieldName)
298
    {
299
        // Handle dot delimiters
300
        if (strpos($fieldName, '.') !== false) {
301
            $parts = explode('.', $fieldName);
302
            // Ensure that any letter following a dot is uppercased, so that the regex below can break it up
303
            // into words
304
            $label = implode(array_map('ucfirst', $parts));
305
        } else {
306
            $label = $fieldName;
307
        }
308
309
        // Replace any capital letter that is followed by a lowercase letter with a space, the lowercased
310
        // version of itself then the remaining lowercase letters.
311
        $labelWithSpaces = preg_replace_callback('/([A-Z])([a-z]+)/', function ($matches) {
312
            return ' ' . strtolower($matches[1]) . $matches[2];
313
        }, $label);
314
315
        // Add a space before any capital letter block that is at the end of the string
316
        $labelWithSpaces = preg_replace('/([a-z])([A-Z]+)$/', '$1 $2', $labelWithSpaces);
317
318
        // The first letter should be uppercase
319
        return ucfirst(trim($labelWithSpaces));
320
    }
321
322
    /**
323
     * Creates a new field.
324
     *
325
     * @param string $name The internal field name, passed to forms.
326
     * @param null|string|\SilverStripe\View\ViewableData $title The human-readable field label.
327
     * @param mixed $value The value of the field.
328
     */
329
    public function __construct($name, $title = null, $value = null)
330
    {
331
        $this->setName($name);
332
333
        if ($title === null) {
334
            $this->title = self::name_to_label($name);
335
        } else {
336
            $this->title = $title;
337
        }
338
339
        if ($value !== null) {
340
            $this->setValue($value);
341
        }
342
343
        parent::__construct();
344
345
        $this->setupDefaultClasses();
346
    }
347
348
    /**
349
     * Set up the default classes for the form. This is done on construct so that the default classes can be removed
350
     * after instantiation
351
     */
352
    protected function setupDefaultClasses()
353
    {
354
        $defaultClasses = $this->config()->get('default_classes');
355
        if ($defaultClasses) {
356
            foreach ($defaultClasses as $class) {
357
                $this->addExtraClass($class);
358
            }
359
        }
360
    }
361
362
    /**
363
     * Return a link to this field
364
     *
365
     * @param string $action
366
     * @return string
367
     * @throws LogicException If no form is set yet
368
     */
369
    public function Link($action = null)
370
    {
371
        if (!$this->form) {
372
            throw new LogicException(
373
                'Field must be associated with a form to call Link(). Please use $field->setForm($form);'
374
            );
375
        }
376
377
        $link = Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
378
        $this->extend('updateLink', $link, $action);
379
        return $link;
380
    }
381
382
    /**
383
     * Returns the HTML ID of the field.
384
     *
385
     * The ID is generated as FormName_FieldName. All Field functions should ensure that this ID is
386
     * included in the field.
387
     *
388
     * @return string
389
     */
390
    public function ID()
391
    {
392
        return $this->getTemplateHelper()->generateFieldID($this);
393
    }
394
395
    /**
396
     * Returns the HTML ID for the form field holder element.
397
     *
398
     * @return string
399
     */
400
    public function HolderID()
401
    {
402
        return $this->getTemplateHelper()->generateFieldHolderID($this);
403
    }
404
405
    /**
406
     * Returns the current {@link FormTemplateHelper} on either the parent
407
     * Form or the global helper set through the {@link Injector} layout.
408
     *
409
     * To customize a single {@link FormField}, use {@link setTemplate} and
410
     * provide a custom template name.
411
     *
412
     * @return FormTemplateHelper
413
     */
414
    public function getTemplateHelper()
415
    {
416
        if ($this->form) {
417
            return $this->form->getTemplateHelper();
418
        }
419
420
        return FormTemplateHelper::singleton();
421
    }
422
423
    /**
424
     * Returns the field name.
425
     *
426
     * @return string
427
     */
428
    public function getName()
429
    {
430
        return $this->name;
431
    }
432
433
    /**
434
     * Returns the field input name.
435
     *
436
     * @return string
437
     */
438
    public function getInputType()
439
    {
440
        return $this->inputType;
441
    }
442
443
    /**
444
     * Returns the field value.
445
     *
446
     * @see FormField::setSubmittedValue()
447
     * @return mixed
448
     */
449
    public function Value()
450
    {
451
        return $this->value;
452
    }
453
454
    /**
455
     * Method to save this form field into the given {@link DataObject}.
456
     *
457
     * By default, makes use of $this->dataValue()
458
     *
459
     * @param DataObject|DataObjectInterface $record DataObject to save data into
460
     */
461
    public function saveInto(DataObjectInterface $record)
462
    {
463
        $component = $record;
464
        $fieldName = $this->name;
465
466
        // Allow for dot syntax
467
        if (($pos = strrpos($this->name, '.')) !== false) {
468
            $relation = substr($this->name, 0, $pos);
469
            $fieldName = substr($this->name, $pos + 1);
470
            $component = $record->relObject($relation);
0 ignored issues
show
Bug introduced by
The method relObject() does not exist on SilverStripe\ORM\DataObjectInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to SilverStripe\ORM\DataObjectInterface. ( Ignorable by Annotation )

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

470
            /** @scrutinizer ignore-call */ 
471
            $component = $record->relObject($relation);
Loading history...
471
        }
472
473
        if ($fieldName) {
474
            $component->setCastedField($fieldName, $this->dataValue());
475
        }
476
    }
477
478
    /**
479
     * Returns the field value suitable for insertion into the data object.
480
     * @see Formfield::setValue()
481
     * @return mixed
482
     */
483
    public function dataValue()
484
    {
485
        return $this->value;
486
    }
487
488
    /**
489
     * Returns the field label - used by templates.
490
     *
491
     * @return string
492
     */
493
    public function Title()
494
    {
495
        return $this->title;
496
    }
497
498
    /**
499
     * Set the title of this formfield.
500
     * Note: This expects escaped HTML.
501
     *
502
     * @param string $title Escaped HTML for title
503
     * @return $this
504
     */
505
    public function setTitle($title)
506
    {
507
        $this->title = $title;
508
        return $this;
509
    }
510
511
    /**
512
     * Gets the contextual label than can be used for additional field description.
513
     * Can be shown to the right or under the field in question.
514
     *
515
     * @return string Contextual label text
516
     */
517
    public function RightTitle()
518
    {
519
        return $this->rightTitle;
520
    }
521
522
    /**
523
     * Sets the right title for this formfield
524
     *
525
     * @param string|DBField $rightTitle Plain text string, or a DBField with appropriately escaped HTML
526
     * @return $this
527
     */
528
    public function setRightTitle($rightTitle)
529
    {
530
        $this->rightTitle = $rightTitle;
531
        return $this;
532
    }
533
534
    /**
535
     * @return string
536
     */
537
    public function LeftTitle()
538
    {
539
        return $this->leftTitle;
540
    }
541
542
    /**
543
     * @param string $leftTitle
544
     *
545
     * @return $this
546
     */
547
    public function setLeftTitle($leftTitle)
548
    {
549
        $this->leftTitle = $leftTitle;
550
551
        return $this;
552
    }
553
554
    /**
555
     * Compiles all CSS-classes. Optionally includes a "form-group--no-label" class if no title was set on the
556
     * FormField.
557
     *
558
     * Uses {@link Message()} and {@link MessageType()} to add validation error classes which can
559
     * be used to style the contained tags.
560
     *
561
     * @return string
562
     */
563
    public function extraClass()
564
    {
565
        $classes = [];
566
567
        $classes[] = $this->Type();
568
569
        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...
570
            $classes = array_merge(
571
                $classes,
572
                array_values($this->extraClasses)
573
            );
574
        }
575
576
        if (!$this->Title()) {
577
            $classes[] = 'form-group--no-label';
578
        }
579
580
        // Allow custom styling of any element in the container based on validation errors,
581
        // e.g. red borders on input tags.
582
        //
583
        // CSS class needs to be different from the one rendered through {@link FieldHolder()}.
584
        if ($this->getMessage()) {
585
            $classes[] .= 'holder-' . $this->getMessageType();
586
        }
587
588
        return implode(' ', $classes);
589
    }
590
591
    /**
592
     * Add one or more CSS-classes to the FormField container.
593
     *
594
     * Multiple class names should be space delimited.
595
     *
596
     * @param string $class
597
     *
598
     * @return $this
599
     */
600
    public function addExtraClass($class)
601
    {
602
        $classes = preg_split('/\s+/', $class);
603
604
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
605
            $this->extraClasses[$class] = $class;
606
        }
607
608
        return $this;
609
    }
610
611
    /**
612
     * Remove one or more CSS-classes from the FormField container.
613
     *
614
     * @param string $class
615
     *
616
     * @return $this
617
     */
618
    public function removeExtraClass($class)
619
    {
620
        $classes = preg_split('/\s+/', $class);
621
622
        foreach ($classes as $class) {
0 ignored issues
show
introduced by
$class is overwriting one of the parameters of this function.
Loading history...
623
            unset($this->extraClasses[$class]);
624
        }
625
626
        return $this;
627
    }
628
629
    protected function getDefaultAttributes(): array
630
    {
631
        $attributes = [
632
            'type' => $this->getInputType(),
633
            'name' => $this->getName(),
634
            'value' => $this->Value(),
635
            'class' => $this->extraClass(),
636
            'id' => $this->ID(),
637
            'disabled' => $this->isDisabled(),
638
            'readonly' => $this->isReadonly(),
639
            'autofocus' => $this->isAutofocus()
640
        ];
641
642
        if ($this->Required()) {
643
            $attributes['required'] = 'required';
644
            $attributes['aria-required'] = 'true';
645
        }
646
647
        return $attributes;
648
    }
649
650
    /**
651
     * Returns a version of a title suitable for insertion into an HTML attribute.
652
     *
653
     * @return string
654
     */
655
    public function attrTitle()
656
    {
657
        return Convert::raw2att($this->title);
658
    }
659
660
    /**
661
     * Returns a version of a title suitable for insertion into an HTML attribute.
662
     *
663
     * @return string
664
     */
665
    public function attrValue()
666
    {
667
        return Convert::raw2att($this->value);
668
    }
669
670
    /**
671
     * Set the field value.
672
     *
673
     * If a FormField requires specific behaviour for loading content from either the database
674
     * or a submitted form value they should override setSubmittedValue() instead.
675
     *
676
     * @param mixed $value Either the parent object, or array of source data being loaded
677
     * @param array|DataObject $data {@see Form::loadDataFrom}
678
     * @return $this
679
     */
680
    public function setValue($value, $data = null)
681
    {
682
        $this->value = $value;
683
        return $this;
684
    }
685
686
    /**
687
     * Set value assigned from a submitted form postback.
688
     * Can be overridden to handle custom behaviour for user-localised
689
     * data formats.
690
     *
691
     * @param mixed $value
692
     * @param array|DataObject $data
693
     * @return $this
694
     */
695
    public function setSubmittedValue($value, $data = null)
696
    {
697
        return $this->setValue($value, $data);
698
    }
699
700
    /**
701
     * Set the field name.
702
     *
703
     * @param string $name
704
     *
705
     * @return $this
706
     */
707
    public function setName($name)
708
    {
709
        $this->name = $name;
710
711
        return $this;
712
    }
713
714
    /**
715
     * Set the field input type.
716
     *
717
     * @param string $type
718
     *
719
     * @return $this
720
     */
721
    public function setInputType($type)
722
    {
723
        $this->inputType = $type;
724
725
        return $this;
726
    }
727
728
    /**
729
     * Set the container form.
730
     *
731
     * This is called automatically when fields are added to forms.
732
     *
733
     * @param Form $form
734
     *
735
     * @return $this
736
     */
737
    public function setForm($form)
738
    {
739
        $this->form = $form;
740
741
        return $this;
742
    }
743
744
    /**
745
     * Get the currently used form.
746
     *
747
     * @return Form
748
     */
749
    public function getForm()
750
    {
751
        return $this->form;
752
    }
753
754
    /**
755
     * Return true if security token protection is enabled on the parent {@link Form}.
756
     *
757
     * @return bool
758
     */
759
    public function securityTokenEnabled()
760
    {
761
        $form = $this->getForm();
762
763
        if (!$form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
764
            return false;
765
        }
766
767
        return $form->getSecurityToken()->isEnabled();
768
    }
769
770
    public function castingHelper($field)
771
    {
772
        // Override casting for field message
773
        if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
774
            return $helper;
775
        }
776
        return parent::castingHelper($field);
777
    }
778
779
    /**
780
     * Set the custom error message to show instead of the default format.
781
     *
782
     * Different from setError() as that appends it to the standard error messaging.
783
     *
784
     * @param string $customValidationMessage
785
     *
786
     * @return $this
787
     */
788
    public function setCustomValidationMessage($customValidationMessage)
789
    {
790
        $this->customValidationMessage = $customValidationMessage;
791
792
        return $this;
793
    }
794
795
    /**
796
     * Get the custom error message for this form field. If a custom message has not been defined
797
     * then just return blank. The default error is defined on {@link Validator}.
798
     *
799
     * @return string
800
     */
801
    public function getCustomValidationMessage()
802
    {
803
        return $this->customValidationMessage;
804
    }
805
806
    /**
807
     * Set name of template (without path or extension).
808
     *
809
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
810
     * method on the subclass for support.
811
     *
812
     * @param string $template
813
     *
814
     * @return $this
815
     */
816
    public function setTemplate($template)
817
    {
818
        $this->template = $template;
819
820
        return $this;
821
    }
822
823
    /**
824
     * @return string
825
     */
826
    public function getTemplate()
827
    {
828
        return $this->template;
829
    }
830
831
    /**
832
     * @return string
833
     */
834
    public function getFieldHolderTemplate()
835
    {
836
        return $this->fieldHolderTemplate;
837
    }
838
839
    /**
840
     * Set name of template (without path or extension) for the holder, which in turn is
841
     * responsible for rendering {@link Field()}.
842
     *
843
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
844
     * method on the subclass for support.
845
     *
846
     * @param string $fieldHolderTemplate
847
     *
848
     * @return $this
849
     */
850
    public function setFieldHolderTemplate($fieldHolderTemplate)
851
    {
852
        $this->fieldHolderTemplate = $fieldHolderTemplate;
853
854
        return $this;
855
    }
856
857
    /**
858
     * @return string
859
     */
860
    public function getSmallFieldHolderTemplate()
861
    {
862
        return $this->smallFieldHolderTemplate;
863
    }
864
865
    /**
866
     * Set name of template (without path or extension) for the small holder, which in turn is
867
     * responsible for rendering {@link Field()}.
868
     *
869
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
870
     * method on the subclass for support.
871
     *
872
     * @param string $smallFieldHolderTemplate
873
     *
874
     * @return $this
875
     */
876
    public function setSmallFieldHolderTemplate($smallFieldHolderTemplate)
877
    {
878
        $this->smallFieldHolderTemplate = $smallFieldHolderTemplate;
879
880
        return $this;
881
    }
882
883
    /**
884
     * Returns the form field.
885
     *
886
     * Although FieldHolder is generally what is inserted into templates, all of the field holder
887
     * templates make use of $Field. It's expected that FieldHolder will give you the "complete"
888
     * representation of the field on the form, whereas Field will give you the core editing widget,
889
     * such as an input tag.
890
     *
891
     * @param array $properties
892
     * @return DBHTMLText
893
     */
894
    public function Field($properties = [])
895
    {
896
        $context = $this;
897
898
        $this->extend('onBeforeRender', $context, $properties);
899
900
        if (count($properties)) {
901
            $context = $context->customise($properties);
902
        }
903
904
        $result = $context->renderWith($this->getTemplates());
905
906
        // Trim whitespace from the result, so that trailing newlines are suppressed. Works for strings and HTMLText values
907
        if (is_string($result)) {
908
            $result = trim($result);
909
        } elseif ($result instanceof DBField) {
910
            $result->setValue(trim($result->getValue()));
911
        }
912
913
        return $result;
914
    }
915
916
    /**
917
     * Returns a "field holder" for this field.
918
     *
919
     * Forms are constructed by concatenating a number of these field holders.
920
     *
921
     * The default field holder is a label and a form field inside a div.
922
     *
923
     * @see FieldHolder.ss
924
     *
925
     * @param array $properties
926
     *
927
     * @return DBHTMLText
928
     */
929
    public function FieldHolder($properties = [])
930
    {
931
        $context = $this;
932
933
        $this->extend('onBeforeRenderHolder', $context, $properties);
934
935
        if (count($properties)) {
936
            $context = $this->customise($properties);
937
        }
938
939
        return $context->renderWith($this->getFieldHolderTemplates());
940
    }
941
942
    /**
943
     * Returns a restricted field holder used within things like FieldGroups.
944
     *
945
     * @param array $properties
946
     *
947
     * @return string
948
     */
949
    public function SmallFieldHolder($properties = [])
950
    {
951
        $context = $this;
952
953
        if (count($properties)) {
954
            $context = $this->customise($properties);
955
        }
956
957
        return $context->renderWith($this->getSmallFieldHolderTemplates());
958
    }
959
960
    /**
961
     * Returns an array of templates to use for rendering {@link FieldHolder}.
962
     *
963
     * @return array
964
     */
965
    public function getTemplates()
966
    {
967
        return $this->_templates($this->getTemplate());
968
    }
969
970
    /**
971
     * Returns an array of templates to use for rendering {@link FieldHolder}.
972
     *
973
     * @return array
974
     */
975
    public function getFieldHolderTemplates()
976
    {
977
        return $this->_templates(
978
            $this->getFieldHolderTemplate(),
979
            '_holder'
980
        );
981
    }
982
983
    /**
984
     * Returns an array of templates to use for rendering {@link SmallFieldHolder}.
985
     *
986
     * @return array
987
     */
988
    public function getSmallFieldHolderTemplates()
989
    {
990
        return $this->_templates(
991
            $this->getSmallFieldHolderTemplate(),
992
            '_holder_small'
993
        );
994
    }
995
996
997
    /**
998
     * Generate an array of class name strings to use for rendering this form field into HTML.
999
     *
1000
     * @param string $customTemplate
1001
     * @param string $customTemplateSuffix
1002
     *
1003
     * @return array
1004
     */
1005
    protected function _templates($customTemplate = null, $customTemplateSuffix = null)
1006
    {
1007
        $templates = SSViewer::get_templates_by_class(static::class, $customTemplateSuffix, __CLASS__);
1008
        // Prefer any custom template
1009
        if ($customTemplate) {
1010
            // Prioritise direct template
1011
            array_unshift($templates, $customTemplate);
1012
        }
1013
        return $templates;
1014
    }
1015
1016
    /**
1017
     * Returns true if this field is a composite field.
1018
     *
1019
     * To create composite field types, you should subclass {@link CompositeField}.
1020
     *
1021
     * @return bool
1022
     */
1023
    public function isComposite()
1024
    {
1025
        return false;
1026
    }
1027
1028
    /**
1029
     * Returns true if this field has its own data.
1030
     *
1031
     * Some fields, such as titles and composite fields, don't actually have any data. It doesn't
1032
     * make sense for data-focused methods to look at them. By overloading hasData() to return
1033
     * false, you can prevent any data-focused methods from looking at it.
1034
     *
1035
     * @see FieldList::collateDataFields()
1036
     *
1037
     * @return bool
1038
     */
1039
    public function hasData()
1040
    {
1041
        return true;
1042
    }
1043
1044
    /**
1045
     * @return bool
1046
     */
1047
    public function isReadonly()
1048
    {
1049
        return $this->readonly;
1050
    }
1051
1052
    /**
1053
     * Sets a read-only flag on this FormField.
1054
     *
1055
     * Use performReadonlyTransformation() to transform this instance.
1056
     *
1057
     * Setting this to false has no effect on the field.
1058
     *
1059
     * @param bool $readonly
1060
     *
1061
     * @return $this
1062
     */
1063
    public function setReadonly($readonly)
1064
    {
1065
        $this->readonly = $readonly;
1066
        return $this;
1067
    }
1068
1069
    /**
1070
     * @return bool
1071
     */
1072
    public function isDisabled()
1073
    {
1074
        return $this->disabled;
1075
    }
1076
1077
    /**
1078
     * Sets a disabled flag on this FormField.
1079
     *
1080
     * Use performDisabledTransformation() to transform this instance.
1081
     *
1082
     * Setting this to false has no effect on the field.
1083
     *
1084
     * @param bool $disabled
1085
     *
1086
     * @return $this
1087
     */
1088
    public function setDisabled($disabled)
1089
    {
1090
        $this->disabled = $disabled;
1091
1092
        return $this;
1093
    }
1094
1095
    /**
1096
     * @return bool
1097
     */
1098
    public function isAutofocus()
1099
    {
1100
        return $this->autofocus;
1101
    }
1102
1103
    /**
1104
     * Sets a autofocus flag on this FormField.
1105
     *
1106
     * @param bool $autofocus
1107
     * @return $this
1108
     */
1109
    public function setAutofocus($autofocus)
1110
    {
1111
        $this->autofocus = $autofocus;
1112
        return $this;
1113
    }
1114
1115
    /**
1116
     * Returns a read-only version of this field.
1117
     *
1118
     * @return FormField
1119
     */
1120
    public function performReadonlyTransformation()
1121
    {
1122
        $readonlyClassName = static::class . '_Readonly';
1123
1124
        if (ClassInfo::exists($readonlyClassName)) {
1125
            $clone = $this->castedCopy($readonlyClassName);
1126
        } else {
1127
            $clone = $this->castedCopy(ReadonlyField::class);
1128
        }
1129
1130
        $clone->setReadonly(true);
1131
1132
        return $clone;
1133
    }
1134
1135
    /**
1136
     * Return a disabled version of this field.
1137
     *
1138
     * Tries to find a class of the class name of this field suffixed with "_Disabled", failing
1139
     * that, finds a method {@link setDisabled()}.
1140
     *
1141
     * @return FormField
1142
     */
1143
    public function performDisabledTransformation()
1144
    {
1145
        $disabledClassName = static::class . '_Disabled';
1146
1147
        if (ClassInfo::exists($disabledClassName)) {
1148
            $clone = $this->castedCopy($disabledClassName);
1149
        } else {
1150
            $clone = clone $this;
1151
        }
1152
1153
        $clone->setDisabled(true);
1154
1155
        return $clone;
1156
    }
1157
1158
    /**
1159
     * @param FormTransformation $transformation
1160
     *
1161
     * @return mixed
1162
     */
1163
    public function transform(FormTransformation $transformation)
1164
    {
1165
        return $transformation->transform($this);
1166
    }
1167
1168
    /**
1169
     * Returns whether the current field has the given class added
1170
     *
1171
     * @param string $class
1172
     *
1173
     * @return bool
1174
     */
1175
    public function hasClass($class)
1176
    {
1177
        $classes = explode(' ', strtolower($this->extraClass()));
1178
        return in_array(strtolower(trim($class)), $classes);
1179
    }
1180
1181
    /**
1182
     * Returns the field type.
1183
     *
1184
     * The field type is the class name with the word Field dropped off the end, all lowercase.
1185
     *
1186
     * It's handy for assigning HTML classes. Doesn't signify the input type attribute.
1187
     *
1188
     * @see {@link getAttributes()}.
1189
     *
1190
     * @return string
1191
     */
1192
    public function Type()
1193
    {
1194
        $type = new ReflectionClass($this);
1195
        return strtolower(preg_replace('/Field$/', '', $type->getShortName()));
1196
    }
1197
1198
    /**
1199
     * Abstract method each {@link FormField} subclass must implement, determines whether the field
1200
     * is valid or not based on the value.
1201
     *
1202
     * @todo Make this abstract.
1203
     *
1204
     * @param Validator $validator
1205
     * @return bool
1206
     */
1207
    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

1207
    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...
1208
    {
1209
        return true;
1210
    }
1211
1212
    /**
1213
     * Describe this field, provide help text for it.
1214
     *
1215
     * By default, renders as a span class="description" underneath the form field.
1216
     *
1217
     * @param string $description
1218
     *
1219
     * @return $this
1220
     */
1221
    public function setDescription($description)
1222
    {
1223
        $this->description = $description;
1224
1225
        return $this;
1226
    }
1227
1228
    /**
1229
     * @return string
1230
     */
1231
    public function getDescription()
1232
    {
1233
        return $this->description;
1234
    }
1235
1236
    /**
1237
     * @return string
1238
     */
1239
    public function debug()
1240
    {
1241
        $strValue = is_string($this->value) ? $this->value : print_r($this->value, true);
1242
1243
        return sprintf(
1244
            '%s (%s: %s : <span style="color:red;">%s</span>) = %s',
1245
            Convert::raw2att(static::class),
1246
            Convert::raw2att($this->name),
1247
            Convert::raw2att($this->title),
1248
            $this->getMessageCast() == ValidationResult::CAST_HTML ? Convert::raw2xml($this->message) : $this->message,
1249
            Convert::raw2att($strValue)
0 ignored issues
show
Bug introduced by
It seems like $strValue can also be of type true; however, parameter $val of SilverStripe\Core\Convert::raw2att() does only seem to accept array|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

1249
            Convert::raw2att(/** @scrutinizer ignore-type */ $strValue)
Loading history...
1250
        );
1251
    }
1252
1253
    /**
1254
     * This function is used by the template processor. If you refer to a field as a $ variable, it
1255
     * will return the $Field value.
1256
     *
1257
     * @return string
1258
     */
1259
    public function forTemplate()
1260
    {
1261
        return $this->Field();
1262
    }
1263
1264
    /**
1265
     * @return bool
1266
     */
1267
    public function Required()
1268
    {
1269
        if ($this->form && ($validator = $this->form->getValidator())) {
1270
            return $validator->fieldIsRequired($this->name);
1271
        }
1272
1273
        return false;
1274
    }
1275
1276
    /**
1277
     * Set the FieldList that contains this field.
1278
     *
1279
     * @param FieldList $containerFieldList
1280
     * @return $this
1281
     */
1282
    public function setContainerFieldList($containerFieldList)
1283
    {
1284
        $this->containerFieldList = $containerFieldList;
1285
        return $this;
1286
    }
1287
1288
    /**
1289
     * Get the FieldList that contains this field.
1290
     *
1291
     * @return FieldList
1292
     */
1293
    public function getContainerFieldList()
1294
    {
1295
        return $this->containerFieldList;
1296
    }
1297
1298
    /**
1299
     * @return null|FieldList
1300
     */
1301
    public function rootFieldList()
1302
    {
1303
        if ($this->containerFieldList) {
1304
            return $this->containerFieldList->rootFieldList();
1305
        }
1306
1307
        $class = static::class;
1308
        throw new \RuntimeException("rootFieldList() called on {$class} object without a containerFieldList");
1309
    }
1310
1311
    /**
1312
     * Returns another instance of this field, but "cast" to a different class. The logic tries to
1313
     * retain all of the instance properties, and may be overloaded by subclasses to set additional
1314
     * ones.
1315
     *
1316
     * Assumes the standard FormField parameter signature with its name as the only mandatory
1317
     * argument. Mainly geared towards creating *_Readonly or *_Disabled subclasses of the same
1318
     * type, or casting to a {@link ReadonlyField}.
1319
     *
1320
     * Does not copy custom field templates, since they probably won't apply to the new instance.
1321
     *
1322
     * @param mixed $classOrCopy Class name for copy, or existing copy instance to update
1323
     *
1324
     * @return FormField
1325
     */
1326
    public function castedCopy($classOrCopy)
1327
    {
1328
        $field = $classOrCopy;
1329
1330
        if (!is_object($field)) {
1331
            $field = $classOrCopy::create($this->name);
1332
        }
1333
1334
        $field
1335
            ->setValue($this->value)
1336
            ->setForm($this->form)
1337
            ->setTitle($this->Title())
1338
            ->setLeftTitle($this->LeftTitle())
1339
            ->setRightTitle($this->RightTitle())
1340
            ->addExtraClass($this->extraClass) // Don't use extraClass(), since this merges calculated values
1341
            ->setDescription($this->getDescription());
1342
1343
        // Only include built-in attributes, ignore anything set through getAttributes().
1344
        // Those might change important characteristics of the field, e.g. its "type" attribute.
1345
        foreach ($this->attributes as $attributeKey => $attributeValue) {
1346
            $field->setAttribute($attributeKey, $attributeValue);
1347
        }
1348
1349
        return $field;
1350
    }
1351
1352
    /**
1353
     * Determine if the value of this formfield accepts front-end submitted values and is saveable.
1354
     *
1355
     * @return bool
1356
     */
1357
    public function canSubmitValue()
1358
    {
1359
        return $this->hasData() && !$this->isReadonly() && !$this->isDisabled();
1360
    }
1361
1362
    /**
1363
     * Sets the component type the FormField will be rendered as on the front-end.
1364
     *
1365
     * @param string $componentType
1366
     * @return FormField
1367
     */
1368
    public function setSchemaComponent($componentType)
1369
    {
1370
        $this->schemaComponent = $componentType;
1371
        return $this;
1372
    }
1373
1374
    /**
1375
     * Gets the type of front-end component the FormField will be rendered as.
1376
     *
1377
     * @return string
1378
     */
1379
    public function getSchemaComponent()
1380
    {
1381
        return $this->schemaComponent;
1382
    }
1383
1384
    /**
1385
     * Sets the schema data used for rendering the field on the front-end.
1386
     * Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
1387
     * Any passed keys that are not defined in {@link getSchemaDataDefaults()} are ignored.
1388
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1389
     *
1390
     * @param array $schemaData - The data to be merged with $this->schemaData.
1391
     * @return FormField
1392
     *
1393
     * @todo Add deep merging of arrays like `data` and `attributes`.
1394
     */
1395
    public function setSchemaData($schemaData = [])
1396
    {
1397
        $defaults = $this->getSchemaData();
1398
        $this->schemaData = array_merge($this->schemaData, array_intersect_key($schemaData, $defaults));
1399
        return $this;
1400
    }
1401
1402
    /**
1403
     * Gets the schema data used to render the FormField on the front-end.
1404
     *
1405
     * @return array
1406
     */
1407
    public function getSchemaData()
1408
    {
1409
        $defaults = $this->getSchemaDataDefaults();
1410
        return array_replace_recursive($defaults, array_intersect_key($this->schemaData, $defaults));
1411
    }
1412
1413
    /**
1414
     * @todo Throw exception if value is missing, once a form field schema is mandatory across the CMS
1415
     *
1416
     * @return string
1417
     */
1418
    public function getSchemaDataType()
1419
    {
1420
        return $this->schemaDataType;
1421
    }
1422
1423
    /**
1424
     * Gets the defaults for $schemaData.
1425
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaData()} are ignored.
1426
     * Instead the `data` array should be used to pass around ad hoc data.
1427
     *
1428
     * @return array
1429
     */
1430
    public function getSchemaDataDefaults()
1431
    {
1432
        return [
1433
            'name' => $this->getName(),
1434
            'id' => $this->ID(),
1435
            'type' => $this->getInputType(),
1436
            'schemaType' => $this->getSchemaDataType(),
1437
            'component' => $this->getSchemaComponent(),
1438
            'holderId' => $this->HolderID(),
1439
            'title' => $this->obj('Title')->getSchemaValue(),
1440
            'source' => null,
1441
            'extraClass' => $this->extraClass(),
1442
            'description' => $this->obj('Description')->getSchemaValue(),
1443
            'rightTitle' => $this->obj('RightTitle')->getSchemaValue(),
1444
            'leftTitle' => $this->obj('LeftTitle')->getSchemaValue(),
1445
            'readOnly' => $this->isReadonly(),
1446
            'disabled' => $this->isDisabled(),
1447
            'customValidationMessage' => $this->getCustomValidationMessage(),
1448
            'validation' => $this->getSchemaValidation(),
1449
            'attributes' => [],
1450
            'autoFocus' => $this->isAutofocus(),
1451
            'data' => [],
1452
        ];
1453
    }
1454
1455
    /**
1456
     * Sets the schema data used for rendering the field on the front-end.
1457
     * Merges the passed array with the current `$schemaState` or {@link getSchemaStateDefaults()}.
1458
     * Any passed keys that are not defined in {@link getSchemaStateDefaults()} are ignored.
1459
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1460
     *
1461
     * @param array $schemaState The data to be merged with $this->schemaData.
1462
     * @return FormField
1463
     *
1464
     * @todo Add deep merging of arrays like `data` and `attributes`.
1465
     */
1466
    public function setSchemaState($schemaState = [])
1467
    {
1468
        $defaults = $this->getSchemaState();
1469
        $this->schemaState = array_merge($this->schemaState, array_intersect_key($schemaState, $defaults));
1470
        return $this;
1471
    }
1472
1473
    /**
1474
     * Gets the schema state used to render the FormField on the front-end.
1475
     *
1476
     * @return array
1477
     */
1478
    public function getSchemaState()
1479
    {
1480
        $defaults = $this->getSchemaStateDefaults();
1481
        return array_merge($defaults, array_intersect_key($this->schemaState, $defaults));
1482
    }
1483
1484
    /**
1485
     * Gets the defaults for $schemaState.
1486
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaState()} are ignored.
1487
     * Instead the `data` array should be used to pass around ad hoc data.
1488
     * Includes validation data if the field is associated to a {@link Form},
1489
     * and {@link Form->validate()} has been called.
1490
     *
1491
     * @todo Make form / field messages not always stored as html; Store value / casting as separate values.
1492
     * @return array
1493
     */
1494
    public function getSchemaStateDefaults()
1495
    {
1496
        $state = [
1497
            'name' => $this->getName(),
1498
            'id' => $this->ID(),
1499
            'value' => $this->Value(),
1500
            'message' => $this->getSchemaMessage(),
1501
            'data' => [],
1502
        ];
1503
1504
        return $state;
1505
    }
1506
1507
    /**
1508
     * Return list of validation rules. Each rule is a key value pair.
1509
     * The key is the rule name. The value is any information the frontend
1510
     * validation handler can understand, or just `true` to enable.
1511
     *
1512
     * @return array
1513
     */
1514
    public function getSchemaValidation()
1515
    {
1516
        $validationList = [];
1517
        if ($this->Required()) {
1518
            $validationList['required'] = true;
1519
        }
1520
        $this->extend('updateSchemaValidation', $validationList);
1521
        return $validationList;
1522
    }
1523
}
1524