Passed
Push — develop ( 5f0710...340c0b )
by Neill
12:23 queued 14s
created

Form::setFields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
ccs 6
cts 6
cp 1
crap 3
1
<?php
2
/**
3
 * @link http://www.newicon.net/neon
4
 * @copyright Copyright (c) 2016-18 Newicon Ltd
5
 * @license http://www.newicon.net/neon/license
6
 */
7
namespace neon\core\form;
8
9
use neon\core\form\fields\Field;
10
use neon\core\grid\query\IQuery;
11
use neon\core\helpers\Collection;
12
use neon\core\helpers\Hash;
13
use neon\core\traits\PropertiesTrait;
14
use neon\core\form\traits\BuilderTrait;
15
use ReflectionClass;
16
use yii\base\InvalidCallException;
17
use yii\base\UnknownPropertyException;
18
use neon\core\helpers\Arr;
19
use neon\core\helpers\Html;
20
use neon\core\form\assets\FormAsset;
21
use neon\core\form\interfaces\IField;
22
23
class Form extends FormField implements IField, \ArrayAccess
24
{
25
	use PropertiesTrait;
26
	use BuilderTrait;
27
28
	/**
29
	 * The form submission method, such as "post", "get", "put", "delete" (case-insensitive).
30
	 * Since most browsers only support "post" and "get", if other methods are given, they will
31
	 * be simulated using "post", and a hidden input will be added which contains the actual method type.
32
	 * @see \yii\web\Request::$methodParam for more details.
33
	 * @var string
34
	 */
35
	public $method = 'post';
36
	/**
37
	 * @var bool
38
	 */
39
	public $readOnly = false;
40
	/**
41
	 * @var bool
42
	 */
43
	public $printOnly = false;
44
	/**
45
	 * @var boolean whether to enable client-side data validation.
46
	 */
47
	public $enableClientValidation = true;
48
	/**
49
	 * @var boolean whether to enable AJAX-based data validation.
50
	 */
51
	public $enableAjaxValidation = true;
52
	/**
53
	 * @var boolean whether to enable AJAX-based form submission.
54
	 */
55
	public $enableAjaxSubmission = false;
56
	/**
57
	 * @var array|string the URL for performing AJAX-based validation. This property will be processed by
58
	 * [[Url::to()]]. Please refer to [[Url::to()]] for more details on how to configure this property.
59
	 * If this property is not set, it will take the value of the form's action attribute.
60
	 */
61
	public $validationUrl;
62
	/**
63
	 * @var string the name of the GET parameter indicating the validation request is an AJAX request.
64
	 */
65
	public $ajaxParam = 'ajax';
66
	/**
67
	 * @var string the type of data that you're expecting back from the server.
68
	 */
69
	public $ajaxDataType = 'json';
70
	/**
71
	 * @var boolean whether to scroll to the first error after validation.
72
	 * @since 2.0.6
73
	 */
74
	public $scrollToError = true;
75
	/**
76
	 * @var bool
77
	 */
78
	public $forceUniqueFieldNames = true;
79
	/**
80
	 * @var string  the name of the button used to submit the form
81
	 */
82
	public $submittedButton = null;
83
84
	/**
85
	 * @var bool Whether the form has been submitted - alters the js rendering as to whether errors should display
86
	 */
87
	public $isSubmitted = false;
88
89
	/**
90
	 * An array of form html attributes to be applied to the form tag
91
	 * @see \neon\core\helpers\Html::renderTagAttributes()
92
	 * @var array
93
	 */
94
	protected $_attributes = [];
95
96
	/**
97
	 * Sets html attributes where the key is the attribute name and the value is the value
98
	 * Will merge certain properties like 'class' use replace if you do not want the merge behaviour
99
	 * @param $attrs
100
	 * @param bool $replace - will replace any existing attributes
101
	 * @return $this - chainable interface
102
	 */
103 4
	public function setAttributes($attrs, $replace=false)
104
	{
105 4
		if ($replace) {
106
			$this->_attributes = $attrs;
107
		} else {
108 4
			if (isset($attrs['class'])) {
109
				$this->_attributes['class'] = ' ' . $attrs['class'];
110
				unset($attrs['class']);
111
			}
112 4
			$this->_attributes = array_merge($this->_attributes, $attrs);
113
		}
114 4
		return $this;
115
	}
116
117
	/**
118
	 * Returns all currently defined html attributes
119
	 * @return array
120
	 */
121 12
	public function getAttributes()
122
	{
123 12
		return $this->_attributes;
124
	}
125
126
	/**
127
	 * @inheritDoc
128
	 */
129
	public static $autoIdPrefix = 'neonForm';
130
131
	/**
132
	 * If this form is a sub form and repeater is true
133
	 * then this is a repeatable form instance
134
	 * @var bool
135
	 */
136
	public $repeater = false;
137
138
	// protected props configurable via getters and setter
139
	protected $_template = 'form.tpl';
140
141
	protected $_hint;
142
	protected $_label;
143
	protected $_classLabel;
144
	protected $_dataKey = null;
145
146
	/**
147
	 * Constructor.
148
	 * @inheritdoc
149
	 * @param array|string $config  - name value pairs that will be used to initialize the object properties or a string name
150
	 * if a string is provided then it is used as the name property
151
	 * @param array $config additional configuration
152
	 */
153 124
	public function __construct($name=[], array $config=[])
154
	{
155
		// The $name field can also be a configuration array
156 124
		if (is_array($name)) {
157 56
			$config = $name;
158
		}
159
		// if a name has been specified then add this to the config
160 70
		elseif (is_string($name)) {
161 70
			$config['name'] = $name;
162
		}
163
		// call the parent object construct function - this will configure the form object with the config properties
164 124
		parent::__construct($config);
165 114
	}
166
167
	/**
168
	 * initialise the form
169
	 * @return void
170
	 */
171 114
	public function init()
172
	{
173 114
	}
174
175
	/**
176
	 * @return string
177
	 */
178
	public function formName()
179
	{
180
		return $this->getInputName();
181
	}
182
183
	/**
184
	 * Set the form action url
185
	 * @param array|string $action - suitable format for Url::to
186
	 */
187 4
	public function setAction($action)
188
	{
189 4
		$this->_attributes['action'] = $action;
190 4
	}
191
192
	/**
193
	 * Return the action url - should be in format suitable for Url::to
194
	 * @return array|string
195
	 */
196 12
	public function getAction()
197
	{
198 12
		return isset($this->_attributes['action']) ? $this->_attributes['action'] : '';
199
	}
200
201
	/**
202
	 * @inheritdoc
203
	 */
204
	public function registerScripts($view=null, $mount=true)
205
	{
206
		$view = ($view === null) ? $this->getView() : $view;
207
		FormAsset::register($view);
208
		foreach ($this->getFields() as $field)
209
			$field->registerScripts($view);
210
		if ($this->isRootForm()) {
211
			if ($mount)
212
				$view->registerJs('(new Vue()).$mount("#' . $this->id . '");', $view::POS_END, $this->id . 'vue');
213
		}
214
	}
215
216
	/**
217
	 * run the widget - essentially render the form
218
	 * This follows the standard widget convention of calling run.
219
	 * Render can not be used as this is reserved in Yii widgets for rendering view template files
220
	 * @return string
221
	 */
222
	public function run()
223
	{
224
		$this->registerScripts($this->getView());
225
		return $this->renderFormHtml();
226
	}
227
228
	/**
229
	 * Render the forms html output
230
	 * @return string
231
	 */
232
	public function renderFormHtml()
233
	{
234
		$data = ['form' => $this, 'token' => $this->getObjectToken()];
235
		if ($this->isSubForm()) {
236
			$tpl = $this->repeater ? 'repeater-instance.tpl' : $this->_template;
237
			$data['parentFormId'] = $this->getForm()->getId();
238
			return $this->render($tpl, $data);
239
		}
240
		return $this->render($this->_template, $data);
241
	}
242
243
// region: Validation and error specific functions
244
// ============================================================================
245
246
	/**
247
	 * Performs the data validation.
248
	 *
249
	 * @See \yii\base\Model
250
	 *
251
	 * @param array   $names list of attribute names that should be validated.
252
	 * If this parameter is empty, it means any attribute listed in the applicable
253
	 * validation rules should be validated.
254
	 * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation
255
	 *
256
	 * @return boolean whether the validation is successful without any error.
257
	 * @throws UnknownPropertyException - if a validator attempts to access an unknown property
258
	 */
259 4
	public function validate($names = null, $clearErrors = true)
260
	{
261 4
		$valid = true;
262 4
		foreach($this->getFields($names) as $key => $field) {
263 4
			$valid = $field->validate() && $valid;
264
		}
265
		// if validation has run - you will want to see it when the form is loaded
266 4
		$this->isSubmitted = true;
267 4
		return $valid;
268
	}
269
270
	/**
271
	 * Validates one or several models and returns an error message array indexed by the attribute IDs.
272
	 * This is a helper method that simplifies the way of writing AJAX validation code.
273
	 * For example, you may use the following code in a controller action to respond
274
	 * to an AJAX validation request:
275
	 * ```php
276
	 * $model = new Post;
277
	 * $model->load($_POST);
278
	 * if (Yii::$app->request->isAjax) {
279
	 *     Yii::$app->response->format = Response::FORMAT_JSON;
280
	 *     return ActiveForm::validate($model);
281
	 * }
282
	 * // ... respond to non-AJAX request ...
283
	 * ```
284
	 * To validate multiple models, simply pass each model as a parameter to this method, like
285
	 * the following:
286
	 * ```php
287
	 * ActiveForm::validate($model1, $model2, ...);
288
	 * ```
289
	 * If this parameter is empty, it means any attribute listed in the applicable
290
	 * validation rules should be validated.
291
	 * When this method is used to validate multiple models, this parameter will be interpreted
292
	 * as a model.
293
	 * @throws UnknownPropertyException - if a validator attempts to access an unknown property
294
	 * @return array the error message array indexed by the attribute IDs.
295
	 */
296
	public function getValidationData()
297
	{
298
		$this->validate();
299
		$result = $this->_getFormErrors($this);
300
		return $result;
301
	}
302
303
	/**
304
	 * Get all form errors indexed by field id
305
	 *
306
	 * @param \neon\core\form\Form $form - A form object
307
	 * @param array $errors
308
	 * @return array
309
	 */
310
	protected function _getFormErrors($form, &$errors=[])
311
	{
312
		foreach ($form->getFields() as $field) {
313
			if ($field->isForm()) {
314
				$this->_getFormErrors($field, $errors);
0 ignored issues
show
Bug introduced by
$field of type neon\core\form\fields\Field is incompatible with the type neon\core\form\Form expected by parameter $form of neon\core\form\Form::_getFormErrors(). ( Ignorable by Annotation )

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

314
				$this->_getFormErrors(/** @scrutinizer ignore-type */ $field, $errors);
Loading history...
315
			} else {
316
				$errors[$field->getIdPath()] = $field->getErrors();
317
			}
318
		}
319
		return $errors;
320
	}
321
322
	/**
323
	 * Store an array of errors for this object
324
	 * @var array
325
	 */
326
	private $_errors = [];
327
328
	/**
329
	 * Get all errors from this form and any fields
330
	 * @return array
331
	 */
332
	public function getErrors()
333
	{
334
		$errors = [];
335
		foreach($this->getFields() as $name => $field) {
336
			if ($field->hasError())
337
				$errors[$name] = $field->getErrors();
338
		}
339
		return array_merge($this->_errors, $errors);
340
	}
341
342
	/**
343
	 * Returns true if this form or any subform has errors
344
	 * @return boolean
345
	 */
346
	public function hasError()
347
	{
348
		return !empty($this->getErrors());
349
	}
350
351
	/**
352
	 * Adds a new error to this form.
353
	 *
354
	 * This method is dual purposed so that errors can be set directly on the form
355
	 * or, if during validation of a field that needed to pass in the form model,
356
	 * we can add the error to that field instead. This is a bit of a hack but
357
	 * required because of how validateAttribute works in Yii Validators.
358
	 *
359
	 * @param string $fieldNameOrError  if $error is not null, then this is
360
	 *   the form field name, otherwise it is the error
361
	 * @param string $error  if this is not null then it is the error
362
	 * @return void
363
	 */
364
	public function addError($fieldNameOrError='', $error=null)
365
	{
366
		if ($error === null)
367
			$this->_errors[] = $fieldNameOrError;
368
		else
369
			$this->getField($fieldNameOrError)->addError($error);
370
	}
371
372
// ============================================================================
373
// endregion
374
375
	/**
376
	 * Gets a field label - this function matches the structure of yii models so the form can
377
	 * be used as the model input for Yii validation and render functions
378
	 * @param string $attribute - the field name
379
	 * @return String
380
	 */
381
	public function getAttributeLabel($attribute)
382
	{
383
		return $this->getField($attribute)->getLabel();
384
	}
385
386
	/**
387
	 * Shortcut function to load in post data and validate the form
388
	 * @return bool
389
	 */
390
	public function validatePost()
391
	{
392
		if ($this->getRequest()->getIsPost()) {
393
			$this->load();
394
			return $this->validate();
395
		}
396
		return false;
397
	}
398
399
	/**
400
	 * @inheritdoc
401
	 */
402
	public function getViewPath()
403
	{
404
		return __DIR__ . DIRECTORY_SEPARATOR . 'views';
405
	}
406
407
	/**
408
	 * Generate a name that is unique in the context of the other fields in the form
409
	 * for e.g. if you have one field with a name of "field" in your form and you call this function with the same name of
410
	 * "field" it will return "field_1" and so on to ensure the name is unique
411
	 * @param string $name the name of the field - it will be appended with a incremented to number to enforce uniqueness
412
	 *
413
	 * @return string the unique name
414
	 */
415
	public function generateUniqueFieldName($name)
416
	{
417
		$i = 1;
418
		while ($this->hasField($name)) {
419
			if (preg_match('/.*(_[0-9]+)/', $name, $matches)) {
420
				$name = str_replace($matches[1], '_' . $i++, $name);
421
			} else {
422
				$name .= '_' . $i++;
423
			}
424
		}
425
		return $name;
426
	}
427
428
// region: Field Management
429
// ============================================================================
430
	/**
431
	 * @var array of IField Objects
432
	 */
433
	protected $_fields = [];
434
435
	/**
436
	 * Get field by its name
437
	 *
438
	 * @param string $name the name of the field
439
	 * @throws InvalidCallException if no field exists with name
440
	 * @return fields\Field|Form
441
	 */
442 58
	public function getField($name)
443
	{
444 58
		if (!isset($this->_fields[$name])) {
445
			throw new InvalidCallException("No field with the name '$name' exists in the form");
446
		}
447 58
		return $this->_fields[$name];
448
	}
449
450
	/**
451
	 * Get a field by its path
452
	 * @param string $path - '.' delimited path to field - the first item should be the current form name or id
453
	 * @return IField|Form|null
454
	 * @throws \Exception if the first part of the path is not the current form
455
	 */
456 2
	public function getFieldByPath($path)
457
	{
458
		// the root path bit can be the the name or the id of the form
459
		// all children of the root will use the name property
460
		// multiple versions of the same form can exist on a page
461
		// in these scenarios the forms each can have different root form ids.
462 2
		$bits = explode('.', $path);
463 2
		if (!($bits[0] === $this->name || ($this->isRootForm() && $bits[0] === $this->id)))
464
			throw new \Exception("The first part of the path should be the form name or the id if this is a root form. The forms name and id is: name={$this->name}, id={$this->id}. The path you used was: path=$path.");
465
		// remove first item and reindex array : note unset does not update array indexes
466 2
		array_splice($bits, 0, 1);
467 2
		$field = $this->getField($bits[0]);
468 2
		if ($field->isForm()) {
469
			// if this is a form then delegate to the sub form to find its children.
470
			// this is important for repeaters that manage a collection of forms with unique ids
471 2
			return $field->getFieldByPath(implode('.', $bits));
0 ignored issues
show
Bug introduced by
The method getFieldByPath() does not exist on neon\core\form\fields\Field. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

471
			return $field->/** @scrutinizer ignore-call */ getFieldByPath(implode('.', $bits));
Loading history...
472
		}
473 2
		return $field;
474
	}
475
476
	/**
477
	 * Returns boolean whether the field exists
478
	 *
479
	 * @param string $name
480
	 * @return bool
481
	 */
482 54
	public function hasField($name)
483
	{
484 54
		return isset($this->_fields[$name]);
485
	}
486
487
	/**
488
	 * Return an array of form field objects indexed by the field name
489
	 *
490
	 * @param array $names | null provide an array of names to return array of only the specified fields default is null
491
	 * which will return all fields
492
	 * @throws InvalidCallException if a name key specified in the $names array does not exist as a field
493
	 * @return \neon\core\form\fields\Field[] [name => field object]
494
	 */
495 62
	public function getFields($names=null)
496
	{
497 62
		$this->orderFields();
498 62
		if ($names === null)
499 62
			return $this->_fields;
500
501
		return Arr::only($this->_fields, $names);
502
	}
503
504
	/**
505
	 * Get an array of child sub forms
506
	 *
507
	 * @return \neon\core\form\Form[] indexed by form name
508
	 */
509 2
	public function getSubForms()
510
	{
511 2
		$ret = [];
512 2
		foreach ($this->_fields as $key => $field) {
513 2
			if ($field instanceof Form) {
514 2
				$ret[$key] = $field;
515
			}
516
		}
517 2
		return $ret;
518
	}
519
520
	/**
521
	 * Get a sub form
522
	 * @param string $name
523
	 * @throws \Exception if no subform exists with the name specified
524
	 * @return Form
525
	 */
526
	public function getSubForm($name)
527
	{
528
		if (! $this->hasField($name)) {
529
			throw new \Exception("No sub form exists with the name '$name'");
530
		}
531
		return $this->getField($name);
532
	}
533
534
	/**
535
	 * Order the fields by their `order` property values
536
	 */
537 62
	public function orderFields()
538
	{
539 62
		Arr::multisort($this->_fields, 'order');
540 62
	}
541
542
	/**
543
	 * Takes a configuration array which is an array of field configuration arrays
544
	 * This would look something like this:
545
	 *
546
	 * ```php
547
	 *  [
548
	 *      [
549
	 *           'class' => '\neon\core\form\fields\Text'
550
	 *           'name' => 'first_name'
551
	 *           // 'validators' => ...
552
	 *           // ... (other config options)
553
	 *      ],
554
	 *      [
555
	 *           'class' => '\neon\core\form\fields\Text'
556
	 *           'name' => 'last_name'
557
	 *           "validators" : [
558
	 *               ["string", ["max" : 10]]
559
	 *           ]
560
	 *      ],
561
	 *      'dataKey' => [
562
	 *           'class' => '\neon\core\form\fields\Text',
563
	 *           'label' => 'Thing'
564
	 *      ]
565
	 *  ]
566
	 * ```
567
	 *
568
	 * **Note** that if a key is defined for a field and no name or dataKey property exists in the config array
569
	 * then the key will be used as the ```dataKey``` and the ```name``` property for the field.
570
	 *
571
	 * Therefore given a config array of:
572
	 *
573
	 * ```php
574
	 *
575
	 * [
576
	 *     'myField' => [
577
	 *          'class' => 'neon\core\form\fields\Text'
578
	 *     ]
579
	 * ]
580
	 * ```
581
	 *
582
	 * Will add a field with the config:
583
	 *
584
	 * ```php
585
	 * [
586
	 *      'class' => 'neon\core\form\fields\Text',
587
	 *      'name' => 'myField',
588
	 *      'dataKey' => 'myField'
589
	 * ]
590
	 * ```
591
	 * @param array $config
592
	 * @return $this - make chainable method
593
	 */
594 10
	public function setFields($config)
595
	{
596 10
		foreach ($config as $key => $fieldConfig) {
597
			// if a key is defined in the array then assume we want this to become the name and the data key for the field element
598
			// this will be ignored if 'name' or 'dataKey' is explicitly defined in $fieldConfig
599 10
			if (is_string($key)) {
600 4
				$fieldConfig = Arr::merge(['name' => $key, 'dataKey' => $key], $fieldConfig);
601
			}
602 10
			$this->add($fieldConfig);
603
		}
604 10
		return $this;
605
	}
606
607
	/**
608
	 * Remove a field
609
	 * @param string $name
610
	 */
611
	public function removeField($name)
612
	{
613
		unset($this->_fields[$name]);
614
	}
615
616
	/**
617
	 * A generic add function to add fields to a form
618
	 *
619
	 * ```PHP
620
	 * $field = new \neon\core\form\fields\Email(['name' => 'email']);
621
	 * $form->add($field);
622
	 * ```
623
	 *
624
	 * @param array|IField $field a configuration array or an IField object
625
	 * @param array $defaults (optional) - This is ignored if the $field parameter is an object otherwise
626
	 *   it represents defaults for the configuration array
627
	 * @param boolean $overwrite (optional) - whether to overwrite an existing field of the same name defaults to false
628
	 * @throws exceptions\UnknownFieldClass - if the field class can not be created
629
	 * @return fields\Field
630
	 */
631 112
	public function add($field, $defaults=[], $overwrite=false)
632
	{
633 112
		$object = $this->makeField($field, $defaults);
634 104
		return $this->_addField($object, $overwrite);
635
	}
636
637
	/**
638
	 * Add a form field to this form
639
	 * @param  IField  $object instance of a form Field
640
	 * @param boolean $overwrite (optional) - Whether to overwrite any existing fields with the same name default is false
641
	 *
642
	 * @return \neon\core\form\fields\Field
643
	 */
644 104
	protected function _addField(IField $object, $overwrite=false)
645
	{
646
		// pass the field a reference to its parent form
647 104
		$object->setForm($this);
648 104
		if ($object->getName() == '') {
649
			throw new \InvalidArgumentException('A field object must have a name set before adding it to the form. You tried to add ' . print_r($object->toArray(), true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($object->toArray(), true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

649
			throw new \InvalidArgumentException('A field object must have a name set before adding it to the form. You tried to add ' . /** @scrutinizer ignore-type */ print_r($object->toArray(), true));
Loading history...
650
		}
651 104
		if ($object instanceof Field || $object instanceof Form) {
652
			// the field object seems to be valid so before we add it we just check this is not a conflict with an existing field
653
			// if overwrite is true then we overwrite with the new field
654 104
			if (!$overwrite && array_key_exists($object->getName(), $this->_fields)) {
655
				throw new \InvalidArgumentException('The field with the key "'.$object->getName().'" already exists in the forms field array');
656
			}
657
			// add this form as the parent form reference
658 104
			$this->_fields[$object->getName()] = $object;
659
		} else {
660
			throw new \InvalidArgumentException('The $object parameter was not a valid \neon\core\form\Field or \neon\core\form\Form object "' . print_r($object, true) . '" given');
0 ignored issues
show
Bug introduced by
Are you sure print_r($object, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

660
			throw new \InvalidArgumentException('The $object parameter was not a valid \neon\core\form\Field or \neon\core\form\Form object "' . /** @scrutinizer ignore-type */ print_r($object, true) . '" given');
Loading history...
661
		}
662 104
		return $object;
663
	}
664
665
// endregion
666
667
// region: Render functions
668
// ============================================================================
669
670
	/**
671
	 * render the form header html
672
	 * @return string
673
    */
674
	public function renderHeader()
675
	{
676
		// TODO: 27/06/2018 - toArray does a fair bit of work if form has multiple fields set, we want to serialize all properties EXCEPT for fields
677
		$options = $this->toArray(['except'=>['fields']]);
678
		unset($options['fields']);
679
		return "<neon-core-form-form v-bind='{$this->jsonEncode($options)}' id='{$this->id}'>";
680
	}
681
682
	/**
683
	 * Render the csrf validation token prevent - cross site request forgery
684
	 * @return string
685
	 */
686
	public function renderCsrfField()
687
	{
688
		return '<input type="hidden" name="'.$this->getRequest()->csrfParam.'" value="' . $this->getRequest()->getCsrfToken() . '" />';
689
	}
690
691
	/**
692
	 * Get the CSRF param string - this is the parameter name that the request expects the csrf token to be in
693
	 * @see $this->getCsrfToken()
694
	 * @return string
695
	 * @deprecated use $this->getRequest()->csrfParam
696
	 */
697 10
	public function getCsrfParam()
698
	{
699
		// this method assumes the form has been created via a request
700
		// which is not always true.
701
		try {
702 10
			return $this->getRequest()->csrfParam;
703
		} catch (\Exception $e) {
704
			return null;
705
		}
706
	}
707
708
	/**
709
	 * Get the csrf request token string
710
	 * @see $this->getCsrfParam()
711
	 * @return string
712
	 * @deprecated use $this->getRequest()->getCsrfToken
713
	 */
714 10
	public function getCsrfToken()
715
	{
716
		// this method assumes the form has been created via a request
717
		// which is not always true.
718
		try {
719 10
			return $this->getRequest()->csrfToken;
720
		} catch (\Exception $e) {
721
			return null;
722
		}
723
	}
724
725
	/**
726
	 * render the form footer html
727
	 * @return string
728
	 */
729
	public function renderFooter()
730
	{
731
		$this->registerScripts($this->getView());
732
		return '</neon-core-form-form>';
733
	}
734
735
// endregion
736
737
738
	/**
739
	 * Check to see if there is request data for this form
740
	 * If there is then this will return that data
741
	 * @throws \yii\base\InvalidConfigException getBodyParams may throw this error if a registered parser does not implement the [[RequestParserInterface]].
742
	 * @return bool  true if there is request data
743
	 */
744 2
	public function hasRequestData()
745
	{
746 2
		if ($this->shouldProcess()) {
747 2
			$params = $this->getRequest()->getBodyParams();
748 2
			return isset($params[$this->getName()]);
749
		}
750
		return false;
751
	}
752
753
	/**
754
	 * Get hold of the raw unprocessed form request data. To get the
755
	 * actual form data use @see getData
756
	 * @param $key  if passed in, select data[$key]
0 ignored issues
show
Bug introduced by
The type neon\core\form\if was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
757
	 * @throws \yii\base\InvalidConfigException getBodyParams may throw this error if a registered parser does not implement the [[RequestParserInterface]].
758
	 * @return array  the request data
759
	 */
760 2
	public function getRawRequestData($key=null)
761
	{
762 2
		$params = $this->getRequest()->getBodyParams();
763 2
		$data = isset($params[$this->getName()]) ? $params[$this->getName()] : [];
764 2
		if (!empty($key)) {
765
			return isset($data[$key]) ? $data[$key] : [];
766
		}
767 2
		return $data;
768
	}
769
770
	/**
771
	 * Load data into the form
772
	 * typical example ```$form->load(neon()->request->post())```
773
	 * The passed in array can be indexed by the form name. e.g. [[FormName] => ['field_1' => 'val', 'field2' => 'val']]
774
	 * Or simply be a list of fields the load function checks to see if the key is its form name or a name of one of its
775
	 * fields.
776
	 *
777
	 * Load is really only a short cut method when you do not want to do the following:
778
	 *
779
	 * ```php
780
	 * // if you call load without parameters
781
	 * $this->load(); // This will look for the form name as a post parameter key which is equivalent to the following:
782
	 * $this->setValue(neon()->request->post('my_form_name'));
783
	 * // equivalent of:
784
	 * $this->load(neon()->request->post('my_form_name'));
785
	 * ```
786
	 *
787
	 * @param array|null $data  if null will attempt to load the form from the current request data
788
	 * @throws \Exception - if request object is configured incorrectly
789
	 * @return $this
790
	 */
791 48
	public function load($data=[])
792
	{
793 48
		if (empty($data) && $this->hasRequestData()) {
794 2
			$data = $this->getRawRequestData();
795
		}
796 48
		$this->setValue($data);
797 48
		return $this;
798
	}
799
800
	/**
801
	 * Load form data from the system representations - these will be data in
802
	 * the same format as is generated by the getData() function
803
	 *
804
	 * @param array $data
805
	 */
806 2
	public function loadFromDb($data)
807
	{
808 2
		foreach ($data as $name => $value) {
809 2
			if ($this->hasField($name)) {
810 2
				$this->getField($name)->setValueFromDb($value);
811
			}
812
		}
813 2
	}
814
815
	/**
816
	 * @inheritdoc
817
	 */
818 48
	public function getData()
819
	{
820 48
		$data = [];
821 48
		foreach ($this->getFields() as $field) {
822 48
			if (!$field->deleted) {
823 48
				if ($field->getIsInput())
824 48
					$data[$field->getDataKey()] = $field->getData();
825
			}
826
		}
827 48
		return $data;
828
	}
829
830
	/**
831
	 * Gets a representation of the forms data by its value display
832
	 * Typically this will use the human readable output for the fields of the form
833
	 * @return array
834
	 */
835
	public function getDataDisplay()
836
	{
837
		$data = [];
838
		foreach ($this->getFields() as $field) {
839
			if ($field->getIsInput())
840
				$data[$field->getName()] = $field->getValueDisplay();
841
		}
842
		return $data;
843
	}
844
845
// region: implement the IField interface - this allows forms to be used as a field within a parent form
846
// =================================================================================================================
847
848
	/**
849
	 *  @inheritdoc
850
	 */
851 2
	public function getDataKey()
852
	{
853 2
		if ($this->_dataKey !== null)
854 2
			return $this->_dataKey;
855 2
		return $this->getName();
856
	}
857
858
	/**
859
	 * @inheritdoc
860
	 */
861 4
	public function setDataKey($key)
862
	{
863 4
		$this->_dataKey = $key;
864 4
		return $this;
865
	}
866
867
	/**
868
	 * @inheritdoc
869
	 */
870 14
	public function getName()
871
	{
872 14
		if ($this->_name == null) {
873
			$reflector = new ReflectionClass($this);
874
			$this->_name = $reflector->getShortName();
875
		}
876 14
		return $this->_name;
877
	}
878
879
	/**
880
	 * Root forms always require an id
881
	 * @return String
882
	 */
883 12
	public function getId($autoGenerate=false)
884
	{
885 12
		if ($this->_id)
886 2
			return $this->_id;
887 12
		return $this->getName();
888
	}
889
890
	/**
891
	 * @inheritDoc
892
	 */
893 52
	public function setValue($value)
894
	{
895 52
		foreach ($value as $name => $val) {
896 52
			if ($this->hasField($name)) {
897 52
				$this->getField($name)->setValue($val);
898
			}
899
		}
900 52
		return $this;
901
	}
902
903
	/**
904
	 * @inheritDoc
905
	 */
906 2
	public function setValueFromDb($value)
907
	{
908 2
		$this->loadFromDb($value);
909 2
		return $this;
910
	}
911
912
	/**
913
	 * @inheritdoc
914
	 */
915 46
	public function getValue()
916
	{
917 46
		$value = [];
918 46
		foreach ($this->getFields() as $field) {
919 46
			if (!$field->deleted) {
920 46
				if ($field->getIsInput())
921 46
					$value[$field->getDataKey()] = $field->getValue();
922
			}
923
		}
924 46
		return $value;
925
	}
926
927
	/**
928
	 * Get the hint message for this field
929
	 * @return string
930
	 */
931 12
	public function getHint()
932
	{
933 12
		return $this->_hint;
934
	}
935
936
	/**
937
	 * Set the hint for this field
938
	 *
939
	 * @param string $hint
940
	 *
941
	 * @return $this
942
	 */
943 10
	public function setHint($hint)
944
	{
945 10
		$this->_hint = $hint;
946 10
		return $this;
947
	}
948
949
	/**
950
	 * Set the label for the field
951
	 *
952
	 * @param string $label
953
	 * @return $this is a chainable method
954
	 */
955 10
	public function setLabel($label)
956
	{
957 10
		$this->_label = $label;
958 10
		return $this;
959
	}
960
961
	/**
962
	 * Return the label for this field
963
	 * @return $this is a chainable method
964
	 */
965 12
	public function getLabel()
966
	{
967 12
		return $this->_label;
968
	}
969
970
	/**
971
	 * Set the classLabel for the field
972
	 *  a user friendly name for the field class
973
	 * @param string $label
974
	 * @return $this is a chainable method
975
	 */
976
	public function setClassLabel($label)
977
	{
978
		$this->_classLabel = $label;
979
		return $this;
980
	}
981
982
	/**
983
	 * Return the class label for this field
984
	 * @return $this is a chainable method
985
	 */
986
	public function getClassLabel()
987
	{
988
		return $this->_classLabel;
989
	}
990
991
	/**
992
	 * Whether this is a form
993
	 * @return bool
994
	 */
995 12
	public function isForm()
996
	{
997 12
		return true;
998
	}
999
1000
	/**
1001
	 * @inheritdoc - IField implementation,
1002
	 * Possibly applicable to the form - perhaps if set the form will complain unless all its children are set??
1003
	 */
1004
	public function setRequired($required=true)
1005
	{}
1006
// endregion
1007
1008
	/**
1009
	 * Helper function to reduce controller code
1010
	 * if the form supports ajax validation this will return
1011
	 * the appropriate validation array data and set the response object to process as json
1012
	 * note the result of this function should be returned by the controller action.
1013
	 *
1014
	 * Inside the controller action:
1015
	 *
1016
	 * ~~~php
1017
	 * if (neon()->request->isAjax) {
1018
	 *     return $form->ajaxValidation()
1019
	 * }
1020
	 * ~~~
1021
	 *
1022
	 * Steve: Personally I dislike this code - I dislike the current yii ajax form validation clogging up my controller actions
1023
	 * The ajax validation is so tightly coupled to the Form and the JQuery yiiActiveForm plugin that the controller
1024
	 * just gets in the way - its only job is to know its ajax - perhaps setting up a form object convention so we can
1025
	 * have a more generic ajax validation route for forms would remove this layer of cruft from every single controller action
1026
	 * @throws UnknownPropertyException - if a validator attempts to access an unknown property
1027
	 *
1028
	 * @return array
1029
	 */
1030
	public function ajaxValidation()
1031
	{
1032
		$this->load();
1033
		$this->validate();
1034
		return $this->getValidationData();
1035
	}
1036
1037
	/**
1038
	 * Process the form in the context of http request
1039
	 * The form class knows generally how it sends data via its method property
1040
	 * Therefore process the data in the context of the form - load the data and validate against the form fields
1041
	 * @return boolean true if validate if data is loaded and validates successfully
1042
	 */
1043
	public function processRequest()
1044
	{
1045
		if ($this->hasRequestData()) {
1046
			$this->load();
1047
			return $this->validate();
1048
		}
1049
		return false;
1050
	}
1051
1052
	/**
1053
	 * Whether the form should process the request.
1054
	 * Essentially if the current request method matches the form method.
1055
	 * Typically if the request is POST and the form method is POST this function will return true.
1056
	 * @return bool
1057
	 */
1058 2
	public function shouldProcess()
1059
	{
1060 2
		return $this->getRequest()->getMethod() == strtoupper($this->method);
1061
	}
1062
1063
	/**
1064
	 * @return string
1065
	 */
1066
	public function __toString()
1067
	{
1068
		return $this->run();
1069
	}
1070
1071
	/**
1072
	 * Array serialize
1073
	 */
1074 12
	public function getProperties()
1075
	{
1076
		$props = [
1077 12
			'class', 'id', 'name', 'enableAjaxValidation','enableAjaxSubmission',
1078
			'label', 'hint', 'inline', 'visible', 'order', 'action', 'readOnly', 'printOnly', 'validationUrl',
1079
			'attributes', 'fields', 'objectToken', 'isSubmitted'
1080
		];
1081 12
		if ($this->isRootForm()) {
1082 10
			$props[] = 'csrfParam';
1083 10
			$props[] = 'csrfToken';
1084
		}
1085 12
		return $props;
1086
	}
1087
1088
	/**
1089
	 * Get the full class name of this class
1090
	 * @return string
1091
	 */
1092 12
	public function getClass()
1093
	{
1094 12
		return get_class($this);
1095
	}
1096
1097
	/**
1098
	 * Return the rendered output of the form
1099
	 * @return string
1100
	 */
1101
	public function getValueDisplay($context='')
1102
	{
1103
		return $this->run();
1104
	}
1105
1106
	/* =========================================================================
1107
	 * Creation and Extraction of forms by Definition
1108
	 * =========================================================================
1109
	 *
1110
	 * The form definition is defined partially by the form and partially
1111
	 * by the systems that will deal with storing this information
1112
	 *
1113
	 * =========================================================================
1114
	 */
1115
1116
	/**
1117
	 * Get the definition of this form suitable for use elsewhere.
1118
	 * This is not the same as getting the toArray version of this form for
1119
	 * the following reasons:
1120
	 *
1121
	 * 1. there are additional values that whilst they could be extracted out from
1122
	 * the toArray data elsewhere puts the responsibility for knowing how to in
1123
	 * the wrong place (it forces data scrapping). A change in definition here
1124
	 * should not break code elsewhere.
1125
	 *
1126
	 * 2. there are irrelevant fields on the form that should be removed. These
1127
	 * are any that are to do with security between client and server but which
1128
	 * don't change the meaning of the form. Only those values that are properly
1129
	 * part of the form definition should be returned.
1130
	 */
1131 6
	public function exportDefinition()
1132
	{
1133 6
		$definition = $this->toArray();
1134
		// remove any irrelevant fields or ones that will be created separately
1135 6
		unset($definition['csrfParam']);
1136 6
		unset($definition['csrfToken']);
1137 6
		unset($definition['fields']);
1138
1139
		$formDefinition = [
1140 6
			'name' => $this->getName(),
1141 6
			'label' => $this->getLabel(),
1142 6
			'description' => $this->getHint(),
1143 6
			'definition' => $definition
1144
		];
1145 6
		$fields = [];
1146 6
		foreach ($this->getFields() as $key => $field) {
1147 6
			$fieldDefinition = $field->exportDefinition();
1148
1149 6
			if (! is_null($field->ddsDataType) && ! is_null($fieldDefinition) )
1150 6
				$fields[$key] = $fieldDefinition;
1151
		}
1152 6
		$formDefinition['definition']['fields'] = $fields;
1153 6
		return $formDefinition;
1154
	}
1155
1156
	// TODO: remove the need for this function!
1157
	public function getDdsName()
1158
	{
1159
		return preg_replace("/[^a-z0-9_]/", '', strtolower(preg_replace("/ +/", '_', $this->getName())));
1160
	}
1161
1162
// endregion
1163
1164
// region: ArrayAccess implementation methods
1165
// ============================================================================
1166
1167
	/**
1168
	 * @inheritDoc
1169
	 */
1170
	public function offsetExists($offset)
1171
	{
1172
		return $this->hasField($offset);
1173
	}
1174
1175
	/**
1176
	 * @inheritDoc
1177
	 */
1178 52
	public function offsetGet($offset)
1179
	{
1180 52
		return $this->getField($offset);
1181
	}
1182
1183
	/**
1184
	 * @inheritDoc
1185
	 */
1186
	public function offsetSet($offset, $value)
1187
	{
1188
		$object = $this->makeField($value);
1189
		$object->setName($offset);
1190
		return $this->add($object, [], true);
1191
	}
1192
1193
	/**
1194
	 * @inheritDoc
1195
	 */
1196
	public function offsetUnset($offset)
1197
	{
1198
		$this->removeField($offset);
1199
	}
1200
// endregion
1201
1202
// region: Filter Form
1203
// ============================================================================
1204
	/**
1205
	 * Asks each field member for a suitable field to represent its search data.  Then returns this as a form object.
1206
	 * Some member fields will have different fields to represent them in a search form.
1207
	 * For example a Date field may return a DateRange field in order to search.
1208
	 * A checkbox may return a drop down list of search choices e.g. ("on" | "off" | "all")
1209
	 *
1210
	 * @param array $config
1211
	 *
1212
	 * @return Form
1213
	 */
1214
	public function getFilterForm($config=[])
1215
	{
1216
		$searchForm = new Form($config);
1217
		foreach($this->getFields() as $field) {
1218
			$filterField = $field->getFilterField();
1219
1220
			if ($filterField !== false) {
1221
				$searchForm->add($field->getFilterField(), ['name' => $field->getName()]);
1222
			}
1223
		}
1224
		return $searchForm;
1225
	}
1226
1227
	/**
1228
	 * @inheritDoc
1229
	 */
1230
	public function getFilterField()
1231
	{
1232
		return $this->getFilterForm();
1233
	}
1234
1235
	/**
1236
	 * @inheritDoc
1237
	 */
1238
	public function processAsFilter(IQuery $query, $searchData = null)
1239
	{
1240
		$searchData = ($searchData === null) ? $this->getValue() : $searchData;
1241
		foreach($this->getFields() as $key => $field) {
1242
			if (isset($searchData[$key])) {
1243
				$field->processAsFilter($query, $searchData[$key]);
1244
			}
1245
		}
1246
		return $query;
1247
	}
1248
// endregion
1249
1250
	/**
1251
	 * @var \neon\core\web\Request
1252
	 */
1253
	protected $_request;
1254
1255
	/**
1256
	 * Returns the request object
1257
	 * @return \neon\core\web\Request
1258
	 */
1259 12
	public function getRequest()
1260
	{
1261 12
		if ($this->_request === null)
1262 10
			$this->_request = neon()->request;
1263 12
		return $this->_request;
1264
	}
1265
1266
	/**
1267
	 * Set the request object the form should use
1268
	 *
1269
	 * @param \neon\core\web\Request $request
1270
	 */
1271 6
	public function setRequest($request)
1272
	{
1273 6
		$this->_request = $request;
1274 6
	}
1275
1276
	/**
1277
	 * Reset all the values within the form
1278
	 */
1279
	public function reset()
1280
	{
1281
		foreach($this->getFields() as $field)
1282
			$field->reset();
1283
	}
1284
1285
	/**
1286
	 * @inheritdoc
1287
	 */
1288 2
	public function getIsInput()
1289
	{
1290 2
		return true;
1291
	}
1292
1293
	/**
1294
	 * Generate fake data
1295
	 * @return array
1296
	 */
1297
	public function fake()
1298
	{
1299
		$fake = [];
1300
		foreach($this->getFields() as $key => $field) {
1301
			$val = $field->fake();
1302
			if (empty($val)) continue;
1303
			$fake[$key] = $val;
1304
		}
1305
		return $fake;
1306
	}
1307
1308
	/**
1309
	 * @deprecated should be using phoebe this should be replaced with using the loadFromDefinition via phoebe
1310
	 * @see Deprecated::ddsLoadFrom
1311
	 */
1312
	public function ddsLoadFrom($classType, $includeDeleted=false)
1313
	{
1314
		return Deprecated::ddsLoadFrom($this, $classType, $includeDeleted);
0 ignored issues
show
Deprecated Code introduced by
The function neon\core\form\Deprecated::ddsLoadFrom() has been deprecated: should be using phoebe this should be replaced with using the loadFromDefinition via phoebe ( Ignorable by Annotation )

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

1314
		return /** @scrutinizer ignore-deprecated */ Deprecated::ddsLoadFrom($this, $classType, $includeDeleted);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1315
	}
1316
1317
	/**
1318
	 * @deprecated - should be using phoebe
1319
	 * @see Deprecated::ddsSaveDefinition
1320
	 */
1321
	public function ddsSaveDefinition($classType=null, $ddsMembers=null)
1322
	{
1323
		Deprecated::ddsSaveDefinition($this, $classType, $ddsMembers);
0 ignored issues
show
Deprecated Code introduced by
The function neon\core\form\Deprecated::ddsSaveDefinition() has been deprecated. ( Ignorable by Annotation )

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

1323
		/** @scrutinizer ignore-deprecated */ Deprecated::ddsSaveDefinition($this, $classType, $ddsMembers);
Loading history...
1324
	}
1325
1326
	/**
1327
	 * @deprecated should be using phoebe
1328
	 * @see Deprecated::ddsAddField
1329
	 */
1330
	public function ddsAddField($member)
1331
	{
1332
		Deprecated::ddsAddField($this, $member);
0 ignored issues
show
Deprecated Code introduced by
The function neon\core\form\Deprecated::ddsAddField() has been deprecated: should be using phoebe ( Ignorable by Annotation )

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

1332
		/** @scrutinizer ignore-deprecated */ Deprecated::ddsAddField($this, $member);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1333
	}
1334
1335
	/**
1336
	 * Generate a token pointing to a serialised version of the form
1337
	 * This can then be unserialised using Hash::getObjectFromToken($token);
1338
	 * We only ever seralize the root form token - todo - seems to break properties trait
1339
	 * @return string
1340
	 * @throws \ReflectionException
1341
	 */
1342 12
	public function getObjectToken()
1343
	{
1344 12
		return Hash::setObjectToToken($this);
1345
	}
1346
1347
	/**
1348
	 * @inheritdoc
1349
	 */
1350
	public function getComponentDetails()
1351
	{
1352
		return false;
1353
	}
1354
1355
	/**
1356
	 * Alias of getField
1357
	 * @param $name
1358
	 * @return Field|Form
1359
	 */
1360
	public function get($name)
1361
	{
1362
		return $this->getField($name);
1363
	}
1364
}
1365