Overview

Namespaces

  • None
  • Wei
    • Validator

Classes

  • Wei\Validator\All
  • Wei\Validator\AllOf
  • Wei\Validator\Alnum
  • Wei\Validator\Alpha
  • Wei\Validator\BaseValidator
  • Wei\Validator\Between
  • Wei\Validator\Blank
  • Wei\Validator\Callback
  • Wei\Validator\CharLength
  • Wei\Validator\Chinese
  • Wei\Validator\Color
  • Wei\Validator\Contains
  • Wei\Validator\CreditCard
  • Wei\Validator\Date
  • Wei\Validator\DateTime
  • Wei\Validator\Decimal
  • Wei\Validator\Digit
  • Wei\Validator\Dir
  • Wei\Validator\DivisibleBy
  • Wei\Validator\DoubleByte
  • Wei\Validator\Email
  • Wei\Validator\EndsWith
  • Wei\Validator\EqualTo
  • Wei\Validator\Exists
  • Wei\Validator\FieldExists
  • Wei\Validator\File
  • Wei\Validator\GreaterThan
  • Wei\Validator\GreaterThanOrEqual
  • Wei\Validator\IdCardCn
  • Wei\Validator\IdCardHk
  • Wei\Validator\IdCardMo
  • Wei\Validator\IdCardTw
  • Wei\Validator\IdenticalTo
  • Wei\Validator\Image
  • Wei\Validator\In
  • Wei\Validator\Ip
  • Wei\Validator\Length
  • Wei\Validator\LessThan
  • Wei\Validator\LessThanOrEqual
  • Wei\Validator\Lowercase
  • Wei\Validator\Luhn
  • Wei\Validator\MaxLength
  • Wei\Validator\MinLength
  • Wei\Validator\MobileCn
  • Wei\Validator\NaturalNumber
  • Wei\Validator\NoneOf
  • Wei\Validator\Null
  • Wei\Validator\Number
  • Wei\Validator\OneOf
  • Wei\Validator\Password
  • Wei\Validator\Phone
  • Wei\Validator\PhoneCn
  • Wei\Validator\PlateNumberCn
  • Wei\Validator\PositiveInteger
  • Wei\Validator\PostcodeCn
  • Wei\Validator\Present
  • Wei\Validator\QQ
  • Wei\Validator\RecordExists
  • Wei\Validator\Regex
  • Wei\Validator\Required
  • Wei\Validator\SomeOf
  • Wei\Validator\StartsWith
  • Wei\Validator\Time
  • Wei\Validator\Tld
  • Wei\Validator\Type
  • Wei\Validator\Uppercase
  • Wei\Validator\Url
  • Wei\Validator\Uuid
  • Overview
  • Namespace
  • Function
  1: <?php
  2: /**
  3:  * Wei Framework
  4:  *
  5:  * @copyright   Copyright (c) 2008-2015 Twin Huang
  6:  * @license     http://opensource.org/licenses/mit-license.php MIT License
  7:  */
  8: 
  9: namespace Wei;
 10: 
 11: /**
 12:  * A service that parse the URL to request data
 13:  *
 14:  * @author      Twin Huang <twinhuang@qq.com>
 15:  * @link        The code is inspired by the awesome framework - Kohana
 16:  *               http://kohanaframework.org/3.0/guide/api/Kohana_Route
 17:  */
 18: class Router extends Base
 19: {
 20:     /**
 21:      * The routes configurations
 22:      *
 23:      * @var array
 24:      */
 25:     protected $routes = array();
 26: 
 27:     /**
 28:      * The default route options
 29:      *
 30:      * @var array
 31:      *
 32:      * Name     | Type     | Description
 33:      * ---------|----------|-------------
 34:      * pattern  | string   | The string to be complied to regex
 35:      * rules    | array    | The regex rules
 36:      * defaults | array    | The defaults params of the route
 37:      * method   | string   | The required request method of the route
 38:      * regex    | string   | The regex complied from the pattern, just leave it blank when set a new route
 39:      */
 40:     protected $routeOptions = array(
 41:         'pattern'   => null,
 42:         'rules'     => array(),
 43:         'defaults'  => array(),
 44:         'method'    => null,
 45:         'regex'     => null,
 46:     );
 47: 
 48:     /**
 49:      * The string to split out at the beginning of path
 50:      *
 51:      * @var array
 52:      */
 53:     protected $namespaces = array('admin', 'api');
 54: 
 55:     /**
 56:      * The string to split out after namespace string
 57:      *
 58:      * @var array
 59:      */
 60:     protected $scopes = array('user');
 61: 
 62:     /**
 63:      * The resources that contains "/", eg articles/categories, products/categories, issues/comments.
 64:      *
 65:      * @var array
 66:      */
 67:     protected $combinedResources = array();
 68: 
 69:     /**
 70:      * @var string
 71:      */
 72:     protected $defaultController = 'index';
 73: 
 74:     /**
 75:      * @var string
 76:      */
 77:     protected $defaultAction = 'index';
 78: 
 79:     /**
 80:      * An array contains the HTTP method and action name
 81:      *
 82:      * @var array
 83:      */
 84:     protected $methodToAction = array(
 85:         'GET-collection' => 'index',
 86:         'GET' => 'show',
 87:         'POST' => 'create',
 88:         'PATCH' => 'update',
 89:         'PUT' => 'update',
 90:         'DELETE' => 'destroy'
 91:     );
 92: 
 93:     /**
 94:      * Singular inflector rules
 95:      *
 96:      * @var array
 97:      */
 98:     protected $singularRules = array(
 99:         '/(o|x|ch|ss|sh)es$/i' => '\1', // heroes, potatoes, tomatoes
100:         '/([^aeiouy]|qu)ies$/i' => '\1y', // histories
101:         '/s$/i' => '',
102:     );
103: 
104:     /**
105:      * The plural to singular array
106:      *
107:      * @var array
108:      */
109:     protected $singulars = array(
110:         'aliases' => 'alias',
111:         'analyses' => 'analysis',
112:         'buses' => 'bus',
113:         'children' => 'child',
114:         'cookies' => 'cookie',
115:         'criteria' => 'criterion',
116:         'data' => 'datum',
117:         'lives' => 'life',
118:         'matrices' => 'matrix',
119:         'men' => 'man',
120:         'menus' => 'menu',
121:         'monies' => 'money',
122:         'news' => 'news',
123:         'people' => 'person',
124:         'quizzes' => 'quiz',
125:     );
126: 
127:     /**
128:      * Set routes
129:      *
130:      * @param array $routes
131:      * @return $this
132:      */
133:     public function setRoutes($routes)
134:     {
135:         foreach ($routes as $route) {
136:             $this->set($route);
137:         }
138:         return $this;
139:     }
140: 
141:     /**
142:      * Return routes
143:      *
144:      * @return array
145:      */
146:     public function getRoutes()
147:     {
148:         return $this->routes;
149:     }
150: 
151:     /**
152:      * Add one route
153:      *
154:      * @param  array $route the options of the route
155:      * @throws \InvalidArgumentException When argument is not string or array
156:      * @return $this
157:      */
158:     public function set($route)
159:     {
160:         if (is_string($route)) {
161:             $this->routes[] = array(
162:                     'pattern' => $route
163:                 ) + $this->routeOptions;
164:         } elseif (is_array($route)) {
165:             $this->routes[] = $route + $this->routeOptions;
166:         } else {
167:             throw new \InvalidArgumentException(sprintf(
168:                 'Expected argument of type string or array, "%s" given',
169:                 is_object($route) ? get_class($route) : gettype($route)
170:             ));
171:         }
172: 
173:         return $this;
174:     }
175: 
176:     /**
177:      * Get the route by id
178:      *
179:      * @param  string $id The id of the route
180:      * @return false|array
181:      */
182:     public function getRoute($id)
183:     {
184:         return isset($this->routes[$id]) ? $this->routes[$id] : false;
185:     }
186: 
187:     /**
188:      * Remove the route
189:      *
190:      * @param  string      $id The id of the route
191:      * @return $this
192:      */
193:     public function remove($id)
194:     {
195:         unset($this->routes[$id]);
196:         return $this;
197:     }
198: 
199:     /**
200:      * Prepare the route pattern to regex
201:      *
202:      * @param  array  $route the route array
203:      * @return array
204:      */
205:     protected function compile(&$route)
206:     {
207:         if ($route['regex']) {
208:             return $route;
209:         }
210: 
211:         $regex = preg_replace('#[.\+*?[^\]${}=!|:-]#', '\\\\$0', $route['pattern']);
212: 
213:         $regex = str_replace(array('(', ')'), array('(?:', ')?'), $regex);
214: 
215:         $regex = str_replace(array('<', '>'), array('(?P<', '>.+?)'), $regex);
216: 
217:         if ($route['rules']) {
218:             $search = $replace = array();
219:             foreach ($route['rules'] as $key => $rule) {
220:                 $search[] = '<' . $key . '>.+?';
221:                 $replace[] = '<' . $key . '>' . $rule;
222:             }
223:             $regex = str_replace($search, $replace, $regex);
224:         }
225: 
226:         $route['regex'] = '#^' . $regex . '$#uUD';
227: 
228:         return $route;
229:     }
230: 
231:     /**
232:      * Check if there is a route matches the path info and method,
233:      * and return the parameters, or return false when not matched
234:      *
235:      * @param  string $pathInfo    The path info to match
236:      * @param  string|null $method The request method to match, maybe GET, POST, etc
237:      * @return false|array
238:      */
239:     public function match($pathInfo, $method = null)
240:     {
241:         $pathInfo = rtrim($pathInfo, '/');
242:         !$pathInfo && $pathInfo = '/';
243:         foreach ($this->routes as $id => $route) {
244:             if (false !== ($parameters = $this->matchRoute($pathInfo, $method, $id))) {
245:                 return $parameters;
246:             }
247:         }
248:         return false;
249:     }
250: 
251:     /**
252:      * Parse the path info to parameter set
253:      *
254:      * @param  string $pathInfo    The path info to match
255:      * @param  string|null $method The request method to match, maybe GET, POST, etc
256:      * @return array
257:      */
258:     public function matchParamSet($pathInfo, $method = 'GET')
259:     {
260:         $params = $this->match($pathInfo, $method);
261:         $restParams = $this->matchRestParamSet($pathInfo, $method);
262:         return $params ? array_merge(array($params), $restParams) : $restParams;
263:     }
264: 
265:     /**
266:      * Check if the route matches the path info and method,
267:      * and return the parameters, or return false when not matched
268:      *
269:      * @param  string      $pathInfo The path info to match
270:      * @param  string      $method The request method to match
271:      * @param  string      $id The id of the route
272:      * @return false|array
273:      */
274:     protected function matchRoute($pathInfo, $method, $id)
275:     {
276:         $route = $this->compile($this->routes[$id]);
277: 
278:         // When $route['method'] is not provided, accepts all request methods
279:         if ($method && $route['method'] && !preg_match('#' . $route['method'] . '#i', $method)) {
280:             return false;
281:         }
282: 
283:         // Check if the route matches the path info
284:         if (!preg_match($route['regex'], $pathInfo, $matches)) {
285:             return false;
286:         }
287: 
288:         // Get params in the path info
289:         $parameters = array();
290:         foreach ($matches as $key => $parameter) {
291:             if (is_int($key)) {
292:                 continue;
293:             }
294:             $parameters[$key] = $parameter;
295:         }
296: 
297:         $parameters += $route['defaults'];
298: 
299:         preg_match_all('#<([a-zA-Z0-9_]++)>#', $route['pattern'], $matches);
300:         foreach ($matches[1] as $key) {
301:             if (!array_key_exists($key, $parameters)) {
302:                 $parameters[$key] = null;
303:             }
304:         }
305: 
306:         return array('_id' => $id) + $parameters;
307:     }
308: 
309:     /**
310:      * Parse the path info to multi REST parameters
311:      *
312:      * @param string $path
313:      * @param string $method
314:      * @return array
315:      */
316:     public function matchRestParamSet($path, $method = 'GET')
317:     {
318:         $rootCtrl = '';
319:         $params = array();
320:         $routes = array();
321: 
322:         // Split out format
323:         if (strpos($path, '.') !== false) {
324:             $params['_format'] = pathinfo($path, PATHINFO_EXTENSION);
325:             $path = substr($path, 0, - strlen($params['_format']) - 1);
326:         }
327: 
328:         $parts = $this->parsePath($path);
329: 
330:         // Split out namespace
331:         if (count($parts) > 1 && in_array($parts[0], $this->namespaces)) {
332:             $rootCtrl .= array_shift($parts) . '/';
333:         }
334: 
335:         // Split out scope
336:         if (count($parts) > 1 && in_array($parts[0], $this->scopes)) {
337:             $rootCtrl .= array_shift($parts) . '/';
338:         }
339: 
340:         $baseCtrl = $rootCtrl;
341: 
342:         // The first parameter must be controller name,
343:         // the second parameter may be action name or id, eg posts/show, posts/1
344:         // the last parameter may be controller name or action name or null, eg posts/1/comments, posts/1/edit
345:         $lastParts = array_pad(array_splice($parts, count($parts) % 2 == 0 ? -2 : -3), 3, null);
346:         list($ctrl, $actOrId, $ctrlOrAct) = $lastParts;
347: 
348:         // Generate part of controller name and query parameters
349:         $count = count($parts);
350:         for ($i = 0; $i < $count; $i += 2) {
351:             $baseCtrl .= $parts[$i] . '/';
352:             $params[$this->singularize($parts[$i]) . 'Id'] = $parts[$i + 1];
353:         }
354: 
355:         if (is_null($actOrId)) {
356:             // GET|POST|... /, GET|POST|... posts
357:             $routes[] = array(
358:                 'controller' => $baseCtrl . ($ctrl ?: $this->defaultController),
359:                 'action' => $this->methodToAction($method, true),
360:             );
361:         } elseif (is_null($ctrlOrAct)) {
362:             // GET posts/show
363:             if ($this->isAction($actOrId)) {
364:                 $routes[] = array(
365:                     'controller' => $baseCtrl . $ctrl,
366:                     'action' => $actOrId,
367:                 );
368:             }
369: 
370:             // GET|PUT|... posts/1
371:             $routes[] = array(
372:                 'controller' => $baseCtrl . $ctrl,
373:                 'action' => $this->methodToAction($method),
374:                 'id' => $actOrId
375:             );
376: 
377:             // GET posts/1/comment/new => comment::new postId=1
378:             if ($count >= 2 && $this->isAction($actOrId)) {
379:                 $routes[] = array(
380:                     'controller' => $rootCtrl . $ctrl,
381:                     'action' => $actOrId,
382:                 );
383:             }
384:         } else {
385:             // GET posts/1/edit
386:             $routes[] = array(
387:                 'controller' => $baseCtrl . $ctrl,
388:                 'action' => $ctrlOrAct,
389:                 'id' => $actOrId
390:             );
391: 
392:             // GET|PUT|... posts/1/comments
393:             $routes[] = array(
394:                 'controller' => $baseCtrl . $ctrl . '/' . $ctrlOrAct,
395:                 'action' => $this->methodToAction($method, true),
396:                 $this->singularize($ctrl) . 'Id' => $actOrId,
397:             );
398: 
399:             // GET|PUT|... posts/1/comments, use the last parameter as main controller name
400:             $routes[] = array('controller' => $ctrlOrAct) + $routes[1];
401:         }
402: 
403:         foreach ($routes as &$route) {
404:             $route['controller'] = $this->camelize($route['controller']);
405:             $route['action'] = $this->camelize($route['action']);
406:             $route += $params;
407:         }
408:         return $routes;
409:     }
410: 
411:     /**
412:      * Parse path to resource names, actions and ids
413:      *
414:      * @param string $path
415:      * @return array
416:      */
417:     protected function parsePath($path)
418:     {
419:         $path = ltrim($path, '/');
420:         foreach ($this->combinedResources as $resource) {
421:             if (strpos($path, $resource) !== false) {
422:                 list($part1, $part2) = explode($resource, $path, 2);
423:                 $parts = array_merge(explode('/', $part1), array($resource), explode('/', $part2));
424:                 return array_values(array_filter($parts));
425:             }
426:         }
427:         return explode('/', $path);
428:     }
429: 
430:     /**
431:      * Set combined resources
432:      *
433:      * @param array $combinedResources
434:      * @return $this
435:      */
436:     public function setCombinedResources(array $combinedResources)
437:     {
438:         $this->combinedResources += $combinedResources;
439:         return $this;
440:     }
441: 
442:     /**
443:      * Append words to singulars
444:      *
445:      * @param array $singulars
446:      * @return $this
447:      */
448:     public function setSingulars(array $singulars)
449:     {
450:         $this->singulars += $this->singulars;
451:         return $this;
452:     }
453: 
454:     /**
455:      * Returns a word in singular form.
456:      *
457:      * The implementation is borrowed from Doctrine Inflector
458:      *
459:      * @param string $word
460:      * @return string
461:      * @link https://github.com/doctrine/inflector
462:      */
463:     protected function singularize($word)
464:     {
465:         if (isset($this->singulars[$word])) {
466:             return $this->singulars[$word];
467:         }
468: 
469:         foreach ($this->singularRules as $rule => $replacement) {
470:             if (preg_match($rule, $word)) {
471:                 $this->singulars[$word] = preg_replace($rule, $replacement, $word);
472:                 return $this->singulars[$word];
473:             }
474:         }
475: 
476:         $this->singulars[$word] = $word;
477:         return $word;
478:     }
479: 
480:     /**
481:      * Camelizes a word
482:      *
483:      * @param string $word The word to camelize.
484:      *
485:      * @return string The camelized word.
486:      */
487:     protected function camelize($word)
488:     {
489:         return lcfirst(str_replace(' ', '', ucwords(strtr($word, '_-', '  '))));
490:     }
491: 
492:     /**
493:      * Convert HTTP method to action name
494:      *
495:      * @param string $method
496:      * @param bool $collection
497:      * @return string
498:      */
499:     protected function methodToAction($method, $collection = false)
500:     {
501:         if ($method == 'GET' && $collection) {
502:             $method = $method . '-collection';
503:         }
504:         return isset($this->methodToAction[$method]) ? $this->methodToAction[$method] : $this->defaultAction;
505:     }
506: 
507:     /**
508:      * @param string $action
509:      * @return bool
510:      */
511:     protected function isAction($action)
512:     {
513:         return $action && !is_numeric($action[0]);
514:     }
515: }
Wei Framework API documentation generated by ApiGen