├── tests ├── bootstrap.php ├── ParametersTest.php ├── Components │ ├── LoggerAwareTraitTest.php │ ├── ContainerAwareTraitTest.php │ ├── ArrayResourceGetterTraitTest.php │ └── ArrayResourceTraitTest.php ├── ConfigTest.php ├── ApplicationTest.php ├── RouterTest.php └── ControllerTest.php ├── src ├── Exception │ ├── HttpErrorException.php │ ├── HttpNotFoundException.php │ ├── HttpMethodNotAllowedException.php │ └── DCException.php ├── RouteInterface.php ├── Events │ ├── BootEvent.php │ ├── DietcubeEvents.php │ ├── DietcubeEventAbstract.php │ ├── FinishRequestEvent.php │ ├── FilterResponseEvent.php │ ├── RoutingEvent.php │ └── ExecuteActionEvent.php ├── Controller │ ├── ErrorControllerInterface.php │ ├── ErrorController.php │ └── DebugController.php ├── Components │ ├── LoggerAwareTrait.php │ ├── ContainerAwareTrait.php │ ├── ArrayResourceTrait.php │ └── ArrayResourceGetterTrait.php ├── Config.php ├── Parameters.php ├── template │ ├── error │ │ ├── error500.html.twig │ │ ├── error404.html.twig │ │ └── error403.html.twig │ └── debug │ │ └── debug.html.twig ├── Twig │ └── DietcubeExtension.php ├── Controller.php ├── Router.php ├── Response.php ├── Application.php └── Dispatcher.php ├── .gitignore ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .php_cs.dist ├── .travis.yml ├── CHANGES.md ├── composer.json ├── phpunit.xml.dist ├── LICENSE └── README.md /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | app = $app; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Controller/ErrorControllerInterface.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/src') 4 | ->in(__DIR__ . '/tests') 5 | ; 6 | 7 | return PhpCsFixer\Config::create() 8 | ->setRules([ 9 | '@PSR2' => true, 10 | 'array_syntax' => ['syntax' => 'short'], 11 | ]) 12 | ->setFinder($finder) 13 | ; 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | php: 6 | - 5.6 7 | - 7.0 8 | - 7.1 9 | - 7.2 10 | - hhvm 11 | 12 | before_script: 13 | - composer install 14 | 15 | script: 16 | - ./vendor/bin/php-cs-fixer fix --verbose --diff --dry-run 17 | - composer test 18 | 19 | matrix: 20 | allow_failures: 21 | - php: hhvm 22 | -------------------------------------------------------------------------------- /src/Components/LoggerAwareTrait.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Events/DietcubeEvents.php: -------------------------------------------------------------------------------- 1 | _array_resource = $config; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Parameters.php: -------------------------------------------------------------------------------- 1 | _array_resource = $params; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Events/DietcubeEventAbstract.php: -------------------------------------------------------------------------------- 1 | app; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | CHANGES 2 | =========== 3 | 4 | 1.0.2 5 | ----------- 6 | 7 | * Create logger in Application (not in Dispatcher) 8 | * Create renderer object when it is needed. 9 | * Includes fixes a little bugs and refactors. 10 | 11 | 1.0.1 12 | ----------- 13 | 14 | * Support HTTP 451 status (Thanks @zonuexe #2). 15 | * Fix: Controller::redirect() method sent wrong headers (Thanks @b-kaxa #5). 16 | * Fix: Response class is now LoggerAware (#6). 17 | * Includes fixes a little bugs and refactors. 18 | -------------------------------------------------------------------------------- /src/Components/ContainerAwareTrait.php: -------------------------------------------------------------------------------- 1 | container = $container; 23 | 24 | return $this; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/ParametersTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($_SERVER['PATH'], $params->get('PATH')); 16 | $this->assertEquals($_SERVER, $params->getData()); 17 | 18 | $this->assertEquals(null, $params->get('THE_KEY_NOT_EXISTS')); 19 | $this->assertEquals('default-value', $params->get('THE_KEY_NOT_EXISTS', 'default-value')); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dietcube/dietcube", 3 | "description": "dietcube", 4 | "license": "MIT", 5 | "require": { 6 | "nikic/fast-route": "0.7.x", 7 | "twig/twig": "1.23.x", 8 | "pimple/pimple": "3.0.x", 9 | "psr/log": "1.0.0", 10 | "monolog/monolog": "1.17.x", 11 | "symfony/event-dispatcher": "3.0.x" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "^5.7 || ^6.4 || ^7.4", 15 | "friendsofphp/php-cs-fixer": "^2.7 !=2.16.1" 16 | }, 17 | "scripts": { 18 | "test": "phpunit", 19 | "cs-fix": "php-cs-fixer fix" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Dietcube\\": "src/" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Controller/ErrorController.php: -------------------------------------------------------------------------------- 1 | getResponse()->setStatusCode(404); 15 | return $this->render('error404'); 16 | } 17 | 18 | public function methodNotAllowed() 19 | { 20 | $this->getResponse()->setStatusCode(403); 21 | return $this->render('error403'); 22 | } 23 | 24 | public function internalError(\Exception $error) 25 | { 26 | $this->getResponse()->setStatusCode(500); 27 | return $this->render('error500', ['error' => $error]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/template/error/error500.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |
23 |

Error

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Components/ArrayResourceTrait.php: -------------------------------------------------------------------------------- 1 | _array_resource; 20 | foreach ($key_parts as $key) { 21 | if (!is_array($ref_value)) { 22 | $ref_value = []; 23 | } 24 | if (!array_key_exists($key, $ref_value)) { 25 | $ref_value[$key] = []; 26 | } 27 | $ref_value = &$ref_value[$key]; 28 | } 29 | $ref_value = $value; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/template/error/error404.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |
23 |

404 Not Found

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/template/error/error403.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |
23 |

403 Method Not Allowed

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/Components/LoggerAwareTraitTest.php: -------------------------------------------------------------------------------- 1 | assertNull($obj->getLogger()); 21 | 22 | $obj->setLogger($logger); 23 | $this->assertInstanceOf('\\Psr\\Log\\LoggerInterface', $obj->getLogger()); 24 | } 25 | } 26 | 27 | class ConcreteComponentWithLogger 28 | { 29 | use LoggerAwareTrait; 30 | 31 | public function getLogger() 32 | { 33 | return $this->logger; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Components/ContainerAwareTraitTest.php: -------------------------------------------------------------------------------- 1 | assertNull($obj->getContainer()); 21 | 22 | $obj->setContainer($container); 23 | $this->assertInstanceOf('\\Pimple\Container', $obj->getContainer()); 24 | } 25 | } 26 | 27 | class ConcreteComponentWithContainer 28 | { 29 | use ContainerAwareTrait; 30 | 31 | public function getContainer() 32 | { 33 | return $this->container; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/FinishRequestEvent.php: -------------------------------------------------------------------------------- 1 | app = $app; 21 | $this->response = $response; 22 | } 23 | 24 | /** 25 | * @return Response 26 | */ 27 | public function getResponse() 28 | { 29 | return $this->response; 30 | } 31 | 32 | /** 33 | * @param Response $response 34 | */ 35 | public function setResponse(Response $response) 36 | { 37 | $this->response = $response; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Events/FilterResponseEvent.php: -------------------------------------------------------------------------------- 1 | app = $app; 21 | $this->response = $response; 22 | } 23 | 24 | /** 25 | * @return Response 26 | */ 27 | public function getResponse() 28 | { 29 | return $this->response; 30 | } 31 | 32 | /** 33 | * @param Response $response 34 | */ 35 | public function setResponse(Response $response) 36 | { 37 | $this->response = $response; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | ./tests 7 | 8 | 9 | 10 | 13 | ./src 14 | 15 | ./tests 16 | ./src/template 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Components/ArrayResourceGetterTrait.php: -------------------------------------------------------------------------------- 1 | getResourceData(); 16 | } 17 | 18 | $key_parts = explode('.', $key); 19 | $value = $this->_array_resource; 20 | foreach ($key_parts as $key) { 21 | if (!is_array($value)) { 22 | return $default; 23 | } elseif (!array_key_exists($key, $value)) { 24 | return $default; 25 | } 26 | $value = $value[$key]; 27 | } 28 | return $value; 29 | } 30 | 31 | public function getResourceData() 32 | { 33 | return $this->_array_resource; 34 | } 35 | 36 | public function clearResource() 37 | { 38 | $this->_array_resource = []; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | 'cake', 16 | 'database' => [ 17 | 'host' => 'localhost', 18 | 'port' => 3306, 19 | ], 20 | ]; 21 | $config = new Config($config_array); 22 | 23 | $this->assertEquals('cake', $config->get('diet')); 24 | $this->assertEquals(12345, $config->get('cake', 12345)); // default 25 | 26 | // array 27 | $this->assertEquals([ 28 | 'host' => 'localhost', 29 | 'port' => 3306, 30 | ], $config->get('database')); 31 | $this->assertEquals('localhost', $config->get('database.host')); 32 | $this->assertEquals(3306, $config->get('database.port')); 33 | $this->assertEquals($config_array, $config->get()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Mercari, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/Events/RoutingEvent.php: -------------------------------------------------------------------------------- 1 | app = $app; 21 | $this->router = $router; 22 | } 23 | 24 | public function getRouter() 25 | { 26 | return $this->router; 27 | } 28 | 29 | public function setRouter(Router $router) 30 | { 31 | $this->router = $router; 32 | } 33 | 34 | public function setHandler($handler) 35 | { 36 | $this->handler = $handler; 37 | 38 | return $this; 39 | } 40 | 41 | public function getHandler($handler) 42 | { 43 | return $this->handler; 44 | } 45 | 46 | public function setVars(array $vars) 47 | { 48 | $this->vars = $vars; 49 | } 50 | 51 | public function getVars(array $vars) 52 | { 53 | return $this->vars; 54 | } 55 | 56 | public function setRouteInfo($handler, array $vars = []) 57 | { 58 | $this->handler = $handler; 59 | $this->vars = $vars; 60 | } 61 | 62 | public function getRouteInfo() 63 | { 64 | return [$this->handler, $this->vars]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [ARCHIVED] Dietcube 2 | ---------- 3 | # Archived 4 | 5 | This repository is no longer maintained and now is read-only. 6 | Please consider migrating to other modern frameworks. 7 | 8 | Thank you all for your contributions. 9 | 10 | ========= 11 | 12 | Dietcube is the world super fly weight & flexible PHP framework. 13 | 14 | [![Latest Stable Version](https://poser.pugx.org/dietcube/dietcube/v/stable)](https://packagist.org/packages/dietcube/dietcube) 15 | [![Total Downloads](https://poser.pugx.org/dietcube/dietcube/downloads)](https://packagist.org/packages/dietcube/dietcube) 16 | [![License](https://poser.pugx.org/dietcube/dietcube/license)](https://packagist.org/packages/dietcube/dietcube) 17 | 18 | Dietcube has: 19 | 20 | * MVC Architecture, 21 | * DI Container (Pimple), 22 | * Router (FastRoute), 23 | * EventDispatcher (Symfony EventDispatcher), 24 | * Renderer (Twig), 25 | * Logger (Monolog), 26 | * and some core components. 27 | 28 | 29 | Install 30 | --------- 31 | 32 | Use Dietcube on your project. 33 | 34 | ``` 35 | composer require dietcube/dietcube 36 | ``` 37 | 38 | Start project with the skeleton: 39 | 40 | ``` 41 | composer create-project dietcube/project -s dev your-project 42 | ``` 43 | 44 | Contribution 45 | --------- 46 | 47 | Please read the CLA below carefully before submitting your contribution. 48 | 49 | https://www.mercari.com/cla/ 50 | 51 | 52 | License 53 | --------- 54 | 55 | See [LICENSE](LICENSE) file. 56 | 57 | Authors 58 | --------- 59 | 60 | * @sotarok 61 | * @YuiSakamoto 62 | * @kajiken 63 | * @DQNEO 64 | -------------------------------------------------------------------------------- /src/Twig/DietcubeExtension.php: -------------------------------------------------------------------------------- 1 | container['router']; 40 | return $router->url($handler, $data, $query_params, $is_absolute); 41 | } 42 | 43 | /** 44 | * This method is the shortcut for Router::url() with true of is_absolute flag. 45 | * 46 | * @param string $handler 47 | * @param array $data 48 | * @param array $query_params 49 | * @return string url 50 | */ 51 | public function absoluteUrl($handler, array $data = [], array $query_params = []) 52 | { 53 | return $this->url($handler, $data, $query_params, true); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controller/DebugController.php: -------------------------------------------------------------------------------- 1 | get('app'); 15 | $app_root = $app->getAppRoot(); 16 | $vendor_dir = $app->getVendorDir(); 17 | $router = $this->get('router'); 18 | 19 | return $this->render('@debug/debug', [ 20 | 'config' => $app->getConfig()->getData(), 21 | 'router' => [ 22 | 'dispatched_url' => $router->getDispatchedUrl(), 23 | 'dispatched_method' => $router->getDispatchedMethod(), 24 | 'route_info' => $router->getRouteInfo(), 25 | ], 26 | 'dirs' => [ 27 | 'app_root' => $app_root, 28 | 'vendor_dir' => $vendor_dir, 29 | 'config_dir' => $app->getConfigDir(), 30 | 'webroot_dir' => $app->getWebrootDir(), 31 | 'resource_dir' => $app->getResourceDir(), 32 | 'template_dir' => $app->getTemplateDir(), 33 | 'tmp_dir' => $app->getTmpDir(), 34 | ], 35 | 'error_class_name' => get_class($errors), 36 | 'errors' => $errors, 37 | 'error_trace' => preg_replace( 38 | ['!' . $app_root . '!', '!' . $vendor_dir . '!', ], 39 | ['#root ', '#vendor ', ], 40 | $errors->getTraceAsString() 41 | ), 42 | 'get_params' => $this->get('global.get')->get(), 43 | 'post_params' => $this->get('global.post')->get(), 44 | 'cookie_params' => $this->get('global.cookie')->get(), 45 | 'server_params' => $this->get('global.server')->get(), 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(__DIR__, $app->getAppRoot()); 27 | $this->assertEquals('development', $app->getEnv()); 28 | $this->assertEquals( 29 | [ 30 | 'config.php', 31 | 'config_development.php', 32 | ], 33 | $app->getConfigFiles() 34 | ); 35 | $this->assertEquals('Dietcube', $app->getAppNamespace()); 36 | $this->assertEquals(false, $app->isDebug()); 37 | 38 | $app->initHttpRequest($container); 39 | $this->assertEquals('www.dietcube.org', $app->getHost()); 40 | $this->assertEquals('80', $app->getPort()); 41 | $this->assertEquals('/documentation/setup', $app->getPath()); 42 | $this->assertEquals('http', $app->getProtocol()); 43 | $this->assertEquals('http://www.dietcube.org', $app->getUrl()); 44 | 45 | $this->assertEquals(dirname(__DIR__) . '/webroot', $app->getWebrootDir()); 46 | $this->assertEquals(__DIR__ . '/resource', $app->getResourceDir()); 47 | $this->assertEquals(__DIR__ . '/template', $app->getTemplateDir()); 48 | $this->assertEquals('.html.twig', $app->getTemplateExt()); 49 | $this->assertEquals(__DIR__ . '/config', $app->getConfigDir()); 50 | $this->assertEquals(dirname(__DIR__) . '/tmp', $app->getTmpDir()); 51 | } 52 | } 53 | 54 | class MockApplication extends Application 55 | { 56 | public function config(Container $container) 57 | { 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Components/ArrayResourceGetterTraitTest.php: -------------------------------------------------------------------------------- 1 | 1, 20 | 'config_bool' => true, 21 | 'config_string' => 'data', 22 | 'config_array' => [1, 2, 3], 23 | 'db' => [ 24 | 'dsn' => 'mysql:123', 25 | 'user' => 'aoi_miyazaki', 26 | ], 27 | ]; 28 | $obj = new ConcreteResource($data); 29 | 30 | $this->assertEquals(1, $obj->getResource('config1')); 31 | $this->assertTrue($obj->getResource('config_bool')); 32 | $this->assertSame([1, 2, 3], $obj->getResource('config_array')); 33 | $this->assertNull($obj->getResource('config_string1'), 'non exists key'); 34 | 35 | $this->assertSame(2, $obj->getResource('config2', 2), 'non-exiss key and default'); 36 | 37 | $this->assertSame($data, $obj->getResource(), 'get all'); 38 | $this->assertSame($data, $obj->getResourceData(), 'get all'); 39 | 40 | $this->assertEquals('aoi_miyazaki', $obj->getResource('db.user')); 41 | $this->assertSame([ 42 | 'dsn' => 'mysql:123', 43 | 'user' => 'aoi_miyazaki', 44 | ], $obj->getResource('db')); 45 | $this->assertEquals('aoi-no-password', $obj->getResource('db.password', 'aoi-no-password'), 'default'); 46 | 47 | $this->assertEquals(null, $obj->getResource('db.user.name')); 48 | 49 | // clear 50 | $obj->clearResource(); 51 | $this->assertSame([], $obj->getResourceData()); 52 | $this->assertEquals(null, $obj->getResource('config1')); 53 | } 54 | } 55 | 56 | class ConcreteResource 57 | { 58 | use ArrayResourceGetterTrait; 59 | 60 | public function __construct(array $array = []) 61 | { 62 | $this->_array_resource = $array; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Events/ExecuteActionEvent.php: -------------------------------------------------------------------------------- 1 | app = $app; 21 | $this->executable = $executable; 22 | $this->vars = $vars; 23 | } 24 | 25 | /** 26 | * @return Application 27 | */ 28 | public function getApplication() 29 | { 30 | return $this->app; 31 | } 32 | 33 | /** 34 | * @param callable $executable 35 | */ 36 | public function setExecutable($executable) 37 | { 38 | if (!is_callable($executable)) { 39 | throw new \InvalidArgumentException("Passed argument for setExecutable is not callable."); 40 | } 41 | $this->executable = $executable; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Set executable by valid handler. 48 | * This is a shortcut method to create controller by shorter name. 49 | * e.g. User::login 50 | * 51 | * @param string $handler 52 | */ 53 | public function setExecutableByHandler($handler) 54 | { 55 | list($controller_name, $action_name) = $this->app->getControllerByHandler($handler); 56 | $controller = $this->app->createController($controller_name); 57 | 58 | $this->setExecutable([$controller, $action_name]); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @return callable executable 65 | */ 66 | public function getExecutable() 67 | { 68 | return $this->executable; 69 | } 70 | 71 | /** 72 | * @param array $vars 73 | */ 74 | public function setVars(array $vars) 75 | { 76 | $this->vars = $vars; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | public function getVars() 85 | { 86 | return $this->vars; 87 | } 88 | 89 | /** 90 | * @param string $result 91 | */ 92 | public function setResult($result) 93 | { 94 | $this->result = $result; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @return string 101 | */ 102 | public function getResult() 103 | { 104 | return $this->result; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Components/ArrayResourceTraitTest.php: -------------------------------------------------------------------------------- 1 | 1, 20 | 'db' => [ 21 | 'dsn' => 'mysql:123', 22 | 'user' => 'aoi_miyazaki', 23 | ], 24 | ]; 25 | $obj = new ConcreteResource2($data); 26 | 27 | $this->assertEquals(1, $obj->getResource('config1')); 28 | 29 | $this->assertSame($data, $obj->getResource(), 'get all'); 30 | $this->assertSame($data, $obj->getResourceData(), 'get all'); 31 | 32 | $this->assertEquals('aoi_miyazaki', $obj->getResource('db.user')); 33 | $this->assertSame([ 34 | 'dsn' => 'mysql:123', 35 | 'user' => 'aoi_miyazaki', 36 | ], $obj->getResource('db')); 37 | $this->assertEquals('aoi-no-password', $obj->getResource('db.password', 'aoi-no-password'), 'default'); 38 | 39 | $this->assertEquals(null, $obj->getResource('db.user.name')); 40 | 41 | // set 42 | $obj->setResource('config1', 2); 43 | $this->assertEquals(2, $obj->getResource('config1')); 44 | 45 | $obj->setResource('db.user', 'aya_ueto'); 46 | $this->assertEquals('aya_ueto', $obj->getResource('db.user')); 47 | 48 | // new value 49 | $obj->setResource('hoge.fuga.piyo', 'hogera'); 50 | $this->assertEquals('hogera', $obj->getResource('hoge.fuga.piyo')); 51 | $this->assertSame(['piyo' => 'hogera'], $obj->getResource('hoge.fuga')); 52 | $this->assertSame(['fuga' => ['piyo' => 'hogera']], $obj->getResource('hoge')); 53 | 54 | // non array new value 55 | $obj->setResource('non_array.value', 100); 56 | $this->assertEquals(100, $obj->getResource('non_array.value')); 57 | 58 | // clear 59 | $obj->clearResource(); 60 | $this->assertSame([], $obj->getResourceData()); 61 | $this->assertEquals(null, $obj->getResource('config1')); 62 | } 63 | 64 | public function testSafetyForInvalidUsecase() 65 | { 66 | // the object has non array $_array_resource as default 67 | $obj = new ConcreteResource3(); 68 | 69 | $obj->setResource('non_array.value', 100); 70 | $this->assertEquals(100, $obj->getResource('non_array.value')); 71 | } 72 | } 73 | 74 | class ConcreteResource2 75 | { 76 | use ArrayResourceTrait; 77 | 78 | public function __construct(array $array = []) 79 | { 80 | $this->_array_resource = $array; 81 | } 82 | } 83 | 84 | class ConcreteResource3 85 | { 86 | use ArrayResourceTrait; 87 | 88 | public function __construct() 89 | { 90 | $this->_array_resource = null; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Controller.php: -------------------------------------------------------------------------------- 1 | container = $container; 20 | } 21 | 22 | /** 23 | * @return $this 24 | */ 25 | public function setVars($key, $value = null) 26 | { 27 | if (is_array($key)) { 28 | $this->view_vars = array_merge($this->view_vars, $key); 29 | } else { 30 | $this->view_vars[$key] = $value; 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | protected function isPost() 37 | { 38 | if (stripos($this->container['global.server']->get('REQUEST_METHOD'), 'post') === 0) { 39 | return true; 40 | } 41 | 42 | return false; 43 | } 44 | 45 | protected function get($name) 46 | { 47 | return $this->container[$name]; 48 | } 49 | 50 | protected function query($name, $default = null) 51 | { 52 | return $this->container['global.get']->get($name, $default); 53 | } 54 | 55 | protected function body($name = null, $default = null) 56 | { 57 | return $this->container['global.post']->get($name, $default); 58 | } 59 | 60 | protected function generateUrl($handler, array $data = [], array $query_params = [], $is_absolute = false) 61 | { 62 | return $this->container['router']->url($handler, $data, $query_params, $is_absolute); 63 | } 64 | 65 | protected function findTemplate($name) 66 | { 67 | return $name . $this->get('app')->getTemplateExt(); 68 | } 69 | 70 | protected function render($name, array $vars = []) 71 | { 72 | $template = $this->findTemplate($name); 73 | 74 | return $this->get('app.renderer')->render($template, array_merge($this->view_vars, $vars)); 75 | } 76 | 77 | protected function redirect($uri, $code = 302) 78 | { 79 | $response = $this->getResponse(); 80 | 81 | $response->setStatusCode($code); 82 | $response->setHeader('Location', $uri); 83 | 84 | return null; 85 | } 86 | 87 | protected function getResponse() 88 | { 89 | return $this->get('response'); 90 | } 91 | 92 | /** 93 | * Helper method to respond JSON. 94 | * 95 | * @param array $vars 96 | * @param string|null $charset 97 | * @return string JSON encoded string 98 | */ 99 | protected function json($vars, $charset = 'utf-8') 100 | { 101 | $this->getResponse()->setHeader('Content-Type', 'application/json;charset=' . $charset); 102 | 103 | return json_encode($vars); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/RouterTest.php: -------------------------------------------------------------------------------- 1 | dispatch('GET', '/about'); 15 | $this->assertSame([\FastRoute\Dispatcher::FOUND, 'Page::about', []], $route_info); 16 | 17 | $route_info = $router->dispatch('GET', '/privacy'); 18 | $this->assertSame([\FastRoute\Dispatcher::FOUND, 'Page::privacy', []], $route_info); 19 | } 20 | 21 | public function testDispatchWithData() 22 | { 23 | $router = static::createRouter(); 24 | 25 | $route_info = $router->dispatch('GET', '/user/12345'); 26 | $this->assertSame([\FastRoute\Dispatcher::FOUND, 'User::detail', ['id' => '12345']], $route_info); 27 | 28 | $this->assertSame($route_info, $router->getRouteInfo()); 29 | $this->assertEquals('GET', $router->getDispatchedMethod()); 30 | $this->assertEquals('/user/12345', $router->getDispatchedUrl()); 31 | } 32 | 33 | public function testDispatchPageNotFound() 34 | { 35 | $router = static::createRouter(); 36 | 37 | $route_info = $router->dispatch('GET', '/unknown'); 38 | $this->assertSame([\FastRoute\Dispatcher::NOT_FOUND], $route_info); 39 | } 40 | 41 | /** 42 | * @expectedException \RuntimeException 43 | */ 44 | public function testDispatchIsNotInitialized() 45 | { 46 | $router = static::createRouterWithoutInit(); 47 | 48 | $router->dispatch('GET', '/about'); 49 | } 50 | 51 | /** 52 | * @expectedException \RuntimeException 53 | */ 54 | public function testNotExistHandler() 55 | { 56 | $router = static::createRouter(); 57 | 58 | $router->url('Page::notExistsHandler'); 59 | } 60 | 61 | /** 62 | * @expectedException \InvalidArgumentException 63 | */ 64 | public function testNotExistSegumentName() 65 | { 66 | $router = static::createRouter(); 67 | 68 | $router->url('User::detail', ['invalid_key_name' => 12345]); 69 | } 70 | 71 | public function testGenerateUrl() 72 | { 73 | $router = static::createRouter(); 74 | 75 | $this->assertSame('/about', $router->url('Page::about')); 76 | $this->assertSame('/privacy', $router->url('Page::privacy')); 77 | } 78 | 79 | public function testGenerateUrlWithData() 80 | { 81 | $router = static::createRouter(); 82 | 83 | $this->assertSame('/user/12345', $router->url('User::detail', ['id' => 12345])); 84 | } 85 | 86 | public function testGenerateUrlWithDataAndQueryParams() 87 | { 88 | $router = static::createRouter(); 89 | 90 | $this->assertSame('/user/12345?from=top', $router->url('User::detail', ['id' => 12345], ['from' => 'top'])); 91 | } 92 | 93 | public static function createRouter() 94 | { 95 | $router = static::createRouterWithoutInit(); 96 | $router->init(); 97 | return $router; 98 | } 99 | 100 | public static function createRouterWithoutInit() 101 | { 102 | $router = new Router(new Container); 103 | $router->addRoute(new RouteFixture); 104 | return $router; 105 | } 106 | } 107 | 108 | 109 | class RouteFixture implements RouteInterface 110 | { 111 | public function definition(Container $container) 112 | { 113 | return [ 114 | ['GET', '/about', 'Page::about'], 115 | ['GET', '/privacy', 'Page::privacy'], 116 | ['GET', '/user/{id}', 'User::detail'], 117 | ]; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/template/debug/debug.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | Error: {{ errors.getMessage }} 21 | 22 | 23 | 24 | 39 | 40 |
41 |
42 |
43 |
44 |

{{ error_class_name }}

45 |

46 | {{ errors.getMessage }} 47 |

48 |
49 | 50 |

Stack Trace

51 |
{{ error_trace }}
52 | 53 |

Routes

54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Controller::Action{{ router.route_info[1] }}
Params{{ dump(router.route_info[2]) }}
Method{{ router.dispatched_method }}
Url{{ router.dispatched_url }}
75 | 76 |

Application

77 |

Directories

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
App Root (#root){{ dirs.app_root }}
Config{{ dirs.config_dir }}
Template{{ dirs.template_dir }}
Resource{{ dirs.resource_dir }}
Tmp{{ dirs.tmp_dir }}
Vendor (#vendor){{ dirs.vendor_dir }}
105 | 106 |

Config

107 |
{{ dump(config) }}
108 | 109 | 110 |
111 |
112 | 113 |

Server Information

114 |

GET

115 |
{{ dump(get_params) }}
116 |

POST

117 |
{{ dump(post_params) }}
118 |

COOKIE

119 |
{{ dump(cookie_params) }}
120 |

SERVER

121 |
{{ dump(server_params) }}
122 | 123 |
124 |
125 |
126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /tests/ControllerTest.php: -------------------------------------------------------------------------------- 1 | $hoge]); 20 | $controller = new Controller($container); 21 | 22 | $method = $this->getInvokableMethod('get'); 23 | $this->assertSame($hoge, $method->invokeArgs($controller, ['hoge'])); 24 | } 25 | 26 | public function testIsPostOnPost() 27 | { 28 | $_SERVER['REQUEST_METHOD'] = 'post'; 29 | $container = self::getContainerAsFixture(['global.server' => new Parameters($_SERVER)]); 30 | $controller = new Controller($container); 31 | 32 | $method = $this->getInvokableMethod('isPost'); 33 | $this->assertTrue($method->invoke($controller)); 34 | } 35 | 36 | public function testIsPostOnGet() 37 | { 38 | $_SERVER['REQUEST_METHOD'] = 'get'; 39 | $container = self::getContainerAsFixture(['global.server' => new Parameters($_SERVER)]); 40 | $controller = new Controller($container); 41 | 42 | $method = $this->getInvokableMethod('isPost'); 43 | $this->assertFalse($method->invoke($controller)); 44 | } 45 | 46 | public static function getInvokableMethod($method) 47 | { 48 | $class = new \ReflectionClass('\\Dietcube\\Controller'); 49 | $method = $class->getMethod($method); 50 | $method->setAccessible(true); 51 | return $method; 52 | } 53 | 54 | public static function getContainerAsFixture(array $fixture = []) 55 | { 56 | $container = new Container(); 57 | 58 | foreach ($fixture as $key => $value) { 59 | $container[$key] = $value; 60 | } 61 | return $container; 62 | } 63 | 64 | public function testSetVars() 65 | { 66 | $app = $this->getMockBuilder('\Dietcube\Application')->disableOriginalConstructor()->getMockForAbstractClass(); 67 | $renderer = $this->createMock('Twig_Environment'); 68 | $renderer->expects($this->any())->method('render')->will($this->returnArgument(1)); 69 | 70 | $container = self::getContainerAsFixture(['app' => $app, 'app.renderer' => $renderer]); 71 | $controller = new Controller($container); 72 | 73 | $controller->setVars('foo', 'bar'); 74 | $render = $this->getInvokableMethod('render'); 75 | 76 | $this->assertEquals(['foo' => 'bar'], $render->invokeArgs($controller, ['template'])); 77 | 78 | $controller->setVars(['foo' => 'baz']); 79 | $this->assertEquals(['foo' => 'baz'], $render->invokeArgs($controller, ['template'])); 80 | } 81 | 82 | public function testRenderVars() 83 | { 84 | $app = $this->getMockBuilder('\Dietcube\Application')->disableOriginalConstructor()->getMockForAbstractClass(); 85 | 86 | $renderer = $this->createMock('Twig_Environment'); 87 | $renderer->expects($this->any())->method('render')->will($this->returnArgument(1)); 88 | 89 | $container = self::getContainerAsFixture(['app' => $app, 'app.renderer' => $renderer]); 90 | $controller = new Controller($container); 91 | 92 | $controller->setVars('key', 'value'); 93 | $render = $this->getInvokableMethod('render'); 94 | 95 | $this->assertEquals(['key' => 'value'], $render->invokeArgs($controller, ['template'])); 96 | $this->assertEquals(['key' => 'value2'], $render->invokeArgs($controller, ['template', ['key' => 'value2']])); 97 | } 98 | 99 | public function testFindTemplate() 100 | { 101 | $app = $this->getMockBuilder('Dietcube\Application')->disableOriginalConstructor()->getMock(); 102 | $app->expects($this->atLeastOnce())->method('getTemplateExt')->will($this->returnValue('.html.jinja2')); 103 | 104 | $container = self::getContainerAsFixture(['app' => $app]); 105 | $controller = new Controller($container); 106 | $findTemplate = $this->getInvokableMethod('findTemplate'); 107 | 108 | $this->assertEquals('template.html.jinja2', $findTemplate->invokeArgs($controller, ['template'])); 109 | $this->assertEquals('index.html.jinja2', $findTemplate->invokeArgs($controller, ['index'])); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Router.php: -------------------------------------------------------------------------------- 1 | container = $container; 36 | } 37 | 38 | /** 39 | * @return $this 40 | */ 41 | public function addRoute(RouteInterface $route) 42 | { 43 | $this->routes[] = $route; 44 | return $this; 45 | } 46 | 47 | public function init() 48 | { 49 | $collector = new RouteCollector( 50 | new StdRouteParser(), 51 | new GroupCountBasedDataGenerator() 52 | ); 53 | 54 | foreach ($this->routes as $route) { 55 | foreach ($route->definition($this->container) as list($method, $route_name, $handler_name)) { 56 | $collector->addRoute($method, $route_name, $handler_name); 57 | } 58 | } 59 | 60 | $this->dispatcher = new GroupCountBasedDispatcher($collector->getData()); 61 | } 62 | 63 | /** 64 | * URL からディスパッチ対象を取得する 65 | * 66 | * @param string $http_method 67 | * @param string $url 68 | * @return array 69 | */ 70 | public function dispatch($http_method, $url) 71 | { 72 | if ($this->dispatcher === null) { 73 | throw new \RuntimeException('Route dispatcher is not initialized'); 74 | } 75 | 76 | $this->dispatched_http_method = $http_method; 77 | $this->dispatched_url = $url; 78 | $this->route_info = $this->dispatcher->dispatch($http_method, $url); 79 | 80 | return $this->route_info; 81 | } 82 | 83 | /** 84 | * Generate URL from route name (handler name). 85 | * This methods is inspired by Slim3's Router. 86 | * @see https://github.com/slimphp/Slim/blob/3494b3625ec51c2de90d9d893767d97f876e49ff/Slim/Router.php#L162 87 | * 88 | * @param string $handler Route handler name 89 | * @param array $data Route URI segments replacement data 90 | * @param array $query_params Optional query string parameters 91 | * @param bool $is_absolute Whether generate absolute url or not 92 | * @return string 93 | * @throws \RuntimeException If named route does not exist 94 | * @throws \InvalidArgumentException If required data not provided 95 | */ 96 | public function url($handler, array $data = [], array $query_params = [], $is_absolute = false) 97 | { 98 | if ($this->named_routes === null) { 99 | $this->buildNameIndex(); 100 | } 101 | 102 | if (!isset($this->named_routes[$handler])) { 103 | throw new \RuntimeException('Named route does not exist for name: ' . $handler); 104 | } 105 | 106 | $route = $this->named_routes[$handler]; 107 | $url = preg_replace_callback('/{([^}]+)}/', function ($match) use ($data) { 108 | $segment_name = explode(':', $match[1])[0]; 109 | if (!isset($data[$segment_name])) { 110 | throw new \InvalidArgumentException('Missing data for URL segment: ' . $segment_name); 111 | } 112 | return $data[$segment_name]; 113 | }, $route); 114 | 115 | if ($query_params) { 116 | $url .= '?' . http_build_query($query_params); 117 | } 118 | 119 | if ($is_absolute) { 120 | $url = $this->container['app']->getUrl() . $url; 121 | } 122 | 123 | return $url; 124 | } 125 | 126 | /** 127 | * @return $this 128 | */ 129 | public function getRouteInfo() 130 | { 131 | return $this->route_info; 132 | } 133 | 134 | public function getDispatchedMethod() 135 | { 136 | return $this->dispatched_http_method; 137 | } 138 | 139 | public function getDispatchedUrl() 140 | { 141 | return $this->dispatched_url; 142 | } 143 | 144 | protected function buildNameIndex() 145 | { 146 | $this->named_routes = []; 147 | foreach ($this->routes as $route) { 148 | foreach ($route->definition($this->container) as list($method, $route_name, $handler_name)) { 149 | if ($handler_name) { 150 | $this->named_routes[$handler_name] = $route_name; 151 | } 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 'Continue', 19 | 101 => 'Switching Protocols', 20 | 102 => 'Processing', 21 | 200 => 'OK', 22 | 201 => 'Created', 23 | 202 => 'Accepted', 24 | 203 => 'Non-Authoritative Information', 25 | 204 => 'No Content', 26 | 205 => 'Reset Content', 27 | 206 => 'Partial Content', 28 | 207 => 'Multi-status', 29 | 208 => 'Already Reported', 30 | 300 => 'Multiple Choices', 31 | 301 => 'Moved Permanently', 32 | 302 => 'Found', 33 | 303 => 'See Other', 34 | 304 => 'Not Modified', 35 | 305 => 'Use Proxy', 36 | 306 => 'Switch Proxy', 37 | 307 => 'Temporary Redirect', 38 | 400 => 'Bad Request', 39 | 401 => 'Unauthorized', 40 | 402 => 'Payment Required', 41 | 403 => 'Forbidden', 42 | 404 => 'Not Found', 43 | 405 => 'Method Not Allowed', 44 | 406 => 'Not Acceptable', 45 | 407 => 'Proxy Authentication Required', 46 | 408 => 'Request Time-out', 47 | 409 => 'Conflict', 48 | 410 => 'Gone', 49 | 411 => 'Length Required', 50 | 412 => 'Precondition Failed', 51 | 413 => 'Request Entity Too Large', 52 | 414 => 'Request-URI Too Large', 53 | 415 => 'Unsupported Media Type', 54 | 416 => 'Requested range not satisfiable', 55 | 417 => 'Expectation Failed', 56 | 418 => 'I\'m a teapot', 57 | 422 => 'Unprocessable Entity', 58 | 423 => 'Locked', 59 | 424 => 'Failed Dependency', 60 | 425 => 'Unordered Collection', 61 | 426 => 'Upgrade Required', 62 | 428 => 'Precondition Required', 63 | 429 => 'Too Many Requests', 64 | 431 => 'Request Header Fields Too Large', 65 | 451 => 'Unavailable For Legal Reasons', 66 | 500 => 'Internal Server Error', 67 | 501 => 'Not Implemented', 68 | 502 => 'Bad Gateway', 69 | 503 => 'Service Unavailable', 70 | 504 => 'Gateway Time-out', 71 | 505 => 'HTTP Version not supported', 72 | 506 => 'Variant Also Negotiates', 73 | 507 => 'Insufficient Storage', 74 | 508 => 'Loop Detected', 75 | 511 => 'Network Authentication Required', 76 | ]; 77 | 78 | /** @var null|string */ 79 | protected $reason_phrase = ''; 80 | 81 | /** @var int */ 82 | protected $status_code = 200; 83 | 84 | /** @var null|string */ 85 | protected $body = null; 86 | 87 | protected $headers = []; 88 | 89 | /** @var string */ 90 | protected $version; 91 | 92 | public function __construct($status_code = 200, $headers = [], $body = null, $version = "1.1") 93 | { 94 | $this->status_code = $status_code; 95 | $this->headers = $headers; 96 | $this->body = $body; 97 | $this->version = $version; 98 | } 99 | 100 | /** 101 | * @return $this 102 | */ 103 | public function setStatusCode($status_code) 104 | { 105 | if (null === self::PHRASES[$this->status_code]) { 106 | throw new \InvalidArgumentException("Invalid status code '{$this->status_code}'"); 107 | } 108 | 109 | $this->status_code = $status_code; 110 | $this->setReasonPhrase(); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * @return int 117 | */ 118 | public function getStatusCode() 119 | { 120 | return $this->status_code; 121 | } 122 | 123 | /** 124 | * @return $this 125 | */ 126 | public function setBody($body) 127 | { 128 | $this->body = $body; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * @return $this 135 | */ 136 | public function setReasonPhrase($phrase = null) 137 | { 138 | if ($phrase !== null) { 139 | $this->reason_phrase = $phrase; 140 | return $this; 141 | } 142 | 143 | if (null !== self::PHRASES[$this->status_code]) { 144 | $this->reason_phrase = self::PHRASES[$this->status_code]; 145 | return $this; 146 | } 147 | 148 | throw new \InvalidArgumentException("Invalid status code '{$this->status_code}'"); 149 | } 150 | 151 | /** 152 | * @return string 153 | */ 154 | public function getReasonPhrase() 155 | { 156 | return $this->reason_phrase; 157 | } 158 | 159 | /** 160 | * @return string|null 161 | */ 162 | public function getBody() 163 | { 164 | return $this->body; 165 | } 166 | 167 | public function sendBody() 168 | { 169 | if ($this->body !== null) { 170 | echo $this->body; 171 | } 172 | } 173 | 174 | public function sendHeaders() 175 | { 176 | if (headers_sent()) { 177 | $this->logger || $this->logger->error('Header already sent.'); 178 | return $this; 179 | } 180 | 181 | $this->sendHttpHeader(); 182 | foreach ($this->headers as $name => $value) { 183 | $v = implode(',', $value); 184 | header("{$name}: {$v}", true); 185 | } 186 | } 187 | 188 | public function sendHttpHeader() 189 | { 190 | header("HTTP/{$this->version} {$this->status_code} {$this->reason_phrase}", true); 191 | } 192 | 193 | /** 194 | * @return $this 195 | */ 196 | public function setHeaders(array $headers) 197 | { 198 | foreach ($headers as $header => $value) { 199 | $this->setHeader($header, $value); 200 | } 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * @return $this 207 | */ 208 | public function setHeader($header, $value) 209 | { 210 | $header = trim($header); 211 | if (!is_array($value)) { 212 | $value = trim($value); 213 | $this->headers[$header][] = $value; 214 | } else { 215 | foreach ($value as $v) { 216 | $v = trim($v); 217 | $this->headers[$header][] = $v; 218 | } 219 | } 220 | 221 | return $this; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | app_root = $app_root; 44 | $this->app_namespace = $this->detectAppNamespace(); 45 | $this->env = $env; 46 | 47 | $this->dirs = $this->getDefaultDirs(); 48 | } 49 | 50 | /** 51 | * @return Container 52 | */ 53 | public function getContainer() 54 | { 55 | return $this->container; 56 | } 57 | 58 | public function loadConfig() 59 | { 60 | $config = []; 61 | foreach ($this->getConfigFiles() as $config_file) { 62 | $load_config_file = $this->getConfigDir() . '/' . $config_file; 63 | if (!file_exists($load_config_file)) { 64 | continue; 65 | } 66 | 67 | $config = array_merge($config, require $load_config_file); 68 | } 69 | 70 | $this->config = new Config($config); 71 | $this->bootConfig(); 72 | } 73 | 74 | public function initHttpRequest(Container $container) 75 | { 76 | $server = $container['global.server']->get(); 77 | $this->host = $server['HTTP_HOST']; 78 | $this->port = $server['SERVER_PORT']; 79 | $this->protocol = (($this->port == '443' || (isset($server['X_FORWARDED_PROTO']) && $server['X_FORWARDED_PROTO'] == 'https')) ? 'https' : 'http'); 80 | $this->path = parse_url($server['REQUEST_URI'])['path']; 81 | $this->url = $this->protocol . '://' . $this->host; 82 | } 83 | 84 | public function init(Container $container) 85 | { 86 | } 87 | 88 | abstract public function config(Container $container); 89 | 90 | /** 91 | * @return string 92 | */ 93 | public function getEnv() 94 | { 95 | return $this->env; 96 | } 97 | 98 | public function getAppRoot() 99 | { 100 | return $this->app_root; 101 | } 102 | 103 | public function getAppNamespace() 104 | { 105 | return $this->app_namespace; 106 | } 107 | 108 | public function getRoute() 109 | { 110 | $route_class = $this->getAppNamespace() . '\\Route'; 111 | return new $route_class; 112 | } 113 | 114 | /** 115 | * @return Config 116 | */ 117 | public function getConfig() 118 | { 119 | return $this->config; 120 | } 121 | 122 | public function setDir($dirname, $path) 123 | { 124 | $this->dirs[$dirname] = $path; 125 | } 126 | 127 | public function getHost() 128 | { 129 | return $this->host; 130 | } 131 | 132 | public function getProtocol() 133 | { 134 | return $this->protocol; 135 | } 136 | 137 | public function getPort() 138 | { 139 | return $this->port; 140 | } 141 | 142 | public function getPath() 143 | { 144 | return $this->path; 145 | } 146 | 147 | public function getUrl() 148 | { 149 | return $this->url; 150 | } 151 | 152 | public function getWebrootDir() 153 | { 154 | return $this->dirs['webroot']; 155 | } 156 | 157 | public function getResourceDir() 158 | { 159 | return $this->dirs['resource']; 160 | } 161 | 162 | public function getTemplateDir() 163 | { 164 | return $this->dirs['template']; 165 | } 166 | 167 | public function getTemplateExt() 168 | { 169 | return '.html.twig'; 170 | } 171 | 172 | public function getConfigDir() 173 | { 174 | return $this->dirs['config']; 175 | } 176 | 177 | public function getTmpDir() 178 | { 179 | return $this->dirs['tmp']; 180 | } 181 | 182 | public function getVendorDir() 183 | { 184 | return $this->dirs['vendor']; 185 | } 186 | 187 | public function isDebug() 188 | { 189 | return $this->debug; 190 | } 191 | 192 | public function getConfigFiles() 193 | { 194 | return [ 195 | 'config.php', 196 | 'config_' . $this->getEnv() . '.php', 197 | ]; 198 | } 199 | 200 | public function getControllerByHandler($handler) 201 | { 202 | // @TODO check 203 | list($controller, $action_name) = explode('::', $handler, 2); 204 | if (!$controller || !$action_name) { 205 | throw new DCException('Error: handler error'); 206 | } 207 | 208 | $controller_name = $this->getAppNamespace() 209 | . '\\Controller\\' 210 | . str_replace('/', '\\', $controller) 211 | . 'Controller'; 212 | 213 | return [$controller_name, $action_name]; 214 | } 215 | 216 | public function createController($controller_name) 217 | { 218 | $controller = new $controller_name($this->container); 219 | $controller->setVars('env', $this->getEnv()); 220 | $controller->setVars('config', $this->container['app.config']->getData()); 221 | 222 | return $controller; 223 | } 224 | 225 | protected function getDefaultDirs() 226 | { 227 | return [ 228 | 'controller' => $this->app_root . '/Controller', 229 | 'config' => $this->app_root . '/config', 230 | 'template' => $this->app_root . '/template', 231 | 'resource' => $this->app_root . '/resource', 232 | 'webroot' => dirname($this->app_root) . '/webroot', 233 | 'tests' => dirname($this->app_root) . '/tests', 234 | 'vendor' => dirname($this->app_root) . '/vendor', 235 | 'tmp' => dirname($this->app_root) . '/tmp', 236 | ]; 237 | } 238 | 239 | protected function bootConfig() 240 | { 241 | $this->debug = $this->config->get('debug', false); 242 | } 243 | 244 | protected function detectAppNamespace() 245 | { 246 | $ref = new \ReflectionObject($this); 247 | return $ref->getNamespaceName(); 248 | } 249 | 250 | public function createLogger($path, $level = Logger::WARNING) 251 | { 252 | $logger = new Logger('app'); 253 | $logger->pushProcessor(new PsrLogMessageProcessor); 254 | 255 | if (is_writable($path) || is_writable(dirname($path))) { 256 | $logger->pushHandler(new StreamHandler($path, $level)); 257 | } else { 258 | if ($this->isDebug()) { 259 | throw new DCException("Log path '{$path}' is not writable. Make sure your logger.path of config."); 260 | } 261 | $logger->pushHandler(new ErrorLogHandler(ErrorLogHandler::OPERATING_SYSTEM, $level)); 262 | $logger->warning("Log path '{$path}' is not writable. Make sure your logger.path of config."); 263 | $logger->warning("error_log() is used for application logger instead at this time."); 264 | } 265 | 266 | return $logger; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | app = $app; 44 | } 45 | 46 | public function boot() 47 | { 48 | $this->app->loadConfig(); 49 | 50 | $container = $this->container = new Container(); 51 | 52 | $this->container['event_dispatcher'] = $this->event_dispatcher = new EventDispatcher(); 53 | 54 | $this->container['app'] = $this->app; 55 | $this->app->setContainer($container); 56 | $config = $this->container['app.config'] = $this->app->getConfig(); 57 | 58 | $this->container['logger'] = $logger = $this->app->createLogger( 59 | $config->get('logger.path'), 60 | $config->get('logger.level', Logger::WARNING) 61 | ); 62 | 63 | $logger->debug('Application booted. env={env}', ['env' => $this->app->getEnv()]); 64 | $logger->debug('Config file loaded. config_files={files}', ['files' => implode(',', $this->app->getConfigFiles())]); 65 | 66 | $this->bootGlobals(); 67 | 68 | $this->app->initHttpRequest($this->container); 69 | $this->app->init($this->container); 70 | 71 | if (!isset($this->container['router'])) { 72 | $this->container['router'] = new Router($this->container); 73 | $this->container['router']->addRoute($this->app->getRoute()); 74 | } 75 | 76 | if (!isset($this->container['app.renderer'])) { 77 | $this->container['app.renderer'] = function () { 78 | return $this->createRenderer(); 79 | }; 80 | } 81 | 82 | $this->app->config($this->container); 83 | 84 | $this->event_dispatcher->dispatch(DietcubeEvents::BOOT, new BootEvent($this->app)); 85 | } 86 | 87 | protected function createRenderer() 88 | { 89 | $config = $this->container['app.config']; 90 | $loader = new \Twig_Loader_Filesystem($this->app->getTemplateDir()); 91 | $twig = new \Twig_Environment($loader, [ 92 | 'debug' => $config->get('debug', false), 93 | 'cache' => $config->get('twig.cache', false), 94 | 'charset' => $config->get('twig.charset', 'utf-8'), 95 | ]); 96 | 97 | // add built-in template path 98 | $loader->addPath(__DIR__ . '/template/error'); 99 | 100 | // add built-in extension 101 | $twig->addExtension((new DietcubeExtension())->setContainer($this->container)); 102 | 103 | if ($this->app->isDebug()) { 104 | // add built-in debug template path 105 | $twig->addExtension(new \Twig_Extension_Debug()); 106 | $loader->addPath(__DIR__ . '/template/debug', 'debug'); 107 | } 108 | 109 | $twig->addGlobal('query', $this->container['global.get']->getData()); 110 | $twig->addGlobal('body', $this->container['global.post']->getData()); 111 | 112 | return $twig; 113 | } 114 | 115 | protected function bootGlobals() 116 | { 117 | $this->container['global.server'] = new Parameters($_SERVER); 118 | $this->container['global.get'] = new Parameters($_GET); 119 | $this->container['global.post'] = new Parameters($_POST); 120 | $this->container['global.files'] = new Parameters($_FILES); 121 | $this->container['global.cookie'] = new Parameters($_COOKIE); 122 | } 123 | 124 | /** 125 | * @return Response 126 | */ 127 | protected function prepareResponse() 128 | { 129 | $response = new Response(); 130 | $response->setLogger($this->container['logger']); 131 | $this->container['response'] = $response; 132 | 133 | return $response; 134 | } 135 | 136 | /** 137 | * @return Response 138 | */ 139 | public function handleRequest() 140 | { 141 | $container = $this->container; 142 | 143 | // prepare handle request 144 | $response = $this->prepareResponse(); 145 | 146 | $method = $container['global.server']->get('REQUEST_METHOD'); 147 | $path = $container['app']->getPath(); 148 | $this->event_dispatcher->addListener(DietcubeEvents::ROUTING, function (Event $event) use ($method, $path) { 149 | list($handler, $vars) = $this->dispatchRouter($method, $path); 150 | 151 | $event->setRouteInfo($handler, $vars); 152 | }); 153 | 154 | $event = new RoutingEvent($this->app, $container['router']); 155 | $this->event_dispatcher->dispatch(DietcubeEvents::ROUTING, $event); 156 | 157 | list($handler, $vars) = $event->getRouteInfo(); 158 | 159 | $action_result = $this->executeAction($handler, $vars); 160 | $response = $response->setBody($action_result); 161 | 162 | return $this->filterResponse($response); 163 | } 164 | 165 | /** 166 | * @param \Exception $errors 167 | * @return Response 168 | */ 169 | public function handleError(\Exception $errors) 170 | { 171 | $logger = $this->container['logger']; 172 | if (!isset($this->container['response'])) { 173 | $response = $this->prepareResponse(); 174 | } else { 175 | $response = $this->container['response']; 176 | } 177 | 178 | $action_result = ""; 179 | 180 | $logger->error('Error occurred. ', [ 181 | 'error' => get_class($errors), 182 | 'message' => $errors->getMessage(), 183 | 'trace' => $errors->getTraceAsString(), 184 | ]); 185 | if ($this->app->isDebug()) { 186 | $debug_controller = isset($this->container['app.debug_controller']) 187 | ? $this->container['app.debug_controller'] 188 | : __NAMESPACE__ . '\\Controller\\DebugController'; 189 | $controller = $this->app->createController($debug_controller); 190 | 191 | // FIXME: debug controller method name? 192 | $action_result = $this->executeAction([$controller, 'dumpErrors'], ['errors' => $errors], $fire_events = false); 193 | } else { 194 | list($controller_name, $action_name) = $this->detectErrorAction($errors); 195 | $controller = $this->app->createController($controller_name); 196 | 197 | $action_result = $this->executeAction([$controller, $action_name], ['errors' => $errors], $fire_events = false); 198 | } 199 | 200 | $response->setBody($action_result); 201 | 202 | return $this->filterResponse($response); 203 | } 204 | 205 | public function executeAction($handler, $vars = [], $fire_events = true) 206 | { 207 | $logger = $this->container['logger']; 208 | $executable = null; 209 | 210 | if (is_callable($handler)) { 211 | $executable = $handler; 212 | } else { 213 | list($controller_name, $action_name) = $this->app->getControllerByHandler($handler); 214 | 215 | if (!class_exists($controller_name)) { 216 | throw new DCException("Controller {$controller_name} is not exists."); 217 | } 218 | $controller = $this->app->createController($controller_name); 219 | $executable = [$controller, $action_name]; 220 | } 221 | 222 | if ($fire_events) { 223 | $event = new ExecuteActionEvent($this->app, $executable, $vars); 224 | $this->event_dispatcher->dispatch(DietcubeEvents::EXECUTE_ACTION, $event); 225 | 226 | $executable = $event->getExecutable(); 227 | $vars = $event->getVars(); 228 | } 229 | 230 | // Executable may changed by custom event so parse info again. 231 | if ($executable instanceof \Closure) { 232 | $controller_name = 'function()'; 233 | $action_name = '-'; 234 | } else { 235 | $controller_name = get_class($executable[0]); 236 | $action_name = $executable[1]; 237 | 238 | if (!is_callable($executable)) { 239 | // anon function is always callable so when the handler is anon function it never come here. 240 | $logger->error('Action not dispatchable.', ['controller' => $controller_name, 'action_name' => $action_name]); 241 | throw new DCException("'{$controller_name}::{$action_name}' is not a valid action."); 242 | } 243 | } 244 | 245 | $logger->debug('Execute action.', ['controller' => $controller_name, 'action' => $action_name, 'vars' => $vars]); 246 | return call_user_func_array($executable, $vars); 247 | } 248 | 249 | protected function getErrorController() 250 | { 251 | $error_controller = isset($this->container['app.error_controller']) 252 | ? $this->container['app.error_controller'] 253 | : __NAMESPACE__ . '\\Controller\\ErrorController'; 254 | return $error_controller; 255 | } 256 | 257 | /** 258 | * Dispatch router with HTTP request information. 259 | * 260 | * @param $method 261 | * @param $path 262 | * @return array 263 | */ 264 | protected function dispatchRouter($method, $path) 265 | { 266 | $router = $this->container['router']; 267 | $logger = $this->container['logger']; 268 | 269 | $logger->debug('Router dispatch.', ['method' => $method, 'path' => $path]); 270 | 271 | $router->init(); 272 | $route_info = $router->dispatch($method, $path); 273 | 274 | $handler = null; 275 | $vars = []; 276 | 277 | switch ($route_info[0]) { 278 | case RouteDispatcher::NOT_FOUND: 279 | $logger->debug('Routing failed. Not Found.'); 280 | throw new HttpNotFoundException('404 Not Found'); 281 | break; 282 | case RouteDispatcher::METHOD_NOT_ALLOWED: 283 | $logger->debug('Routing failed. Method Not Allowd.'); 284 | throw new HttpMethodNotAllowedException('405 Method Not Allowed'); 285 | break; 286 | case RouteDispatcher::FOUND: 287 | $handler = $route_info[1]; 288 | $vars = $route_info[2]; 289 | $logger->debug('Route found.', ['handler' => $handler]); 290 | break; 291 | } 292 | 293 | return [$handler, $vars]; 294 | } 295 | 296 | protected function detectErrorAction(\Exception $errors) 297 | { 298 | $error_controller = $this->getErrorController(); 299 | if ($errors instanceof HttpNotFoundException) { 300 | return [$error_controller, Controller::ACTION_NOT_FOUND]; 301 | } elseif ($errors instanceof HttpMethodNotAllowedException) { 302 | return [$error_controller, Controller::ACTION_METHOD_NOT_ALLOWED]; 303 | } 304 | 305 | // Do internalError action for any errors. 306 | return [$error_controller, Controller::ACTION_INTERNAL_ERROR]; 307 | } 308 | 309 | /** 310 | * Dispatch FILTER_RESPONSE event to filter response. 311 | * 312 | * @param Response $response 313 | * @return Response 314 | */ 315 | protected function filterResponse(Response $response) 316 | { 317 | $event = new FilterResponseEvent($this->app, $response); 318 | $this->event_dispatcher->dispatch(DietcubeEvents::FILTER_RESPONSE, $event); 319 | 320 | return $this->finishRequest($event->getResponse()); 321 | } 322 | 323 | /** 324 | * Finish request and send response. 325 | * 326 | * @param Response $response 327 | * @return Response 328 | */ 329 | protected function finishRequest(Response $response) 330 | { 331 | $event = new FinishRequestEvent($this->app, $response); 332 | $this->event_dispatcher->dispatch(DietcubeEvents::FINISH_REQUEST, $event); 333 | 334 | $response = $event->getResponse(); 335 | 336 | $response->sendHeaders(); 337 | $response->sendBody(); 338 | 339 | return $response; 340 | } 341 | 342 | public static function getEnv($env = 'production') 343 | { 344 | if (isset($_SERVER['DIET_ENV'])) { 345 | $env = $_SERVER['DIET_ENV']; 346 | } elseif (getenv('DIET_ENV')) { 347 | $env = getenv('DIET_ENV'); 348 | } 349 | return $env; 350 | } 351 | 352 | public static function invoke($app_class, $app_root_dir, $env) 353 | { 354 | $app = new $app_class($app_root_dir, $env); 355 | $dispatcher = new static($app); 356 | $dispatcher->boot(); 357 | 358 | try { 359 | $response = $dispatcher->handleRequest(); 360 | } catch (\Exception $e) { 361 | // Please handle errors occurred on executing Dispatcher::handleError with your web server. 362 | // Dietcube doesn't care these errors. 363 | $response = $dispatcher->handleError($e); 364 | } 365 | } 366 | } 367 | --------------------------------------------------------------------------------