Component::maxLength()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
cc 2
eloc 3
nc 2
nop 2
crap 2
1
<?php
2
3
namespace BootPress\Validator;
4
5
use BootPress\Page\Component as Page;
6
7
class Component
8
{
9
    
10
    /**
11
     * Custom validation rules that you would like to apply.
12
     * 
13
     * @var callable[]
14
     */
15
    public $rules = array();
16
    
17
    
18
    /**
19
     * Before you check if ``$this->certified()``, these are the error messages associated with each validation rule.  After ``$this->certified()``, these are all the errors we encountered (if any).  You can customize and add as you see fit.
20
     * 
21
     * @var string[]
22
     */
23
    public $errors = array(
24
        'remote' => 'Please fix this field.',
25
        'required' => 'This field is required.',
26
        'equalTo' => 'Please enter the same value again.',
27
        'notEqualTo' => 'Please enter a different value, values must not be the same.',
28
        'number' => 'Please enter a valid number.',
29
        'integer' => 'A positive or negative non-decimal number please.',
30
        'digits' => 'Please enter only digits.',
31
        'min' => 'Please enter a value greater than or equal to {0}.',
32
        'max' => 'Please enter a value less than or equal to {0}.',
33
        'range' => 'Please enter a value between {0} and {1}.',
34
        'alphaNumeric' => 'Letters, numbers, and underscores only please.',
35
        'minLength' => 'Please enter at least {0} characters.',
36
        'maxLength' => 'Please enter no more than {0} characters.',
37
        'rangeLength' => 'Please enter a value between {0} and {1} characters long.',
38
        'minWords' => 'Please enter at least {0} words.',
39
        'maxWords' => 'Please enter {0} words or less.',
40
        'rangeWords' => 'Please enter between {0} and {1} words.',
41
        'pattern' => 'Invalid format.',
42
        'date' => 'Please enter a valid date.',
43
        'email' => 'Please enter a valid email address.',
44
        'url' => 'Please enter a valid URL.',
45
        'ipv4' => 'Please enter a valid IP v4 address.',
46
        'ipv6' => 'Please enter a valid IP v6 address.',
47
        'inList' => 'Please make a valid selection.',
48
        'noWhiteSpace' => 'No white space please.',
49
    );
50
    
51
    /**
52
     * This is where we save all of the information ``$this->set()``ed for each field.
53
     * 
54
     * @var array
55
     */
56
    protected $data = array();
57
    
58
    /**
59
     * These are the user submitted values for each field.
60
     * 
61
     * @var array
62
     */
63
    protected $values = array();
64
    
65
    /**
66
     * Whether the form has been submitted or not.  Null if we don't know.
67
     * 
68
     * @var null|bool
69
     */
70
    protected $submitted = null;
71
    
72
    /**
73
     * Either false or an array of all the submitted values.
74
     * 
75
     * @var false|array
76
     */
77
    protected $certified = false;
78
    
79
    /**
80
     * The rules we reserve for validation until the end.
81
     * 
82
     * @var string[]
83
     */
84
    protected $reserved = array('default', 'required', 'equalTo', 'notEqualTo');
85
    
86
    /**
87
     * The rules we define in-house.
88
     * 
89
     * @var string[]
90
     */
91
    protected $methods = array('number', 'integer', 'digits', 'min', 'max', 'range', 'alphaNumeric', 'minLength', 'maxLength', 'rangeLength', 'minWords', 'maxWords', 'rangeWords', 'pattern', 'date', 'email', 'url', 'ipv4', 'ipv6', 'inList', 'yesNo', 'trueFalse', 'noWhiteSpace', 'singleSpace');
92
    
93
    /**
94
     * So that we have something to work with no matter what happens to ``$this->errors`` (anything can happen) public property.
95
     * 
96
     * @var string[]
97
     */
98
    protected $default_errors = array();
99
100
    /**
101
     * Creates a BootPress\Validator\Component object.
102
     * 
103
     * @param array $values The user submitted variables you want to validate.
104
     * 
105
     * ```php
106
     * $validator = new Validator($_POST);
107
     * ```
108
     */
109 19
    public function __construct(array $values = array())
110
    {
111 19
        $this->values = $values;
112 19
        $this->default_errors = $this->errors;
113 19
    }
114
115
    /**
116
     * This allows you to set individually (or all at once) the validation rules and filters for each form field.  The value of every $field you set here is automatically ``trim()``ed and returned when ``$this->submittted()``.
117
     * 
118
     * @param string $field The name of your form field.  If this is an ``array($field => $rules, $field, ...)`` then we loop through each one and call this method ourselves over and over.
119
     * 
120
     * Your $field names can be an array by adding brackets to the end ie. 'name[]'.  They can also be multi-dimensional arrays such as 'name[first]', or 'name[players][]', or 'name[parent][child]', etc.  The important thing to remember is that you must always use the exact name given here when referencing them in other methods.
121
     * 
122
     * @param string|array $rules A pipe delimited set (or an array) of rules to validate and filter the $field with.  You can also specify custom messages by making this an ``array($rule => $message, ...)``.  Parameters are comma-delimited, and placed within '**[]**' two brackets.  The available options are:
123
     *
124
     * - '**remote[rule]**' - Set ``$form->rules['rule'] = function($value){}`` to determine the validity of a submitted value.  The function should return a boolean true or false.
125
     * - '**default**' - A default value if the field is empty, or not even set.
126
     * - '**required**' - This field must have a value, and cannot be empty.
127
     * - '**equalTo[field]**' - Must match the same value as contained in the other form field.
128
     * - '**notEqualTo[field]**' - Must NOT match the same value as contained in the other form field.
129
     * - Numbers:
130
     *   - '**number**' - Must be a valid decimal number, positive or negative, integer or float, commas okay.  Defaults to 0.
131
     *   - '**integer**' - Must be a postive or negative integer number, no commas.  Defaults to 0.
132
     *   - '**digits**' - Must be a positive integer number, no commas.  Defaults to 0.
133
     *   - '**min[number]**' - Must be greater than or equal to [number].
134
     *   - '**max[number]**' - Must be less than or equal to [number].
135
     *   - '**range[min, max]**' - Must be greater than or equal to [min], and less than or equal to [max].
136
     * - Strings:
137
     *   - '**alphaNumeric**' - Alpha (a-z), numeric (0-9), and underscore (_) characters only.
138
     *   - '**minLength[integer]**' - String length must be greater than or equal to [integer].
139
     *   - '**maxLength[integer]**' - String length must be less than or equal to [integer].
140
     *   - '**rangeLength[minLength, maxLength]**' - String length must be greater than or equal to [minLength], and less than or equal to [maxLength].
141
     *   - '**minWords[integer]**' - Number of words must be greater than or equal to [integer].
142
     *   - '**maxWords[integer]**' - Number of words must be less than or equal to [integer].
143
     *   - '**rangeWords[minWords, maxWords]**' - Number of words must be greater than or equal to [minWords], and less than or equal to [maxWords].
144
     *   - '**pattern[regex]**' - Must match the supplied [ECMA Javascript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) compatible [regex].
145
     *   - '**date**' - Must be a valid looking date.  No particular format is enforced.
146
     *   - '**email**' - Must be a valid looking email.
147
     *   - '**url**' - Must be a valid looking url.
148
     *   - '**ipv4**' - Must be a valid looking ipv4 address.
149
     *   - '**ipv6**' - Must be a valid looking ipv6 address.
150
     *   - '**inList[1,2,3]**' - Must be one of a comma-separated list of acceptable values.
151
     *   - '**noWhiteSpace**' - Must contain no white space.
152
     * - Filters:
153
     *   - '**singleSpace**' - Removes any doubled-up whitespace so that you only have single spaces between words.
154
     *   - '**trueFalse**' - Returns a **1** (true) or **0** (false) integer.
155
     *   - '**yesNo**' - Returns a '**Y**' or '**N**' value.
156
     * 
157
     * ```php
158
     * $validator->set('name', 'required');
159
     * 
160
     * $validator->set('email', 'required|email');
161
     * 
162
     * $validator->set(array(
163
     *     'password' => 'required|alphaNumeric|minLength[5]|noWhiteSpace',
164
     *     'confim' => array('required', 'matches[password]'),
165
     * ));
166
     * 
167
     * $validator->set('field', array('required' => 'Do this or else.'));
168
     * ```
169
     */
170 11
    public function set($field, $rules = '')
171
    {
172 11
        if (is_array($field)) {
173 1
            foreach ($field as $name => $rules) {
174 1
                if (is_numeric($name) && is_string($rules)) {
175 1
                    $this->set($rules);
176 1
                } else {
177 1
                    $this->set($name, $rules);
178
                }
179 1
            }
180
181 1
            return;
182
        }
183 11
        $page = Page::html();
184 11
        $process = (is_array($rules)) ? $rules : array_map('trim', explode('|', $rules));
185 11
        $custom = array();
186 11
        $rules = array();
187 11
        foreach ($process as $rule => $message) {
188 11
            if (is_numeric($rule)) {
189 7
                $rule = $message;
190 7
                $message = false;
191 7
            }
192 11
            $param = true;
193 11
            if (preg_match("/(.*?)\[(.*)\]/", $rule, $match)) {
194 4
                $rule = $match[1];
195 4
                $param = $match[2];
196 4
                if (strpos($param, ',') !== false) {
197 4
                    $param = array_map('trim', explode(',', $param));
198 4
                }
199 4
            }
200 11
            if ($message) {
201 5
                $custom[$rule] = $message;
202 5
            }
203 11
            $rules[$rule] = $param;
204 11
        }
205 11
        $validate = array();
206 11
        foreach ($rules as $rule => $param) {
207
            switch ($rule) {
208 11
                case 'remote':
209 1
                    if ($value = $page->get($this->base($field))) {
210 1
                        return $page->sendJson($this->remote($value, $param));
211
                    }
212 1
                    $validate['remote'] = $page->url();
213 1
                    break;
214 11
                case 'required':
215 9
                    $validate['required'] = 'true';
216 9
                    break;
217 5
                case 'equalTo':
218 1
                    $validate['equalTo'] = '#'.$this->id($param);
219 1
                    break;
220 5
                case 'notEqualTo':
221 1
                    $validate['notEqualTo'] = '#'.$this->id($param);
222 1
                    break;
223 5
                case 'number':
224 1
                    $validate['number'] = 'true';
225 1
                    $rules['default'] = 0;
226 1
                    break;
227 5
                case 'integer':
228 1
                    $validate['integer'] = 'true';
229 1
                    $rules['default'] = 0;
230 1
                    break;
231 5
                case 'digits':
232 1
                    $validate['digits'] = 'true';
233 1
                    $rules['default'] = 0;
234 1
                    break;
235 5
                case 'min':
236 1
                    $validate['min'] = $param;
237 1
                    break;
238 5
                case 'max':
239 1
                    $validate['max'] = $param;
240 1
                    break;
241 5
                case 'range':
242 1
                    $validate['range'] = $param;
243 1
                    break;
244 5
                case 'alphaNumeric':
245 1
                    $validate['alphanumeric'] = 'true';
246 1
                    break;
247 5
                case 'minLength':
248 1
                    $validate['minlength'] = $param;
249 1
                    break;
250 5
                case 'maxLength':
251 1
                    $validate['maxlength'] = $param;
252 1
                    break;
253 5
                case 'rangeLength':
254 1
                    $validate['rangelength'] = $param;
255 1
                    break;
256 5
                case 'minWords':
257 1
                    $validate['minWords'] = $param;
258 1
                    break;
259 5
                case 'maxWords':
260 1
                    $validate['maxWords'] = $param;
261 1
                    break;
262 5
                case 'rangeWords':
263 1
                    $validate['rangeWords'] = $param;
264 1
                    break;
265 5
                case 'pattern':
266 1
                    $validate['pattern'] = $param;
267 1
                    break; // must use javascript ecma regex
268 5
                case 'date':
269 1
                    $validate['date'] = 'true';
270 1
                    break;
271 5
                case 'email':
272 1
                    $validate['email'] = 'true';
273 1
                    break;
274 5
                case 'url':
275 1
                    $validate['url'] = 'true';
276 1
                    break;
277 5
                case 'ipv4':
278 1
                    $validate['ipv4'] = 'true';
279 1
                    break;
280 5
                case 'ipv6':
281 1
                    $validate['ipv6'] = 'true';
282 1
                    break;
283 5
                case 'inList':
284 4
                    if (!is_array($param)) {
285 1
                        $param = array($param);
286 1
                        $rules[$rule] = $param;
287 1
                    }
288 4
                    $validate['inList'] = implode(',', $param);
289
                    // json string arrays do not play nicely with data-rule-... attributes
290 4
                    $page->jquery('jQuery.validator.addMethod("inList", function(value, element, params) { return this.optional(element) || $.inArray(value, params.split(",")) !== -1; }, "Please make a valid selection.");');
291 4
                    break;
292 3
                case 'noWhiteSpace':
293 1
                    $validate['nowhitespace'] = 'true';
294 1
                    break;
295 3
                case 'trueFalse':
296 1
                    $rules['default'] = 0;
297 1
                    break;
298 3
                case 'yesNo':
299 3
                    $rules['default'] = 'N';
300 3
                    break;
301
            }
302 11
        }
303 11
        foreach ($validate as $rule => $param) {
304 10
            if (is_array($param)) {
305 1
                $validate[$rule] = implode(',', $param);
306 1
            }
307 11
        }
308 11
        $messages = array();
309 11
        foreach ($rules as $rule => $param) {
310 11
            if (isset($custom[$rule]) || isset($this->errors[$rule])) {
311 10
                $error = (isset($custom[$rule])) ? $custom[$rule] : $this->errors[$rule];
312 10
                if (!isset($this->default_errors[$rule]) || $this->default_errors[$rule] != $error) {
313 5
                    $messages[$rule] = $error;
314 5
                }
315 10
            }
316 11
        }
317 11
        $indexes = array();
318 11
        sscanf($field, '%[^[][', $indexes[0]);
319 11
        $value = null;
320 11
        if ((bool) preg_match_all('/\[(.*?)\]/', $field, $matches)) {
321 2
            foreach ($matches[1] as $key) {
322 2
                if ($key !== '') {
323 1
                    $indexes[] = $key;
324 1
                }
325 2
            }
326 2
            $value = $this->reduce($this->values, $indexes);
327 11
        } elseif (isset($this->values[$field])) {
328 1
            $value = $this->values[$field];
329 1
        }
330 11
        $error = null;
331 11
        if (!is_null($value)) {
332 1
            $value = (is_array($value)) ? array_map('trim', $value) : trim($value);
333 1
            foreach ($rules as $rule => $param) {
334 1
                list($value, $error) = $this->validate($value, $rule, $param);
335 1
                if (empty($value) || !empty($error)) {
336 1
                    break;
337
                }
338 1
            }
339 1
        }
340 11
        $this->data[$field] = array(
341 11
            'id' => '#'.$this->id($field),
342 11
            'field' => $indexes,
343 11
            'rules' => $rules,
344 11
            'validate' => (!empty($validate)) ? $validate : null,
345 11
            'messages' => (!empty($messages)) ? $messages : null,
346 11
            'value' => $value,
347 11
            'error' => $error,
348
        );
349 11
    }
350
351
    /**
352
     * This method goes through all of the fields you set above, determines if the form has been sent, and picks out any errors.
353
     * 
354
     * @return false|array Returns an array of validated and filtered form values for every ``$validator->set('field')`` IF the form was submitted (ie. at least one field has it's $_GET or $_POST counterpart), AND there were no errors.
355
     * 
356
     * ```php
357
     * if ($vars = $validator->certified()) {
358
     *     // process $vars
359
     * }
360
     * ```
361
     */
362 3
    public function certified()
363
    {
364 3
        if (!is_null($this->submitted)) {
365 1
            return $this->certified; // so we don't overwrite error messages
366
        }
367 3
        $errors = array();
368 3
        $this->values = array();
369 3
        $this->submitted = false;
370 3
        foreach ($this->data as $field => $data) {
371 2
            if (!is_null($data['value'])) {
372 1
                $this->submitted = true;
373 1
            }
374 2
            if (!empty($data['value'])) {
375 1
                $this->values[$field] = $data['value'];
376 2
            } elseif (strpos($field, '[]') !== false) {
377 1
                $this->values[$field] = array();
378 1
            } else {
379 2
                $this->values[$field] = (isset($data['rules']['default'])) ? $data['rules']['default'] : '';
380
            }
381 2
            if (!is_null($data['error'])) {
382 1
                $errors[$field] = $data['error'];
383 1
            }
384 3
        }
385 3
        if ($this->submitted) {
386 1
            $submitted = array();
387 1
            foreach ($this->data as $field => $data) {
388 1
                $value =& $submitted;
389 1
                foreach ($data['field'] as $index) {
390 1
                    $value =& $value[$index];
391 1
                }
392 1
                $value = $this->values[$field];
393 1
                if (isset($data['rules']['required']) && empty($value)) {
394 1
                    $errors[$field] = $this->errorMessage('required');
395 1
                } elseif (!isset($errors[$field])) {
396 1
                    if (isset($data['rules']['equalTo']) && $value != $this->value($data['rules']['equalTo'])) {
397 1
                        $errors[$field] = $this->errorMessage('equalTo');
398 1
                    } elseif (isset($data['rules']['notEqualTo']) && $value == $this->value($data['rules']['notEqualTo'])) {
399 1
                        $errors[$field] = $this->errorMessage('notEqualTo');
400 1
                    }
401 1
                }
402 1
            }
403 1
            $this->certified = (!empty($errors)) ? false : $submitted;
404 1
        }
405 3
        $this->errors = $errors;
406
407 3
        return $this->certified;
408
    }
409
    
410
    /**
411
     * Allows you to know if a form $field has been required, or not.
412
     * 
413
     * @param string $field 
414
     * 
415
     * @return bool  Whether the field is required or not.
416
     */
417 1
    public function required($field)
418
    {
419 1
        return (isset($this->data[$field]['rules']['required'])) ? true : false;
420
    }
421
    
422
    /**
423
     * Returns the submitted value of the $field that should be used when displaying the form.
424
     * 
425
     * The array feature comes in handy when you want to save the values to your database.
426
     * 
427
     * @param string|array $field 
428
     * 
429
     * @return mixed
430
     */
431 10
    public function value($field)
432
    {
433 10
        if (is_array($field)) {
434 1
            $values = array();
435 1
            foreach ($field as $key => $value) {
436 1
                $values[$key] = $this->value($value);
437 1
            }
438
439 1
            return $values;
440
        }
441
        
442 10
        return ($this->submitted && isset($this->values[$field])) ? $this->values[$field] : null;
443
    }
444
445
    /**
446
     * Returns an error message (if any) that should be used when displaying the form.
447
     * 
448
     * @param string $field 
449
     * 
450
     * @return null|string
451
     */
452 2
    public function error($field)
453
    {
454 2
        return ($this->submitted && isset($this->errors[$field])) ? $this->errors[$field] : null;
455
    }
456
457
    /**
458
     * Returns all of the rules set up for the $field.
459
     * 
460
     * @param string $field 
461
     * 
462
     * @return string[]
463
     * 
464
     * ```php
465
     * foreach ($validator->rules($field) as $validate => $param) {
466
     *     $attributes["data-rule-{$validate}"] = htmlspecialchars($param);
467
     * }
468
     * ```
469
     */
470 11
    public function rules($field)
471
    {
472 11
        return (isset($this->data[$field]) && $validate = $this->data[$field]['validate']) ? $validate : array();
0 ignored issues
show
Bug introduced by
The variable $validate does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
473
    }
474
475
    /**
476
     * Returns a $field's rules and associated error messages.
477
     * 
478
     * @param string $field 
479
     * 
480
     * @return string[]
481
     * 
482
     * ```php
483
     * foreach ($validator->messages($field) as $rule => $message) {
484
     *     $attributes["data-msg-{$rule}"] = htmlspecialchars($message);
485
     * }
486
     * ```
487
     */
488 11
    public function messages($field)
489
    {
490 11
        return (isset($this->data[$field]) && $messages = $this->data[$field]['messages']) ? $messages : array();
0 ignored issues
show
Bug introduced by
The variable $messages does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
491
    }
492
493
    /**
494
     * Includes Jörn's jQuery Validation code that this component was meant to sync perfectly with.
495
     * 
496
     * @param string   $form    The jQuery identifier of your form.
497
     * @param string[] $options The rules and custom messages should be added to each inputs data-... attributes using ``$this->rules()`` and ``$this->messages()``.  Any other fine-tuning can be done here.  The $options values must be pre json encoded ie. quotes around strings ('"string"'), brackets for arrays ('[]'), quoted bools ('false').  The reason for this is because we cannot json_encode functions properly ('function(){}').
498
     * 
499
     * ```php
500
     * $validator->jquery('#form', array('debug'=>'true'));
501
     * ```
502
     * 
503
     * @see https://jqueryvalidation.org/validate/
504
     */
505 2
    public function jquery($form, array $options = array())
506
    {
507 2
        $page = Page::html();
508 2
        foreach ($options as $key => $value) {
509 1
            $options[$key] = $key.':'.$value;
510 2
        }
511 2
        $page->jquery('$("'.$form.'").validate({'.implode(', ', $options).'});');
512 2
        $page->link('https://cdn.jsdelivr.net/jquery.validation/1.15.0/jquery.validate.min.js');
513 2
        $page->link('https://cdn.jsdelivr.net/jquery.validation/1.15.0/additional-methods.min.js');
514 2
    }
515
516
    /**
517
     * Returns the unique id assigned to the $field.
518
     * 
519
     * @param string $field 
520
     * 
521
     * @return string
522
     */
523 11
    public function id($field)
524
    {
525 11
        static $ids = array();
526 11
        if (!isset($ids[$field])) {
527 4
            $ids[$field] = Page::html()->id($this->base($field));
528 4
        }
529
530 11
        return $ids[$field];
531
    }
532
533
    /**
534
     * Determines if the $value is a valid decimal number.  Can be positive or negative, integer or float, and commas to separate thousandths are okay.
535
     * 
536
     * @param string $value
537
     * 
538
     * @return bool
539
     */
540 1
    public static function number($value)
541
    {
542 1
        return (bool) preg_match('/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/', (string) $value);
543
    }
544
545
    /**
546
     * Deterimes if the $value is a positive or negative integer number, no commas.
547
     * 
548
     * @param string $value
549
     * 
550
     * @return bool
551
     */
552 1
    public static function integer($value)
553
    {
554 1
        return (bool) preg_match('/^-?\d+$/', $value);
555
    }
556
557
    /**
558
     * Deterimes if the $value is a positive integer number, no commas.
559
     * 
560
     * @param string $value
561
     * 
562
     * @return bool
563
     */
564 1
    public static function digits($value)
565
    {
566 1
        return (bool) preg_match('/^\d+$/', $value);
567
    }
568
569
    /**
570
     * Determines if the $value is greater than or equal to $param.
571
     * 
572
     * @param float $value 
573
     * @param float $param 
574
     * 
575
     * @return bool
576
     */
577 2
    public static function min($value, $param)
578
    {
579 2
        return is_numeric($value) ? ($value >= $param) : false;
580
    }
581
582
    /**
583
     * Determines if the $value is less than or equal to $param.
584
     * 
585
     * @param float $value 
586
     * @param float $param 
587
     * 
588
     * @return bool
589
     */
590 1
    public static function max($value, $param)
591
    {
592 1
        return is_numeric($value) ? ($value <= $param) : false;
593
    }
594
595
    /**
596
     * Determines if the $value is greater than or equal to $param[0], and less than or equal to $param[1].
597
     * 
598
     * @param float   $value 
599
     * @param float[] $param 
600
     * 
601
     * @return bool
602
     */
603 1
    public static function range($value, array $param)
604
    {
605 1
        return is_numeric($value) ? ($value >= $param[0] && $value <= $param[1]) : false;
606
    }
607
608
    /**
609
     * Deterimes if the $value has alpha (a-z), numeric (0-9), and underscore (_) characters only.
610
     * 
611
     * @param string $value
612
     * 
613
     * @return bool
614
     */
615 1
    public static function alphaNumeric($value)
616
    {
617 1
        return (bool) preg_match('/^\w+$/', $value);
618
    }
619
620
    /**
621
     * Determines if the $value's length is greater than or equal to $param.
622
     * 
623
     * @param string $value 
624
     * @param int    $param 
625
     * 
626
     * @return bool
627
     */
628 2
    public static function minLength($value, $param)
629
    {
630 2
        $length = is_array($value) ? count($value) : mb_strlen($value);
631
632 2
        return $length >= $param;
633
    }
634
635
    /**
636
     * Determines if the $value's length is less than or equal to $param.
637
     * 
638
     * @param string $value 
639
     * @param int    $param 
640
     * 
641
     * @return bool
642
     */
643 1
    public static function maxLength($value, $param)
644
    {
645 1
        $length = is_array($value) ? count($value) : mb_strlen($value);
646
647 1
        return $length <= $param;
648
    }
649
650
    /**
651
     * Determines if the $value's length is greater than or equal to $param[0], and less than or equal to $param[1].
652
     * 
653
     * @param string $value 
654
     * @param int[]  $param 
655
     * 
656
     * @return bool
657
     */
658 1
    public static function rangeLength($value, array $param)
659
    {
660 1
        $length = is_array($value) ? count($value) : mb_strlen($value);
661
662 1
        return $length >= $param[0] && $length <= $param[1];
663
    }
664
665
    /**
666
     * Determines if the number of $value's words are greater than or equal to $param.
667
     * 
668
     * @param string $value 
669
     * @param int    $param 
670
     * 
671
     * @return bool
672
     */
673 1
    public static function minWords($value, $param)
674
    {
675 1
        $count = self::numWords($value);
676
677 1
        return $count >= $param;
678
    }
679
680
    /**
681
     * Determines if the number of $value's words are less than or equal to $param.
682
     * 
683
     * @param string $value 
684
     * @param int    $param 
685
     * 
686
     * @return bool
687
     */
688 1
    public static function maxWords($value, $param)
689
    {
690 1
        $count = self::numWords($value);
691
692 1
        return $count <= $param;
693
    }
694
695
    /**
696
     * Determines if the number of $value's words are greater than or equal to $param[0], and less than or equal to $param[1].
697
     * 
698
     * @param string $value 
699
     * @param int[]  $param 
700
     * 
701
     * @return bool
702
     */
703 1
    public static function rangeWords($value, array $param)
704
    {
705 1
        $count = self::numWords($value);
706
707 1
        return $count >= $param[0] && $count <= $param[1];
708
    }
709
710
    /**
711
     * Determines if the $value matches the supplied regex ($param).
712
     * 
713
     * @param string $value 
714
     * @param string $param 
715
     * 
716
     * @return bool
717
     */
718 1
    public static function pattern($value, $param)
719
    {
720 1
        return (bool) preg_match($param, $value);
721
    }
722
723
    /**
724
     * Determines if the $value is a parseable date.
725
     * 
726
     * @param string $value 
727
     * 
728
     * @return bool
729
     */
730 1
    public static function date($value)
731
    {
732 1
        return (bool) strtotime($value);
733
    }
734
735
    /**
736
     * Determines if the $value is a valid looking email.
737
     * 
738
     * @param string $value 
739
     * 
740
     * @return bool
741
     */
742 2
    public static function email($value)
743
    {
744 2
        return (bool) preg_match('/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/', $value);
745
    }
746
747
    /**
748
     * Determines if the $value is a valid looking url.
749
     * 
750
     * @param string $value 
751
     * 
752
     * @return bool
753
     */
754 1
    public static function url($value)
755
    {
756 1
        return (bool) preg_match('_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]-*)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$_iuS', $value);
757
    }
758
759
    /**
760
     * Determines if the $value is a valid looking ipv4 address.
761
     * 
762
     * @param string $value 
763
     * 
764
     * @return bool
765
     */
766 1
    public static function ipv4($value)
767
    {
768 1
        return (bool) preg_match('/^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/i', $value);
769
    }
770
771
    /**
772
     * Determines if the $value is a valid looking ipv6 address.
773
     * 
774
     * @param string $value 
775
     * 
776
     * @return bool
777
     */
778 1
    public static function ipv6($value)
779
    {
780 1
        return (bool) preg_match('/^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i', $value);
781
    }
782
783
    /**
784
     * Determines if the $value exists in the $param array.
785
     * 
786
     * @param string $value 
787
     * @param array  $param 
788
     * 
789
     * @return bool
790
     */
791 2
    public static function inList($value, array $param)
792
    {
793 2
        return in_array($value, $param);
794
    }
795
796
    /**
797
     * Determines if the $value contains any white space.
798
     * 
799
     * @param string $value 
800
     * 
801
     * @return bool
802
     */
803 1
    public static function noWhiteSpace($value)
804
    {
805 1
        return (empty($value)) ? true : (bool) preg_match('/^\S+$/i', $value);
806
    }
807
808
    /**
809
     * Removes any doubled-up whitespace from the $value.
810
     * 
811
     * @param string $value 
812
     * 
813
     * @return string
814
     */
815 2
    public static function singleSpace($value)
816
    {
817 2
        return preg_replace('/\s(?=\s)/', '', $value);
818
    }
819
820
    /**
821
     * Returns a **1** (true) or **0** (false) integer depending on the $value.
822
     * 
823
     * @param mixed $value 
824
     * 
825
     * @return int
826
     */
827 1
    public static function trueFalse($value)
828
    {
829 1
        if ((is_numeric($value) && $value > 0) || strtoupper($value) == 'Y') {
830 1
            return 1;
831
        }
832
833 1
        return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
834
    }
835
836
    /**
837
     * Returns a '**Y**' or '**N**' string depending on the $value.
838
     * 
839
     * @param mixed $value 
840
     * 
841
     * @return string
842
     */
843 1
    public static function yesNo($value)
844
    {
845 1
        if ((is_numeric($value) && $value > 0) || strtoupper($value) == 'Y') {
846 1
            return 'Y';
847
        }
848
849 1
        return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 'Y' : 'N';
850
    }
851
852
    /**
853
     * Returns the number of words in $value.
854
     * 
855
     * @param string $value 
856
     * 
857
     * @return integer
858
     */
859 3
    private static function numWords($value)
860
    {
861 3
        $value = preg_replace('/<.[^<>]*?>/', ' ', $value);
862 3
        $value = preg_replace('/&nbsp;|&#160;/i', ' ', $value);
863 3
        $value = preg_replace('/[.(),;:!?%#$\'\"_+=\/\-“”’]*/', '', $value);
864 3
        preg_match_all('/\b\w+\b/', $value, $words);
865
866 3
        return count($words[0]);
867
    }
868
869
     /**
870
      * A helper method to validate a $value via a callable $param.
871
      * 
872
      * @param string $value 
873
      * @param string $param 
874
      * 
875
      * @return string
876
      */
877 1
    private function remote($value, $param)
878
    {
879 1
        $value = (isset($this->rules[$param]) && is_callable($this->rules[$param])) ? $this->rules[$param]($value) : false;
880 1
        if (!is_string($value)) {
881 1
            $value = ($value) ? 'true' : 'false';
882 1
        }
883
884 1
        return $value;
885
    }
886
887
    /**
888
     * A helper method to determine the value in an array based on a string eg. 'array[value]'
889
     * 
890
     * @param mixed $array 
891
     * @param array $indexes 
892
     * @param int   $i  
893
     * 
894
     * @return mixed
895
     */
896 2
    private function reduce($array, array $indexes, $i = 0)
897
    {
898 2
        if (is_array($array) && isset($indexes[$i])) {
899 2
            return isset($array[$indexes[$i]]) ? $this->reduce($array[$indexes[$i]], $indexes, ($i + 1)) : null;
900
        }
901
902 1
        return ($array === '') ? null : $array;
903
    }
904
905
    /**
906
     * A helper method to remove any reference to an array ie. 'array[value]' would be just 'array'.
907
     * 
908
     * @param string $field 
909
     * 
910
     * @return string
911
     */
912 4
    private function base($field)
913
    {
914 4
        return ($split = strpos($field, '[')) ? substr($field, 0, $split) : $field;
915
    }
916
917
    /**
918
     * A helper method to validate a string or an array of values based on it's $rule's and $param's.
919
     * 
920
     * @param mixed  $value 
921
     * @param string $rule 
922
     * @param mixed  $param 
923
     * 
924
     * @return array The derived value, and error (if any).
925
     */
926 1
    private function validate($value, $rule, $param)
927
    {
928 1
        if (in_array($rule, $this->reserved)) {
929 1
            return array($value, null);
930
        }
931 1
        if (is_array($value) && !in_array($rule, array('minLength', 'maxLength', 'rangeLength'))) {
932 1
            $values = $value;
933 1
            $errors = array();
934 1
            foreach ($values as $key => $value) {
935 1
                list($value, $error) = $this->validate($value, $rule, $param);
936 1
                $values[$key] = $value;
937 1
                if ($error) {
938 1
                    $errors[$key] = $error;
939 1
                }
940 1
            }
941
942 1
            return array($values, !empty($errors) ? array_shift($errors) : null);
943
        }
944 1
        $error = null;
945 1
        if ($rule == 'remote') {
946 1
            if ('true' != $result = $this->remote($value, $param)) {
947 1
                $error = ($result == 'false') ? $this->errorMessage($rule, $param) : $result;
948 1
            }
949 1
        } elseif (isset($this->rules[$rule]) && is_callable($this->rules[$rule])) {
950 1
            if (!is_bool($result = $this->rules[$rule]($value, $param))) {
951 1
                $value = $result;
952 1
            }
953 1
            if ($result === false) {
954 1
                $error = $this->errorMessage($rule, $param);
955 1
            }
956 1
        } elseif (in_array($rule, $this->methods)) {
957 1
            if (!is_bool($result = self::$rule($value, $param))) {
958 1
                $value = $result;
959 1
            }
960 1
            if ($result === false) {
961 1
                $error = $this->errorMessage($rule, $param);
962 1
            }
963 1
        }
964
965 1
        return array($value, $error);
966
    }
967
968
    /**
969
     * A helper method to retrieve the associated error message when something goes wrong.
970
     * 
971
     * @param string $rule 
972
     * @param mixed  $param  
973
     * 
974
     * @return null|string
975
     */
976 1
    private function errorMessage($rule, $param = null)
977
    {
978 1
        if ($rule == 'remote' && isset($this->errors[$param])) {
979 1
            return $this->errorMessage($param);
980
        }
981 1
        if (is_null($param)) {
982 1
            $param = '';
983 1
        }
984 1
        $params = array_pad((array) $param, 2, '');
985
986 1
        return (isset($this->errors[$rule])) ? str_replace(array('{0}', '{1}'), $params, $this->errors[$rule]) : null;
987
    }
988
}
989