1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie\Routing;
13
14 use ICanBoogie\HTTP\RedirectResponse;
15 use ICanBoogie\HTTP\Request;
16 use ICanBoogie\HTTP\Response;
17
18 use ICanBoogie\Route;
19 use ICanBoogie\Routes;
20
21 /**
22 * Dispatches requests among the defined routes.
23 *
24 * If a route matching the request is found, the `$route` and `$decontextualized_path`
25 * properties are add to the request object. `$route` holds the route object,
26 * `$decontextualized_path` holds the decontextualized path. The path is decontextualized using
27 * the {@link decontextualize()} function.
28 *
29 * <pre>
30 * use ICanBoogie\HTTP\Dispatcher;
31 *
32 * $dispatcher = new Dispatcher(array('routes' => 'ICanBoogie\Routing\Dispatcher'));
33 * </pre>
34 */
35 class Dispatcher implements \ICanBoogie\HTTP\IDispatcher
36 {
37 public function __invoke(Request $request)
38 {
39 $decontextualized_path = decontextualize($request->normalized_path);
40
41 if ($decontextualized_path != '/')
42 {
43 $decontextualized_path = rtrim($decontextualized_path, '/');
44 }
45
46 $route = Routes::get()->find($decontextualized_path, $captured, $request->method);
47
48 if (!$route)
49 {
50 return;
51 }
52
53 if ($route->location)
54 {
55 return new RedirectResponse(contextualize($route->location), 302);
56 }
57
58 $request->path_params = $captured + $request->path_params;
59 $request->params = $captured + $request->params;
60 $request->route = $route;
61 $request->decontextualized_path = $decontextualized_path;
62
63 return $this->dispatch($route, $request);
64 }
65
66 /**
67 * Dispatches the route.
68 *
69 * @param Route $route
70 * @param Request $request
71 *
72 * @return Response|null
73 */
74 protected function dispatch(Route $route, Request $request)
75 {
76 new Dispatcher\BeforeDispatchEvent($this, $route, $request, $response);
77
78 if (!$response)
79 {
80 $controller = $route->controller;
81
82 #
83 # if the controller is not a callable then it is considered as a class name and
84 # is used to instantiate the controller.
85 #
86
87 if (!is_callable($controller))
88 {
89 $controller_class = $controller;
90 $controller = new $controller_class($route);
91 }
92
93 $response = $controller($request);
94
95 if ($response !== null && !($response instanceof Response))
96 {
97 $response = new Response($response, 200, array
98 (
99 'Content-Type' => 'text/html; charset=utf-8'
100 ));
101 }
102 }
103
104 new Dispatcher\DispatchEvent($this, $route, $request, $response);
105
106 return $response;
107 }
108
109 /**
110 * Fires {@link \ICanBoogie\Routing\Dispatcher\RescueEvent} and returns the response provided
111 * by third parties. If no response was provided, the exception (or the exception provided by
112 * third parties) is rethrown.
113 *
114 * @return \ICanBoogie\HTTP\Response
115 */
116 public function rescue(\Exception $exception, Request $request)
117 {
118 if (isset($request->route))
119 {
120 new Dispatcher\RescueEvent($exception, $request, $request->route, $response);
121
122 if ($response)
123 {
124 return $response;
125 }
126 }
127
128 throw $exception;
129 }
130 }
131
132 /*
133 * Events
134 */
135
136 namespace ICanBoogie\Routing\Dispatcher;
137
138 use ICanBoogie\HTTP\Request;
139 use ICanBoogie\HTTP\Response;
140 use ICanBoogie\Route;
141 use ICanBoogie\Routing\Dispatcher;
142
143 /**
144 * Event class for the `ICanBoogie\Routing\Dispatcher::dispatch:before` event.
145 *
146 * Third parties may use this event to provide a response to the request before the route is
147 * mapped. The event is usually used by third parties to redirect requests or provide cached
148 * responses.
149 */
150 class BeforeDispatchEvent extends \ICanBoogie\Event
151 {
152 /**
153 * The route.
154 *
155 * @var \ICanBoogie\Route
156 */
157 public $route;
158
159 /**
160 * The HTTP request.
161 *
162 * @var \ICanBoogie\HTTP\Request
163 */
164 public $request;
165
166 /**
167 * Reference to the HTTP response.
168 *
169 * @var \ICanBoogie\HTTP\Response
170 */
171 public $response;
172
173 /**
174 * The event is constructed with the type `dispatch:before`.
175 *
176 * @param Dispatcher $target
177 * @param array $payload
178 */
179 public function __construct(Dispatcher $target, Route $route, Request $request, &$response)
180 {
181 if ($response !== null && !($response instanceof Response))
182 {
183 throw new \InvalidArgumentException('$response must be an instance of ICanBoogie\HTTP\Response. Given: ' . get_class($response) . '.');
184 }
185
186 $this->route = $route;
187 $this->request = $request;
188 $this->response = &$response;
189
190 parent::__construct($target, 'dispatch:before');
191 }
192 }
193
194 /**
195 * Event class for the `ICanBoogie\Routing\Dispatcher::dispatch` event.
196 *
197 * Third parties may use this event to alter the response before it is returned by the dispatcher.
198 */
199 class DispatchEvent extends \ICanBoogie\Event
200 {
201 /**
202 * The route.
203 *
204 * @var \ICanBoogie\Route
205 */
206 public $route;
207
208 /**
209 * The request.
210 *
211 * @var \ICanBoogie\HTTP\Request
212 */
213 public $request;
214
215 /**
216 * Reference to the response.
217 *
218 * @var \ICanBoogie\HTTP\Response|null
219 */
220 public $response;
221
222 /**
223 * The event is constructed with the type `dispatch`.
224 *
225 * @param Dispatcher $target
226 * @param array $payload
227 */
228 public function __construct(Dispatcher $target, Route $route, Request $request, &$response)
229 {
230 $this->route = $route;
231 $this->request = $request;
232 $this->response = &$response;
233
234 parent::__construct($target, 'dispatch');
235 }
236 }
237
238 /**
239 * Event class for the `ICanBoogie\Routing\Dispatcher::rescue` event.
240 *
241 * Third parties may use this event to _rescue_ an exception by providing a suitable response.
242 * Third parties may also use this event to replace the exception to rethrow.
243 */
244 class RescueEvent extends \ICanBoogie\Exception\RescueEvent
245 {
246 /**
247 * Route to rescue.
248 *
249 * @var \ICanBoogie\Route
250 */
251 public $route;
252
253 /**
254 * Initializes the {@link $route} property.
255 *
256 * @param \Exception $target
257 * @param \ICanBoogie\HTTP\Request $request
258 * @param \ICanBoogie\Route $route
259 * @param \ICanBoogie\HTTP\Response|null $response
260 */
261 public function __construct(\Exception &$target, Request $request, Route $route, &$response)
262 {
263 $this->route = $route;
264
265 parent::__construct($target, $request, $response);
266 }
267 }