Overview

Packages

  • Pinoco
    • PAL

Classes

  • Pinoco
  • Pinoco_Delegate
  • Pinoco_DynamicVars
  • Pinoco_HttpRequestVars
  • Pinoco_List
  • Pinoco_MIMEType
  • Pinoco_NativeRenderer
  • Pinoco_NothingVars
  • Pinoco_NullRenderer
  • Pinoco_OptionalParam
  • Pinoco_Pagination
  • Pinoco_PDOStatementWrapper
  • Pinoco_PDOWrapper
  • Pinoco_Renderer
  • Pinoco_Router
  • Pinoco_TALRenderer
  • Pinoco_TestEnvironment
  • Pinoco_Validator
  • Pinoco_ValidatorContext
  • Pinoco_Vars

Interfaces

  • Pinoco_ArrayConvertible

Functions

  • __pinoco_autoload_impl
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Pinoco: makes existing static web site dynamic transparently.
  4:  * Copyright 2010-2012, Hisateru Tanaka <tanakahisateru@gmail.com>
  5:  *
  6:  * Licensed under The MIT License
  7:  * Redistributions of files must retain the above copyright notice.
  8:  *
  9:  * PHP Version 5
 10:  *
 11:  * @author     Hisateru Tanaka <tanakahisateru@gmail.com>
 12:  * @copyright  Copyright 2010-2012, Hisateru Tanaka <tanakahisateru@gmail.com>
 13:  * @license    MIT License (http://www.opensource.org/licenses/mit-license.php)
 14:  * @package    Pinoco
 15:  */
 16: 
 17: /**
 18:  * Procedural validation utility.
 19:  *
 20:  * <code>
 21:  * $validator = new Pinoco_Validator($data);
 22:  * $validator->check('name')->is('not-empty')->is('max-length 255');
 23:  * $validator->check('age')->is('not-empty')->is('integer')
 24:  *                         ->is('>= 21', 'Adult only.');
 25:  * if ($validator->valid) {
 26:  *     echo "OK";
 27:  * }
 28:  * else {
 29:  *     foreach ($validator->errors as $field=>$context) {
 30:  *         echo $field . ":" . $context->message . "\n";
 31:  *     }
 32:  * }
 33:  * </code>
 34:  *
 35:  * Builtin tests:
 36:  *   pass, fail, empty, not-empty, max-length, min-length, in a,b,c, not-in a,b,c,
 37:  *   numeric, integer, alpha, alpha-numeric, == n, != n, > n, >= n, < n,  <= n,
 38:  *   match /regexp/, not-match /regexp/, email, url
 39:  *
 40:  * @package Pinoco
 41:  * @property-read Pinoco_Vars $result All context objects.
 42:  * @property-read Pinoco_Vars $errors Invalid context objects only.
 43:  * @property-read Pinoco_Vars $values Validated values unwrapped.
 44:  * @property-read boolean $valid   Totally valid or not.
 45:  * @property-read boolean $invalid Totally invalid or not.
 46:  */
 47: class Pinoco_Validator extends Pinoco_DynamicVars
 48: {
 49:     protected $_tests;
 50:     protected $_filters;
 51:     protected $_messages;
 52: 
 53:     private $_target;
 54:     private $_result;
 55:     private $_errors;
 56:     private $_values;
 57: 
 58:     /**
 59:      * Constructor
 60:      *
 61:      * @param string $target
 62:      * @param array $messages
 63:      */
 64:     public function __construct($target, $messages=array())
 65:     {
 66:         parent::__construct();
 67: 
 68:         $this->_tests = array();
 69:         $this->_setupBuiltinTests();
 70: 
 71:         $this->_filters = array();
 72:         $this->_setupBuiltinFilters();
 73: 
 74:         $this->_messages = array();
 75:         $this->overrideErrorMessages($messages);
 76: 
 77:         $this->_target = $target;
 78:         $this->_result = new Pinoco_Vars();
 79:         $this->_errors = null;
 80:         $this->_values = null;
 81:     }
 82: 
 83:     private function _setupBuiltinTests()
 84:     {
 85:         // builtin testers
 86:         $this->defineValidityTest('pass', array($this, '_testPassComplex'),
 87:             "Valid.", true);
 88:         $this->defineValidityTest('fail', array($this, '_testFailComplex'),
 89:             "Invalid.", true);
 90:         $this->defineValidityTest('empty', array($this, '_testEmptyComplex'),
 91:             "Leave as empty.", true);
 92:         $this->defineValidityTest('not-empty', array($this, '_testNotEmptyComplex'),
 93:             "Required.", true);
 94:         $this->defineValidityTest('max-length', array($this, '_testMaxLength'),
 95:             "In {param} letters.");
 96:         $this->defineValidityTest('min-length', array($this, '_testMinLength'),
 97:             "At least {param} letters.");
 98:         $this->defineValidityTest('in', array($this, '_testIn'),
 99:             "Choose in {param}.");
100:         $this->defineValidityTest('not-in', array($this, '_testNotIn'),
101:             "Choose else of {param}.");
102:         $this->defineValidityTest('numeric', array($this, '_testNumeric'),
103:             "By number.");
104:         $this->defineValidityTest('integer', array($this, '_testInteger'),
105:             "By integer number.");
106:         $this->defineValidityTest('alpha', array($this, '_testAlpha'),
107:             "Alphabet only.");
108:         $this->defineValidityTest('alpha-numeric', array($this, '_testAlphaNumeric'),
109:             "Alphabet or number.");
110:         $this->defineValidityTest('array', array($this, '_testArray'),
111:             "By Array.");
112:         $this->defineValidityTest('==', array($this, '_testEqual'),
113:             "Shuld equal to {param}.");
114:         $this->defineValidityTest('!=', array($this, '_testNotEqual'),
115:             "Should not equal to {param}.");
116:         $this->defineValidityTest('>', array($this, '_testGreaterThan'),
117:             "Greater than {param}.");
118:         $this->defineValidityTest('>=', array($this, '_testGreaterThanOrEqual'),
119:             "Greater than or equals to {param}.");
120:         $this->defineValidityTest('<', array($this, '_testLessorThan'),
121:             "Lessor than {param}.");
122:         $this->defineValidityTest('<=', array($this, '_testLessorThanOrEqual'),
123:             "Lessor than or equals to {param}.");
124:         $this->defineValidityTest('match', array($this, '_testMatch'),
125:             "Invalid pattern.");
126:         $this->defineValidityTest('not-match', array($this, '_testNotMatch'),
127:             "Not allowed pattern.");
128:         $this->defineValidityTest('email', array($this, '_testEmail'),
129:             "Email only.");
130:         $this->defineValidityTest('url', array($this, '_testUrl'),
131:             "URL only.");
132:     }
133: 
134:     private function _setupBuiltinFilters()
135:     {
136:         // builtin filters
137:         $this->defineFilter('trim', array($this, '_filterTrim'));
138:         $this->defineFilter('ltrim', array($this, '_filterLtrim'));
139:         $this->defineFilter('rtrim', array($this, '_filterRtrim'));
140:     }
141: 
142:     /**
143:      * Defines custom test.
144:      *
145:      * @param string $testName
146:      * @param callback $callable
147:      * @param string $message
148:      * @param boolean $complex
149:      * @return void
150:      */
151:     public function defineValidityTest($testName, $callable, $message, $complex=false)
152:     {
153:         $this->_tests[$testName] = array(
154:             'callback' => $callable,
155:             'message' => $message,
156:             'complex' => $complex,
157:         );
158:     }
159: 
160:     /**
161:      * Defines custom filter.
162:      *
163:      * @param string $filterName
164:      * @param callback $callable
165:      * @param boolean $complex
166:      * @return void
167:      */
168:     public function defineFilter($filterName, $callable, $complex=false)
169:     {
170:         $this->_filters[$filterName] = array(
171:             'callback' => $callable,
172:             'complex' => $complex,
173:         );
174:     }
175: 
176:     /**
177:      * Overrides error messages.
178:      *
179:      * @param array $messages
180:      * @return void
181:      */
182:     public function overrideErrorMessages($messages)
183:     {
184:         foreach ($messages as $test=>$msg) {
185:             $this->_messages[$test] = $msg;
186:         }
187:     }
188: 
189:     /**
190:      * Resolve error message by test name.
191:      *
192:      * @param string $testName
193:      * @return string
194:      */
195:     public function getMessageFor($testName)
196:     {
197:         if (is_scalar($testName)) {
198:             if (isset($this->_messages[$testName])) {
199:                 return $this->_messages[$testName];
200:             }
201:             elseif (isset($this->_tests[$testName])) {
202:                 return $this->_tests[$testName]['message'];
203:             }
204:             else {
205:                 return 'not registered';
206:             }
207:         }
208:         else {
209:             return 'invalid';
210:         }
211:     }
212: 
213:     /**
214:      * Check existence and fetch value at the same time.
215:      * (called by self and validation context)
216:      *
217:      * @param string $name
218:      * @return array
219:      */
220:     public function fetchExistenceAndValue($name)
221:     {
222:         //type check
223:         if ($this->_target instanceof Pinoco_Vars) {
224:             $exists = $this->_target->has($name);
225:             $value = $this->_target->get($name);
226:         }
227:         elseif ($this->_target instanceof Pinoco_List) {
228:             $exists = intval($name) < $this->_target->count();
229:             $value = $exists ? $this->_target[$name] : null;
230:         }
231:         elseif (is_array($this->_target)) {
232:             $exists = isset($this->_target[$name]);
233:             $value = $exists ? $this->_target[$name] : null;
234:         }
235:         elseif (is_object($this->_target)) {
236:             $exists = isset($this->_target->$name);
237:             $value = $exists ? $this->_target->$name : null;
238:         }
239:         else {
240:             $exists = false;
241:             $value = null;
242:         }
243:         return array($exists, $value);
244:     }
245: 
246:     private function prepareCallback($methods, $methodName, $param)
247:     {
248:         $this->_errors = null;
249:         $this->_values = null;
250:         if (is_scalar($methodName) && isset($methods[$methodName])) {
251:             return array(
252:                 $methods[$methodName]['callback'],
253:                 $methods[$methodName]['complex'],
254:                 array($param)
255:             );
256:         }
257:         elseif (is_callable($methodName)) {
258:             return array(
259:                 $methodName,
260:                 false,
261:                 $param ? explode(' ', $param) : array()
262:             );
263:         }
264:         else {
265:             // test method not registered
266:             return array(
267:                 null,
268:                 false,
269:                 array()
270:             );
271:         }
272:     }
273: 
274:     private function prepareValue($field, $filtered, $filteredValue)
275:     {
276:         // fetch
277:         if ($filtered) {
278:             return array(
279:                 true,
280:                 $filteredValue
281:             );
282:         }
283:         else {
284:             return $this->fetchExistenceAndValue($field);
285:         }
286:     }
287: 
288:     private function callMethod($callable, $complex, $params, $exists, $value, $forFilter=false)
289:     {
290:         if ($complex) {
291:             // complex test: full information presented
292:             //               and should be checked if empty or not
293:             $args = array($exists, $value);
294:         }
295:         else {
296:             if (!$forFilter) {
297:                 // simple test: empty always success
298:                 if (!$exists || empty($value) && !($value === "0" || $value === 0 || $value === false || $value === array())) {
299:                     // validation must be passed and value is as is.
300:                     return array(true, $value);
301:                 }
302:             }
303:             $args = array($value);
304:         }
305:         foreach ($params as $p) {
306:             $args[] = $p;
307:         }
308:         return call_user_func_array($callable, $args);
309:     }
310: 
311:     /**
312:      * Executes validation test.
313:      * (called by validation context)
314:      *
315:      * @param string $field
316:      * @param boolean $filtered
317:      * @param mixed $filteredValue
318:      * @param string $testName
319:      * @param string $param
320:      * @return array
321:      */
322:     public function execValidityTest($field, $filtered, $filteredValue, $testName, $param)
323:     {
324:         list($callable, $complex, $params) = $this->prepareCallback($this->_tests, $testName, $param);
325:         list($exists, $value) = $this->prepareValue($field, $filtered, $filteredValue);
326:         if ($callable == null) {
327:             return array(false, $value);
328:         }
329:         else {
330:             return array(
331:                 $this->callMethod($callable, $complex, $params, $exists, $value),
332:                 $value
333:             );
334:         }
335:     }
336: 
337:     /**
338:      * Executes validation test to array with logical AND.
339:      * (called by validation context)
340:      *
341:      * @param string $field
342:      * @param boolean $filtered
343:      * @param mixed $filteredValue
344:      * @param string $testName
345:      * @param string $param
346:      * @return array
347:      */
348:     public function execValidityTestAll($field, $filtered, $filteredValue, $testName, $param)
349:     {
350:         list($callable, $complex, $params) = $this->prepareCallback($this->_tests, $testName, $param);
351:         list($exists, $value) = $this->prepareValue($field, $filtered, $filteredValue);
352:         if ($callable == null || !(is_array($value) || $value instanceof Traversable)) {
353:             return array(false, $value);
354:         }
355:         else {
356:             foreach ($value as $v) {
357:                 $result = $this->callMethod($callable, $complex, $params, $exists, $v);
358:                 if (!$result) {
359:                     return array(false, $value);
360:                 }
361:             }
362:             return array(true, $value);
363:         }
364:     }
365: 
366:     /**
367:      * Executes validation test to array with logical OR.
368:      * (called by validation context)
369:      *
370:      * @param string $field
371:      * @param boolean $filtered
372:      * @param mixed $filteredValue
373:      * @param string $testName
374:      * @param string $param
375:      * @return array
376:      */
377:     public function execValidityTestAny($field, $filtered, $filteredValue, $testName, $param)
378:     {
379:         list($callable, $complex, $params) = $this->prepareCallback($this->_tests, $testName, $param);
380:         list($exists, $value) = $this->prepareValue($field, $filtered, $filteredValue);
381:         if ($callable == null || !(is_array($value) || $value instanceof Traversable)) {
382:             return array(false, $value);
383:         }
384:         else {
385:             foreach ($value as $v) {
386:                 $result = $this->callMethod($callable, $complex, $params, $exists, $v);
387:                 if ($result) {
388:                     return array(true, $value);
389:                 }
390:             }
391:             return array(false, $value);
392:         }
393:     }
394: 
395:     /**
396:      * Executes filter.
397:      * (called by validation context)
398:      *
399:      * @param string $field
400:      * @param boolean $filtered
401:      * @param mixed $filteredValue
402:      * @param mixed $filterName
403:      * @param string $param
404:      * @return array
405:      */
406:     public function execFilter($field, $filtered, $filteredValue, $filterName, $param)
407:     {
408:         list($callable, $complex, $params) = $this->prepareCallback($this->_filters, $filterName, $param);
409:         list($exists, $value) = $this->prepareValue($field, $filtered, $filteredValue);
410:         if ($callable == null) {
411:             return array(true, null);
412:         }
413:         else {
414:             return array(
415:                 true,
416:                 $this->callMethod($callable, $complex, $params, $exists, $value, true)
417:             );
418:         }
419:     }
420: 
421:     /**
422:      * Executes filter for each elements.
423:      * (called by validation context)
424:      *
425:      * @param string $field
426:      * @param boolean $filtered
427:      * @param mixed $filteredValue
428:      * @param mixed $filterName
429:      * @param string $param
430:      * @return array
431:      */
432:     public function execFilterMap($field, $filtered, $filteredValue, $filterName, $param)
433:     {
434:         list($callable, $complex, $params) = $this->prepareCallback($this->_filters, $filterName, $param);
435:         list($exists, $value) = $this->prepareValue($field, $filtered, $filteredValue);
436:         if ($callable == null || !(is_array($value) || $value instanceof Traversable)) {
437:             return array(true, null);
438:         }
439:         else {
440:             if ($value instanceof Pinoco_List) {
441:                 $result = new Pinoco_List();
442:                 foreach ($value as $v) {
443:                     $result->push($this->callMethod($callable, $complex, $params, $exists, $v, true));
444:                 }
445:             }
446:             else {
447:                 $result = array();
448:                 foreach ($value as $v) {
449:                     $result[] = $this->callMethod($callable, $complex, $params, $exists, $v, true);
450:                 }
451:             }
452:             return array(true, $result);
453:         }
454:     }
455: 
456:     /**
457:      * Returns independent validation context.
458:      *
459:      * @param string $name
460:      * @param string|bool $label
461:      * @return Pinoco_ValidatorContext
462:      */
463:     public function contextFor($name, $label=false)
464:     {
465:         return new Pinoco_ValidatorContext($this, $name, $label);
466:     }
467: 
468:     /**
469:      * Starts named property check.
470:      *
471:      * @param string $name
472:      * @param string|bool $label
473:      * @return Pinoco_ValidatorContext
474:      */
475:     public function check($name, $label=false)
476:     {
477:         $this->_errors = null;
478:         $this->_values = null;
479:         if (!$this->_result->has($name)) {
480:             $this->_result->set($name, $this->contextFor($name, $label));
481:         }
482:         return $this->_result->get($name);
483:     }
484: 
485:     /**
486:      * Clears previous result and restarts named property check.
487:      *
488:      * @param string $name
489:      * @param string|bool $label
490:      * @return Pinoco_ValidatorContext
491:      */
492:     public function recheck($name, $label=false)
493:     {
494:         $this->_errors = null;
495:         $this->_values = null;
496:         $this->_result->set($name, $this->contextFor($name, $label));
497:         return $this->_result->get($name);
498:     }
499: 
500:     /**
501:      * Clears previous result.
502:      *
503:      * @param string $name
504:      * @return void
505:      */
506:     public function uncheck($name)
507:     {
508:         $this->_errors = null;
509:         $this->_values = null;
510:         if ($this->_result->has($name)) {
511:             $this->_result->remove($name);
512:         }
513:     }
514: 
515:     /**
516:      * Exports test all results.
517:      *
518:      * @return Pinoco_Vars
519:      */
520:     public function get_result()
521:     {
522:         return $this->_result;
523:     }
524: 
525:     /**
526:      * Exports test results only failed.
527:      *
528:      * @return Pinoco_Vars
529:      */
530:     public function get_errors()
531:     {
532:         if ($this->_errors === null) {
533:             $this->_errors = new Pinoco_Vars();
534:             foreach ($this->_result->keys() as $field) {
535:                 $result = $this->_result->get($field);
536:                 if ($result->invalid) {
537:                     $this->_errors->set($field, $result);
538:                 }
539:             }
540:         }
541:         return $this->_errors;
542:     }
543: 
544:     /**
545:      * Exports test results only failed.
546:      *
547:      * @return Pinoco_Vars
548:      */
549:     public function get_values()
550:     {
551:         if ($this->_values === null) {
552:             $this->_values = new Pinoco_Vars();
553:             foreach ($this->_result->keys() as $field) {
554:                 $result = $this->_result->get($field);
555:                 $this->_values->set($field, $result->value);
556:             }
557:         }
558:         return $this->_values;
559:     }
560: 
561:     /**
562:      * Returns which all tests succeeded or not.
563:      *
564:      * @return boolean
565:      */
566:     public function get_valid()
567:     {
568:         return ($this->get_errors()->count() == 0);
569:     }
570: 
571:     /**
572:      * Returns which validator has one or more failed tests.
573:      *
574:      * @return boolean
575:      */
576:     public function get_invalid()
577:     {
578:         return !$this->get_valid();
579:     }
580: 
581:     /**
582:      * Returns all succeeded checking results to be used in form's initial state.
583:      * If you fetch a field not given by $values, you will get a passed checking
584:      * context instead.
585:      *
586:      * @param array $values
587:      * @return Pinoco_Vars
588:      */
589:     public static function emptyResult($values=array())
590:     {
591:         $validator = new self($values);
592:         foreach ($values as $name=>$value) {
593:             $validator->check($name)->is('pass');
594:         }
595:         $result = $validator->result;
596:         $result->setDefault($validator->contextFor('any')->is('pass'));
597:         $result->setLoose(true);
598:         return $result;
599:     }
600: 
601:     /////////////////////////////////////////////////////////////////////
602:     // builtin tests
603:     private function _testPassComplex($exists, $value)
604:     {
605:         return true;
606:     }
607:     private function _testFailComplex($exists, $value)
608:     {
609:         return false;
610:     }
611:     private function _testEmptyComplex($exists, $value)
612:     {
613:         if (!$exists || $value === null) { return true; }
614:         if ($value === "0" || $value === 0 || $value === false || $value === array()) {
615:             return false;
616:         }
617:         return empty($value);
618:     }
619:     private function _testNotEmptyComplex($exists, $value)
620:     {
621:         return !$this->_testEmptyComplex($exists, $value);
622:     }
623: 
624:     private function _testMaxLength($value, $param=0)
625:     {
626:         return strlen(strval($value)) <= $param;
627:     }
628:     private function _testMinLength($value, $param=0)
629:     {
630:         return strlen(strval($value)) >= $param;
631:     }
632:     private function _testIn($value, $param='')
633:     {
634:         $as = explode(',', $param);
635:         foreach ($as as $a) {
636:             if ($value == trim($a)) { return true; }
637:         }
638:         return false;
639:     }
640:     private function _testNotIn($value, $param='')
641:     {
642:         return !$this->_testIn($value, $param);
643:     }
644:     private function _testNumeric($value)
645:     {
646:         return is_numeric($value);
647:     }
648:     private function _testInteger($value)
649:     {
650:         return is_integer($value);
651:     }
652:     private function _testAlpha($value)
653:     {
654:         return ctype_alpha($value);
655:     }
656:     private function _testAlphaNumeric($value)
657:     {
658:         return ctype_alnum($value);
659:     }
660:     private function _testArray($value)
661:     {
662:         return is_array($value) || $value instanceof Traversable;
663:     }
664:     private function _testEqual($value, $param=null)
665:     {
666:         return $value == $param;
667:     }
668:     private function _testNotEqual($value, $param=null)
669:     {
670:         return !$this->_testEqual($value, $param);
671:     }
672:     private function _testGreaterThan($value, $param=0)
673:     {
674:         return $value > $param;
675:     }
676:     private function _testGreaterThanOrEqual($value, $param=0)
677:     {
678:         return $value >= $param;
679:     }
680:     private function _testLessorThan($value, $param=0)
681:     {
682:         return $value < $param;
683:     }
684:     private function _testLessorThanOrEqual($value, $param=0)
685:     {
686:         return $value <= $param;
687:     }
688:     private function _testMatch($value, $param='/^$/')
689:     {
690:         return preg_match($param, $value);
691:     }
692:     private function _testNotMatch($value, $param='/^$/')
693:     {
694:         return !$this->_testMatch($value, $param);
695:     }
696:     private function _testEmail($value)
697:     {
698:         return preg_match('/@[A-Z0-9][A-Z0-9_-]*(\.[A-Z0-9][A-Z0-9_-]*)*$/i', $value);
699:     }
700:     private function _testUrl($value)
701:     {
702:         return preg_match('/^[A-Z]+:\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)*):?(\d+)?\/?/i', $value);
703:     }
704: 
705:     /////////////////////////////////////////////////////////////////////
706:     // builtin filters
707:     private function _filterTrim($value)
708:     {
709:         return trim($value);
710:     }
711:     private function _filterLtrim($value)
712:     {
713:         return ltrim($value);
714:     }
715:     private function _filterRtrim($value)
716:     {
717:         return rtrim($value);
718:     }
719: }
720: 
721: 
Pinoco 0.8.0 Documentation API documentation generated by ApiGen 2.8.0