├── 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 | [](https://packagist.org/packages/dietcube/dietcube)
15 | [](https://packagist.org/packages/dietcube/dietcube)
16 | [](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 | | Controller::Action |
57 | {{ router.route_info[1] }} |
58 |
59 |
60 |
61 | | Params |
62 | {{ dump(router.route_info[2]) }} |
63 |
64 |
65 |
66 | | Method |
67 | {{ router.dispatched_method }} |
68 |
69 |
70 |
71 | | Url |
72 | {{ router.dispatched_url }} |
73 |
74 |
75 |
76 |
Application
77 |
Directories
78 |
79 |
80 |
81 | | App Root (#root) |
82 | {{ dirs.app_root }} |
83 |
84 |
85 | | Config |
86 | {{ dirs.config_dir }} |
87 |
88 |
89 | | Template |
90 | {{ dirs.template_dir }} |
91 |
92 |
93 | | Resource |
94 | {{ dirs.resource_dir }} |
95 |
96 |
97 | | Tmp |
98 | {{ dirs.tmp_dir }} |
99 |
100 |
101 | | Vendor (#vendor) |
102 | {{ dirs.vendor_dir }} |
103 |
104 |
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 |
--------------------------------------------------------------------------------