├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Action.php ├── ActionController.php ├── ActionMiddleware.php ├── App.php ├── Bag.php ├── Configurator.php ├── Container.php ├── Controller.php ├── Dumper │ ├── BaseDumper.php │ ├── CliDumper.php │ ├── DumperInterface.php │ └── HtmlDumper.php ├── Exceptions │ ├── FatalErrorException.php │ ├── HttpErrorException.php │ └── HttpNotFoundException.php ├── Hook.php ├── Http │ ├── Request.php │ ├── Response.php │ ├── ResponseHeaderBag.php │ └── UploadedFile.php ├── MacroableTrait.php ├── Provider.php ├── Router │ ├── Route.php │ ├── RouteGroup.php │ └── Router.php ├── Util │ ├── Arr.php │ └── Str.php └── View │ ├── BasicViewEngine.php │ ├── View.php │ ├── ViewEngineInterface.php │ └── ViewServiceProvider.php └── tests ├── BagTests.php ├── ContainerTests.php ├── HookTests.php ├── RouterTests.php ├── RunAppTests.php └── resources └── views ├── hello-name.php └── hello.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | phpunit -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | 8 | before_script: 9 | - composer self-update 10 | - composer install --dev 11 | 12 | script: phpunit --coverage-text 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ##Copyright (c) 2015 Muhammad Syifa 2 | 3 | #MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rakit Framework 2 | ================= 3 | 4 | [![Build Status](https://img.shields.io/travis/rakit/framework.svg?style=flat-square)](https://travis-ci.org/rakit/framework) 5 | [![Dependency Status](https://www.versioneye.com/user/projects/5683f558eb4f47003c000b64/badge.svg?style=flat-square)](https://www.versioneye.com/user/projects/5683f558eb4f47003c000b64) 6 | [![Github Issues](http://githubbadges.herokuapp.com/emsifa/rakit-framework/issues.svg?style=flat-square)](https://github.com/emsifa/rakit-framework/issues) 7 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://doge.mit-license.org) 8 | 9 | Rakit framework adalah PHP micro framework untuk membantu kamu membuat web app atau web service dengan mudah. 10 | Rakit framework pada dasarnya terinspirasi dari Laravel dan Slim framework. 11 | Untuk itu beberapa API akan terlihat mirip-mirip dengan framework tersebut. 12 | 13 | ## Dokumentasi 14 | 15 | * Indonesia: [https://rakit.github.io/id](https://rakit.github.io/id/index.html) 16 | * English: [https://rakit.github.io](https://rakit.github.io) 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rakit/framework", 3 | "description": "Another PHP micro framework inspired by Slim 2, laravel and lumen", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Muhammad Syifa", 8 | "email": "emsifa@gmail.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Rakit\\Framework\\": "src/" 14 | } 15 | }, 16 | "minimum-stability": "dev", 17 | "require": {}, 18 | "require-dev": { 19 | "phpunit/phpunit": "4.*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests 5 | 6 | 7 | 8 | 9 | ./src 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Action.php: -------------------------------------------------------------------------------- 1 | app = $app; 35 | $this->type = $type; 36 | $this->index = $index; 37 | $this->action = $action; 38 | } 39 | 40 | /** 41 | * Get application instance 42 | * 43 | * @return Rakit\Framework\App 44 | */ 45 | public function getApp() 46 | { 47 | return $this->app; 48 | } 49 | 50 | /** 51 | * Get action index 52 | * 53 | * @return int 54 | */ 55 | public function getIndex() 56 | { 57 | return $this->index; 58 | } 59 | 60 | /** 61 | * Get action type 62 | * 63 | * @return string 64 | */ 65 | public function getType() 66 | { 67 | return $this->type; 68 | } 69 | 70 | /** 71 | * Get defined action 72 | * 73 | * @return mixed 74 | */ 75 | public function getDefinedAction() 76 | { 77 | return $this->action; 78 | } 79 | 80 | /** 81 | * Abstract get resolved callable 82 | * 83 | * @return callable 84 | */ 85 | abstract public function getCallable(); 86 | 87 | /** 88 | * Get next action 89 | * 90 | * @return Rakit\Framework\Action | null 91 | */ 92 | public function getNext() 93 | { 94 | $actions = $this->app['actions']; 95 | $next_index = $this->index+1; 96 | return array_key_exists($next_index, $actions)? $actions[$next_index] : null; 97 | } 98 | 99 | /** 100 | * Check if action is controller 101 | * 102 | * @return boolean 103 | */ 104 | public function isController() 105 | { 106 | return $this->getType() == 'controller'; 107 | } 108 | 109 | /** 110 | * Check if action is middleware 111 | * 112 | * @return boolean 113 | */ 114 | public function isMiddleware() 115 | { 116 | return $this->getType() == 'middleware'; 117 | } 118 | 119 | /** 120 | * Check if defined action using string class method like ClassName@method 121 | * 122 | * @return boolean 123 | */ 124 | public function useStringClassMethod() 125 | { 126 | $defined_action = (string) $this->getDefinedAction(); 127 | return count(explode('@', $defined_action)) == 2; 128 | } 129 | 130 | /** 131 | * Run action 132 | * 133 | * @return Rakit\Framework\Http\Response 134 | */ 135 | public function run() 136 | { 137 | $app = $this->getApp(); 138 | $callable = $this->getCallable(); 139 | 140 | if (!is_callable($callable)) { 141 | $defined_action = (string) $this->getDefinedAction(); 142 | $reason = $defined_action." is not callable"; 143 | if ($this->useStringClassMethod()) { 144 | list($class, $method) = explode('@', $defined_action, 2); 145 | if (!class_exists($class)) { 146 | $reason = "Class {$class} doesn't exists"; 147 | } elseif (!method_exists($class, $method)) { 148 | $reason = "Method {$class}::{$method} doesn't exists"; 149 | } 150 | } 151 | 152 | throw new \InvalidArgumentException("Cannot run action ".$this->getType()." '".$this->getDefinedAction()."'. ".$reason, 1); 153 | } 154 | 155 | $returned = call_user_func($callable); 156 | 157 | if(is_array($returned)) { 158 | $app->response->json($returned, null, $app->response->getContentType()); 159 | } elseif(is_string($returned)) { 160 | $app->response->html($returned, null, $app->response->getContentType()); 161 | } 162 | 163 | return $returned; 164 | } 165 | 166 | /** 167 | * Invoke action 168 | * 169 | * @return Rakit\Framework\Http\Response 170 | */ 171 | public function __invoke() 172 | { 173 | return $this->run(); 174 | } 175 | 176 | } -------------------------------------------------------------------------------- /src/ActionController.php: -------------------------------------------------------------------------------- 1 | getDefinedAction(); 20 | $matched_route = $this->app->request->route(); 21 | $params = $matched_route->params; 22 | 23 | $callable = $this->app->resolveController($action, $params); 24 | 25 | return $callable; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/ActionMiddleware.php: -------------------------------------------------------------------------------- 1 | getNext(); 22 | $action = $this->getDefinedAction(); 23 | 24 | $params = [$this->app->request, $this->app->response, $next]; 25 | $callable = $this->app->resolveMiddleware($action, $params); 26 | 27 | return $callable; 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/App.php: -------------------------------------------------------------------------------- 1 | name = $name; 57 | $default_configs = []; 58 | $configs = array_merge($default_configs, $configs); 59 | 60 | $this->container = new Container; 61 | $this['app'] = $this; 62 | $this['config'] = new Configurator($configs); 63 | $this['router'] = new Router($this); 64 | $this['hook'] = new Hook($this); 65 | $this['request'] = new Request($this); 66 | $this['response'] = new Response($this); 67 | 68 | static::$instances[$name] = $this; 69 | 70 | if(count(static::$instances) == 1) { 71 | static::setDefaultInstance($name); 72 | } 73 | 74 | $this->registerErrorHandlers(); 75 | $this->registerBaseHooks(); 76 | $this->registerDefaultMacros(); 77 | $this->registerBaseProviders(); 78 | } 79 | 80 | /** 81 | * Register a Service Provider into waiting lists 82 | * 83 | * @param string $class 84 | */ 85 | public function provide($class) 86 | { 87 | $this->providers[$class] = $provider = $this->container->make($class); 88 | if(false === $provider instanceof Provider) { 89 | throw new InvalidArgumentException("Provider {$class} must be instance of Rakit\\Framework\\Provider", 1); 90 | } 91 | 92 | $provider->register(); 93 | } 94 | 95 | /** 96 | * Register hook 97 | * 98 | * @param string $event 99 | * @param Closure $callable 100 | */ 101 | public function on($event, Closure $callable) 102 | { 103 | return $this->hook->on($event, $callable); 104 | } 105 | 106 | /** 107 | * Register hook once 108 | * 109 | * @param string $event 110 | * @param Closure $callable 111 | */ 112 | public function once($event, Closure $callable) 113 | { 114 | return $this->hook->once($event, $callable); 115 | } 116 | 117 | /** 118 | * Set middleware 119 | * 120 | * @param string $name 121 | * @param mixed $callable 122 | * @return void 123 | */ 124 | public function setMiddleware($name, $callable) 125 | { 126 | $this->middlewares[$name] = $callable; 127 | } 128 | 129 | /** 130 | * Check middleware is registered or not 131 | * 132 | * @param string $name 133 | * @return void 134 | */ 135 | public function hasMiddleware($name) 136 | { 137 | return isset($this->middlewares[$name]); 138 | } 139 | 140 | /** 141 | * Add global middleware 142 | * 143 | * @param string|callable $name_or_callable 144 | * @return void 145 | */ 146 | public function useMiddleware($name_or_callable) 147 | { 148 | if (!is_callable($name_or_callable)) { 149 | if (is_string($name_or_callable) AND $this->hasMiddleware($name_or_callable)) { 150 | $callable = $this->middlewares[$name_or_callable]; 151 | } else { 152 | throw new InvalidArgumentException("Cannot use global middleware. Middleware must be callable or registered middleware", 1); 153 | } 154 | } else { 155 | $callable = $name_or_callable; 156 | } 157 | 158 | $this->global_middlewares[] = $callable; 159 | } 160 | 161 | /** 162 | * Register GET route 163 | * 164 | * @param string $path 165 | * @param mixed $action 166 | * @return Rakit\Framework\Routing\Route 167 | */ 168 | public function get($path, $action) 169 | { 170 | return $this->route('GET', $path, $action); 171 | } 172 | 173 | /** 174 | * Register POST route 175 | * 176 | * @param string $path 177 | * @param mixed $action 178 | * @return Rakit\Framework\Routing\Route 179 | */ 180 | public function post($path, $action) 181 | { 182 | return $this->route('POST', $path, $action); 183 | } 184 | 185 | /** 186 | * Register PUT route 187 | * 188 | * @param string $path 189 | * @param mixed $action 190 | * @return Rakit\Framework\Routing\Route 191 | */ 192 | public function put($path, $action) 193 | { 194 | return $this->route('PUT', $path, $action); 195 | } 196 | 197 | /** 198 | * Register PATCH route 199 | * 200 | * @param string $path 201 | * @param mixed $action 202 | * @return Rakit\Framework\Routing\Route 203 | */ 204 | public function patch($path, $action) 205 | { 206 | return $this->route('PATCH', $path, $action); 207 | } 208 | 209 | /** 210 | * Register DELETE route 211 | * 212 | * @param string $path 213 | * @param mixed $action 214 | * @return Rakit\Framework\Routing\Route 215 | */ 216 | public function delete($path, $action) 217 | { 218 | return $this->route('DELETE', $path, $action); 219 | } 220 | 221 | /** 222 | * Register Group route 223 | * 224 | * @param string $path 225 | * @param Closure $grouper 226 | * @return Rakit\Framework\Routing\Route 227 | */ 228 | public function group($prefix, Closure $grouper) 229 | { 230 | return $this->router->group($prefix, $grouper); 231 | } 232 | 233 | /** 234 | * Registering a route 235 | * 236 | * @param string $path 237 | * @param mixed $action 238 | * @return Rakit\Framework\Routing\Route 239 | */ 240 | public function route($methods, $path, $action) 241 | { 242 | $route = $this->router->add($methods, $path, $action); 243 | if (!empty($this->global_middlewares)) { 244 | $route->middleware($this->global_middlewares); 245 | } 246 | return $route; 247 | } 248 | 249 | /** 250 | * Handle specified exception 251 | */ 252 | public function handle($exception_class, Closure $fn) 253 | { 254 | $this->exception_handlers[$exception_class] = $fn; 255 | } 256 | 257 | /** 258 | * Booting app 259 | * 260 | * @return boolean 261 | */ 262 | public function boot() 263 | { 264 | if($this->booted) return false; 265 | 266 | $app = $this; 267 | 268 | $providers = $this->providers; 269 | foreach($providers as $provider) { 270 | $provider->boot(); 271 | } 272 | 273 | // reset providers, we don't need them anymore 274 | $this->providers = []; 275 | 276 | return $this->booted = true; 277 | } 278 | 279 | /** 280 | * Run application 281 | * 282 | * @param string $path 283 | * @param string $method 284 | * @return void 285 | */ 286 | public function run($method = null, $path = null) 287 | { 288 | try { 289 | $this->boot(); 290 | 291 | $path = $path ?: $this->request->path(); 292 | $method = $method ?: $this->request->server['REQUEST_METHOD']; 293 | 294 | /** 295 | * for HEAD request 296 | * instead to add some code in router that will slow down performance 297 | * we trick it by change it to GET for dispatching only 298 | */ 299 | $matched_route = $this->router->findMatch($path, $method == 'HEAD'? 'GET' : $method); 300 | 301 | if(!$matched_route) { 302 | return $this->notFound(); 303 | } 304 | 305 | $this->request->defineRoute($matched_route); 306 | $this->hook->apply(strtoupper($method), [$matched_route, $this]); 307 | 308 | $middlewares = $matched_route->getMiddlewares(); 309 | $action = $matched_route->getAction(); 310 | 311 | $actions = $this->makeActions($middlewares, $action); 312 | if(isset($actions[0])) { 313 | $actions[0](); 314 | } 315 | 316 | $this->response->send(); 317 | 318 | return $this; 319 | } catch (Exception $e) { 320 | return $this->exception($e); 321 | } 322 | } 323 | 324 | public function exception(Exception $e) 325 | { 326 | $status_code = $e->getCode(); 327 | $status_message = $this->response->getStatusMessage($status_code); 328 | 329 | // if status message is null, 330 | // that mean 'exception code' is not one of 'available http response status codes' 331 | // so, change it to 500 332 | if(!$status_message) { 333 | $status_code = 500; 334 | } 335 | 336 | $this->response->setStatus($status_code); 337 | 338 | // because we register exception by handle() method, 339 | // we will manually catch exception class 340 | // first we need to get exception class 341 | $exception_class = get_class($e); 342 | 343 | // then we need parent classes too 344 | $exception_classes = array_values(class_parents($exception_class)); 345 | array_unshift($exception_classes, $exception_class); 346 | 347 | // now $exception_classes should be ['CatchedException', 'CatchedExceptionParent', ..., 'Exception'] 348 | // next, we need to get exception handler 349 | $custom_handler = null; 350 | foreach($exception_classes as $xclass) { 351 | if(array_key_exists($xclass, $this->exception_handlers)) { 352 | $custom_handler = $this->exception_handlers[$xclass]; 353 | } 354 | 355 | $this->hook->apply($xclass, [$e]); 356 | } 357 | 358 | $this->hook->apply('error', [$e]); 359 | 360 | if(true === $this->config['app.debug']) { 361 | $this->debugException($e); 362 | } else { 363 | if($custom_handler) { 364 | $this->container->call($custom_handler, [$e]); 365 | } elseif($e instanceof HttpNotFoundException) { 366 | $this->response->html("Error 404! Page not found"); 367 | } else { 368 | $this->response->html("Something went wrong"); 369 | } 370 | } 371 | 372 | $this->response->send(); 373 | return $this; 374 | } 375 | 376 | protected function debugException(Exception $e) 377 | { 378 | $debugger = PHP_SAPI == 'cli'? new CliDumper : new HtmlDumper; 379 | $this->response->html($debugger->render($e)); 380 | } 381 | 382 | /** 383 | * Stop application 384 | * 385 | * @return void 386 | */ 387 | public function stop() 388 | { 389 | $this->hook->apply("app.exit", [$this]); 390 | exit(); 391 | } 392 | 393 | /** 394 | * Not Found 395 | */ 396 | public function notFound() 397 | { 398 | $method = $this->request->server['REQUEST_METHOD']; 399 | $path = $this->request->path(); 400 | 401 | if($this->request->route()) { 402 | $message = "Error 404! Looks like you are throwing this manually"; 403 | } else { 404 | $message = "Error 404! No route matched with '{$method} {$path}'"; 405 | } 406 | 407 | throw new HttpNotFoundException($message); 408 | } 409 | 410 | /** 411 | * Abort app 412 | * 413 | * @param int $status 414 | * 415 | * @return void 416 | */ 417 | public function abort($status, $message = null) 418 | { 419 | if($status == 404) { 420 | return $this->notFound(); 421 | } else { 422 | throw new HttpErrorException; 423 | } 424 | } 425 | 426 | /** 427 | * Set default instance name 428 | * 429 | * @param string $name 430 | */ 431 | public static function setDefaultInstance($name) 432 | { 433 | static::$default_instance = $name; 434 | } 435 | 436 | /** 437 | * Getting an application instance 438 | * 439 | * @param string $name 440 | */ 441 | public static function getInstance($name = null) 442 | { 443 | if(!$name) $name = static::$default_instance; 444 | return static::$instances[$name]; 445 | } 446 | 447 | /** 448 | * Make/build app actions 449 | * 450 | * @param array $middlewares 451 | * @param mixed $controller 452 | * @return void 453 | */ 454 | protected function makeActions(array $middlewares, $controller) 455 | { 456 | $app = $this; 457 | $actions = array_merge($middlewares, [$controller]); 458 | $index_controller = count($actions)-1; 459 | 460 | $actions = []; 461 | foreach($middlewares as $i => $action) { 462 | $actions[] = new ActionMiddleware($this, $i, $action); 463 | } 464 | 465 | $actions[] = new ActionController($this, count($middlewares), $controller); 466 | 467 | $this['actions'] = $actions; 468 | return $actions; 469 | } 470 | 471 | /** 472 | * Resolving middleware action 473 | */ 474 | public function resolveMiddleware($middleware_action, array $params = array()) 475 | { 476 | if(is_string($middleware_action)) { 477 | $explode_params = explode(':', $middleware_action); 478 | 479 | $middleware_name = $explode_params[0]; 480 | if(isset($explode_params[1])) { 481 | $params = array_merge($params, explode(',', $explode_params[1])); 482 | } 483 | 484 | // if middleware is registered, get middleware 485 | if(array_key_exists($middleware_name, $this->middlewares)) { 486 | // Get middleware. so now, callable should be string Foo@bar, Closure, or function name 487 | $callable = $this->middlewares[$middleware_name]; 488 | $resolved_callable = $this->resolveCallable($callable, $params); 489 | } else { 490 | // othwewise, middleware_name should be Foo@bar or Foo 491 | $callable = $middleware_name; 492 | $resolved_callable = $this->resolveCallable($callable, $params); 493 | } 494 | } else { 495 | $resolved_callable = $this->resolveCallable($middleware_action, $params); 496 | } 497 | 498 | if(!is_callable($resolved_callable)) { 499 | if(is_array($middleware_action)) { 500 | $invalid_middleware = 'Array'; 501 | } else { 502 | $invalid_middleware = $middleware_action; 503 | } 504 | 505 | throw new \Exception('Middleware "'.$invalid_middleware.'" is not valid middleware or it is not registered'); 506 | } 507 | 508 | return $resolved_callable; 509 | } 510 | 511 | public function resolveController($controller_action, array $params = array()) 512 | { 513 | return $this->resolveCallable($controller_action, $params); 514 | } 515 | 516 | /** 517 | * Register base hooks 518 | */ 519 | protected function registerBaseHooks() 520 | { 521 | 522 | } 523 | 524 | /** 525 | * Register base providers 526 | */ 527 | public function registerBaseProviders() 528 | { 529 | $base_providers = [ 530 | 'Rakit\Framework\View\ViewServiceProvider', 531 | ]; 532 | 533 | foreach($base_providers as $provider_class) { 534 | $this->provide($provider_class); 535 | } 536 | } 537 | 538 | /** 539 | * Register error handler 540 | */ 541 | public function registerErrorHandlers() 542 | { 543 | $app = $this; 544 | 545 | // set error handler 546 | set_error_handler(function($severity, $message, $file, $line) use ($app) { 547 | if (!(error_reporting() & $severity)) { 548 | return; 549 | } 550 | 551 | $exception = new ErrorException($message, 500, $severity, $file, $line); 552 | $app->exception($exception); 553 | $app->stop(); 554 | }); 555 | 556 | // set fatal error handler 557 | register_shutdown_function(function() use ($app) { 558 | $error = error_get_last(); 559 | if($error) { 560 | $errno = $error["type"]; 561 | $errfile = $error["file"]; 562 | $errline = $error["line"]; 563 | $errstr = $error["message"]; 564 | 565 | $message = "[$errno] $errstr in $errfile line $errline"; 566 | 567 | $exception = new FatalErrorException($message, 500, 1, $errfile, $errline); 568 | 569 | $app->exception($exception); 570 | $app->stop(); 571 | } 572 | }); 573 | } 574 | 575 | /** 576 | * Register default macros 577 | */ 578 | protected function registerDefaultMacros() 579 | { 580 | $this->macro('resolveCallable', function($unresolved_callable, array $params = array()) { 581 | if(is_string($unresolved_callable)) { 582 | // in case "Foo@bar:baz,qux", baz and qux should be parameters, separate it! 583 | $explode_params = explode(':', $unresolved_callable); 584 | 585 | $unresolved_callable = $explode_params[0]; 586 | if(isset($explode_params[1])) { 587 | $params = array_merge($params, explode(',', $explode_params[1])); 588 | } 589 | 590 | // now $unresolved_callable should be "Foo@bar" or "foo", 591 | // if there is '@' character, transform it to array class callable 592 | $explode_method = explode('@', $unresolved_callable); 593 | if(isset($explode_method[1])) { 594 | $callable = [$explode_method[0], $explode_method[1]]; 595 | } else { 596 | // otherwise, just leave it as string, maybe that was function name 597 | $callable = $explode_method[0]; 598 | } 599 | } else { 600 | $callable = $unresolved_callable; 601 | } 602 | 603 | $app = $this; 604 | 605 | // last.. wrap callable in Closure 606 | return !is_callable($callable)? false : function() use ($app, $callable, $params) { 607 | if ($callable instanceof Closure) { 608 | $callable = Closure::bind($callable, $app, App::class); 609 | } 610 | return $app->container->call($callable, $params); 611 | }; 612 | }); 613 | 614 | $this->macro('baseUrl', function($path) { 615 | $path = '/'.trim($path, '/'); 616 | $base_url = trim($this->config->get('app.base_url', 'http://localhost:8000'), '/'); 617 | 618 | return $base_url.$path; 619 | }); 620 | 621 | $this->macro('asset', function($path) { 622 | return $this->baseUrl($path); 623 | }); 624 | 625 | $this->macro('indexUrl', function($path) { 626 | $path = trim($path, '/'); 627 | $index_file = trim($this->config->get('app.index_file', ''), '/'); 628 | return $this->baseUrl($index_file.'/'.$path); 629 | }); 630 | 631 | $this->macro('routeUrl', function($route_name, array $params = array()) { 632 | if($route_name instanceof Route) { 633 | $route = $route_name; 634 | } else { 635 | $route = $this->router->findRouteByName($route_name); 636 | if(! $route) { 637 | throw new \Exception("Trying to get url from unregistered route named '{$route_name}'"); 638 | } 639 | } 640 | 641 | $path = $route->getPath(); 642 | $path = str_replace(['(',')'], '', $path); 643 | foreach($params as $param => $value) { 644 | $path = preg_replace('/:'.$param.'\??/', $value, $path); 645 | } 646 | 647 | $path = preg_replace('/\/?\:[a-zA-Z0-9._-]+/','', $path); 648 | 649 | return $this->indexUrl($path); 650 | }); 651 | 652 | $this->macro('redirect', function($defined_url) { 653 | if(preg_match('/http(s)?\:\/\//', $defined_url)) { 654 | $url = $defined_url; 655 | } elseif($this->router->findRouteByName($defined_url)) { 656 | $url = $this->routeUrl($defined_url); 657 | } else { 658 | $url = $this->indexUrl($defined_url); 659 | } 660 | 661 | $this->hook->apply('response.redirect', [$url, $defined_url]); 662 | 663 | header("Location: ".$url); 664 | exit(); 665 | }); 666 | 667 | 668 | $this->macro('dd', function() { 669 | var_dump(func_get_args()); 670 | exit(); 671 | }); 672 | 673 | $app = $this; 674 | $this->response->macro('redirect', function($defined_url) use ($app) { 675 | return $app->redirect($defined_url); 676 | }); 677 | } 678 | 679 | public function bind($key, $value) 680 | { 681 | if (is_string($value)) { 682 | if (!class_exists($value)) { 683 | throw new InvalidArgumentException("Cannot bind {$value}, class {$value} is not exists"); 684 | } 685 | 686 | $value = function($container) use ($value) { 687 | return $container->getOrMake($value); 688 | }; 689 | } 690 | 691 | $this->container->register($key, $value); 692 | } 693 | 694 | /** 695 | * --------------------------------------------------------------- 696 | * Setter and getter 697 | * --------------------------------------------------------------- 698 | */ 699 | public function __set($key, $value) 700 | { 701 | $this->container->register($key, $value); 702 | } 703 | 704 | public function __get($key) 705 | { 706 | return $this->container->get($key); 707 | } 708 | 709 | /** 710 | * --------------------------------------------------------------- 711 | * ArrayAccess interface methods 712 | * --------------------------------------------------------------- 713 | */ 714 | public function offsetSet($key, $value) { 715 | return $this->container->register($key, $value); 716 | } 717 | 718 | public function offsetExists($key) { 719 | return $this->container->has($key); 720 | } 721 | 722 | public function offsetUnset($key) { 723 | return $this->container->remove($key); 724 | } 725 | 726 | public function offsetGet($key) { 727 | return $this->container->get($key); 728 | } 729 | 730 | } 731 | -------------------------------------------------------------------------------- /src/Bag.php: -------------------------------------------------------------------------------- 1 | items = $items; 21 | } 22 | 23 | /** 24 | * Check existances of a key 25 | * 26 | * @param string $key 27 | * @return boolean 28 | */ 29 | public function has($key) { 30 | $key = $this->resolveKey($key); 31 | return Arr::has($this->items, $key); 32 | } 33 | 34 | /** 35 | * Set item 36 | * 37 | * @param string $key 38 | * @param mixed $value 39 | * @return self 40 | */ 41 | public function set($key, $value) { 42 | $key = $this->resolveKey($key); 43 | 44 | if(is_array($key)) { 45 | $this->items = $key; 46 | } else { 47 | Arr::set($this->items, $key, $value); 48 | } 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Get item value by given key 55 | * 56 | * @param string $key 57 | * @param mixed $default 58 | * @return mixed 59 | */ 60 | public function get($key, $default = null) { 61 | $key = $this->resolveKey($key); 62 | 63 | return Arr::get($this->items, $key, $default); 64 | } 65 | 66 | /** 67 | * Remove item by given key 68 | * 69 | * @param string $key 70 | * @return self 71 | */ 72 | public function remove($key) { 73 | $key = $this->resolveKey($key); 74 | 75 | Arr::remove($this->items, $key); 76 | return $this; 77 | } 78 | 79 | /** 80 | * Get items by given keys 81 | * 82 | * @param array $keys 83 | * @param mixed $default 84 | * @return array 85 | */ 86 | public function only($keys, $result_array = false) 87 | { 88 | $keys = (array) $keys; 89 | $bag = new static; 90 | foreach($keys as $key) { 91 | $key = $this->resolveKey($key); 92 | $bag[$key] = $this->get($key); 93 | } 94 | 95 | return $result_array? $bag->all() : $bag; 96 | } 97 | 98 | /** 99 | * Get all items, except given keys 100 | * 101 | * @param array $keys 102 | * @return array 103 | */ 104 | public function except($keys, $result_array = false) 105 | { 106 | $keys = (array) $keys; 107 | $bag = new static; 108 | 109 | foreach($this->all(false) as $key => $value) { 110 | $key = $this->resolveKey($key); 111 | $bag->set($key, $value); 112 | } 113 | 114 | foreach($keys as $key) { 115 | $key = $this->resolveKey($key); 116 | $bag->remove($key); 117 | } 118 | 119 | return $result_array? $bag->all() : $bag; 120 | } 121 | 122 | /** 123 | * Get all items 124 | * 125 | * @param bool $deep 126 | * @return array 127 | */ 128 | public function all($deep = true) { 129 | $items = $this->items; 130 | 131 | if($deep) { 132 | foreach($this->namespaces as $ns => $bag) { 133 | $items[$ns] = $bag->all(true); 134 | } 135 | } 136 | 137 | return $items; 138 | } 139 | 140 | /** 141 | * Get count item keys 142 | * 143 | * @param bool $deep 144 | * @return int 145 | */ 146 | public function count($deep = false) 147 | { 148 | $items = $this->all($deep); 149 | return count($items); 150 | } 151 | 152 | /** 153 | * Get items size 154 | * 155 | * @param bool $deep 156 | * @return int 157 | */ 158 | public function size($deep = true) 159 | { 160 | $values = Arr::flatten($this->all($deep)); 161 | return count($values); 162 | } 163 | 164 | public function resolveKey($key) 165 | { 166 | return $key; 167 | } 168 | 169 | /** 170 | * --------------------------------------------------------------------------------------- 171 | * ArrayAccess interface methods 172 | * --------------------------------------------------------------------------------------- 173 | */ 174 | public function offsetSet($key, $value) { 175 | $this->set($key, $value); 176 | } 177 | 178 | public function offsetExists($key) { 179 | return $this->has($key); 180 | } 181 | 182 | public function offsetUnset($key) { 183 | $this->remove($key); 184 | } 185 | 186 | public function offsetGet($key) { 187 | return $this->get($key, null); 188 | } 189 | 190 | /** 191 | * --------------------------------------------------------------------------------------- 192 | * Namespace setter and getter 193 | * --------------------------------------------------------------------------------------- 194 | */ 195 | public function __set($namespace, $value) { 196 | $bag = new Bag(); 197 | $this->namespaces[$namespace] = $bag; 198 | 199 | if(is_array($value)) { 200 | $bag->set($value, null); 201 | } 202 | } 203 | 204 | public function __get($namespace) { 205 | if(isset($this->namespaces[$namespace])) { 206 | return $this->namespaces[$namespace]; 207 | } else { 208 | throw new \Exception("Undefined config namespace {$namespace}"); 209 | } 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/Configurator.php: -------------------------------------------------------------------------------- 1 | loadFile($file); 20 | } 21 | } 22 | 23 | /** 24 | * Merge configs from configurations in a directory 25 | * 26 | * @param string $dir 27 | * @return void 28 | */ 29 | public function mergeDir($dir) 30 | { 31 | $configs = new static; 32 | $configs->loadDir($dir); 33 | 34 | $this_configs = $this->all(false); 35 | $merge_configs = $configs->all(false); 36 | 37 | $merged_configs = array_replace_recursive($this_configs, $merge_configs); 38 | 39 | $this->items = $merged_configs; 40 | } 41 | 42 | /** 43 | * Load configs from a file 44 | * 45 | * @param string $file 46 | * @return void 47 | */ 48 | public function loadFile($file) 49 | { 50 | $filename = pathinfo($file, PATHINFO_FILENAME); 51 | $this->set($filename, require($file)); 52 | } 53 | 54 | /** 55 | * Set configuration namespace 56 | * 57 | * @param string $namespace 58 | * @param mixed $value 59 | * @return void 60 | */ 61 | public function __set($namespace, $value) 62 | { 63 | $bag = new static; 64 | $this->namespaces[$namespace] = $bag; 65 | 66 | if(is_array($value)) { 67 | $bag->set($value, null); 68 | } elseif(is_string($value)) { 69 | $bag->loadDir($value); 70 | } 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | container[$this->resolveKey($k)] = $value; 47 | } 48 | } 49 | 50 | /** 51 | * Protect anonymous function value 52 | * 53 | * @param Closure $fn 54 | * @return Closure 55 | */ 56 | public function protect(Closure $fn) 57 | { 58 | return function() use ($fn) { 59 | return $fn; 60 | }; 61 | } 62 | 63 | /** 64 | * Wrap singleton service in a closure 65 | * 66 | * @param Closure $fn 67 | * @return Closure 68 | */ 69 | public function singleton(Closure $fn) 70 | { 71 | return function($container) use ($fn) { 72 | static $object; 73 | 74 | if(!$object) { 75 | $object = $fn($container); 76 | } 77 | 78 | return $object; 79 | }; 80 | } 81 | 82 | /** 83 | * Check key 84 | * 85 | * @param string $key 86 | * @return bool 87 | */ 88 | public function has($key) 89 | { 90 | return isset($this->container[$key]); 91 | } 92 | 93 | /** 94 | * Get value for specified key in container 95 | * 96 | * @param string $key 97 | * @return mixed 98 | */ 99 | public function get($key) 100 | { 101 | if(!$this->has($key)) return null; 102 | 103 | $closure = $this->container[$key]; 104 | $value = $closure($this); 105 | 106 | // register classname into container if not exists 107 | if(is_object($value)) { 108 | $class = get_class($value); 109 | if(!$this->has($class)) $this->register($class, $closure); 110 | } 111 | 112 | return $value; 113 | } 114 | 115 | /** 116 | * Get or make value in container 117 | * 118 | * @param string $key 119 | * @param array $args 120 | * @return mixed 121 | */ 122 | public function getOrMake($key, array $args = array()) 123 | { 124 | if(!$this->has($key)) { 125 | return $this->make($key, $args); 126 | } else { 127 | return $this->get($key); 128 | } 129 | } 130 | 131 | /** 132 | * Remove a key in container 133 | * 134 | * @param string $key 135 | */ 136 | public function remove($key) 137 | { 138 | unset($this->container[$key]); 139 | } 140 | 141 | /** 142 | * Make object with Injecting some dependencies into constructor 143 | * 144 | * @param string $class 145 | * @param array $args 146 | * @return Object 147 | */ 148 | public function make($class, array $args = array()) 149 | { 150 | $class = $this->resolveKey($class); 151 | $reflection = new ReflectionClass($class); 152 | $constructor = $reflection->getConstructor(); 153 | 154 | if(!empty($constructor)) { 155 | $parameters = $constructor->getParameters(); 156 | $args = $this->resolveArguments($parameters, $args); 157 | } 158 | 159 | return $reflection->newInstanceArgs($args); 160 | } 161 | 162 | /** 163 | * Call closure or method with injecting parameters 164 | * 165 | * @param callable $callable 166 | * @param array $args 167 | * @return mixed 168 | */ 169 | public function call($callable, array $args = array(), array $class_args = array()) 170 | { 171 | if(!is_callable($callable)) { 172 | return new InvalidArgumentException("Callable must be callable, ".gettype($callable)." given"); 173 | } 174 | 175 | if(is_array($callable)) { 176 | list($class, $method) = $callable; 177 | $reflection = new ReflectionMethod($class, $method); 178 | if(is_string($callable[0])) { 179 | $callable[0] = $this->make($callable[0], $class_args); 180 | } 181 | } else { 182 | $reflection = new ReflectionFunction($callable); 183 | } 184 | 185 | $parameters = $reflection->getParameters(); 186 | $args = $this->resolveArguments($parameters, $args); 187 | 188 | return call_user_func_array($callable, $args); 189 | } 190 | 191 | /** 192 | * Resolve key 193 | * 194 | * @param string $key 195 | * @return string resolved key 196 | */ 197 | protected function resolveKey($key) 198 | { 199 | return trim($key, "\\"); 200 | } 201 | 202 | /** 203 | * Build arguments from array ReflectionParameter 204 | * 205 | * @param ReflectionParameter[] $parameters 206 | * @param array $args 207 | * @return array 208 | */ 209 | public function resolveArguments(array $parameters, array $additional_args = array()) 210 | { 211 | $resolved_args = []; 212 | $container = $this->container; 213 | foreach($additional_args as $arg_value) { 214 | if(is_object($arg_value)) { 215 | $container[get_class($arg_value)] = $arg_value; 216 | } 217 | } 218 | 219 | foreach($parameters as $i => $param) { 220 | $param_class = $param->getClass(); 221 | 222 | if($param_class) { 223 | $classname = $param_class->getName(); 224 | $arg = isset($additional_args[0])? $additional_args[0] : null; 225 | if (is_object($arg) AND get_class($arg) == $classname) { 226 | $resolved_args[] = $arg; 227 | } else { 228 | $resolved_args[] = $this->getOrMake($classname); 229 | } 230 | } elseif(!empty($additional_args)) { 231 | $resolved_args[] = array_shift($additional_args); 232 | } 233 | } 234 | 235 | return $resolved_args; 236 | } 237 | 238 | /** 239 | * Get Callable dependencies 240 | * 241 | * @return array Class dependencies 242 | */ 243 | public static function getCallableDependencies($callable) 244 | { 245 | if(is_array($callable)) { 246 | list($class, $method) = $callable; 247 | $reflection = new ReflectionMethod($class, $method); 248 | } else { 249 | $reflection = new ReflectionFunction($callable); 250 | } 251 | 252 | $parameters = $reflection->getParameters(); 253 | 254 | $dependencies = []; 255 | 256 | foreach($parameters as $param) { 257 | if($param->getClass()) { 258 | $dependencies[] = $param->getClass()->getName(); 259 | } 260 | } 261 | 262 | return $dependencies; 263 | } 264 | 265 | /** 266 | * --------------------------------------------------------------- 267 | * ArrayAccess interface methods 268 | * --------------------------------------------------------------- 269 | */ 270 | public function offsetSet($key, $value) { 271 | return $this->register($key, $value); 272 | } 273 | 274 | public function offsetExists($key) { 275 | return $this->has($key); 276 | } 277 | 278 | public function offsetUnset($key) { 279 | return $this->remove($key); 280 | } 281 | 282 | public function offsetGet($key) { 283 | return $this->get($key); 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /src/Controller.php: -------------------------------------------------------------------------------- 1 | app = $app; 10 | } 11 | 12 | public function __call($method, array $args) 13 | { 14 | return call_user_func_array([$this->app, $method], $args); 15 | } 16 | 17 | public function __get($key) 18 | { 19 | return $this->app->{$key}; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Dumper/BaseDumper.php: -------------------------------------------------------------------------------- 1 | getFile()) { 13 | $file = $e->getFile(); 14 | } elseif(isset($trace_data['class'])) { 15 | $ref = new \ReflectionClass($trace_data['class']); 16 | $file = $ref->getFileName(); 17 | } else { 18 | $file = "unknown file"; 19 | } 20 | 21 | return $file; 22 | } 23 | 24 | protected function getPossibilityLine(array $trace_data, \Exception $e = null) 25 | { 26 | if(isset($trace_data['line'])) { 27 | $line = $trace_data['line']; 28 | } elseif($e AND $e->getLine()) { 29 | $line = $e->getLine(); 30 | } else { 31 | $line = null; 32 | } 33 | 34 | return $line; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/Dumper/CliDumper.php: -------------------------------------------------------------------------------- 1 | getTrace(); 11 | $main_cause = array_shift($trace); 12 | $index = count($e->getTrace()); 13 | $result = ""; 14 | 15 | $message = $e->getMessage(); 16 | $class = get_class($e); 17 | 18 | // $result .= str_repeat("=", 60); 19 | $result .= "\n\n You got an '{$class}'\n"; 20 | $result .= " \"{$message}\"\n"; 21 | 22 | foreach($trace as $i => $data) { 23 | $file = $this->getPossibilityFile($data); 24 | $line = $this->getPossibilityLine($data); 25 | $message = $this->getMessage($data); 26 | $result .= "\n".str_pad(count($trace)-$i, 3, ' ', STR_PAD_LEFT).") ".$message; 27 | $result .= "\n {$file} [{$line}]\n"; 28 | } 29 | 30 | $result .= "\n\n"; 31 | // $result .= str_repeat("=", 60); 32 | 33 | return $result; 34 | } 35 | 36 | protected function getMessage($trace_data) 37 | { 38 | if(isset($trace_data['class'])) { 39 | $message = $trace_data['class']; 40 | if(isset($trace_data['function'])) { 41 | $message .= $trace_data['type'].$trace_data['function'].'()'; 42 | } 43 | } elseif($trace_data['function']) { 44 | $message = $trace_data['function'].'()'; 45 | } else { 46 | $message = ""; 47 | } 48 | 49 | return $message; 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/Dumper/DumperInterface.php: -------------------------------------------------------------------------------- 1 | getTrace(); 11 | $main_cause = array_shift($trace); 12 | $index = count($e->getTrace()); 13 | 14 | if($e instanceof \ErrorException AND count($trace) > 0) { 15 | $main_cause = array_shift($trace); 16 | $index -= 1; 17 | } 18 | 19 | $dom_main_cause = $this->renderMainCause($index, $e, $main_cause); 20 | $dom_traces = $this->renderTraces($trace); 21 | $message = $e->getMessage(); 22 | $class = get_class($e); 23 | 24 | $result = " 25 | 26 | 27 | 137 | 138 | 139 |
140 |

{$class}

141 |

{$message}

142 | 143 |
144 | 145 | 146 | "; 147 | 148 | return $result; 149 | } 150 | 151 | protected function renderMainCause($index, \Exception $e, array $trace) 152 | { 153 | $message = $this->getMessage($trace); 154 | $file = $this->getPossibilityFile($trace, $e); 155 | $line = $this->getPossibilityLine($trace, $e); 156 | 157 | return "
  • ".$this->renderTrace($index, $message, $file, $line)."
  • "; 158 | } 159 | 160 | protected function renderTraces(array $traces) 161 | { 162 | $dom_traces = ""; 163 | $count_traces = count($traces); 164 | foreach($traces as $i => $trace) { 165 | $no = $count_traces-$i; 166 | $message = $this->getMessage($trace); 167 | $file = $this->getPossibilityFile($trace); 168 | $line = $this->getPossibilityLine($trace); 169 | 170 | $dom_traces .= '
  • '.$this->renderTrace($no, $message, $file, $line).'
  • '; 171 | } 172 | return $dom_traces; 173 | } 174 | 175 | protected function renderTrace($index, $message, $file, $line = null) 176 | { 177 | $dom_index = "#{$index}"; 178 | $dom_message = "

    {$message}

    "; 179 | $info_line = $line? " on line {$line}": ", unknown line"; 180 | $dom_info = "{$file}{$info_line}"; 181 | 182 | return "{$dom_index}{$dom_message}{$dom_info}"; 183 | } 184 | 185 | protected function getMessage($trace_data) 186 | { 187 | $args_string = implode(', ', $this->getArgsDefinition($trace_data)); 188 | 189 | if(isset($trace_data['class'])) { 190 | $message = $trace_data['class']; 191 | if(isset($trace_data['function'])) { 192 | $message .= ''.$trace_data['type'].''.$trace_data['function'].'('.$args_string.')'; 193 | } 194 | } elseif($trace_data['function']) { 195 | $message = $trace_data['function'].'('.$args_string.')'; 196 | } else { 197 | $message = ""; 198 | } 199 | 200 | return $message; 201 | } 202 | 203 | protected function getArgsDefinition(array $trace_data) 204 | { 205 | $args = isset($trace_data['args'])? $trace_data['args'] : []; 206 | $args_definitions = []; 207 | foreach ($args as $arg) { 208 | if (is_object($arg)) { 209 | $args_definitions[] = get_class($arg); 210 | } elseif(is_array($arg)) { 211 | $args_definitions[] = 'Array('.count($arg).')'; 212 | } elseif(is_bool($arg)) { 213 | $args_definitions[] = $arg? 'true' : 'false'; 214 | } elseif(is_string($arg)) { 215 | $args_definitions[] = '"'.$arg.'"'; 216 | } else { 217 | $args_definitions[] = $arg; 218 | } 219 | } 220 | 221 | return $args_definitions; 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /src/Exceptions/FatalErrorException.php: -------------------------------------------------------------------------------- 1 | app = $app; 14 | $this->hooks = new Bag; 15 | } 16 | 17 | public function on($event, $actions, $limit = null) 18 | { 19 | $actions = (array) $actions; 20 | $hooks = $this->hooks->get($event, []); 21 | foreach($actions as $action) { 22 | $id = uniqid(); 23 | $path = $event.'.'.$id; 24 | $hook = new \stdClass; 25 | $hook->action = $action; 26 | $hook->limit = $limit; 27 | $hook->path = $path; 28 | 29 | $hooks[$id] = $hook; 30 | } 31 | 32 | $this->hooks->set($event, $hooks); 33 | return $this; 34 | } 35 | 36 | public function once($event, $actions) 37 | { 38 | return $this->on($event, $actions, 1); 39 | } 40 | 41 | public function applyFirst($event) 42 | { 43 | $hooks = Arr::flatten($this->hooks->get($event, [])); 44 | $first_hook = array_shift($hooks); 45 | return $first_hook? $this->applyHook($hook) : null; 46 | } 47 | 48 | public function applyLast($event) 49 | { 50 | $hooks = Arr::flatten($this->hooks->get($event, [])); 51 | $last_hook = array_pop($hooks); 52 | return $last_hook? $this->applyHook($hook) : null; 53 | } 54 | 55 | public function apply($event, array $params = array()) 56 | { 57 | $hooks = Arr::flatten($this->hooks->get($event, [])); 58 | $results = []; 59 | 60 | foreach($hooks as $hook) { 61 | $results[] = $this->applyHook($hook, $params); 62 | } 63 | 64 | return $results; 65 | } 66 | 67 | protected function applyHook($hook, $params) 68 | { 69 | $path = $hook->path; 70 | $limit = $hook->limit; 71 | $action = $this->resolveCallable($hook->action, $params); 72 | 73 | $result = $this->app->container->call($action, $params); 74 | 75 | if(is_int($limit)) { 76 | $limit -= 1; 77 | 78 | if($limit < 1) { 79 | $this->hooks->remove($path); 80 | } else { 81 | $hook->limit = $limit; 82 | } 83 | } 84 | 85 | return $result; 86 | } 87 | 88 | public function resolveCallable($callable, $params) 89 | { 90 | return $this->app->resolveCallable($callable, $params); 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /src/Http/Request.php: -------------------------------------------------------------------------------- 1 | defineRoute($route); 27 | $this->app = $app; 28 | 29 | $this->inputs = new Bag((array) $_POST + (array) $_GET); 30 | $this->files = new Bag((array) $_FILES); 31 | $this->server = new Bag($_SERVER); 32 | } 33 | 34 | public function path() 35 | { 36 | $path_info = $this->server->get('PATH_INFO'); 37 | 38 | if(!$path_info) { 39 | $request_uri = $this->server->get('REQUEST_URI'); 40 | $script_name = $this->server->get('SCRIPT_NAME'); 41 | if (pathinfo($script_name, PATHINFO_EXTENSION) == 'php') { 42 | $path_info = str_replace($script_name, '', $request_uri); 43 | } else { 44 | $path_info = $request_uri; 45 | } 46 | 47 | $path_info = explode('?', $path_info, 2)[0]; 48 | } 49 | 50 | return '/'.trim($path_info, '/'); 51 | } 52 | 53 | public function segment($index) 54 | { 55 | $paths = explode('/', $this->path()); 56 | return isset($paths[$index])? $paths[$index] : null; 57 | } 58 | 59 | public function all() 60 | { 61 | $all_inputs = $this->inputs->all(); 62 | $all_files = $this->files->all(); 63 | foreach($all_files as $key => $value) { 64 | $all_files[$key] = $this->hasMultiFiles($key)? $this->multiFiles($key) : $this->file($key); 65 | } 66 | 67 | return array_merge($all_inputs, $all_files); 68 | } 69 | 70 | public function has($key) 71 | { 72 | return $this->inputs->has($key); 73 | } 74 | 75 | public function get($key, $default = null) 76 | { 77 | return $this->inputs->get($key, $default); 78 | } 79 | 80 | public function only(array $keys) 81 | { 82 | return $this->inputs->only($keys, true); 83 | } 84 | 85 | public function except(array $keys) 86 | { 87 | return $this->inputs->except($keys, true); 88 | } 89 | 90 | public function file($key) 91 | { 92 | $_file = $this->files[$key]; 93 | return $this->hasFile($key)? $this->makeInputFile($_file) : NULL; 94 | } 95 | 96 | public function multiFiles($key) 97 | { 98 | if(!$this->hasMultiFiles($key)) return array(); 99 | 100 | $input_files = array(); 101 | 102 | $files = $this->files[$key]; 103 | 104 | $names = $files["name"]; 105 | $types = $files["type"]; 106 | $temps = $files["tmp_name"]; 107 | $errors = $files["error"]; 108 | $sizes = $files["size"]; 109 | 110 | foreach($temps as $i => $tmp) { 111 | if(empty($tmp) OR !is_uploaded_file($tmp)) continue; 112 | 113 | $_file = array( 114 | 'name' => $names[$i], 115 | 'type' => $types[$i], 116 | 'tmp_name' => $tmp, 117 | 'error' => $errors[$i], 118 | 'size' => $sizes[$i] 119 | ); 120 | 121 | $input_files[] = $this->makeInputFile($_file); 122 | } 123 | 124 | return $input_files; 125 | } 126 | 127 | public function hasFile($key) 128 | { 129 | $file = $this->files[$key]; 130 | 131 | if(!$file) return FALSE; 132 | 133 | $tmp = $file["tmp_name"]; 134 | 135 | if(!is_string($tmp)) return FALSE; 136 | 137 | return is_uploaded_file($tmp); 138 | } 139 | 140 | public function hasMultiFiles($key) 141 | { 142 | $files = $this->files[$key]; 143 | 144 | if(!$files) return FALSE; 145 | 146 | $uploaded_files = $files["tmp_name"]; 147 | if(!is_array($uploaded_files)) return FALSE; 148 | 149 | foreach($uploaded_files as $tmp_file) { 150 | if(!empty($tmp_file) AND is_uploaded_file($tmp_file)) return TRUE; 151 | } 152 | 153 | return FALSE; 154 | } 155 | 156 | protected function makeInputFile(array $_file) 157 | { 158 | return new UploadedFile($_file); 159 | } 160 | 161 | public function defineRoute(Route $route) 162 | { 163 | if($this->route) return; 164 | 165 | $this->params = $route->params; 166 | $this->route = $route; 167 | } 168 | 169 | public function route() 170 | { 171 | return $this->route; 172 | } 173 | 174 | public function isMethod($method) 175 | { 176 | return strtoupper($this->method()) == strtoupper($method); 177 | } 178 | 179 | public function isMethodGet() 180 | { 181 | return $this->isMethod(static::METHOD_GET); 182 | } 183 | 184 | public function isMethodPost() 185 | { 186 | return $this->isMethod(static::METHOD_POST); 187 | } 188 | 189 | public function isMethodPut() 190 | { 191 | return $this->isMethod(static::METHOD_PUT); 192 | } 193 | 194 | public function isMethodPatch() 195 | { 196 | return $this->isMethod(static::METHOD_PATCH); 197 | } 198 | 199 | public function isMethodDelete() 200 | { 201 | return $this->isMethod(static::METHOD_DELETE); 202 | } 203 | 204 | public function method() 205 | { 206 | return $_SERVER['REQUEST_METHOD']; 207 | } 208 | 209 | public function isHttps() 210 | { 211 | if( isset($_SERVER['HTTPS'] ) ) { 212 | return true; 213 | } else { 214 | return false; 215 | } 216 | } 217 | 218 | public function isHttp() 219 | { 220 | return !$this->isHttps(); 221 | } 222 | 223 | public function isAjax() 224 | { 225 | if(!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { 226 | return true; 227 | } else { 228 | return false; 229 | } 230 | } 231 | 232 | public function isJson() 233 | { 234 | return $this->json() !== NULL; 235 | } 236 | 237 | public function json() 238 | { 239 | $raw_body = $this->body(); 240 | $json = json_decode($raw_body, true); 241 | 242 | if(is_array($json)) { 243 | $data = new Bag($json); 244 | } else { 245 | $data = NULL; 246 | } 247 | 248 | return $data; 249 | } 250 | 251 | public function body() 252 | { 253 | return file_get_contents("php://input"); 254 | } 255 | 256 | public function param($key, $default = null) 257 | { 258 | $params = $this->params; 259 | return (array_key_exists($key, $params))? $params[$key] : $default; 260 | } 261 | 262 | public function params() 263 | { 264 | return $this->params; 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /src/Http/Response.php: -------------------------------------------------------------------------------- 1 | '100 Continue', 21 | 101 => '101 Switching Protocols', 22 | 23 | //Successful 2xx 24 | 200 => '200 OK', 25 | 201 => '201 Created', 26 | 202 => '202 Accepted', 27 | 203 => '203 Non-Authoritative Information', 28 | 204 => '204 No Content', 29 | 205 => '205 Reset Content', 30 | 206 => '206 Partial Content', 31 | 32 | //Redirection 3xx 33 | 300 => '300 Multiple Choices', 34 | 301 => '301 Moved Permanently', 35 | 302 => '302 Found', 36 | 303 => '303 See Other', 37 | 304 => '304 Not Modified', 38 | 305 => '305 Use Proxy', 39 | 306 => '306 (Unused)', 40 | 307 => '307 Temporary Redirect', 41 | 42 | //Client Error 4xx 43 | 400 => '400 Bad Request', 44 | 401 => '401 Unauthorized', 45 | 402 => '402 Payment Required', 46 | 403 => '403 Forbidden', 47 | 404 => '404 Not Found', 48 | 405 => '405 Method Not Allowed', 49 | 406 => '406 Not Acceptable', 50 | 407 => '407 Proxy Authentication Required', 51 | 408 => '408 Request Timeout', 52 | 409 => '409 Conflict', 53 | 410 => '410 Gone', 54 | 411 => '411 Length Required', 55 | 412 => '412 Precondition Failed', 56 | 413 => '413 Request Entity Too Large', 57 | 414 => '414 Request-URI Too Long', 58 | 415 => '415 Unsupported Media Type', 59 | 416 => '416 Requested Range Not Satisfiable', 60 | 417 => '417 Expectation Failed', 61 | 418 => '418 I\'m a teapot', 62 | 422 => '422 Unprocessable Entity', 63 | 423 => '423 Locked', 64 | 65 | //Server Error 5xx 66 | 500 => '500 Internal Server Error', 67 | 501 => '501 Not Implemented', 68 | 502 => '502 Bad Gateway', 69 | 503 => '503 Service Unavailable', 70 | 504 => '504 Gateway Timeout', 71 | 505 => '505 HTTP Version Not Supported' 72 | ); 73 | 74 | public $body = ""; 75 | 76 | public $dump_output = ""; 77 | 78 | protected $has_sent = false; 79 | 80 | public function __construct(App $app) 81 | { 82 | $this->app = $app; 83 | $this->headers = new ResponseHeaderBag; 84 | $this->reset(); 85 | } 86 | 87 | /** 88 | * set http status code 89 | * 90 | * @param int $status http status to set 91 | */ 92 | public function setStatus($status) 93 | { 94 | $status = (int) $status; 95 | if(!array_key_exists($status, $this->http_status_messages)) return $this; 96 | 97 | $this->status = $status; 98 | return $this; 99 | } 100 | 101 | /** 102 | * get response status code 103 | * 104 | * @return int 105 | */ 106 | public function getStatus() 107 | { 108 | return (int) $this->status; 109 | } 110 | 111 | /** 112 | * Check response status code 113 | * 114 | * @param string|int status code, can be 4xx, 2xx, 40x, 20x, etc 115 | * @return boolean 116 | */ 117 | public function isStatus($status_code) 118 | { 119 | $status = (string) $this->getStatus(); 120 | $regex = '/'.str_replace('x', '[0-9]', $status_code).'/'; // it should be /4[0-9][0-9]/ 121 | 122 | return (preg_match($regex, $status) != 0); 123 | } 124 | 125 | /** 126 | * set response content type 127 | * 128 | * @param string $type response content type 129 | */ 130 | public function setContentType($type) 131 | { 132 | $this->headers["CONTENT_TYPE"] = $type; 133 | return $this; 134 | } 135 | 136 | /** 137 | * get setted response content type 138 | * 139 | * @return string response content type 140 | */ 141 | public function getContentType() 142 | { 143 | return $this->headers["CONTENT_TYPE"]; 144 | } 145 | 146 | public function json(array $data, $status = null, $content_type = null) 147 | { 148 | $json = json_encode($data); 149 | $this->setContentType($content_type ? $content_type : static::CONTENT_TYPE_JSON); 150 | $this->setStatus($status); 151 | $this->body = $json; 152 | 153 | return $this; 154 | } 155 | 156 | public function html($content, $status = null, $content_type = null) 157 | { 158 | $this->setContentType($content_type ? $content_type : static::CONTENT_TYPE_HTML); 159 | $this->setStatus($status); 160 | $this->body = $content; 161 | 162 | return $this; 163 | } 164 | 165 | public function isJson() 166 | { 167 | return ($this->getContentType() == static::CONTENT_TYPE_JSON); 168 | } 169 | 170 | public function isHtml() 171 | { 172 | return ($this->getContentType() == static::CONTENT_TYPE_HTML); 173 | } 174 | 175 | public function reset() 176 | { 177 | $this->has_sent = false; 178 | 179 | return $this 180 | ->setStatus(200) 181 | ->clean(); 182 | } 183 | 184 | public function clean() 185 | { 186 | $this->body = ""; 187 | $this->dump_output = ""; 188 | return $this; 189 | } 190 | 191 | public function send($output = null, $status = null) 192 | { 193 | if($this->has_sent) return; 194 | 195 | if($output) { 196 | $this->body .= $output; 197 | } 198 | 199 | if($status) { 200 | $this->setStatus($status); 201 | } 202 | 203 | $status_str = (string) $this->status; 204 | 205 | $this->app->hook->apply($status_str[0].'xx', [$this, $this->app]); 206 | $this->app->hook->apply($status_str[0].$status_str[1].'x', [$this, $this->app]); 207 | $this->app->hook->apply($this->status, [$this, $this->app]); 208 | $this->app->hook->apply("response.before_send", [$this, $this->app]); 209 | 210 | if(!headers_sent()) $this->writeHeaders(); 211 | 212 | if('HEAD' != $this->app->request->server['REQUEST_METHOD']) { 213 | echo $this->body; 214 | } 215 | 216 | $this->has_sent = true; 217 | $this->app->hook->apply("response.after_send", [$this, $this->app]); 218 | } 219 | 220 | public function getStatusMessage($status_code) 221 | { 222 | if(array_key_exists($status_code, $this->http_status_messages)) { 223 | return $this->http_status_messages[$status_code]; 224 | } else { 225 | return null; 226 | } 227 | } 228 | 229 | protected function writeHeaders() 230 | { 231 | $headers = $this->headers->all(false); 232 | $content_type = ($type = $this->getContentType()) ? $type : static::CONTENT_TYPE_HTML; 233 | 234 | // http://stackoverflow.com/questions/6163970/set-response-status-code 235 | header("HTTP/1.1 ".$this->getStatusMessage($this->status), true, $this->status); 236 | header('Content-type: '.$content_type); 237 | foreach($headers as $key => $value) { 238 | $header = $this->normalizeHeaderKey($key).': '.$value; 239 | header($header); 240 | } 241 | } 242 | 243 | // http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields 244 | protected function normalizeHeaderKey($key) 245 | { 246 | return $this->headers->unresolveKey($key); 247 | } 248 | 249 | 250 | } -------------------------------------------------------------------------------- /src/Http/ResponseHeaderBag.php: -------------------------------------------------------------------------------- 1 | resolveKey($key); 17 | 18 | $not_ucwords = array( 19 | 'P3P' => 'P3P', 20 | 'X XSS PROTECTION' => 'X-XSS-Protection', 21 | 'X UA COMPATIBLE', 'X-UA-Compatible', 22 | 'X WEBKIT CSP' => 'X-WebKit-CSP', 23 | 'WWW AUTHENTICATE' => 'WWW-Authenticate' 24 | ); 25 | 26 | if(array_key_exists($key, $not_ucwords)) { 27 | return $not_ucwords[$key]; 28 | } else { 29 | $key = ucwords($key); 30 | $key = str_replace(' ', '-', $key); 31 | 32 | return $key; 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/Http/UploadedFile.php: -------------------------------------------------------------------------------- 1 | tmp = $_file['tmp_name']; 30 | $this->size = $_file['size']; 31 | $this->error = $_file['error']; 32 | $this->mimeType = $_file['type']; 33 | $this->location = $this->tmp; 34 | $this->originalName = $_file['name']; 35 | $this->name = pathinfo($_file['name'], PATHINFO_FILENAME); 36 | $this->extension = pathinfo($_file['name'], PATHINFO_EXTENSION); 37 | } 38 | 39 | public function getClientOriginalName() 40 | { 41 | return $this->originalName; 42 | } 43 | 44 | public function getFilename() 45 | { 46 | $ext = empty($this->extension)? "" : ".".$this->extension; 47 | return $this->name.$ext; 48 | } 49 | 50 | public function getTemporaryFile() 51 | { 52 | return $this->tmp; 53 | } 54 | 55 | public function getMimeType() 56 | { 57 | return $this->mimeType; 58 | } 59 | 60 | public function getLocation() 61 | { 62 | return $this->location; 63 | } 64 | 65 | public function move($location, $filename = null) 66 | { 67 | if ($filename) { 68 | $pathinfo = pathinfo($filename); 69 | $this->extension = $pathinfo['extension']; 70 | $this->name = $pathinfo['filename']; 71 | } 72 | 73 | if(!is_uploaded_file($this->tmp)) return FALSE; 74 | 75 | $location = rtrim($location, "/"); 76 | 77 | if(!is_dir($location)) { 78 | throw new \RuntimeException("Upload directory '{$location}' not found", 1); 79 | } else if(!is_writable($location)) { 80 | throw new \RuntimeException("Upload directory '{$location}' is not writable", 2); 81 | } 82 | 83 | $filepath = $location."/".$this->getFilename(); 84 | 85 | move_uploaded_file($this->tmp, $filepath); 86 | 87 | $has_moved = (false == is_uploaded_file($this->tmp)); 88 | 89 | if($has_moved) { 90 | $this->location = $filepath; 91 | } else { 92 | throw new \RuntimeException(" 93 | Upload file failed because unexpected reason. 94 | Maybe there is miss configuration in your php.ini settings" 95 | , 3); 96 | } 97 | } 98 | 99 | public function __toString() 100 | { 101 | return (string) file_get_contents($this->getLocation()); 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /src/MacroableTrait.php: -------------------------------------------------------------------------------- 1 | bindTo($this, get_class($this)), $parameters); 82 | } 83 | else 84 | { 85 | return call_user_func_array(static::$macros[$method], $parameters); 86 | } 87 | } 88 | 89 | throw new BadMethodCallException("Method {$method} does not exist."); 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /src/Provider.php: -------------------------------------------------------------------------------- 1 | app = $app; 10 | } 11 | 12 | abstract public function register(); 13 | 14 | abstract public function boot(); 15 | 16 | } -------------------------------------------------------------------------------- /src/Router/Route.php: -------------------------------------------------------------------------------- 1 | allowed_method = $allowed_method; 50 | $this->path = $this->resolvePath($path); 51 | $this->action = $action; 52 | $this->middlewares = $middlewares; 53 | $this->conditions = $conditions; 54 | } 55 | 56 | /** 57 | * Get route name 58 | * 59 | * @return string 60 | */ 61 | public function getName() 62 | { 63 | return $this->name; 64 | } 65 | 66 | /** 67 | * Get route path 68 | * 69 | * @return string 70 | */ 71 | public function getPath() 72 | { 73 | return $this->path; 74 | } 75 | 76 | /** 77 | * Get route allowed methods 78 | * 79 | * @return array 80 | */ 81 | public function getMethod() 82 | { 83 | return $this->allowed_method; 84 | } 85 | 86 | /** 87 | * Get route parameter conditions 88 | * 89 | * @return array 90 | */ 91 | public function getConditions() 92 | { 93 | return $this->conditions; 94 | } 95 | 96 | /** 97 | * Get registered middlewares 98 | * 99 | * @return array 100 | */ 101 | public function getMiddlewares() 102 | { 103 | return $this->middlewares; 104 | } 105 | 106 | /** 107 | * Get route action controller 108 | * 109 | * @return mixed action 110 | */ 111 | public function getAction() 112 | { 113 | return $this->action; 114 | } 115 | 116 | /** 117 | * Set route name 118 | * 119 | * @param string $name 120 | * @return self 121 | */ 122 | public function name($name) 123 | { 124 | $this->name = $name; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * Set parameter condition(regex) 131 | * 132 | * @param string $param 133 | * @param string $regex 134 | * @return self 135 | */ 136 | public function where($param, $regex) 137 | { 138 | $this->conditions[$param] = $regex; 139 | return $this; 140 | } 141 | 142 | /** 143 | * Set route middleware 144 | * 145 | * @param string|array $middlewares 146 | * @return void 147 | */ 148 | public function middleware($middlewares) 149 | { 150 | $this->middlewares = array_merge($this->middlewares, (array) $middlewares); 151 | return $this; 152 | } 153 | 154 | /** 155 | * Check if route is GET 156 | * 157 | * @return bool 158 | */ 159 | public function isGet() 160 | { 161 | return $this->isAllowing("GET"); 162 | } 163 | 164 | /** 165 | * Check if route is POST 166 | * 167 | * @return bool 168 | */ 169 | public function isPost() 170 | { 171 | return $this->isAllowing("POST"); 172 | } 173 | 174 | /** 175 | * Check if route is PUT 176 | * 177 | * @return bool 178 | */ 179 | public function isPut() 180 | { 181 | return $this->isAllowing("PUT"); 182 | } 183 | 184 | /** 185 | * Check if route is PATCH 186 | * 187 | * @return bool 188 | */ 189 | public function isPatch() 190 | { 191 | return $this->isAllowing("PATCH"); 192 | } 193 | 194 | /** 195 | * Check if route is DELETE 196 | * 197 | * @return bool 198 | */ 199 | public function isDelete() 200 | { 201 | return $this->isAllowing("DELETE"); 202 | } 203 | 204 | /** 205 | * Check if route allowing given method 206 | * 207 | * @return bool 208 | */ 209 | public function isAllowing($method) 210 | { 211 | return $this->resolveMethodName($method) == $this->getMethod(); 212 | } 213 | 214 | /** 215 | * Resolve method name 216 | * 217 | * @return string 218 | */ 219 | protected function resolveMethodName($method) 220 | { 221 | return strtoupper($method); 222 | } 223 | 224 | /** 225 | * Resolve path 226 | * 227 | * @return string 228 | */ 229 | protected function resolvePath($path) 230 | { 231 | return '/'.trim($path, '/'); 232 | } 233 | 234 | public function __call($method, $params) 235 | { 236 | $params = implode(',', $params); 237 | $middleware = $method.':'.$params; 238 | return $this->middleware($middleware); 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /src/Router/RouteGroup.php: -------------------------------------------------------------------------------- 1 | path = $this->resolvePath($path); 25 | $this->grouper = $grouper; 26 | $this->middlewares = $middlewares; 27 | $this->conditions = $conditions; 28 | } 29 | 30 | /** 31 | * Get path 32 | * 33 | * @return string $path 34 | */ 35 | public function getPath() 36 | { 37 | return $this->path; 38 | } 39 | 40 | /** 41 | * Get route collections 42 | * 43 | * @return array of Rakit\Framework\Router\Route 44 | */ 45 | public function getRoutes() 46 | { 47 | // reset routes and groups 48 | $this->routes = array(); 49 | $this->groups = array(); 50 | // run grouper 51 | call_user_func($this->grouper, $this); 52 | 53 | $routes = $this->routes; 54 | $groups = $this->groups; 55 | foreach($groups as $group) { 56 | $routes = array_merge($routes, $group->getRoutes()); 57 | } 58 | 59 | return $routes; 60 | } 61 | 62 | /** 63 | * Register routes by many methods 64 | * 65 | * @param array $methods 66 | * @param string $path 67 | * @return Route[] 68 | */ 69 | public function map(array $methods, $path, $handler) 70 | { 71 | return $this->group($path, function($group) use ($methods, $handler) { 72 | foreach($methods as $method) { 73 | $group->add($method, '/', $handler); 74 | } 75 | }); 76 | } 77 | 78 | /** 79 | * Register a Route 80 | * 81 | * @param string $method 82 | * @param string $path 83 | * @return Route 84 | */ 85 | public function add($method, $path, $handler) 86 | { 87 | $middlewares = $this->getMiddlewares(); 88 | $conditions = $this->getConditions(); 89 | $path = $this->getPath().$path; 90 | 91 | $route = new Route($method, $path, $handler, $middlewares, $conditions); 92 | $this->routes[] = $route; 93 | 94 | return $route; 95 | } 96 | 97 | /** 98 | * Register GET route 99 | * 100 | * @param string $path 101 | * @return Route 102 | */ 103 | public function get($path, $handler) 104 | { 105 | return $this->add('GET', $path, $handler); 106 | } 107 | 108 | /** 109 | * Register POST route 110 | * 111 | * @param string $path 112 | * @return Route 113 | */ 114 | public function post($path, $handler) 115 | { 116 | return $this->add('POST', $path, $handler); 117 | } 118 | 119 | /** 120 | * Register PUT route 121 | * 122 | * @param string $path 123 | * @return Route 124 | */ 125 | public function put($path, $handler) 126 | { 127 | return $this->add('PUT', $path, $handler); 128 | } 129 | 130 | /** 131 | * Register PATCH route 132 | * 133 | * @param string $path 134 | * @return Route 135 | */ 136 | public function patch($path, $handler) 137 | { 138 | return $this->add('PATCH', $path, $handler); 139 | } 140 | 141 | /** 142 | * Register DELETE route 143 | * 144 | * @param string $path 145 | * @return Route 146 | */ 147 | public function delete($path, $handler) 148 | { 149 | return $this->add('DELETE', $path, $handler); 150 | } 151 | 152 | /** 153 | * Grouping routes with Closure 154 | * 155 | * @param string $path 156 | * @param Closure $grouper 157 | * @return RouteMap 158 | */ 159 | public function group($path, Closure $grouper) 160 | { 161 | $path = $this->getPath().$path; 162 | $middlewares = $this->getMiddlewares(); 163 | $conditions = $this->getConditions(); 164 | $group = new RouteGroup($path, $grouper, $middlewares, $conditions); 165 | 166 | $this->groups[] = $group; 167 | 168 | return $group; 169 | } 170 | 171 | /** 172 | * Set parameter condition 173 | * 174 | * @param string $param 175 | * @param string $condition 176 | * @return void 177 | */ 178 | public function where($param, $condition) 179 | { 180 | $this->conditions[$param] = $condition; 181 | return $this; 182 | } 183 | 184 | /** 185 | * Append middlewares 186 | * 187 | * @param mixed $middlewares 188 | * @return void 189 | */ 190 | public function middleware($middlewares) 191 | { 192 | $this->middlewares = array_merge($this->middlewares, (array) $middlewares); 193 | return $this; 194 | } 195 | 196 | /** 197 | * Get parameter conditions 198 | * 199 | * @return array 200 | */ 201 | public function getConditions() 202 | { 203 | return $this->conditions; 204 | } 205 | 206 | /** 207 | * Get middlewares 208 | * 209 | * @return array 210 | */ 211 | public function getMiddlewares() 212 | { 213 | return $this->middlewares; 214 | } 215 | 216 | /** 217 | * Resolve path 218 | * 219 | * @return string 220 | */ 221 | protected function resolvePath($path) 222 | { 223 | return '/'.trim($path, '/'); 224 | } 225 | 226 | public function __call($method, $params) 227 | { 228 | $params = implode(',', $params); 229 | $middleware = $method.':'.$params; 230 | return $this->middleware($middleware); 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /src/Router/Router.php: -------------------------------------------------------------------------------- 1 | routes; 64 | foreach($this->groups as $group) { 65 | $routes = array_merge($routes, $group->getRoutes()); 66 | } 67 | 68 | return $routes; 69 | } 70 | 71 | /** 72 | * Register a Route 73 | * 74 | * @param string $method 75 | * @param string $path 76 | * @return Route 77 | */ 78 | public function add($method, $path, $handler) 79 | { 80 | $route = new Route($method, $path, $handler); 81 | $this->routes[] = $route; 82 | 83 | return $route; 84 | } 85 | 86 | /** 87 | * Register GET Route 88 | * 89 | * @param string $path 90 | * @param Closure|string $handler 91 | * @return Route 92 | */ 93 | public function get($path, $handler) 94 | { 95 | return $this->add('GET', $path, $handler); 96 | } 97 | 98 | /** 99 | * Register POST Route 100 | * 101 | * @param string $path 102 | * @param Closure|string $handler 103 | * @return Route 104 | */ 105 | public function post($path, $handler) 106 | { 107 | return $this->add('POST', $path, $handler); 108 | } 109 | 110 | /** 111 | * Register PUT Route 112 | * 113 | * @param string $path 114 | * @param Closure|string $handler 115 | * @return Route 116 | */ 117 | public function put($path, $handler) 118 | { 119 | return $this->add('PUT', $path, $handler); 120 | } 121 | 122 | /** 123 | * Register PATCH Route 124 | * 125 | * @param string $path 126 | * @param Closure|string $handler 127 | * @return Route 128 | */ 129 | public function patch($path, $handler) 130 | { 131 | return $this->add('PATCH', $path, $handler); 132 | } 133 | 134 | /** 135 | * Register DELETE Route 136 | * 137 | * @param string $path 138 | * @param Closure|string $handler 139 | * @return Route 140 | */ 141 | public function delete($path, $handler) 142 | { 143 | return $this->add('DELETE', $path, $handler); 144 | } 145 | 146 | /** 147 | * Grouping routes with Closure 148 | * 149 | * @param string $path 150 | * @param Closure $grouper 151 | * @return RouteGroup 152 | */ 153 | public function group($path, Closure $grouper) 154 | { 155 | $group = new RouteGroup($path, $grouper); 156 | $this->groups[] = $group; 157 | return $group; 158 | } 159 | 160 | /** 161 | * Grouping some routes in different methods 162 | * 163 | * @param string $path 164 | * @param Closure|string $handler 165 | * @return RouteGroup 166 | */ 167 | public function map(array $methods, $path, $handler) 168 | { 169 | $group = new RouteGroup($path, function($group) use ($methods, $handler) { 170 | foreach($methods as $method) { 171 | $group->add($method, '/', $handler); 172 | } 173 | }); 174 | 175 | $this->groups[] = $group; 176 | return $group; 177 | } 178 | 179 | /** 180 | * Register GET, POST, PUT, PATCH & DELETE routes with same path & handler 181 | * 182 | * @param string $path 183 | * @param Closure|string $handler 184 | * @return RouteGroup 185 | */ 186 | public function any($path, $handler) 187 | { 188 | return $this->map(['GET','POST','PUT','PATCH','DELETE'], $path, $handler); 189 | } 190 | 191 | /** 192 | * Enable cache routes by giving cache file name 193 | * 194 | * @param string|callable $cache_file 195 | * @return null 196 | */ 197 | public function cache($cache_file) 198 | { 199 | if (!is_string($cache_file) AND !is_callable($cache_file)) { 200 | throw new \InvalidArgumentException("Cache name must be string or callable", 1); 201 | } 202 | 203 | $this->cache_file = $cache_file; 204 | } 205 | 206 | /** 207 | * Find route by given path and method 208 | * 209 | * @param string $path 210 | * @param string $method 211 | * @return null|Route 212 | */ 213 | public function dispatch($method, $path) 214 | { 215 | $pattern = $this->makePattern($method, $path); 216 | $all_routes = $this->getRoutes(); 217 | 218 | $cache_file = $this->cache_file; 219 | $chunk_routes = array_chunk($all_routes, $this->max_routes_in_regex); 220 | 221 | if ($cache_file AND is_callable($cache_file)) { 222 | $cache_file = call_user_func_array($cache_file, [$all_routes, $this]); 223 | } 224 | 225 | if ($cache_file AND file_exists($cache_file) AND $cached_routes = $this->getCachedRoutes($cache_file)) { 226 | $route_regexes = $cached_routes; 227 | } else { 228 | $route_regexes = []; 229 | 230 | foreach($chunk_routes as $i => $routes) { 231 | $regex = []; 232 | foreach($routes as $i => $route) { 233 | $regex[] = $this->toRegex($route, $i); 234 | } 235 | $regex = "~^(?|".implode("|", $regex).")$~x"; 236 | $route_regexes[] = $regex; 237 | } 238 | 239 | if ($cache_file) { 240 | $this->cacheRoutes($cache_file, $route_regexes); 241 | } 242 | } 243 | 244 | foreach($route_regexes as $i => $regex) { 245 | $routes = $chunk_routes[$i]; 246 | 247 | if(!preg_match($regex, $pattern, $matches)) { 248 | continue; 249 | } 250 | 251 | $index = (count($matches) - 1 - $this->max_params); 252 | 253 | $matched_route = $routes[$index]; 254 | $matched_route->params = []; 255 | $path = $matched_route->getPath(); 256 | 257 | $params = $this->getDeclaredPathParams($matched_route); 258 | foreach($params as $i => $param) { 259 | // find param index in $matches 260 | // the problem is if using optional parameter like: 261 | // /foo/:bar(/:baz) 262 | // the regex should look like this: 263 | // /foo/()(/())? 264 | // so the index of :baz is not $i, 265 | // so the trick is to add $i with count char '(' before that :param in route path 266 | // for example if route path like this: 267 | // /foo/:bar(/:baz(/:qux)), the regex: /foo/()(/()(/())?)? 268 | // so index for :qux is 3+2, where 2 is count '(' before :qux 269 | $pos = strpos($path, ':'.$param); 270 | $count_open_bracket = substr_count($path, '(', 0, $pos); 271 | $value = $matches[$i+1+$count_open_bracket]; 272 | 273 | if($value) { 274 | $matched_route->params[$param] = $value; 275 | } 276 | } 277 | 278 | return $matched_route; 279 | } 280 | 281 | return null; 282 | } 283 | 284 | /** 285 | * Alias for dispatch 286 | */ 287 | public function findMatch($path, $method) 288 | { 289 | return $this->dispatch($method, $path); 290 | } 291 | 292 | /** 293 | * Get route by given name 294 | * 295 | * @param string $route_name 296 | * @return null|Route 297 | */ 298 | public function findRouteByName($name) 299 | { 300 | $routes = $this->getRoutes(); 301 | foreach($routes as $route) { 302 | if($route->getName() == $name) return $route; 303 | } 304 | return null; 305 | } 306 | 307 | /** 308 | * Get cache route regex from cache file 309 | * 310 | * @param string $cache_file 311 | * @return array 312 | */ 313 | protected function getCachedRoutes($cache_file) 314 | { 315 | $content = file_get_contents($cache_file); 316 | return unserialize($content); 317 | } 318 | 319 | /** 320 | * Save routes to cache file 321 | * 322 | * @param string $cache_file 323 | * @return array 324 | */ 325 | protected function cacheRoutes($cache_file, $route_regexes) 326 | { 327 | $content = serialize($route_regexes); 328 | return file_put_contents($cache_file, $content); 329 | } 330 | 331 | /** 332 | * Tranfrom route path into Regex 333 | * 334 | * @param Route $route 335 | * @return string regex 336 | */ 337 | protected function toRegex(Route $route, $index) 338 | { 339 | $method = $route->getMethod(); 340 | $path = $route->getPath(); 341 | $conditions = $route->getConditions(); 342 | $params = $this->getDeclaredPathParams($route); 343 | 344 | $regex = $this->makePattern($method, $path); 345 | 346 | // transform /foo/:bar(/:baz) => /foo/:bar(/:baz)? 347 | $regex = str_replace(')', ')?', $regex); 348 | 349 | foreach($params as $param) { 350 | if (array_key_exists($param, $conditions)) { 351 | $regex = str_replace(':'.$param, '('.$conditions[$param].')', $regex); 352 | } else { 353 | $regex = str_replace(':'.$param, '('.$this->default_param_regex.')', $regex); 354 | } 355 | } 356 | 357 | $count_brackets = substr_count($regex, "("); 358 | $count_added_brackets = $this->max_params + ($index - $count_brackets); 359 | 360 | $regex .= str_repeat("()", $count_added_brackets); 361 | 362 | return $regex; 363 | } 364 | 365 | /** 366 | * Getting declared parameters by given route object 367 | * 368 | * @param Route $route 369 | * @return array 370 | */ 371 | protected function getDeclaredPathParams(Route $route) 372 | { 373 | $path = $route->getPath(); 374 | preg_match_all('/\:([a-z_][a-z0-9_]+)/i', $path, $match); 375 | return $match[1]; 376 | } 377 | 378 | public function makePattern($method, $path) 379 | { 380 | return $method.$path; 381 | } 382 | 383 | } 384 | -------------------------------------------------------------------------------- /src/Util/Arr.php: -------------------------------------------------------------------------------- 1 | $value) 44 | { 45 | list($innerKey, $innerValue) = call_user_func($callback, $key, $value); 46 | 47 | $results[$innerKey] = $innerValue; 48 | } 49 | 50 | return $results; 51 | } 52 | 53 | /** 54 | * Divide an array into two arrays. One with keys and the other with values. 55 | * 56 | * @param array $array 57 | * @return array 58 | */ 59 | public static function divide($array) 60 | { 61 | return [array_keys($array), array_values($array)]; 62 | } 63 | 64 | /** 65 | * Flatten a multi-dimensional associative array with dots. 66 | * 67 | * @param array $array 68 | * @param string $prepend 69 | * @return array 70 | */ 71 | public static function dot($array, $prepend = '') 72 | { 73 | $results = []; 74 | 75 | foreach ($array as $key => $value) 76 | { 77 | if (is_array($value)) 78 | { 79 | $results = array_merge($results, static::dot($value, $prepend.$key.'.')); 80 | } 81 | else 82 | { 83 | $results[$prepend.$key] = $value; 84 | } 85 | } 86 | 87 | return $results; 88 | } 89 | 90 | /** 91 | * Get all of the given array except for a specified array of items. 92 | * 93 | * @param array $array 94 | * @param array|string $keys 95 | * @return array 96 | */ 97 | public static function except($array, $keys) 98 | { 99 | return array_diff_key($array, array_flip((array) $keys)); 100 | } 101 | 102 | /** 103 | * Fetch a flattened array of a nested array element. 104 | * 105 | * @param array $array 106 | * @param string $key 107 | * @return array 108 | */ 109 | public static function fetch($array, $key) 110 | { 111 | foreach (explode('.', $key) as $segment) 112 | { 113 | $results = []; 114 | 115 | foreach ($array as $value) 116 | { 117 | if (array_key_exists($segment, $value = (array) $value)) 118 | { 119 | $results[] = $value[$segment]; 120 | } 121 | } 122 | 123 | $array = array_values($results); 124 | } 125 | 126 | return array_values($results); 127 | } 128 | 129 | /** 130 | * Return the first element in an array passing a given truth test. 131 | * 132 | * @param array $array 133 | * @param callable $callback 134 | * @param mixed $default 135 | * @return mixed 136 | */ 137 | public static function first($array, callable $callback, $default = null) 138 | { 139 | foreach ($array as $key => $value) 140 | { 141 | if (call_user_func($callback, $key, $value)) return $value; 142 | } 143 | 144 | return $default; 145 | } 146 | 147 | /** 148 | * Return the last element in an array passing a given truth test. 149 | * 150 | * @param array $array 151 | * @param callable $callback 152 | * @param mixed $default 153 | * @return mixed 154 | */ 155 | public static function last($array, callable $callback, $default = null) 156 | { 157 | return static::first(array_reverse($array), $callback, $default); 158 | } 159 | 160 | /** 161 | * Flatten a multi-dimensional array into a single level. 162 | * 163 | * @param array $array 164 | * @return array 165 | */ 166 | public static function flatten($array) 167 | { 168 | $return = []; 169 | 170 | array_walk_recursive($array, function($x) use (&$return) { $return[] = $x; }); 171 | 172 | return $return; 173 | } 174 | 175 | /** 176 | * Remove one or many array items from a given array using "dot" notation. 177 | * 178 | * @param array $array 179 | * @param array|string $keys 180 | * @return void 181 | */ 182 | public static function forget(&$array, $keys) 183 | { 184 | $original =& $array; 185 | 186 | foreach ((array) $keys as $key) 187 | { 188 | $parts = explode('.', $key); 189 | 190 | while (count($parts) > 1) 191 | { 192 | $part = array_shift($parts); 193 | 194 | if (isset($array[$part]) && is_array($array[$part])) 195 | { 196 | $array =& $array[$part]; 197 | } 198 | } 199 | 200 | unset($array[array_shift($parts)]); 201 | 202 | // clean up after each pass 203 | $array =& $original; 204 | } 205 | } 206 | 207 | /** 208 | * Get an item from an array using "dot" notation. 209 | * 210 | * @param array $array 211 | * @param string $key 212 | * @param mixed $default 213 | * @return mixed 214 | */ 215 | public static function get($array, $key, $default = null) 216 | { 217 | if (is_null($key)) return $array; 218 | 219 | if (isset($array[$key])) return $array[$key]; 220 | 221 | foreach (explode('.', $key) as $segment) 222 | { 223 | if ( ! is_array($array) || ! array_key_exists($segment, $array)) 224 | { 225 | return $default; 226 | } 227 | 228 | $array = $array[$segment]; 229 | } 230 | 231 | return $array; 232 | } 233 | 234 | /** 235 | * Check if an item exists in an array using "dot" notation. 236 | * 237 | * @param array $array 238 | * @param string $key 239 | * @return bool 240 | */ 241 | public static function has($array, $key) 242 | { 243 | if (empty($array) || is_null($key)) return false; 244 | 245 | if (array_key_exists($key, $array)) return true; 246 | 247 | foreach (explode('.', $key) as $segment) 248 | { 249 | if ( ! is_array($array) || ! array_key_exists($segment, $array)) 250 | { 251 | return false; 252 | } 253 | 254 | $array = $array[$segment]; 255 | } 256 | 257 | return true; 258 | } 259 | 260 | /** 261 | * Get a subset of the items from the given array. 262 | * 263 | * @param array $array 264 | * @param array|string $keys 265 | * @return array 266 | */ 267 | public static function only($array, $keys) 268 | { 269 | return array_intersect_key($array, array_flip((array) $keys)); 270 | } 271 | 272 | /** 273 | * Pluck an array of values from an array. 274 | * 275 | * @param array $array 276 | * @param string $value 277 | * @param string $key 278 | * @return array 279 | */ 280 | public static function pluck($array, $value, $key = null) 281 | { 282 | $results = []; 283 | 284 | foreach ($array as $item) 285 | { 286 | $itemValue = data_get($item, $value); 287 | 288 | // If the key is "null", we will just append the value to the array and keep 289 | // looping. Otherwise we will key the array using the value of the key we 290 | // received from the developer. Then we'll return the final array form. 291 | if (is_null($key)) 292 | { 293 | $results[] = $itemValue; 294 | } 295 | else 296 | { 297 | $itemKey = data_get($item, $key); 298 | 299 | $results[$itemKey] = $itemValue; 300 | } 301 | } 302 | 303 | return $results; 304 | } 305 | 306 | /** 307 | * Get a value from the array, and remove it. 308 | * 309 | * @param array $array 310 | * @param string $key 311 | * @param mixed $default 312 | * @return mixed 313 | */ 314 | public static function pull(&$array, $key, $default = null) 315 | { 316 | $value = static::get($array, $key, $default); 317 | 318 | static::forget($array, $key); 319 | 320 | return $value; 321 | } 322 | 323 | /** 324 | * Set an array item to a given value using "dot" notation. 325 | * 326 | * If no key is given to the method, the entire array will be replaced. 327 | * 328 | * @param array $array 329 | * @param string $key 330 | * @param mixed $value 331 | * @return array 332 | */ 333 | public static function set(&$array, $key, $value) 334 | { 335 | if (is_null($key)) return $array = $value; 336 | 337 | $keys = explode('.', $key); 338 | 339 | while (count($keys) > 1) 340 | { 341 | $key = array_shift($keys); 342 | 343 | // If the key doesn't exist at this depth, we will just create an empty array 344 | // to hold the next value, allowing us to create the arrays to hold final 345 | // values at the correct depth. Then we'll keep digging into the array. 346 | if ( ! isset($array[$key]) || ! is_array($array[$key])) 347 | { 348 | $array[$key] = []; 349 | } 350 | 351 | $array =& $array[$key]; 352 | } 353 | 354 | $array[array_shift($keys)] = $value; 355 | 356 | return $array; 357 | } 358 | 359 | /** 360 | * Filter the array using the given callback. 361 | * 362 | * @param array $array 363 | * @param callable $callback 364 | * @return array 365 | */ 366 | public static function where($array, callable $callback) 367 | { 368 | $filtered = []; 369 | 370 | foreach ($array as $key => $value) 371 | { 372 | if (call_user_func($callback, $key, $value)) $filtered[$key] = $value; 373 | } 374 | 375 | return $filtered; 376 | } 377 | 378 | public static function remove(array &$array, $key) 379 | { 380 | $keys = explode('.', $key); 381 | 382 | while(count($keys) > 1) { 383 | $key = array_shift($keys); 384 | 385 | if(!isset($array[$key]) OR !is_array($array[$key])) { 386 | $array[$key] = array(); 387 | } 388 | 389 | $array =& $array[$key]; 390 | } 391 | 392 | unset($array[array_shift($keys)]); 393 | } 394 | 395 | } 396 | -------------------------------------------------------------------------------- /src/Util/Str.php: -------------------------------------------------------------------------------- 1 | view_path = $view_path; 14 | } 15 | 16 | public function render($file, array $data = array()) 17 | { 18 | $view_file = $this->view_path.'/'.$file; 19 | 20 | if(!file_exists($view_file)) { 21 | throw new RuntimeException("Cannot render view '{$view_file}', file not found", 1); 22 | } 23 | 24 | $render = function($__file, array $__data) { 25 | extract($__data); 26 | 27 | ob_start(); 28 | include($__file); 29 | return ob_get_clean(); 30 | }; 31 | 32 | return $render($view_file, $data); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/View/View.php: -------------------------------------------------------------------------------- 1 | app = $app; 18 | $this->setEngine($engine); 19 | } 20 | 21 | public function setEngine(ViewEngineInterface $engine) 22 | { 23 | $this->engine = $engine; 24 | } 25 | 26 | public function set($key, $value) 27 | { 28 | $this->data[$key] = $value; 29 | } 30 | 31 | public function get($key, $default = null) 32 | { 33 | return isset($this->data[$key])? $this->data[$key] : $default; 34 | } 35 | 36 | public function render($file, array $data = array()) 37 | { 38 | $data = array_merge($this->data, $data); 39 | $data['app'] = $this->app; 40 | 41 | return $this->engine->render($file, $data); 42 | } 43 | 44 | public function __call($method, $params) 45 | { 46 | return call_user_func_array([$this->engine, $method], $params); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/View/ViewEngineInterface.php: -------------------------------------------------------------------------------- 1 | app; 15 | $app['view:Rakit\Framework\View\View'] = $app->container->singleton(function($container) use ($app) { 16 | $view_path = $app->config->get('view.path'); 17 | $engine = $app->config->get('view.engine'); 18 | 19 | if(!$engine) { 20 | $engine = new BasicViewEngine($view_path); 21 | } elseif(is_string($engine)) { 22 | $engine = $container->make($engine, [$view_path]); 23 | } 24 | 25 | return new View($container['app'], $engine); 26 | }); 27 | } 28 | 29 | public function boot() 30 | { 31 | $app = $this->app; 32 | $app->response->macro('view', function($file, array $data = array()) use ($app) { 33 | $rendered = $app->view->render($file, $data); 34 | return $app->response->html($rendered); 35 | }); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /tests/BagTests.php: -------------------------------------------------------------------------------- 1 | bag = new Bag(); 11 | } 12 | 13 | public function tearDown() { 14 | $this->bag = null; 15 | } 16 | 17 | public function testSet() 18 | { 19 | $this->bag->set("foo.bar", "foobar"); 20 | 21 | $items = $this->bag->all(); 22 | 23 | $this->assertEquals($items['foo']['bar'], "foobar"); 24 | } 25 | 26 | public function testSetDotNotation() 27 | { 28 | $this->bag["foo.bar"] = "foobar"; 29 | 30 | $items = $this->bag->all(); 31 | 32 | $this->assertEquals($items['foo']['bar'], "foobar"); 33 | } 34 | 35 | public function testGet() 36 | { 37 | $this->bag->set("foo.bar", "foobar"); 38 | 39 | $value = $this->bag->get("foo.bar"); 40 | $undefined = $this->bag->get("undefined.key"); 41 | $default_value = $this->bag->get("undefined.key", "default value"); 42 | 43 | $this->assertEquals($value, "foobar"); 44 | $this->assertEquals(NULL, $undefined); 45 | $this->assertEquals($default_value, "default value"); 46 | } 47 | 48 | public function testGetDotNotation() 49 | { 50 | $this->bag->set("foo.bar", "foobar"); 51 | 52 | $value = $this->bag["foo.bar"]; 53 | $undefined = $this->bag["undefined.key"]; 54 | 55 | $this->assertEquals($value, "foobar"); 56 | } 57 | 58 | public function testHas() 59 | { 60 | $this->bag->set("foo.bar", "foobar"); 61 | 62 | $this->assertTrue($this->bag->has("foo.bar")); 63 | $this->assertFalse($this->bag->has("undefined.key")); 64 | } 65 | 66 | public function testExcept() 67 | { 68 | $this->bag->set('foo.A', 'A'); 69 | $this->bag->set('foo.B', 'B'); 70 | $this->bag->set('foo.C', 'C'); 71 | $this->bag->set('bar.D', 'C'); 72 | 73 | $new_bag = $this->bag->except(['foo.B']); 74 | 75 | $this->assertTrue(isset($new_bag['foo'])); 76 | $this->assertTrue(isset($new_bag['bar'])); 77 | $this->assertTrue(isset($new_bag['foo']['A'])); 78 | $this->assertTrue(isset($new_bag['foo']['C'])); 79 | $this->assertFalse(isset($new_bag['foo']['B'])); 80 | } 81 | 82 | public function testOnly() 83 | { 84 | $this->bag->set('foo.A', 'A'); 85 | $this->bag->set('foo.B', 'B'); 86 | $this->bag->set('foo.C', 'C'); 87 | 88 | $new_bag = $this->bag->only(['foo.A', 'foo.C']); 89 | 90 | $this->assertTrue(isset($new_bag['foo'])); 91 | $this->assertTrue(isset($new_bag['foo']['A'])); 92 | $this->assertTrue(isset($new_bag['foo']['C'])); 93 | $this->assertFalse(isset($new_bag['foo']['B'])); 94 | } 95 | 96 | public function testNamespace() 97 | { 98 | // add namespace store 99 | $this->bag->store = array( 100 | 'store_name' => 'my store', 101 | 'database' => array( 102 | 'host' => 'localhost', 103 | 'username' => 'db_user', 104 | 'password' => 'db_password', 105 | 'dbname' => 'db_store' 106 | ) 107 | ); 108 | 109 | // get simple value from namespace 110 | $store_name = $this->bag->store->get("store_name"); 111 | $this->assertEquals($store_name, "my store"); 112 | 113 | // get value using dot notation 114 | $db_host = $this->bag->store["database.host"]; 115 | $this->assertEquals($db_host, "localhost"); 116 | 117 | // set value using set 118 | $this->bag->store->set("database.host", "new_db_host"); 119 | // set value using dot notation 120 | $this->bag->store->set("database.dbname", "new_store_db"); 121 | 122 | // get all changed items store 123 | $items = $this->bag->store->all(); 124 | 125 | $this->assertEquals(5, $this->bag->size()); 126 | $this->assertEquals($items["database"]["host"], "new_db_host"); 127 | $this->assertEquals($items["database"]["dbname"], "new_store_db"); 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /tests/ContainerTests.php: -------------------------------------------------------------------------------- 1 | foo = $foo; 21 | } 22 | 23 | } 24 | 25 | class HasMethodDependFoo { 26 | 27 | public function getFoo(Foo $foo) 28 | { 29 | return $foo->foo; 30 | } 31 | 32 | } 33 | /** 34 | * ------------------------------------------------------------------------- 35 | * 36 | */ 37 | 38 | class ContainerTests extends PHPUnit_Framework_TestCase { 39 | 40 | protected $container; 41 | 42 | public function setUp() { 43 | $this->container = new Container(); 44 | } 45 | 46 | public function tearDown() { 47 | $this->container = null; 48 | } 49 | 50 | public function testValueInjection() 51 | { 52 | $this->container['foo'] = "bar"; 53 | 54 | $this->assertEquals("bar", $this->container['foo']); 55 | } 56 | 57 | public function testSingleton() 58 | { 59 | $this->container['foo'] = $this->container->singleton(function() { 60 | return new Foo; 61 | }); 62 | 63 | $this->assertTrue($this->container['foo'] === $this->container['foo']); 64 | } 65 | 66 | public function testNotSingleton() 67 | { 68 | $this->container['foo'] = function() { 69 | return new Foo; 70 | }; 71 | 72 | $this->assertTrue($this->container['foo'] !== $this->container['foo']); 73 | } 74 | 75 | public function testConstructorInjection() 76 | { 77 | $this->container['foo:Foo'] = $this->container->singleton(function() { 78 | return new Foo; 79 | }); 80 | 81 | $this->container['dependFoo'] = $this->container->make('DependFoo'); 82 | $this->assertTrue($this->container['foo'] === $this->container['dependFoo']->foo); 83 | } 84 | 85 | public function testConstructorInjectionAlsoInjectUnregisteredClass() 86 | { 87 | $dependFoo = $this->container->make('DependFoo'); 88 | $this->assertTrue($dependFoo->foo instanceof Foo); 89 | } 90 | 91 | public function testMethodInjection() 92 | { 93 | $this->container['foo:Foo'] = $this->container->singleton(function() { 94 | return new Foo; 95 | }); 96 | 97 | $this->assertEquals( 98 | $this->container['foo']->foo, 99 | $this->container->call(['HasMethodDependFoo', 'getFoo']) 100 | ); 101 | } 102 | 103 | public function testMethodInjectionAlsoInjectUnregisteredClass() 104 | { 105 | $this->assertEquals($this->container->call(['HasMethodDependFoo', 'getFoo']), "foobar"); 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /tests/HookTests.php: -------------------------------------------------------------------------------- 1 | app = new App(''); 11 | $this->hook = $this->app->hook; 12 | } 13 | 14 | public function tearDown() { 15 | $this->app = null; 16 | $this->hook = null; 17 | } 18 | 19 | public function testApply() 20 | { 21 | $hook = $this->hook; 22 | 23 | $hook->on('test', function() { 24 | echo "foo"; 25 | }); 26 | 27 | $hook->on('test', function() { 28 | echo "bar"; 29 | }); 30 | 31 | $this->assertEquals("foobar", $this->getOutput( 32 | function() use ($hook) { 33 | $hook->apply("test"); 34 | } 35 | )); 36 | } 37 | 38 | public function testApplyOnceEvent() 39 | { 40 | $hook = $this->hook; 41 | 42 | $hook->once('test', function() { 43 | echo "foo"; 44 | }); 45 | 46 | $this->assertEquals("foo", $this->getOutput( 47 | function() use ($hook) { 48 | $hook->apply("test"); 49 | } 50 | )); 51 | 52 | $this->assertEmpty($this->getOutput( 53 | function() use ($hook) { 54 | $hook->apply("test"); 55 | } 56 | )); 57 | } 58 | 59 | 60 | public function testApplyWithDotNotation() 61 | { 62 | $hook = $this->hook; 63 | 64 | $hook->on('test.foo', function() { 65 | echo "foo"; 66 | }); 67 | 68 | $hook->on('test.bar.baz', function() { 69 | echo "barbaz"; 70 | }); 71 | 72 | $hook->on('test.qux', function() { 73 | echo "qux"; 74 | }); 75 | 76 | $this->assertEquals("foobarbazqux", $this->getOutput( 77 | function() use ($hook) { 78 | $hook->apply("test"); 79 | } 80 | )); 81 | } 82 | 83 | protected function getOutput(\Closure $callback) 84 | { 85 | ob_start(); 86 | $callback(); 87 | return ob_get_clean(); 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /tests/RouterTests.php: -------------------------------------------------------------------------------- 1 | app = new App('router-test', ['app' => ['debug' => true]]); 12 | $this->router = $this->app->router; 13 | } 14 | 15 | public function tearDown() { 16 | $this->app = null; 17 | } 18 | 19 | public function testDispatchRouteGet() 20 | { 21 | $this->router->post('/route/:param', 'handler'); 22 | $this->router->get('/route/:param', 'handler'); 23 | $this->router->put('/route/:param', 'handler'); 24 | 25 | $route = $this->router->dispatch('GET', '/route/value'); 26 | 27 | $this->assertInstanceOf('Rakit\Framework\Router\Route', $route); 28 | $this->assertEquals('GET', $route->getMethod()); 29 | } 30 | 31 | public function testDispatchRoutePost() 32 | { 33 | $this->router->get('/route/:param', 'handler'); 34 | $this->router->post('/route/:param', 'handler'); 35 | $this->router->put('/route/:param', 'handler'); 36 | 37 | $route = $this->router->dispatch('POST', '/route/value'); 38 | 39 | $this->assertInstanceOf('Rakit\Framework\Router\Route', $route); 40 | $this->assertEquals('POST', $route->getMethod()); 41 | } 42 | 43 | public function testDispatchRoutePut() 44 | { 45 | $this->router->get('/route/:param', 'handler'); 46 | $this->router->put('/route/:param', 'handler'); 47 | $this->router->post('/route/:param', 'handler'); 48 | 49 | $route = $this->router->dispatch('PUT', '/route/value'); 50 | 51 | $this->assertInstanceOf('Rakit\Framework\Router\Route', $route); 52 | $this->assertEquals('PUT', $route->getMethod()); 53 | } 54 | 55 | public function testDispatchRoutePatch() 56 | { 57 | $this->router->get('/route/:param', 'handler'); 58 | $this->router->patch('/route/:param', 'handler'); 59 | $this->router->post('/route/:param', 'handler'); 60 | 61 | $route = $this->router->dispatch('PATCH', '/route/value'); 62 | 63 | $this->assertInstanceOf('Rakit\Framework\Router\Route', $route); 64 | $this->assertEquals('PATCH', $route->getMethod()); 65 | } 66 | 67 | public function testDispatchRouteDelete() 68 | { 69 | $this->router->get('/route/:param', 'handler'); 70 | $this->router->delete('/route/:param', 'handler'); 71 | $this->router->post('/route/:param', 'handler'); 72 | 73 | $route = $this->router->dispatch('DELETE', '/route/value'); 74 | 75 | $this->assertInstanceOf('Rakit\Framework\Router\Route', $route); 76 | $this->assertEquals('DELETE', $route->getMethod()); 77 | } 78 | 79 | public function testDispatchParameters() 80 | { 81 | $this->router->get('/route/:foo/:bar', 'handler'); 82 | 83 | $route = $this->router->dispatch('GET', '/route/param1/param2'); 84 | 85 | $this->assertInstanceOf('Rakit\Framework\Router\Route', $route); 86 | 87 | $params = $route->params; 88 | $this->assertEquals(2, count($params)); 89 | $this->assertEquals($params['foo'], 'param1'); 90 | $this->assertEquals($params['bar'], 'param2'); 91 | } 92 | 93 | public function testDispatchOptionalParameter() 94 | { 95 | $this->router->get('/route/:foo(/:bar(/:baz))', 'handler'); 96 | 97 | foreach(['/route/one' => 1, '/route/one/two' => 2, '/route/one/two/three' => 3] as $path => $expected_count) { 98 | $route = $this->router->dispatch('GET', $path); 99 | $this->assertInstanceOf('Rakit\Framework\Router\Route', $route); 100 | $this->assertEquals($expected_count, count($route->params)); 101 | } 102 | } 103 | 104 | public function testDispatchCustomRegexParameter() 105 | { 106 | $this->router->get('/edit/:id', 'handler')->where('id', '\d{2}'); 107 | 108 | $tests = [ 109 | '/edit/foo' => false, 110 | '/edit/12foo' => false, 111 | '/edit/foo99' => false, 112 | '/edit/1' => false, 113 | '/edit/123' => false, 114 | '/edit/11' => true, 115 | ]; 116 | 117 | foreach($tests as $path => $expected) { 118 | $route = $this->router->dispatch('GET', $path); 119 | $this->assertEquals($expected, !is_null($route)); 120 | } 121 | } 122 | 123 | public function testRouteNaming() 124 | { 125 | $this->router->get('/login', 'handler')->name('form-login'); 126 | $this->router->post('/login', 'handler')->name('post-login'); 127 | 128 | $route = $this->router->findRouteByName('post-login'); 129 | 130 | $this->assertInstanceOf('Rakit\Framework\Router\Route', $route); 131 | $this->assertEquals('POST', $route->getMethod()); 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /tests/RunAppTests.php: -------------------------------------------------------------------------------- 1 | app = new App('test', [ 15 | 'app' => [ 16 | 'debug' => false 17 | ] 18 | ]); 19 | 20 | // handle 404 21 | $this->app->on(404, function($response) { 22 | return $response->setStatus(404)->html("Not Found!"); 23 | }); 24 | } 25 | 26 | public function tearDown() { 27 | $this->app = null; 28 | } 29 | 30 | /** 31 | * @runInSeparateProcess 32 | * @preserveGlobalState enabled 33 | */ 34 | public function testHelloWorld() 35 | { 36 | $this->app->get("/", function() { 37 | return "Hello World!"; 38 | }); 39 | 40 | $this->assertResponse("GET", "/", "Hello World!", 200); 41 | } 42 | 43 | /** 44 | * @runInSeparateProcess 45 | * @preserveGlobalState enabled 46 | */ 47 | public function testNotFound() 48 | { 49 | $app = $this->app; 50 | 51 | $this->assertResponse("GET", "/unregistered-route", 'Not Found!', 404); 52 | } 53 | 54 | /** 55 | * @runInSeparateProcess 56 | * @preserveGlobalState enabled 57 | */ 58 | public function testMethodGet() 59 | { 60 | $this->app->get("/foo", function() { 61 | return "get"; 62 | }); 63 | 64 | $this->assertResponse("GET", "/foo", 'get'); 65 | } 66 | 67 | /** 68 | * @runInSeparateProcess 69 | * @preserveGlobalState enabled 70 | */ 71 | public function testMethodPost() 72 | { 73 | $this->app->post("/foo", function() { 74 | return "post"; 75 | }); 76 | 77 | $this->assertResponse("POST", "/foo", 'post'); 78 | } 79 | 80 | /** 81 | * @runInSeparateProcess 82 | * @preserveGlobalState enabled 83 | */ 84 | public function testMethodPut() 85 | { 86 | $this->app->put("/foo", function() { 87 | return "put"; 88 | }); 89 | 90 | $this->assertResponse("PUT", "/foo", 'put'); 91 | } 92 | 93 | /** 94 | * @runInSeparateProcess 95 | * @preserveGlobalState enabled 96 | */ 97 | public function testMethodPatch() 98 | { 99 | $this->app->patch("/foo", function() { 100 | return "patch"; 101 | }); 102 | 103 | $this->assertResponse("PATCH", "/foo", 'patch'); 104 | } 105 | 106 | /** 107 | * @runInSeparateProcess 108 | * @preserveGlobalState enabled 109 | */ 110 | public function testMethodDelete() 111 | { 112 | $this->app->delete("/foo", function() { 113 | return "delete"; 114 | }); 115 | 116 | $this->assertResponse("DELETE", "/foo", 'delete'); 117 | } 118 | 119 | /** 120 | * @runInSeparateProcess 121 | * @preserveGlobalState enabled 122 | */ 123 | public function testMethodHead() 124 | { 125 | $this->app->get("/foo", function() { 126 | return "head"; 127 | }); 128 | 129 | // in HEAD request, we don't send response body 130 | $this->assertResponse("HEAD", "/foo", ''); 131 | } 132 | 133 | /** 134 | * @runInSeparateProcess 135 | * @preserveGlobalState enabled 136 | */ 137 | public function testRouteParam() 138 | { 139 | $this->app->get("/hello/:name/:age", function($name, $age) { 140 | return $name."-".$age; 141 | }); 142 | 143 | $this->assertResponse("GET", "/hello/foo/12", 'foo-12'); 144 | $this->assertResponse("GET", "/hello/bar/24", 'bar-24'); 145 | $this->assertResponse("GET", "/hello/bar", 'Not Found!', 404); 146 | } 147 | 148 | /** 149 | * @runInSeparateProcess 150 | * @preserveGlobalState enabled 151 | */ 152 | public function testOptionalRouteParam() 153 | { 154 | $this->app->get("/hello/:name(/:age)", function($name, $age = 1) { 155 | return $name."-".$age; 156 | }); 157 | 158 | $this->assertResponse("GET", "/hello/bar", 'bar-1'); 159 | $this->assertResponse("GET", "/hello/foo/12", 'foo-12'); 160 | } 161 | 162 | /** 163 | * @runInSeparateProcess 164 | * @preserveGlobalState enabled 165 | */ 166 | public function testRouteParamCondition() 167 | { 168 | $this->app->get("/hello/:name/:age", function($name, $age = 1) { 169 | return $name."-".$age; 170 | })->where('age', '\d+'); 171 | 172 | $this->assertResponse("GET", "/hello/foo/12", 'foo-12'); 173 | $this->assertResponse("GET", "/hello/bar/baz", 'Not Found!', 404); 174 | } 175 | 176 | /** 177 | * @runInSeparateProcess 178 | * @preserveGlobalState enabled 179 | */ 180 | public function testMiddlewareBefore() 181 | { 182 | $this->app->setMiddleware('foobar', function($req, $res, $next) { 183 | $req->foobar = "foobar"; 184 | return $next(); 185 | }); 186 | 187 | $this->app->get("/foo", function(Request $request) { 188 | return $request->foobar; 189 | })->middleware('foobar'); 190 | 191 | $this->assertResponse("GET", "/foo", 'foobar'); 192 | } 193 | 194 | /** 195 | * @runInSeparateProcess 196 | * @preserveGlobalState enabled 197 | */ 198 | public function testMiddlewareAfter() 199 | { 200 | $this->app->setMiddleware('uppercase', function($req, $res, $next) { 201 | $next(); 202 | return strtoupper($res->body); 203 | }); 204 | 205 | $this->app->get("/foo", function(Request $request) { 206 | return "foo"; 207 | })->uppercase(); 208 | 209 | $this->assertResponse("GET", "/foo", 'FOO'); 210 | } 211 | 212 | /** 213 | * @runInSeparateProcess 214 | * @preserveGlobalState enabled 215 | */ 216 | public function testMiddlewareBeforeAndAfter() 217 | { 218 | $this->app->setMiddleware('uppercase', function($req, $res, $next) { 219 | $req->foobar = "foobar"; 220 | 221 | $next(); 222 | 223 | return strtoupper($res->body); 224 | }); 225 | 226 | $this->app->get("/foo", function(Request $request) { 227 | return $request->foobar."bazQux"; 228 | })->uppercase(); 229 | 230 | $this->assertResponse("GET", "/foo", 'FOOBARBAZQUX'); 231 | } 232 | 233 | /** 234 | * @runInSeparateProcess 235 | * @preserveGlobalState enabled 236 | */ 237 | public function testMiddlewareParam() 238 | { 239 | $this->app->setMiddleware('setStr', function($req, $res, $next, $str) { 240 | $req->str = $str; 241 | return $next(); 242 | }); 243 | 244 | $this->app->get("/foo", function(Request $request) { 245 | return $request->str; 246 | })->setStr('foobar'); 247 | 248 | $this->assertResponse("GET", "/foo", 'foobar'); 249 | } 250 | 251 | /** 252 | * @runInSeparateProcess 253 | * @preserveGlobalState enabled 254 | */ 255 | public function testMultipleMiddleware() 256 | { 257 | $this->app->setMiddleware('setStr', function($req, $res, $next, $str) { 258 | $req->str = $str; 259 | return $next(); 260 | }); 261 | 262 | $this->app->setMiddleware('uppercase', function($req, $res, $next) { 263 | $next(); 264 | return strtoupper($res->body); 265 | }); 266 | 267 | $this->app->setMiddleware('jsonify', function($req, $res, $next) { 268 | $next(); 269 | return $res->json(['body' => $res->body]); 270 | }); 271 | 272 | $this->app->get("/foo", function(Request $request) { 273 | return $request->str."bazQux"; 274 | })->setStr('foobar')->jsonify()->uppercase(); 275 | 276 | $this->assertResponse("GET", "/foo", '{"body":"FOOBARBAZQUX"}', 200, 'application/json'); 277 | } 278 | 279 | /** 280 | * @runInSeparateProcess 281 | * @preserveGlobalState enabled 282 | */ 283 | public function testIgnoringController() 284 | { 285 | $this->app->setMiddleware('no-controller', function($req, $res, $next) { 286 | return "controller ignored"; 287 | }); 288 | 289 | $this->app->get("/foo", function(Request $request) { 290 | return "foobar"; 291 | })->middleware('no-controller'); 292 | 293 | $this->assertResponse("GET", "/foo", 'controller ignored'); 294 | } 295 | 296 | /** 297 | * @runInSeparateProcess 298 | * @preserveGlobalState enabled 299 | */ 300 | public function testRouteGroup() 301 | { 302 | $this->app->group('/group', function($group) { 303 | 304 | $group->get('/hello', function() { 305 | return "IM IN GROUP"; 306 | }); 307 | 308 | }); 309 | 310 | $this->assertResponse("GET", "/group/hello", 'IM IN GROUP'); 311 | } 312 | 313 | /** 314 | * @runInSeparateProcess 315 | * @preserveGlobalState enabled 316 | */ 317 | public function testResponseJson() 318 | { 319 | $this->app->get('/anything.json', function() { 320 | return [ 321 | 'message' => 'hello' 322 | ]; 323 | }); 324 | 325 | $this->assertResponse("GET", "/anything.json", '{"message":"hello"}', 200, 'application/json'); 326 | } 327 | 328 | /** 329 | * @runInSeparateProcess 330 | * @preserveGlobalState enabled 331 | */ 332 | public function testMiddlewareKeepResponseToJson() 333 | { 334 | $this->app->setMiddleware('uppercase', function($req, $res, $next) { 335 | $next(); 336 | return strtoupper($res->body); 337 | }); 338 | 339 | $this->app->get('/anything.json', function() { 340 | return [ 341 | 'message' => 'hello' 342 | ]; 343 | })->uppercase(); 344 | 345 | $this->assertResponse("GET", "/anything.json", '{"MESSAGE":"HELLO"}', 200, 'application/json'); 346 | } 347 | 348 | /** 349 | * @runInSeparateProcess 350 | * @preserveGlobalState enabled 351 | */ 352 | public function testGlobalMiddleware() 353 | { 354 | $this->app->setMiddleware('uppercase', function($req, $res, $next) { 355 | $next(); 356 | return strtoupper($res->body); 357 | }); 358 | 359 | $this->app->useMiddleware('uppercase'); 360 | 361 | $this->app->get('/anything.json', function() { 362 | return [ 363 | 'message' => 'hello' 364 | ]; 365 | }); 366 | 367 | $this->assertResponse("GET", "/anything.json", '{"MESSAGE":"HELLO"}', 200, 'application/json'); 368 | } 369 | 370 | /** 371 | * @runInSeparateProcess 372 | * @preserveGlobalState enabled 373 | */ 374 | public function testRouteGroupParamCondition() 375 | { 376 | $this->app->group('/u/:username', function($group) { 377 | 378 | $group->get('/profile', function($username, Request $request) { 379 | return $username.' profile'; 380 | }); 381 | 382 | })->where('username', '[a-zA-Z_]+'); 383 | 384 | $this->assertResponse("GET", "/u/foobar/profile", 'foobar profile'); 385 | $this->assertResponse("GET", "/u/foobar/123", 'Not Found!', 404); 386 | } 387 | 388 | /** 389 | * @runInSeparateProcess 390 | * @preserveGlobalState enabled 391 | */ 392 | public function testRouteGroupMiddleware() 393 | { 394 | $this->app->setMiddleware('setStr', function($req, $res, $next, $str) { 395 | $req->str = $str; 396 | return $next(); 397 | }); 398 | 399 | $this->app->group('/group', function($group) { 400 | 401 | $group->get('/hello', function(Request $req) { 402 | return $req->str; 403 | }); 404 | 405 | })->setStr('foobar'); 406 | 407 | $this->assertResponse("GET", "/group/hello", 'foobar'); 408 | } 409 | 410 | /** 411 | * @runInSeparateProcess 412 | * @preserveGlobalState enabled 413 | */ 414 | public function testResponseView() 415 | { 416 | $this->app->config['view.path'] = __DIR__.'/resources/views'; 417 | 418 | $this->app->get("/hello", function(Response $response) { 419 | return $response->view('hello.php'); 420 | }); 421 | 422 | $this->assertResponse("GET", "/hello", '

    Hello World!

    '); 423 | } 424 | 425 | /** 426 | * @runInSeparateProcess 427 | * @preserveGlobalState enabled 428 | */ 429 | public function testPassDataIntoView() 430 | { 431 | $this->app->config['view.path'] = __DIR__.'/resources/views'; 432 | 433 | $this->app->get("/hello", function(Response $response) { 434 | return $response->view('hello-name.php', [ 435 | 'name' => 'John' 436 | ]); 437 | }); 438 | 439 | $this->assertResponse("GET", "/hello", '

    Hello John!

    '); 440 | } 441 | 442 | /** 443 | * @runInSeparateProcess 444 | * @preserveGlobalState enabled 445 | */ 446 | public function testAppBindedToClosure() 447 | { 448 | $this->app->something = "foo"; 449 | $this->app->foo = "bar"; 450 | $this->app->setMiddleware('test', function($req, $res, $next) { 451 | $next(); 452 | return strtoupper($this->something.$res->body); 453 | }); 454 | 455 | $this->app->get("/hello", function() { 456 | return $this->foo; 457 | })->test(); 458 | 459 | $this->assertResponse("GET", "/hello", 'FOOBAR'); 460 | } 461 | 462 | /** 463 | * @runInSeparateProcess 464 | * @preserveGlobalState enabled 465 | */ 466 | public function testException() 467 | { 468 | $this->app->get("/error", function(Response $response) { 469 | throw new \Exception; 470 | }); 471 | 472 | $this->assertResponse("GET", "/error", 'Something went wrong', 500); 473 | } 474 | 475 | /** 476 | * @runInSeparateProcess 477 | * @preserveGlobalState enabled 478 | */ 479 | public function testHandleException() 480 | { 481 | $this->app->handle('InvalidArgumentException', function($e, Response $response) { 482 | return $response->html("Invalid Argument", 501); 483 | }); 484 | 485 | $this->app->get("/error", function(Response $response) { 486 | throw new \InvalidArgumentException; 487 | }); 488 | 489 | $this->assertResponse("GET", "/error", 'Invalid Argument', 501); 490 | } 491 | 492 | protected function runAndGetResponse($method, $path) 493 | { 494 | $this->app->request->server['REQUEST_METHOD'] = $method; 495 | 496 | //buffer output, so output won't appear in terminal 497 | ob_start(); 498 | $this->app->run($method, $path); 499 | $rendered = ob_get_clean(); 500 | 501 | $response = clone $this->app->response; 502 | $this->app->response->reset(); 503 | 504 | return [$response, $rendered]; 505 | } 506 | 507 | protected function assertResponse($method, $path, $assert_body, $assert_status = 200, $assert_content_type = 'text/html') 508 | { 509 | $at = $method.' '.$path.' => '.$assert_body; 510 | list($response, $rendered) = $this->runAndGetResponse($method, $path); 511 | 512 | $this->assertEquals($rendered, $assert_body, $at); 513 | $this->assertEquals($response->getStatus(), $assert_status, $at); 514 | $this->assertEquals($response->getContentType(), $assert_content_type, $at); 515 | } 516 | 517 | } -------------------------------------------------------------------------------- /tests/resources/views/hello-name.php: -------------------------------------------------------------------------------- 1 |

    Hello !

    -------------------------------------------------------------------------------- /tests/resources/views/hello.php: -------------------------------------------------------------------------------- 1 |

    Hello World!

    --------------------------------------------------------------------------------