Completed
Push — master ( ec55c5...b062f4 )
by Lars
01:21
created

Validator::validate()   F

Complexity

Conditions 24
Paths 630

Size

Total Lines 144
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 57
CRAP Score 24.1626

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 144
ccs 57
cts 61
cp 0.9344
rs 2.248
cc 24
eloc 72
nc 630
nop 2
crap 24.1626

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 18
  public function __construct($formHTML, $selector = '')
69
  {
70 18
    $this->validatorRulesManager = new ValidatorRulesManager();
71
72 18
    $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 18
    $this->selector = $selector;
74
75 18
    $this->parseHtmlDomForRules();
76 18
  }
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 2
  public function addCustomRule(string $name, $validator)
92
  {
93 2
    $this->validatorRulesManager->addCustomRule($name, $validator);
94 2
  }
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 4
    switch ($fieldFilter) {
115
      case 'trim':
116 4
        return \trim($currentFieldData);
117
      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 3
  public function autoSelectRuleByInputType(string $type)
138
  {
139
    $matchingArray = [
140 3
        '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 3
    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 11
  public function getAllRules(): array
169
  {
170 11
    return $this->rules;
171
  }
172
173
  /**
174
   * @param array  $formValues
175
   * @param string $field
176
   *
177
   * @return mixed|null
178
   */
179 14
  private function getCurrentFieldValue(array $formValues, string $field)
180
  {
181 14
    $fieldArrayPos = UTF8::strpos($field, '[');
182 14
    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 11
      $currentFieldValue = $formValues[$field] ?? null;
229
    }
230
231 14
    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 13
  public function getTranslator()
256
  {
257 13
    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 18
  public function parseHtmlDomForRules(): bool
266
  {
267
    // init
268 18
    $this->rules = [];
269 18
    $inputForm = [];
270
271 18
    if ($this->selector) {
272 2
      $forms = $this->formDocument->find($this->selector);
273
    } else {
274 16
      $forms = $this->formDocument->find('form');
275
    }
276
277 18
    if (\count($forms) === 0) {
278 1
      return false;
279
    }
280
281
    // get the first form
282 17
    $form = $forms[0];
283
284
    // get the from-id
285 17
    if ($form->id) {
286 10
      $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 17
    $inputFromFields = $form->getElementsByTagName('input');
293 17
    foreach ($inputFromFields as $inputFormField) {
294 17
      $this->parseInputForRules($inputFormField, $formHelperId);
295 17
      $this->parseInputForFilter($inputFormField, $formHelperId);
296
    }
297
298
    // get the <input>-elements with a matching form="id"
299 17
    if (\strpos($formHelperId, 'html-form-validator-tmp') !== 0) {
300 10
      $inputFromFieldsTmpAll = $this->formDocument->find('input');
301 10
      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 10
        if ($inputFromFieldTmp->form == $formHelperId) {
303 1
          $this->parseInputForRules($inputFromFieldTmp, $formHelperId);
304 10
          $this->parseInputForFilter($inputFromFieldTmp, $formHelperId);
305
        }
306
      }
307
    }
308
309 17
    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 17
  private function parseInputForFilter(SimpleHtmlDom $inputField, string $formHelperId)
319
  {
320 17
    if (!$inputField->hasAttribute('data-filter')) {
321 17
      return;
322
    }
323
324 6
    $inputName = $inputField->getAttribute('name');
325 6
    $inputFilter = $inputField->getAttribute('data-filter');
326
327 6
    if ($inputFilter === 'auto') {
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 17
  private function parseInputForRules(SimpleHtmlDom $inputField, string $formHelperId)
342
  {
343 17
    if (!$inputField->hasAttribute('data-validator')) {
344 14
      return;
345
    }
346
347 16
    $inputName = $inputField->getAttribute('name');
348 16
    $inputRule = $inputField->getAttribute('data-validator');
349
350 16
    if ($inputRule === 'auto') {
351 3
      $inputType = $inputField->getAttribute('type');
352 3
      $inputRule = $this->autoSelectRuleByInputType($inputType);
353
    }
354
355 16
    if ($inputField->hasAttribute('required')) {
356 16
      $this->required_rules[$formHelperId][$inputName] = $inputRule;
357
    }
358
359 16
    $this->rules[$formHelperId][$inputName] = $inputRule;
360 16
  }
361
362
  /**
363
   * @param callable $translator
364
   *
365
   * @return Validator
366
   */
367 1
  public function setTranslator(callable $translator): Validator
368
  {
369 1
    $this->translator = $translator;
370
371 1
    return $this;
372
  }
373
374
  /**
375
   * Loop the form data through form rules.
376
   *
377
   * @param array $formValues
378
   * @param bool  $useNoValidationRuleException
379
   *
380
   * @throws UnknownValidationRule
381
   *
382
   * @return ValidatorResult
383
   */
384 16
  public function validate(array $formValues, $useNoValidationRuleException = false): ValidatorResult
385
  {
386
    if (
387 16
        $useNoValidationRuleException === true
388
        &&
389 16
        \count($this->rules) === 0
390
    ) {
391 1
      throw new NoValidationRule(
392 1
          'No rules defined in the html.'
393
      );
394
    }
395
396
    // init
397 15
    $validatorResult = new ValidatorResult($this->formDocument);
398
399 15
    foreach ($this->rules as $formHelperId => $formFields) {
400 14
      foreach ($formFields as $field => $fieldRuleOuter) {
401
402 14
        $currentFieldValue = $this->getCurrentFieldValue($formValues, $field);
403
404
        //
405
        // use the filter
406
        //
407
408 14
        if (isset($this->filters[$formHelperId][$field])) {
409 5
          $filtersOuter = $this->filters[$formHelperId][$field];
410 5
          if (\strpos($filtersOuter, '|') !== false) {
411 4
            $fieldFilters = \explode('|', $filtersOuter);
412
          } else {
413 4
            $fieldFilters = (array)$filtersOuter;
414
          }
415
416 5
          foreach ($fieldFilters as $fieldFilter) {
417
418 5
            if (!$fieldFilter) {
419
              continue;
420
            }
421 5
            $currentFieldValue = $this->applyFilter($currentFieldValue, $fieldFilter);
422
          }
423
        }
424
425
        //
426
        // save the new values into the result-object
427
        //
428
429 14
        $validatorResult->setValues($field, $currentFieldValue);
430
431
        //
432
        // skip validation, if there was no value and validation is not required
433
        //
434
435
        if (
436 14
            $currentFieldValue === null
437
            &&
438 14
            !isset($this->required_rules[$formHelperId][$field])
439
        ) {
440 3
          continue;
441
        }
442
443
        //
444
        // use the validation rules from the dom
445
        //
446
447 14
        if (\strpos($fieldRuleOuter, '|') !== false) {
448 7
          $fieldRules = \explode('|', $fieldRuleOuter);
449
        } else {
450 10
          $fieldRules = (array)$fieldRuleOuter;
451
        }
452
453 14
        foreach ($fieldRules as $fieldRule) {
454
455 14
          if (!$fieldRule) {
456
            continue;
457
          }
458
459 14
          $validationClassArray = $this->validatorRulesManager->getClassViaAlias($fieldRule);
460
461 14
          if ($validationClassArray['object']) {
462 1
            $validationClass = $validationClassArray['object'];
463 14
          } else if ($validationClassArray['class']) {
464 14
            $validationClass = $validationClassArray['class'];
465
          } else {
466
            $validationClass = null;
467
          }
468
469 14
          $validationClassArgs = $validationClassArray['classArgs'] ?? null;
470
471 14
          if ($validationClass instanceof AbstractRule) {
472
473 1
            $respectValidator = $validationClass;
474
475
          } else {
476
477
            try {
478 14
              $respectValidatorFactory = new Factory();
479 14
              $respectValidatorFactory->prependRulePrefix('voku\\HtmlFormValidator\\Rules');
480
481 14
              if ($validationClassArgs !== null) {
482 4
                $respectValidator = $respectValidatorFactory->rule($validationClass, $validationClassArgs);
483
              } else {
484 14
                $respectValidator = $respectValidatorFactory->rule($validationClass);
485
              }
486
487 1
            } catch (ComponentException $componentException) {
488 1
              throw new UnknownValidationRule(
489 1
                  'No rule defined for: ' . $field . ' (rule: ' . $fieldRule . ' | class: ' . $validationClass . ')',
490 1
                  500,
491 1
                  $componentException
492
              );
493
            }
494
495
          }
496
497 13
          $hasPassed = false;
498 13
          $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...
499
500
          try {
501 13
            $hasPassed = $respectValidator->assert($currentFieldValue);
502 8
          } catch (NestedValidationException $nestedValidationException) {
503
504 1
            if ($translator) {
505
              $nestedValidationException->setParam('translator', $translator);
506
            }
507
508 1
            $validatorResult->setError($field, $nestedValidationException->getFullMessage());
509 7
          } catch (ValidationException $validationException) {
510
511 7
            if ($translator) {
512 1
              $validationException->setParam('translator', $translator);
513
            }
514
515 7
            $validatorResult->setError($field, $validationException->getMainMessage());
516
          }
517
518 13
          if ($hasPassed === true) {
519 13
            continue;
520
          }
521
522
        }
523
      }
524
    }
525
526 14
    return $validatorResult;
527
  }
528
529
}
530