Passed
Push — 2.6.0 ( ...f22176 )
by steve
18:56
created

Form::ajaxValidation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
ccs 0
cts 4
cp 0
crap 2
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
		$options = $this->toArray();
677
		unset($options['fields']);
678
		return "<neon-core-form-form v-bind='{$this->jsonEncode($options)}' id='{$this->id}'>";
679
	}
680
681
	/**
682
	 * Render the csrf validation token prevent - cross site request forgery
683
	 * @return string
684
	 */
685
	public function renderCsrfField()
686
	{
687
		return '<input type="hidden" name="'.$this->getRequest()->csrfParam.'" value="' . $this->getRequest()->getCsrfToken() . '" />';
688
	}
689
690
	/**
691
	 * Get the CSRF param string - this is the parameter name that the request expects the csrf token to be in
692
	 * @see $this->getCsrfToken()
693
	 * @return string
694
	 * @deprecated use $this->getRequest()->csrfParam
695
	 */
696 10
	public function getCsrfParam()
697
	{
698
		// this method assumes the form has been created via a request
699
		// which is not always true.
700
		try {
701 10
			return $this->getRequest()->csrfParam;
702
		} catch (\Exception $e) {
703
			return null;
704
		}
705
	}
706
707
	/**
708
	 * Get the csrf request token string
709
	 * @see $this->getCsrfParam()
710
	 * @return string
711
	 * @deprecated use $this->getRequest()->getCsrfToken
712
	 */
713 10
	public function getCsrfToken()
714
	{
715
		// this method assumes the form has been created via a request
716
		// which is not always true.
717
		try {
718 10
			return $this->getRequest()->csrfToken;
719
		} catch (\Exception $e) {
720
			return null;
721
		}
722
	}
723
724
	/**
725
	 * render the form footer html
726
	 * @return string
727
	 */
728
	public function renderFooter()
729
	{
730
		$this->registerScripts($this->getView());
731
		return '</neon-core-form-form>';
732
	}
733
734
// endregion
735
736
737
	/**
738
	 * Check to see if there is request data for this form
739
	 * If there is then this will return that data
740
	 * @throws \yii\base\InvalidConfigException getBodyParams may throw this error if a registered parser does not implement the [[RequestParserInterface]].
741
	 * @return bool  true if there is request data
742
	 */
743 2
	public function hasRequestData()
744
	{
745 2
		if ($this->shouldProcess()) {
746 2
			$params = $this->getRequest()->getBodyParams();
747 2
			return isset($params[$this->getName()]);
748
		}
749
		return false;
750
	}
751
752
	/**
753
	 * Get hold of the raw unprocessed form request data. To get the
754
	 * actual form data use @see getData
755
	 * @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...
756
	 * @throws \yii\base\InvalidConfigException getBodyParams may throw this error if a registered parser does not implement the [[RequestParserInterface]].
757
	 * @return array  the request data
758
	 */
759 2
	public function getRawRequestData($key=null)
760
	{
761 2
		$params = $this->getRequest()->getBodyParams();
762 2
		$data = isset($params[$this->getName()]) ? $params[$this->getName()] : [];
763 2
		if (!empty($key)) {
764
			return isset($data[$key]) ? $data[$key] : [];
765
		}
766 2
		return $data;
767
	}
768
769
	/**
770
	 * Load data into the form
771
	 * typical example ```$form->load(neon()->request->post())```
772
	 * The passed in array can be indexed by the form name. e.g. [[FormName] => ['field_1' => 'val', 'field2' => 'val']]
773
	 * 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
774
	 * fields.
775
	 *
776
	 * Load is really only a short cut method when you do not want to do the following:
777
	 *
778
	 * ```php
779
	 * // if you call load without parameters
780
	 * $this->load(); // This will look for the form name as a post parameter key which is equivalent to the following:
781
	 * $this->setValue(neon()->request->post('my_form_name'));
782
	 * // equivalent of:
783
	 * $this->load(neon()->request->post('my_form_name'));
784
	 * ```
785
	 *
786
	 * @param array|null $data  if null will attempt to load the form from the current request data
787
	 * @throws \Exception - if request object is configured incorrectly
788
	 * @return $this
789
	 */
790 48
	public function load($data=[])
791
	{
792 48
		if (empty($data) && $this->hasRequestData()) {
793 2
			$data = $this->getRawRequestData();
794
		}
795 48
		$this->setValue($data);
796 48
		return $this;
797
	}
798
799
	/**
800
	 * Load form data from the system representations - these will be data in
801
	 * the same format as is generated by the getData() function
802
	 *
803
	 * @param array $data
804
	 */
805 2
	public function loadFromDb($data)
806
	{
807 2
		foreach ($data as $name => $value) {
808 2
			if ($this->hasField($name)) {
809 2
				$this->getField($name)->setValueFromDb($value);
810
			}
811
		}
812 2
	}
813
814
	/**
815
	 * @inheritdoc
816
	 */
817 48
	public function getData()
818
	{
819 48
		$data = [];
820 48
		foreach ($this->getFields() as $field) {
821 48
			if (!$field->deleted) {
822 48
				if ($field->getIsInput())
823 48
					$data[$field->getDataKey()] = $field->getData();
824
			}
825
		}
826 48
		return $data;
827
	}
828
829
	/**
830
	 * Gets a representation of the forms data by its value display
831
	 * Typically this will use the human readable output for the fields of the form
832
	 * @return array
833
	 */
834
	public function getDataDisplay()
835
	{
836
		$data = [];
837
		foreach ($this->getFields() as $field) {
838
			if ($field->getIsInput())
839
				$data[$field->getName()] = $field->getValueDisplay();
840
		}
841
		return $data;
842
	}
843
844
// region: implement the IField interface - this allows forms to be used as a field within a parent form
845
// =================================================================================================================
846
847
	/**
848
	 *  @inheritdoc
849
	 */
850 2
	public function getDataKey()
851
	{
852 2
		if ($this->_dataKey !== null)
853 2
			return $this->_dataKey;
854 2
		return $this->getName();
855
	}
856
857
	/**
858
	 * @inheritdoc
859
	 */
860 4
	public function setDataKey($key)
861
	{
862 4
		$this->_dataKey = $key;
863 4
		return $this;
864
	}
865
866
	/**
867
	 * @inheritdoc
868
	 */
869 14
	public function getName()
870
	{
871 14
		if ($this->_name == null) {
872
			$reflector = new ReflectionClass($this);
873
			$this->_name = $reflector->getShortName();
874
		}
875 14
		return $this->_name;
876
	}
877
878
	/**
879
	 * Root forms always require an id
880
	 * @return String
881
	 */
882 12
	public function getId($autoGenerate=false)
883
	{
884 12
		if ($this->_id)
885 2
			return $this->_id;
886 12
		return $this->getName();
887
	}
888
889
	/**
890
	 * @inheritDoc
891
	 */
892 52
	public function setValue($value)
893
	{
894 52
		foreach ($value as $name => $val) {
895 52
			if ($this->hasField($name)) {
896 52
				$this->getField($name)->setValue($val);
897
			}
898
		}
899 52
		return $this;
900
	}
901
902
	/**
903
	 * @inheritDoc
904
	 */
905 2
	public function setValueFromDb($value)
906
	{
907 2
		$this->loadFromDb($value);
908 2
		return $this;
909
	}
910
911
	/**
912
	 * @inheritdoc
913
	 */
914 46
	public function getValue()
915
	{
916 46
		$value = [];
917 46
		foreach ($this->getFields() as $field) {
918 46
			if (!$field->deleted) {
919 46
				if ($field->getIsInput())
920 46
					$value[$field->getDataKey()] = $field->getValue();
921
			}
922
		}
923 46
		return $value;
924
	}
925
926
	/**
927
	 * Get the hint message for this field
928
	 * @return string
929
	 */
930 12
	public function getHint()
931
	{
932 12
		return $this->_hint;
933
	}
934
935
	/**
936
	 * Set the hint for this field
937
	 *
938
	 * @param string $hint
939
	 *
940
	 * @return $this
941
	 */
942 10
	public function setHint($hint)
943
	{
944 10
		$this->_hint = $hint;
945 10
		return $this;
946
	}
947
948
	/**
949
	 * Set the label for the field
950
	 *
951
	 * @param string $label
952
	 * @return $this is a chainable method
953
	 */
954 10
	public function setLabel($label)
955
	{
956 10
		$this->_label = $label;
957 10
		return $this;
958
	}
959
960
	/**
961
	 * Return the label for this field
962
	 * @return $this is a chainable method
963
	 */
964 12
	public function getLabel()
965
	{
966 12
		return $this->_label;
967
	}
968
969
	/**
970
	 * Set the classLabel for the field
971
	 *  a user friendly name for the field class
972
	 * @param string $label
973
	 * @return $this is a chainable method
974
	 */
975
	public function setClassLabel($label)
976
	{
977
		$this->_classLabel = $label;
978
		return $this;
979
	}
980
981
	/**
982
	 * Return the class label for this field
983
	 * @return $this is a chainable method
984
	 */
985
	public function getClassLabel()
986
	{
987
		return $this->_classLabel;
988
	}
989
990
	/**
991
	 * Whether this is a form
992
	 * @return bool
993
	 */
994 12
	public function isForm()
995
	{
996 12
		return true;
997
	}
998
999
	/**
1000
	 * @inheritdoc - IField implementation,
1001
	 * Possibly applicable to the form - perhaps if set the form will complain unless all its children are set??
1002
	 */
1003
	public function setRequired($required=true)
1004
	{}
1005
// endregion
1006
1007
	/**
1008
	 * Helper function to reduce controller code
1009
	 * if the form supports ajax validation this will return
1010
	 * the appropriate validation array data and set the response object to process as json
1011
	 * note the result of this function should be returned by the controller action.
1012
	 *
1013
	 * Inside the controller action:
1014
	 *
1015
	 * ~~~php
1016
	 * if (neon()->request->isAjax) {
1017
	 *     return $form->ajaxValidation()
1018
	 * }
1019
	 * ~~~
1020
	 *
1021
	 * Steve: Personally I dislike this code - I dislike the current yii ajax form validation clogging up my controller actions
1022
	 * The ajax validation is so tightly coupled to the Form and the JQuery yiiActiveForm plugin that the controller
1023
	 * just gets in the way - its only job is to know its ajax - perhaps setting up a form object convention so we can
1024
	 * have a more generic ajax validation route for forms would remove this layer of cruft from every single controller action
1025
	 * @throws UnknownPropertyException - if a validator attempts to access an unknown property
1026
	 *
1027
	 * @return array
1028
	 */
1029
	public function ajaxValidation()
1030
	{
1031
		$this->load();
1032
		$this->validate();
1033
		return $this->getValidationData();
1034
	}
1035
1036
	/**
1037
	 * Process the form in the context of http request
1038
	 * The form class knows generally how it sends data via its method property
1039
	 * Therefore process the data in the context of the form - load the data and validate against the form fields
1040
	 * @return boolean true if validate if data is loaded and validates successfully
1041
	 */
1042
	public function processRequest()
1043
	{
1044
		if ($this->hasRequestData()) {
1045
			$this->load();
1046
			return $this->validate();
1047
		}
1048
		return false;
1049
	}
1050
1051
	/**
1052
	 * Whether the form should process the request.
1053
	 * Essentially if the current request method matches the form method.
1054
	 * Typically if the request is POST and the form method is POST this function will return true.
1055
	 * @return bool
1056
	 */
1057 2
	public function shouldProcess()
1058
	{
1059 2
		return $this->getRequest()->getMethod() == strtoupper($this->method);
1060
	}
1061
1062
	/**
1063
	 * @return string
1064
	 */
1065
	public function __toString()
1066
	{
1067
		return $this->run();
1068
	}
1069
1070
	/**
1071
	 * Array serialize
1072
	 */
1073 12
	public function getProperties()
1074
	{
1075
		$props = [
1076 12
			'class', 'id', 'name', 'enableAjaxValidation','enableAjaxSubmission',
1077
			'label', 'hint', 'inline', 'visible', 'order', 'action', 'readOnly', 'printOnly', 'validationUrl',
1078
			'attributes', 'fields', 'objectToken', 'isSubmitted'
1079
		];
1080 12
		if ($this->isRootForm()) {
1081 10
			$props[] = 'csrfParam';
1082 10
			$props[] = 'csrfToken';
1083
		}
1084 12
		return $props;
1085
	}
1086
1087
	/**
1088
	 * Get the full class name of this class
1089
	 * @return string
1090
	 */
1091 12
	public function getClass()
1092
	{
1093 12
		return get_class($this);
1094
	}
1095
1096
	/**
1097
	 * Return the rendered output of the form
1098
	 * @return string
1099
	 */
1100
	public function getValueDisplay($context='')
1101
	{
1102
		return $this->run();
1103
	}
1104
1105
	/* =========================================================================
1106
	 * Creation and Extraction of forms by Definition
1107
	 * =========================================================================
1108
	 *
1109
	 * The form definition is defined partially by the form and partially
1110
	 * by the systems that will deal with storing this information
1111
	 *
1112
	 * =========================================================================
1113
	 */
1114
1115
	/**
1116
	 * Get the definition of this form suitable for use elsewhere.
1117
	 * This is not the same as getting the toArray version of this form for
1118
	 * the following reasons:
1119
	 *
1120
	 * 1. there are additional values that whilst they could be extracted out from
1121
	 * the toArray data elsewhere puts the responsibility for knowing how to in
1122
	 * the wrong place (it forces data scrapping). A change in definition here
1123
	 * should not break code elsewhere.
1124
	 *
1125
	 * 2. there are irrelevant fields on the form that should be removed. These
1126
	 * are any that are to do with security between client and server but which
1127
	 * don't change the meaning of the form. Only those values that are properly
1128
	 * part of the form definition should be returned.
1129
	 */
1130 6
	public function exportDefinition()
1131
	{
1132 6
		$definition = $this->toArray();
1133
		// remove any irrelevant fields or ones that will be created separately
1134 6
		unset($definition['csrfParam']);
1135 6
		unset($definition['csrfToken']);
1136 6
		unset($definition['fields']);
1137 6
		unset($definition['errors']);
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