├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml ├── public ├── .htaccess └── index.php └── src ├── Application.php ├── Application ├── AbstractApplication.php ├── ApplicationEvent.php ├── ApplicationInterface.php ├── HttpApplicationEvent.php ├── Init │ ├── InitConfigurationTrait.php │ └── InitHaltHookTrait.php ├── MiddlewareAwareInterface.php ├── MiddlewareRunner.php ├── Providers │ ├── MonologServiceProvider.php │ └── WhoopsServiceProvider.php ├── Services │ ├── Whoops │ │ ├── ApplicationSystemFacade.php │ │ └── HandlerService.php │ └── WhoopsService.php └── TerminableInterface.php ├── Configuration.php ├── Console.php ├── Console ├── Command.php ├── ConsoleEvent.php └── Dispatcher.php ├── Stratigility ├── ApplicationMiddleware.php └── MiddlewarePipeAdapter.php └── Symfony └── HttpKernelAdapter.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Hawkbit Changelog 2 | 3 | ## 2.3.2 4 | 5 | ### Fixed 6 | 7 | - Fix issue #34. Bind closure always to application instance. 8 | 9 | ### Altered 10 | 11 | - Upgrade league/container to 2.4 12 | - Upgrade zendframework/zend-diactoros to 1.4 13 | - Update container implementation to psr 11 14 | 15 | ## 2.3.1 16 | 17 | ### Fixed 18 | 19 | - Reopen and fix #29, Special Thanks to @BlackScorp for fixing and @designcise for identification 20 | 21 | ## 2.3.0 22 | 23 | ### Added 24 | 25 | - Add basic usage example 26 | 27 | ### Altered 28 | 29 | - Add official league/route 3.0 support 30 | 31 | ### Fixed 32 | 33 | - Fix #29, #30 34 | 35 | ## 2.2.0 36 | 37 | ### Added 38 | 39 | - Add console application 40 | - Add constructor injection for console commands 41 | 42 | ### Altered 43 | 44 | - Move application interface to correct position 45 | - Refactor configuration initiation into trait and reuse in application and console 46 | 47 | ## 2.1.3 48 | 49 | ### Fixed 50 | 51 | - Fix #25 Ajax request is always forcing JSON request and response 52 | - Fix #26 use league/route at dev master until official middleware release 53 | 54 | ### Removed 55 | 56 | - Remove HHVM integration due to inconsitencies 57 | 58 | ## 2.1.2 59 | 60 | ### Fixed 61 | 62 | - Fix #24 incorrect and buggy error handling 63 | 64 | ## 2.1.1 65 | 66 | ### Added 67 | 68 | - Add tests for controller constructor injection 69 | 70 | ## 2.1.0 71 | 72 | ### Fixed 73 | 74 | - Validate response contract after executing 75 | 76 | ### Altered 77 | 78 | - Whoops handlers has been refactored to `\Hawkbit\Application\Services\Whoops\HandlerService` 79 | - Error und shutdown handling has been refactored to `Hawkbit\Application\Init\InitHaltHooksTrait` 80 | 81 | ### Added 82 | 83 | - Add exception stack `Hawkbit\Application\Application\AbtractApplication::getExceptionStack` of all occured exceptions 84 | - Provide backwards / onwards compatibility for PHP 7 `\Throwables` 85 | 86 | 87 | ### Altered 88 | 89 | - Update [League Router](https://github.com/thephpleague/route/tree/507606b53d3935e7830aa7c48c43337bc2b1b2ba) and use router middleware implementation instead of application middleware. 90 | - Update Zend Stratigility to 1.3.1 91 | - Advanced error capturing and error stack for debugging 92 | 93 | ### Removed 94 | 95 | - Remove dotted notation from from configuration 96 | - Remove vagrant machine 97 | 98 | ## 2.0.1 99 | 100 | ### Altered 101 | 102 | - Fix documentation 103 | - Fix typo 104 | 105 | ## 2.0 106 | 107 | ### Notice 108 | 109 | __Migrate Turbine from PhpThinkTank to hawkbit.__ 110 | 111 | ### Added 112 | 113 | - Add `\Hawkbit\Application\Configuration` (extending `\Zend\Config\Config`) as default configuration storage 114 | - Add PSR7 middleware implementation `\Hawkbit\Application\Application\MiddlewareRunner` for advanced control of application lifecycle 115 | 116 | ### Altered 117 | 118 | - Change Hawkbit\Application test namespace to Hawkbit\Application\Tests 119 | - Rewrite event behavior for advanced interception of requests, responses and errors 120 | - Implement dot chaining for nested configuration 121 | 122 | ## 1.1.7 123 | 124 | ### Altered 125 | 126 | - Fix wrong response determined by content type delegation 127 | 128 | ## 1.1.6 129 | 130 | ### Added 131 | 132 | - Add vagrant development environment 133 | - Add shutdown event 134 | - Add logic to force response emitting if headers already send 135 | 136 | ### Altered 137 | 138 | - Delegate request content type to response 139 | - Rename `Application::cleanUp` to `Application::collectGarbage` 140 | - Rename `Application::finishRequest` to `Application::shutdown` 141 | - Rename `Application::subscribe` to `Application::addListener` 142 | - Enhance error handling for different content types 143 | - Log application errors correctly, logging is silenced by default. 144 | 145 | ### Deprecated 146 | 147 | - `Application::cleanUp` 148 | - `Application::finishRequest` 149 | - `Application::subscribe` 150 | 151 | 152 | ## 1.1.5 153 | 154 | ### Added 155 | 156 | - Add `\Hawkbit\Application\Application\ConfiguratorInterface` 157 | 158 | ### Altered 159 | 160 | - `Application::getConfigurator` is now bound to `\Hawkbit\Application\Application\ConfiguratorInterface` contract 161 | 162 | ## 1.1.4 163 | 164 | ### Fixes 165 | 166 | - [\#9](../../issues/9) If class exists and is not part of container, `League\Container\Container::has` returns now false. 167 | 168 | ## 1.1.3 169 | 170 | ### Altered 171 | 172 | - Accept and process `\ArrayAccess` and `\Traversable` as configuration 173 | 174 | ## 1.1.2 175 | 176 | ### Altered 177 | 178 | - Replace applications [route collection methods](https://github.com/thephpleague/route/blob/master/src/RouteCollectionInterface.php) with `\League\Route\RouteCollectionMapTrait` 179 | - Application implements `\League\Route\RouteCollectionInterface` 180 | - add `\League\Route\RouteCollectionInterface::map()` 181 | - add `\Hawkbit\Application\Application::group()` for creating route groups, see [documentation](http://route.thephpleague.com/route-groups/) 182 | 183 | ### Deprecated 184 | 185 | - `\Hawkbit\Application\Application::subscribe()` 186 | 187 | ## 1.1.1 188 | 189 | ### Altered 190 | 191 | - Upgrade `league/route` from dev-develop to stable 2.x (`~2.0`) release 192 | 193 | ## 1.1.0 194 | 195 | ### Added 196 | 197 | - Add `filp/whoops` as default error handler 198 | - Add `zendframework/zend-stratigility` integration 199 | 200 | ### Altered 201 | 202 | - add request and response accessors 203 | - refactor error handling and replace exception decorator with whoops 204 | - pass and receive all config 205 | - remove possibilty to configure events, routes and services by callables 206 | - rename `Hawkbit\Application\Psr7\TerminableInterface` to `Hawkbit\Application\TerminableInterface` 207 | - rename debug config option to error 208 | - change configuration engine from `array` to instance of `\ArrayAccess` 209 | - Signature changes of `Hawkbit\Application\Application::handle`, `Hawkbit\Application\Application::run`, `Hawkbit\Application\Application::__construct`, `Hawkbit\Application\Application::handleErrors` 210 | 211 | ### Removed 212 | 213 | - `Hawkbit\Application\Psr7\HttpKernelInterface` replaced by `Hawkbit\Application\ApplicationInterface` 214 | 215 | ## 1.0.0 (2016-03-04) 216 | 217 | ### Added 218 | 219 | - `Hawkbit\Application\Psr7\HttpKernelInterface` and `Hawkbit\Application\Psr7\TerminableInterface` port of symfony HttpKernelInterface for PSR-7 compatibility 220 | - Add `zend/diactoros` for PSR-7 http support 221 | - provide compatibility with adapter `Hawkbit\Application\Symfony\HttpKernelAdapter` for StackPHP and other Symfony HttpKernelInterface implementations 222 | - Add `filp/whoops` as default error handler 223 | - Add `zendframework/zend-stratigility` integration 224 | 225 | ### Altered 226 | 227 | - upgrade `league/container` to latest version 2 and add interopt compatibility 228 | - upgrade `league/route` to latest version 2 (currently under development) 229 | - replace symfony request and response with diactoros request and response 230 | - enable auto wiring of container configurable and enable by default 231 | - events, routes and services configurable by callables 232 | - add request and response accessors 233 | - refactor error handling and replace exception decorator with whoops 234 | - enhance configuration handling 235 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at marco_bunge@web.de. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/Hawkbit). 6 | 7 | ## Pull Requests 8 | 9 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 10 | 11 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 12 | 13 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 14 | 15 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 16 | 17 | - **Create feature branches** - Don't ask us to pull from your master branch. 18 | 19 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 20 | 21 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 22 | 23 | ## Install 24 | 25 | ``` bash 26 | $ composer install --dev 27 | ``` 28 | 29 | ## Running Tests 30 | 31 | ``` bash 32 | $ composer test 33 | ``` 34 | 35 | **Happy coding**! -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alex Bilbie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hawkbit\Application 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Build Status][ico-travis]][link-travis] 6 | [![Total Downloads][ico-downloads]][link-downloads] 7 | [![Coverage Status][ico-coveralls]][link-coveralls] 8 | 9 | Hawkbit\Application micro framework is a high customizable, event driven and compatible with 10 | [PSR-7](https://github.com/php-fig/http-message), 11 | [StackPHP](http://stackphp.com/) and 12 | [Zend Stratigility](https://github.com/zendframework/zend-stratigility). 13 | 14 | Hawkbit\Application uses latest versions of [League\Route](https://github.com/thephpleague/route) for routing, 15 | [League\Container](https://github.com/thephpleague/container) for dependency injection, 16 | [League\Event](https://github.com/thephpleague/event) for event dispatching, 17 | [Zend Config](https://docs.zendframework.com/zend-config/) for configuration. 18 | 19 | Hawkbit\Application is an advanced derivate of [Proton](https://github.com/alexbilbie/Proton) and part of Hawkbit\Application Component collection by Marco Bunge. Hawkbit\Application 1.x is also known as Blast Hawkbit\Application. 20 | 21 | ### Quick start 22 | 23 | Please see [public/](public/) for example usage and read documentation. 24 | 25 | ### Integrations 26 | 27 | Hawkbit\Application delivers also optional packages: 28 | 29 | - Database: [`hawkbit/database`](https://github.com/HawkBitPhp/hawkbit-database) 30 | - Doctrine ORM Wrapper: [`hawkbit/doctrine`](https://github.com/HawkBitPhp/hawkbit-doctrine) 31 | - Plates View Engine: [`hawkbit/presentation`](https://github.com/HawkBitPhp/hawkbit-presentation) 32 | 33 | ## Motivation 34 | 35 | > My vision is to provide a micro framework which is able to handle HTTP and CLI in the same fashion. The developer should be able to reuse it's code, design it's business layer by his needs. Hawkbit should be a supporting tool instead of predefined framework. And yes it is under active development. 36 | > 37 | > I love PSR, phpleague und a minimal set of dependecies and want to create a micro framework which is used the best packages out there and bundeld in a nice application layer. I'm also love the style of component-based development. 38 | > 39 | > Hawkbit is built on top phpleague packages and keep PSR in mind. Hawkbit is designed to co-exist whith your code instead of replace code base. Hawkbit does has a small dependency footprint. Last but not least Hawkbit does not force the developer how to design the application bussiness logic, since we prefer to use POPO's for Controllers / Commands (the accessor to bussiness logic). 40 | > 41 | > At the moment I design and develop all Hawkbit packages and manage the whole codebase. I would be appreciate for support or even better: for contributors! 42 | 43 | Please refer to [Issue #33](https://github.com/HawkBitPhp/hawkbit/issues/33) for details. 44 | 45 | ## Special thanks 46 | 47 | Thank you for PR, identifing Bus, or any other Improvements! 48 | 49 | - [designcise](https://github.com/designcise) 50 | - [Vitalij Mik](https://github.com/BlackScorp) 51 | 52 | ## Install 53 | 54 | ### Using Composer 55 | 56 | Hawkbit\Application is available on [Packagist](https://packagist.org/packages/hawkbit/hawkbit) and can be installed using [Composer](https://getcomposer.org/). This can be done by running the following command or by updating your `composer.json` file. 57 | 58 | ```bash 59 | composer require hawkbit/hawkbit 60 | ``` 61 | 62 | composer.json 63 | 64 | ```javascript 65 | { 66 | "require": { 67 | "hawkbit/hawkbit": "~2.0" 68 | } 69 | } 70 | ``` 71 | 72 | Be sure to also include your Composer autoload file in your project: 73 | 74 | ```php 75 | 'value' 113 | ]; 114 | $app = new \Hawkbit\Application($config); 115 | ``` 116 | 117 | Add routes 118 | 119 | ```php 120 | get('/', function ($request, $response) { 124 | $response->getBody()->write('

It works!

'); 125 | return $response; 126 | }); 127 | 128 | $app->get('/hello/{name}', function ($request, $response, $args) { 129 | $response->getBody()->write( 130 | sprintf('

Hello, %s!

', $args['name']) 131 | ); 132 | return $response; 133 | }); 134 | ``` 135 | 136 | Run application 137 | 138 | ```php 139 | run(); 142 | ``` 143 | 144 | See also our example at `/public/index.php`. 145 | 146 | ## Configuration 147 | 148 | Add additional configuration to application 149 | 150 | Hawkbit\Application Configuration is managed by [zend-config](https://docs.zendframework.com/zend-config/). 151 | 152 | ```php 153 | setConfig([ 157 | 'database' => [ 158 | 'default' => 'mysql://root:root@localhost/acmedb', 159 | ], 160 | 'services' => [ 161 | 'Acme\Services\ViewProvider', 162 | ] 163 | ]); 164 | 165 | //add a single value 166 | $app->setConfig('baseurl', 'localhost/'); 167 | 168 | $app->getConfig()->baseurl = 'localhost/'; 169 | $app->getConfig()['baseurl'] = 'localhost/'; 170 | ``` 171 | 172 | Access configuration 173 | 174 | ```php 175 | getConfig(); 179 | 180 | //get configuration item 181 | $default = $app->getConfig('database')->default; // returns 'mysql://root:root@localhost/acmedb 182 | $default = $app->getConfig()->database->default; // returns 'mysql://root:root@localhost/acmedb 183 | $default = $app->getConfig('database')['default']; // returns 'mysql://root:root@localhost/acmedb 184 | $default = $app->getConfig()['database']['default']; // returns 'mysql://root:root@localhost/acmedb 185 | ``` 186 | 187 | ## Middlewares 188 | 189 | Hawkbit\Application middlewares allows advanced control of lifecycle execution. 190 | 191 | ```php 192 | addMiddleware(new Acme\SomeMiddleware); 195 | ``` 196 | 197 | Hawkbit\Application uses it's own runner `Hawkbit\Application\MiddelwareRunner` 198 | 199 | ## Routing 200 | 201 | Hawkbit\Application uses routing integration of `league/route` and allows access to route collection methods directly. 202 | 203 | Basic usage with anonymous functions: 204 | 205 | ```php 206 | get('/', function ($request, $response) { 210 | $response->getBody()->write('

It works!

'); 211 | return $response; 212 | }); 213 | 214 | $app->get('/hello/{name}', function ($request, $response, $args) { 215 | $response->getBody()->write( 216 | sprintf('

Hello, %s!

', $args['name']) 217 | ); 218 | return $response; 219 | }); 220 | 221 | $app->run(); 222 | ``` 223 | 224 | #### Access app from anonymous function 225 | 226 | Hawkbit\Application allows to access itself from anonymous function through closure binding. 227 | 228 | ```php 229 | get('/hello/{name}', function ($request, $response, $args) { 232 | 233 | // access Hawkbit\Application 234 | $app = $this; 235 | 236 | $response->getBody()->write( 237 | sprintf('

Hello, %s!

', $args['name']) 238 | ); 239 | return $response; 240 | }); 241 | 242 | ``` 243 | 244 | Basic usage with controllers: 245 | 246 | ```php 247 | get('/', 'HomeController::index'); // calls index method on HomeController class 254 | 255 | $app->run(); 256 | ``` 257 | 258 | ```php 259 | getBody()->write('

It works!

'); 271 | return $response; 272 | } 273 | } 274 | ``` 275 | 276 | Automatic constructor injection of controllers: 277 | 278 | ```php 279 | getContainer()->add('CustomService', new CustomService); 288 | $app->get('/', 'HomeController::index'); // calls index method on HomeController class 289 | 290 | $app->run(); 291 | ``` 292 | 293 | *Please use boot method in Service Providers for correct injection of services into controller!* 294 | 295 | ```php 296 | service = $service; 316 | } 317 | 318 | /** 319 | * @return CustomService 320 | */ 321 | public function getService() 322 | { 323 | return $this->service; 324 | } 325 | 326 | public function index(ServerRequestInterface $request, ResponseInterface $response, array $args) 327 | { 328 | //do somehing with service 329 | $service = $this->getService(); 330 | return $response; 331 | } 332 | } 333 | ``` 334 | 335 | For more information about routes [read this guide](http://route.thephpleague.com/) 336 | 337 | ### Route groups 338 | 339 | Hawkbit\Application add support for route groups. 340 | 341 | ```php 342 | group('/admin', function (\League\Route\RouteGroup $route) { 345 | 346 | //access app container (or any other method!) 347 | $app = $this; 348 | 349 | $route->map('GET', '/acme/route1', 'AcmeController::actionOne'); 350 | $route->map('GET', '/acme/route2', 'AcmeController::actionTwo'); 351 | $route->map('GET', '/acme/route3', 'AcmeController::actionThree'); 352 | }); 353 | ``` 354 | 355 | #### Available vars 356 | 357 | - `$route` - `\League\Route\RouteGroup` 358 | - `$this` - `\Hawkbit\Application` 359 | 360 | ## Middleware integrations 361 | 362 | ### StackPHP 363 | 364 | Basic usage with StackPHP (using `Stack\Builder` and `Stack\Run`): 365 | 366 | ```php 367 | get('/', function ($request, $response) { 375 | $response->setContent('

Hello World

'); 376 | return $response; 377 | }); 378 | 379 | $httpKernel = new Hawkbit\Application\Symfony\HttpKernelAdapter($app); 380 | 381 | $stack = (new \Stack\Builder()) 382 | ->push('Some/MiddleWare') // This will execute first 383 | ->push('Some/MiddleWare') // This will execute second 384 | ->push('Some/MiddleWare'); // This will execute third 385 | 386 | $app = $stack->resolve($httpKernel); 387 | \Stack\run($httpKernel); // The app will run after all the middlewares have run 388 | ``` 389 | 390 | ### Zend Stratigility 391 | 392 | Basic usage with Stratigility (using `Zend\Stratigility\MiddlewarePipe`): 393 | 394 | ```php 395 | get('/', function($request, ResponseInterface $response){ 404 | $response->getBody()->write('Hello World'); 405 | }); 406 | $middleware = new MiddlewarePipeAdapter($application); 407 | 408 | //wrap html heading 409 | $middleware->pipe('/', function($request, ResponseInterface $response, $next){ 410 | $response->getBody()->write('

'); 411 | 412 | /** @var ResponseInterface $response */ 413 | $response = $next($request, $response); 414 | 415 | $response->getBody()->write('

'); 416 | }); 417 | 418 | /** @var ResponseInterface $response */ 419 | $response = $middleware(ServerRequestFactory::fromGlobals(), $application->getResponse()); 420 | 421 | echo $response->getBody(); //prints

Hello World

422 | 423 | ``` 424 | 425 | ## Error handling 426 | 427 | Hawkbit\Application uses Whoops error handling framework and determines the error handler by request content type. 428 | 429 | Set your own handler: 430 | 431 | ```php 432 | getErrorHandler()->push(new Acme\ErrorResponseHandler); 435 | ``` 436 | 437 | By default Hawkbit\Application runs with error options disabled. To enable debugging add 438 | 439 | ```php 440 | setConfig('error', true); 443 | ``` 444 | 445 | By default Hawkbit\Application is catching all errors. To disable error catching add 446 | 447 | ```php 448 | setConfig('error.catch', false); 451 | ``` 452 | 453 | ## Console 454 | 455 | The console application inherit all methods from Http Application except routing and PSR-7 handling and capturing. 456 | In addition to http application, the console application does not support all events (Refer to events for more 457 | information!) 458 | 459 | ## Logging 460 | 461 | Hawkbit\Application has built in support for Monolog. To access a channel call: 462 | 463 | ```php 464 | getLogger('channel name'); 467 | ``` 468 | 469 | For more information about channels read this guide - [https://github.com/Seldaek/monolog/blob/master/doc/usage.md#leveraging-channels](https://github.com/Seldaek/monolog/blob/master/doc/usage.md#leveraging-channels). 470 | 471 | ## Events 472 | 473 | You can intercept requests and responses at seven points during the lifecycle. You can manipulate Request, Response and 474 | ErrorResponse via `Hawkbit\ApplicationEvent`. 475 | 476 | ### Application event 477 | 478 | ```php 479 | getParamCollection(); // returns a mutable \ArrayObject 485 | 486 | // access application 487 | $event->getApplication(); 488 | 489 | ``` 490 | 491 | ### request.received 492 | 493 | ```php 494 | addListener($app::EVENT_REQUEST_RECEIVED, function (\Hawkbit\Application\ApplicationEvent $event) { 497 | $request = $event->getRequest(); 498 | 499 | // manipulate $request 500 | 501 | $event->setRequest($request); 502 | }); 503 | ``` 504 | 505 | This event is fired when a request is received but before it has been processed by the router. 506 | 507 | ### response.created 508 | 509 | *Not available for Console applications!* 510 | 511 | ```php 512 | addListener($app::EVENT_RESPONSE_CREATED, function (\Hawkbit\Application\ApplicationEvent $event) { 515 | $request = $event->getRequest(); 516 | $response = $event->getResponse(); 517 | 518 | // manipulate request or response 519 | 520 | $event->setRequest($request); 521 | $event->setResponse($response); 522 | }); 523 | ``` 524 | 525 | This event is fired when a response has been created but before it has been output. 526 | 527 | ### response.sent 528 | 529 | *Not available for Console applications! Please use `shutdown`* 530 | 531 | ```php 532 | addListener($app::EVENT_RESPONSE_SENT, function (\Hawkbit\Application\ApplicationEvent $event) { 535 | $request = $event->getRequest(); 536 | $response = $event->getResponse(); 537 | 538 | // manipulate request or response 539 | 540 | $event->setRequest($request); 541 | $event->setResponse($response); 542 | }); 543 | ``` 544 | 545 | This event is fired when a response has been output and before the application lifecycle is completed. Not available for Console Applications! 546 | 547 | ### runtime.error 548 | 549 | ```php 550 | addListener($app::EVENT_RUNTIME_ERROR, function (\Hawkbit\Application\ApplicationEvent $event, $exception) use ($app) { 553 | //process exception 554 | }); 555 | ``` 556 | 557 | This event is always fired when an error occurs. 558 | 559 | ### lifecycle.error 560 | 561 | *Not available for Console applications! Please use `runtime.error`* 562 | 563 | `$errorResponse` is used as default response 564 | 565 | ```php 566 | addListener($app::EVENT_LIFECYCLE_ERROR, function (\Hawkbit\Application\ApplicationEvent $event, \Exception $exception) { 569 | $errorResponse = $event->getErrorResponse(); 570 | 571 | //manipulate error response and process exception 572 | 573 | $event->setErrorResponse($errorResponse); 574 | }); 575 | ``` 576 | 577 | This event is fired only when an error occurs while handling request/response lifecycle. 578 | This event is fired after runtime.error 579 | 580 | ### lifecycle.complete 581 | 582 | *Not available for Console applications! Please use `shutdown`* 583 | 584 | ```php 585 | addListener($app::EVENT_LIFECYCLE_COMPLETE, function (\Hawkbit\Application\ApplicationEvent $event) { 588 | // access the request using $event->getRequest() 589 | // access the response using $event->getResponse() 590 | }); 591 | ``` 592 | 593 | This event is fired when a response has been output and before the application lifecycle is completed. 594 | 595 | ### shutdown 596 | 597 | ```php 598 | addListener($app::EVENT_SHUTDOWN, function (\Hawkbit\Application\ApplicationEvent $event, $response, $terminatedOutputBuffers = []) { 601 | // access the response using $event->getResponse() 602 | // access terminated output buffer contents 603 | // or force application exit() 604 | }); 605 | ``` 606 | 607 | This event is always fired after each operation is completed or failed. 608 | 609 | ### Custom Events 610 | 611 | You can fire custom events using the event emitter directly: 612 | 613 | ```php 614 | addListener('custom.event', function ($event, $time) { 618 | return 'the time is '.$time; 619 | }); 620 | 621 | // or with class addListener 622 | $app->addListener(Acme\Event::class, function (Acme\Event $event, $time) { 623 | return 'the time is '.$time; 624 | }); 625 | 626 | // Publish 627 | $app->getEventEmitter()->emit('custom.event', time()); 628 | ``` 629 | 630 | ## Dependency Injection Container 631 | 632 | Hawkbit\Application uses `League/Container` as its dependency injection container. 633 | 634 | You can bind singleton objects into the container from the main application object using ArrayAccess: 635 | 636 | ```php 637 | getConfig('database'); 641 | $manager = new Illuminate\Database\Capsule\Manager; 642 | 643 | $manager->addConnection([ 644 | 'driver' => 'mysql', 645 | 'host' => $config['host'], 646 | 'database' => $config['name'], 647 | 'username' => $config['user'], 648 | 'password' => $config['pass'], 649 | 'charset' => 'utf8', 650 | 'collation' => 'utf8_unicode_ci' 651 | ], 'default'); 652 | 653 | $manager->setAsGlobal(); 654 | 655 | return $manager; 656 | }; 657 | ``` 658 | 659 | or by container access: 660 | 661 | ```php 662 | getContainer()->share('db', function () use($app) { 665 | $config = $app->getConfig('database'); 666 | $manager = new Illuminate\Database\Capsule\Manager; 667 | 668 | $manager->addConnection([ 669 | 'driver' => 'mysql', 670 | 'host' => $config['db_host'], 671 | 'database' => $config['db_name'], 672 | 'username' => $config['db_user'], 673 | 'password' => $config['db_pass'], 674 | 'charset' => 'utf8', 675 | 'collation' => 'utf8_unicode_ci' 676 | ], 'default'); 677 | 678 | $manager->setAsGlobal(); 679 | 680 | return $manager; 681 | }); 682 | ``` 683 | 684 | Multitons can be added using the `add` method on the container: 685 | 686 | ```php 687 | getContainer()->add('foo', function () { 691 | return new Foo(); 692 | }); 693 | ``` 694 | 695 | Service providers can be registered using the `register` method on the Hawkbit\Application app or `addServiceProvider` on the container: 696 | 697 | ```php 698 | register('\My\Service\Provider'); 701 | $app->getContainer()->addServiceProvider('\My\Service\Provider'); 702 | ``` 703 | 704 | For more information about service providers check out this page - [http://container.thephpleague.com/service-providers/](http://container.thephpleague.com/service-providers/). 705 | 706 | For easy testing down the road it is recommended you embrace constructor injection: 707 | 708 | ```php 709 | getContainer()->add('Bar', function () { 712 | return new Bar(); 713 | }); 714 | 715 | $app->getContainer()->add('Foo', function () use ($app) { 716 | return new Foo( 717 | $app->getContainer()->get('Bar') 718 | ); 719 | }); 720 | ``` 721 | 722 | ### Container 723 | 724 | Set your own container needs an instance of `\League\Container\ContainerInterface` 725 | 726 | ```php 727 | setContainer($container); 730 | ``` 731 | 732 | Get container 733 | 734 | ```php 735 | getContainer(); 738 | ``` 739 | 740 | ## Services 741 | 742 | Hawkbit\Application uses dependency injection container to access services. Following integrations can be exchanged. 743 | 744 | ### Configurator 745 | 746 | Uses in `Application::setConfig()`,`Application::getConfig()` and `Application::hasConfig()` 747 | 748 | ```php 749 | getConfigurator(); 752 | ``` 753 | 754 | ```php 755 | getContainer()->share(\Zend\Config\Config::class, new \Zend\Config\Config([], true)); 758 | ``` 759 | 760 | ### error handler 761 | 762 | ```php 763 | getContainer()->share(\Whoops\Run::class, new \Whoops\Run()); 766 | ``` 767 | 768 | ```php 769 | getErrorHandler(); 772 | ``` 773 | 774 | ### error response handler 775 | 776 | ```php 777 | getContainer()->share(\Whoops\Handler\HandlerInterface::class, Acme\ErrorResponseHandler::class); 780 | ``` 781 | 782 | ```php 783 | getErrorResponseHandler(); 786 | ``` 787 | 788 | ### psr logger 789 | 790 | Get a new logger instance by channel name 791 | 792 | ```php 793 | getContainer()->add(\Psr\Log\LoggerInterface::class, \Monolog\Logger::class); 796 | ``` 797 | 798 | ```php 799 | getLogger('channel name'); 802 | ``` 803 | 804 | Get a list of available logger channels 805 | 806 | ```php 807 | getLoggerChannels(); 810 | ``` 811 | 812 | ### psr server request 813 | 814 | ```php 815 | getContainer()->share(\Psr\Http\Message\ServerRequestInterface::class, \Zend\Diactoros\ServerRequestFactory::fromGlobals()); 818 | ``` 819 | 820 | ```php 821 | getRequest(); 824 | ``` 825 | 826 | ### psr response 827 | 828 | ```php 829 | getContainer()->add(\Psr\Http\Message\ResponseInterface::class, \Zend\Diactoros\Response::class); 832 | ``` 833 | 834 | ```php 835 | getRequest(); 838 | ``` 839 | 840 | ### response emitter 841 | 842 | ```php 843 | getContainer()->share(\Zend\Diactoros\Response\EmitterInterface::class, \Zend\Diactoros\Response\SapiEmitter::class); 846 | ``` 847 | 848 | ```php 849 | getResponseEmitter(); 852 | ``` 853 | 854 | 855 | ## Change log 856 | 857 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 858 | 859 | ## Testing 860 | 861 | ``` bash 862 | $ composer test 863 | ``` 864 | 865 | ## Contributing 866 | 867 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 868 | 869 | ## Security 870 | 871 | If you discover any security related issues, please email instead of using the issue tracker. 872 | 873 | ## Credits 874 | 875 | - [Marco Bunge](https://github.com/mbunge) 876 | - [Alex Bilbie](https://github.com/alexbilbie) (Proton) 877 | - [All contributors](https://github.com/hawkbit/hawkbit/graphs/contributors) 878 | 879 | ## License 880 | 881 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 882 | 883 | [ico-version]: https://img.shields.io/packagist/v/hawkbit/hawkbit.svg?style=flat-square 884 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 885 | [ico-travis]: https://img.shields.io/travis/HawkBitPhp/hawkbit/master.svg?style=flat-square 886 | [ico-downloads]: https://img.shields.io/packagist/dt/hawkbit/hawkbit.svg?style=flat-square 887 | [ico-coveralls]: https://img.shields.io/coveralls/HawkBitPhp/hawkbit/master.svg?style=flat-square 888 | 889 | [link-packagist]: https://packagist.org/packages/hawkbit/hawkbit 890 | [link-travis]: https://travis-ci.org/HawkBitPhp/hawkbit 891 | [link-downloads]: https://packagist.org/packages/hawkbit/hawkbit 892 | [link-author]: https://github.com/mbunge 893 | [link-contributors]: ../../contributors 894 | [link-coveralls]: https://coveralls.io/github/HawkBitPhp/hawkbit 895 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hawkbit/hawkbit", 3 | "type": "library", 4 | "description": "PSR-7 Micro PHP framework", 5 | "keywords": [ 6 | "framework", 7 | "StackPHP", 8 | "HttpKernelInterface", 9 | "PSR-7", 10 | "middleware", 11 | "micro", 12 | "Strategility", 13 | "Hawkbit" 14 | ], 15 | "homepage": "https://github.com/Hawkbit", 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Marco Bunge", 20 | "email": "marco_bunge@web.de" 21 | }, 22 | { 23 | "name": "Alex Bilbie", 24 | "email": "hello@alexbilbie.com" 25 | } 26 | ], 27 | "require": { 28 | "php": ">=5.5.9", 29 | "filp/whoops": "^2.1", 30 | "league/route": "~3.0", 31 | "league/container": "^2.4", 32 | "league/event": "^2.1", 33 | "league/climate": "^3.2", 34 | "monolog/monolog": "^1.22", 35 | "zendframework/zend-config": "^2.6", 36 | "zendframework/zend-diactoros": "^1.4" 37 | }, 38 | "require-dev": { 39 | "phpunit/phpunit": "~4.8", 40 | "symfony/http-kernel": "~2.7|~3.0", 41 | "symfony/psr-http-message-bridge": "*", 42 | "zendframework/zend-stratigility": "~1.1.2|~1.3.1" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Hawkbit\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Hawkbit\\Tests\\": "tests/" 52 | }, 53 | "files": [ 54 | "tests/TestAsset/Functions.php", 55 | "tests/TestAsset/SapiResponse.php" 56 | ] 57 | }, 58 | "provide": { 59 | "psr/container-implementation": "~1.0", 60 | "psr/http-message-implementation": "~1.0", 61 | "psr/log-implementation": "~1.0" 62 | }, 63 | "suggest": { 64 | "symfony/http-kernel": "Dependency of symfony http foundation adapter", 65 | "symfony/psr-http-message-bridge": "Dependency of symfony http foundation adapter", 66 | "zendframework/zend-stratigility": "Dependency of stratigility middleware adapter" 67 | }, 68 | "scripts": { 69 | "test": [ 70 | "cd vendor/phpunit/phpunit", 71 | "phpunit --configuration phpunit.xml" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | ./tests/Stubs 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # Some hosts may require you to use the `RewriteBase` directive. 4 | # If you need to use the `RewriteBase` directive, it should be the 5 | # absolute physical path to the directory that contains this htaccess file. 6 | # 7 | RewriteBase / 8 | 9 | # Disallow set PHPSESSID 10 | RewriteCond %{QUERY_STRING} PHPSESSID=.*$ 11 | RewriteRule .* %{REQUEST_URI}? [R=301,L] 12 | 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteRule ^ index.php [QSA,L] 16 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | get('/', function (ServerRequestInterface $request, ResponseInterface $response) { 11 | $response->getBody()->write('

Hello, World!

'); 12 | 13 | return $response; 14 | }); 15 | 16 | $app->run(); -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit; 13 | 14 | use Hawkbit\Application\Init\InitHaltHookTrait; 15 | use Hawkbit\Application\AbstractApplication; 16 | use Hawkbit\Application\Init\InitConfigurationTrait; 17 | use Hawkbit\Application\MiddlewareAwareInterface; 18 | use Hawkbit\Application\Providers\MonologServiceProvider; 19 | use Hawkbit\Application\Providers\WhoopsServiceProvider; 20 | use Hawkbit\Application\TerminableInterface; 21 | use League\Container\ReflectionContainer; 22 | use League\Container\ServiceProvider\ServiceProviderInterface; 23 | use League\Route\Http\Exception\NotFoundException; 24 | use League\Route\Middleware\ExecutionChain; 25 | use League\Route\RouteCollection; 26 | use League\Route\RouteCollectionInterface; 27 | use League\Route\RouteCollectionMapTrait; 28 | use Psr\Http\Message\ResponseInterface; 29 | use Psr\Http\Message\ServerRequestInterface; 30 | use Hawkbit\Application\HttpApplicationEvent; 31 | use Zend\Diactoros\Response; 32 | use Zend\Diactoros\ServerRequestFactory; 33 | 34 | /** 35 | * Hawkbit Application Class. 36 | */ 37 | final class Application extends AbstractApplication 38 | implements RouteCollectionInterface, TerminableInterface, MiddlewareAwareInterface 39 | { 40 | 41 | use RouteCollectionMapTrait; 42 | use InitConfigurationTrait; 43 | use InitHaltHookTrait; 44 | 45 | /** 46 | * @var \League\Route\RouteCollection 47 | */ 48 | protected $router; 49 | 50 | /** 51 | * @var \Zend\Diactoros\Response\EmitterInterface 52 | */ 53 | protected $responseEmitter; 54 | 55 | /** 56 | * Flag which is forcing response termination after 57 | * handling request / response lifecycle 58 | * 59 | * Response always destroyed before throwing exceptions! 60 | * 61 | * @var bool 62 | */ 63 | private $terminate = true; 64 | 65 | /** 66 | * Flag which forces response emitting and ignores already sended output 67 | * 68 | * @var bool 69 | */ 70 | private $forceResponseEmitting = false; 71 | 72 | /** 73 | * Get content type of current request or response 74 | * 75 | * @var string 76 | */ 77 | private $contentType = 'text/html'; 78 | 79 | /** 80 | * @var string 81 | */ 82 | protected $applicationEventClass = HttpApplicationEvent::class; 83 | 84 | /** 85 | * New Application. 86 | * 87 | * @param bool|array $configuration Enable debug mode 88 | * @param ServiceProviderInterface[] $defaultProviders 89 | */ 90 | public function __construct($configuration = [], array $defaultProviders = [ 91 | MonologServiceProvider::class, 92 | WhoopsServiceProvider::class 93 | ]) 94 | { 95 | $this->init($configuration); 96 | 97 | foreach ($defaultProviders as $provider){ 98 | $this->getContainer()->addServiceProvider($provider); 99 | } 100 | } 101 | 102 | /** 103 | * @param $configuration 104 | */ 105 | public function init($configuration = []) 106 | { 107 | $this->initConfiguration($configuration); 108 | $this->initContentType(); 109 | $this->initHaltHooks(); 110 | } 111 | 112 | /******************************************* 113 | * 114 | * Middleware 115 | * 116 | */ 117 | 118 | /** 119 | * Add a middleware 120 | * 121 | * @param callable $middleware 122 | */ 123 | public function addMiddleware(callable $middleware) 124 | { 125 | $this->getRouter()->middleware($middleware); 126 | } 127 | 128 | /** 129 | * @return callable[] 130 | */ 131 | public function getMiddlewares() 132 | { 133 | return $this->getRouter()->getMiddlewareStack(); 134 | } 135 | 136 | /******************************************* 137 | * 138 | * GETTER / SETTER 139 | * 140 | */ 141 | 142 | /** 143 | * Get the Emitter. 144 | * 145 | * @deprecated 146 | * @return \League\Event\EmitterInterface 147 | */ 148 | public function getEmitter() 149 | { 150 | return $this->getEventEmitter(); 151 | } 152 | 153 | /** 154 | * Return the router. 155 | * 156 | * @return \League\Route\RouteCollection 157 | */ 158 | public function getRouter() 159 | { 160 | if (!isset($this->router)) { 161 | $container = clone $this->getContainer(); 162 | $container->delegate(new ReflectionContainer); 163 | $this->router = (new RouteCollection($container)); 164 | } 165 | 166 | /** @var RouteCollection $router */ 167 | $router = $this->validateContract($this->router, RouteCollection::class); 168 | return $router; 169 | } 170 | 171 | /** 172 | * Get the request 173 | * 174 | * @return \Psr\Http\Message\ServerRequestInterface 175 | */ 176 | public function getRequest() 177 | { 178 | if (!$this->getContainer()->has(ServerRequestInterface::class)) { 179 | $this->getContainer()->share(ServerRequestInterface::class,function(){ 180 | $beforeIndexPosition = strpos($_SERVER['PHP_SELF'],'/index.php'); 181 | /** 182 | * If there is some string before /index.php then remove this string from REQUEST_URI 183 | */ 184 | if(false !== $beforeIndexPosition && $beforeIndexPosition > 0){ 185 | $scriptUrl = substr($_SERVER['PHP_SELF'],0,$beforeIndexPosition); 186 | $_SERVER['REQUEST_URI'] = str_replace(['/index.php',$scriptUrl],'',$_SERVER['REQUEST_URI']); 187 | } 188 | return ServerRequestFactory::fromGlobals()->withHeader('content-type', $this->getContentType()); 189 | }); 190 | } 191 | 192 | /** @var ServerRequestInterface $request */ 193 | $request = $this->validateContract($this->getContainer()->get(ServerRequestInterface::class), 194 | ServerRequestInterface::class); 195 | return $request; 196 | } 197 | 198 | /** 199 | * Get the response 200 | * 201 | * @param string $content 202 | * @return \Psr\Http\Message\ResponseInterface 203 | */ 204 | public function getResponse($content = '') 205 | { 206 | //transform content by content type 207 | if ($this->isJsonRequest()) { 208 | if ($content instanceof Response\JsonResponse) { 209 | $content = json_decode($content->getBody()); 210 | } elseif (!is_array($content)) { 211 | $content = [$content]; 212 | } 213 | } else { 214 | if (is_array($content)) { 215 | $content = implode('', $content); 216 | } elseif ($content instanceof ResponseInterface) { 217 | $content = $content->getBody()->__toString(); 218 | } 219 | } 220 | 221 | if (!$this->getContainer()->has(ResponseInterface::class)) { 222 | if ($this->isCli()) { 223 | $class = Response\TextResponse::class; 224 | } elseif ($this->isJsonRequest()) { 225 | $class = Response\JsonResponse::class; 226 | } else { 227 | $class = Response\HtmlResponse::class; 228 | } 229 | $this->getContainer()->add(ResponseInterface::class, $class); 230 | } 231 | 232 | /** @var ResponseInterface $response */ 233 | $response = $this->validateContract($this->getContainer()->get(ResponseInterface::class, [$content]), 234 | ResponseInterface::class); 235 | 236 | //inject request content type 237 | $request = $this->getRequest(); 238 | 239 | $contentTypeKey = 'content-type'; 240 | foreach ($request->getHeader($contentTypeKey) as $contentType) { 241 | $response = $response->withHeader($contentTypeKey, $contentType); 242 | } 243 | 244 | return $response; 245 | } 246 | 247 | 248 | /** 249 | * Get response emitter 250 | * 251 | * @return \Zend\Diactoros\Response\EmitterInterface 252 | */ 253 | public function getResponseEmitter() 254 | { 255 | if (!$this->getContainer()->has(Response\EmitterInterface::class)) { 256 | $this->getContainer()->share(Response\EmitterInterface::class, new Response\SapiEmitter()); 257 | } 258 | 259 | /** @var Response\EmitterInterface $contract */ 260 | $contract = $this->validateContract($this->getContainer()->get(Response\EmitterInterface::class), 261 | Response\EmitterInterface::class); 262 | return $contract; 263 | } 264 | 265 | /** 266 | * @return string 267 | */ 268 | public function getContentType() 269 | { 270 | return $this->contentType; 271 | } 272 | 273 | /** 274 | * @param string $contentType 275 | * @return Application 276 | */ 277 | public function setContentType($contentType) 278 | { 279 | $this->contentType = $contentType; 280 | return $this; 281 | } 282 | 283 | /** 284 | * Check if request is a ajax request 285 | * 286 | * @return bool 287 | */ 288 | public function isAjaxRequest() 289 | { 290 | return 291 | false !== strpos( 292 | strtolower(ServerRequestFactory::getHeader('x-requested-with', $this->getRequest()->getHeaders(), '')), 293 | 'xmlhttprequest' 294 | ) && $this->isHttpRequest(); 295 | } 296 | 297 | /** 298 | * Check if request is a ajax request 299 | * 300 | * @return bool 301 | */ 302 | public function isJsonRequest() 303 | { 304 | return 305 | false !== strpos( 306 | $this->getContentType(), 307 | 'json' 308 | ) && $this->isHttpRequest(); 309 | } 310 | 311 | /** 312 | * Check if request is a ajax request 313 | * 314 | * @return bool 315 | */ 316 | public function isSoapRequest() 317 | { 318 | return 319 | false !== strpos( 320 | $this->getContentType(), 321 | 'soap' 322 | ) && $this->isHttpRequest(); 323 | } 324 | 325 | /** 326 | * Check if request is a ajax request 327 | * 328 | * @return bool 329 | */ 330 | public function isXmlRequest() 331 | { 332 | return 333 | false !== strpos( 334 | $this->getContentType(), 335 | 'xml' 336 | ) && $this->isHttpRequest(); 337 | } 338 | 339 | /** 340 | * Check that terminate the response request 341 | * lifecycle is enabled 342 | * 343 | * @return boolean 344 | */ 345 | public function canTerminate() 346 | { 347 | return $this->terminate; 348 | } 349 | 350 | /** 351 | * Set terminating flag 352 | * 353 | * @param boolean $terminate 354 | * @return $this 355 | */ 356 | public function setCanTerminate($terminate) 357 | { 358 | $this->terminate = $terminate; 359 | return $this; 360 | } 361 | 362 | /** 363 | * @return boolean 364 | */ 365 | public function canForceResponseEmitting() 366 | { 367 | return $this->forceResponseEmitting; 368 | } 369 | 370 | /** 371 | * @param boolean $forceResponseEmitting 372 | * @return $this 373 | */ 374 | public function setForceResponseEmitting($forceResponseEmitting) 375 | { 376 | $this->forceResponseEmitting = $forceResponseEmitting; 377 | return $this; 378 | } 379 | 380 | /******************************************* 381 | * 382 | * ROUTER 383 | * 384 | */ 385 | 386 | /** 387 | * Add a route to the map. 388 | * 389 | * @param $method 390 | * @param $route 391 | * @param $action 392 | * 393 | * @return \League\Route\Route 394 | */ 395 | public function map($method, $route, $action) 396 | { 397 | return $this->getRouter()->map($method, $route, $this->bindClosureToInstance($action, $this)); 398 | } 399 | 400 | /** 401 | * Add a group of routes to the collection. Binds $this to app instance 402 | * 403 | * @param $prefix 404 | * @param callable $group 405 | * 406 | * @return \League\Route\RouteGroup 407 | */ 408 | public function group($prefix, callable $group) 409 | { 410 | return $this->getRouter()->group($prefix, $this->bindClosureToInstance($group, $this)); 411 | } 412 | 413 | /******************************************* 414 | * 415 | * LIFECYCLE INVOCATION 416 | * 417 | */ 418 | 419 | /** 420 | * Convert request into response. If an error occurs, Hawkbit tries 421 | * to handle error as response. 422 | * 423 | * @param ServerRequestInterface $request 424 | * @param ResponseInterface $response 425 | * @param bool $catch 426 | * 427 | * @return ResponseInterface 428 | * 429 | * @throws \Throwable 430 | */ 431 | public function handle( 432 | ServerRequestInterface $request, 433 | ResponseInterface $response = null, 434 | $catch = self::DEFAULT_ERROR_CATCH 435 | ) 436 | { 437 | /** @var ResponseInterface $response */ 438 | // Passes the request to the container 439 | if (!$this->getContainer()->has(ServerRequestInterface::class)) { 440 | $this->getContainer()->share(ServerRequestInterface::class, $request); 441 | } 442 | 443 | //inject request content type 444 | $contentType = ServerRequestFactory::getHeader('content-type', $this->getRequest()->getHeaders(), ''); 445 | $this->setContentType($contentType); 446 | 447 | if ($response === null) { 448 | $response = $this->getResponse(); 449 | } 450 | 451 | $applicationEvent = $this->getApplicationEvent(); 452 | $applicationEvent->setRequest($request); 453 | $applicationEvent->setResponse($response); 454 | 455 | try { 456 | $response = $this->handleResponse($this->handleRequest($request), $response); 457 | }catch (\Exception $exception){ 458 | $notFoundException = $exception instanceof NotFoundException; 459 | $response = $this->handleError($exception, $request, $response, $catch) 460 | ->withStatus($notFoundException ? 404 : 500); 461 | } 462 | 463 | // validate response 464 | $response = $this->validateContract($response, ResponseInterface::class); 465 | 466 | // update content type 467 | $this->setContentType(ServerRequestFactory::getHeader('content-type', $response->getHeaders(), '')); 468 | 469 | return $response; 470 | } 471 | 472 | /** 473 | * Convert request into response. 474 | * 475 | * @param ServerRequestInterface $request 476 | * @return ServerRequestInterface 477 | */ 478 | public function handleRequest(ServerRequestInterface $request) 479 | { 480 | $applicationEvent = $this->getApplicationEvent(); 481 | $applicationEvent->setName(self::EVENT_REQUEST_RECEIVED); 482 | $this->emit($applicationEvent, $request); 483 | 484 | return $applicationEvent->getRequest(); 485 | } 486 | 487 | /** 488 | * Handle Response 489 | * 490 | * @param ServerRequestInterface $request 491 | * @param ResponseInterface $response 492 | * @return ResponseInterface 493 | */ 494 | public function handleResponse(ServerRequestInterface $request, ResponseInterface $response) 495 | { 496 | $applicationEvent = $this->getApplicationEvent(); 497 | $applicationEvent->setName(self::EVENT_RESPONSE_CREATED); 498 | $applicationEvent->setResponse($this->getRouter()->dispatch( 499 | $request, 500 | $response 501 | )); 502 | 503 | $this->emit($applicationEvent, $request, $response); 504 | 505 | return $applicationEvent->getResponse(); 506 | } 507 | 508 | /** 509 | * Handle error and return response of error message or throw 510 | * error if error.catch is disabled. 511 | * 512 | * @param \Throwable|\Exception $exception 513 | * @param \Psr\Http\Message\ServerRequestInterface $request 514 | * @param ResponseInterface $response 515 | * @param bool $catch 516 | * 517 | * @return \Psr\Http\Message\ResponseInterface 518 | * 519 | * @throws string 520 | */ 521 | public function handleError( 522 | $exception, 523 | ServerRequestInterface $request, 524 | ResponseInterface $response, 525 | $catch = self::DEFAULT_ERROR_CATCH 526 | ) 527 | { 528 | // notify app that an error occurs 529 | $this->error = true; 530 | 531 | $errorHandler = $this->getErrorHandler(); 532 | 533 | // if delivered value of $catch, then configured value, then default value 534 | $catch = self::DEFAULT_ERROR_CATCH !== $catch ? $catch : $this->getConfig(self::KEY_ERROR_CATCH, $catch); 535 | 536 | $showError = $this->getConfig(self::KEY_ERROR, static::DEFAULT_ERROR); 537 | 538 | $this->pushException($errorHandler->decorateException($exception)); 539 | 540 | //execute application middlewares 541 | try { 542 | $middlewares = $this->getRouter()->getMiddlewareStack(); 543 | $middlewareRunner = new ExecutionChain(); 544 | foreach ($middlewares as $middleware) { 545 | $middlewareRunner->middleware($middleware); 546 | } 547 | 548 | $response = $middlewareRunner->execute($request, $response); 549 | }catch (\Exception $e){ 550 | $this->pushException($e); 551 | } 552 | 553 | // get last occured exception 554 | $exception = $this->getLastException(); 555 | 556 | $message = $errorHandler->getErrorMessage($exception); 557 | 558 | $errorResponse = $this->determineErrorResponse($exception, $message, $response, $request); 559 | 560 | if ( 561 | false === $catch 562 | && true === $showError 563 | ) { 564 | $this->throwException($exception); 565 | } 566 | 567 | return $errorResponse; 568 | } 569 | 570 | /** 571 | * Handle response / request lifecycle 572 | * 573 | * When $callable is a valid callable, callable will executed before emit response 574 | * 575 | * @param \Psr\Http\Message\ServerRequestInterface $request A Request instance 576 | * @param \Psr\Http\Message\ResponseInterface $response A response instance 577 | * 578 | * @return $this 579 | * 580 | */ 581 | public function run(ServerRequestInterface $request = null, ResponseInterface $response = null) 582 | { 583 | if ($request === null) { 584 | $request = $this->getRequest(); 585 | } 586 | $response = $this->handle($request, $response); 587 | 588 | $this->emitResponse($request, $response); 589 | 590 | if ($this->canTerminate()) { 591 | $this->terminate($request, $response); 592 | } 593 | 594 | $this->shutdown($response); 595 | 596 | return $this; 597 | } 598 | 599 | /** 600 | * Emit a response 601 | * 602 | * @param \Psr\Http\Message\ServerRequestInterface $request 603 | * @param \Psr\Http\Message\ResponseInterface $response 604 | * @throws \Exception 605 | */ 606 | public function emitResponse(ServerRequestInterface $request, ResponseInterface $response) 607 | { 608 | try { 609 | $this->getResponseEmitter()->emit($response); 610 | } catch (\Exception $e) { 611 | if ($this->canForceResponseEmitting()) { 612 | // flush buffers 613 | $maxBufferLevel = ob_get_level(); 614 | 615 | while (ob_get_level() > $maxBufferLevel) { 616 | ob_end_flush(); 617 | } 618 | 619 | // print response 620 | echo $response->getBody(); 621 | } else { 622 | throw $e; 623 | } 624 | } 625 | $applicationEvent = $this->getApplicationEvent(); 626 | $applicationEvent->setName(self::EVENT_RESPONSE_SENT); 627 | $applicationEvent->setRequest($request); 628 | $applicationEvent->setResponse($response); 629 | $this->emit($applicationEvent); 630 | } 631 | 632 | /** 633 | * Terminates a request/response cycle. 634 | * 635 | * @param \Psr\Http\Message\ServerRequestInterface $request 636 | * @param \Psr\Http\Message\ResponseInterface $response 637 | * 638 | * @return void 639 | */ 640 | public function terminate(ServerRequestInterface $request, ResponseInterface $response) 641 | { 642 | $applicationEvent = $this->getApplicationEvent(); 643 | $applicationEvent->setRequest($request); 644 | $applicationEvent->setResponse($response); 645 | $applicationEvent->setName(self::EVENT_LIFECYCLE_COMPLETE); 646 | $this->emit($applicationEvent); 647 | 648 | } 649 | 650 | /** 651 | * Finish request. Collect garbage and terminate output buffering 652 | * 653 | * @param \Psr\Http\Message\ResponseInterface $response 654 | * @return void 655 | */ 656 | public function shutdown($response = null) 657 | { 658 | 659 | $this->collectGarbage(); 660 | $applicationEvent = $this->getApplicationEvent(); 661 | $applicationEvent->setResponse($response); 662 | $applicationEvent->setName(self::EVENT_SYSTEM_SHUTDOWN); 663 | $this->emit($applicationEvent, $this->terminateOutputBuffering(1, $response)); 664 | } 665 | 666 | /** 667 | * Close response stream and terminate output buffering 668 | * 669 | * @param int $level 670 | * @param null|\Psr\Http\Message\ResponseInterface $response 671 | * @return array 672 | */ 673 | public function terminateOutputBuffering($level = 0, $response = null) 674 | { 675 | 676 | // close response stream before terminating output buffer 677 | // and only if response is an instance of 678 | // \Psr\Http\ResponseInterface 679 | if ($response instanceof ResponseInterface) { 680 | $body = $response->getBody(); 681 | if ($body->isReadable()) { 682 | $body->close(); 683 | } 684 | } 685 | 686 | // Command line output buffering is disabled in cli by default 687 | if ($this->isCli()) { 688 | return []; 689 | } 690 | 691 | // $level needs to be a numeric value 692 | if (!is_numeric($level)) { 693 | $level = 0; 694 | } 695 | 696 | // force type casting to an integer value 697 | if (!is_int($level)) { 698 | $level = (int)$level; 699 | } 700 | 701 | // avoid infinite loop on clearing 702 | // output buffer by set level to 0 703 | // if $level is smaller 704 | if (-1 > $level) { 705 | $level = 0; 706 | } 707 | 708 | // terminate all output buffers until $level is 0 or desired level 709 | // collect all contents and return 710 | $content = []; 711 | while (ob_get_level() > $level) { 712 | $content[] = ob_get_clean(); 713 | } 714 | return $content; 715 | } 716 | 717 | /** 718 | * Perform garbage collection 719 | */ 720 | public function collectGarbage() 721 | { 722 | // try to enable garbage collection 723 | if (!gc_enabled()) { 724 | @gc_enable(); 725 | } 726 | 727 | // collect garbage only if garbage 728 | // collection is enabled 729 | if (gc_enabled()) { 730 | gc_collect_cycles(); 731 | } 732 | } 733 | 734 | /** 735 | * Perform garbage collection 736 | * @deprecated 737 | */ 738 | public function cleanUp() 739 | { 740 | return $this->collectGarbage(); 741 | } 742 | 743 | /** 744 | * Determines response for error. Emits `lifecycle.error` event. 745 | * 746 | * @param \Throwable $exception 747 | * @param string $message 748 | * @param ResponseInterface $response 749 | * @param ServerRequestInterface $request 750 | * @return ResponseInterface 751 | */ 752 | private function determineErrorResponse( 753 | $exception, 754 | $message, 755 | ResponseInterface $response, 756 | ServerRequestInterface $request 757 | ) 758 | { 759 | $applicationEvent = $this->getApplicationEvent(); 760 | $applicationEvent->setName(self::EVENT_LIFECYCLE_ERROR); 761 | $applicationEvent->setRequest($request); 762 | $applicationEvent->setResponse($response); 763 | $applicationEvent->setErrorResponse($this->getResponse()); 764 | 765 | $this->emit($applicationEvent, $exception); 766 | 767 | $errorResponse = $applicationEvent->getErrorResponse(); 768 | 769 | if (!$errorResponse->getBody()->isWritable()) { 770 | return $errorResponse; 771 | } 772 | 773 | $content = $errorResponse->getBody()->__toString(); 774 | if (empty($content)) { 775 | $errorResponse->getBody()->write($message); 776 | } 777 | 778 | return $errorResponse; 779 | } 780 | 781 | /** 782 | * @return HttpApplicationEvent 783 | */ 784 | public function getApplicationEvent() 785 | { 786 | /** @var HttpApplicationEvent $applicationEvent */ 787 | $applicationEvent = parent::getApplicationEvent(); 788 | return $applicationEvent; 789 | } 790 | 791 | /** 792 | * 793 | */ 794 | protected function initContentType() 795 | { 796 | // configure request content type 797 | $this->setContentType(ServerRequestFactory::getHeader('content-type', ServerRequestFactory::fromGlobals()->getHeaders(), $this->getContentType())); 798 | } 799 | 800 | } 801 | -------------------------------------------------------------------------------- /src/Application/AbstractApplication.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Daniyal Hamid (@Designcise) 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit\Application; 13 | 14 | use Hawkbit\Application\Services\WhoopsService; 15 | use Hawkbit\Configuration; 16 | use League\Container\Container; 17 | use League\Container\ContainerAwareInterface; 18 | use League\Container\ContainerInterface; 19 | use League\Event\Emitter; 20 | use League\Event\EmitterInterface; 21 | use League\Event\EmitterTrait; 22 | use League\Event\ListenerAcceptorInterface; 23 | use Monolog\Logger; 24 | use Psr\Log\LoggerInterface; 25 | 26 | abstract class AbstractApplication implements ApplicationInterface, ContainerAwareInterface, ListenerAcceptorInterface, \ArrayAccess 27 | { 28 | use EmitterTrait; 29 | 30 | /** 31 | * Set while handle exception. 32 | * @var bool 33 | */ 34 | protected $error = false; 35 | 36 | /** 37 | * @var array 38 | */ 39 | protected $loggers = []; 40 | 41 | /** 42 | * @var ContainerInterface 43 | */ 44 | protected $container; 45 | 46 | /** 47 | * @var ApplicationEvent 48 | */ 49 | protected $applicationEvent; 50 | 51 | /** 52 | * @var string 53 | */ 54 | protected $applicationEventClass = ApplicationEvent::class; 55 | 56 | /** 57 | * @var \Exception[]|\Throwable[] 58 | */ 59 | protected $exceptionStack = []; 60 | 61 | /** IOC */ 62 | 63 | /** 64 | * Set a container. 65 | * 66 | * @param \League\Container\ContainerInterface $container 67 | * @return $this 68 | */ 69 | public function setContainer(ContainerInterface $container) 70 | { 71 | $application = $this; 72 | $container->share(ApplicationInterface::class, $application); 73 | $container->share(\Interop\Container\ContainerInterface::class, $container); 74 | 75 | $this->container = $container; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Get the container. 82 | * 83 | * @return \League\Container\Container|\League\Container\ContainerInterface 84 | */ 85 | public function getContainer() 86 | { 87 | if (!isset($this->container)) { 88 | $this->setContainer(new Container); 89 | } 90 | 91 | return $this->container; 92 | } 93 | 94 | 95 | /** 96 | * Array Access get. 97 | * 98 | * @param string $key 99 | * 100 | * @return mixed 101 | */ 102 | public function offsetGet($key) 103 | { 104 | return $this->getContainer()->get($key); 105 | } 106 | 107 | /** 108 | * Array Access set. 109 | * 110 | * @param string $key 111 | * @param mixed $value 112 | * 113 | * @return void 114 | */ 115 | public function offsetSet($key, $value) 116 | { 117 | $this->getContainer()->share($key, $value); 118 | } 119 | 120 | /** 121 | * Removing services are not support by 122 | * `league/container` 2.0 and greater 123 | * 124 | * @param string $key 125 | * 126 | * @return void 127 | */ 128 | public function offsetUnset($key) 129 | { 130 | } 131 | 132 | /** 133 | * Array Access isset. 134 | * 135 | * @param string $key 136 | * 137 | * @return bool 138 | */ 139 | public function offsetExists($key) 140 | { 141 | return $this->getContainer()->has($key); 142 | } 143 | 144 | /** 145 | * Register a new service provider 146 | * 147 | * @param $serviceProvider 148 | */ 149 | public function register($serviceProvider) 150 | { 151 | $this->getContainer()->addServiceProvider($serviceProvider); 152 | } 153 | 154 | /** Config */ 155 | 156 | /** 157 | * Get configuration container 158 | * 159 | * @return \Hawkbit\Configuration 160 | * 161 | */ 162 | public function getConfigurator() 163 | { 164 | if (!$this->getContainer()->has(Configuration::class)) { 165 | $this->getContainer()->share(Configuration::class, (new Configuration([], true))); 166 | } 167 | 168 | return $this->getContainer()->get(Configuration::class); 169 | } 170 | 171 | 172 | /** 173 | * Set a config item. Add recursive if key is traversable. 174 | * 175 | * @param string|array|\Traversable $key 176 | * @param mixed $value 177 | * 178 | * @return $this 179 | */ 180 | public function setConfig($key, $value = null) 181 | { 182 | $configurator = $this->getConfigurator(); 183 | if(!is_scalar($key)){ 184 | $configuratorClass = get_class($configurator); 185 | $configurator->merge(new $configuratorClass($key, true)); 186 | }else{ 187 | $configurator[$key] = $value; 188 | } 189 | 190 | return $this; 191 | } 192 | 193 | /** 194 | * Get a config key's value 195 | * 196 | * @param string $key 197 | * @param mixed $default 198 | * 199 | * @return mixed 200 | */ 201 | public function getConfig($key = null, $default = null) 202 | { 203 | 204 | $configurator = $this->getConfigurator(); 205 | if (null === $key) { 206 | return $configurator; 207 | } 208 | 209 | return $configurator->get($key, $default); 210 | } 211 | 212 | /** 213 | * Check if key exists 214 | * 215 | * @param $key 216 | * 217 | * @return bool 218 | */ 219 | public function hasConfig($key) 220 | { 221 | $configurator = $this->getConfigurator(); 222 | return isset($configurator[$key]); 223 | } 224 | 225 | /** 226 | * @return \Hawkbit\Application\Services\WhoopsService 227 | */ 228 | public function getErrorHandler() 229 | { 230 | /** @var \Hawkbit\Application\Services\WhoopsService $contract */ 231 | $contract = $this->validateContract($this->getContainer()->get(WhoopsService::class), WhoopsService::class); 232 | return $contract; 233 | } 234 | 235 | /** 236 | * @return \Exception[]|\Throwable[] 237 | */ 238 | public function getExceptionStack() 239 | { 240 | return $this->exceptionStack; 241 | } 242 | 243 | /** 244 | * @param \Exception|\Throwable $exception 245 | * @return $this 246 | */ 247 | public function pushException($exception){ 248 | array_push($this->exceptionStack, $exception); 249 | return $this; 250 | } 251 | 252 | /** 253 | * @return \Exception|\Throwable 254 | */ 255 | public function getLastException(){ 256 | $exceptionStack = $this->getExceptionStack(); 257 | $exception = end($exceptionStack); 258 | reset($exception); 259 | return $exception; 260 | } 261 | 262 | /** Events */ 263 | 264 | /** 265 | * Return the event emitter. 266 | * 267 | * @return \League\Event\Emitter|\League\Event\EmitterInterface 268 | */ 269 | public function getEventEmitter() 270 | { 271 | if (!$this->getContainer()->has(EmitterInterface::class)) { 272 | $this->getContainer()->share(EmitterInterface::class, new Emitter()); 273 | } 274 | 275 | /** @var EmitterInterface $validateContract */ 276 | $validateContract = $this->validateContract($this->getContainer()->get(EmitterInterface::class), EmitterInterface::class); 277 | return $validateContract; 278 | } 279 | 280 | /** Logging */ 281 | 282 | /** 283 | * Return a logger 284 | * 285 | * @param string $channel 286 | * @return \Psr\Log\LoggerInterface 287 | */ 288 | public function getLogger($channel = 'default') 289 | { 290 | if (isset($this->loggers[$channel])) { 291 | return $this->loggers[$channel]; 292 | } 293 | 294 | /** @var Logger $logger */ 295 | $logger = $this->getContainer()->get(LoggerInterface::class, [$channel]); 296 | 297 | $this->loggers[$channel] = $logger; 298 | 299 | /** @var LoggerInterface $contract */ 300 | $contract = $this->validateContract($this->loggers[$channel], LoggerInterface::class); 301 | return $contract; 302 | } 303 | 304 | /** 305 | * Get a list of logger names 306 | * 307 | * @return string[] 308 | */ 309 | public function getLoggerChannels() 310 | { 311 | return array_keys($this->loggers); 312 | } 313 | 314 | /** 315 | * throw a exception 316 | * 317 | * @param \Throwable|\Exception $exception 318 | * 319 | * @throws \Throwable|\Exception 320 | */ 321 | public function throwException($exception) 322 | { 323 | $this->shutdown(); 324 | throw $exception; 325 | } 326 | 327 | /** 328 | * Validates that class is instance of contract 329 | * 330 | * @param $class 331 | * @param $contract 332 | * 333 | * @return string|object 334 | * 335 | * @throws \InvalidArgumentException|\LogicException 336 | */ 337 | public function validateContract($class, $contract) 338 | { 339 | $validateObject = function ($object) { 340 | //does need trigger when calling *_exists with object 341 | $condition = is_string($object) ? class_exists($object) || interface_exists($object) : is_object($object); 342 | if (false === $condition) { 343 | $this->throwException(new \InvalidArgumentException('Class not exists ' . $object)); 344 | } 345 | }; 346 | 347 | $convertClassToString = function ($object) { 348 | if (is_object($object)) { 349 | $object = get_class($object); 350 | } 351 | 352 | return is_string($object) ? $object : false; 353 | }; 354 | 355 | $validateObject($class); 356 | $validateObject($contract); 357 | 358 | if (!($class instanceof $contract)) { 359 | 360 | if (is_object($class)) { 361 | $class = get_class($class); 362 | } 363 | $this->throwException(new \LogicException($convertClassToString($class) . ' needs to be an instance of ' . $convertClassToString($contract))); 364 | } 365 | 366 | return $class; 367 | } 368 | 369 | /** 370 | * Bind any closure to application instance 371 | * 372 | * @param $closure 373 | * @param $instance 374 | * 375 | * @return mixed 376 | */ 377 | protected function bindClosureToInstance($closure, $instance) 378 | { 379 | if ($closure instanceof \Closure) { 380 | $closure = $closure->bind($closure, $instance, get_class($instance)); 381 | } 382 | 383 | return $closure; 384 | } 385 | 386 | /** 387 | * @return ApplicationEvent 388 | */ 389 | public function getApplicationEvent() 390 | { 391 | if (null === $this->applicationEvent) { 392 | $class = $this->applicationEventClass; 393 | $this->applicationEvent = new $class($this); 394 | } 395 | return $this->applicationEvent; 396 | } 397 | 398 | 399 | /** 400 | * Check server environment for cli 401 | * 402 | * @return bool 403 | */ 404 | public function isCli() 405 | { 406 | return php_sapi_name() === 'cli'; 407 | } 408 | 409 | /** 410 | * Check server environment for http 411 | * 412 | * @return bool 413 | */ 414 | public function isHttpRequest() 415 | { 416 | return !$this->isCli(); 417 | } 418 | 419 | /** 420 | * Check if an error has been occurred 421 | * 422 | * @return boolean 423 | */ 424 | public function isError() 425 | { 426 | return $this->error; 427 | } 428 | 429 | /** 430 | * Shutdown application lifecycle 431 | * 432 | * @return void 433 | */ 434 | abstract public function shutdown(); 435 | 436 | /** 437 | * Initialize Application 438 | * 439 | * @return void 440 | */ 441 | abstract public function init(); 442 | } 443 | -------------------------------------------------------------------------------- /src/Application/ApplicationEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit\Application; 13 | 14 | 15 | use League\Event\AbstractEvent; 16 | use Psr\Http\Message\ResponseInterface; 17 | use Psr\Http\Message\ServerRequestInterface; 18 | use Hawkbit\Application\ApplicationInterface; 19 | use Zend\Stdlib\ArrayObject; 20 | 21 | class ApplicationEvent extends AbstractEvent 22 | { 23 | /** 24 | * @var 25 | */ 26 | private $name; 27 | /** 28 | * @var ApplicationInterface 29 | */ 30 | private $application; 31 | 32 | /** 33 | * @var \ArrayObject 34 | */ 35 | private $paramCollection = null; 36 | 37 | /** 38 | * ApplicationEvent constructor. 39 | * @param ApplicationInterface $application 40 | */ 41 | public function __construct(ApplicationInterface $application) 42 | { 43 | $this->application = $application; 44 | $this->paramCollection = new ArrayObject(); 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getName() 51 | { 52 | return $this->name; 53 | } 54 | 55 | /** 56 | * @param mixed $name 57 | * @return $this 58 | */ 59 | public function setName($name) 60 | { 61 | $this->name = $name; 62 | return $this; 63 | } 64 | 65 | /** 66 | * @return ApplicationInterface 67 | */ 68 | public function getApplication() 69 | { 70 | return $this->application; 71 | } 72 | 73 | /** 74 | * @return \ArrayObject 75 | */ 76 | public function getParamCollection() 77 | { 78 | return $this->paramCollection; 79 | } 80 | } -------------------------------------------------------------------------------- /src/Application/ApplicationInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit\Application; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | 17 | interface ApplicationInterface 18 | { 19 | 20 | /** 21 | * This event is fired when a request is received but before it has been processed by the router. 22 | */ 23 | const EVENT_REQUEST_RECEIVED = 'request.received'; 24 | 25 | /** 26 | * This event is fired when a response has been created but before it has been output. 27 | */ 28 | const EVENT_RESPONSE_CREATED = 'response.created'; 29 | 30 | /** 31 | * This event is fired when a response has been output. 32 | */ 33 | const EVENT_RESPONSE_SENT = 'response.sent'; 34 | 35 | /** 36 | * This event is fired only when an error occurs while handling request/response lifecycle. 37 | * This event is fired after `runtime.error` 38 | */ 39 | const EVENT_LIFECYCLE_ERROR = 'lifecycle.error'; 40 | 41 | /** 42 | * This event is always fired when an error occurs. 43 | */ 44 | const EVENT_HANDLE_ERROR = 'handle.error'; 45 | 46 | /** 47 | * This event is always fired when an error occurs. 48 | */ 49 | const EVENT_SYSTEM_ERROR = 'system.error'; 50 | 51 | /** 52 | * This event is fired before completing application lifecycle. 53 | */ 54 | const EVENT_LIFECYCLE_COMPLETE = 'lifecycle.complete'; 55 | 56 | /** 57 | * This event is fired on each shutdown. 58 | */ 59 | const EVENT_SYSTEM_SHUTDOWN = 'system.shutdown'; 60 | 61 | /** 62 | * This event is fired when throw an exception. 63 | */ 64 | const EVENT_SYSTEM_EXCEPTION = 'system.exception'; 65 | 66 | const KEY_ERROR_CATCH = 'error.catch'; 67 | const KEY_ERROR = 'error'; 68 | 69 | /** 70 | * Show or hide errors 71 | */ 72 | const DEFAULT_ERROR = false; 73 | 74 | /** 75 | * catch or throw errors 76 | */ 77 | const DEFAULT_ERROR_CATCH = true; 78 | } 79 | -------------------------------------------------------------------------------- /src/Application/HttpApplicationEvent.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit\Application; 13 | 14 | 15 | use League\Event\AbstractEvent; 16 | use Psr\Http\Message\ResponseInterface; 17 | use Psr\Http\Message\ServerRequestInterface; 18 | use Hawkbit\Application\ApplicationInterface; 19 | use Zend\Stdlib\ArrayObject; 20 | 21 | class HttpApplicationEvent extends ApplicationEvent 22 | { 23 | 24 | /** 25 | * @var 26 | */ 27 | private $response; 28 | 29 | /** 30 | * @var 31 | */ 32 | private $request; 33 | 34 | /** 35 | * @var 36 | */ 37 | private $errorResponse; 38 | 39 | /** 40 | * @return ResponseInterface 41 | */ 42 | public function getResponse() 43 | { 44 | return $this->response; 45 | } 46 | 47 | /** 48 | * @param ResponseInterface $response 49 | * @return HttpApplicationEvent 50 | */ 51 | public function setResponse($response) 52 | { 53 | $this->response = $response; 54 | return $this; 55 | } 56 | 57 | /** 58 | * @return ServerRequestInterface 59 | */ 60 | public function getRequest() 61 | { 62 | return $this->request; 63 | } 64 | 65 | /** 66 | * @param ServerRequestInterface $request 67 | * @return HttpApplicationEvent 68 | */ 69 | public function setRequest($request) 70 | { 71 | $this->request = $request; 72 | return $this; 73 | } 74 | 75 | /** 76 | * @return ResponseInterface 77 | */ 78 | public function getErrorResponse() 79 | { 80 | return $this->errorResponse; 81 | } 82 | 83 | /** 84 | * @param ResponseInterface $errorResponse 85 | * @return HttpApplicationEvent 86 | */ 87 | public function setErrorResponse($errorResponse) 88 | { 89 | $this->errorResponse = $errorResponse; 90 | return $this; 91 | } 92 | } -------------------------------------------------------------------------------- /src/Application/Init/InitConfigurationTrait.php: -------------------------------------------------------------------------------- 1 | setConfig($this::KEY_ERROR, $configuration); 25 | } elseif ( 26 | is_array($configuration) || 27 | ($configuration instanceof \ArrayAccess || 28 | $configuration instanceof \Traversable) 29 | ) { 30 | $this->setConfig($configuration); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Application/Init/InitHaltHookTrait.php: -------------------------------------------------------------------------------- 1 | getApplicationEvent(); 28 | $this->emit($event->setName($this::EVENT_HANDLE_ERROR), $level, $message, $file, $line); 29 | }); 30 | 31 | // exception handler 32 | set_exception_handler(function ($exception) { 33 | /** @var $this Application|Console */ 34 | /** @var \Exception|\Throwable $exception */ 35 | // Convert throwable to exception for backwards compatibility 36 | if (!($exception instanceof \Exception)) { 37 | $throwable = $exception; 38 | $exception = new \ErrorException( 39 | $throwable->getMessage(), 40 | $throwable->getCode(), 41 | E_ERROR, 42 | $throwable->getFile(), 43 | $throwable->getLine() 44 | ); 45 | } 46 | 47 | $event = $this->getApplicationEvent(); 48 | $this->emit($event->setName($this::EVENT_SYSTEM_EXCEPTION), $exception); 49 | }); 50 | 51 | if (method_exists($this, 'shutdown')) { 52 | // shutdown function 53 | register_shutdown_function([$this, 'shutdown']); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Application/MiddlewareAwareInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | /** 13 | * Created by PhpStorm. 14 | * User: marco.bunge 15 | * Date: 04.10.2016 16 | * Time: 00:02 17 | */ 18 | 19 | namespace Hawkbit\Application; 20 | 21 | use Psr\Http\Message\ResponseInterface; 22 | use Psr\Http\Message\ServerRequestInterface; 23 | 24 | class MiddlewareRunner 25 | { 26 | 27 | /** 28 | * @var array 29 | */ 30 | private $middlewares; 31 | 32 | /** 33 | * MiddlewareRunner constructor. 34 | * @param array $middlewares 35 | */ 36 | public function __construct(array $middlewares) 37 | { 38 | $this->middlewares = $middlewares; 39 | } 40 | 41 | /** 42 | * @param $middleware 43 | * @return $this 44 | */ 45 | public function addMiddleware($middleware) 46 | { 47 | $this->middlewares[] = $middleware; 48 | return $this; 49 | } 50 | 51 | /** 52 | * Execute middlewares 53 | * 54 | * @param callable $final 55 | * @param callable|null $fail 56 | * @param array $args Arguments for middleware 57 | * @return mixed 58 | */ 59 | public function run(array $args = [], callable $final = null, callable $fail = null) 60 | { 61 | $result = null; 62 | 63 | // declare default final middleware 64 | if (!is_callable($final)) { 65 | $final = function ($command) { 66 | return $command; 67 | }; 68 | } 69 | 70 | // declare default error middleware 71 | if (!is_callable($fail)) { 72 | $fail = function ($exception) { 73 | throw $exception; 74 | }; 75 | } 76 | 77 | try { 78 | $result = $this->handle($args, $final); 79 | } catch (\Exception $exception) { 80 | $result = $this->handleError($args, $fail, $exception, $result); 81 | } 82 | 83 | return $result; 84 | } 85 | 86 | /** 87 | * Resolve middleware queue 88 | * 89 | * @param $middlewares 90 | * @return \Closure 91 | */ 92 | public function resolve($middlewares) 93 | { 94 | $last = function ($request, $response) { 95 | // no op 96 | }; 97 | while ($middleware = array_pop($middlewares)) { 98 | if (is_object($middleware)) { 99 | if (method_exists($middleware, '__invoke')) { 100 | $middleware = [$middleware, '__invoke']; 101 | } 102 | } 103 | 104 | if (!is_callable($middleware)) { 105 | throw new \InvalidArgumentException('Middle needs to be callable'); 106 | } 107 | 108 | $last = function () use ($middleware, $last) { 109 | $args = func_get_args(); 110 | $args[] = $last; 111 | return call_user_func_array($middleware, $args); 112 | }; 113 | } 114 | return $last; 115 | } 116 | 117 | /** 118 | * @param array $args 119 | * @return mixed 120 | */ 121 | public function handle(array $args, callable $final) 122 | { 123 | $middlewares = $this->middlewares; 124 | array_push($middlewares, $final); 125 | return call_user_func_array($this->resolve($middlewares), $args); 126 | } 127 | 128 | /** 129 | * @param array $args 130 | * @param callable $fail 131 | * @param \Exception|\Throwable $exception 132 | * @param $result 133 | * @return mixed 134 | */ 135 | public function handleError(array $args, callable $fail, $exception, $result) 136 | { 137 | // modify middleware args 138 | // push exception to top 139 | array_unshift($args, $exception); 140 | 141 | // push result to end 142 | $args[] = $result; 143 | 144 | // execute error middleware 145 | $result = call_user_func_array($fail, $args); 146 | return $result; 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /src/Application/Providers/MonologServiceProvider.php: -------------------------------------------------------------------------------- 1 | container property or the `getContainer` method 26 | * from the ContainerAwareTrait. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->getContainer()->add(LoggerInterface::class, Logger::class); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Application/Providers/WhoopsServiceProvider.php: -------------------------------------------------------------------------------- 1 | container property or the `getContainer` method 34 | * from the ContainerAwareTrait. 35 | * 36 | * @return void 37 | */ 38 | public function register() 39 | { 40 | 41 | } 42 | 43 | /** 44 | * Method will be invoked on registration of a service provider implementing 45 | * this interface. Provides ability for eager loading of Service Providers. 46 | * 47 | * @return void 48 | */ 49 | public function boot() 50 | { 51 | /** @var Application\AbstractApplication $app */ 52 | $app = $this->getContainer()->get(ApplicationInterface::class); 53 | 54 | $errorHandler = new Run(new ApplicationSystemFacade($app)); 55 | $handlerService = new HandlerService($app); 56 | 57 | $errorHandler->pushHandler([$handlerService, 'recognizeErrorResponseHandler']); 58 | $errorHandler->pushHandler([$handlerService, 'notifySystemWithError']); 59 | $errorHandler->register(); 60 | 61 | $service = new Application\Services\WhoopsService($errorHandler, $app); 62 | $this->getContainer()->share(Application\Services\WhoopsService::class, $service); 63 | } 64 | } -------------------------------------------------------------------------------- /src/Application/Services/Whoops/ApplicationSystemFacade.php: -------------------------------------------------------------------------------- 1 | application = $application; 33 | } 34 | 35 | 36 | /** 37 | * @param callable $handler 38 | * @param int|string $types 39 | * @return EmitterTrait 40 | */ 41 | public function setErrorHandler(callable $handler, $types = 'use-php-defaults') 42 | { 43 | 44 | // $capturedHandler = $handler; 45 | // $event = ApplicationInterface::EVENT_HANDLE_ERROR; 46 | // $handler = function () use($capturedHandler, $event) { 47 | // $args = func_get_args(); 48 | // $this->application->emit($event, $args); 49 | // call_user_func_array($capturedHandler, $args); 50 | // }; 51 | // 52 | // return parent::setErrorHandler($handler, $types); 53 | 54 | $this->errorHandler = $handler; 55 | return $this->application->addListener( 56 | ApplicationInterface::EVENT_HANDLE_ERROR, 57 | function ($event) use ($handler) { 58 | $args = func_get_args(); 59 | array_shift($args); 60 | call_user_func_array($handler, $args); 61 | } 62 | ); 63 | } 64 | 65 | /** 66 | * @param callable $handler 67 | * @return EmitterTrait 68 | */ 69 | public function setExceptionHandler(callable $handler) 70 | { 71 | return $this->application->addListener( 72 | ApplicationInterface::EVENT_SYSTEM_EXCEPTION, 73 | function ($event) use ($handler) { 74 | $args = func_get_args(); 75 | array_shift($args); 76 | call_user_func_array($handler, $args); 77 | } 78 | ); 79 | } 80 | 81 | /** 82 | * 83 | */ 84 | public function restoreExceptionHandler() 85 | { 86 | $this->application->removeListener(ApplicationInterface::EVENT_HANDLE_ERROR, $this->exceptionHandler); 87 | $this->exceptionHandler = null; 88 | } 89 | 90 | /** 91 | * 92 | */ 93 | public function restoreErrorHandler() 94 | { 95 | $this->application->removeListener(ApplicationInterface::EVENT_SYSTEM_EXCEPTION, $this->exceptionHandler); 96 | $this->exceptionHandler = null; 97 | } 98 | 99 | /** 100 | * @param callable $function 101 | * @return EmitterTrait 102 | */ 103 | public function registerShutdownFunction(callable $function) 104 | { 105 | return $this->application->addListener( 106 | ApplicationInterface::EVENT_SYSTEM_SHUTDOWN, 107 | function ($event) use ($function) { 108 | call_user_func_array($function, []); 109 | } 110 | ); 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /src/Application/Services/Whoops/HandlerService.php: -------------------------------------------------------------------------------- 1 | application = $application; 37 | } 38 | 39 | /** 40 | * Recognize response handler for error by content type and environment. 41 | * 42 | * Cli is always handled as plaintext! 43 | * 44 | * @param $exception 45 | * @param $inspector 46 | * @param Run $run 47 | * @return int|null 48 | */ 49 | public function recognizeErrorResponseHandler($exception, $inspector, Run $run) 50 | { 51 | $app = $this->application; 52 | 53 | $handlerClass = PrettyPageHandler::class; 54 | if ($app->isCli() || false === $app->getConfig(ApplicationInterface::KEY_ERROR, ApplicationInterface::DEFAULT_ERROR)) { 55 | $handlerClass = PlainTextHandler::class; 56 | } 57 | 58 | if ($app instanceof Application) { 59 | if ($app->isSoapRequest() || $app->isXmlRequest()) { 60 | $handlerClass = XmlResponseHandler::class; 61 | } elseif ($app->isAjaxRequest() || $app->isJsonRequest()) { 62 | $handlerClass = JsonResponseHandler::class; 63 | } 64 | } 65 | 66 | /** @var HandlerInterface $handler */ 67 | $handler = new $handlerClass; 68 | $handler->setException($exception); 69 | $handler->setInspector($inspector); 70 | $handler->setRun($run); 71 | return $handler->handle(); 72 | } 73 | 74 | /** 75 | * Notify system after error occurs 76 | * 77 | * @param \Exception|\Throwable $exception 78 | * @return int 79 | */ 80 | public function notifySystemWithError($exception) 81 | { 82 | 83 | $app = $this->application; 84 | // log all errors 85 | $app->getLogger()->error($exception->getMessage()); 86 | 87 | $applicationEvent = $app->getApplicationEvent(); 88 | $applicationEvent->setName(ApplicationInterface::EVENT_SYSTEM_ERROR); 89 | $app->emit($applicationEvent, $exception); 90 | 91 | return Handler::DONE; 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /src/Application/Services/WhoopsService.php: -------------------------------------------------------------------------------- 1 | runner = $runner; 36 | $this->application = $application; 37 | } 38 | 39 | /** 40 | * Determine error message by error configuration 41 | * 42 | * @param \Throwable|\Exception $exception 43 | * 44 | * @return string 45 | * 46 | */ 47 | public function getErrorMessage($exception) 48 | { 49 | $application = $this->application; 50 | $errorHandler = $this->runner; 51 | 52 | // quit if error occured 53 | $shouldQuit = $application->isError(); 54 | 55 | // 56 | if($application instanceof Application){ 57 | // if request type ist not strict e.g. xml or json consider error config 58 | if (!$application->isXmlRequest() && !$application->isJsonRequest()) { 59 | $shouldQuit = $shouldQuit && $application->getConfig(ApplicationInterface::KEY_ERROR, ApplicationInterface::DEFAULT_ERROR); 60 | } 61 | } 62 | 63 | $errorHandler->allowQuit($shouldQuit); 64 | 65 | $method = $errorHandler::EXCEPTION_HANDLER; 66 | ob_start(); 67 | $errorHandler->$method($exception); 68 | $message = ob_get_clean(); 69 | 70 | return $message; 71 | } 72 | 73 | /** 74 | * Convert any type into an exception 75 | * 76 | * @param $error 77 | * @return \Exception|string 78 | */ 79 | public function decorateException($error) 80 | { 81 | $application = $this->application; 82 | 83 | if (is_callable($error)) { 84 | $error = $application->getContainer()->call($error, [$application]); 85 | } 86 | 87 | if (is_object($error) && !($error instanceof \Exception)) { 88 | $error = method_exists($error, 89 | '__toString') ? $error->__toString() : 'Error with object ' . get_class($error); 90 | } 91 | 92 | if (is_resource($error)) { 93 | $error = 'Error with resource type ' . get_resource_type($error); 94 | } 95 | 96 | if (is_array($error)) { 97 | $error = implode("\n", $error); 98 | } 99 | 100 | if (!($error instanceof \Exception)) { 101 | $error = new \Exception(is_scalar($error) ? $error : 'Error with ' . gettype($error)); 102 | } 103 | 104 | return $error; 105 | } 106 | } -------------------------------------------------------------------------------- /src/Application/TerminableInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit\Application; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | 17 | 18 | /** 19 | * PSR-7 port of \Symfony\Component\HttpKernel\TerminableInterface 20 | * 21 | * Terminable extends the Kernel request/response cycle with dispatching a post 22 | * response event after sending the response and before shutting down the kernel. 23 | * 24 | * @author Jordi Boggiano 25 | * @author Pierre Minnieur 26 | * @author Marco Bunge 27 | */ 28 | interface TerminableInterface 29 | { 30 | /** 31 | * Terminates a request/response cycle. 32 | * 33 | * Should be called after sending the response and before shutting down the kernel. 34 | * 35 | * @param ServerRequestInterface $request A Request instance 36 | * @param ResponseInterface $response A Response instance 37 | */ 38 | public function terminate(ServerRequestInterface $request, ResponseInterface $response); 39 | } 40 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | /** 13 | * Dot chaining support for nested configuratio 14 | * 15 | * @see \Hawkbit\Configuration::internalGet() 16 | * @see \Hawkbit\Configuration::internalSet() 17 | * @see \Hawkbit\Configuration::internalUnset() 18 | * 19 | * (c) m1 20 | * 21 | * For the full copyright and license information, please view the LICENSE 22 | * file that was distributed with m1/vars source code. 23 | * 24 | * @package m1/vars 25 | * @version 1.1.0 26 | * @author Miles Croxford 27 | * @copyright Copyright (c) Miles Croxford 28 | * @license http://github.com/m1/vars/blob/master/LICENSE 29 | * @link http://github.com/m1/vars/blob/master/README.MD Documentation 30 | */ 31 | 32 | namespace Hawkbit; 33 | 34 | 35 | use Zend\Config\Config; 36 | 37 | /** 38 | * Supports dot chaining for nested configuration inspired by M1/vars 39 | * 40 | * Class Configuration 41 | * @package Hawkbit 42 | */ 43 | final class Configuration extends Config 44 | { 45 | 46 | } -------------------------------------------------------------------------------- /src/Console.php: -------------------------------------------------------------------------------- 1 | init($configuration); 66 | 67 | foreach ($defaultProviders as $provider) { 68 | $this->getContainer()->addServiceProvider($provider); 69 | } 70 | } 71 | 72 | /** 73 | * Shutdown application lifecycle 74 | * 75 | * @return void 76 | */ 77 | public function shutdown() 78 | { 79 | // dispatch shutdown event 80 | $applicationEvent = $this->getApplicationEvent(); 81 | $applicationEvent->setName(self::EVENT_SYSTEM_SHUTDOWN); 82 | $this->emit($applicationEvent); 83 | 84 | exit((int)$this->isError()); 85 | } 86 | 87 | /** 88 | * Initialize Application 89 | * 90 | * @param array $configuration 91 | * @return void 92 | */ 93 | public function init($configuration = []) 94 | { 95 | $this->initConfiguration($configuration); 96 | $this->initHaltHooks(); 97 | } 98 | 99 | /** 100 | * Map first argument to callback 101 | * 102 | * - callable 103 | * - [class, method], including __invoke 104 | * 105 | * Argument matching full integrated from climate http://climate.thephpleague.com/arguments/ 106 | * 107 | * @param $name 108 | * @param $callback 109 | * @param array $arguments 110 | * @return Command 111 | */ 112 | public function map($name, $callback, array $arguments = []) 113 | { 114 | $command = new Command($name, $callback, $arguments); 115 | $this->commands[$name] = $command; 116 | return $command; 117 | } 118 | 119 | /** 120 | * Dispatch command from given args 121 | * 122 | * @param array $args 123 | */ 124 | public function handle(array $args = []) 125 | { 126 | // remove source file name from argv 127 | $source = array_shift($args); 128 | 129 | /** @var ConsoleEvent $applicationEvent */ 130 | $applicationEvent = $this->getApplicationEvent(); 131 | $applicationEvent->setName(self::EVENT_REQUEST_RECEIVED); 132 | $applicationEvent->setSourceFile($source); 133 | $applicationEvent->setArguments($args); 134 | $this->emit($applicationEvent); 135 | 136 | // init dispatcher 137 | $dispatcher = new Dispatcher($this->commands, $this->container); 138 | 139 | $middlewareRunner = new MiddlewareRunner($this->getMiddlewares()); 140 | 141 | $middlewareRunner->run([$applicationEvent->getArguments()], function ($args) use ($applicationEvent, $dispatcher) { 142 | // dispatch command with args from cli 143 | $dispatcher->dispatch($args); 144 | }); 145 | } 146 | 147 | /** 148 | * execute console lifecycle 149 | * 150 | * @param array|null $argv 151 | */ 152 | public function run(array $argv = null) 153 | { 154 | // If no $argv is provided then use the global PHP defined $argv. 155 | if (is_null($argv)) { 156 | global $argv; 157 | } 158 | 159 | // handle call 160 | $this->handle($argv); 161 | 162 | // exit cli with code 0 or even 1 for error 163 | $this->shutdown(); 164 | } 165 | 166 | /** 167 | * @param $command 168 | * @return bool 169 | */ 170 | public function hasCommand($command) 171 | { 172 | return isset($this->commands[$command]); 173 | } 174 | 175 | /** 176 | * Add a middleware 177 | * 178 | * @param callable $middleware 179 | */ 180 | public function addMiddleware(callable $middleware) 181 | { 182 | $this->middlewares[] = $this->bindClosureToInstance($middleware, $this); 183 | } 184 | 185 | /** 186 | * @return callable[] 187 | */ 188 | public function getMiddlewares() 189 | { 190 | return $this->middlewares; 191 | } 192 | } -------------------------------------------------------------------------------- /src/Console/Command.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 35 | 36 | $manager = new Manager(); 37 | if(!is_null($arguments)){ 38 | $manager->add($arguments); 39 | } 40 | $this->arguments = $manager; 41 | $this->name = $name; 42 | } 43 | 44 | /** 45 | * @return callable 46 | */ 47 | public function getHandler() 48 | { 49 | return $this->handler; 50 | } 51 | 52 | /** 53 | * @return Manager 54 | */ 55 | public function getArguments() 56 | { 57 | return $this->arguments; 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/Console/ConsoleEvent.php: -------------------------------------------------------------------------------- 1 | arguments; 33 | } 34 | 35 | /** 36 | * @param array $arguments 37 | * @return ConsoleEvent 38 | */ 39 | public function setArguments($arguments) 40 | { 41 | $this->arguments = $arguments; 42 | return $this; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getSourceFile() 49 | { 50 | return $this->sourceFile; 51 | } 52 | 53 | /** 54 | * @param string $sourceFile 55 | * @return ConsoleEvent 56 | */ 57 | public function setSourceFile($sourceFile) 58 | { 59 | $this->sourceFile = $sourceFile; 60 | return $this; 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/Console/Dispatcher.php: -------------------------------------------------------------------------------- 1 | commands = $commands; 34 | $dispatcherContainer = clone $container; 35 | $dispatcherContainer->delegate(new ReflectionContainer()); 36 | $this->container = $dispatcherContainer; 37 | } 38 | 39 | /** 40 | * @param $command 41 | * @return bool 42 | */ 43 | private function hasCommand($command) 44 | { 45 | return isset($this->commands[$command]); 46 | } 47 | 48 | /** 49 | * Dispatch handler for given command 50 | * 51 | * @param $argv 52 | */ 53 | public function dispatch($argv){ 54 | $name = reset($argv); 55 | 56 | if(!$this->hasCommand($name)){ 57 | throw new \InvalidArgumentException('Command not found'); 58 | } 59 | 60 | // load command 61 | $command = $this->commands[$name]; 62 | 63 | // parse arguments 64 | $arguments = $command->getArguments(); 65 | $arguments->parse($argv); 66 | 67 | // execute command 68 | $handler = $command->getHandler(); 69 | if(is_array($handler)){ 70 | $class = $handler[0]; 71 | // if($this->container->has($class)){ 72 | // $this->container->add($class); 73 | // } 74 | $obj = $this->container->get($class); 75 | $handler[0] = $obj; 76 | } 77 | call_user_func_array($handler, [$arguments]); 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /src/Stratigility/ApplicationMiddleware.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit\Stratigility; 13 | 14 | 15 | use Psr\Http\Message\ResponseInterface as Response; 16 | use Psr\Http\Message\ServerRequestInterface as Request; 17 | use Hawkbit\Application\ApplicationInterface; 18 | use Zend\Stratigility\MiddlewareInterface; 19 | 20 | class ApplicationMiddleware implements MiddlewareInterface 21 | { 22 | /** 23 | * @var ApplicationInterface 24 | */ 25 | private $application; 26 | 27 | /** 28 | * ApplicationMiddleware constructor. 29 | * @param ApplicationInterface $application 30 | */ 31 | public function __construct(ApplicationInterface $application) 32 | { 33 | $this->application = $application; 34 | } 35 | 36 | /** 37 | * @return ApplicationInterface 38 | */ 39 | public function getApplication() 40 | { 41 | return $this->application; 42 | } 43 | 44 | /** 45 | * Process an incoming request and/or response. 46 | * 47 | * Accepts a server-side request and a response instance, and does 48 | * something with them. 49 | * 50 | * If the response is not complete and/or further processing would not 51 | * interfere with the work done in the middleware, or if the middleware 52 | * wants to delegate to another process, it can use the `$out` callable 53 | * if present. 54 | * 55 | * If the middleware does not return a value, execution of the current 56 | * request is considered complete, and the response instance provided will 57 | * be considered the response to return. 58 | * 59 | * Alternately, the middleware may return a response instance. 60 | * 61 | * Often, middleware will `return $out();`, with the assumption that a 62 | * later middleware will return a response. 63 | * 64 | * @param Request $request 65 | * @param Response $response 66 | * @param null|callable $out 67 | * @return null|Response 68 | */ 69 | public function __invoke(Request $request, Response $response, callable $out = null) 70 | { 71 | $response = $this->getApplication()->handle($request, $response); 72 | if(is_callable($out)){ 73 | $out($request,$response); 74 | } 75 | return $response; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Stratigility/MiddlewarePipeAdapter.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit\Stratigility; 13 | 14 | 15 | use League\Route\Http\Exception; 16 | use Psr\Http\Message\ResponseInterface; 17 | use Psr\Http\Message\ServerRequestInterface; 18 | use Hawkbit\Application; 19 | use Zend\Diactoros\Response\HtmlResponse; 20 | use Zend\Stratigility\MiddlewarePipe; 21 | 22 | class MiddlewarePipeAdapter extends MiddlewarePipe 23 | { 24 | /** 25 | * @var Application 26 | */ 27 | private $application; 28 | 29 | /** 30 | * MiddlewareAdapter constructor. 31 | * @param Application $application 32 | */ 33 | public function __construct(Application $application) 34 | { 35 | parent::__construct(); 36 | $this->application = $application; 37 | } 38 | 39 | /** 40 | * @return Application 41 | */ 42 | public function getApplication() 43 | { 44 | return $this->application; 45 | } 46 | 47 | /** 48 | * Handle the request 49 | * 50 | * @param ServerRequestInterface $request 51 | * @param ResponseInterface $response 52 | * @param callable|null $out 53 | * @return ResponseInterface|void 54 | */ 55 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $out = null){ 56 | $application = $this->getApplication(); 57 | 58 | // handling errors by application with 59 | // custom $finalHandler if $out is null 60 | $finalHandler = $out ? $out : function (ServerRequestInterface $request, ResponseInterface $response, $err = null) use($application, $out){ 61 | if($err){ 62 | return $application->handleError($err, $request, $response); 63 | } 64 | return $response; 65 | }; 66 | $this->pipe(function(ServerRequestInterface $request, ResponseInterface $response, $next = null) use ($application){ 67 | return $application->handle($request, $response); 68 | }); 69 | return parent::__invoke($request, $response, $finalHandler); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Symfony/HttpKernelAdapter.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Alex Bilbie 7 | * @copyright Marco Bunge 8 | * 9 | * @license MIT 10 | */ 11 | 12 | namespace Hawkbit\Symfony; 13 | 14 | 15 | 16 | use Hawkbit\Application; 17 | use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory; 18 | use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; 19 | use Symfony\Component\HttpFoundation\Request; 20 | use Symfony\Component\HttpFoundation\Response; 21 | use Symfony\Component\HttpKernel\HttpKernelInterface; 22 | use Symfony\Component\HttpKernel\TerminableInterface; 23 | 24 | class HttpKernelAdapter implements HttpKernelInterface, TerminableInterface 25 | { 26 | 27 | /** 28 | * @var Application 29 | */ 30 | private $application; 31 | 32 | /** 33 | * @var DiactorosFactory 34 | */ 35 | private $diactorosFactory; 36 | 37 | /** 38 | * @var HttpFoundationFactory 39 | */ 40 | private $httpFoundationFactory; 41 | 42 | public function __construct(Application $application){ 43 | 44 | $this->application = $application; 45 | } 46 | 47 | /** 48 | * @return Application 49 | */ 50 | public function getApplication() 51 | { 52 | return $this->application; 53 | } 54 | 55 | /** 56 | * Handles a Request to convert it to a Response. 57 | * 58 | * Bridging between PSR-7 and Symfony HTTP 59 | * 60 | * When $catch is true, the implementation must catch all exceptions 61 | * and do its best to convert them to a Response instance. 62 | * 63 | * @param Request $request A Request instance 64 | * @param int $type The type of the request 65 | * (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST) 66 | * @param bool $catch Whether to catch exceptions or not 67 | * 68 | * @return Response A Response instance 69 | * 70 | * @throws \Exception When an Exception occurs during processing 71 | */ 72 | public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) 73 | { 74 | $response = $this->getApplication()->handle($this->getDiactorosFactory()->createRequest($request), null, $catch); 75 | return $this->getHttpFoundationFactory()->createResponse($response); 76 | } 77 | 78 | /** 79 | * Terminates a request/response cycle. 80 | * 81 | * Should be called after sending the response and before shutting down the kernel. 82 | * 83 | * @param Request $request A Request instance 84 | * @param Response $response A Response instance 85 | */ 86 | public function terminate(Request $request, Response $response) 87 | { 88 | $diactorosFactory = $this->getDiactorosFactory(); 89 | $this->getApplication()->terminate($diactorosFactory->createRequest($this->recomposeSymfonyRequest($request)), $diactorosFactory->createResponse($response)); 90 | } 91 | 92 | /** 93 | * Convert from symfony http foundation to PSR-7 94 | * 95 | * @return DiactorosFactory 96 | */ 97 | public function getDiactorosFactory() 98 | { 99 | if($this->diactorosFactory === null){ 100 | $this->diactorosFactory = new DiactorosFactory(); 101 | } 102 | return $this->diactorosFactory; 103 | } 104 | 105 | /** 106 | * Convert from PSR-7 to symfony http foundation 107 | * 108 | * @return HttpFoundationFactory 109 | */ 110 | public function getHttpFoundationFactory() 111 | { 112 | if($this->httpFoundationFactory === null){ 113 | $this->httpFoundationFactory = new HttpFoundationFactory(); 114 | } 115 | return $this->httpFoundationFactory; 116 | } 117 | 118 | /** 119 | * Avoid exception throw when request content is null 120 | * 121 | * @param Request $request 122 | * @return Request 123 | */ 124 | private function recomposeSymfonyRequest(Request $request) 125 | { 126 | try{ 127 | try { 128 | $content = $request->getContent(true); 129 | } catch (\LogicException $e) { 130 | $content = $request->getContent(); 131 | } 132 | }catch(\LogicException $e){ 133 | $content = file_get_contents('php://input'); 134 | } 135 | return $request::create( 136 | $request->getUri(), 137 | $request->getMethod(), 138 | $request->getMethod() === 'PATCH' ? $request->request->all() : $request->query->all(), 139 | $request->cookies->all(), 140 | $request->files->all(), 141 | $request->server->all(), 142 | $content 143 | ); 144 | } 145 | } 146 | --------------------------------------------------------------------------------