Completed
Push — master ( 622a07...7c2344 )
by Daniel
108:51 queued 72:30
created

FormField::canSubmitValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 2
nc 3
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use ReflectionClass;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\RequestHandler;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\Core\Convert;
10
use SilverStripe\Dev\Deprecation;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\DataObjectInterface;
13
use SilverStripe\ORM\FieldType\DBField;
14
use SilverStripe\ORM\FieldType\DBHTMLText;
15
use SilverStripe\View\SSViewer;
16
17
/**
18
 * Represents a field in a form.
19
 *
20
 * A FieldList contains a number of FormField objects which make up the whole of a form.
21
 *
22
 * In addition to single fields, FormField objects can be "composite", for example, the
23
 * {@link TabSet} field. Composite fields let us define complex forms without having to resort to
24
 * custom HTML.
25
 *
26
 * To subclass:
27
 *
28
 * Define a {@link dataValue()} method that returns a value suitable for inserting into a single
29
 * database field.
30
 *
31
 * For example, you might tidy up the format of a date or currency field. Define {@link saveInto()}
32
 * to totally customise saving.
33
 *
34
 * For example, data might be saved to the filesystem instead of the data record, or saved to a
35
 * component of the data record instead of the data record itself.
36
 *
37
 * A form field can be represented as structured data through {@link FormSchema},
38
 * including both structure (name, id, attributes, etc.) and state (field value).
39
 * Can be used by for JSON data which is consumed by a front-end application.
40
 */
41
class FormField extends RequestHandler
42
{
43
44
    /** @see $schemaDataType */
45
    const SCHEMA_DATA_TYPE_STRING = 'String';
46
47
    /** @see $schemaDataType */
48
    const SCHEMA_DATA_TYPE_HIDDEN = 'Hidden';
49
50
    /** @see $schemaDataType */
51
    const SCHEMA_DATA_TYPE_TEXT = 'Text';
52
53
    /** @see $schemaDataType */
54
    const SCHEMA_DATA_TYPE_HTML = 'HTML';
55
56
    /** @see $schemaDataType */
57
    const SCHEMA_DATA_TYPE_INTEGER = 'Integer';
58
59
    /** @see $schemaDataType */
60
    const SCHEMA_DATA_TYPE_DECIMAL = 'Decimal';
61
62
    /** @see $schemaDataType */
63
    const SCHEMA_DATA_TYPE_MULTISELECT = 'MultiSelect';
64
65
    /** @see $schemaDataType */
66
    const SCHEMA_DATA_TYPE_SINGLESELECT = 'SingleSelect';
67
68
    /** @see $schemaDataType */
69
    const SCHEMA_DATA_TYPE_DATE = 'Date';
70
71
    /** @see $schemaDataType */
72
    const SCHEMA_DATA_TYPE_DATETIME = 'DateTime';
73
74
    /** @see $schemaDataType */
75
    const SCHEMA_DATA_TYPE_TIME = 'Time';
76
77
    /** @see $schemaDataType */
78
    const SCHEMA_DATA_TYPE_BOOLEAN = 'Boolean';
79
80
    /** @see $schemaDataType */
81
    const SCHEMA_DATA_TYPE_CUSTOM = 'Custom';
82
83
    /** @see $schemaDataType */
84
    const SCHEMA_DATA_TYPE_STRUCTURAL = 'Structural';
85
86
    /**
87
     * @var Form
88
     */
89
    protected $form;
90
91
    /**
92
     * @var string
93
     */
94
    protected $name;
95
96
    /**
97
     * @var null|string
98
     */
99
    protected $title;
100
101
    /**
102
     * @var mixed
103
     */
104
    protected $value;
105
106
    /**
107
     * @var string
108
     */
109
    protected $message;
110
111
    /**
112
     * @var string
113
     */
114
    protected $messageType;
115
116
    /**
117
     * @var string
118
     */
119
    protected $extraClass;
120
121
    /**
122
     * Adds a title attribute to the markup.
123
     *
124
     * @var string
125
     *
126
     * @todo Implement in all subclasses
127
     */
128
    protected $description;
129
130
    /**
131
     * Extra CSS classes for the FormField container.
132
     *
133
     * @var array
134
     */
135
    protected $extraClasses;
136
137
    /**
138
     * @config
139
     * @var array $default_classes The default classes to apply to the FormField
140
     */
141
    private static $default_classes = [];
142
143
    /**
144
     * Right-aligned, contextual label for the field.
145
     *
146
     * @var string
147
     */
148
    protected $rightTitle;
149
150
    /**
151
     * Left-aligned, contextual label for the field.
152
     *
153
     * @var string
154
     */
155
    protected $leftTitle;
156
157
    /**
158
     * Stores a reference to the FieldList that contains this object.
159
     *
160
     * @var FieldList
161
     */
162
    protected $containerFieldList;
163
164
    /**
165
     * @var bool
166
     */
167
    protected $readonly = false;
168
169
    /**
170
     * @var bool
171
     */
172
    protected $disabled = false;
173
174
    /**
175
     * Custom validation message for the field.
176
     *
177
     * @var string
178
     */
179
    protected $customValidationMessage = '';
180
181
    /**
182
     * Name of the template used to render this form field. If not set, then will look up the class
183
     * ancestry for the first matching template where the template name equals the class name.
184
     *
185
     * To explicitly use a custom template or one named other than the form field see
186
     * {@link setTemplate()}.
187
     *
188
     * @var string
189
     */
190
    protected $template;
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 setFieldHolderTemplate()}.
198
     *
199
     * @var string
200
     */
201
    protected $fieldHolderTemplate;
202
203
    /**
204
     * @var string
205
     */
206
    protected $smallFieldHolderTemplate;
207
208
    /**
209
     * All attributes on the form field (not the field holder).
210
     *
211
     * Partially determined based on other instance properties.
212
     *
213
     * @see getAttributes()
214
     *
215
     * @var array
216
     */
217
    protected $attributes = [];
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 = array(
266
        'FieldHolder' => 'HTMLFragment',
267
        'Field' => 'HTMLFragment',
268
        'AttributesHTML' => 'HTMLFragment', // property $AttributesHTML version
269
        '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...
270
        'Value' => 'Text',
271
        'extraClass' => 'Text',
272
        'ID' => 'Text',
273
        'isReadOnly' => 'Boolean',
274
        'HolderID' => 'Text',
275
        'Title' => 'Text',
276
        'RightTitle' => 'Text',
277
        'MessageType' => 'Text',
278
        'Message' => 'HTMLFragment',
279
        'Description' => 'HTMLFragment',
280
    );
281
282
    /**
283
     * Structured schema state representing the FormField's current data and validation.
284
     * Used to render the FormField as a ReactJS Component on the front-end.
285
     *
286
     * @var array
287
     */
288
    protected $schemaState = [];
289
290
    /**
291
     * Takes a field name and converts camelcase to spaced words. Also resolves combined field
292
     * names with dot syntax to spaced words.
293
     *
294
     * Examples:
295
     *
296
     * - 'TotalAmount' will return 'Total Amount'
297
     * - 'Organisation.ZipCode' will return 'Organisation Zip Code'
298
     *
299
     * @param string $fieldName
300
     *
301
     * @return string
302
     */
303
    public static function name_to_label($fieldName)
304
    {
305
        if (strpos($fieldName, '.') !== false) {
306
            $parts = explode('.', $fieldName);
307
308
            $label = $parts[count($parts) - 2] . ' ' . $parts[count($parts) - 1];
309
        } else {
310
            $label = $fieldName;
311
        }
312
313
        return preg_replace('/([a-z]+)([A-Z])/', '$1 $2', $label);
314
    }
315
316
    /**
317
     * Construct and return HTML tag.
318
     *
319
     * @param string $tag
320
     * @param array $attributes
321
     * @param null|string $content
322
     *
323
     * @return string
324
     */
325
    public static function create_tag($tag, $attributes, $content = null)
326
    {
327
        $preparedAttributes = '';
328
329
        foreach ($attributes as $attributeKey => $attributeValue) {
330
            if (!empty($attributeValue) || $attributeValue === '0' || ($attributeKey == 'value' && $attributeValue !== null)) {
331
                $preparedAttributes .= sprintf(
332
                    ' %s="%s"',
333
                    $attributeKey,
334
                    Convert::raw2att($attributeValue)
335
                );
336
            }
337
        }
338
339
        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...
340
            return sprintf(
341
                '<%s%s>%s</%s>',
342
                $tag,
343
                $preparedAttributes,
344
                $content,
345
                $tag
346
            );
347
        }
348
349
        return sprintf(
350
            '<%s%s />',
351
            $tag,
352
            $preparedAttributes
353
        );
354
    }
355
356
    /**
357
     * Creates a new field.
358
     *
359
     * @param string $name The internal field name, passed to forms.
360
     * @param null|string $title The human-readable field label.
361
     * @param mixed $value The value of the field.
362
     */
363
    public function __construct($name, $title = null, $value = null)
364
    {
365
        $this->setName($name);
366
367
        if ($title === null) {
368
            $this->title = self::name_to_label($name);
369
        } else {
370
            $this->title = $title;
371
        }
372
373
        if ($value !== null) {
374
            $this->setValue($value);
375
        }
376
377
        parent::__construct();
378
379
        $this->setupDefaultClasses();
380
    }
381
382
    /**
383
     * Set up the default classes for the form. This is done on construct so that the default classes can be removed
384
     * after instantiation
385
     */
386
    protected function setupDefaultClasses()
387
    {
388
        $defaultClasses = self::config()->get('default_classes');
389
        if ($defaultClasses) {
390
            foreach ($defaultClasses as $class) {
391
                $this->addExtraClass($class);
392
            }
393
        }
394
    }
395
396
    /**
397
     * Return a link to this field.
398
     *
399
     * @param string $action
400
     *
401
     * @return string
402
     */
403
    public function Link($action = null)
404
    {
405
        return Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
406
    }
407
408
    /**
409
     * Returns the HTML ID of the field.
410
     *
411
     * The ID is generated as FormName_FieldName. All Field functions should ensure that this ID is
412
     * included in the field.
413
     *
414
     * @return string
415
     */
416
    public function ID()
417
    {
418
        return $this->getTemplateHelper()->generateFieldID($this);
419
    }
420
421
    /**
422
     * Returns the HTML ID for the form field holder element.
423
     *
424
     * @return string
425
     */
426
    public function HolderID()
427
    {
428
        return $this->getTemplateHelper()->generateFieldHolderID($this);
429
    }
430
431
    /**
432
     * Returns the current {@link FormTemplateHelper} on either the parent
433
     * Form or the global helper set through the {@link Injector} layout.
434
     *
435
     * To customize a single {@link FormField}, use {@link setTemplate} and
436
     * provide a custom template name.
437
     *
438
     * @return FormTemplateHelper
439
     */
440
    public function getTemplateHelper()
441
    {
442
        if ($this->form) {
443
            return $this->form->getTemplateHelper();
444
        }
445
446
        return FormTemplateHelper::singleton();
447
    }
448
449
    /**
450
     * Returns the field name.
451
     *
452
     * @return string
453
     */
454
    public function getName()
455
    {
456
        return $this->name;
457
    }
458
459
    /**
460
     * Returns the field message, used by form validation.
461
     *
462
     * Use {@link setError()} to set this property.
463
     *
464
     * @return string
465
     */
466
    public function Message()
467
    {
468
        return $this->message;
469
    }
470
471
    /**
472
     * Returns the field message type.
473
     *
474
     * Arbitrary value which is mostly used for CSS classes in the rendered HTML, e.g "required".
475
     *
476
     * Use {@link setError()} to set this property.
477
     *
478
     * @return string
479
     */
480
    public function MessageType()
481
    {
482
        return $this->messageType;
483
    }
484
485
    /**
486
     * Returns the field value.
487
     *
488
     * @return mixed
489
     */
490
    public function Value()
491
    {
492
        return $this->value;
493
    }
494
495
    /**
496
     * Method to save this form field into the given {@link DataObject}.
497
     *
498
     * By default, makes use of $this->dataValue()
499
     *
500
     * @param DataObject|DataObjectInterface $record DataObject to save data into
501
     */
502
    public function saveInto(DataObjectInterface $record)
503
    {
504
        if ($this->name) {
505
            $record->setCastedField($this->name, $this->dataValue());
506
        }
507
    }
508
509
    /**
510
     * Returns the field value suitable for insertion into the data object.
511
     *
512
     * @return mixed
513
     */
514
    public function dataValue()
515
    {
516
        return $this->value;
517
    }
518
519
    /**
520
     * Returns the field label - used by templates.
521
     *
522
     * @return string
523
     */
524
    public function Title()
525
    {
526
        return $this->title;
527
    }
528
529
    /**
530
     * Set the title of this formfield.
531
     * Note: This expects escaped HTML.
532
     *
533
     * @param string $title Escaped HTML for title
534
     * @return $this
535
     */
536
    public function setTitle($title)
537
    {
538
        $this->title = $title;
539
        return $this;
540
    }
541
542
    /**
543
     * Gets the contextual label than can be used for additional field description.
544
     * Can be shown to the right or under the field in question.
545
     *
546
     * @return string Contextual label text.
547
     */
548
    public function RightTitle()
549
    {
550
        return $this->rightTitle;
551
    }
552
553
    /**
554
     * Sets the right title for this formfield
555
     * Note: This expects escaped HTML.
556
     *
557
     * @param string $rightTitle Escaped HTML for title
558
     * @return $this
559
     */
560
    public function setRightTitle($rightTitle)
561
    {
562
        $this->rightTitle = $rightTitle;
563
        return $this;
564
    }
565
566
    /**
567
     * @return string
568
     */
569
    public function LeftTitle()
570
    {
571
        return $this->leftTitle;
572
    }
573
574
    /**
575
     * @param string $leftTitle
576
     *
577
     * @return $this
578
     */
579
    public function setLeftTitle($leftTitle)
580
    {
581
        $this->leftTitle = $leftTitle;
582
583
        return $this;
584
    }
585
586
    /**
587
     * Compiles all CSS-classes. Optionally includes a "form-group--no-label" class if no title was set on the
588
     * FormField.
589
     *
590
     * Uses {@link Message()} and {@link MessageType()} to add validation error classes which can
591
     * be used to style the contained tags.
592
     *
593
     * @return string
594
     */
595
    public function extraClass()
596
    {
597
        $classes = array();
598
599
        $classes[] = $this->Type();
600
601
        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...
602
            $classes = array_merge(
603
                $classes,
604
                array_values($this->extraClasses)
605
            );
606
        }
607
608
        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...
609
            $classes[] = 'form-group--no-label';
610
        }
611
612
        // Allow custom styling of any element in the container based on validation errors,
613
        // e.g. red borders on input tags.
614
        //
615
        // CSS class needs to be different from the one rendered through {@link FieldHolder()}.
616
        if ($this->Message()) {
617
            $classes[] .= 'holder-' . $this->MessageType();
618
        }
619
620
        return implode(' ', $classes);
621
    }
622
623
    /**
624
     * Add one or more CSS-classes to the FormField container.
625
     *
626
     * Multiple class names should be space delimited.
627
     *
628
     * @param string $class
629
     *
630
     * @return $this
631
     */
632
    public function addExtraClass($class)
633
    {
634
        $classes = preg_split('/\s+/', $class);
635
636
        foreach ($classes as $class) {
637
            $this->extraClasses[$class] = $class;
638
        }
639
640
        return $this;
641
    }
642
643
    /**
644
     * Remove one or more CSS-classes from the FormField container.
645
     *
646
     * @param string $class
647
     *
648
     * @return $this
649
     */
650
    public function removeExtraClass($class)
651
    {
652
        $classes = preg_split('/\s+/', $class);
653
654
        foreach ($classes as $class) {
655
            unset($this->extraClasses[$class]);
656
        }
657
658
        return $this;
659
    }
660
661
    /**
662
     * Set an HTML attribute on the field element, mostly an <input> tag.
663
     *
664
     * Some attributes are best set through more specialized methods, to avoid interfering with
665
     * built-in behaviour:
666
     *
667
     * - 'class': {@link addExtraClass()}
668
     * - 'title': {@link setDescription()}
669
     * - 'value': {@link setValue}
670
     * - 'name': {@link setName}
671
     *
672
     * Caution: this doesn't work on most fields which are composed of more than one HTML form
673
     * field.
674
     *
675
     * @param string $name
676
     * @param string $value
677
     *
678
     * @return $this
679
     */
680
    public function setAttribute($name, $value)
681
    {
682
        $this->attributes[$name] = $value;
683
684
        return $this;
685
    }
686
687
    /**
688
     * Get an HTML attribute defined by the field, or added through {@link setAttribute()}.
689
     *
690
     * Caution: this doesn't work on all fields, see {@link setAttribute()}.
691
     *
692
     * @param string $name
693
     * @return string
694
     */
695
    public function getAttribute($name)
696
    {
697
        $attributes = $this->getAttributes();
698
699
        if (isset($attributes[$name])) {
700
            return $attributes[$name];
701
        }
702
703
        return null;
704
    }
705
706
    /**
707
     * Allows customization through an 'updateAttributes' hook on the base class.
708
     * Existing attributes are passed in as the first argument and can be manipulated,
709
     * but any attributes added through a subclass implementation won't be included.
710
     *
711
     * @return array
712
     */
713
    public function getAttributes()
714
    {
715
        $attributes = array(
716
            'type' => 'text',
717
            'name' => $this->getName(),
718
            'value' => $this->Value(),
719
            'class' => $this->extraClass(),
720
            'id' => $this->ID(),
721
            'disabled' => $this->isDisabled(),
722
            'readonly' => $this->isReadonly()
723
        );
724
725
        if ($this->Required()) {
726
            $attributes['required'] = 'required';
727
            $attributes['aria-required'] = 'true';
728
        }
729
730
        $attributes = array_merge($attributes, $this->attributes);
731
732
        $this->extend('updateAttributes', $attributes);
733
734
        return $attributes;
735
    }
736
737
    /**
738
     * Custom attributes to process. Falls back to {@link getAttributes()}.
739
     *
740
     * If at least one argument is passed as a string, all arguments act as excludes, by name.
741
     *
742
     * @param array $attributes
743
     *
744
     * @return string
745
     */
746
    public function getAttributesHTML($attributes = null)
747
    {
748
        $exclude = null;
749
750
        if (is_string($attributes)) {
751
            $exclude = func_get_args();
752
        }
753
754
        if (!$attributes || is_string($attributes)) {
755
            $attributes = $this->getAttributes();
756
        }
757
758
        $attributes = (array) $attributes;
759
760
        $attributes = array_filter($attributes, function ($v) {
761
            return ($v || $v === 0 || $v === '0');
762
        });
763
764
        if ($exclude) {
765
            $attributes = array_diff_key(
766
                $attributes,
767
                array_flip($exclude)
768
            );
769
        }
770
771
        // Create markup
772
        $parts = array();
773
774
        foreach ($attributes as $name => $value) {
775
            if ($value === true) {
776
                $parts[] = sprintf('%s="%s"', $name, $name);
777
            } else {
778
                $parts[] = sprintf('%s="%s"', $name, Convert::raw2att($value));
779
            }
780
        }
781
782
        return implode(' ', $parts);
783
    }
784
785
    /**
786
     * Returns a version of a title suitable for insertion into an HTML attribute.
787
     *
788
     * @return string
789
     */
790
    public function attrTitle()
791
    {
792
        return Convert::raw2att($this->title);
793
    }
794
795
    /**
796
     * Returns a version of a title suitable for insertion into an HTML attribute.
797
     *
798
     * @return string
799
     */
800
    public function attrValue()
801
    {
802
        return Convert::raw2att($this->value);
803
    }
804
805
    /**
806
     * Set the field value.
807
     *
808
     * @param mixed $value
809
     * @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...
810
     * @return $this
811
     */
812
    public function setValue($value)
813
    {
814
        $this->value = $value;
815
        return $this;
816
    }
817
818
    /**
819
     * Set the field name.
820
     *
821
     * @param string $name
822
     *
823
     * @return $this
824
     */
825
    public function setName($name)
826
    {
827
        $this->name = $name;
828
829
        return $this;
830
    }
831
832
    /**
833
     * Set the container form.
834
     *
835
     * This is called automatically when fields are added to forms.
836
     *
837
     * @param Form $form
838
     *
839
     * @return $this
840
     */
841
    public function setForm($form)
842
    {
843
        $this->form = $form;
844
845
        return $this;
846
    }
847
848
    /**
849
     * Get the currently used form.
850
     *
851
     * @return Form
852
     */
853
    public function getForm()
854
    {
855
        return $this->form;
856
    }
857
858
    /**
859
     * Return true if security token protection is enabled on the parent {@link Form}.
860
     *
861
     * @return bool
862
     */
863
    public function securityTokenEnabled()
864
    {
865
        $form = $this->getForm();
866
867
        if (!$form) {
868
            return false;
869
        }
870
871
        return $form->getSecurityToken()->isEnabled();
872
    }
873
874
    /**
875
     * Sets the error message to be displayed on the form field.
876
     *
877
     * Allows HTML content, so remember to use Convert::raw2xml().
878
     *
879
     * @param string $message
880
     * @param string $messageType
881
     *
882
     * @return $this
883
     */
884
    public function setError($message, $messageType)
885
    {
886
        $this->message = $message;
887
        $this->messageType = $messageType;
888
889
        return $this;
890
    }
891
892
    /**
893
     * Set the custom error message to show instead of the default format.
894
     *
895
     * Different from setError() as that appends it to the standard error messaging.
896
     *
897
     * @param string $customValidationMessage
898
     *
899
     * @return $this
900
     */
901
    public function setCustomValidationMessage($customValidationMessage)
902
    {
903
        $this->customValidationMessage = $customValidationMessage;
904
905
        return $this;
906
    }
907
908
    /**
909
     * Get the custom error message for this form field. If a custom message has not been defined
910
     * then just return blank. The default error is defined on {@link Validator}.
911
     *
912
     * @return string
913
     */
914
    public function getCustomValidationMessage()
915
    {
916
        return $this->customValidationMessage;
917
    }
918
919
    /**
920
     * Set name of template (without path or extension).
921
     *
922
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
923
     * method on the subclass for support.
924
     *
925
     * @param string $template
926
     *
927
     * @return $this
928
     */
929
    public function setTemplate($template)
930
    {
931
        $this->template = $template;
932
933
        return $this;
934
    }
935
936
    /**
937
     * @return string
938
     */
939
    public function getTemplate()
940
    {
941
        return $this->template;
942
    }
943
944
    /**
945
     * @return string
946
     */
947
    public function getFieldHolderTemplate()
948
    {
949
        return $this->fieldHolderTemplate;
950
    }
951
952
    /**
953
     * Set name of template (without path or extension) for the holder, which in turn is
954
     * responsible for rendering {@link Field()}.
955
     *
956
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
957
     * method on the subclass for support.
958
     *
959
     * @param string $fieldHolderTemplate
960
     *
961
     * @return $this
962
     */
963
    public function setFieldHolderTemplate($fieldHolderTemplate)
964
    {
965
        $this->fieldHolderTemplate = $fieldHolderTemplate;
966
967
        return $this;
968
    }
969
970
    /**
971
     * @return string
972
     */
973
    public function getSmallFieldHolderTemplate()
974
    {
975
        return $this->smallFieldHolderTemplate;
976
    }
977
978
    /**
979
     * Set name of template (without path or extension) for the small holder, which in turn is
980
     * responsible for rendering {@link Field()}.
981
     *
982
     * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
983
     * method on the subclass for support.
984
     *
985
     * @param string $smallFieldHolderTemplate
986
     *
987
     * @return $this
988
     */
989
    public function setSmallFieldHolderTemplate($smallFieldHolderTemplate)
990
    {
991
        $this->smallFieldHolderTemplate = $smallFieldHolderTemplate;
992
993
        return $this;
994
    }
995
996
    /**
997
     * Returns the form field.
998
     *
999
     * Although FieldHolder is generally what is inserted into templates, all of the field holder
1000
     * templates make use of $Field. It's expected that FieldHolder will give you the "complete"
1001
     * representation of the field on the form, whereas Field will give you the core editing widget,
1002
     * such as an input tag.
1003
     *
1004
     * @param array $properties
1005
     * @return DBHTMLText
1006
     */
1007
    public function Field($properties = array())
1008
    {
1009
        $context = $this;
1010
1011
        if (count($properties)) {
1012
            $context = $context->customise($properties);
1013
        }
1014
1015
        $this->extend('onBeforeRender', $this);
1016
1017
        $result = $context->renderWith($this->getTemplates());
1018
1019
        // Trim whitespace from the result, so that trailing newlines are supressed. Works for strings and HTMLText values
1020
        if (is_string($result)) {
1021
            $result = trim($result);
1022
        } elseif ($result instanceof DBField) {
1023
            $result->setValue(trim($result->getValue()));
1024
        }
1025
1026
        return $result;
1027
    }
1028
1029
    /**
1030
     * Returns a "field holder" for this field.
1031
     *
1032
     * Forms are constructed by concatenating a number of these field holders.
1033
     *
1034
     * The default field holder is a label and a form field inside a div.
1035
     *
1036
     * @see FieldHolder.ss
1037
     *
1038
     * @param array $properties
1039
     *
1040
     * @return DBHTMLText
1041
     */
1042
    public function FieldHolder($properties = array())
1043
    {
1044
        $context = $this;
1045
1046
        if (count($properties)) {
1047
            $context = $this->customise($properties);
1048
        }
1049
1050
        return $context->renderWith($this->getFieldHolderTemplates());
1051
    }
1052
1053
    /**
1054
     * Returns a restricted field holder used within things like FieldGroups.
1055
     *
1056
     * @param array $properties
1057
     *
1058
     * @return string
1059
     */
1060
    public function SmallFieldHolder($properties = array())
1061
    {
1062
        $context = $this;
1063
1064
        if (count($properties)) {
1065
            $context = $this->customise($properties);
1066
        }
1067
1068
        return $context->renderWith($this->getSmallFieldHolderTemplates());
1069
    }
1070
1071
    /**
1072
     * Returns an array of templates to use for rendering {@link FieldHolder}.
1073
     *
1074
     * @return array
1075
     */
1076
    public function getTemplates()
1077
    {
1078
        return $this->_templates($this->getTemplate());
1079
    }
1080
1081
    /**
1082
     * Returns an array of templates to use for rendering {@link FieldHolder}.
1083
     *
1084
     * @return array
1085
     */
1086
    public function getFieldHolderTemplates()
1087
    {
1088
        return $this->_templates(
1089
            $this->getFieldHolderTemplate(),
1090
            '_holder'
1091
        );
1092
    }
1093
1094
    /**
1095
     * Returns an array of templates to use for rendering {@link SmallFieldHolder}.
1096
     *
1097
     * @return array
1098
     */
1099
    public function getSmallFieldHolderTemplates()
1100
    {
1101
        return $this->_templates(
1102
            $this->getSmallFieldHolderTemplate(),
1103
            '_holder_small'
1104
        );
1105
    }
1106
1107
1108
    /**
1109
     * Generate an array of class name strings to use for rendering this form field into HTML.
1110
     *
1111
     * @param string $customTemplate
1112
     * @param string $customTemplateSuffix
1113
     *
1114
     * @return array
1115
     */
1116
    protected function _templates($customTemplate = null, $customTemplateSuffix = null)
1117
    {
1118
        $templates = SSViewer::get_templates_by_class(get_class($this), $customTemplateSuffix, __CLASS__);
1119
        // Prefer any custom template
1120
        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...
1121
            // Prioritise direct template
1122
            array_unshift($templates, $customTemplate);
1123
        }
1124
        return $templates;
1125
    }
1126
1127
    /**
1128
     * Returns true if this field is a composite field.
1129
     *
1130
     * To create composite field types, you should subclass {@link CompositeField}.
1131
     *
1132
     * @return bool
1133
     */
1134
    public function isComposite()
1135
    {
1136
        return false;
1137
    }
1138
1139
    /**
1140
     * Returns true if this field has its own data.
1141
     *
1142
     * Some fields, such as titles and composite fields, don't actually have any data. It doesn't
1143
     * make sense for data-focused methods to look at them. By overloading hasData() to return
1144
     * false, you can prevent any data-focused methods from looking at it.
1145
     *
1146
     * @see FieldList::collateDataFields()
1147
     *
1148
     * @return bool
1149
     */
1150
    public function hasData()
1151
    {
1152
        return true;
1153
    }
1154
1155
    /**
1156
     * @return bool
1157
     */
1158
    public function isReadonly()
1159
    {
1160
        return $this->readonly;
1161
    }
1162
1163
    /**
1164
     * Sets a read-only flag on this FormField.
1165
     *
1166
     * Use performReadonlyTransformation() to transform this instance.
1167
     *
1168
     * Setting this to false has no effect on the field.
1169
     *
1170
     * @param bool $readonly
1171
     *
1172
     * @return $this
1173
     */
1174
    public function setReadonly($readonly)
1175
    {
1176
        $this->readonly = $readonly;
1177
1178
        return $this;
1179
    }
1180
1181
    /**
1182
     * @return bool
1183
     */
1184
    public function isDisabled()
1185
    {
1186
        return $this->disabled;
1187
    }
1188
1189
    /**
1190
     * Sets a disabled flag on this FormField.
1191
     *
1192
     * Use performDisabledTransformation() to transform this instance.
1193
     *
1194
     * Setting this to false has no effect on the field.
1195
     *
1196
     * @param bool $disabled
1197
     *
1198
     * @return $this
1199
     */
1200
    public function setDisabled($disabled)
1201
    {
1202
        $this->disabled = $disabled;
1203
1204
        return $this;
1205
    }
1206
1207
    /**
1208
     * Returns a read-only version of this field.
1209
     *
1210
     * @return FormField
1211
     */
1212
    public function performReadonlyTransformation()
1213
    {
1214
        $readonlyClassName = $this->class . '_Readonly';
1215
1216
        if (ClassInfo::exists($readonlyClassName)) {
1217
            $clone = $this->castedCopy($readonlyClassName);
1218
        } else {
1219
            $clone = $this->castedCopy('SilverStripe\\Forms\\ReadonlyField');
1220
        }
1221
1222
        $clone->setReadonly(true);
1223
1224
        return $clone;
1225
    }
1226
1227
    /**
1228
     * Return a disabled version of this field.
1229
     *
1230
     * Tries to find a class of the class name of this field suffixed with "_Disabled", failing
1231
     * that, finds a method {@link setDisabled()}.
1232
     *
1233
     * @return FormField
1234
     */
1235
    public function performDisabledTransformation()
1236
    {
1237
        $disabledClassName = $this->class . '_Disabled';
1238
1239
        if (ClassInfo::exists($disabledClassName)) {
1240
            $clone = $this->castedCopy($disabledClassName);
1241
        } else {
1242
            $clone = clone $this;
1243
        }
1244
1245
        $clone->setDisabled(true);
1246
1247
        return $clone;
1248
    }
1249
1250
    /**
1251
     * @param FormTransformation $transformation
1252
     *
1253
     * @return mixed
1254
     */
1255
    public function transform(FormTransformation $transformation)
1256
    {
1257
        return $transformation->transform($this);
1258
    }
1259
1260
    /**
1261
     * @param string $class
1262
     *
1263
     * @return int
1264
     */
1265
    public function hasClass($class)
1266
    {
1267
        $patten = '/' . strtolower($class) . '/i';
1268
1269
        $subject = strtolower($this->class . ' ' . $this->extraClass());
1270
1271
        return preg_match($patten, $subject);
1272
    }
1273
1274
    /**
1275
     * Returns the field type.
1276
     *
1277
     * The field type is the class name with the word Field dropped off the end, all lowercase.
1278
     *
1279
     * It's handy for assigning HTML classes. Doesn't signify the <input type> attribute.
1280
     *
1281
     * @see {link getAttributes()}.
1282
     *
1283
     * @return string
1284
     */
1285
    public function Type()
1286
    {
1287
        $type = new ReflectionClass($this);
1288
        return strtolower(preg_replace('/Field$/', '', $type->getShortName()));
1289
    }
1290
1291
    /**
1292
     * @deprecated 4.0 Use FormField::create_tag()
1293
     *
1294
     * @param string $tag
1295
     * @param array $attributes
1296
     * @param null|string $content
1297
     *
1298
     * @return string
1299
     */
1300
    public function createTag($tag, $attributes, $content = null)
1301
    {
1302
        Deprecation::notice('4.0', 'Use FormField::create_tag()');
1303
1304
        return self::create_tag($tag, $attributes, $content);
1305
    }
1306
1307
    /**
1308
     * Abstract method each {@link FormField} subclass must implement, determines whether the field
1309
     * is valid or not based on the value.
1310
     *
1311
     * @todo Make this abstract.
1312
     *
1313
     * @param Validator $validator
1314
     * @return bool
1315
     */
1316
    public function validate($validator)
1317
    {
1318
        return true;
1319
    }
1320
1321
    /**
1322
     * Describe this field, provide help text for it.
1323
     *
1324
     * By default, renders as a <span class="description"> underneath the form field.
1325
     *
1326
     * @param string $description
1327
     *
1328
     * @return $this
1329
     */
1330
    public function setDescription($description)
1331
    {
1332
        $this->description = $description;
1333
1334
        return $this;
1335
    }
1336
1337
    /**
1338
     * @return string
1339
     */
1340
    public function getDescription()
1341
    {
1342
        return $this->description;
1343
    }
1344
1345
    /**
1346
     * @return string
1347
     */
1348
    public function debug()
1349
    {
1350
        return sprintf(
1351
            '%s (%s: %s : <span style="color:red;">%s</span>) = %s',
1352
            $this->class,
1353
            $this->name,
1354
            $this->title,
1355
            $this->message,
1356
            $this->value
1357
        );
1358
    }
1359
1360
    /**
1361
     * This function is used by the template processor. If you refer to a field as a $ variable, it
1362
     * will return the $Field value.
1363
     *
1364
     * @return string
1365
     */
1366
    public function forTemplate()
1367
    {
1368
        return $this->Field();
1369
    }
1370
1371
    /**
1372
     * @return bool
1373
     */
1374
    public function Required()
1375
    {
1376
        if ($this->form && ($validator = $this->form->getValidator())) {
1377
            return $validator->fieldIsRequired($this->name);
1378
        }
1379
1380
        return false;
1381
    }
1382
1383
    /**
1384
     * Set the FieldList that contains this field.
1385
     *
1386
     * @param FieldList $containerFieldList
1387
     * @return $this
1388
     */
1389
    public function setContainerFieldList($containerFieldList)
1390
    {
1391
        $this->containerFieldList = $containerFieldList;
1392
        return $this;
1393
    }
1394
1395
    /**
1396
     * Get the FieldList that contains this field.
1397
     *
1398
     * @return FieldList
1399
     */
1400
    public function getContainerFieldList()
1401
    {
1402
        return $this->containerFieldList;
1403
    }
1404
1405
    /**
1406
     * @return null|FieldList
1407
     */
1408
    public function rootFieldList()
1409
    {
1410
        if (is_object($this->containerFieldList)) {
1411
            return $this->containerFieldList->rootFieldList();
1412
        }
1413
1414
        user_error(
1415
            "rootFieldList() called on $this->class object without a containerFieldList",
1416
            E_USER_ERROR
1417
        );
1418
1419
        return null;
1420
    }
1421
1422
    /**
1423
     * Returns another instance of this field, but "cast" to a different class. The logic tries to
1424
     * retain all of the instance properties, and may be overloaded by subclasses to set additional
1425
     * ones.
1426
     *
1427
     * Assumes the standard FormField parameter signature with its name as the only mandatory
1428
     * argument. Mainly geared towards creating *_Readonly or *_Disabled subclasses of the same
1429
     * type, or casting to a {@link ReadonlyField}.
1430
     *
1431
     * Does not copy custom field templates, since they probably won't apply to the new instance.
1432
     *
1433
     * @param mixed $classOrCopy Class name for copy, or existing copy instance to update
1434
     *
1435
     * @return FormField
1436
     */
1437
    public function castedCopy($classOrCopy)
1438
    {
1439
        $field = $classOrCopy;
1440
1441
        if (!is_object($field)) {
1442
            $field = new $classOrCopy($this->name);
1443
        }
1444
1445
        $field
1446
            ->setValue($this->value)
1447
            ->setForm($this->form)
1448
            ->setTitle($this->Title())
1449
            ->setLeftTitle($this->LeftTitle())
1450
            ->setRightTitle($this->RightTitle())
1451
            ->addExtraClass($this->extraClass) // Don't use extraClass(), since this merges calculated values
1452
            ->setDescription($this->getDescription());
1453
1454
        // Only include built-in attributes, ignore anything set through getAttributes().
1455
        // Those might change important characteristics of the field, e.g. its "type" attribute.
1456
        foreach ($this->attributes as $attributeKey => $attributeValue) {
1457
            $field->setAttribute($attributeKey, $attributeValue);
1458
        }
1459
1460
        return $field;
1461
    }
1462
1463
    /**
1464
     * Determine if the value of this formfield accepts front-end submitted values and is saveable.
1465
     *
1466
     * @return bool
1467
     */
1468
    public function canSubmitValue()
1469
    {
1470
        return $this->hasData() && !$this->isReadonly() && !$this->isDisabled();
1471
    }
1472
1473
    /**
1474
     * Sets the component type the FormField will be rendered as on the front-end.
1475
     *
1476
     * @param string $componentType
1477
     * @return FormField
1478
     */
1479
    public function setSchemaComponent($componentType)
1480
    {
1481
        $this->schemaComponent = $componentType;
1482
        return $this;
1483
    }
1484
1485
    /**
1486
     * Gets the type of front-end component the FormField will be rendered as.
1487
     *
1488
     * @return string
1489
     */
1490
    public function getSchemaComponent()
1491
    {
1492
        return $this->schemaComponent;
1493
    }
1494
1495
    /**
1496
     * Sets the schema data used for rendering the field on the front-end.
1497
     * Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
1498
     * Any passed keys that are not defined in {@link getSchemaDataDefaults()} are ignored.
1499
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1500
     *
1501
     * @param array $schemaData - The data to be merged with $this->schemaData.
1502
     * @return FormField
1503
     *
1504
     * @todo Add deep merging of arrays like `data` and `attributes`.
1505
     */
1506
    public function setSchemaData($schemaData = [])
1507
    {
1508
        $defaults = $this->getSchemaData();
1509
        $this->schemaData = array_merge($this->schemaData, array_intersect_key($schemaData, $defaults));
1510
        return $this;
1511
    }
1512
1513
    /**
1514
     * Gets the schema data used to render the FormField on the front-end.
1515
     *
1516
     * @return array
1517
     */
1518
    public function getSchemaData()
1519
    {
1520
        $defaults = $this->getSchemaDataDefaults();
1521
        return array_replace_recursive($defaults, array_intersect_key($this->schemaData, $defaults));
1522
    }
1523
1524
    /**
1525
     * @todo Throw exception if value is missing, once a form field schema is mandatory across the CMS
1526
     *
1527
     * @return string
1528
     */
1529
    public function getSchemaDataType()
1530
    {
1531
        return $this->schemaDataType;
1532
    }
1533
1534
    /**
1535
     * Gets the defaults for $schemaData.
1536
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaData()} are ignored.
1537
     * Instead the `data` array should be used to pass around ad hoc data.
1538
     *
1539
     * @return array
1540
     */
1541
    public function getSchemaDataDefaults()
1542
    {
1543
        return [
1544
            'name' => $this->getName(),
1545
            'id' => $this->ID(),
1546
            'type' => $this->getSchemaDataType(),
1547
            'component' => $this->getSchemaComponent(),
1548
            'holderId' => $this->HolderID(),
1549
            'title' => $this->Title(),
1550
            'source' => null,
1551
            'extraClass' => $this->extraClass(),
1552
            'description' => $this->obj('Description')->getSchemaValue(),
1553
            'rightTitle' => $this->RightTitle(),
1554
            'leftTitle' => $this->LeftTitle(),
1555
            'readOnly' => $this->isReadonly(),
1556
            'disabled' => $this->isDisabled(),
1557
            'customValidationMessage' => $this->getCustomValidationMessage(),
1558
            'validation' => $this->getSchemaValidation(),
1559
            'attributes' => [],
1560
            'data' => [],
1561
        ];
1562
    }
1563
1564
    /**
1565
     * Sets the schema data used for rendering the field on the front-end.
1566
     * Merges the passed array with the current `$schemaState` or {@link getSchemaStateDefaults()}.
1567
     * Any passed keys that are not defined in {@link getSchemaStateDefaults()} are ignored.
1568
     * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1569
     *
1570
     * @param array $schemaState The data to be merged with $this->schemaData.
1571
     * @return FormField
1572
     *
1573
     * @todo Add deep merging of arrays like `data` and `attributes`.
1574
     */
1575
    public function setSchemaState($schemaState = [])
1576
    {
1577
        $defaults = $this->getSchemaState();
1578
        $this->schemaState = array_merge($this->schemaState, array_intersect_key($schemaState, $defaults));
1579
        return $this;
1580
    }
1581
1582
    /**
1583
     * Gets the schema state used to render the FormField on the front-end.
1584
     *
1585
     * @return array
1586
     */
1587
    public function getSchemaState()
1588
    {
1589
        $defaults = $this->getSchemaStateDefaults();
1590
        return array_merge($defaults, array_intersect_key($this->schemaState, $defaults));
1591
    }
1592
1593
    /**
1594
     * Gets the defaults for $schemaState.
1595
     * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaState()} are ignored.
1596
     * Instead the `data` array should be used to pass around ad hoc data.
1597
     * Includes validation data if the field is associated to a {@link Form},
1598
     * and {@link Form->validate()} has been called.
1599
     *
1600
     * @todo Make form / field messages not always stored as html; Store value / casting as separate values.
1601
     * @return array
1602
     */
1603
    public function getSchemaStateDefaults()
1604
    {
1605
        $state = [
1606
            'name' => $this->getName(),
1607
            'id' => $this->ID(),
1608
            'value' => $this->Value(),
1609
            'message' => null,
1610
            'data' => [],
1611
        ];
1612
1613
        if ($message = $this->Message()) {
1614
            $state['message'] = [
1615
                'value' => ['html' => $message],
1616
                'type' => $this->MessageType(),
1617
            ];
1618
        }
1619
1620
        return $state;
1621
    }
1622
1623
    /**
1624
     * Return list of validation rules. Each rule is a key value pair.
1625
     * The key is the rule name. The value is any information the frontend
1626
     * validation handler can understand, or just `true` to enable.
1627
     *
1628
     * @return array
1629
     */
1630
    public function getSchemaValidation()
1631
    {
1632
        if ($this->Required()) {
1633
            return [ 'required' => true ];
1634
        }
1635
        return [];
1636
    }
1637
}
1638