Passed
Pull Request — 4 (#10033)
by Maxime
06:42
created

FormField::getForm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
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
     * @var Tip|null
189
     */
190
    private $titleTip;
191
192
    /**
193
     * Name of the template used to render this form field. If not set, then will look up the class
194
     * ancestry for the first matching template where the template name equals the class name.
195
     *
196
     * To explicitly use a custom template or one named other than the form field see
197
     * {@link setTemplate()}.
198
     *
199
     * @var string
200
     */
201
    protected $template;
202
203
    /**
204
     * Name of the template used to render this form field. If not set, then will look up the class
205
     * ancestry for the first matching template where the template name equals the class name.
206
     *
207
     * To explicitly use a custom template or one named other than the form field see
208
     * {@link setFieldHolderTemplate()}.
209
     *
210
     * @var string
211
     */
212
    protected $fieldHolderTemplate;
213
214
    /**
215
     * @var string
216
     */
217
    protected $smallFieldHolderTemplate;
218
219
    /**
220
     * The data type backing the field. Represents the type of value the
221
     * form expects to receive via a postback. Should be set in subclasses.
222
     *
223
     * The values allowed in this list include:
224
     *
225
     *   - String: Single line text
226
     *   - Hidden: Hidden field which is posted back without modification
227
     *   - Text: Multi line text
228
     *   - HTML: Rich html text
229
     *   - Integer: Whole number value
230
     *   - Decimal: Decimal value
231
     *   - MultiSelect: Select many from source
232
     *   - SingleSelect: Select one from source
233
     *   - Date: Date only
234
     *   - DateTime: Date and time
235
     *   - Time: Time only
236
     *   - Boolean: Yes or no
237
     *   - Custom: Custom type declared by the front-end component. For fields with this type,
238
     *     the component property is mandatory, and will determine the posted value for this field.
239
     *   - Structural: Represents a field that is NOT posted back. This may contain other fields,
240
     *     or simply be a block of stand-alone content. As with 'Custom',
241
     *     the component property is mandatory if this is assigned.
242
     *
243
     * Each value has an equivalent constant, e.g. {@link self::SCHEMA_DATA_TYPE_STRING}.
244
     *
245
     * @var string
246
     */
247
    protected $schemaDataType;
248
249
    /**
250
     * The type of front-end component to render the FormField as.
251
     *
252
     * @skipUpgrade
253
     * @var string
254
     */
255
    protected $schemaComponent;
256
257
    /**
258
     * Structured schema data representing the FormField.
259
     * Used to render the FormField as a ReactJS Component on the front-end.
260
     *
261
     * @var array
262
     */
263
    protected $schemaData = [];
264
265
    private static $casting = [
0 ignored issues
show
introduced by
The private property $casting is not used, and could be removed.
Loading history...
266
        'FieldHolder' => 'HTMLFragment',
267
        'SmallFieldHolder' => 'HTMLFragment',
268
        'Field' => 'HTMLFragment',
269
        'AttributesHTML' => 'HTMLFragment', // property $AttributesHTML version
270
        'getAttributesHTML' => 'HTMLFragment', // method $getAttributesHTML($arg) version
271
        'Value' => 'Text',
272
        'extraClass' => 'Text',
273
        'ID' => 'Text',
274
        'isReadOnly' => 'Boolean',
275
        'HolderID' => 'Text',
276
        'Title' => 'Text',
277
        'RightTitle' => 'Text',
278
        'Description' => 'HTMLFragment',
279
    ];
280
281
    /**
282
     * Structured schema state representing the FormField's current data and validation.
283
     * Used to render the FormField as a ReactJS Component on the front-end.
284
     *
285
     * @var array
286
     */
287
    protected $schemaState = [];
288
289
    /**
290
     * Takes a field name and converts camelcase to spaced words. Also resolves combined field
291
     * names with dot syntax to spaced words.
292
     *
293
     * Examples:
294
     *
295
     * - 'TotalAmount' will return 'Total amount'
296
     * - 'Organisation.ZipCode' will return 'Organisation zip code'
297
     *
298
     * @param string $fieldName
299
     *
300
     * @return string
301
     */
302
    public static function name_to_label($fieldName)
303
    {
304
        // Handle dot delimiters
305
        if (strpos($fieldName, '.') !== false) {
306
            $parts = explode('.', $fieldName);
307
            // Ensure that any letter following a dot is uppercased, so that the regex below can break it up
308
            // into words
309
            $label = implode(array_map('ucfirst', $parts));
310
        } else {
311
            $label = $fieldName;
312
        }
313
314
        // Replace any capital letter that is followed by a lowercase letter with a space, the lowercased
315
        // version of itself then the remaining lowercase letters.
316
        $labelWithSpaces = preg_replace_callback('/([A-Z])([a-z]+)/', function ($matches) {
317
            return ' ' . strtolower($matches[1]) . $matches[2];
318
        }, $label);
319
320
        // Add a space before any capital letter block that is at the end of the string
321
        $labelWithSpaces = preg_replace('/([a-z])([A-Z]+)$/', '$1 $2', $labelWithSpaces);
322
323
        // The first letter should be uppercase
324
        return ucfirst(trim($labelWithSpaces));
325
    }
326
327
    /**
328
     * Creates a new field.
329
     *
330
     * @param string $name The internal field name, passed to forms.
331
     * @param null|string|\SilverStripe\View\ViewableData $title The human-readable field label.
332
     * @param mixed $value The value of the field.
333
     */
334
    public function __construct($name, $title = null, $value = null)
335
    {
336
        $this->setName($name);
337
338
        if ($title === null) {
339
            $this->title = self::name_to_label($name);
340
        } else {
341
            $this->title = $title;
342
        }
343
344
        if ($value !== null) {
345
            $this->setValue($value);
346
        }
347
348
        parent::__construct();
349
350
        $this->setupDefaultClasses();
351
    }
352
353
    /**
354
     * Set up the default classes for the form. This is done on construct so that the default classes can be removed
355
     * after instantiation
356
     */
357
    protected function setupDefaultClasses()
358
    {
359
        $defaultClasses = $this->config()->get('default_classes');
360
        if ($defaultClasses) {
361
            foreach ($defaultClasses as $class) {
362
                $this->addExtraClass($class);
363
            }
364
        }
365
    }
366
367
    /**
368
     * Return a link to this field
369
     *
370
     * @param string $action
371
     * @return string
372
     * @throws LogicException If no form is set yet
373
     */
374
    public function Link($action = null)
375
    {
376
        if (!$this->form) {
377
            throw new LogicException(
378
                'Field must be associated with a form to call Link(). Please use $field->setForm($form);'
379
            );
380
        }
381
382
        $link = Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
383
        $this->extend('updateLink', $link, $action);
384
        return $link;
385
    }
386
387
    /**
388
     * Returns the HTML ID of the field.
389
     *
390
     * The ID is generated as FormName_FieldName. All Field functions should ensure that this ID is
391
     * included in the field.
392
     *
393
     * @return string
394
     */
395
    public function ID()
396
    {
397
        return $this->getTemplateHelper()->generateFieldID($this);
398
    }
399
400
    /**
401
     * Returns the HTML ID for the form field holder element.
402
     *
403
     * @return string
404
     */
405
    public function HolderID()
406
    {
407
        return $this->getTemplateHelper()->generateFieldHolderID($this);
408
    }
409
410
    /**
411
     * Returns the current {@link FormTemplateHelper} on either the parent
412
     * Form or the global helper set through the {@link Injector} layout.
413
     *
414
     * To customize a single {@link FormField}, use {@link setTemplate} and
415
     * provide a custom template name.
416
     *
417
     * @return FormTemplateHelper
418
     */
419
    public function getTemplateHelper()
420
    {
421
        if ($this->form) {
422
            return $this->form->getTemplateHelper();
423
        }
424
425
        return FormTemplateHelper::singleton();
426
    }
427
428
    /**
429
     * Returns the field name.
430
     *
431
     * @return string
432
     */
433
    public function getName()
434
    {
435
        return $this->name;
436
    }
437
438
    /**
439
     * Returns the field input name.
440
     *
441
     * @return string
442
     */
443
    public function getInputType()
444
    {
445
        return $this->inputType;
446
    }
447
448
    /**
449
     * Returns the field value.
450
     *
451
     * @see FormField::setSubmittedValue()
452
     * @return mixed
453
     */
454
    public function Value()
455
    {
456
        return $this->value;
457
    }
458
459
    /**
460
     * Method to save this form field into the given {@link DataObject}.
461
     *
462
     * By default, makes use of $this->dataValue()
463
     *
464
     * @param DataObject|DataObjectInterface $record DataObject to save data into
465
     */
466
    public function saveInto(DataObjectInterface $record)
467
    {
468
        $component = $record;
469
        $fieldName = $this->name;
470
471
        // Allow for dot syntax
472
        if (($pos = strrpos($this->name, '.')) !== false) {
473
            $relation = substr($this->name, 0, $pos);
474
            $fieldName = substr($this->name, $pos + 1);
475
            $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

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

1231
    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...
1232
    {
1233
        return true;
1234
    }
1235
1236
    /**
1237
     * Describe this field, provide help text for it.
1238
     *
1239
     * By default, renders as a span class="description" underneath the form field.
1240
     *
1241
     * @param string $description
1242
     *
1243
     * @return $this
1244
     */
1245
    public function setDescription($description)
1246
    {
1247
        $this->description = $description;
1248
1249
        return $this;
1250
    }
1251
1252
    /**
1253
     * @return string
1254
     */
1255
    public function getDescription()
1256
    {
1257
        return $this->description;
1258
    }
1259
1260
    /**
1261
     * @return string
1262
     */
1263
    public function debug()
1264
    {
1265
        $strValue = is_string($this->value) ? $this->value : print_r($this->value, true);
1266
1267
        return sprintf(
1268
            '%s (%s: %s : <span style="color:red;">%s</span>) = %s',
1269
            Convert::raw2att(static::class),
1270
            Convert::raw2att($this->name),
1271
            Convert::raw2att($this->title),
1272
            $this->getMessageCast() == ValidationResult::CAST_HTML ? Convert::raw2xml($this->message) : $this->message,
1273
            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

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