commit ce8dbfc435b15ac471ca71394dc6d3d21f95308a Author: Mike Cao Date: Thu Mar 31 07:49:55 2011 +0000 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cfbe851 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2011 Mike Cao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0492ec8 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Flight + +Flight is an extensible micro-framework for PHP. + +## Example + + include 'flight/Flight.php'; + + Flight::route('/', function(){ + echo 'hello world!'; + }); + + Flight::start(); diff --git a/flight/Flight.php b/flight/Flight.php new file mode 100644 index 0000000..1bd5b08 --- /dev/null +++ b/flight/Flight.php @@ -0,0 +1,546 @@ + + * @license http://www.opensource.org/licenses/mit-license.php + * @version 0.1 + */ +class Flight { + /** + * Stored variables. + * + * @var array + */ + protected static $vars = array(); + + /** + * Registered classes. + * + * @var array + */ + protected static $classes = array(); + + /** + * Mapped methods. + * + * @var array + */ + protected static $methods = array(); + + /** + * Method filters. + * + * @var array + */ + protected static $filters = array(); + + // Don't allow object instantiation + private function __construct() {} + private function __destruct() {} + private function __clone() {} + + /** + * Handles calls to static methods. + * + * @param string $name Method name + * @param array $args Method parameters + */ + public static function __callStatic($name, $params) { + // Check if call is mapped to a method + if (isset(self::$methods[$name]) || method_exists(__CLASS__, '_'.$name)) { + $method = self::$methods[$name] ?: array(__CLASS__, '_'.$name); + + // Run pre-filters + if (!empty(self::$filters['before'][$name])) { + self::filter(self::$filters['before'][$name], $params); + } + + // Run requested method + $output = self::execute($method, $params); + + // Run post-filters + if (!empty(self::$filters['after'][$name])) { + self::filter(self::$filters['after'][$name], $output); + } + + return $output; + } + + // Otherwise try to autoload class + return self::load($name, (bool)$params[0]); + } + + /** + * Maps a callback to a framework method. + * + * @param string $name Method name + * @param callback $callback Callback function + */ + public static function map($name, $callback) { + if (method_exists(__CLASS__, $name)) { + throw new Exception('Cannot override an existing framework method.'); + } + self::$methods[$name] = $callback; + } + + /** + * Registers a class to a framework method. + * + * @param string $name Method name + * @param string $class Class name + * @param array $params Class initialization parameters + * @param callback $callback Function to call after object instantiation + */ + public static function register($name, $class, array $params = array(), $callback = null) { + if (method_exists(__CLASS__, $name)) { + throw new Exception('Cannot override an existing framework method.'); + } + self::$classes[$name] = array($class, $params, $callback); + } + + /** + * Loads a registered class. + * + * @param string $class Class name + * @param array $method List of methods to load + */ + public static function load($name, $shared = true) { + if (isset(self::$classes[$name])) { + list($class, $params, $callback) = self::$classes[$name]; + + $obj = ($shared) ? + self::getInstance($class, $params) : + self::getClass($class, $params); + + if ($callback) self::execute($callback, $ref = array($obj)); + + return $obj; + } + + return self::getInstance(ucfirst($name)); + } + + /** + * Adds a pre-filter to a method. + * + * @param string $name Method name + * @param callback $callback Callback function + */ + public static function before($name, $callback) { + self::$filters['before'][$name][] = $callback; + } + + /** + * Adds a post-filter to a method. + * + * @param string $name Method name + * @param callback $callback Callback function + */ + public static function after($name, $callback) { + self::$filters['after'][$name][] = $callback; + } + + /** + * Executes a callback function. + * + * @param callback $callback Callback function + * @param array $params Function parameters + * @return mixed Function results + */ + public static function execute($callback, &$params) { + if (is_callable($callback)) { + return is_array($callback) ? + self::invokeMethod($callback, $params) : + self::callFunction($callback, $params); + } + } + + /** + * Executes a chain of method filters. + * + * @param array $filters Chain of filters + * @param array $params Method parameters + * @param mixed $data Method output + */ + public static function filter($filters, &$data) { + foreach ($filters as $callback) { + $continue = self::execute($callback, $params = array(&$data)); + if ($continue === false) break; + } + } + + /** + * Calls a function. + * + * @param string $func Name of function to call + * @param array $params Function parameters + */ + public static function callFunction($func, array &$params = array()) { + switch (count($params)) { + case 0: + return $func(); + case 1: + return $func($params[0]); + case 2: + return $func($params[0], $params[1]); + case 3: + return $func($params[0], $params[1], $params[2]); + case 4: + return $func($params[0], $params[1], $params[2], $params[3]); + case 5: + return $func($params[0], $params[1], $params[2], $params[3], $params[4]); + default: + return call_user_func_array($func, $params); + } + } + + /** + * Invokes a method. + * + * @param mixed $func Class method as either an array or string + * @param array $params Class initialization parameters + */ + public static function invokeMethod($func, array &$params = array()) { + list($class, $method) = $func; + + switch (count($params)) { + case 0: + return $class::$method(); + case 1: + return $class::$method($params[0]); + case 2: + return $class::$method($params[0], $params[1]); + case 3: + return $class::$method($params[0], $params[1], $params[2]); + case 4: + return $class::$method($params[0], $params[1], $params[2], $params[3]); + case 5: + return $class::$method($params[0], $params[1], $params[2], $params[3], $params[4]); + default: + return call_user_func_array($func, $params); + } + } + + /** + * Gets a single instance of a class. + * + * @param string $class Class name + * @param array $params Class initialization parameters + */ + public static function getInstance($class, array $params = array()) { + static $instances = array(); + + if (!isset($instances[$class])) { + $instances[$class] = self::getClass($class, $params); + } + + return $instances[$class]; + } + + /** + * Gets a class object. + * + * @param string $class Class name + * @param array $params Class initialization parameters + */ + public static function getClass($class, array $params = array()) { + switch (count($params)) { + case 0: + return new $class(); + case 1: + return new $class($params[0]); + case 2: + return new $class($params[0], $params[1]); + case 3: + return new $class($params[0], $params[1], $params[2]); + case 4: + return new $class($params[0], $params[1], $params[2], $params[3]); + case 5: + return new $class($params[0], $params[1], $params[2], $params[3], $params[4]); + default: + $ref_class = new ReflectionClass($class); + return $ref_class->newInstanceArgs($params); + } + } + + /** + * Gets a variable. + * + * @param string $key Key + * @return mixed + */ + public static function get($key) { + return self::$vars[$key]; + } + + /** + * Sets a variable. + * + * @param mixed $key Key + * @param string $value Value + */ + public static function set($key, $value = null) { + // If key is an array, save each key value pair + if (is_array($key)) { + foreach ($key as $k => $v) { + self::$vars[$k] = $v; + } + } + // If key is an object, save each property + else if (is_object($key)) { + foreach (get_object_vars($key) as $k => $v) { + self::$vars[$k] = $v; + } + } + else if (is_string($key)) { + self::$vars[$key] = $value; + } + } + + /** + * Checks if a variable exists. + * + * @param string $key Key + * @return bool Variable status + */ + public static function exists($key) { + return isset(self::$vars[$key]); + } + + /** + * Unsets a variable. If no key is passed in, clear all variables. + * + * @param string $key Key + */ + public static function clear($key = null) { + if (is_null($key)) { + self::$vars = array(); + } + else { + unset(self::$vars[$key]); + } + } + + /** + * Initializes the framework. + */ + public static function init() { + static $initialized = false; + + if (!$initialized) { + // Register autoloader + spl_autoload_register(array(__CLASS__, 'autoload')); + + // Handle errors internally + set_error_handler(array(__CLASS__, 'handleError')); + + // Handle exceptions internally + set_exception_handler(array(__CLASS__, 'handleException')); + + // Turn off notices + error_reporting (E_ALL ^ E_NOTICE); + + // Fix magic quotes + if (get_magic_quotes_gpc()) { + $func = function ($value) use (&$func) { + return is_array($value) ? array_map($func, $value) : stripslashes($value); + }; + $_GET = array_map($func, $_GET); + $_POST = array_map($func, $_POST); + $_COOKIE = array_map($func, $_COOKIE); + } + + // Enable output buffering + ob_start(); + + $initialized = true; + } + } + + /** + * Autoloads classes. + * + * @param string $class Class name + */ + public static function autoload($class) { + $file = str_replace('\\', '/', str_replace('_', '/', $class)).'.php'; + $base = (strpos($file, '/') === false) ? __DIR__ : (self::get('flight.lib.path') ?: '.'); + + if (file_exists($base.'/'.$file)) { + require $base.'/'.$file; + } + else { + throw new Exception('Unable to load file: '.$base.'/'.$file); + } + } + + /** + * Custom error handler. + */ + public static function handleError($errno, $errstr, $errfile, $errline) { + if (in_array($errno, array(E_USER_ERROR, E_RECOVERABLE_ERROR))) { + static::error(new ErrorException($errstr, 0, $errno, $errfile, $errline)); + } + } + + /** + * Custom exception handler. + */ + public static function handleException(Exception $e) { + try { + static::error($e); + } + catch (Exception $ex) { + exit( + '

500 Internal Server Error

'. + '

'.$ex->getMessage().'

'. + '
'.$ex->getTraceAsString().'
' + ); + } + } + + /** + * Routes a URL to a callback function. + * + * @param string $pattern URL pattern to match + * @param callback $callback Callback function + */ + public static function _route($pattern, $callback) { + self::router()->map($pattern, $callback); + } + + /** + * Start the framework. + */ + public static function _start() { + // Route the request + $result = self::router()->route(self::request()); + + if ($result !== false) { + list($callback, $params) = $result; + + self::execute($callback, $params); + } + else { + self::notFound(); + } + + // Disable caching for AJAX requests + if (self::request()->isAjax) { + self::response()->cache(false); + } + + // Allow post-filters to run + self::after('start', array(__CLASS__, 'stop')); + } + + /** + * Stops the framework and outputs the current response. + */ + public static function _stop() { + self::response()-> + write(ob_get_clean())-> + send(); + } + + /** + * Stops processing and returns a given response. + * + * @param int $code HTTP status code + * @param int $msg Response text + */ + public static function _halt($code = 200, $text = '') { + self::response(false)-> + status($code)-> + write($text)-> + cache(false)-> + send(); + } + + /** + * Sends an HTTP 500 response for any errors. + * + * @param object $ex Exception + */ + public static function _error(Exception $e) { + self::response(false)-> + status(500)-> + write( + '

500 Internal Server Error

'. + '

'.$e->getMessage().'

'. + '
'.$e->getTraceAsString().'
' + )-> + send(); + } + + /** + * Sends an HTTP 404 response when a URL is not found. + */ + public static function _notFound() { + self::response(false)-> + status(404)-> + write( + '

404 Not Found

'. + '

The page you have requested could not be found.

'. + str_repeat(' ', 512) + )-> + send(); + } + + /** + * Redirects the current request to another URL. + * + * @param string $url URL + */ + public static function _redirect($url, $code = 303) { + self::response(false)-> + status($code)-> + header('Location', $url)-> + write($url)-> + send(); + } + + /** + * Renders a template. + * + * @param string $file Template file + * @param array $data Template data + */ + public static function _render($file, $data = null) { + self::view()->render($file, $data); + } + + /** + * Handles ETag HTTP caching. + * + * @param string $id ETag identifier + * @param string $type ETag type + */ + public static function _etag($id, $type = 'strong') { + $id = (($type === 'weak') ? 'W/' : '').$id; + + self::response()->header('ETag', $id); + + if ($_SERVER['HTTP_IF_NONE_MATCH'] === $id) { + self::halt(304); + } + } + + /** + * Handles last modified HTTP caching. + * + * @param int $time Unix timestamp + */ + public static function _lastModified($time) { + self::response()->header('Last-Modified', date(DATE_RFC1123, $time)); + + if (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time) { + self::halt(304); + } + } +} + +// Initialize framework on include +Flight::init(); +?> diff --git a/flight/Request.php b/flight/Request.php new file mode 100644 index 0000000..7ca202a --- /dev/null +++ b/flight/Request.php @@ -0,0 +1,66 @@ + + * @license http://www.opensource.org/licenses/mit-license.php + * @version 0.1 + */ +class Request { + /** + * Constructor. + * + * @param array $config Request configuration + */ + public function __construct($config = array()) { + // Default properties + if (empty($config)) { + $config = array( + 'url' => $_SERVER['REQUEST_URI'], + 'base' => str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'])), + 'method' => $_SERVER['REQUEST_METHOD'], + 'referrer' => $_SERVER['HTTP_REFERER'], + 'ipAddress' => $_SERVER['REMOTE_ADDR'], + 'isAjax' => ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'), + 'query' => array(), + 'data' => $_POST, + 'cookies' => $_COOKIE, + 'files' => $_FILES + ); + } + + self::init($config); + } + + /** + * Initialize request properties. + * + * @param array $properties Array of request properties + */ + public function init($properties) { + foreach ($properties as $name => $value) { + $this->{$name} = $value; + } + + if ($this->base != '/' && strpos($this->url, $this->base) === 0) { + $this->url = substr($this->url, strlen($this->base)); + } + + $this->query = self::parseQuery($this->url); + } + + /** + * Parse query parameters from a URL. + */ + public function parseQuery($url) { + $params = array(); + + $args = parse_url($url); + if (isset($args['query'])) { + parse_str($args['query'], $params); + } + + return $params; + } +} +?> diff --git a/flight/Response.php b/flight/Response.php new file mode 100644 index 0000000..8a5ae4f --- /dev/null +++ b/flight/Response.php @@ -0,0 +1,165 @@ + + * @license http://www.opensource.org/licenses/mit-license.php + * @version 0.1 + */ +class Response { + protected $headers = array(); + protected $status = 200; + protected $body; + + public static $codes = array( + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported' + ); + + /** + * Sets the HTTP status of the response. + * + * @param int $code HTTP status code. + */ + public function status($code) { + if (array_key_exists($code, self::$codes)) { + if (strpos(php_sapi_name(), 'cgi') !== false) { + header('Status: '.$code.' '.self::$codes[$code], true); + } + else { + header(($_SERVER['SERVER_PROTOCOL'] ?: 'HTTP/1.1').' '.self::$codes[$code], true, $code); + } + } + else { + throw new Exception('Invalid status code.'); + } + + return $this; + } + + /** + * Adds a header to the response. + * + * @param string|array $key Header name or array of names and values + * @param string $value Header value + */ + public function header($name, $value = null) { + if (is_array($name)) { + foreach ($name as $k => $v) { + $this->headers[$k] = $v; + } + } + else { + $this->headers[$name] = $value; + } + + return $this; + } + + /** + * Writes content to the response body. + * + * @param string $str Response content + */ + public function write($str) { + $this->body .= $str; + + return $this; + } + + /** + * Clears the response. + */ + public function clear() { + $this->headers = array(); + $this->status = 200; + $this->body = ''; + + return $this; + } + + /** + * Sets caching headers for the response. + * + * @param int|string $expires Expiration time + */ + public function cache($expires) { + if ($expires === false) { + $this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; + $this->headers['Cache-Control'] = array( + 'no-store, no-cache, must-revalidate', + 'post-check=0, pre-check=0', + 'max-age=0' + ); + $this->headers['Pragma'] = 'no-cache'; + } + else { + $expires = is_int($expires) ? $expires : strtotime($expires); + $this->headers['Expires'] = gmdate('D, d M Y H:i:s', $expires) . ' GMT'; + $this->headers['Cache-Control'] = 'max-age='.($expires - time()); + } + + return $this; + } + + /** + * Sends the response and exits the program. + */ + public function send() { + ob_end_clean(); + + if (!headers_sent()) { + foreach ($this->headers as $field => $value) { + if (is_array($value)) { + foreach ($value as $v) { + header($field.': '.$v); + } + } + else { + header($field.': '.$value); + } + } + } + + exit($this->body); + } +} +?> diff --git a/flight/Router.php b/flight/Router.php new file mode 100644 index 0000000..3b8afed --- /dev/null +++ b/flight/Router.php @@ -0,0 +1,103 @@ + + * @license http://www.opensource.org/licenses/mit-license.php + * @version 0.1 + */ +class Router { + protected $routes = array(); + + /** + * Maps a URL pattern to a callback function. + * + * @param string $pattern URL pattern to match + * @param callback $callback Callback function + */ + public function map($pattern, $callback) { + list($method, $url) = explode(' ', trim($pattern), 2); + + if (!is_null($url)) { + foreach (explode('|', $method) as $value) { + $this->routes[$value][$url] = $callback; + } + } + else { + $this->routes['*'][$pattern] = $callback; + } + } + + /** + * Tries to match a requst to a route. Also parses named parameters in the url. + * + * @param string $pattern URL pattern + * @param object $request Request object + */ + public function match($pattern, $url, array &$params = array()) { + $ids = array(); + + // Build the regex for matching + $regex = '/^'.implode('\/', array_map( + function($str) use (&$ids){ + if ($str == '*') { + $str = '(.*)'; + } + else if (@$str{0} == '@') { + if (preg_match('/@(\w+)(\:([^\/]*))?/', $str, $matches)) { + $ids[$matches[1]] = true; + return '(?P<'.$matches[1].'>'.(isset($matches[3]) ? $matches[3] : '[^(\/|\?)]*').')'; + } + } + return $str; + }, + explode('/', $pattern) + )).'\/?(?:\?.*)?$/i'; + + // Attempt to match route and named parameters + if (preg_match($regex, $url, $matches)) { + if (!empty($ids)) { + $params = array_intersect_key($matches, $ids); + } + return true; + } + + return false; + } + + /** + * Routes the current request. + * + * @param object $request Request object + */ + public function route(&$request) { + $params = array(); + $routes = $this->routes[$request->method] + ($this->routes['*'] ?: array()); + + foreach ($routes as $pattern => $callback) { + if ($request->url === $pattern || self::match($pattern, $request->url, $params)) { + $request->matched = $pattern; + return array($callback, array($params)); + } + } + + return false; + } + + /** + * Gets mapped routes. + * + * @return array Array of routes + */ + public function getRoutes() { + return $this->routes; + } + + /** + * Resets the router. + */ + public function clear() { + $this->routes = array(); + } +} +?> diff --git a/flight/View.php b/flight/View.php new file mode 100644 index 0000000..2a38a92 --- /dev/null +++ b/flight/View.php @@ -0,0 +1,67 @@ + + * @license http://www.opensource.org/licenses/mit-license.php + * @version 0.1 + */ +class View { + protected $templatePath; + + public function __construct($templatePath = null) { + $this->templatePath = $templatePath ?: './views'; + } + + /** + * Renders a template. + * + * @param string $file Template file + * @param array $data Template data + */ + public function render($file, $data = null) { + // Bind template data to view + if (!is_null($data)) { + if (is_array($data)) { + foreach ($data as $key => $value) { + $this->{$key} = $value; + } + } + else if (is_object($data)) { + foreach (get_object_vars($data) as $key => $value) { + $this->{$key} = $value; + } + } + } + + // Display template + include $this->templatePath.'/'.((substr($file,-4) == '.php') ? $file : $file.'.php'); + } + + /** + * Gets the output of a template. + * + * @param string $file Template file + * @param array $data Template data + */ + public function fetch($file, $data = null) { + ob_start(); + + $this->render($file, $data); + $output = ob_get_contents(); + + ob_end_clean(); + + return $output; + } + + /** + * Displays escaped output. + * + * @param string $str String to escape + */ + public function e($str) { + echo htmlentities($str); + } +} +?>