Completed
Push — master ( b4526e...0fa727 )
by Damian
16s
created

FormField::setSchemaComponent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
/**
4
 * Represents a field in a form.
5
 *
6
 * A FieldList contains a number of FormField objects which make up the whole of a form.
7
 *
8
 * In addition to single fields, FormField objects can be "composite", for example, the
9
 * {@link TabSet} field. Composite fields let us define complex forms without having to resort to
10
 * custom HTML.
11
 *
12
 * To subclass:
13
 *
14
 * Define a {@link dataValue()} method that returns a value suitable for inserting into a single
15
 * database field.
16
 *
17
 * For example, you might tidy up the format of a date or currency field. Define {@link saveInto()}
18
 * to totally customise saving.
19
 *
20
 * For example, data might be saved to the filesystem instead of the data record, or saved to a
21
 * component of the data record instead of the data record itself.
22
 *
23
 * A form field can be represented as structured data through {@link FormSchema},
24
 * including both structure (name, id, attributes, etc.) and state (field value).
25
 * Can be used by for JSON data which is consumed by a front-end application.
26
 *
27
 * @package forms
28
 * @subpackage core
29
 */
30
class FormField extends RequestHandler {
31
32
	/** @see $schemaDataType */
33
	const SCHEMA_DATA_TYPE_STRING = 'String';
34
35
	/** @see $schemaDataType */
36
	const SCHEMA_DATA_TYPE_HIDDEN = 'Hidden';
37
38
	/** @see $schemaDataType */
39
	const SCHEMA_DATA_TYPE_TEXT = 'Text';
40
41
	/** @see $schemaDataType */
42
	const SCHEMA_DATA_TYPE_HTML = 'HTML';
43
44
	/** @see $schemaDataType */
45
	const SCHEMA_DATA_TYPE_INTEGER = 'Integer';
46
47
	/** @see $schemaDataType */
48
	const SCHEMA_DATA_TYPE_DECIMAL = 'Decimal';
49
50
	/** @see $schemaDataType */
51
	const SCHEMA_DATA_TYPE_MULTISELECT = 'MultiSelect';
52
53
	/** @see $schemaDataType */
54
	const SCHEMA_DATA_TYPE_SINGLESELECT = 'SingleSelect';
55
56
	/** @see $schemaDataType */
57
	const SCHEMA_DATA_TYPE_DATE = 'Date';
58
59
	/** @see $schemaDataType */
60
	const SCHEMA_DATA_TYPE_DATETIME = 'DateTime';
61
62
	/** @see $schemaDataType */
63
	const SCHEMA_DATA_TYPE_TIME = 'Time';
64
65
	/** @see $schemaDataType */
66
	const SCHEMA_DATA_TYPE_BOOLEAN = 'Boolean';
67
68
	/** @see $schemaDataType */
69
	const SCHEMA_DATA_TYPE_CUSTOM = 'Custom';
70
71
	/** @see $schemaDataType */
72
	const SCHEMA_DATA_TYPE_STRUCTURAL = 'Structural';
73
74
	/**
75
	 * @var Form
76
	 */
77
	protected $form;
78
79
	/**
80
	 * @var string
81
	 */
82
	protected $name;
83
84
	/**
85
	 * @var null|string
86
	 */
87
	protected $title;
88
89
	/**
90
	 * @var mixed
91
	 */
92
	protected $value;
93
94
	/**
95
	 * @var string
96
	 */
97
	protected $message;
98
99
	/**
100
	 * @var string
101
	 */
102
	protected $messageType;
103
104
	/**
105
	 * @var string
106
	 */
107
	protected $extraClass;
108
109
	/**
110
	 * Adds a title attribute to the markup.
111
	 *
112
	 * @var string
113
	 *
114
	 * @todo Implement in all subclasses
115
	 */
116
	protected $description;
117
118
	/**
119
	 * Extra CSS classes for the FormField container.
120
	 *
121
	 * @var array
122
	 */
123
	protected $extraClasses;
124
125
	/**
126
	 * @config
127
	 * @var array $default_classes The default classes to apply to the FormField
128
	 */
129
	private static $default_classes = [];
130
131
132
	/**
133
	 * @var bool
134
	 */
135
	public $dontEscape;
136
137
	/**
138
	 * Right-aligned, contextual label for the field.
139
	 *
140
	 * @var string
141
	 */
142
	protected $rightTitle;
143
144
	/**
145
	 * Left-aligned, contextual label for the field.
146
	 *
147
	 * @var string
148
	 */
149
	protected $leftTitle;
150
151
	/**
152
	 * Stores a reference to the FieldList that contains this object.
153
	 *
154
	 * @var FieldList
155
	 */
156
	protected $containerFieldList;
157
158
	/**
159
	 * @var bool
160
	 */
161
	protected $readonly = false;
162
163
	/**
164
	 * @var bool
165
	 */
166
	protected $disabled = false;
167
168
	/**
169
	 * Custom validation message for the field.
170
	 *
171
	 * @var string
172
	 */
173
	protected $customValidationMessage = '';
174
175
	/**
176
	 * Name of the template used to render this form field. If not set, then will look up the class
177
	 * ancestry for the first matching template where the template name equals the class name.
178
	 *
179
	 * To explicitly use a custom template or one named other than the form field see
180
	 * {@link setTemplate()}.
181
	 *
182
	 * @var string
183
	 */
184
	protected $template;
185
186
	/**
187
	 * Name of the template used to render this form field. If not set, then will look up the class
188
	 * ancestry for the first matching template where the template name equals the class name.
189
	 *
190
	 * To explicitly use a custom template or one named other than the form field see
191
	 * {@link setFieldHolderTemplate()}.
192
	 *
193
	 * @var string
194
	 */
195
	protected $fieldHolderTemplate;
196
197
	/**
198
	 * @var string
199
	 */
200
	protected $smallFieldHolderTemplate;
201
202
	/**
203
	 * All attributes on the form field (not the field holder).
204
	 *
205
	 * Partially determined based on other instance properties.
206
	 *
207
	 * @see getAttributes()
208
	 *
209
	 * @var array
210
	 */
211
	protected $attributes = [];
212
213
	/**
214
	 * The data type backing the field. Represents the type of value the
215
	 * form expects to receive via a postback. Should be set in subclasses.
216
	 *
217
	 * The values allowed in this list include:
218
	 *
219
	 *   - String: Single line text
220
	 *   - Hidden: Hidden field which is posted back without modification
221
	 *   - Text: Multi line text
222
	 *   - HTML: Rich html text
223
	 *   - Integer: Whole number value
224
	 *   - Decimal: Decimal value
225
	 *   - MultiSelect: Select many from source
226
	 *   - SingleSelect: Select one from source
227
	 *   - Date: Date only
228
	 *   - DateTime: Date and time
229
	 *   - Time: Time only
230
	 *   - Boolean: Yes or no
231
	 *   - Custom: Custom type declared by the front-end component. For fields with this type,
232
	 *     the component property is mandatory, and will determine the posted value for this field.
233
	 *   - Structural: Represents a field that is NOT posted back. This may contain other fields,
234
	 *     or simply be a block of stand-alone content. As with 'Custom',
235
	 *     the component property is mandatory if this is assigned.
236
	 *
237
	 * Each value has an equivalent constant, e.g. {@link self::SCHEMA_DATA_TYPE_STRING}.
238
	 *
239
	 * @var string
240
	 */
241
	 protected $schemaDataType;
242
243
	/**
244
	 * The type of front-end component to render the FormField as.
245
	 *
246
	 * @var string
247
	 */
248
	protected $schemaComponent;
249
250
	/**
251
	 * Structured schema data representing the FormField.
252
	 * Used to render the FormField as a ReactJS Component on the front-end.
253
	 *
254
	 * @var array
255
	 */
256
	protected $schemaData = [];
257
258
	/**
259
	 * Structured schema state representing the FormField's current data and validation.
260
	 * Used to render the FormField as a ReactJS Component on the front-end.
261
	 *
262
	 * @var array
263
	 */
264
	protected $schemaState = [];
265
266
	/**
267
	 * Takes a field name and converts camelcase to spaced words. Also resolves combined field
268
	 * names with dot syntax to spaced words.
269
	 *
270
	 * Examples:
271
	 *
272
	 * - 'TotalAmount' will return 'Total Amount'
273
	 * - 'Organisation.ZipCode' will return 'Organisation Zip Code'
274
	 *
275
	 * @param string $fieldName
276
	 *
277
	 * @return string
278
	 */
279
	public static function name_to_label($fieldName) {
280
		if(strpos($fieldName, '.') !== false) {
281
			$parts = explode('.', $fieldName);
282
283
			$label = $parts[count($parts) - 2] . ' ' . $parts[count($parts) - 1];
284
		} else {
285
			$label = $fieldName;
286
		}
287
288
		return preg_replace('/([a-z]+)([A-Z])/', '$1 $2', $label);
289
	}
290
291
	/**
292
	 * Construct and return HTML tag.
293
	 *
294
	 * @param string $tag
295
	 * @param array $attributes
296
	 * @param null|string $content
297
	 *
298
	 * @return string
299
	 */
300
	public static function create_tag($tag, $attributes, $content = null) {
301
		$preparedAttributes = '';
302
303
		foreach($attributes as $attributeKey => $attributeValue) {
304
			if(!empty($attributeValue) || $attributeValue === '0' || ($attributeKey == 'value' && $attributeValue !== null)) {
305
				$preparedAttributes .= sprintf(
306
					' %s="%s"', $attributeKey, Convert::raw2att($attributeValue)
307
				);
308
			}
309
		}
310
311
		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...
312
			return sprintf(
313
				'<%s%s>%s</%s>', $tag, $preparedAttributes, $content, $tag
314
			);
315
		}
316
317
		return sprintf(
318
			'<%s%s />', $tag, $preparedAttributes
319
		);
320
	}
321
322
	/**
323
	 * Creates a new field.
324
	 *
325
	 * @param string $name The internal field name, passed to forms.
326
	 * @param null|string $title The human-readable field label.
327
	 * @param mixed $value The value of the field.
328
	 */
329
	public function __construct($name, $title = null, $value = null) {
330
		$this->name = $name;
331
332
		if($title === null) {
333
			$this->title = self::name_to_label($name);
334
		} else {
335
			$this->title = $title;
336
		}
337
338
		if($value !== null) {
339
			$this->setValue($value);
340
		}
341
342
		parent::__construct();
343
344
		$this->setupDefaultClasses();
345
	}
346
347
	/**
348
	 * Set up the default classes for the form. This is done on construct so that the default classes can be removed
349
	 * after instantiation
350
	 */
351
	protected function setupDefaultClasses() {
352
		$defaultClasses = self::config()->get('default_classes');
353
		if ($defaultClasses) {
354
			foreach ($defaultClasses as $class) {
0 ignored issues
show
Bug introduced by
The expression $defaultClasses of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
355
				$this->addExtraClass($class);
356
			}
357
		}
358
	}
359
360
	/**
361
	 * Return a link to this field.
362
	 *
363
	 * @param string $action
364
	 *
365
	 * @return string
366
	 */
367
	public function Link($action = null) {
368
		return Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
369
	}
370
371
	/**
372
	 * Returns the HTML ID of the field.
373
	 *
374
	 * The ID is generated as FormName_FieldName. All Field functions should ensure that this ID is
375
	 * included in the field.
376
	 *
377
	 * @return string
378
	 */
379
	public function ID() {
380
		return $this->getTemplateHelper()->generateFieldID($this);
381
	}
382
383
	/**
384
	 * Returns the HTML ID for the form field holder element.
385
	 *
386
	 * @return string
387
	 */
388
	public function HolderID() {
389
		return $this->getTemplateHelper()->generateFieldHolderID($this);
390
	}
391
392
	/**
393
	 * Returns the current {@link FormTemplateHelper} on either the parent
394
	 * Form or the global helper set through the {@link Injector} layout.
395
	 *
396
	 * To customize a single {@link FormField}, use {@link setTemplate} and
397
	 * provide a custom template name.
398
	 *
399
	 * @return FormTemplateHelper
400
	 */
401
	public function getTemplateHelper() {
402
		if($this->form) {
403
			return $this->form->getTemplateHelper();
404
		}
405
406
		return Injector::inst()->get('FormTemplateHelper');
407
	}
408
409
	/**
410
	 * Returns the field name.
411
	 *
412
	 * @return string
413
	 */
414
	public function getName() {
415
		return $this->name;
416
	}
417
418
	/**
419
	 * Returns the field message, used by form validation.
420
	 *
421
	 * Use {@link setError()} to set this property.
422
	 *
423
	 * @return string
424
	 */
425
	public function Message() {
426
		return $this->message;
427
	}
428
429
	/**
430
	 * Returns the field message type.
431
	 *
432
	 * Arbitrary value which is mostly used for CSS classes in the rendered HTML, e.g "required".
433
	 *
434
	 * Use {@link setError()} to set this property.
435
	 *
436
	 * @return string
437
	 */
438
	public function MessageType() {
439
		return $this->messageType;
440
	}
441
442
	/**
443
	 * Returns the field value.
444
	 *
445
	 * @return mixed
446
	 */
447
	public function Value() {
448
		return $this->value;
449
	}
450
451
	/**
452
	 * Method to save this form field into the given {@link DataObject}.
453
	 *
454
	 * By default, makes use of $this->dataValue()
455
	 *
456
	 * @param DataObjectInterface $record DataObject to save data into
457
	 */
458
	public function saveInto(DataObjectInterface $record) {
459
		if($this->name) {
460
			$record->setCastedField($this->name, $this->dataValue());
461
		}
462
	}
463
464
	/**
465
	 * Returns the field value suitable for insertion into the data object.
466
	 *
467
	 * @return mixed
468
	 */
469
	public function dataValue() {
470
		return $this->value;
471
	}
472
473
	/**
474
	 * Returns the field label - used by templates.
475
	 *
476
	 * @return string
477
	 */
478
	public function Title() {
479
		return $this->title;
480
	}
481
482
	/**
483
	 * @param string $title
484
	 *
485
	 * @return $this
486
	 */
487
	public function setTitle($title) {
488
		$this->title = $title;
489
490
		return $this;
491
	}
492
493
	/**
494
	 * Gets the contextual label than can be used for additional field description.
495
	 * Can be shown to the right or under the field in question.
496
	 *
497
	 * @return string Contextual label text.
498
	 */
499
	public function RightTitle() {
500
		return $this->rightTitle;
501
	}
502
503
	/**
504
	 * @param string $rightTitle
505
	 *
506
	 * @return $this
507
	 */
508
	public function setRightTitle($rightTitle) {
509
		$this->rightTitle = $rightTitle;
510
511
		return $this;
512
	}
513
514
	/**
515
	 * @return string
516
	 */
517
	public function LeftTitle() {
518
		return $this->leftTitle;
519
	}
520
521
	/**
522
	 * @param string $leftTitle
523
	 *
524
	 * @return $this
525
	 */
526
	public function setLeftTitle($leftTitle) {
527
		$this->leftTitle = $leftTitle;
528
529
		return $this;
530
	}
531
532
	/**
533
	 * Compiles all CSS-classes. Optionally includes a "nolabel" class if no title was set on the
534
	 * FormField.
535
	 *
536
	 * Uses {@link Message()} and {@link MessageType()} to add validation error classes which can
537
	 * be used to style the contained tags.
538
	 *
539
	 * @return string
540
	 */
541
	public function extraClass() {
542
		$classes = array();
543
544
		$classes[] = $this->Type();
545
546
		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...
547
			$classes = array_merge(
548
				$classes,
549
				array_values($this->extraClasses)
550
			);
551
		}
552
553
		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...
554
			$classes[] = 'nolabel';
555
		}
556
557
		// Allow custom styling of any element in the container based on validation errors,
558
		// e.g. red borders on input tags.
559
		//
560
		// CSS class needs to be different from the one rendered through {@link FieldHolder()}.
561
		if($this->Message()) {
562
			$classes[] .= 'holder-' . $this->MessageType();
563
		}
564
565
		return implode(' ', $classes);
566
	}
567
568
	/**
569
	 * Add one or more CSS-classes to the FormField container.
570
	 *
571
	 * Multiple class names should be space delimited.
572
	 *
573
	 * @param string $class
574
	 *
575
	 * @return $this
576
	 */
577
	public function addExtraClass($class) {
578
		$classes = preg_split('/\s+/', $class);
579
580
		foreach ($classes as $class) {
581
			$this->extraClasses[$class] = $class;
582
		}
583
584
		return $this;
585
	}
586
587
	/**
588
	 * Remove one or more CSS-classes from the FormField container.
589
	 *
590
	 * @param string $class
591
	 *
592
	 * @return $this
593
	 */
594
	public function removeExtraClass($class) {
595
		$classes = preg_split('/\s+/', $class);
596
597
		foreach ($classes as $class) {
598
			unset($this->extraClasses[$class]);
599
		}
600
601
		return $this;
602
	}
603
604
	/**
605
	 * Set an HTML attribute on the field element, mostly an <input> tag.
606
	 *
607
	 * Some attributes are best set through more specialized methods, to avoid interfering with
608
	 * built-in behaviour:
609
	 *
610
	 * - 'class': {@link addExtraClass()}
611
	 * - 'title': {@link setDescription()}
612
	 * - 'value': {@link setValue}
613
	 * - 'name': {@link setName}
614
	 *
615
	 * Caution: this doesn't work on most fields which are composed of more than one HTML form
616
	 * field.
617
	 *
618
	 * @param string $name
619
	 * @param string $value
620
	 *
621
	 * @return $this
622
	 */
623
	public function setAttribute($name, $value) {
624
		$this->attributes[$name] = $value;
625
626
		return $this;
627
	}
628
629
	/**
630
	 * Get an HTML attribute defined by the field, or added through {@link setAttribute()}.
631
	 *
632
	 * Caution: this doesn't work on all fields, see {@link setAttribute()}.
633
	 *
634
	 * @return null|string
635
	 */
636
	public function getAttribute($name) {
637
		$attributes = $this->getAttributes();
638
639
		if(isset($attributes[$name])) {
640
			return $attributes[$name];
641
		}
642
643
		return null;
644
	}
645
646
	/**
647
	 * Allows customization through an 'updateAttributes' hook on the base class.
648
	 * Existing attributes are passed in as the first argument and can be manipulated,
649
	 * but any attributes added through a subclass implementation won't be included.
650
	 *
651
	 * @return array
652
	 */
653
	public function getAttributes() {
654
		$attributes = array(
655
			'type' => 'text',
656
			'name' => $this->getName(),
657
			'value' => $this->Value(),
658
			'class' => $this->extraClass(),
659
			'id' => $this->ID(),
660
			'disabled' => $this->isDisabled(),
661
			'readonly' => $this->isReadonly()
662
		);
663
664
		if($this->Required()) {
665
			$attributes['required'] = 'required';
666
			$attributes['aria-required'] = 'true';
667
		}
668
669
		$attributes = array_merge($attributes, $this->attributes);
670
671
		$this->extend('updateAttributes', $attributes);
672
673
		return $attributes;
674
	}
675
676
	/**
677
	 * Custom attributes to process. Falls back to {@link getAttributes()}.
678
	 *
679
	 * If at least one argument is passed as a string, all arguments act as excludes, by name.
680
	 *
681
	 * @param array $attributes
682
	 *
683
	 * @return string
684
	 */
685
	public function getAttributesHTML($attributes = null) {
686
		$exclude = null;
687
688
		if(is_string($attributes)) {
689
			$exclude = func_get_args();
690
		}
691
692
		if(!$attributes || is_string($attributes)) {
693
			$attributes = $this->getAttributes();
694
		}
695
696
		$attributes = (array) $attributes;
697
698
		$attributes = array_filter($attributes, function ($v) {
699
			return ($v || $v === 0 || $v === '0');
700
		});
701
702
		if($exclude) {
703
			$attributes = array_diff_key(
704
				$attributes,
705
				array_flip($exclude)
706
			);
707
		}
708
709
		// Create markup
710
		$parts = array();
711
712
		foreach($attributes as $name => $value) {
713
			if($value === true) {
714
				$parts[] = sprintf('%s="%s"', $name, $name);
715
			} else {
716
				$parts[] = sprintf('%s="%s"', $name, Convert::raw2att($value));
717
			}
718
		}
719
720
		return implode(' ', $parts);
721
	}
722
723
	/**
724
	 * Returns a version of a title suitable for insertion into an HTML attribute.
725
	 *
726
	 * @return string
727
	 */
728
	public function attrTitle() {
729
		return Convert::raw2att($this->title);
730
	}
731
732
	/**
733
	 * Returns a version of a title suitable for insertion into an HTML attribute.
734
	 *
735
	 * @return string
736
	 */
737
	public function attrValue() {
738
		return Convert::raw2att($this->value);
739
	}
740
741
	/**
742
	 * Set the field value.
743
	 *
744
	 * @param mixed $value
745
	 * @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...
746
	 *
747
	 * @return $this
748
	 */
749
	public function setValue($value) {
750
		$this->value = $value;
751
		return $this;
752
	}
753
754
	/**
755
	 * Set the field name.
756
	 *
757
	 * @param string $name
758
	 *
759
	 * @return $this
760
	 */
761
	public function setName($name) {
762
		$this->name = $name;
763
764
		return $this;
765
	}
766
767
	/**
768
	 * Set the container form.
769
	 *
770
	 * This is called automatically when fields are added to forms.
771
	 *
772
	 * @param Form $form
773
	 *
774
	 * @return $this
775
	 */
776
	public function setForm($form) {
777
		$this->form = $form;
778
779
		return $this;
780
	}
781
782
	/**
783
	 * Get the currently used form.
784
	 *
785
	 * @return Form
786
	 */
787
	public function getForm() {
788
		return $this->form;
789
	}
790
791
	/**
792
	 * Return true if security token protection is enabled on the parent {@link Form}.
793
	 *
794
	 * @return bool
795
	 */
796
	public function securityTokenEnabled() {
797
		$form = $this->getForm();
798
799
		if(!$form) {
800
			return false;
801
		}
802
803
		return $form->getSecurityToken()->isEnabled();
804
	}
805
806
	/**
807
	 * Sets the error message to be displayed on the form field.
808
	 *
809
	 * Allows HTML content, so remember to use Convert::raw2xml().
810
	 *
811
	 * @param string $message
812
	 * @param string $messageType
813
	 *
814
	 * @return $this
815
	 */
816
	public function setError($message, $messageType) {
817
		$this->message = $message;
818
		$this->messageType = $messageType;
819
820
		return $this;
821
	}
822
823
	/**
824
	 * Set the custom error message to show instead of the default format.
825
	 *
826
	 * Different from setError() as that appends it to the standard error messaging.
827
	 *
828
	 * @param string $customValidationMessage
829
	 *
830
	 * @return $this
831
	 */
832
	public function setCustomValidationMessage($customValidationMessage) {
833
		$this->customValidationMessage = $customValidationMessage;
834
835
		return $this;
836
	}
837
838
	/**
839
	 * Get the custom error message for this form field. If a custom message has not been defined
840
	 * then just return blank. The default error is defined on {@link Validator}.
841
	 *
842
	 * @return string
843
	 */
844
	public function getCustomValidationMessage() {
845
		return $this->customValidationMessage;
846
	}
847
848
	/**
849
	 * Set name of template (without path or extension).
850
	 *
851
	 * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
852
	 * method on the subclass for support.
853
	 *
854
	 * @param string $template
855
	 *
856
	 * @return $this
857
	 */
858
	public function setTemplate($template) {
859
		$this->template = $template;
860
861
		return $this;
862
	}
863
864
	/**
865
	 * @return string
866
	 */
867
	public function getTemplate() {
868
		return $this->template;
869
	}
870
871
	/**
872
	 * @return string
873
	 */
874
	public function getFieldHolderTemplate() {
875
		return $this->fieldHolderTemplate;
876
	}
877
878
	/**
879
	 * Set name of template (without path or extension) for the holder, which in turn is
880
	 * responsible for rendering {@link Field()}.
881
	 *
882
	 * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
883
	 * method on the subclass for support.
884
	 *
885
	 * @param string $fieldHolderTemplate
886
	 *
887
	 * @return $this
888
	 */
889
	public function setFieldHolderTemplate($fieldHolderTemplate) {
890
		$this->fieldHolderTemplate = $fieldHolderTemplate;
891
892
		return $this;
893
	}
894
895
	/**
896
	 * @return string
897
	 */
898
	public function getSmallFieldHolderTemplate() {
899
		return $this->smallFieldHolderTemplate;
900
	}
901
902
	/**
903
	 * Set name of template (without path or extension) for the small holder, which in turn is
904
	 * responsible for rendering {@link Field()}.
905
	 *
906
	 * Caution: Not consistently implemented in all subclasses, please check the {@link Field()}
907
	 * method on the subclass for support.
908
	 *
909
	 * @param string $smallFieldHolderTemplate
910
	 *
911
	 * @return $this
912
	 */
913
	public function setSmallFieldHolderTemplate($smallFieldHolderTemplate) {
914
		$this->smallFieldHolderTemplate = $smallFieldHolderTemplate;
915
916
		return $this;
917
	}
918
919
	/**
920
	 * Returns the form field.
921
	 *
922
	 * Although FieldHolder is generally what is inserted into templates, all of the field holder
923
	 * templates make use of $Field. It's expected that FieldHolder will give you the "complete"
924
	 * representation of the field on the form, whereas Field will give you the core editing widget,
925
	 * such as an input tag.
926
	 *
927
	 * @param array $properties
928
	 *
929
	 * @return string
930
	 */
931
	public function Field($properties = array()) {
932
		$context = $this;
933
934
		if(count($properties)) {
935
			$context = $context->customise($properties);
936
		}
937
938
		$this->extend('onBeforeRender', $this);
939
940
		$result = $context->renderWith($this->getTemplates());
941
942
		// Trim whitespace from the result, so that trailing newlines are supressed. Works for strings and HTMLText values
943
		if(is_string($result)) {
944
			$result = trim($result);
945
		} else if($result instanceof DBField) {
0 ignored issues
show
Bug introduced by
The class DBField does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
946
			$result->setValue(trim($result->getValue()));
947
		}
948
949
		return $result;
950
	}
951
952
	/**
953
	 * Returns a "field holder" for this field.
954
	 *
955
	 * Forms are constructed by concatenating a number of these field holders.
956
	 *
957
	 * The default field holder is a label and a form field inside a div.
958
	 *
959
	 * @see FieldHolder.ss
960
	 *
961
	 * @param array $properties
962
	 *
963
	 * @return string
964
	 */
965
	public function FieldHolder($properties = array()) {
966
		$context = $this;
967
968
		if(count($properties)) {
969
			$context = $this->customise($properties);
970
		}
971
972
		return $context->renderWith($this->getFieldHolderTemplates());
973
	}
974
975
	/**
976
	 * Returns a restricted field holder used within things like FieldGroups.
977
	 *
978
	 * @param array $properties
979
	 *
980
	 * @return string
981
	 */
982
	public function SmallFieldHolder($properties = array()) {
983
		$context = $this;
984
985
		if(count($properties)) {
986
			$context = $this->customise($properties);
987
		}
988
989
		return $context->renderWith($this->getSmallFieldHolderTemplates());
990
	}
991
992
	/**
993
	 * Returns an array of templates to use for rendering {@link FieldHolder}.
994
	 *
995
	 * @return array
996
	 */
997
	public function getTemplates() {
998
		return $this->_templates($this->getTemplate());
999
	}
1000
1001
	/**
1002
	 * Returns an array of templates to use for rendering {@link FieldHolder}.
1003
	 *
1004
	 * @return array
1005
	 */
1006
	public function getFieldHolderTemplates() {
1007
		return $this->_templates(
1008
			$this->getFieldHolderTemplate(),
1009
			'_holder'
1010
		);
1011
	}
1012
1013
	/**
1014
	 * Returns an array of templates to use for rendering {@link SmallFieldHolder}.
1015
	 *
1016
	 * @return array
1017
	 */
1018
	public function getSmallFieldHolderTemplates() {
1019
		return $this->_templates(
1020
			$this->getSmallFieldHolderTemplate(),
1021
			'_holder_small'
1022
		);
1023
	}
1024
1025
1026
	/**
1027
	 * Generate an array of class name strings to use for rendering this form field into HTML.
1028
	 *
1029
	 * @param string $customTemplate
1030
	 * @param string $customTemplateSuffix
1031
	 *
1032
	 * @return array
1033
	 */
1034
	private function _templates($customTemplate = null, $customTemplateSuffix = null) {
1035
		$matches = array();
1036
1037
		foreach(array_reverse(ClassInfo::ancestry($this)) as $className) {
1038
			$matches[] = $className . $customTemplateSuffix;
1039
1040
			if($className == "FormField") {
1041
				break;
1042
			}
1043
		}
1044
1045
		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...
1046
			array_unshift($matches, $customTemplate);
1047
		}
1048
1049
		return $matches;
1050
	}
1051
1052
	/**
1053
	 * Returns true if this field is a composite field.
1054
	 *
1055
	 * To create composite field types, you should subclass {@link CompositeField}.
1056
	 *
1057
	 * @return bool
1058
	 */
1059
	public function isComposite() {
1060
		return false;
1061
	}
1062
1063
	/**
1064
	 * Returns true if this field has its own data.
1065
	 *
1066
	 * Some fields, such as titles and composite fields, don't actually have any data. It doesn't
1067
	 * make sense for data-focused methods to look at them. By overloading hasData() to return
1068
	 * false, you can prevent any data-focused methods from looking at it.
1069
	 *
1070
	 * @see FieldList::collateDataFields()
1071
	 *
1072
	 * @return bool
1073
	 */
1074
	public function hasData() {
1075
		return true;
1076
	}
1077
1078
	/**
1079
	 * @return bool
1080
	 */
1081
	public function isReadonly() {
1082
		return $this->readonly;
1083
	}
1084
1085
	/**
1086
	 * Sets a read-only flag on this FormField.
1087
	 *
1088
	 * Use performReadonlyTransformation() to transform this instance.
1089
	 *
1090
	 * Setting this to false has no effect on the field.
1091
	 *
1092
	 * @param bool $readonly
1093
	 *
1094
	 * @return $this
1095
	 */
1096
	public function setReadonly($readonly) {
1097
		$this->readonly = $readonly;
1098
1099
		return $this;
1100
	}
1101
1102
	/**
1103
	 * @return bool
1104
	 */
1105
	public function isDisabled() {
1106
		return $this->disabled;
1107
	}
1108
1109
	/**
1110
	 * Sets a disabled flag on this FormField.
1111
	 *
1112
	 * Use performDisabledTransformation() to transform this instance.
1113
	 *
1114
	 * Setting this to false has no effect on the field.
1115
	 *
1116
	 * @param bool $disabled
1117
	 *
1118
	 * @return $this
1119
	 */
1120
	public function setDisabled($disabled) {
1121
		$this->disabled = $disabled;
1122
1123
		return $this;
1124
	}
1125
1126
	/**
1127
	 * Returns a read-only version of this field.
1128
	 *
1129
	 * @return FormField
1130
	 */
1131
	public function performReadonlyTransformation() {
1132
		$readonlyClassName = $this->class . '_Readonly';
1133
1134
		if(ClassInfo::exists($readonlyClassName)) {
1135
			$clone = $this->castedCopy($readonlyClassName);
1136
		} else {
1137
			$clone = $this->castedCopy('ReadonlyField');
1138
		}
1139
1140
		$clone->setReadonly(true);
1141
1142
		return $clone;
1143
	}
1144
1145
	/**
1146
	 * Return a disabled version of this field.
1147
	 *
1148
	 * Tries to find a class of the class name of this field suffixed with "_Disabled", failing
1149
	 * that, finds a method {@link setDisabled()}.
1150
	 *
1151
	 * @return FormField
1152
	 */
1153
	public function performDisabledTransformation() {
1154
		$disabledClassName = $this->class . '_Disabled';
1155
1156
		if(ClassInfo::exists($disabledClassName)) {
1157
			$clone = $this->castedCopy($disabledClassName);
1158
		} else {
1159
			$clone = clone $this;
1160
		}
1161
1162
		$clone->setDisabled(true);
1163
1164
		return $clone;
1165
	}
1166
1167
	/**
1168
	 * @param FormTransformation $transformation
1169
	 *
1170
	 * @return mixed
1171
	 */
1172
	public function transform(FormTransformation $transformation) {
1173
		return $transformation->transform($this);
1174
	}
1175
1176
	/**
1177
	 * @param string $class
1178
	 *
1179
	 * @return int
1180
	 */
1181
	public function hasClass($class) {
1182
		$patten = '/' . strtolower($class) . '/i';
1183
1184
		$subject = strtolower($this->class . ' ' . $this->extraClass());
1185
1186
		return preg_match($patten, $subject);
1187
	}
1188
1189
	/**
1190
	 * Returns the field type.
1191
	 *
1192
	 * The field type is the class name with the word Field dropped off the end, all lowercase.
1193
	 *
1194
	 * It's handy for assigning HTML classes. Doesn't signify the <input type> attribute.
1195
	 *
1196
	 * @see {link getAttributes()}.
1197
	 *
1198
	 * @return string
1199
	 */
1200
	public function Type() {
1201
		return strtolower(preg_replace('/Field$/', '', $this->class));
1202
	}
1203
1204
	/**
1205
	 * @deprecated 4.0 Use FormField::create_tag()
1206
	 *
1207
	 * @param string $tag
1208
	 * @param array $attributes
1209
	 * @param null|string $content
1210
	 *
1211
	 * @return string
1212
	 */
1213
	public function createTag($tag, $attributes, $content = null) {
1214
		Deprecation::notice('4.0', 'Use FormField::create_tag()');
1215
1216
		return self::create_tag($tag, $attributes, $content);
1217
	}
1218
1219
	/**
1220
	 * Abstract method each {@link FormField} subclass must implement, determines whether the field
1221
	 * is valid or not based on the value.
1222
	 *
1223
	 * @todo Make this abstract.
1224
	 *
1225
	 * @param Validator
1226
	 *
1227
	 * @return bool
1228
	 */
1229
	public function validate($validator) {
1230
		return true;
1231
	}
1232
1233
	/**
1234
	 * Describe this field, provide help text for it.
1235
	 *
1236
	 * By default, renders as a <span class="description"> underneath the form field.
1237
	 *
1238
	 * @param string $description
1239
	 *
1240
	 * @return $this
1241
	 */
1242
	public function setDescription($description) {
1243
		$this->description = $description;
1244
1245
		return $this;
1246
	}
1247
1248
	/**
1249
	 * @return string
1250
	 */
1251
	public function getDescription() {
1252
		return $this->description;
1253
	}
1254
1255
	/**
1256
	 * @return string
1257
	 */
1258
	public function debug() {
1259
		return sprintf(
1260
			'%s (%s: %s : <span style="color:red;">%s</span>) = %s',
1261
			$this->class,
1262
			$this->name,
1263
			$this->title,
1264
			$this->message,
1265
			$this->value
1266
		);
1267
	}
1268
1269
	/**
1270
	 * This function is used by the template processor. If you refer to a field as a $ variable, it
1271
	 * will return the $Field value.
1272
	 *
1273
	 * @return string
1274
	 */
1275
	public function forTemplate() {
1276
		return $this->Field();
1277
	}
1278
1279
	/**
1280
	 * @return bool
1281
	 */
1282
	public function Required() {
1283
		if($this->form && ($validator = $this->form->Validator)) {
0 ignored issues
show
Bug introduced by
The property Validator does not seem to exist. Did you mean validator?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1284
			return $validator->fieldIsRequired($this->name);
1285
		}
1286
1287
		return false;
1288
	}
1289
1290
	/**
1291
	 * Set the FieldList that contains this field.
1292
	 *
1293
	 * @param FieldList $containerFieldList
1294
	 *
1295
	 * @return FieldList
1296
	 */
1297
	public function setContainerFieldList($containerFieldList) {
1298
		$this->containerFieldList = $containerFieldList;
1299
1300
		return $this;
1301
	}
1302
1303
	/**
1304
	 * Get the FieldList that contains this field.
1305
	 *
1306
	 * @return FieldList
1307
	 */
1308
	public function getContainerFieldList() {
1309
		return $this->containerFieldList;
1310
	}
1311
1312
	/**
1313
	 * @return null|FieldList
1314
	 */
1315
	public function rootFieldList() {
1316
		if(is_object($this->containerFieldList)) {
1317
			return $this->containerFieldList->rootFieldList();
1318
		}
1319
1320
		user_error(
1321
			"rootFieldList() called on $this->class object without a containerFieldList",
1322
			E_USER_ERROR
1323
		);
1324
1325
		return null;
1326
	}
1327
1328
	/**
1329
	 * Returns another instance of this field, but "cast" to a different class. The logic tries to
1330
	 * retain all of the instance properties, and may be overloaded by subclasses to set additional
1331
	 * ones.
1332
	 *
1333
	 * Assumes the standard FormField parameter signature with its name as the only mandatory
1334
	 * argument. Mainly geared towards creating *_Readonly or *_Disabled subclasses of the same
1335
	 * type, or casting to a {@link ReadonlyField}.
1336
	 *
1337
	 * Does not copy custom field templates, since they probably won't apply to the new instance.
1338
	 *
1339
	 * @param mixed $classOrCopy Class name for copy, or existing copy instance to update
1340
	 *
1341
	 * @return FormField
1342
	 */
1343
	public function castedCopy($classOrCopy) {
1344
		$field = $classOrCopy;
1345
1346
		if(!is_object($field)) {
1347
			$field = new $classOrCopy($this->name);
1348
		}
1349
1350
		$field
1351
			->setValue($this->value)
1352
			->setForm($this->form)
1353
			->setTitle($this->Title())
1354
			->setLeftTitle($this->LeftTitle())
1355
			->setRightTitle($this->RightTitle())
1356
			->addExtraClass($this->extraClass())
1357
			->setDescription($this->getDescription());
1358
1359
		// Only include built-in attributes, ignore anything set through getAttributes().
1360
		// Those might change important characteristics of the field, e.g. its "type" attribute.
1361
		foreach($this->attributes as $attributeKey => $attributeValue) {
1362
			$field->setAttribute($attributeKey, $attributeValue);
1363
		}
1364
1365
		$field->dontEscape = $this->dontEscape;
1366
1367
		return $field;
1368
	}
1369
1370
	/**
1371
	 * Determine if escaping of this field should be disabled
1372
	 *
1373
	 * @param bool $dontEscape
1374
	 * @return $this
1375
	 */
1376
	public function setDontEscape($dontEscape) {
1377
		$this->dontEscape = $dontEscape;
1378
		return $this;
1379
	}
1380
1381
	/**
1382
	 * Determine if escaping is disabled
1383
	 *
1384
	 * @return bool
1385
	 */
1386
	public function getDontEscape() {
1387
		return $this->dontEscape;
1388
	}
1389
1390
	/**
1391
	 * Sets the component type the FormField will be rendered as on the front-end.
1392
	 *
1393
	 * @param string $componentType
1394
	 * @return FormField
1395
	 */
1396
	public function setSchemaComponent($componentType) {
1397
		$this->schemaComponent = $componentType;
1398
		return $this;
1399
	}
1400
1401
	/**
1402
	 * Gets the type of front-end component the FormField will be rendered as.
1403
	 *
1404
	 * @return string
1405
	 */
1406
	public function getSchemaComponent() {
1407
		return $this->schemaComponent;
1408
	}
1409
1410
	/**
1411
	 * Sets the schema data used for rendering the field on the front-end.
1412
	 * Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
1413
	 * Any passed keys that are not defined in {@link getSchemaDataDefaults()} are ignored.
1414
	 * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1415
	 *
1416
	 * @param array $schemaData - The data to be merged with $this->schemaData.
1417
	 * @return FormField
1418
	 *
1419
	 * @todo Add deep merging of arrays like `data` and `attributes`.
1420
	 */
1421
	public function setSchemaData($schemaData = []) {
1422
		$current = $this->getSchemaData();
1423
1424
		$this->schemaData = array_merge($current, array_intersect_key($schemaData, $current));
1425
		return $this;
1426
	}
1427
1428
	/**
1429
	 * Gets the schema data used to render the FormField on the front-end.
1430
	 *
1431
	 * @return array
1432
	 */
1433
	public function getSchemaData() {
1434
		return array_merge($this->getSchemaDataDefaults(), $this->schemaData);
1435
	}
1436
1437
	/**
1438
	 * @todo Throw exception if value is missing, once a form field schema is mandatory across the CMS
1439
	 *
1440
	 * @return string
1441
	 */
1442
	public function getSchemaDataType() {
1443
		return $this->schemaDataType;
1444
	}
1445
1446
	/**
1447
	 * Gets the defaults for $schemaData.
1448
	 * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaData()} are ignored.
1449
	 * Instead the `data` array should be used to pass around ad hoc data.
1450
	 *
1451
	 * @return array
1452
	 */
1453
	public function getSchemaDataDefaults() {
1454
		return [
1455
			'name' => $this->getName(),
1456
			'id' => $this->ID(),
1457
			'type' => $this->getSchemaDataType(),
1458
			'component' => $this->getSchemaComponent(),
1459
			'holder_id' => null,
1460
			'title' => $this->Title(),
1461
			'source' => null,
1462
			'extraClass' => $this->ExtraClass(),
1463
			'description' => $this->getDescription(),
1464
			'rightTitle' => $this->RightTitle(),
1465
			'leftTitle' => $this->LeftTitle(),
1466
			'readOnly' => $this->isReadOnly(),
1467
			'disabled' => $this->isDisabled(),
1468
			'customValidationMessage' => $this->getCustomValidationMessage(),
1469
			'attributes' => [],
1470
			'data' => [],
1471
		];
1472
	}
1473
1474
	/**
1475
	 * Sets the schema data used for rendering the field on the front-end.
1476
	 * Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
1477
	 * Any passed keys that are not defined in {@link getSchemaDataDefaults()} are ignored.
1478
	 * If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
1479
	 *
1480
	 * @param array $schemaData - The data to be merged with $this->schemaData.
0 ignored issues
show
Bug introduced by
There is no parameter named $schemaData. 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...
1481
	 * @return FormField
1482
	 *
1483
	 * @todo Add deep merging of arrays like `data` and `attributes`.
1484
	 */
1485
	public function setSchemaState($schemaState = []) {
1486
		$current = $this->getSchemaState();
1487
1488
		$this->schemaState = array_merge($current, array_intersect_key($schemaState, $current));
1489
		return $this;
1490
	}
1491
1492
	/**
1493
	 * Gets the schema state used to render the FormField on the front-end.
1494
	 *
1495
	 * @return array
1496
	 */
1497
	public function getSchemaState() {
1498
		return array_merge($this->getSchemaStateDefaults(), $this->schemaState);
1499
	}
1500
1501
	/**
1502
	 * Gets the defaults for $schemaState.
1503
	 * The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaState()} are ignored.
1504
	 * Instead the `data` array should be used to pass around ad hoc data.
1505
	 * Includes validation data if the field is associated to a {@link Form},
1506
	 * and {@link Form->validate()} has been called.
1507
	 *
1508
	 * @return array
1509
	 */
1510
	public function getSchemaStateDefaults() {
1511
		$field = $this;
1512
		$form = $this->getForm();
1513
		$validator = $form ? $form->getValidator() : null;
1514
		$errors = $validator ? (array)$validator->getErrors() : [];
1515
		$messages = array_filter(array_map(function($error) use ($field) {
1516
			if($error['fieldName'] === $field->getName()) {
1517
				return [
1518
					'value' => $error['message'],
1519
					'type' => $error['messageType']
1520
				];
1521
			}
1522
		}, $errors));
1523
1524
		return [
1525
			'id' => $this->ID(),
1526
			'value' => $this->Value(),
1527
			'valid' => (count($messages) === 0),
1528
			'messages' => (array)$messages,
1529
			'data' => [],
1530
		];
1531
	}
1532
1533
}
1534