├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── examples ├── basic_example │ ├── .sample.htaccess │ ├── index.php │ ├── nginx.conf │ ├── settings.php │ ├── settings_local.php │ └── templates │ │ └── hello.html └── twig_example │ ├── .htrouter.php │ ├── README.md │ ├── composer.json │ ├── index.php │ └── templates │ ├── base.html │ └── home.html ├── ham └── ham.php └── tests ├── HamTest.php ├── bootstrap.php └── phpunit.xml /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | examples/twig_example/vendor/* 3 | *.lock 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 5 | script: "cd tests; phpunit" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, James Cleveland 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ham 2 | === 3 | 4 | *Now includes tests!* 5 | 6 | 7 | PHP Microframework for use with whatever you like. Basically just a fast router 8 | with nice syntax, and a cache singleton. Will add more things as I go, like 9 | perhaps an extension system, autoloader and some other stuff to make developing 10 | in PHP less irritating than it currently is. 11 | 12 | Routes are converted to regex and cached so this process does not need to 13 | happen every request. Furthermore, the resolved route for a given URI is also 14 | cached so on most requests thare is no regex matching involved. 15 | 16 | There is also now the ability to mount apps on routes within apps, so one could 17 | make an administrator app, then mount it on the main app at /admin. 18 | 19 | PHP presents an interesting challenge because due to it's architecture, 20 | everything has to be re-done each request, which is why I'm leveraging caching 21 | with tiny TTLs to share the results of operations like route resolution 22 | between requests. 23 | 24 | Note: PHP already has many of the features that many microframeworks have, such 25 | as session handling, cookies, and templating. An aim of this project is to 26 | encourage the use of native functionality where possible or where it is good, 27 | but make some parts nicer or extend upon them to bring it up to scratch with 28 | the way I like things. 29 | 30 | Note: For maximum speed gains, use the XCache extension because that supports 31 | caching of closures, unlike APC. 32 | 33 | 34 | Goals 35 | ----- 36 | 37 | * Make pretty much anything I/O related cached with XCache/APC 38 | (whichever is installed) in order to prevent excessive disk usage or path 39 | searching on lots of requests. 40 | * Provide a succinct syntax that means less magic and less code to read 41 | through and learn, without compromising speed or code length, by using native 42 | PHP methods and features. 43 | * Promote a simple, flat way of building applications that don't need 44 | massive levels of abstraction. 45 | * Encourage use of excellent third-party libraries such as Doctrine to prevent 46 | developers writing convoluted, unmaintainable code that people like me have to 47 | pick up and spend hours poring over just to get an idea of what on earth is 48 | going on. 49 | * Define and document development patterns that allow for new developers to 50 | get up to speed quickly and write new code that isn't hacky. 51 | 52 | 53 | Inspired entirely by Flask. 54 | 55 | 56 | Requirements 57 | ------------ 58 | 59 | * PHP 5.3 60 | * XCache (preferred) or APC (still optional) 61 | * Requests pointed at file that you put the app in (eg. 62 | index.php). 63 | 64 | 65 | Hello World 66 | ----------- 67 | 68 | ```php 69 | require '../ham/ham.php'; 70 | 71 | $app = new Ham('example'); 72 | 73 | $app->route('/', function($app) { 74 | return 'Hello, world!'; 75 | }); 76 | 77 | $app->run(); 78 | ``` 79 | 80 | 81 | More Interesting Example 82 | ------------------------ 83 | 84 | ```php 85 | require '../ham/ham.php'; 86 | 87 | $app = new Ham('example'); 88 | $app->config_from_file('settings.php'); 89 | 90 | $app->route('/pork', function($app) { 91 | return "Delicious pork."; 92 | }); 93 | 94 | $hello = function($app, $name='world') { 95 | return $app->render('hello.html', array( 96 | 'name' => $name 97 | )); 98 | }; 99 | $app->route('/hello/', $hello); 100 | $app->route('/', $hello); 101 | 102 | $app->run(); 103 | ``` 104 | 105 | Multiple apps mounted on routes! 106 | -------------------------------- 107 | 108 | ```php 109 | require '../ham/ham.php'; 110 | 111 | $beans = new Ham('beans'); 112 | 113 | $beans->route('/', function($app) { 114 | return "Beans home."; 115 | }); 116 | 117 | $beans->route('/baked', function($app) { 118 | return "Yum!"; 119 | }); 120 | 121 | $app = new Ham('example'); 122 | $app->route('/', function($app) { 123 | return "App home."; 124 | }); 125 | $app->route('/beans', $beans); 126 | $app->run(); 127 | ``` 128 | 129 | Custom Error Handeling 130 | -------------------------------- 131 | 132 | ```php 133 | require 'ham/ham.php'; 134 | 135 | $beans = new Ham('beans'); 136 | 137 | $beans->route('/', function($app) { 138 | return "Beans home."; 139 | }); 140 | 141 | $app->onError(function(){ 142 | return "Burnt Bacon."; 143 | }, "Error message can go here."); 144 | 145 | $app->run(); 146 | ``` 147 | 148 | Output: 149 | 150 | #### /beans/ 151 | 152 | Beans home. 153 | 154 | #### /beans/baked 155 | 156 | Yum! 157 | 158 | #### / 159 | 160 | App home. 161 | 162 | #### /definitely_not_the_page_you_were_looking_for 163 | 164 | Burnt Bacon. 165 | 166 | Have a gander at the example application for more details. 167 | 168 | 169 | To-Dos 170 | ------ 171 | 172 | * Nice logging class and logging support with error levels, e-mailing, etc. 173 | * Sub-application mounting (ala Flask "Blueprints"). 174 | * Sanitisation solution. 175 | * CSRF tokens 176 | * Extension API 177 | 178 | 179 | Extension Ideas 180 | --------------- 181 | 182 | * Form generation (3rd-party? Phorms) 183 | * ORM integration (most likely Doctrine) 184 | * Auth module (using scrypt or something) 185 | * Admin extension 186 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radiosilence/ham", 3 | "description": "PHP Microframework for use with whatever you like.", 4 | "homepage": "https://github.com/radiosilence/Ham", 5 | "support": { 6 | "issues": "https://github.com/radiosilence/Ham/issues", 7 | "source": "https://github.com/radiosilence/Ham" 8 | }, 9 | "license": "BSD-2-Clause", 10 | "keywords": [ 11 | "microframework", 12 | "router" 13 | ], 14 | "require": { 15 | "php": ">=5.3" 16 | }, 17 | "autoload": { 18 | "files": ["ham/ham.php"] 19 | }, 20 | "suggest": { 21 | "XCache (http://xcache.lighttpd.net/)": "Either XCache or APC is required. XCache is preferred.", 22 | "ext-apc": "Either XCache or APC is required. XCache is preferred." 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/basic_example/.sample.htaccess: -------------------------------------------------------------------------------- 1 | # Turn on URL rewriting engine 2 | RewriteEngine On 3 | 4 | # Disable rewriting for existing files or directories 5 | RewriteCond %{REQUEST_FILENAME} !-f 6 | RewriteCond %{REQUEST_FILENAME} !-d 7 | 8 | # Redirect all other requests to index.php 9 | RewriteRule ^.*$ index.php [PT,L] 10 | -------------------------------------------------------------------------------- /examples/basic_example/index.php: -------------------------------------------------------------------------------- 1 | route('/', function($app) { 7 | return "Beans home."; 8 | }); 9 | $beans->route('/baked', function($app) { 10 | return "Yum!"; 11 | }); 12 | 13 | $app = new Ham('example', false, 'logs/' . date('Y-m-d') . '.txt'); 14 | 15 | $app->route('/', function($app) { 16 | $app->logger->log('Home requested'); 17 | 18 | return "Home."; 19 | }); 20 | 21 | $app->route('/', function($app) { 22 | return "Home."; 23 | }); 24 | 25 | $app->route('/hello/', function($app, $name) { 26 | return $app->render('hello.html', array( 27 | 'name' => $name 28 | )); 29 | }); 30 | 31 | $app->route('/beans', $beans); 32 | $app->run(); 33 | -------------------------------------------------------------------------------- /examples/basic_example/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | 5 | root /var/www/ham.com/public_html; 6 | index index.php; 7 | 8 | 9 | server_name localhost; 10 | 11 | location / { 12 | # First attempt to serve request as file, then 13 | # as directory, then fall back to index.php 14 | try_files $uri $uri/ /index.php; 15 | } 16 | 17 | error_page 404 /404.html; 18 | 19 | # redirect server error pages to custom static pages /50x.html 20 | # 21 | error_page 500 502 503 504 /50x.html; 22 | location = /50x.html { 23 | root /usr/share/html/errors; 24 | } 25 | 26 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 27 | # 28 | location ~ \.php$ { 29 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 30 | # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini 31 | # 32 | # # With php5-cgi alone: 33 | # fastcgi_pass 127.0.0.1:9000; 34 | # With php5-fpm: 35 | fastcgi_pass unix:/var/run/php5-fpm.sock; 36 | fastcgi_index index.php; 37 | include fastcgi_params; 38 | } 39 | 40 | # deny access to .htaccess files 41 | location ~ /\.ht { 42 | deny all; 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /examples/basic_example/settings.php: -------------------------------------------------------------------------------- 1 | ! -------------------------------------------------------------------------------- /examples/twig_example/.htrouter.php: -------------------------------------------------------------------------------- 1 | about 27 | 28 | hi from the about page 29 | ``` 30 | 31 | also if you look in the templates they are using inheritence 32 | 33 | 34 | _base.html -> home.html_ 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/twig_example/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radiosilence/ham", 3 | "description": "PHP Microframework for use with whatever you like.", 4 | "homepage": "https://github.com/radiosilence/Ham", 5 | "support": { 6 | "issues": "https://github.com/radiosilence/Ham/issues", 7 | "source": "https://github.com/radiosilence/Ham" 8 | }, 9 | "license": "BSD-2-Clause", 10 | "keywords": [ 11 | "microframework", 12 | "router" 13 | ], 14 | "require": { 15 | "php": ">=5.3", 16 | "twig/twig": "~1.0", 17 | "radiosilence/ham":"*" 18 | }, 19 | "autoload": { 20 | "files": ["./vendor/twig/twig/lib/Twig/Autoloader.php"] 21 | }, 22 | "suggest": { 23 | "XCache (http://xcache.lighttpd.net/)": "Either XCache or APC is required. XCache is preferred.", 24 | "ext-apc": "Either XCache or APC is required. XCache is preferred." 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/twig_example/index.php: -------------------------------------------------------------------------------- 1 | _twig_env = new Twig_Environment(new Twig_Loader_Filesystem($this->template_paths)); 10 | parent::__construct(); 11 | } 12 | 13 | public function render($view,$data,$layout=null){ 14 | return $this->_twig_env->render($view,$data); 15 | } 16 | } 17 | 18 | 19 | 20 | $app = new HamTwig('app',false,"logger"); 21 | 22 | 23 | $app->route('/',function() use ($app) { 24 | $title = "home"; 25 | $content = "hi from the home page"; 26 | 27 | return $app->render('home.html',array( 28 | "page_title"=>$title, 29 | "content"=>$content 30 | )); 31 | }); 32 | 33 | $app->route('/',function($app,$title){ 34 | $content = "hi from the $title page"; 35 | 36 | return $app->render('home.html',array( 37 | "page_title"=>$title, 38 | "content"=>$content 39 | )); 40 | }); 41 | 42 | $app->run(); 43 | -------------------------------------------------------------------------------- /examples/twig_example/templates/base.html: -------------------------------------------------------------------------------- 1 | {% block title %} 2 |

{{ page_title }}

3 | {% endblock %} 4 | 5 | {% block content %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /examples/twig_example/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {{ content }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /ham/ham.php: -------------------------------------------------------------------------------- 1 | name = $name; 23 | if($cache === False) { 24 | $cache = static::create_cache($this->name); 25 | } 26 | $this->cache = $cache; 27 | if($log) { 28 | $this->logger = static::create_logger($log); 29 | } 30 | } 31 | 32 | /** 33 | * Add routes 34 | * @param $uri 35 | * @param $callback 36 | * @param array $request_methods 37 | * @return bool 38 | */ 39 | public function route($uri, $callback, $request_methods=array('GET')) { 40 | if($this === $callback) { 41 | return False; 42 | } 43 | $wildcard = False; 44 | if($callback instanceof Ham) { 45 | $callback->prefix = $uri; 46 | $wildcard = True; 47 | } 48 | 49 | $this->routes[] = array( 50 | 'uri' => $uri, 51 | 'callback' => $callback, 52 | 'request_methods' => $request_methods, 53 | 'wildcard' => $wildcard 54 | ); 55 | 56 | return true; 57 | } 58 | 59 | /** 60 | * Calls route and outputs it to STDOUT 61 | */ 62 | public function run() { 63 | echo $this(); 64 | } 65 | 66 | /** 67 | * Invoke method allows the application to be mounted as a closure. 68 | * @param mixed|bool $app parent application that can be referenced by $app->parent 69 | * @return mixed|string 70 | */ 71 | public function __invoke($app=False) { 72 | $this->parent = $app; 73 | return $this->_route($_SERVER['REQUEST_URI']); 74 | } 75 | 76 | /** 77 | * Exists only as a function to fill as a setter for 78 | * A developer to add custom 404 pages 79 | * If a log message is set, it will append the error functio 80 | * to the developer-defined one. 81 | */ 82 | 83 | public function onError($closure_callback,$logMessage=NULL) { 84 | if($logMessage) { 85 | $closure_callback = function(){ 86 | call_user_func($closure_callback); 87 | $this->error($logMessage); 88 | }; 89 | } 90 | $this->errorFunc = $closure_callback; 91 | 92 | } 93 | 94 | /** 95 | * Called upon when 404 is deduced to be the only outcome 96 | */ 97 | 98 | protected function page_not_found() { 99 | if(isset($this->errorFunc)){ 100 | header("HTTP/1.0 404 Not Found"); 101 | return call_user_func($this->errorFunc); // Dev defined Error 102 | } else { 103 | return static::abort(404); // Generic Error 104 | } 105 | } 106 | 107 | /** 108 | * Makes sure the routes are compiled then scans through them 109 | * and calls whichever one is approprate. 110 | */ 111 | protected function _route($request_uri) { 112 | $uri = parse_url(str_replace($this->config['APP_URI'], '', $request_uri)); 113 | $path = $uri['path']; 114 | $_k = "found_uri:{$path}"; 115 | $found = $this->cache->get($_k); 116 | if(!$found) { 117 | $found = $this->_find_route($path); 118 | $this->cache->set($_k, $found, 10); 119 | } 120 | if(!$found) { 121 | return $this->page_not_found(); 122 | } 123 | $found['args'][0] = $this; 124 | return call_user_func_array($found['callback'], $found['args']); 125 | } 126 | 127 | 128 | protected function _find_route($path) { 129 | $compiled = $this->_get_compiled_routes(); 130 | foreach($compiled as $route) { 131 | if(preg_match($route['compiled'], $path, $args)) { 132 | $found = array( 133 | 'callback' => $route['callback'], 134 | 'args' => $args 135 | ); 136 | return $found; 137 | } 138 | } 139 | return False; 140 | } 141 | 142 | protected function _get_compiled_routes() { 143 | $_k = 'compiled_routes'; 144 | $compiled = $this->cache->get($_k); 145 | if($compiled) 146 | return $compiled; 147 | 148 | $compiled = array(); 149 | foreach($this->routes as $route) { 150 | $route['compiled'] = $this->_compile_route($route['uri'], $route['wildcard']); 151 | $compiled[] = $route; 152 | } 153 | $this->cache->set($_k, $compiled); 154 | return $compiled; 155 | } 156 | 157 | /** 158 | * Takes a route in simple syntax and makes it into a regular expression. 159 | */ 160 | protected function _compile_route($uri, $wildcard) { 161 | $route = $this->_escape_route_uri(rtrim($uri, '/')); 162 | $types = array( 163 | '' => '([0-9\-]+)', 164 | '' => '([0-9\.\-]+)', 165 | '' => '([a-zA-Z0-9\-_]+)', 166 | '' => '([a-zA-Z0-9\-_\/])' 167 | ); 168 | foreach($types as $k => $v) { 169 | $route = str_replace(preg_quote($k), $v, $route); 170 | } 171 | if($wildcard) 172 | $wc = '(.*)?'; 173 | else 174 | $wc = ''; 175 | $ret = '/^' . $this->_escape_route_uri($this->prefix) . $route . '\/?' . $wc . '$/'; 176 | return $ret; 177 | } 178 | 179 | protected function _escape_route_uri($uri) { 180 | return str_replace('/', '\/', preg_quote($uri)); 181 | } 182 | 183 | public function partial($view, $data = null) { 184 | $path = $this->_get_template_path($view); 185 | if(!$path) 186 | return static::abort(500, 'Template not found'); 187 | 188 | ob_start(); 189 | if(is_array($data)) 190 | extract($data); 191 | require $path; 192 | return trim(ob_get_clean()); 193 | } 194 | 195 | /** 196 | * Returns the contents of a template, populated with the data given to it. 197 | */ 198 | public function render($view, $data = null, $layout = null) { 199 | $content = $this->partial($view, $data); 200 | 201 | if ($layout !== false) { 202 | 203 | if ($layout == null) { 204 | $layout = ($this->layout == null) ? 'layout.php' : $this->layout; 205 | } 206 | 207 | $data['content'] = $content; 208 | return $this->partial($layout, $data); 209 | } else { 210 | return $content; 211 | } 212 | } 213 | 214 | public function json($obj, $code = 200) { 215 | header('Content-type: application/json', true, $code); 216 | echo json_encode($obj); 217 | exit; 218 | } 219 | 220 | 221 | /** 222 | * Configure an application object from a file. 223 | */ 224 | public function config_from_file($filename) { 225 | $_k = 'config'; 226 | $this->config = $this->cache->get($_k); 227 | if($this->config) { 228 | return True; 229 | } 230 | require($filename); 231 | $conf = get_defined_vars(); 232 | unset($conf['filename']); 233 | foreach($conf as $k => $v) { 234 | $this->config[$k] = $v; 235 | } 236 | $this->cache->set($_k, $this->config); 237 | 238 | return true; 239 | } 240 | 241 | /** 242 | * Allows configuration file to be specified by environment variable, 243 | * to make deployment easy. 244 | */ 245 | public function config_from_env($var) { 246 | return $this->config_from_file($_ENV[$var]); 247 | } 248 | 249 | protected function _get_template_path($name) { 250 | $_k = "template_path:{$name}"; 251 | $path = $this->cache->get($_k); 252 | if($path) 253 | return $path; 254 | foreach($this->template_paths as $dir) { 255 | $path = $dir . $name; 256 | if(file_exists($path)) { 257 | $this->cache->set($_k, $path); 258 | return $path; 259 | } 260 | } 261 | return False; 262 | } 263 | 264 | /** 265 | * static version of abort 266 | * to allow for calling of abort by class 267 | * @param integer $code 268 | * @param string $message 269 | * @param Ham $app 270 | * @return string 271 | */ 272 | public static function _abort($code, $message='',$app=null) { 273 | if(php_sapi_name() != 'cli') 274 | header("Status: {$code}", False, $code); 275 | $name = !is_null($app) ? 276 | $app->name : 277 | 'App not set, call this function from the app or explicitly pass the $app as the last argument'; 278 | return "

{$code}

{$message}

{$name}

"; 279 | } 280 | 281 | /** 282 | * application specific Cancel method 283 | * @param integer $code 284 | * @param string $message 285 | * @return string 286 | */ 287 | public function abort($code,$message=''){ 288 | return self::_abort($code,$message,$this); 289 | } 290 | 291 | /** 292 | * Cache factory, be it XCache or APC. 293 | */ 294 | public static function create_cache($prefix, $dummy=False,$redisFirst=False) { 295 | if($redisFirst){ 296 | if(class_exists("Redis") && !$dummy){ 297 | return new RedisCache($prefix); 298 | }else if(function_exists('xcache_set') && !$dummy) { 299 | return new XCache($prefix); 300 | } else if(function_exists('apc_fetch') && !$dummy) { 301 | return new APC($prefix); 302 | } else { 303 | return new Dummy($prefix); 304 | } 305 | }else{ 306 | if(function_exists('xcache_set') && !$dummy) { 307 | return new XCache($prefix); 308 | } else if(function_exists('apc_fetch') && !$dummy) { 309 | return new APC($prefix); 310 | } else if(class_exists("Redis") && !$dummy){ 311 | return new RedisCache($prefix); 312 | } else { 313 | return new Dummy($prefix); 314 | } 315 | } 316 | } 317 | 318 | /** 319 | * Logger factory; just FileLogger for now. 320 | */ 321 | public static function create_logger($log_file) { 322 | if (!file_exists($log_file)) { 323 | if (is_writable(dirname($log_file))) { 324 | touch($log_file); 325 | } else { 326 | static::abort(500, "Log file couldn't be created."); 327 | } 328 | } 329 | 330 | if (!is_writable($log_file)) { 331 | static::abort(500, "Log file isn't writable."); 332 | } 333 | 334 | return new FileLogger($log_file); 335 | } 336 | } 337 | 338 | class XCache extends HamCache { 339 | public function get($key) { 340 | return xcache_get($this->_p($key)); 341 | } 342 | public function set($key, $value, $ttl=1) { 343 | return xcache_set($this->_p($key), $value, $ttl); 344 | } 345 | public function inc($key, $interval=1) { 346 | return xcache_inc($this->_p($key), $interval); 347 | } 348 | public function dec($key, $interval=1) { 349 | return xcache_dec($this->_p($key), $interval); 350 | } 351 | } 352 | 353 | class APC extends HamCache { 354 | public function get($key) { 355 | if(!apc_exists($this->_p($key))) 356 | return False; 357 | return apc_fetch($this->_p($key)); 358 | } 359 | public function set($key, $value, $ttl=1) { 360 | try { 361 | return apc_store($this->_p($key), $value, $ttl); 362 | } catch(Exception $e) { 363 | apc_delete($this->_p($key)); 364 | return False; 365 | } 366 | } 367 | public function inc($key, $interval=1) { 368 | return apc_inc($this->_p($key), $interval); 369 | } 370 | public function dec($key, $interval=1) { 371 | return apc_dec($this->_p($key), $interval); 372 | } 373 | } 374 | 375 | class RedisCache extends HamCache{ 376 | public function __construct($prefix=false,$host="127.0.0.1"){ 377 | parent::__construct($prefix); 378 | $this->_conn = new Redis(); 379 | $this->_conn->connect($host); 380 | } 381 | 382 | public function get($key){ 383 | return $this->_conn->get($this->_p($key)); 384 | } 385 | public function set($key, $val,$ttl=false){ 386 | $ttl = $ttl ? $ttl*1000 : null; 387 | if(is_null($ttl)){ 388 | return $this->_conn->set($this->_p($key),$val); 389 | }else{ 390 | return $this->_conn->set($this->_p($key),$val,$ttl); 391 | } 392 | } 393 | public function inc($key,$interval=1){ 394 | $this->_conn->incr($this->_p($key),$interval); 395 | 396 | } 397 | public function dec($key,$interval=1){ 398 | $this->_conn->decr($this->_p($key),$interval); 399 | } 400 | } 401 | 402 | class Dummy extends HamCache { 403 | public function get($key) { 404 | return False; 405 | } 406 | public function set($key, $value, $ttl=1) { 407 | return False; 408 | } 409 | public function inc($key, $interval=1) { 410 | return False; 411 | } 412 | public function dec($key, $interval=1) { 413 | return False; 414 | } 415 | } 416 | 417 | abstract class HamCache { 418 | public $prefix; 419 | 420 | public function __construct($prefix=False) { 421 | $this->prefix = $prefix; 422 | } 423 | protected function _p($key) { 424 | if($this->prefix) 425 | return $this->prefix . ':' . $key; 426 | else 427 | return $key; 428 | } 429 | abstract public function set($key, $value, $ttl=1); 430 | abstract public function get($key); 431 | abstract public function inc($key, $interval=1); 432 | abstract public function dec($key, $interval=1); 433 | } 434 | 435 | class FileLogger extends HamLogger { 436 | public $file; 437 | 438 | public function __construct($file) { 439 | $this->file = $file; 440 | } 441 | 442 | public function write($message, $severity) { 443 | $message = date('Y-m-d H:i:s') . "\t$severity\t$message\n"; 444 | if (!is_writable($this->file)) { 445 | return false; 446 | } 447 | file_put_contents($this->file, $message, FILE_APPEND | LOCK_EX); 448 | 449 | return true; 450 | } 451 | 452 | public function error($message) { 453 | return $this->write($message, 'error'); 454 | } 455 | 456 | public function log($message) { 457 | return $this->write($message, 'log'); 458 | } 459 | 460 | public function info($message) { 461 | return $this->write($message, 'info'); 462 | } 463 | } 464 | 465 | abstract class HamLogger { 466 | abstract public function error($message); 467 | abstract public function log($message); 468 | abstract public function info($message); 469 | } 470 | -------------------------------------------------------------------------------- /tests/HamTest.php: -------------------------------------------------------------------------------- 1 | route('/', function($app) { 10 | return 'hello world'; 11 | }); 12 | $app->route('/hello/', function($app, $name) { 13 | return "hello {$name}"; 14 | }); 15 | 16 | $app->route('/timestwo/', function($app, $int) { 17 | return $int * 2; 18 | }); 19 | $app->route('/add//', function($app, $a, $b) { 20 | return $a + $b; 21 | }); 22 | $app->route('/dividefloat//', function($app, $a, $b) { 23 | if($b == 0) 24 | return 'NaN'; 25 | return $a / $b; 26 | }); 27 | 28 | $beans = new Ham('beans', $cache1); 29 | $beans->route('/', function($app) { 30 | return "beans"; 31 | }); 32 | $beans->route('/baked', function($app) { 33 | return "yum"; 34 | }); 35 | $app->route('/beans', $beans); 36 | $this->app = $app; 37 | } 38 | 39 | protected function tearDown() { 40 | 41 | } 42 | 43 | public function testHelloWorld() { 44 | $app = $this->app; 45 | $_SERVER['REQUEST_URI'] = '/'; 46 | $this->assertEquals('hello world', $app()); 47 | } 48 | public function test404() { 49 | $app = $this->app; 50 | $_SERVER['REQUEST_URI'] = '/asdlkad8o7'; 51 | $this->assertContains('404', $app()); 52 | 53 | } 54 | 55 | public function testStringParameter() { 56 | $app = $this->app; 57 | $_SERVER['REQUEST_URI'] = '/hello/bort'; 58 | $this->assertContains('bort', $app()); 59 | } 60 | 61 | public function testIntParameter() { 62 | $app = $this->app; 63 | $inputs = array(1, 0, 5, 3); 64 | $outputs = array(2, 0, 10, 6); 65 | foreach($inputs as $k => $v) { 66 | $_SERVER['REQUEST_URI'] = "/timestwo/{$v}"; 67 | $this->assertEquals($outputs[$k], $app()); 68 | } 69 | } 70 | 71 | public function testMultiIntParameter() { 72 | $app = $this->app; 73 | $inputs_a = array(1, 5, 2, 6, 3); 74 | $inputs_b = array(0, -2, 7, 20, -10); 75 | $outputs = array( 1, 3, 9, 26, -7); 76 | foreach($inputs_a as $k => $v) { 77 | $_SERVER['REQUEST_URI'] = "/add/{$v}/{$inputs_b[$k]}"; 78 | $this->assertEquals($outputs[$k], $app()); 79 | } 80 | } 81 | 82 | public function testSubAppHome() { 83 | $app = $this->app; 84 | $uris = array('/beans', '/beans/'); 85 | foreach($uris as $uri){ 86 | $_SERVER['REQUEST_URI'] = $uri; 87 | $this->assertEquals('beans', $app()); 88 | } 89 | } 90 | public function testSubAppPage() { 91 | $app = $this->app; 92 | $uris = array('/beans/baked', '/beans/baked/'); 93 | foreach($uris as $uri){ 94 | $_SERVER['REQUEST_URI'] = $uri; 95 | $this->assertEquals('yum', $app()); 96 | } 97 | } 98 | 99 | public function testFloatParameter() { 100 | $app = $this->app; 101 | 102 | $inputs_a = array(1.2, 8.3, 1.176, 0, 3); 103 | $inputs_b = array(23, -1.25, 4.2, 20, 0); 104 | $outputs = array( 105 | 0.052173913, 106 | -6.64, 107 | 0.28, 108 | 0, 109 | 'NaN' 110 | ); 111 | foreach($inputs_a as $k => $v) { 112 | $_SERVER['REQUEST_URI'] = "/dividefloat/{$v}/{$inputs_b[$k]}"; 113 | $this->assertEquals($outputs[$k], $app()); 114 | } 115 | $_SERVER['REQUEST_URI'] = '/dividefloat/1.6/2.5'; 116 | $this->assertEquals('0.64', $app()); 117 | } 118 | 119 | public function testLogging() { 120 | $app = $this->app; 121 | 122 | foreach ( array('log', 'info', 'error') as $type ) { 123 | $pre_lines = count(file('log.txt')); 124 | 125 | $app->logger->$type('message'); 126 | 127 | // First, test that a line was added 128 | $post_lines = count(file('log.txt')); 129 | $this->assertEquals($post_lines, $pre_lines + 1); 130 | 131 | // ...and second, that the the line actually logged the expected value 132 | $match = preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\t' . $type . '\tmessage\n/m', file_get_contents('log.txt')); 133 | $this->assertEquals($match, 1); 134 | } 135 | } 136 | public function testAbortHasName(){ 137 | $app = $this->app; 138 | $this->assertContains($app->name,$app->abort(401,'error')); 139 | } 140 | 141 | public function testStaticAbortHasNoName(){ 142 | $app = $this->app; 143 | $cls = get_class($app); 144 | $this->assertContains('App not set, call this function from the app or explicitly pass the $app as the last argument',$cls::_abort(404,'error')); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | HamTest.php 19 | 20 | 21 | --------------------------------------------------------------------------------