Completed
Push — master ( 5ee37f...7e12c1 )
by Romans
05:10 queued 01:16
created

Controller_Validator_Abstract::rule_caption()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
/**
3
 * Abstract Validator implements the low-level requirements of
4
 * a validator integrated into Agile Toolkit. In normal conditions
5
 * you should use 'Validator' class which extends 'Validator_Advanced'.
6
 *
7
 * If you are writing your own validator rules to a very specific
8
 * objects, you can use extend either this class or Validator_Basic.
9
 *
10
 * NOTE: How to write rules:
11
 *  Do not attempt to put all the rules for a single field on a line.
12
 * Many rules will change the value of acumulator, so please add
13
 * as many rules as necessary
14
 *
15
 * is(array(
16
 *   'name|len|gt|20',
17
 *   'name|!rude',
18
 *   'name|to_ucfirst',
19
 *   ));
20
 *
21
 * Finally - you can add one validator inside another to extends
22
 * it's rules.
23
 */
24
class Controller_Validator_Abstract extends \AbstractController
25
{
26
    /**
27
     * Each ruleset is broken down by field and is stored in this
28
     * array in a normal form. You can get rules for a particular
29
     * field by calling $this->getRules(field);.
30
     */
31
    public $rules = array();
32
33
    public $default_exception = 'Exception_ValidityCheck';
34
35
    /**
36
     * This is a static array which is expanded inside extending
37
     * classes. Extend this inside your validator's init method:.
38
     *
39
     *   $alias['mandatory']='required';
40
     *   $alias['must_have']='required';
41
     */
42
    public $alias = array();
43
44
    /**
45
     * Validator can check either a model, array, form or any other
46
     * object as long as it supports array_access. If you are using
47
     * Model then you can use some additional functionality foreach
48
     * selecting fields.
49
     */
50
    public $source = null;
51
52
    /**
53
     * Name of the field which is currently beind processed.
54
     */
55
    public $active_field;
56
57
    /**
58
     * When transformivg value (such as len) - will contain a
59
     * modifier to the error message.
60
     */
61
    public $prefix = '';
62
63
    public $caption = '';
64
65
    // {{{ Initialization method
66
    public function init()
67
    {
68
        parent::init();
69
        $that = $this;
70
71
        if ($this->owner instanceof Controller_Validator) {
72
            $this->owner->addHook('extraRules', $this);
0 ignored issues
show
Documentation introduced by
$this is of type this<Controller_Validator_Abstract>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
73
74
            return;  // no source, simply extend rules.
75
        }
76
77
        if ((
78
            $this->owner instanceof \Model ||
79
            $this->owner instanceof \Form
80
81
        ) && !$this->owner->hasMethod('is')) {
82
            $this->source = $this->owner; // must support set/get interface
83
            $this->owner->validator = $this;
84
85
            $this->source->addMethod('is', function ($m) use ($that) {
86
                $args = func_get_args();
87
                array_shift($args);
88
89
                call_user_func_array(array($that, 'is'), $args);
90
                $that->on('beforeSave', null, true);
91
92
                return $m;
93
            });
94
        }
95
    }
96
    // }}}
97
98
    // {{{ Rule initialization and normalization methods
99
    // ^^ do not remove - that's a fold in VIM, starts section
100
101
    /**
102
     * This method will go through all the rules you specify, expand
103
     * and normalize them and assign into array indexed by field name.
104
     *
105
     * You do not need to have your fields defined at this point, unless
106
     * you specify wildcards.
107
     *
108
     * This method takes various arguments as described in documentation.
109
     */
110
    public function is()
111
    {
112
        $args = func_get_args();
113
114
        // If only first argument is specified, then it's array of rulesets.
115
        // We will call ourselves with every element.
116
        if (count($args) == 1 && is_array($args[0])) {
117
            foreach ($args[0] as $ruleset) {
118
                // $ruleset here is either array or string with pipes
119
                if (!is_array($ruleset)) {
120
                    $ruleset = array($ruleset);
121
                }
122
                call_user_func_array(array($this, 'is'), $ruleset);
123
            }
124
125
            return $this;
126
        }
127
128
        // If ruleset is specified as a string, we need to expand it
129
        // into an array.
130
        if (count($args) == 1) {
131
            list($field_definition, $rules) = $this->normalizeRules($args[0]);
132
        } else {
133
            $rules = $args;
134
            $field_definition = array_shift($rules);
135
        }
136
137
        // Convert field defintion into list of fields
138
        $fields = $this->expandFieldDefinition($field_definition, $rules);
139
140
        // Save rules for each field
141
        foreach ($fields as $field) {
142
            $this->rules[$field][] = $rules;
143
        }
144
145
        return $this;
146
    }
147
148
    /**
149
     * If you are adding this Controller inside a Model, you don't need to
150
     * set source. If you want controller to work with an array or some other
151
     * object, use setSource().
152
     */
153
    public function setSource($source)
154
    {
155
        $this->source = $source;
156
    }
157
158
    /**
159
     * Provided with string containing rules, this will convert it into
160
     * normal (array) form.
161
     *
162
     * In: "int|required|alphanum|save"  (Basic)
163
     * In: "int!|a-z|"                   (Advanced)
164
     * Out: array('int','required','alphanum','save')
165
     */
166
    public function normalizeRules($rules)
167
    {
168
        // If you want to use a pipe in a regex, custom message etc,
169
        // single-quote the string (escaping would be too confusing in regexes):
170
        //
171
        // This works with:
172
        //
173
        // 'foo?\'my piped | string\''
174
        // "foo?'my piped | string'"
175
        //
176
        // BIG NOTE: There is a reason why there are 2 formats. I don't
177
        // want developres to use ONLY the pipe format. There is always
178
        // multi-argument format, where argument can be anything, and
179
        // we don't complicate things and try to get around regexps
180
        //
181
        // is('name|required?my pipe|string')       // Bad
182
        // is('name','required?my pipe|string')     // Good
183
        // is('name','required?','my pipe|string')  // Best
184
185
        // TODO: clean up
186
        $rules = preg_split('/[|,:]/', $rules);
187
        $field = array_shift($rules);
188
189
        return array($field, $rules);
190
    }
191
192
    /**
193
     * Provided with a multiple field definition, this will convert
194
     * them into an array.
195
     *
196
     * In: "name,surname,foo"        (Basic)
197
     * In: "%boolean,-@address"      (Advanced)
198
     * Out: array('name','surname','foo')
199
     */
200
    public function expandFieldDefinition($field_definition, &$normalized_rules)
201
    {
202
        return explode(',', $field_definition);
203
    }
204
205
    // }}}
206
207
    // {{{ Supplimentary configuration methods
208
    /**
209
     * Call this to get list of parsed rules for specified field.
210
     */
211
    public function getRules($field)
212
    {
213
        return $this->rules[$field];
214
    }
215
216
    /**
217
     * TODO: find these comments very difficult to understand!
218
     *
219
     * Call this to set a different hook when rules are going to be
220
     * applied. By default you have to call now()
221
     *
222
     * on() used by default for when validator is added, however if
223
     * you call it manually (avoiding 3rd argument) it will override
224
     * the default hook. This is done to avoid double-validation
225
     */
226
    public $custom_hook = false;
227
    public function on($hook, $object = null, $default_hook = false)
228
    {
229
        if (!$object) {
230
            $object = $this->owner;
231
        }
232
        if (!$default_hook) {
233
            $this->custom_hook = true;
234
        }
235
236
        $this->has_hook = true;
0 ignored issues
show
Bug introduced by
The property has_hook does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
237
        $that = $this;
238
239
        $object->addHook($hook, function ($m) use ($default_hook, $that) {
0 ignored issues
show
Unused Code introduced by
The parameter $m is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
240
            if ($that->custom_hook && $default_hook) {
241
                return;
242
            }
243
            $that->applyRulesets();
244
        });
245
    }
246
247
    /**
248
     * Apply rules now.
249
     */
250
    public function now()
251
    {
252
        return $this->applyRulesets();
253
    }
254
255
    // }}}
256
257
    // {{{ Methods which are essential when applying rules
258
    /**
259
     * Get list of fields which we are going to validate. In some cases
260
     * it makes no sense to validate fields which are not appearing individually
261
     * the form, therefore this method will look carefully at what you are
262
     * validating.
263
     */
264
    public function getActualFields()
265
    {
266
        return array_keys($this->rules);
267
    }
268
269
    /**
270
     * Go through the list of defined rules and call the corresponding
271
     * filters and convertors.
272
     */
273
    public function applyRulesets()
274
    {
275
        // List of fields which actually need validation at this time.
276
        $fields = $this->getActualFields();
277
278
        foreach ($fields as $field) {
279
            $rulesets = $this->getRules($field);
280
            $this->active_field = $field;
281
            $this->prefix = '';
282
            $this->caption = '';
283
            foreach ($rulesets as $rules){
284
285
                $this->applyRules($field, $rules);
286
            }
287
        }
288
289
        return $this;
290
    }
291
292
    /**
293
     * Pulls next rule out of the rule stack (current_ruleset)
294
     * May allow alias ($name).
295
     */
296
    public function pullRule($alias = false)
297
    {
298
        $v = array_shift($this->current_ruleset);
299
        if ($alias && $v[0] == '$') {
300
            $v = $this->get(substr($v, 1));
301
        }
302
303
        return $this->consumed[] = $v;
304
    }
305
306
    /**
307
     * Adds new rule into a rule-set, which will be executed next.
308
     * You can specify single or multiple rules, this method accepts
309
     * variable arguments.
310
     *
311
     * Rules must be normalized.
312
     */
313
    public function pushRule()
314
    {
315
        $args = func_get_args();
316
317
        // TODO: this can probably be done by args+current_ruleset
318
        foreach (array_reverse($args) as $arg) {
319
            array_unshift($this->current_ruleset, $arg);
320
        }
321
    }
322
323
    /**
324
     * Returns the original value of the field.
325
     */
326
    public function get($field)
327
    {
328
        return $this->source[$field];
329
    }
330
331
    /**
332
     * Retuns field name of rule chain
333
     * being processed.
334
     *
335
     * Second argument to rule_ is field, there are no need for this method
336
     */
337
    /*
338
    function get_active_field()
339
    {
340
        return $this->active_field;
341
    }
342
     */
343
344
    /**
345
     * Changes the original value of the field (for normalization).
346
     */
347
    public function set($field, $value)
348
    {
349
        $this->source[$field] = $value;
350
351
        return $this;
352
    }
353
354
    public function resolveRuleAlias($rule)
355
    {
356
        if (isset($this->alias[$rule])) {
357
            $rule = $this->alias[$rule];
358
        }
359
360
        // Only rule names are passed here,
361
        // not args, so a comma could only be
362
        // a custom message.
363
364
        // TODO: but what about array validation?
365
        // Probably a rare edge case, but we
366
        // should mention it in the docs??
367
368
        if (strpos($rule, '?') !== false) {
369
            list($rule, $error) = explode('?', $rule, 2);
370
371
            // Trim off any leading quote from from
372
            // the error message
373
            $this->custom_error = preg_replace('/^\'/', '', $error);
374
        }
375
376 View Code Duplication
        if (substr($rule, 0, 2) === '>=') {
377
            $this->pushRule(substr($rule, 2));
378
379
            return 'gte';
380
        }
381
382 View Code Duplication
        if (substr($rule, 0, 2) === '<=') {
383
            $this->pushRule(substr($rule, 2));
384
385
            return 'lte';
386
        }
387
388 View Code Duplication
        if (substr($rule, 0, 1) === '>') {
389
            $this->pushRule(substr($rule, 1));
390
391
            return 'gt';
392
        }
393
394 View Code Duplication
        if (substr($rule, 0, 1) === '<') {
395
            $this->pushRule(substr($rule, 1));
396
397
            return 'lt';
398
        }
399
400 View Code Duplication
        if (substr($rule, 0, 2) === '!=') {
401
            $this->pushRule(substr($rule, 1));
402
403
            return 'ne';
404
        }
405
406 View Code Duplication
        if (substr($rule, 0, 1) === '=') {
407
            $this->pushRule(substr($rule, 1));
408
409
            return 'eqf';
410
        }
411
        if (substr($rule, 0, 1) === '[') {
412
            $this->pushRule($rule);
413
414
            return 'regex';
415
        }
416
        if (substr($rule, 0, 1) === '$') {
417
            $this->get(substr($rule, 1));
0 ignored issues
show
Unused Code introduced by
The call to the method Controller_Validator_Abstract::get() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
418
        }
419
420
        return $rule;
421
    }
422
    // }}}
423
424
    // {{{ Methods which are called by the rules
425
    public function fail()
426
    {
427
        $args = func_get_args();
428
        $str = ucfirst($this->prefix.($this->caption?:$this->active_field).' '.lcfirst(array_shift($args)));
429
430
        // Insert any args into placeholders
431
432
        if (count($args) > 0) {
433
            $n = 1;
434
435
            foreach ($args as $arg) {
436
                $tag = sprintf('{{arg%s}}', $n);
437
                $str = str_replace($tag, $arg, $str);
438
                ++$n;
439
            }
440
        }
441
442
        throw $this->exception($this->custom_error ?: $str)
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class BaseException as the method setField() does only exist in the following sub-classes of BaseException: Exception_ValidityCheck. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
443
            ->setField($this->active_field);
444
    }
445
446
    public function stop()
447
    {
448
        $this->bail_out = true;
449
    }
450
    // }}}
451
452
    // {{{ Main rule application loop
453
454
    // Next are system fields, do not access when in doubt.
455
    public $acc = null;
456
    public $consumed = array();
457
    public $current_ruleset = null;
458
    public $custom_error = null;
459
    public $bail_out = false;
460
461
    /**
462
     * This is the main body for rule processing.
463
     */
464
    public function applyRules($field, $ruleset)
465
    {
466
        // Save previous values, just in case
467
        $acc = $this->acc;
468
        $crs = $this->current_ruleset;
469
        $this->bail_out = false;
470
471
        $this->acc = $this->get($field);
472
        $this->current_ruleset = $ruleset;
473
474
        while (!is_null($rule = $this->pullRule())) {
475
            $this->cast = false;
0 ignored issues
show
Bug introduced by
The property cast does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
476
            $this->custom_error = null;
477
478
            if ($rule == 'required') {
479
                $is_required = true;
0 ignored issues
show
Unused Code introduced by
$is_required is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
480
            }
481
482
            // For debugging
483
            $tmp = null;
484
            $this->consumed = array($rule);
485
486
            try {
487
                if ((is_object($rule) || is_array($rule)) && is_callable($rule)) {
488
                    $tmp = $rule($this, $this->acc, $field);
489
                } else {
490
                    // For to_XX rules
491
                    if (substr($rule, 0, 3) == 'to_') {
492
                        if (!$this->hasMethod('rule_'.$rule)) {
493
                            $rule = substr($rule, 3);
494
                        }
495
496
                        $this->cast = true;
497
                    }
498
499
                    if ($rule === '') {
500
                        if ($this->cast) {
501
                            $this->set($field, $this->acc);
502
                        }
503
                        continue;
504
                    }
505
506
                    $rule = $this->resolveRuleAlias($rule);
507
508
                    $tmp = $this->{'rule_'.$rule}($this->acc, $field);
509
                }
510
511 View Code Duplication
                if ($this->debug) {
512
                    echo "<font color=blue>rule_$rule({$this->acc},".
513
                    implode(',', $this->consumed).")=$tmp</font><br/>";
514
                }
515
516
                if (!is_null($tmp)) {
517
                    $this->acc = $tmp;
518
                }
519
                if ($this->cast) {
520
                    $this->set($field, $tmp);
521
                }
522
                if ($this->bail_out) {
523
                    break;
524
                }
525
            } catch (\Exception_ValidityCheck $e) {
526 View Code Duplication
                if ($this->debug) {
527
                    echo "<font color=red>rule_$rule({$this->acc},".
528
                        implode(',', $this->consumed).') failed</font><br/>';
529
                }
530
                $this->acc = $acc;
531
                $this->current_ruleset = $crs;
532
                throw $e
533
                    ->setField($field)
534
                    ->addMoreInfo('val', $this->acc)
535
                    ->addMoreInfo('rule', $rule);
536
            }
537
        }
538
        $this->acc = $acc;
539
        $this->current_ruleset = $crs;
540
    }
541
    // }}}
542
543
    function rule_caption($a)
544
    {
545
        $this->caption = $this->pullRule();
546
        return $a;
547
    }
548
549
    /**
550
     * The only rule in Validator_Abstract. Will fail.
551
     */
552
    public function rule_fail()
553
    {
554
        return $this->fail('Is incorrect');
555
    }
556
}
557