bzr branch
http://bzr.ed.am/www/slight
1
by Tim Marston
initial commit |
1 |
<?php |
2 |
/** |
|
3 |
* Flight: An extensible micro-framework. |
|
4 |
* |
|
5 |
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> |
|
6 |
* @license MIT, http://flightphp.com/license |
|
7 |
*/ |
|
8 |
||
9 |
namespace flight; |
|
10 |
||
11 |
use flight\core\Loader; |
|
12 |
use flight\core\Dispatcher; |
|
13 |
||
14 |
/** |
|
15 |
* The Engine class contains the core functionality of the framework. |
|
16 |
* It is responsible for loading an HTTP request, running the assigned services, |
|
17 |
* and generating an HTTP response. |
|
18 |
*/ |
|
19 |
class Engine { |
|
20 |
/** |
|
21 |
* Stored variables. |
|
22 |
* |
|
23 |
* @var array |
|
24 |
*/ |
|
25 |
protected $vars; |
|
26 |
||
27 |
/** |
|
28 |
* Class loader. |
|
29 |
* |
|
30 |
* @var object |
|
31 |
*/ |
|
32 |
protected $loader; |
|
33 |
||
34 |
/** |
|
35 |
* Event dispatcher. |
|
36 |
* |
|
37 |
* @var object |
|
38 |
*/ |
|
39 |
protected $dispatcher; |
|
40 |
||
41 |
/** |
|
42 |
* Constructor. |
|
43 |
*/ |
|
44 |
public function __construct() { |
|
45 |
$this->vars = array(); |
|
46 |
||
47 |
$this->loader = new Loader(); |
|
48 |
$this->dispatcher = new Dispatcher(); |
|
49 |
||
50 |
$this->init(); |
|
51 |
} |
|
52 |
||
53 |
/** |
|
54 |
* Handles calls to class methods. |
|
55 |
* |
|
56 |
* @param string $name Method name |
|
57 |
* @param array $params Method parameters |
|
58 |
* @return mixed Callback results |
|
59 |
*/ |
|
60 |
public function __call($name, $params) { |
|
61 |
$callback = $this->dispatcher->get($name); |
|
62 |
||
63 |
if (is_callable($callback)) { |
|
64 |
return $this->dispatcher->run($name, $params); |
|
65 |
} |
|
66 |
||
67 |
$shared = (!empty($params)) ? (bool)$params[0] : true; |
|
68 |
||
69 |
return $this->loader->load($name, $shared); |
|
70 |
} |
|
71 |
||
72 |
/*** Core Methods ***/ |
|
73 |
||
74 |
/** |
|
75 |
* Initializes the framework. |
|
76 |
*/ |
|
77 |
public function init() { |
|
78 |
static $initialized = false; |
|
79 |
$self = $this; |
|
80 |
||
81 |
if ($initialized) { |
|
82 |
$this->vars = array(); |
|
83 |
$this->loader->reset(); |
|
84 |
$this->dispatcher->reset(); |
|
85 |
} |
|
86 |
||
87 |
// Register default components |
|
88 |
$this->loader->register('request', '\flight\net\Request'); |
|
89 |
$this->loader->register('response', '\flight\net\Response'); |
|
90 |
$this->loader->register('router', '\flight\net\Router'); |
|
91 |
$this->loader->register('view', '\flight\template\View', array(), function($view) use ($self) { |
|
92 |
$view->path = $self->get('flight.views.path'); |
|
93 |
}); |
|
94 |
||
95 |
// Register framework methods |
|
96 |
$methods = array( |
|
97 |
'start','stop','route','halt','error','notFound', |
|
98 |
'render','redirect','etag','lastModified','json','jsonp' |
|
99 |
); |
|
100 |
foreach ($methods as $name) { |
|
101 |
$this->dispatcher->set($name, array($this, '_'.$name)); |
|
102 |
} |
|
103 |
||
104 |
// Default configuration settings |
|
105 |
$this->set('flight.base_url', null); |
|
106 |
$this->set('flight.handle_errors', true); |
|
107 |
$this->set('flight.log_errors', false); |
|
108 |
$this->set('flight.views.path', './views'); |
|
109 |
||
110 |
$initialized = true; |
|
111 |
} |
|
112 |
||
113 |
/** |
|
114 |
* Enables/disables custom error handling. |
|
115 |
* |
|
116 |
* @param bool $enabled True or false |
|
117 |
*/ |
|
118 |
public function handleErrors($enabled) |
|
119 |
{ |
|
120 |
if ($enabled) { |
|
121 |
set_error_handler(array($this, 'handleError')); |
|
122 |
set_exception_handler(array($this, 'handleException')); |
|
123 |
} |
|
124 |
else { |
|
125 |
restore_error_handler(); |
|
126 |
restore_exception_handler(); |
|
127 |
} |
|
128 |
} |
|
129 |
||
130 |
/** |
|
131 |
* Custom error handler. Converts errors into exceptions. |
|
132 |
* |
|
133 |
* @param int $errno Error number |
|
134 |
* @param int $errstr Error string |
|
135 |
* @param int $errfile Error file name |
|
136 |
* @param int $errline Error file line number |
|
137 |
* @throws \ErrorException |
|
138 |
*/ |
|
139 |
public function handleError($errno, $errstr, $errfile, $errline) { |
|
140 |
if ($errno & error_reporting()) { |
|
141 |
throw new \ErrorException($errstr, $errno, 0, $errfile, $errline); |
|
142 |
} |
|
143 |
} |
|
144 |
||
145 |
/** |
|
146 |
* Custom exception handler. Logs exceptions. |
|
147 |
* |
|
148 |
* @param \Exception $e Thrown exception |
|
149 |
*/ |
|
150 |
public function handleException(\Exception $e) { |
|
151 |
if ($this->get('flight.log_errors')) { |
|
152 |
error_log($e->getMessage()); |
|
153 |
} |
|
154 |
||
155 |
$this->error($e); |
|
156 |
} |
|
157 |
||
158 |
/** |
|
159 |
* Maps a callback to a framework method. |
|
160 |
* |
|
161 |
* @param string $name Method name |
|
162 |
* @param callback $callback Callback function |
|
163 |
* @throws \Exception If trying to map over a framework method |
|
164 |
*/ |
|
165 |
public function map($name, $callback) { |
|
166 |
if (method_exists($this, $name)) { |
|
167 |
throw new \Exception('Cannot override an existing framework method.'); |
|
168 |
} |
|
169 |
||
170 |
$this->dispatcher->set($name, $callback); |
|
171 |
} |
|
172 |
||
173 |
/** |
|
174 |
* Registers a class to a framework method. |
|
175 |
* |
|
176 |
* @param string $name Method name |
|
177 |
* @param string $class Class name |
|
178 |
* @param array $params Class initialization parameters |
|
179 |
* @param callback $callback Function to call after object instantiation |
|
180 |
* @throws \Exception If trying to map over a framework method |
|
181 |
*/ |
|
182 |
public function register($name, $class, array $params = array(), $callback = null) { |
|
183 |
if (method_exists($this, $name)) { |
|
184 |
throw new \Exception('Cannot override an existing framework method.'); |
|
185 |
} |
|
186 |
||
187 |
$this->loader->register($name, $class, $params, $callback); |
|
188 |
} |
|
189 |
||
190 |
/** |
|
191 |
* Adds a pre-filter to a method. |
|
192 |
* |
|
193 |
* @param string $name Method name |
|
194 |
* @param callback $callback Callback function |
|
195 |
*/ |
|
196 |
public function before($name, $callback) { |
|
197 |
$this->dispatcher->hook($name, 'before', $callback); |
|
198 |
} |
|
199 |
||
200 |
/** |
|
201 |
* Adds a post-filter to a method. |
|
202 |
* |
|
203 |
* @param string $name Method name |
|
204 |
* @param callback $callback Callback function |
|
205 |
*/ |
|
206 |
public function after($name, $callback) { |
|
207 |
$this->dispatcher->hook($name, 'after', $callback); |
|
208 |
} |
|
209 |
||
210 |
/** |
|
211 |
* Gets a variable. |
|
212 |
* |
|
213 |
* @param string $key Key |
|
214 |
* @return mixed |
|
215 |
*/ |
|
216 |
public function get($key = null) { |
|
217 |
if ($key === null) return $this->vars; |
|
218 |
||
219 |
return isset($this->vars[$key]) ? $this->vars[$key] : null; |
|
220 |
} |
|
221 |
||
222 |
/** |
|
223 |
* Sets a variable. |
|
224 |
* |
|
225 |
* @param mixed $key Key |
|
226 |
* @param string $value Value |
|
227 |
*/ |
|
228 |
public function set($key, $value = null) { |
|
229 |
if (is_array($key) || is_object($key)) { |
|
230 |
foreach ($key as $k => $v) { |
|
231 |
$this->vars[$k] = $v; |
|
232 |
} |
|
233 |
} |
|
234 |
else { |
|
235 |
$this->vars[$key] = $value; |
|
236 |
} |
|
237 |
} |
|
238 |
||
239 |
/** |
|
240 |
* Checks if a variable has been set. |
|
241 |
* |
|
242 |
* @param string $key Key |
|
243 |
* @return bool Variable status |
|
244 |
*/ |
|
245 |
public function has($key) { |
|
246 |
return isset($this->vars[$key]); |
|
247 |
} |
|
248 |
||
249 |
/** |
|
250 |
* Unsets a variable. If no key is passed in, clear all variables. |
|
251 |
* |
|
252 |
* @param string $key Key |
|
253 |
*/ |
|
254 |
public function clear($key = null) { |
|
255 |
if (is_null($key)) { |
|
256 |
$this->vars = array(); |
|
257 |
} |
|
258 |
else { |
|
259 |
unset($this->vars[$key]); |
|
260 |
} |
|
261 |
} |
|
262 |
||
263 |
/** |
|
264 |
* Adds a path for class autoloading. |
|
265 |
* |
|
266 |
* @param string $dir Directory path |
|
267 |
*/ |
|
268 |
public function path($dir) { |
|
269 |
$this->loader->addDirectory($dir); |
|
270 |
} |
|
271 |
||
272 |
/*** Extensible Methods ***/ |
|
273 |
||
274 |
/** |
|
275 |
* Starts the framework. |
|
276 |
*/ |
|
277 |
public function _start() { |
|
278 |
$dispatched = false; |
|
279 |
$self = $this; |
|
280 |
$request = $this->request(); |
|
281 |
$response = $this->response(); |
|
282 |
$router = $this->router(); |
|
283 |
||
284 |
// Flush any existing output |
|
285 |
if (ob_get_length() > 0) { |
|
286 |
$response->write(ob_get_clean()); |
|
287 |
} |
|
288 |
||
289 |
// Enable output buffering |
|
290 |
ob_start(); |
|
291 |
||
292 |
// Enable error handling |
|
293 |
$this->handleErrors($this->get('flight.handle_errors')); |
|
294 |
||
295 |
// Disable caching for AJAX requests |
|
296 |
if ($request->ajax) { |
|
297 |
$response->cache(false); |
|
298 |
} |
|
299 |
||
300 |
// Allow post-filters to run |
|
301 |
$this->after('start', function() use ($self) { |
|
302 |
$self->stop(); |
|
303 |
}); |
|
304 |
||
305 |
// Route the request |
|
306 |
while ($route = $router->route($request)) { |
|
307 |
$params = array_values($route->params); |
|
308 |
||
309 |
$continue = $this->dispatcher->execute( |
|
310 |
$route->callback, |
|
311 |
$params |
|
312 |
); |
|
313 |
||
314 |
$dispatched = true; |
|
315 |
||
316 |
if (!$continue) break; |
|
317 |
||
318 |
$router->next(); |
|
319 |
||
320 |
$dispatched = false; |
|
321 |
} |
|
322 |
||
323 |
if (!$dispatched) { |
|
324 |
$this->notFound(); |
|
325 |
} |
|
326 |
} |
|
327 |
||
328 |
/** |
|
329 |
* Stops the framework and outputs the current response. |
|
330 |
* |
|
331 |
* @param int $code HTTP status code |
|
332 |
*/ |
|
333 |
public function _stop($code = 200) { |
|
334 |
$this->response() |
|
335 |
->status($code) |
|
336 |
->write(ob_get_clean()) |
|
337 |
->send(); |
|
338 |
} |
|
339 |
||
340 |
/** |
|
341 |
* Stops processing and returns a given response. |
|
342 |
* |
|
343 |
* @param int $code HTTP status code |
|
344 |
* @param string $message Response message |
|
345 |
*/ |
|
346 |
public function _halt($code = 200, $message = '') { |
|
347 |
$this->response(false) |
|
348 |
->status($code) |
|
349 |
->write($message) |
|
350 |
->send(); |
|
351 |
} |
|
352 |
||
353 |
/** |
|
354 |
* Sends an HTTP 500 response for any errors. |
|
355 |
* |
|
356 |
* @param \Exception Thrown exception |
|
357 |
*/ |
|
358 |
public function _error(\Exception $e) { |
|
359 |
$msg = sprintf('<h1>500 Internal Server Error</h1>'. |
|
360 |
'<h3>%s (%s)</h3>'. |
|
361 |
'<pre>%s</pre>', |
|
362 |
$e->getMessage(), |
|
363 |
$e->getCode(), |
|
364 |
$e->getTraceAsString() |
|
365 |
); |
|
366 |
||
367 |
try { |
|
368 |
$this->response(false) |
|
369 |
->status(500) |
|
370 |
->write($msg) |
|
371 |
->send(); |
|
372 |
} |
|
373 |
catch (\Exception $ex) { |
|
374 |
exit($msg); |
|
375 |
} |
|
376 |
} |
|
377 |
||
378 |
/** |
|
379 |
* Sends an HTTP 404 response when a URL is not found. |
|
380 |
*/ |
|
381 |
public function _notFound() { |
|
382 |
$this->response(false) |
|
383 |
->status(404) |
|
384 |
->write( |
|
385 |
'<h1>404 Not Found</h1>'. |
|
386 |
'<h3>The page you have requested could not be found.</h3>'. |
|
387 |
str_repeat(' ', 512) |
|
388 |
) |
|
389 |
->send(); |
|
390 |
} |
|
391 |
||
392 |
/** |
|
393 |
* Routes a URL to a callback function. |
|
394 |
* |
|
395 |
* @param string $pattern URL pattern to match |
|
396 |
* @param callback $callback Callback function |
|
397 |
* @param boolean $pass_route Pass the matching route object to the callback |
|
398 |
*/ |
|
399 |
public function _route($pattern, $callback, $pass_route = false) { |
|
400 |
$this->router()->map($pattern, $callback, $pass_route); |
|
401 |
} |
|
402 |
||
403 |
/** |
|
404 |
* Redirects the current request to another URL. |
|
405 |
* |
|
406 |
* @param string $url URL |
|
407 |
* @param int $code HTTP status code |
|
408 |
*/ |
|
409 |
public function _redirect($url, $code = 303) { |
|
410 |
$base = $this->get('flight.base_url'); |
|
411 |
||
412 |
if ($base === null) { |
|
413 |
$base = $this->request()->base; |
|
414 |
} |
|
415 |
||
416 |
// Append base url to redirect url |
|
417 |
if ($base != '/' && strpos($url, '://') === false) { |
|
418 |
$url = preg_replace('#/+#', '/', $base.'/'.$url); |
|
419 |
} |
|
420 |
||
421 |
$this->response(false) |
|
422 |
->status($code) |
|
423 |
->header('Location', $url) |
|
424 |
->write($url) |
|
425 |
->send(); |
|
426 |
} |
|
427 |
||
428 |
/** |
|
429 |
* Renders a template. |
|
430 |
* |
|
431 |
* @param string $file Template file |
|
432 |
* @param array $data Template data |
|
433 |
* @param string $key View variable name |
|
434 |
*/ |
|
435 |
public function _render($file, $data = null, $key = null) { |
|
436 |
if ($key !== null) { |
|
437 |
$this->view()->set($key, $this->view()->fetch($file, $data)); |
|
438 |
} |
|
439 |
else { |
|
440 |
$this->view()->render($file, $data); |
|
441 |
} |
|
442 |
} |
|
443 |
||
444 |
/** |
|
445 |
* Sends a JSON response. |
|
446 |
* |
|
447 |
* @param mixed $data JSON data |
|
448 |
* @param int $code HTTP status code |
|
449 |
* @param bool $encode Whether to perform JSON encoding |
|
450 |
*/ |
|
451 |
public function _json($data, $code = 200, $encode = true) { |
|
452 |
$json = ($encode) ? json_encode($data) : $data; |
|
453 |
||
454 |
$this->response(false) |
|
455 |
->status($code) |
|
456 |
->header('Content-Type', 'application/json') |
|
457 |
->write($json) |
|
458 |
->send(); |
|
459 |
} |
|
460 |
||
461 |
/** |
|
462 |
* Sends a JSONP response. |
|
463 |
* |
|
464 |
* @param mixed $data JSON data |
|
465 |
* @param string $param Query parameter that specifies the callback name. |
|
466 |
* @param int $code HTTP status code |
|
467 |
* @param bool $encode Whether to perform JSON encoding |
|
468 |
*/ |
|
469 |
public function _jsonp($data, $param = 'jsonp', $code = 200, $encode = true) { |
|
470 |
$json = ($encode) ? json_encode($data) : $data; |
|
471 |
||
472 |
$callback = $this->request()->query[$param]; |
|
473 |
||
474 |
$this->response(false) |
|
475 |
->status($code) |
|
476 |
->header('Content-Type', 'application/javascript') |
|
477 |
->write($callback.'('.$json.');') |
|
478 |
->send(); |
|
479 |
} |
|
480 |
||
481 |
/** |
|
482 |
* Handles ETag HTTP caching. |
|
483 |
* |
|
484 |
* @param string $id ETag identifier |
|
485 |
* @param string $type ETag type |
|
486 |
*/ |
|
487 |
public function _etag($id, $type = 'strong') { |
|
488 |
$id = (($type === 'weak') ? 'W/' : '').$id; |
|
489 |
||
490 |
$this->response()->header('ETag', $id); |
|
491 |
||
492 |
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && |
|
493 |
$_SERVER['HTTP_IF_NONE_MATCH'] === $id) { |
|
494 |
$this->halt(304); |
|
495 |
} |
|
496 |
} |
|
497 |
||
498 |
/** |
|
499 |
* Handles last modified HTTP caching. |
|
500 |
* |
|
501 |
* @param int $time Unix timestamp |
|
502 |
*/ |
|
503 |
public function _lastModified($time) { |
|
504 |
$this->response()->header('Last-Modified', date(DATE_RFC1123, $time)); |
|
505 |
||
506 |
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && |
|
507 |
strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time) { |
|
508 |
$this->halt(304); |
|
509 |
} |
|
510 |
} |
|
511 |
} |