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: * Pattern matching router.
19: * This utility can be used on any phase of Pinoco runtime process.
20: *
21: * <code>
22: * $router = Pinoco::instance()->route();
23: * $router->pass(array('', 'index.html',))
24: * ->on('list', function() use($self, $data) {
25: * // ...
26: * })
27: * ->on('show/{id}', function($id) use($self, $data) {
28: * // ...
29: * })
30: * ->on('POST:post', function() use($self, $data) {
31: * // ...
32: * })
33: * ->on('GET:post', array($this, 'forbidden'))
34: * ->on('*', array($this, 'notfound'));
35: * </code>
36: *
37: * Pattern rules:
38: *
39: * <pre>
40: * '/index' : Fixed route.
41: * 'index' : Fixed route in relative path from current (_enter) hook.
42: * '/show/{id}' : Parametrized one. Such path elements are passed to handler.
43: * 'GET: /edit/{id}' or
44: * 'POST: /edit/{id}' : Different HTTP methods can be different routes.
45: * '*:*' : Matches any patterns. Useful to be bound to Pinoco::notfound() or forbidden().
46: * </pre>
47: *
48: * @package Pinoco
49: */
50: class Pinoco_Router
51: {
52: /** @var bool */
53: protected $matched;
54:
55: private $_tmp_params;
56:
57: /**
58: * Constructor
59: *
60: * @param Pinoco $pinoco
61: */
62: public function __construct($pinoco)
63: {
64: $this->pinoco = $pinoco;
65: $this->matched = false;
66: }
67:
68: /**
69: * Routing rules which binds URI path to any callable.
70: *
71: * Specified handler is called immediately if the route matches. Please note that
72: * this method is not a definition but invoker.
73: *
74: * @param string|array $route
75: * @param callback $handler
76: * @return $this
77: */
78: public function on($route, $handler)
79: {
80: if ($this->matched) {
81: return $this;
82: }
83:
84: if (is_array($route)) {
85: foreach ($route as $r) {
86: $this->on($r, $handler);
87: }
88: return $this;
89: }
90:
91: list($method, $path) = $this->extractRoute($route);
92:
93: if ($this->matchesWithMethod($method)) {
94: return $this;
95: }
96:
97: $matchParams = $this->matchesWithPath($path);
98: if (!is_null($matchParams)) {
99: call_user_func_array($handler, $matchParams);
100: $this->matched = true;
101: }
102:
103: return $this;
104: }
105:
106: /**
107: * Specified route is ignored and delegated to the next script step.
108: * This method is useful to ignore matching patterns below. (e.g. '*:*')
109: *
110: * @param string|array $route
111: * @return $this
112: */
113: public function pass($route)
114: {
115: if ($this->matched) {
116: return $this;
117: }
118:
119: if (is_array($route)) {
120: array_map(array($this, 'pass'), $route);
121: return $this;
122: }
123:
124: list($method, $path) = $this->extractRoute($route);
125:
126: if ($this->matchesWithMethod($method)) {
127: return $this;
128: }
129:
130: $matchParams = $this->matchesWithPath($path);
131: if (!is_null($matchParams)) {
132: $this->matched = true;
133: }
134:
135: return $this;
136: }
137:
138: /**
139: * Returns which a matching router was found and handled or not.
140: *
141: * @return bool
142: */
143: public function wasMatched()
144: {
145: return $this->matched;
146: }
147:
148: /**
149: * @param string $route
150: * @return array
151: */
152: private function extractRoute($route)
153: {
154: $delimpos = strpos($route, ':');
155: if ($delimpos !== false) {
156: $method = strtoupper(trim(substr($route, 0, $delimpos)));
157: $path = trim(substr($route, $delimpos + 1));
158: return array($method, $path);
159: } else {
160: $method = '*';
161: $path = trim($route);
162: return array($method, $path);
163: }
164: }
165:
166: /**
167: * @param $path
168: * @return array
169: */
170: private function matchesWithPath($path)
171: {
172: if (preg_match('|^/|', $path)) {
173: $uri = $this->pinoco->path;
174: } else {
175: // Pinoco::subpath is set when running hook script.
176: // If not, it means taht current flow is out of hook, then
177: // Pinoco::path should be default instead.
178: $uri = !is_null($this->pinoco->subpath) ? $this->pinoco->subpath : $this->pinoco->path;
179: }
180:
181: $this->_tmp_params = array();
182: $pathregexp = $path;
183: $pathregexp = preg_replace('/\|/', '\|', $pathregexp);
184: $pathregexp = preg_replace('/\+/', '\+', $pathregexp);
185: $pathregexp = preg_replace('/\*+/', '.+?', $pathregexp);
186: $pathregexp = preg_replace_callback('/\{(.*?)\}/', array($this, '__each_path_args'), $pathregexp);
187:
188: if (preg_match('|^' . $pathregexp . '$|', $uri, $matches)) {
189: array_shift($matches);
190: return $matches;
191: }
192: else {
193: return null;
194: }
195: }
196:
197: /**
198: * @param $matches
199: * @return string
200: * @internal
201: */
202: public function __each_path_args($matches)
203: {
204: $name = $matches[1];
205: $this->_tmp_params[] = $name;
206: return '([^/]+?)';
207: }
208:
209: /**
210: * @param $method
211: * @return bool
212: */
213: private function matchesWithMethod($method)
214: {
215: return $method != '*' && $method != $this->pinoco->request->method;
216: }
217: }