Completed
Push — master ( f39c4d...b2e354 )
by Sam
03:35 queued 03:17
created

FormField::castedCopy()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

Loading history...
261
        'Value' => 'Text',
262
        'extraClass' => 'Text',
263
        'ID' => 'Text',
264
        'isReadOnly' => 'Boolean',
265
        'HolderID' => 'Text',
266
        'Title' => 'Text',
267
        'RightTitle' => 'Text',
268
        'Description' => 'HTMLFragment',
269
    );
270
271
    /**
272
     * Structured schema state representing the FormField's current data and validation.
273
     * Used to render the FormField as a ReactJS Component on the front-end.
274
     *
275
     * @var array
276
     */
277
    protected $schemaState = [];
278
279
    /**
280
     * Takes a field name and converts camelcase to spaced words. Also resolves combined field
281
     * names with dot syntax to spaced words.
282
     *
283
     * Examples:
284
     *
285
     * - 'TotalAmount' will return 'Total Amount'
286
     * - 'Organisation.ZipCode' will return 'Organisation Zip Code'
287
     *
288
     * @param string $fieldName
289
     *
290
     * @return string
291
     */
292
    public static function name_to_label($fieldName)
293
    {
294
        if (strpos($fieldName, '.') !== false) {
295
            $parts = explode('.', $fieldName);
296
297
            $label = $parts[count($parts) - 2] . ' ' . $parts[count($parts) - 1];
298
        } else {
299
            $label = $fieldName;
300
        }
301
302
        return preg_replace('/([a-z]+)([A-Z])/', '$1 $2', $label);
303
    }
304
305
    /**
306
     * Construct and return HTML tag.
307
     *
308
     * @param string $tag
309
     * @param array $attributes
310
     * @param null|string $content
311
     *
312
     * @return string
313
     */
314
    public static function create_tag($tag, $attributes, $content = null)
315
    {
316
        $preparedAttributes = '';
317
318
        foreach ($attributes as $attributeKey => $attributeValue) {
319
            if (!empty($attributeValue) || $attributeValue === '0' || ($attributeKey == 'value' && $attributeValue !== null)) {
320
                $preparedAttributes .= sprintf(
321
                    ' %s="%s"',
322
                    $attributeKey,
323
                    Convert::raw2att($attributeValue)
324
                );
325
            }
326
        }
327
328
        if ($content || $tag != 'input') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $content of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
329
            return sprintf(
330
                '<%s%s>%s</%s>',
331
                $tag,
332
                $preparedAttributes,
333
                $content,
334
                $tag
335
            );
336
        }
337
338
        return sprintf(
339
            '<%s%s />',
340
            $tag,
341
            $preparedAttributes
342
        );
343
    }
344
345
    /**
346
     * Creates a new field.
347
     *
348
     * @param string $name The internal field name, passed to forms.
349
     * @param null|string $title The human-readable field label.
350
     * @param mixed $value The value of the field.
351
     */
352
    public function __construct($name, $title = null, $value = null)
353
    {
354
        $this->setName($name);
355
356
        if ($title === null) {
357
            $this->title = self::name_to_label($name);
358
        } else {
359
            $this->title = $title;
360
        }
361
362
        if ($value !== null) {
363
            $this->setValue($value);
364
        }
365
366
        parent::__construct();
367
368
        $this->setupDefaultClasses();
369
    }
370
371
    /**
372
     * Set up the default classes for the form. This is done on construct so that the default classes can be removed
373
     * after instantiation
374
     */
375
    protected function setupDefaultClasses()
376
    {
377
        $defaultClasses = self::config()->get('default_classes');
378
        if ($defaultClasses) {
379
            foreach ($defaultClasses as $class) {
380
                $this->addExtraClass($class);
381
            }
382
        }
383
    }
384
385
    /**
386
     * Return a link to this field.
387
     *
388
     * @param string $action
389
     *
390
     * @return string
391
     */
392
    public function Link($action = null)
393
    {
394
        return Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
395
    }
396
397
    /**
398
     * Returns the HTML ID of the field.
399
     *
400
     * The ID is generated as FormName_FieldName. All Field functions should ensure that this ID is
401
     * included in the field.
402
     *
403
     * @return string
404
     */
405
    public function ID()
406
    {
407
        return $this->getTemplateHelper()->generateFieldID($this);
408
    }
409
410
    /**
411
     * Returns the HTML ID for the form field holder element.
412
     *
413
     * @return string
414
     */
415
    public function HolderID()
416
    {
417
        return $this->getTemplateHelper()->generateFieldHolderID($this);
418
    }
419
420
    /**
421
     * Returns the current {@link FormTemplateHelper} on either the parent
422
     * Form or the global helper set through the {@link Injector} layout.
423
     *
424
     * To customize a single {@link FormField}, use {@link setTemplate} and
425
     * provide a custom template name.
426
     *
427
     * @return FormTemplateHelper
428
     */
429
    public function getTemplateHelper()
430
    {
431
        if ($this->form) {
432
            return $this->form->getTemplateHelper();
433
        }
434
435
        return FormTemplateHelper::singleton();
436
    }
437
438
    /**
439
     * Returns the field name.
440
     *
441
     * @return string
442
     */
443
    public function getName()
444
    {
445
        return $this->name;
446
    }
447
448
    /**
449
     * Returns the field value.
450
     *
451
     * @return mixed
452
     */
453
    public function Value()
454
    {
455
        return $this->value;
456
    }
457
458
    /**
459
     * Method to save this form field into the given {@link DataObject}.
460
     *
461
     * By default, makes use of $this->dataValue()
462
     *
463
     * @param DataObject|DataObjectInterface $record DataObject to save data into
464
     */
465
    public function saveInto(DataObjectInterface $record)
466
    {
467
        if ($this->name) {
468
            $record->setCastedField($this->name, $this->dataValue());
469
        }
470
    }
471
472
    /**
473
     * Returns the field value suitable for insertion into the data object.
474
     *
475
     * @return mixed
476
     */
477
    public function dataValue()
478
    {
479
        return $this->value;
480
    }
481
482
    /**
483
     * Returns the field label - used by templates.
484
     *
485
     * @return string
486
     */
487
    public function Title()
488
    {
489
        return $this->title;
490
    }
491
492
    /**
493
     * Set the title of this formfield.
494
     * Note: This expects escaped HTML.
495
     *
496
     * @param string $title Escaped HTML for title
497
     * @return $this
498
     */
499
    public function setTitle($title)
500
    {
501
        $this->title = $title;
502
        return $this;
503
    }
504
505
    /**
506
     * Gets the contextual label than can be used for additional field description.
507
     * Can be shown to the right or under the field in question.
508
     *
509
     * @return string Contextual label text.
510
     */
511
    public function RightTitle()
512
    {
513
        return $this->rightTitle;
514
    }
515
516
    /**
517
     * Sets the right title for this formfield
518
     * Note: This expects escaped HTML.
519
     *
520
     * @param string $rightTitle Escaped HTML for title
521
     * @return $this
522
     */
523
    public function setRightTitle($rightTitle)
524
    {
525
        $this->rightTitle = $rightTitle;
526
        return $this;
527
    }
528
529
    /**
530
     * @return string
531
     */
532
    public function LeftTitle()
533
    {
534
        return $this->leftTitle;
535
    }
536
537
    /**
538
     * @param string $leftTitle
539
     *
540
     * @return $this
541
     */
542
    public function setLeftTitle($leftTitle)
543
    {
544
        $this->leftTitle = $leftTitle;
545
546
        return $this;
547
    }
548
549
    /**
550
     * Compiles all CSS-classes. Optionally includes a "form-group--no-label" class if no title was set on the
551
     * FormField.
552
     *
553
     * Uses {@link Message()} and {@link MessageType()} to add validation error classes which can
554
     * be used to style the contained tags.
555
     *
556
     * @return string
557
     */
558
    public function extraClass()
559
    {
560
        $classes = array();
561
562
        $classes[] = $this->Type();
563
564
        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...
565
            $classes = array_merge(
566
                $classes,
567
                array_values($this->extraClasses)
568
            );
569
        }
570
571
        if (!$this->Title()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->Title() of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
572
            $classes[] = 'form-group--no-label';
573
        }
574
575
        // Allow custom styling of any element in the container based on validation errors,
576
        // e.g. red borders on input tags.
577
        //
578
        // CSS class needs to be different from the one rendered through {@link FieldHolder()}.
579
        if ($this->getMessage()) {
580
            $classes[] .= 'holder-' . $this->getMessageType();
581
        }
582
583
        return implode(' ', $classes);
584
    }
585
586
    /**
587
     * Add one or more CSS-classes to the FormField container.
588
     *
589
     * Multiple class names should be space delimited.
590
     *
591
     * @param string $class
592
     *
593
     * @return $this
594
     */
595
    public function addExtraClass($class)
596
    {
597
        $classes = preg_split('/\s+/', $class);
598
599
        foreach ($classes as $class) {
600
            $this->extraClasses[$class] = $class;
601
        }
602
603
        return $this;
604
    }
605
606
    /**
607
     * Remove one or more CSS-classes from the FormField container.
608
     *
609
     * @param string $class
610
     *
611
     * @return $this
612
     */
613
    public function removeExtraClass($class)
614
    {
615
        $classes = preg_split('/\s+/', $class);
616
617
        foreach ($classes as $class) {
618
            unset($this->extraClasses[$class]);
619
        }
620
621
        return $this;
622
    }
623
624
    /**
625
     * Set an HTML attribute on the field element, mostly an <input> tag.
626
     *
627
     * Some attributes are best set through more specialized methods, to avoid interfering with
628
     * built-in behaviour:
629
     *
630
     * - 'class': {@link addExtraClass()}
631
     * - 'title': {@link setDescription()}
632
     * - 'value': {@link setValue}
633
     * - 'name': {@link setName}
634
     *
635
     * Caution: this doesn't work on most fields which are composed of more than one HTML form
636
     * field.
637
     *
638
     * @param string $name
639
     * @param string $value
640
     *
641
     * @return $this
642
     */
643
    public function setAttribute($name, $value)
644
    {
645
        $this->attributes[$name] = $value;
646
647
        return $this;
648
    }
649
650
    /**
651
     * Get an HTML attribute defined by the field, or added through {@link setAttribute()}.
652
     *
653
     * Caution: this doesn't work on all fields, see {@link setAttribute()}.
654
     *
655
     * @param string $name
656
     * @return string
657
     */
658
    public function getAttribute($name)
659
    {
660
        $attributes = $this->getAttributes();
661
662
        if (isset($attributes[$name])) {
663
            return $attributes[$name];
664
        }
665
666
        return null;
667
    }
668
669
    /**
670
     * Allows customization through an 'updateAttributes' hook on the base class.
671
     * Existing attributes are passed in as the first argument and can be manipulated,
672
     * but any attributes added through a subclass implementation won't be included.
673
     *
674
     * @return array
675
     */
676
    public function getAttributes()
677
    {
678
        $attributes = array(
679
            'type' => 'text',
680
            'name' => $this->getName(),
681
            'value' => $this->Value(),
682
            'class' => $this->extraClass(),
683
            'id' => $this->ID(),
684
            'disabled' => $this->isDisabled(),
685
            'readonly' => $this->isReadonly()
686
        );
687
688
        if ($this->Required()) {
689
            $attributes['required'] = 'required';
690
            $attributes['aria-required'] = 'true';
691
        }
692
693
        $attributes = array_merge($attributes, $this->attributes);
694
695
        $this->extend('updateAttributes', $attributes);
696
697
        return $attributes;
698
    }
699
700
    /**
701
     * Custom attributes to process. Falls back to {@link getAttributes()}.
702
     *
703
     * If at least one argument is passed as a string, all arguments act as excludes, by name.
704
     *
705
     * @param array $attributes
706
     *
707
     * @return string
708
     */
709
    public function getAttributesHTML($attributes = null)
710
    {
711
        $exclude = null;
712
713
        if (is_string($attributes)) {
714
            $exclude = func_get_args();
715
        }
716
717
        if (!$attributes || is_string($attributes)) {
718
            $attributes = $this->getAttributes();
719
        }
720
721
        $attributes = (array) $attributes;
722
723
        $attributes = array_filter($attributes, function ($v) {
724
            return ($v || $v === 0 || $v === '0');
725
        });
726
727
        if ($exclude) {
728
            $attributes = array_diff_key(
729
                $attributes,
730
                array_flip($exclude)
731
            );
732
        }
733
734
        // Create markup
735
        $parts = array();
736
737
        foreach ($attributes as $name => $value) {
738
            if ($value === true) {
739
                $parts[] = sprintf('%s="%s"', $name, $name);
740
            } else {
741
                $parts[] = sprintf('%s="%s"', $name, Convert::raw2att($value));
742
            }
743
        }
744
745
        return implode(' ', $parts);
746
    }
747
748
    /**
749
     * Returns a version of a title suitable for insertion into an HTML attribute.
750
     *
751
     * @return string
752
     */
753
    public function attrTitle()
754
    {
755
        return Convert::raw2att($this->title);
756
    }
757
758
    /**
759
     * Returns a version of a title suitable for insertion into an HTML attribute.
760
     *
761
     * @return string
762
     */
763
    public function attrValue()
764
    {
765
        return Convert::raw2att($this->value);
766
    }
767
768
    /**
769
     * Set the field value.
770
     *
771
     * @param mixed $value
772
     * @param null|array|DataObject $data {@see Form::loadDataFrom}
0 ignored issues
show
Bug introduced by
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1075
            // Prioritise direct template
1076
            array_unshift($templates, $customTemplate);
1077
        }
1078
        return $templates;
1079
    }
1080
1081
    /**
1082
     * Returns true if this field is a composite field.
1083
     *
1084
     * To create composite field types, you should subclass {@link CompositeField}.
1085
     *
1086
     * @return bool
1087
     */
1088
    public function isComposite()
1089
    {
1090
        return false;
1091
    }
1092
1093
    /**
1094
     * Returns true if this field has its own data.
1095
     *
1096
     * Some fields, such as titles and composite fields, don't actually have any data. It doesn't
1097
     * make sense for data-focused methods to look at them. By overloading hasData() to return
1098
     * false, you can prevent any data-focused methods from looking at it.
1099
     *
1100
     * @see FieldList::collateDataFields()
1101
     *
1102
     * @return bool
1103
     */
1104
    public function hasData()
1105
    {
1106
        return true;
1107
    }
1108
1109
    /**
1110
     * @return bool
1111
     */
1112
    public function isReadonly()
1113
    {
1114
        return $this->readonly;
1115
    }
1116
1117
    /**
1118
     * Sets a read-only flag on this FormField.
1119
     *
1120
     * Use performReadonlyTransformation() to transform this instance.
1121
     *
1122
     * Setting this to false has no effect on the field.
1123
     *
1124
     * @param bool $readonly
1125
     *
1126
     * @return $this
1127
     */
1128
    public function setReadonly($readonly)
1129
    {
1130
        $this->readonly = $readonly;
1131
1132
        return $this;
1133
    }
1134
1135
    /**
1136
     * @return bool
1137
     */
1138
    public function isDisabled()
1139
    {
1140
        return $this->disabled;
1141
    }
1142
1143
    /**
1144
     * Sets a disabled flag on this FormField.
1145
     *
1146
     * Use performDisabledTransformation() to transform this instance.
1147
     *
1148
     * Setting this to false has no effect on the field.
1149
     *
1150
     * @param bool $disabled
1151
     *
1152
     * @return $this
1153
     */
1154
    public function setDisabled($disabled)
1155
    {
1156
        $this->disabled = $disabled;
1157
1158
        return $this;
1159
    }
1160
1161
    /**
1162
     * Returns a read-only version of this field.
1163
     *
1164
     * @return FormField
1165
     */
1166
    public function performReadonlyTransformation()
1167
    {
1168
        $readonlyClassName = static::class . '_Readonly';
1169
1170
        if (ClassInfo::exists($readonlyClassName)) {
1171
            $clone = $this->castedCopy($readonlyClassName);
1172
        } else {
1173
            $clone = $this->castedCopy(ReadonlyField::class);
1174
        }
1175
1176
        $clone->setReadonly(true);
1177
1178
        return $clone;
1179
    }
1180
1181
    /**
1182
     * Return a disabled version of this field.
1183
     *
1184
     * Tries to find a class of the class name of this field suffixed with "_Disabled", failing
1185
     * that, finds a method {@link setDisabled()}.
1186
     *
1187
     * @return FormField
1188
     */
1189
    public function performDisabledTransformation()
1190
    {
1191
        $disabledClassName = $this->class . '_Disabled';
1192
1193
        if (ClassInfo::exists($disabledClassName)) {
1194
            $clone = $this->castedCopy($disabledClassName);
1195
        } else {
1196
            $clone = clone $this;
1197
        }
1198
1199
        $clone->setDisabled(true);
1200
1201
        return $clone;
1202
    }
1203
1204
    /**
1205
     * @param FormTransformation $transformation
1206
     *
1207
     * @return mixed
1208
     */
1209
    public function transform(FormTransformation $transformation)
1210
    {
1211
        return $transformation->transform($this);
1212
    }
1213
1214
    /**
1215
     * @param string $class
1216
     *
1217
     * @return int
1218
     */
1219
    public function hasClass($class)
1220
    {
1221
        $patten = '/' . strtolower($class) . '/i';
1222
1223
        $subject = strtolower($this->class . ' ' . $this->extraClass());
1224
1225
        return preg_match($patten, $subject);
1226
    }
1227
1228
    /**
1229
     * Returns the field type.
1230
     *
1231
     * The field type is the class name with the word Field dropped off the end, all lowercase.
1232
     *
1233
     * It's handy for assigning HTML classes. Doesn't signify the <input type> attribute.
1234
     *
1235
     * @see {link getAttributes()}.
1236
     *
1237
     * @return string
1238
     */
1239
    public function Type()
1240
    {
1241
        $type = new ReflectionClass($this);
1242
        return strtolower(preg_replace('/Field$/', '', $type->getShortName()));
1243
    }
1244
1245
    /**
1246
     * @deprecated 4.0 Use FormField::create_tag()
1247
     *
1248
     * @param string $tag
1249
     * @param array $attributes
1250
     * @param null|string $content
1251
     *
1252
     * @return string
1253
     */
1254
    public function createTag($tag, $attributes, $content = null)
1255
    {
1256
        Deprecation::notice('4.0', 'Use FormField::create_tag()');
1257
1258
        return self::create_tag($tag, $attributes, $content);
1259
    }
1260
1261
    /**
1262
     * Abstract method each {@link FormField} subclass must implement, determines whether the field
1263
     * is valid or not based on the value.
1264
     *
1265
     * @todo Make this abstract.
1266
     *
1267
     * @param Validator $validator
1268
     * @return bool
1269
     */
1270
    public function validate($validator)
1271
    {
1272
        return true;
1273
    }
1274
1275
    /**
1276
     * Describe this field, provide help text for it.
1277
     *
1278
     * By default, renders as a <span class="description"> underneath the form field.
1279
     *
1280
     * @param string $description
1281
     *
1282
     * @return $this
1283
     */
1284
    public function setDescription($description)
1285
    {
1286
        $this->description = $description;
1287
1288
        return $this;
1289
    }
1290
1291
    /**
1292
     * @return string
1293
     */
1294
    public function getDescription()
1295
    {
1296
        return $this->description;
1297
    }
1298
1299
    /**
1300
     * @return string
1301
     */
1302
    public function debug()
1303
    {
1304
        return sprintf(
1305
            '%s (%s: %s : <span style="color:red;">%s</span>) = %s',
1306
            $this->class,
1307
            $this->name,
1308
            $this->title,
1309
            $this->message,
1310
            $this->value
1311
        );
1312
    }
1313
1314
    /**
1315
     * This function is used by the template processor. If you refer to a field as a $ variable, it
1316
     * will return the $Field value.
1317
     *
1318
     * @return string
1319
     */
1320
    public function forTemplate()
1321
    {
1322
        return $this->Field();
1323
    }
1324
1325
    /**
1326
     * @return bool
1327
     */
1328
    public function Required()
1329
    {
1330
        if ($this->form && ($validator = $this->form->getValidator())) {
1331
            return $validator->fieldIsRequired($this->name);
1332
        }
1333
1334
        return false;
1335
    }
1336
1337
    /**
1338
     * Set the FieldList that contains this field.
1339
     *
1340
     * @param FieldList $containerFieldList
1341
     * @return $this
1342
     */
1343
    public function setContainerFieldList($containerFieldList)
1344
    {
1345
        $this->containerFieldList = $containerFieldList;
1346
        return $this;
1347
    }
1348
1349
    /**
1350
     * Get the FieldList that contains this field.
1351
     *
1352
     * @return FieldList
1353
     */
1354
    public function getContainerFieldList()
1355
    {
1356
        return $this->containerFieldList;
1357
    }
1358
1359
    /**
1360
     * @return null|FieldList
1361
     */
1362
    public function rootFieldList()
1363
    {
1364
        if (is_object($this->containerFieldList)) {
1365
            return $this->containerFieldList->rootFieldList();
1366
        }
1367
1368
        user_error(
1369
            "rootFieldList() called on $this->class object without a containerFieldList",
1370
            E_USER_ERROR
1371
        );
1372
1373
        return null;
1374
    }
1375
1376
    /**
1377
     * Returns another instance of this field, but "cast" to a different class. The logic tries to
1378
     * retain all of the instance properties, and may be overloaded by subclasses to set additional
1379
     * ones.
1380
     *
1381
     * Assumes the standard FormField parameter signature with its name as the only mandatory
1382
     * argument. Mainly geared towards creating *_Readonly or *_Disabled subclasses of the same
1383
     * type, or casting to a {@link ReadonlyField}.
1384
     *
1385
     * Does not copy custom field templates, since they probably won't apply to the new instance.
1386
     *
1387
     * @param mixed $classOrCopy Class name for copy, or existing copy instance to update
1388
     *
1389
     * @return FormField
1390
     */
1391
    public function castedCopy($classOrCopy)
1392
    {
1393
        $field = $classOrCopy;
1394
1395
        if (!is_object($field)) {
1396
            $field = new $classOrCopy($this->name);
1397
        }
1398
1399
        $field
1400
            ->setValue($this->value)
1401
            ->setForm($this->form)
1402
            ->setTitle($this->Title())
1403
            ->setLeftTitle($this->LeftTitle())
1404
            ->setRightTitle($this->RightTitle())
1405
            ->addExtraClass($this->extraClass) // Don't use extraClass(), since this merges calculated values
1406
            ->setDescription($this->getDescription());
1407
1408
        // Only include built-in attributes, ignore anything set through getAttributes().
1409
        // Those might change important characteristics of the field, e.g. its "type" attribute.
1410
        foreach ($this->attributes as $attributeKey => $attributeValue) {
1411
            $field->setAttribute($attributeKey, $attributeValue);
1412
        }
1413
1414
        return $field;
1415
    }
1416
1417
    /**
1418
     * Determine if the value of this formfield accepts front-end submitted values and is saveable.
1419
     *
1420
     * @return bool
1421
     */
1422
    public function canSubmitValue()
1423
    {
1424
        return $this->hasData() && !$this->isReadonly() && !$this->isDisabled();
1425
    }
1426
1427
    /**
1428
     * Sets the component type the FormField will be rendered as on the front-end.
1429
     *
1430
     * @param string $componentType
1431
     * @return FormField
1432
     */
1433
    public function setSchemaComponent($componentType)
1434
    {
1435
        $this->schemaComponent = $componentType;
1436
        return $this;
1437
    }
1438
1439
    /**
1440
     * Gets the type of front-end component the FormField will be rendered as.
1441
     *
1442
     * @return string
1443
     */
1444
    public function getSchemaComponent()
1445
    {
1446
        return $this->schemaComponent;
1447
    }
1448
1449
    /**
1450
     * Sets the schema data used for rendering the field on the front-end.
1451
     * Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
1452
     * Any passed keys that are not defined in {@link getSchemaDataDefaults()} are ignored.
1453
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1454
     *
1455
     * @param array $schemaData - The data to be merged with $this->schemaData.
1456
     * @return FormField
1457
     *
1458
     * @todo Add deep merging of arrays like `data` and `attributes`.
1459
     */
1460
    public function setSchemaData($schemaData = [])
1461
    {
1462
        $defaults = $this->getSchemaData();
1463
        $this->schemaData = array_merge($this->schemaData, array_intersect_key($schemaData, $defaults));
1464
        return $this;
1465
    }
1466
1467
    /**
1468
     * Gets the schema data used to render the FormField on the front-end.
1469
     *
1470
     * @return array
1471
     */
1472
    public function getSchemaData()
1473
    {
1474
        $defaults = $this->getSchemaDataDefaults();
1475
        return array_replace_recursive($defaults, array_intersect_key($this->schemaData, $defaults));
1476
    }
1477
1478
    /**
1479
     * @todo Throw exception if value is missing, once a form field schema is mandatory across the CMS
1480
     *
1481
     * @return string
1482
     */
1483
    public function getSchemaDataType()
1484
    {
1485
        return $this->schemaDataType;
1486
    }
1487
1488
    /**
1489
     * Gets the defaults for $schemaData.
1490
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaData()} are ignored.
1491
     * Instead the `data` array should be used to pass around ad hoc data.
1492
     *
1493
     * @return array
1494
     */
1495
    public function getSchemaDataDefaults()
1496
    {
1497
        return [
1498
            'name' => $this->getName(),
1499
            'id' => $this->ID(),
1500
            'type' => $this->getSchemaDataType(),
1501
            'component' => $this->getSchemaComponent(),
1502
            'holderId' => $this->HolderID(),
1503
            'title' => $this->Title(),
1504
            'source' => null,
1505
            'extraClass' => $this->extraClass(),
1506
            'description' => $this->obj('Description')->getSchemaValue(),
1507
            'rightTitle' => $this->RightTitle(),
1508
            'leftTitle' => $this->LeftTitle(),
1509
            'readOnly' => $this->isReadonly(),
1510
            'disabled' => $this->isDisabled(),
1511
            'customValidationMessage' => $this->getCustomValidationMessage(),
1512
            'validation' => $this->getSchemaValidation(),
1513
            'attributes' => [],
1514
            'data' => [],
1515
        ];
1516
    }
1517
1518
    /**
1519
     * Sets the schema data used for rendering the field on the front-end.
1520
     * Merges the passed array with the current `$schemaState` or {@link getSchemaStateDefaults()}.
1521
     * Any passed keys that are not defined in {@link getSchemaStateDefaults()} are ignored.
1522
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1523
     *
1524
     * @param array $schemaState The data to be merged with $this->schemaData.
1525
     * @return FormField
1526
     *
1527
     * @todo Add deep merging of arrays like `data` and `attributes`.
1528
     */
1529
    public function setSchemaState($schemaState = [])
1530
    {
1531
        $defaults = $this->getSchemaState();
1532
        $this->schemaState = array_merge($this->schemaState, array_intersect_key($schemaState, $defaults));
1533
        return $this;
1534
    }
1535
1536
    /**
1537
     * Gets the schema state used to render the FormField on the front-end.
1538
     *
1539
     * @return array
1540
     */
1541
    public function getSchemaState()
1542
    {
1543
        $defaults = $this->getSchemaStateDefaults();
1544
        return array_merge($defaults, array_intersect_key($this->schemaState, $defaults));
1545
    }
1546
1547
    /**
1548
     * Gets the defaults for $schemaState.
1549
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaState()} are ignored.
1550
     * Instead the `data` array should be used to pass around ad hoc data.
1551
     * Includes validation data if the field is associated to a {@link Form},
1552
     * and {@link Form->validate()} has been called.
1553
     *
1554
     * @todo Make form / field messages not always stored as html; Store value / casting as separate values.
1555
     * @return array
1556
     */
1557
    public function getSchemaStateDefaults()
1558
    {
1559
        $state = [
1560
            'name' => $this->getName(),
1561
            'id' => $this->ID(),
1562
            'value' => $this->Value(),
1563
            'message' => $this->getSchemaMessage(),
1564
            'data' => [],
1565
        ];
1566
1567
        return $state;
1568
    }
1569
1570
    /**
1571
     * Return list of validation rules. Each rule is a key value pair.
1572
     * The key is the rule name. The value is any information the frontend
1573
     * validation handler can understand, or just `true` to enable.
1574
     *
1575
     * @return array
1576
     */
1577
    public function getSchemaValidation()
1578
    {
1579
        if ($this->Required()) {
1580
            return [ 'required' => true ];
1581
        }
1582
        return [];
1583
    }
1584
}
1585