Completed
Push — master ( fb9144...7bebed )
by Lars
01:33
created

Validator::validate()   D

Complexity

Conditions 22
Paths 31

Size

Total Lines 137
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 54
CRAP Score 22.0704

Importance

Changes 0
Metric Value
dl 0
loc 137
ccs 54
cts 57
cp 0.9474
rs 4.6625
c 0
b 0
f 0
cc 22
eloc 66
nc 31
nop 2
crap 22.0704

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\HtmlFormValidator;
6
7
use Respect\Validation\Exceptions\ComponentException;
8
use Respect\Validation\Exceptions\NestedValidationException;
9
use Respect\Validation\Exceptions\ValidationException;
10
use Respect\Validation\Factory;
11
use Respect\Validation\Rules\AbstractRule;
12
use Respect\Validation\Rules\Email;
13
use Respect\Validation\Rules\HexRgbColor;
14
use Respect\Validation\Rules\Url;
15
use voku\helper\HtmlDomParser;
16
use voku\helper\SimpleHtmlDom;
17
use voku\helper\UTF8;
18
use voku\HtmlFormValidator\Exceptions\NoValidationRule;
19
use voku\HtmlFormValidator\Exceptions\UnknownFilter;
20
use voku\HtmlFormValidator\Exceptions\UnknownValidationRule;
21
22
class Validator
23
{
24
  /**
25
   * @var HtmlDomParser
26
   */
27
  private $formDocument;
28
29
  /**
30
   * @var string[][]
31
   */
32
  private $rules = [];
33
34
  /**
35
   * @var string[][]
36
   */
37
  private $required_rules = [];
38
39
  /**
40
   * @var string[][]
41
   */
42
  private $filters = [];
43
44
  /**
45
   * @var callable[]
46
   */
47
  private $filters_custom = [];
48
49
  /**
50
   * @var callable|null
51
   */
52
  private $translator;
53
54
  /**
55
   * @var ValidatorRulesManager
56
   */
57
  private $validatorRulesManager;
58
59
  /**
60
   * @var string
61
   */
62
  private $selector;
63
64
  /**
65
   * @param string $formHTML
66
   * @param string $selector
67
   */
68 19
  public function __construct($formHTML, $selector = '')
69
  {
70 19
    $this->validatorRulesManager = new ValidatorRulesManager();
71
72 19
    $this->formDocument = HtmlDomParser::str_get_html($formHTML);
0 ignored issues
show
Unused Code introduced by
The call to HtmlDomParser::str_get_html() has too many arguments starting with $formHTML.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
73 19
    $this->selector = $selector;
74
75 19
    $this->parseHtmlDomForRules();
76 19
  }
77
78
  /**
79
   * @param string   $name   <p>A name for the "data-filter"-attribute in the dom.</p>
80
   * @param callable $filter <p>A custom filter.</p>
81
   */
82 1
  public function addCustomFilter(string $name, callable $filter)
83
  {
84 1
    $this->filters_custom[$name] = $filter;
85 1
  }
86
87
  /**
88
   * @param string              $name      <p>A name for the "data-validator"-attribute in the dom.</p>
89
   * @param string|AbstractRule $validator <p>A custom validation class.</p>
90
   */
91 3
  public function addCustomRule(string $name, $validator)
92
  {
93 3
    $this->validatorRulesManager->addCustomRule($name, $validator);
94 3
  }
95
96
  /**
97
   * @param mixed  $currentFieldData
98
   * @param string $fieldFilter
99
   *
100
   * @throws UnknownFilter
101
   *
102
   * @return mixed|string|null
103
   */
104 5
  private function applyFilter($currentFieldData, string $fieldFilter)
105
  {
106 5
    if ($currentFieldData === null) {
107 1
      return null;
108
    }
109
110 5
    if (isset($this->filters_custom[$fieldFilter])) {
111 1
      return \call_user_func($this->filters_custom[$fieldFilter], $currentFieldData);
112
    }
113
114
    switch ($fieldFilter) {
115 4
      case 'trim':
116 4
        return \trim($currentFieldData);
117 4
      case 'escape':
118 4
        return \htmlentities($currentFieldData, ENT_QUOTES | ENT_HTML5, 'UTF-8');
119
    }
120
121 4
    if (method_exists(UTF8::class, $fieldFilter)) {
122 4
      $currentFieldData = \call_user_func([UTF8::class, $fieldFilter], $currentFieldData);
123
    } else {
124
      throw new UnknownFilter(
125
          'No filter available for "' . $fieldFilter . '"'
126
      );
127
    }
128
129 4
    return $currentFieldData;
130
  }
131
132
  /**
133
   * @param string $type
134
   *
135
   * @return string|null
136
   */
137 4
  public function autoSelectRuleByInputType(string $type)
138
  {
139
    $matchingArray = [
140 4
        'email' => Email::class,
141
        'url'   => Url::class,
142
        'color' => HexRgbColor::class,
143
144
        //
145
        // TODO@me -> take a look here
146
        // -> https://github.com/xtreamwayz/html-form-validator/blob/master/src/FormElement/Number.php
147
        //
148
    ];
149
150 4
    return $matchingArray[$type] ?? null;
151
  }
152
153
  /**
154
   * Get the filters that will be applied.
155
   *
156
   * @return string[][]
157
   */
158 1
  public function getAllFilters(): array
159
  {
160 1
    return $this->filters;
161
  }
162
163
  /**
164
   * Get the rules that will be applied.
165
   *
166
   * @return string[][]
167
   */
168 12
  public function getAllRules(): array
169
  {
170 12
    return $this->rules;
171
  }
172
173
  /**
174
   * @param array  $formValues
175
   * @param string $field
176
   *
177
   * @return mixed|null
178
   */
179 15
  private function getCurrentFieldValue(array $formValues, string $field)
180
  {
181 15
    $fieldArrayPos = UTF8::strpos($field, '[');
182 15
    if ($fieldArrayPos !== false) {
183 3
      $fieldStart = UTF8::substr($field, 0, $fieldArrayPos);
184 3
      $fieldArray = UTF8::substr($field, $fieldArrayPos);
185 3
      $fieldHelperChar = '';
186 3
      $fieldArrayTmp = preg_replace_callback(
187 3
          '/\[([^\]]+)\]/',
188 3
          function ($match) use ($fieldHelperChar) {
189 3
            return $match[1] . $fieldHelperChar;
190 3
          },
191 3
          $fieldArray
192
      );
193 3
      $fieldArrayTmp = explode($fieldHelperChar, trim($fieldArrayTmp, $fieldHelperChar));
194
195 3
      $i = 0;
196 3
      $fieldHelper = [];
197 3
      foreach ($fieldArrayTmp as $fieldArrayTmpInner) {
198 3
        $fieldHelper[$i] = $fieldArrayTmpInner;
199
200 3
        $i++;
201
      }
202
203 3
      $currentFieldValue = null;
204
205
      switch ($i) {
206 3
        case 4:
207
          if (isset($formValues[$fieldStart][$fieldHelper[0]][$fieldHelper[1]][$fieldHelper[2]][$fieldHelper[3]])) {
208
            $currentFieldValue = $formValues[$fieldStart][$fieldHelper[0]][$fieldHelper[1]][$fieldHelper[2]][$fieldHelper[3]];
209
          }
210
          break;
211 3
        case 3:
212
          if (isset($formValues[$fieldStart][$fieldHelper[0]][$fieldHelper[1]][$fieldHelper[2]])) {
213
            $currentFieldValue = $formValues[$fieldStart][$fieldHelper[0]][$fieldHelper[1]][$fieldHelper[2]];
214
          }
215
          break;
216 3
        case 2:
217 1
          if (isset($formValues[$fieldStart][$fieldHelper[0]][$fieldHelper[1]])) {
218 1
            $currentFieldValue = $formValues[$fieldStart][$fieldHelper[0]][$fieldHelper[1]];
219
          }
220 1
          break;
221 2
        case 1:
222 2
          if (isset($formValues[$fieldStart][$fieldHelper[0]])) {
223 2
            $currentFieldValue = $formValues[$fieldStart][$fieldHelper[0]];
224
          }
225 3
          break;
226
      }
227
    } else {
228 12
      $currentFieldValue = $formValues[$field] ?? null;
229
    }
230
231 15
    return $currentFieldValue;
232
  }
233
234
  /**
235
   * @return string
236
   */
237 3
  public function getHtml(): string
238
  {
239 3
    return $this->formDocument->html();
240
  }
241
242
  /**
243
   * Get the required rules that will be applied.
244
   *
245
   * @return string[][]
246
   */
247
  public function getRequiredRules(): array
248
  {
249
    return $this->required_rules;
250
  }
251
252
  /**
253
   * @return callable|null
254
   */
255 14
  public function getTranslator()
256
  {
257 14
    return $this->translator;
258
  }
259
260
  /**
261
   * Find the first form on page or via css-selector, and parse <input>-elements.
262
   *
263
   * @return bool
264
   */
265 19
  public function parseHtmlDomForRules(): bool
266
  {
267
    // init
268 19
    $this->rules = [];
269 19
    $inputForm = [];
270
271 19
    if ($this->selector) {
272 2
      $forms = $this->formDocument->find($this->selector);
273
    } else {
274 17
      $forms = $this->formDocument->find('form');
275
    }
276
277 19
    if (\count($forms) === 0) {
278 1
      return false;
279
    }
280
281
    // get the first form
282 18
    $form = $forms[0];
283
284
    // get the from-id
285 18
    if ($form->id) {
286 11
      $formHelperId = $form->id;
287
    } else {
288 7
      $formHelperId = \uniqid('html-form-validator-tmp', true);
289
    }
290
291
    // get the <input>-elements from the form
292 18
    $inputFromFields = $form->getElementsByTagName('input');
293 18
    foreach ($inputFromFields as $inputFormField) {
294 18
      $this->parseInputForRules($inputFormField, $formHelperId);
295 18
      $this->parseInputForFilter($inputFormField, $formHelperId);
296
    }
297
298
    // get the <input>-elements with a matching form="id"
299 18
    if (\strpos($formHelperId, 'html-form-validator-tmp') !== 0) {
300 11
      $inputFromFieldsTmpAll = $this->formDocument->find('input');
301 11
      foreach ($inputFromFieldsTmpAll as $inputFromFieldTmp) {
0 ignored issues
show
Bug introduced by
The expression $inputFromFieldsTmpAll of type array<integer,object<vok...leHtmlDomNodeInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
302 11
        if ($inputFromFieldTmp->form == $formHelperId) {
303 1
          $this->parseInputForRules($inputFromFieldTmp, $formHelperId);
304 11
          $this->parseInputForFilter($inputFromFieldTmp, $formHelperId);
305
        }
306
      }
307
    }
308
309 18
    return (\count($inputForm) >= 0);
310
  }
311
312
  /**
313
   * Determine if element has filter attributes, and save the given filter.
314
   *
315
   * @param SimpleHtmlDom $inputField
316
   * @param string        $formHelperId
317
   */
318 18
  private function parseInputForFilter(SimpleHtmlDom $inputField, string $formHelperId)
319
  {
320 18
    if (!$inputField->hasAttribute('data-filter')) {
321 18
      return;
322
    }
323
324 6
    $inputName = $inputField->getAttribute('name');
325 6
    $inputFilter = $inputField->getAttribute('data-filter');
326
327 6
    if (!$inputFilter) {
328 1
      $inputFilter = 'htmlentities';
329
    }
330
331 6
    $this->filters[$formHelperId][$inputName] = $inputFilter;
332 6
  }
333
334
335
  /**
336
   * Determine if element has validator attributes, and save the given rule.
337
   *
338
   * @param SimpleHtmlDom $inputField
339
   * @param string        $formHelperId
340
   */
341 18
  private function parseInputForRules(SimpleHtmlDom $inputField, string $formHelperId)
342
  {
343 18
    if (!$inputField->hasAttribute('data-validator')) {
344 14
      return;
345
    }
346
347 17
    $inputName = $inputField->getAttribute('name');
348 17
    $inputPattern = $inputField->getAttribute('pattern');
349 17
    $inputRule = $inputField->getAttribute('data-validator');
350
351 17
    if (strpos($inputRule, 'auto') !== false) {
352 4
      $inputType = $inputField->getAttribute('type');
353
354 4
      $inputRule = str_replace(
355 4
          'auto',
356 4
          $this->autoSelectRuleByInputType($inputType),
357 4
          $inputRule
358
      );
359
    }
360
361 17
    if ($inputPattern) {
362 1
      $inputRule .= '|regex(/' . $inputPattern . '/)';
363
    }
364
365 17
    if ($inputField->hasAttribute('required')) {
366 17
      $this->required_rules[$formHelperId][$inputName] = $inputRule;
367
    }
368
369 17
    $this->rules[$formHelperId][$inputName] = $inputRule;
370 17
  }
371
372
  /**
373
   * @param callable $translator
374
   *
375
   * @return Validator
376
   */
377 1
  public function setTranslator(callable $translator): Validator
378
  {
379 1
    $this->translator = $translator;
380
381 1
    return $this;
382
  }
383
384
  /**
385
   * Loop the form data through form rules.
386
   *
387
   * @param array $formValues
388
   * @param bool  $useNoValidationRuleException
389
   *
390
   * @throws UnknownValidationRule
391
   *
392
   * @return ValidatorResult
393
   */
394 17
  public function validate(array $formValues, $useNoValidationRuleException = false): ValidatorResult
395
  {
396
    if (
397 17
        $useNoValidationRuleException === true
398
        &&
399 17
        \count($this->rules) === 0
400
    ) {
401 1
      throw new NoValidationRule(
402 1
          'No rules defined in the html.'
403
      );
404
    }
405
406
    // init
407 16
    $validatorResult = new ValidatorResult($this->formDocument);
408
409 16
    foreach ($this->rules as $formHelperId => $formFields) {
410 15
      foreach ($formFields as $field => $fieldRuleOuter) {
411
412 15
        $currentFieldValue = $this->getCurrentFieldValue($formValues, $field);
413
414
        //
415
        // use the filter
416
        //
417
418 15
        if (isset($this->filters[$formHelperId][$field])) {
419 5
          $filtersOuter = $this->filters[$formHelperId][$field];
420 5
          $fieldFilters = preg_split("/\|+(?![^\(]*\))/", $filtersOuter);
421
422 5
          foreach ($fieldFilters as $fieldFilter) {
423
424 5
            if (!$fieldFilter) {
425
              continue;
426
            }
427
428 5
            $currentFieldValue = $this->applyFilter($currentFieldValue, $fieldFilter);
429
          }
430
        }
431
432
        //
433
        // save the new values into the result-object
434
        //
435
436 15
        $validatorResult->setValues($field, $currentFieldValue);
437
438
        //
439
        // skip validation, if there was no value and validation is not required
440
        //
441
442
        if (
443 15
            $currentFieldValue === null
444
            &&
445 15
            !isset($this->required_rules[$formHelperId][$field])
446
        ) {
447 3
          continue;
448
        }
449
450
        //
451
        // use the validation rules from the dom
452
        //
453
454 15
        $fieldRules = preg_split("/\|+(?![^\(?:]*\))/", $fieldRuleOuter);
455
456 15
        foreach ($fieldRules as $fieldRule) {
457
458 15
          if (!$fieldRule) {
459 1
            continue;
460
          }
461
462 15
          $validationClassArray = $this->validatorRulesManager->getClassViaAlias($fieldRule);
463
464 15
          if ($validationClassArray['object']) {
465 1
            $validationClass = $validationClassArray['object'];
466 15
          } else if ($validationClassArray['class']) {
467 15
            $validationClass = $validationClassArray['class'];
468
          } else {
469
            $validationClass = null;
470
          }
471
472 15
          $validationClassArgs = $validationClassArray['classArgs'] ?? null;
473
474 15
          if ($validationClass instanceof AbstractRule) {
475
476 1
            $respectValidator = $validationClass;
477
478
          } else {
479
480
            try {
481 15
              $respectValidatorFactory = new Factory();
482 15
              $respectValidatorFactory->prependRulePrefix('voku\\HtmlFormValidator\\Rules');
483
484 15
              if ($validationClassArgs !== null) {
485 6
                $respectValidator = $respectValidatorFactory->rule($validationClass, $validationClassArgs);
486
              } else {
487 15
                $respectValidator = $respectValidatorFactory->rule($validationClass);
488
              }
489
490 1
            } catch (ComponentException $componentException) {
491 1
              throw new UnknownValidationRule(
492 1
                  'No rule defined for: ' . $field . ' (rule: ' . $fieldRule . ' | class: ' . $validationClass . ')',
493 1
                  500,
494 1
                  $componentException
495
              );
496
            }
497
498
          }
499
500 14
          $hasPassed = false;
501 14
          $translator = $this->getTranslator();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $translator is correct as $this->getTranslator() (which targets voku\HtmlFormValidator\Validator::getTranslator()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
502
503
          try {
504 14
            $hasPassed = $respectValidator->assert($currentFieldValue);
505 9
          } catch (NestedValidationException $nestedValidationException) {
506
507 1
            if ($translator) {
508
              $nestedValidationException->setParam('translator', $translator);
509
            }
510
511 1
            $validatorResult->setError($field, $nestedValidationException->getFullMessage());
512 8
          } catch (ValidationException $validationException) {
513
514 8
            if ($translator) {
515 1
              $validationException->setParam('translator', $translator);
516
            }
517
518 8
            $validatorResult->setError($field, $validationException->getMainMessage());
519
          }
520
521 14
          if ($hasPassed === true) {
522 14
            continue;
523
          }
524
525
        }
526
      }
527
    }
528
529 15
    return $validatorResult;
530
  }
531
532
}
533