/www/slight

To get this branch, use:
bzr branch http://bzr.ed.am/www/slight

« back to all changes in this revision

Viewing changes to flight/Engine.php

  • Committer: Tim Marston
  • Date: 2015-08-05 18:48:29 UTC
  • Revision ID: tim@ed.am-20150805184829-c3qj13d1h89pkb2l
initial commit

Show diffs side-by-side

added added

removed removed

 
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
}