├── .gitignore ├── config ├── mezzio.middleware_pipeline.config.php ├── phpdebugbar.config.php └── dependency.config.php ├── src ├── ConfigCollectorFactory.php ├── JavascriptRendererFactory.php ├── ConfigProvider.php ├── PhpDebugBarMiddlewareFactory.php ├── StandardDebugBarFactory.php └── PhpDebugBarMiddleware.php ├── phpunit.xml ├── test ├── TestEmitter.php ├── RequestHandlerStub.php ├── Slim3Test.php ├── AbstractMiddlewareRunnerTest.php ├── MezzioTest.php └── PhpDebugBarMiddlewareTest.php ├── LICENSE ├── composer.json ├── .github └── workflows │ └── tests.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /config/mezzio.middleware_pipeline.config.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'middleware' => [ 8 | PhpDebugBarMiddleware::class, 9 | ], 10 | 'priority' => 1000, 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /config/phpdebugbar.config.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'phpdebugbar' => [ 6 | 'javascript_renderer' => [ 7 | 'base_url' => '/phpdebugbar', 8 | ], 9 | 'collectors' => [ 10 | DebugBar\DataCollector\ConfigCollector::class, 11 | ], 12 | 'storage' => null, 13 | ], 14 | ], 15 | ]; 16 | -------------------------------------------------------------------------------- /src/ConfigCollectorFactory.php: -------------------------------------------------------------------------------- 1 | get(ConfigProvider::class); 14 | 15 | return new ConfigCollector($data, 'Config'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | 9 | 10 | ./test 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /config/dependency.config.php: -------------------------------------------------------------------------------- 1 | [ 5 | \PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware::class => \PhpMiddleware\PhpDebugBar\PhpDebugBarMiddlewareFactory::class, 6 | \DebugBar\DataCollector\ConfigCollector::class => \PhpMiddleware\PhpDebugBar\ConfigCollectorFactory::class, 7 | \PhpMiddleware\PhpDebugBar\ConfigProvider::class => \PhpMiddleware\PhpDebugBar\ConfigProvider::class, 8 | \DebugBar\DebugBar::class => \PhpMiddleware\PhpDebugBar\StandardDebugBarFactory::class, 9 | \DebugBar\JavascriptRenderer::class => \PhpMiddleware\PhpDebugBar\JavascriptRendererFactory::class, 10 | ], 11 | ]; 12 | -------------------------------------------------------------------------------- /src/JavascriptRendererFactory.php: -------------------------------------------------------------------------------- 1 | get(DebugBar::class); 15 | $config = $container->get(ConfigProvider::class); 16 | $rendererOptions = $config['phpmiddleware']['phpdebugbar']['javascript_renderer']; 17 | 18 | $renderer = new JavascriptRenderer($debugBar); 19 | $renderer->setOptions($rendererOptions); 20 | 21 | return $renderer; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | public static function getConfig(): array 12 | { 13 | return (new self())(); 14 | } 15 | 16 | /** 17 | * @return array 18 | */ 19 | public function __invoke(): array 20 | { 21 | $config = include __DIR__ . '/../config/phpdebugbar.config.php'; 22 | $config['dependencies'] = include __DIR__ . '/../config/dependency.config.php'; 23 | $config['middleware_pipeline'] = include __DIR__ . '/../config/mezzio.middleware_pipeline.config.php'; 24 | 25 | return $config; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/TestEmitter.php: -------------------------------------------------------------------------------- 1 | response = $response; 17 | 18 | return true; 19 | } 20 | 21 | public function getResponse(): ResponseInterface 22 | { 23 | if ($this->response instanceof ResponseInterface) { 24 | return $this->response; 25 | } 26 | 27 | throw new BadMethodCallException('Not emitted yet'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/PhpDebugBarMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | get(JavascriptRenderer::class); 16 | $responseFactory = $container->get(ResponseFactoryInterface::class); 17 | $streamFactory = $container->get(StreamFactoryInterface::class); 18 | 19 | return new PhpDebugBarMiddleware($renderer, $responseFactory, $streamFactory); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/RequestHandlerStub.php: -------------------------------------------------------------------------------- 1 | response = $response; 19 | } 20 | 21 | public function handle(ServerRequestInterface $request): ResponseInterface 22 | { 23 | $this->called = true; 24 | 25 | return $this->response; 26 | } 27 | 28 | public function isCalled(): bool 29 | { 30 | return $this->called; 31 | } 32 | } -------------------------------------------------------------------------------- /src/StandardDebugBarFactory.php: -------------------------------------------------------------------------------- 1 | get(ConfigProvider::class); 16 | 17 | $collectors = $config['phpmiddleware']['phpdebugbar']['collectors']; 18 | 19 | foreach ($collectors as $collectorName) { 20 | $collector = $container->get($collectorName); 21 | $debugBar->addCollector($collector); 22 | } 23 | 24 | $storage = $config['phpmiddleware']['phpdebugbar']['storage']; 25 | 26 | if (is_string($storage)) { 27 | $debugBar->setStorage( 28 | $container->get($storage) 29 | ); 30 | } 31 | 32 | return $debugBar; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 PHP Middleware 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-middleware/php-debug-bar", 3 | "description": "PHP Debug Bar PSR-15 middleware with PSR-7", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "debug", 8 | "middleware", 9 | "psr", 10 | "psr-7", 11 | "psr-15", 12 | "psr-11" 13 | ], 14 | "require": { 15 | "php": "^7.3 || ^8.0", 16 | "maximebf/debugbar": "^1.4", 17 | "psr/http-server-handler": "^1.0", 18 | "psr/http-server-middleware": "^1.0", 19 | "psr/container-implementation": "^1.0 || ^2.0", 20 | "psr/http-message-implementation": "^1.0", 21 | "psr/http-factory-implementation": "^1.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^9.1.4", 25 | "mikey179/vfsstream": "^1.6.8", 26 | "slim/slim": "^3.0", 27 | "mezzio/mezzio": "^3.0", 28 | "mezzio/mezzio-fastroute": "^3.0.1", 29 | "laminas/laminas-servicemanager": "^3.3.2", 30 | "laminas/laminas-diactoros": "^2.0", 31 | "phpstan/phpstan": "^1.4" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "PhpMiddleware\\PhpDebugBar\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "PhpMiddlewareTest\\PhpDebugBar\\": "test/" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Slim3Test.php: -------------------------------------------------------------------------------- 1 | getContainer(); 22 | $container[ResponseFactoryInterface::class] = new ResponseFactory(); 23 | $container[StreamFactoryInterface::class] = new StreamFactory(); 24 | $container['environment'] = function() use ($server) { 25 | return new Environment($server); 26 | }; 27 | 28 | $config = ConfigProvider::getConfig(); 29 | 30 | foreach ($config['dependencies']['factories'] as $key => $factory) { 31 | $container[$key] = new $factory(); 32 | } 33 | 34 | $middleware = $container->get(PhpDebugBarMiddleware::class); 35 | 36 | $app->add($middleware); 37 | 38 | foreach ($pipe as $pattern => $middleware) { 39 | $app->get($pattern, $middleware); 40 | } 41 | 42 | return $app->run(true); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | jobs: 5 | phpstan: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | 11 | - name: Setup PHP, with composer and extensions 12 | uses: shivammathur/setup-php@v2 13 | with: 14 | php-version: 7.3 15 | 16 | - name: Install dependencies with Composer 17 | uses: ramsey/composer-install@v1 18 | 19 | - name: Run phpstan 20 | run: vendor/bin/phpstan analyse --level=6 src/ 21 | tests: 22 | strategy: 23 | matrix: 24 | dependencies: 25 | - highest 26 | - lowest 27 | php-versions: 28 | - 7.3 29 | - 7.4 30 | - 8.0 31 | - 8.1 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: Setup PHP, with composer and extensions 38 | uses: shivammathur/setup-php@v2 39 | with: 40 | php-version: ${{ matrix.php-versions }} 41 | 42 | - name: Install dependencies with Composer 43 | uses: ramsey/composer-install@v1 44 | with: 45 | dependency-versions: ${{ matrix.dependencies }} 46 | 47 | - name: Run unit tests 48 | run: vendor/bin/phpunit 49 | -------------------------------------------------------------------------------- /test/AbstractMiddlewareRunnerTest.php: -------------------------------------------------------------------------------- 1 | dispatchApplication([ 17 | 'REQUEST_URI' => '/hello', 18 | 'REQUEST_METHOD' => 'GET', 19 | 'HTTP_ACCEPT' => 'text/html', 20 | ], [ 21 | '/hello' => function (ServerRequestInterface $request) { 22 | $response = new Response(); 23 | $response->getBody()->write('Hello!'); 24 | return $response; 25 | }, 26 | ]); 27 | 28 | $responseBody = (string) $response->getBody(); 29 | 30 | $this->assertStringContainsString('var phpdebugbar = new PhpDebugBar.DebugBar();', $responseBody); 31 | $this->assertStringContainsString('Hello!', $responseBody); 32 | $this->assertStringContainsString('"/phpdebugbar/debugbar.js"', $responseBody); 33 | } 34 | 35 | final public function testGetStatics(): void 36 | { 37 | $response = $this->dispatchApplication([ 38 | 'DOCUMENT_ROOT' => __DIR__, 39 | 'REMOTE_ADDR' => '127.0.0.1', 40 | 'REMOTE_PORT' => '40226', 41 | 'SERVER_SOFTWARE' => 'PHP 7.0.8-3ubuntu3 Development Server', 42 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 43 | 'SERVER_NAME' => '0.0.0.0', 44 | 'SERVER_PORT' => '8080', 45 | 'REQUEST_URI' => '/phpdebugbar/debugbar.js', 46 | 'REQUEST_METHOD' => 'GET', 47 | 'SCRIPT_NAME' => '/phpdebugbar/debugbar.js', 48 | 'SCRIPT_FILENAME' => __FILE__, 49 | 'PHP_SELF' => '/phpdebugbar/debugbar.js', 50 | 'HTTP_HOST' => '0.0.0.0:8080', 51 | 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 52 | ]); 53 | 54 | $contentType = $response->getHeaderLine('Content-type'); 55 | 56 | $this->assertStringContainsString('text/javascript', $contentType); 57 | } 58 | 59 | abstract protected function dispatchApplication(array $server, array $pipe = []): ResponseInterface; 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpdebugbar middleware [![Build Status](https://travis-ci.org/php-middleware/phpdebugbar.svg?branch=master)](https://travis-ci.org/php-middleware/phpdebugbar) 2 | [PHP Debug Bar](http://phpdebugbar.com/) as framework-agnostic [PSR-15 middleware](https://www.php-fig.org/psr/psr-15/) with [PSR-7 messages](https://www.php-fig.org/psr/psr-7/) created by [PSR-17 message factories](https://www.php-fig.org/psr/psr-17/). Also provides [PSR-11 container invokable factories](https://www.php-fig.org/psr/psr-11/). 3 | 4 | Framework-agnostic way to attach [PHP Debug Bar](http://phpdebugbar.com/) to your response (html or non-html!). 5 | 6 | ## Installation 7 | 8 | ``` 9 | composer require --dev php-middleware/php-debug-bar 10 | ``` 11 | 12 | To build middleware you need to inject `DebugBar\JavascriptRenderer` (you can get it from `DebugBar\StandardDebugBar`) inside `PhpDebugBarMiddleware` and add it into your middleware runner: 13 | 14 | ```php 15 | $debugbar = new DebugBar\StandardDebugBar(); 16 | $debugbarRenderer = $debugbar->getJavascriptRenderer('/phpdebugbar'); 17 | $middleware = new PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware($debugbarRenderer, $psr17ResponseFactory, $psr17StreamFactory); 18 | 19 | // or use provided factory 20 | $factory = new PhpMiddleware\PhpDebugBar\PhpDebugBarMiddlewareFactory(); 21 | $middleware = $factory($psr11Container); 22 | 23 | $app = new MiddlewareRunner(); 24 | $app->add($middleware); 25 | $app->run($request, $response); 26 | ``` 27 | 28 | You don't need to copy any static assets from phpdebugbar vendor! 29 | 30 | ### How to force disable or enable PHP Debug Bar? 31 | 32 | Sometimes you want to have control when enable or disable PHP Debug Bar: 33 | * custom content negotiation, 34 | * allow debug redirects responses. 35 | 36 | We allow you to disable attaching phpdebugbar using `X-Enable-Debug-Bar: false` header, cookie or request attribute. 37 | To force enable just send request with `X-Enable-Debug-Bar` header, cookie or request attribute with `true` value. 38 | 39 | ### PSR-17 40 | 41 | This package isn't require any PSR-7 implementation - you need to provide it by own. Middleware require ResponseFactory and StreamFactory interfaces. [List of existing interfaces](https://packagist.org/providers/psr/http-factory-implementation). 42 | 43 | #### ... and PSR-11 44 | 45 | If you use provided PSR-11 factories, then your container must have services registered as PSR-17 interface's name. Example for [laminas-diactoros](https://github.com/laminas/laminas-diactoros) implementation and [Pimple](https://pimple.symfony.com/): 46 | 47 | ```php 48 | $container[Psr\Http\Message\ResponseInterface::class] = new Laminas\Diactoros\ResponseFactory(); 49 | $container[Psr\Http\Message\StreamFactoryInterface::class] = new Laminas\Diactoros\StreamFactory(); 50 | ``` 51 | 52 | ### How to install on Mezzio? 53 | 54 | You need to register `PhpMiddleware\PhpDebugBar\ConfigProvider` and pipe provided middleware: 55 | 56 | ```php 57 | $app->pipe(\PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware::class); 58 | ``` 59 | 60 | For more - follow Mezzio [documentation](https://docs.mezzio.dev/mezzio/v3/features/modular-applications/). 61 | 62 | ### How to install on Slim 3? 63 | 64 | Register factories in container: 65 | 66 | ```php 67 | foreach (ConfigProvider::getConfig()['dependencies']['factories'] as $key => $factory) { 68 | $container[$key] = new $factory(); 69 | } 70 | ``` 71 | 72 | and add middleware from container to app: 73 | 74 | ```php 75 | $app->add( 76 | $app->getContainer()->get(\PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware::class) 77 | ); 78 | ``` 79 | 80 | ### How to configure using existing factories? 81 | 82 | Put array with a configuration into `PhpMiddleware\PhpDebugBar\ConfigProvider` service in your container: 83 | 84 | ```php 85 | return [ 86 | 'phpmiddleware' => [ 87 | 'phpdebugbar' => [ 88 | 'javascript_renderer' => [ 89 | 'base_url' => '/phpdebugbar', 90 | ], 91 | 'collectors' => [ 92 | DebugBar\DataCollector\ConfigCollector::class, // Service names of collectors 93 | ], 94 | 'storage' => null, // Service name of storage 95 | ], 96 | ], 97 | ]; 98 | ``` 99 | 100 | You can override existing configuration by merge default configuration with your own (example): 101 | 102 | ```php 103 | return array_merge(PhpMiddleware\PhpDebugBar\ConfigProvider::getConfig(), $myOverritenConfig); 104 | ``` 105 | 106 | ## It's just works with any modern php framework! 107 | 108 | Middleware tested on: 109 | * [Mezzio](https://github.com/mezzio/mezzio) 110 | * [Slim 3.x](https://github.com/slimphp/Slim) 111 | 112 | And any other modern framework [supported PSR-17 middlewares and PSR-7](https://mwop.net/blog/2015-01-08-on-http-middleware-and-psr-7.html). 113 | -------------------------------------------------------------------------------- /test/MezzioTest.php: -------------------------------------------------------------------------------- 1 | dispatchApplication([ 46 | 'REQUEST_URI' => '/hello', 47 | 'REQUEST_METHOD' => 'GET', 48 | 'HTTP_ACCEPT' => 'text/html', 49 | ], [ 50 | '/hello' => function (ServerRequestInterface $request) { 51 | $response = new Response(); 52 | $response->getBody()->write('Hello!'); 53 | return $response; 54 | }, 55 | ]); 56 | 57 | $responseBody = (string) $response->getBody(); 58 | 59 | $this->assertStringContainsString('DebugBar\\\DataCollector\\\ConfigCollector', $responseBody); 60 | } 61 | 62 | protected function dispatchApplication(array $server, array $pipe = []): ResponseInterface 63 | { 64 | $container = $this->createContainer($server); 65 | 66 | $appFactory = new ApplicationFactory(); 67 | $app = $appFactory($container); 68 | 69 | $app->pipe(RouteMiddleware::class); 70 | 71 | $app->pipe(PhpDebugBarMiddleware::class); 72 | 73 | $app->pipe(DispatchMiddleware::class); 74 | 75 | foreach ($pipe as $pattern => $middleware) { 76 | $app->get($pattern, $middleware); 77 | } 78 | 79 | $app->run(); 80 | 81 | return $container->get(EmitterInterface::class)->getResponse(); 82 | } 83 | 84 | private function createContainer(array $server): ContainerInterface 85 | { 86 | $config = ConfigProvider::getConfig(); 87 | $config['debug'] = true; 88 | 89 | $serviceManagerConfig = $config['dependencies']; 90 | $serviceManagerConfig['services']['config'] = $config; 91 | $serviceManagerConfig['services'][EmitterInterface::class] = new TestEmitter(); 92 | $serviceManagerConfig['services'][ServerRequestInterface::class] = function() use ($server) { 93 | return ServerRequestFactory::fromGlobals($server, [], [], [], []); 94 | }; 95 | $serviceManagerConfig['factories'][MiddlewareFactory::class] = MiddlewareFactoryFactory::class; 96 | $serviceManagerConfig['factories'][MiddlewareContainer::class] = MiddlewareContainerFactory::class; 97 | $serviceManagerConfig['factories'][MiddlewarePipe::class] = InvokableFactory::class; 98 | $serviceManagerConfig['factories'][RouteCollector::class] = RouteCollectorFactory::class; 99 | $serviceManagerConfig['factories'][FastRouteRouter::class] = FastRouteRouterFactory::class; 100 | $serviceManagerConfig['factories'][RequestHandlerRunner::class] = RequestHandlerRunnerFactory::class; 101 | $serviceManagerConfig['factories'][ServerRequestErrorResponseGenerator::class] = ServerRequestErrorResponseGeneratorFactory::class; 102 | $serviceManagerConfig['factories'][ResponseInterface::class] = ResponseFactoryFactory::class; 103 | $serviceManagerConfig['factories'][RouteMiddleware::class] = RouteMiddlewareFactory::class; 104 | $serviceManagerConfig['factories'][DispatchMiddleware::class] = DispatchMiddlewareFactory::class; 105 | $serviceManagerConfig['factories'][ResponseFactory::class] = InvokableFactory::class; 106 | $serviceManagerConfig['factories'][StreamFactory::class] = InvokableFactory::class; 107 | $serviceManagerConfig['aliases'][RouterInterface::class] = FastRouteRouter::class; 108 | $serviceManagerConfig['aliases'][\Mezzio\ApplicationPipeline::class] = MiddlewarePipe::class; 109 | $serviceManagerConfig['aliases'][ResponseFactoryInterface::class] = ResponseFactory::class; 110 | $serviceManagerConfig['aliases'][StreamFactoryInterface::class] = StreamFactory::class; 111 | 112 | return new ServiceManager($serviceManagerConfig); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/PhpDebugBarMiddleware.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class PhpDebugBarMiddleware implements MiddlewareInterface 21 | { 22 | public const FORCE_KEY = 'X-Enable-Debug-Bar'; 23 | 24 | /** 25 | * @var DebugBarRenderer 26 | */ 27 | private $debugBarRenderer; 28 | 29 | /** 30 | * @var ResponseFactoryInterface 31 | */ 32 | private $responseFactory; 33 | 34 | /** 35 | * @var StreamFactoryInterface 36 | */ 37 | private $streamFactory; 38 | 39 | public function __construct( 40 | DebugBarRenderer $debugBarRenderer, 41 | ResponseFactoryInterface $responseFactory, 42 | StreamFactoryInterface $streamFactory 43 | ) { 44 | $this->debugBarRenderer = $debugBarRenderer; 45 | $this->responseFactory = $responseFactory; 46 | $this->streamFactory = $streamFactory; 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function process(ServerRequest $request, RequestHandler $handler): Response 53 | { 54 | if ($staticFile = $this->getStaticFile($request->getUri())) { 55 | return $staticFile; 56 | } 57 | 58 | $response = $handler->handle($request); 59 | 60 | if ($this->shouldReturnResponse($request, $response)) { 61 | return $response; 62 | } 63 | 64 | if ($this->isHtmlResponse($response)) { 65 | return $this->attachDebugBarToHtmlResponse($response); 66 | } 67 | 68 | return $this->prepareHtmlResponseWithDebugBar($response); 69 | } 70 | 71 | public function __invoke(ServerRequest $request, Response $response, callable $next): Response 72 | { 73 | $handler = new class($next, $response) implements RequestHandler { 74 | /** 75 | * @var callable 76 | */ 77 | private $next; 78 | 79 | /** 80 | * @var Response 81 | */ 82 | private $response; 83 | 84 | public function __construct(callable $next, Response $response) 85 | { 86 | $this->next = $next; 87 | $this->response = $response; 88 | } 89 | 90 | public function handle(ServerRequest $request): Response 91 | { 92 | return ($this->next)($request, $this->response); 93 | } 94 | }; 95 | return $this->process($request, $handler); 96 | } 97 | 98 | private function shouldReturnResponse(ServerRequest $request, Response $response): bool 99 | { 100 | $forceHeaderValue = $request->getHeaderLine(self::FORCE_KEY); 101 | $forceCookieValue = $request->getCookieParams()[self::FORCE_KEY] ?? ''; 102 | $forceAttributeValue = $request->getAttribute(self::FORCE_KEY, ''); 103 | $isForceEnable = in_array('true', [$forceHeaderValue, $forceCookieValue, $forceAttributeValue], true); 104 | $isForceDisable = in_array('false', [$forceHeaderValue, $forceCookieValue, $forceAttributeValue], true); 105 | 106 | return $isForceDisable || (!$isForceEnable && ($this->isRedirect($response) || !$this->isHtmlAccepted($request))); 107 | } 108 | 109 | private function prepareHtmlResponseWithDebugBar(Response $response): Response 110 | { 111 | $head = $this->debugBarRenderer->renderHead(); 112 | $body = $this->debugBarRenderer->render(); 113 | $outResponseBody = $this->serializeResponse($response); 114 | $template = '%s

DebugBar

Response:

%s
%s'; 115 | $escapedOutResponseBody = htmlspecialchars($outResponseBody); 116 | $result = sprintf($template, $head, $escapedOutResponseBody, $body); 117 | 118 | $stream = $this->streamFactory->createStream($result); 119 | 120 | return $this->responseFactory->createResponse() 121 | ->withBody($stream) 122 | ->withAddedHeader('Content-type', 'text/html'); 123 | } 124 | 125 | private function attachDebugBarToHtmlResponse(Response $response): Response 126 | { 127 | $head = $this->debugBarRenderer->renderHead(); 128 | $body = $this->debugBarRenderer->render(); 129 | $responseBody = $response->getBody(); 130 | 131 | if (! $responseBody->eof() && $responseBody->isSeekable()) { 132 | $responseBody->seek(0, SEEK_END); 133 | } 134 | $responseBody->write($head . $body); 135 | 136 | return $response; 137 | } 138 | 139 | private function getStaticFile(UriInterface $uri): ?Response 140 | { 141 | $path = $this->extractPath($uri); 142 | 143 | if (strpos($path, $this->debugBarRenderer->getBaseUrl()) !== 0) { 144 | return null; 145 | } 146 | 147 | $pathToFile = substr($path, strlen($this->debugBarRenderer->getBaseUrl())); 148 | 149 | $fullPathToFile = $this->debugBarRenderer->getBasePath() . $pathToFile; 150 | 151 | if (!file_exists($fullPathToFile)) { 152 | return null; 153 | } 154 | 155 | $contentType = $this->getContentTypeByFileName($fullPathToFile); 156 | $stream = $this->streamFactory->createStreamFromResource(fopen($fullPathToFile, 'rb')); 157 | 158 | return $this->responseFactory->createResponse() 159 | ->withBody($stream) 160 | ->withAddedHeader('Content-type', $contentType); 161 | } 162 | 163 | private function extractPath(UriInterface $uri): string 164 | { 165 | // Slim3 compatibility 166 | if ($uri instanceof SlimUri) { 167 | $basePath = $uri->getBasePath(); 168 | if (!empty($basePath)) { 169 | return $basePath; 170 | } 171 | } 172 | return $uri->getPath(); 173 | } 174 | 175 | private function getContentTypeByFileName(string $filename): string 176 | { 177 | $ext = pathinfo($filename, PATHINFO_EXTENSION); 178 | 179 | $map = [ 180 | 'css' => 'text/css', 181 | 'js' => 'text/javascript', 182 | 'otf' => 'font/opentype', 183 | 'eot' => 'application/vnd.ms-fontobject', 184 | 'svg' => 'image/svg+xml', 185 | 'ttf' => 'application/font-sfnt', 186 | 'woff' => 'application/font-woff', 187 | 'woff2' => 'application/font-woff2', 188 | ]; 189 | 190 | return $map[$ext] ?? 'text/plain'; 191 | } 192 | 193 | private function isHtmlResponse(Response $response): bool 194 | { 195 | return $this->isHtml($response, 'Content-Type'); 196 | } 197 | 198 | private function isHtmlAccepted(ServerRequest $request): bool 199 | { 200 | return $this->isHtml($request, 'Accept'); 201 | } 202 | 203 | private function isHtml(MessageInterface $message, string $headerName): bool 204 | { 205 | return strpos($message->getHeaderLine($headerName), 'text/html') !== false; 206 | } 207 | 208 | private function isRedirect(Response $response): bool 209 | { 210 | $statusCode = $response->getStatusCode(); 211 | 212 | return $statusCode >= 300 && $statusCode < 400 && $response->getHeaderLine('Location') !== ''; 213 | } 214 | 215 | private function serializeResponse(Response $response) : string 216 | { 217 | $reasonPhrase = $response->getReasonPhrase(); 218 | $headers = $this->serializeHeaders($response->getHeaders()); 219 | $format = 'HTTP/%s %d%s%s%s'; 220 | 221 | if (! empty($headers)) { 222 | $headers = "\r\n" . $headers; 223 | } 224 | 225 | $headers .= "\r\n\r\n"; 226 | 227 | return sprintf( 228 | $format, 229 | $response->getProtocolVersion(), 230 | $response->getStatusCode(), 231 | ($reasonPhrase ? ' ' . $reasonPhrase : ''), 232 | $headers, 233 | $response->getBody() 234 | ); 235 | } 236 | 237 | /** 238 | * @param array> $headers 239 | */ 240 | private function serializeHeaders(array $headers) : string 241 | { 242 | $lines = []; 243 | foreach ($headers as $header => $values) { 244 | $normalized = $this->filterHeader($header); 245 | foreach ($values as $value) { 246 | $lines[] = sprintf('%s: %s', $normalized, $value); 247 | } 248 | } 249 | 250 | return implode("\r\n", $lines); 251 | } 252 | 253 | private function filterHeader(string $header) : string 254 | { 255 | $filtered = str_replace('-', ' ', $header); 256 | $filtered = ucwords($filtered); 257 | return str_replace(' ', '-', $filtered); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /test/PhpDebugBarMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class PhpDebugBarMiddlewareTest extends TestCase 22 | { 23 | protected $debugbarRenderer; 24 | /** @var PhpDebugBarMiddleware */ 25 | protected $middleware; 26 | 27 | protected function setUp(): void 28 | { 29 | $this->debugbarRenderer = $this->getMockBuilder(JavascriptRenderer::class)->disableOriginalConstructor()->getMock(); 30 | $this->debugbarRenderer->method('renderHead')->willReturn('RenderHead'); 31 | $this->debugbarRenderer->method('getBaseUrl')->willReturn('/phpdebugbar'); 32 | $this->debugbarRenderer->method('render')->willReturn('RenderBody'); 33 | $responseFactory = new ResponseFactory(); 34 | $streamFactory = new StreamFactory(); 35 | 36 | $this->middleware = new PhpDebugBarMiddleware($this->debugbarRenderer, $responseFactory, $streamFactory); 37 | } 38 | 39 | public function testTwoPassCallingForCompatibility(): void 40 | { 41 | $request = new ServerRequest(); 42 | $response = new Response(); 43 | $response->getBody()->write('ResponseBody'); 44 | $calledOut = false; 45 | $outFunction = function ($request, $response) use (&$calledOut) { 46 | $calledOut = true; 47 | return $response; 48 | }; 49 | 50 | $result = call_user_func($this->middleware, $request, $response, $outFunction); 51 | 52 | $this->assertTrue($calledOut, 'Out is not called'); 53 | $this->assertSame('ResponseBody', (string) $result->getBody()); 54 | $this->assertSame($response, $result); 55 | } 56 | 57 | public function testNotAttachIfNotAccept(): void 58 | { 59 | $request = new ServerRequest(); 60 | $response = new Response(); 61 | $response->getBody()->write('ResponseBody'); 62 | $requestHandler = new RequestHandlerStub($response); 63 | 64 | $result = $this->middleware->process($request, $requestHandler); 65 | 66 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 67 | $this->assertSame('ResponseBody', (string) $result->getBody()); 68 | $this->assertSame($response, $result); 69 | } 70 | 71 | public function testForceAttachDebugbarIfHeaderPresents(): void 72 | { 73 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json', 'X-Enable-Debug-Bar' => 'true']); 74 | $response = new Response(); 75 | $response->getBody()->write('ResponseBody'); 76 | $requestHandler = new RequestHandlerStub($response); 77 | 78 | $result = $this->middleware->process($request, $requestHandler); 79 | 80 | $this->assertSame(200, $result->getStatusCode()); 81 | $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); 82 | } 83 | 84 | public function testForceAttachDebugbarIfCookiePresents(): void 85 | { 86 | $cookies = ['X-Enable-Debug-Bar' => 'true']; 87 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json'], $cookies); 88 | $response = new Response(); 89 | $response->getBody()->write('ResponseBody'); 90 | $requestHandler = new RequestHandlerStub($response); 91 | 92 | $result = $this->middleware->process($request, $requestHandler); 93 | 94 | $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); 95 | } 96 | 97 | public function testForceAttachDebugbarIfAttributePresents(): void 98 | { 99 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json']); 100 | $request = $request->withAttribute('X-Enable-Debug-Bar', 'true'); 101 | $response = new Response(); 102 | $response->getBody()->write('ResponseBody'); 103 | $requestHandler = new RequestHandlerStub($response); 104 | 105 | $result = $this->middleware->process($request, $requestHandler); 106 | 107 | $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); 108 | } 109 | 110 | public function testAttachToNoneHtmlResponse(): void 111 | { 112 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); 113 | $response = (new Response())->withHeader('test-header', 'value'); 114 | $response->getBody()->write('ResponseBody'); 115 | 116 | $requestHandler = new RequestHandlerStub($response); 117 | 118 | $result = $this->middleware->process($request, $requestHandler); 119 | 120 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 121 | $this->assertNotSame($response, $result); 122 | $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\nTest-Header: value\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); 123 | } 124 | 125 | public function testNotAttachToRedirectResponse(): void 126 | { 127 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); 128 | $response = (new Response())->withStatus(300)->withAddedHeader('Location', 'some-location'); 129 | 130 | $requestHandler = new RequestHandlerStub($response); 131 | 132 | $result = $this->middleware->process($request, $requestHandler); 133 | 134 | $this->assertSame($response, $result); 135 | } 136 | 137 | public function testAttachToNonRedirectResponse(): void 138 | { 139 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); 140 | $response = (new Response())->withStatus(299)->withAddedHeader('Location', 'some-location'); 141 | 142 | $requestHandler = new RequestHandlerStub($response); 143 | 144 | $result = $this->middleware->process($request, $requestHandler); 145 | 146 | $this->assertNotSame($response, $result); 147 | } 148 | 149 | public function testAttachToNonRedirectResponse2(): void 150 | { 151 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); 152 | $response = (new Response())->withStatus(400)->withAddedHeader('Location', 'some-location'); 153 | 154 | $requestHandler = new RequestHandlerStub($response); 155 | 156 | $result = $this->middleware->process($request, $requestHandler); 157 | 158 | $this->assertNotSame($response, $result); 159 | } 160 | 161 | public function testAttachToRedirectResponseWithoutLocation(): void 162 | { 163 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); 164 | $response = (new Response())->withStatus(302); 165 | 166 | $requestHandler = new RequestHandlerStub($response); 167 | 168 | $result = $this->middleware->process($request, $requestHandler); 169 | 170 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 171 | $this->assertNotSame($response, $result); 172 | $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 302 Found\r\n\r\n
RenderBody", (string) $result->getBody()); 173 | } 174 | 175 | public function testForceAttachToRedirectResponse(): void 176 | { 177 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html', 'X-Enable-Debug-Bar' => 'true']); 178 | $response = (new Response())->withStatus(302)->withAddedHeader('Location', 'some-location'); 179 | 180 | $requestHandler = new RequestHandlerStub($response); 181 | 182 | $result = $this->middleware->process($request, $requestHandler); 183 | 184 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 185 | $this->assertNotSame($response, $result); 186 | $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 302 Found\r\nLocation: some-location\r\n\r\n
RenderBody", (string) $result->getBody()); 187 | } 188 | 189 | public function testAttachToHtmlResponse(): void 190 | { 191 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); 192 | $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); 193 | $response->getBody()->write('ResponseBody'); 194 | $requestHandler = new RequestHandlerStub($response); 195 | 196 | $result = $this->middleware->process($request, $requestHandler); 197 | 198 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 199 | $this->assertSame($response, $result); 200 | $this->assertSame('ResponseBodyRenderHeadRenderBody', (string) $result->getBody()); 201 | } 202 | 203 | public function testForceNotAttachDebugbarIfHeaderPresents(): void 204 | { 205 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html', 'X-Enable-Debug-Bar' => 'false']); 206 | $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); 207 | $response->getBody()->write('ResponseBody'); 208 | $requestHandler = new RequestHandlerStub($response); 209 | 210 | $result = $this->middleware->process($request, $requestHandler); 211 | 212 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 213 | $this->assertSame($response, $result); 214 | $this->assertSame('ResponseBody', (string) $result->getBody()); 215 | } 216 | 217 | public function testForceNotAttachDebugbarIfCookiePresents(): void 218 | { 219 | $cookie = ['X-Enable-Debug-Bar' => 'false']; 220 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html'], $cookie); 221 | $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); 222 | $response->getBody()->write('ResponseBody'); 223 | $requestHandler = new RequestHandlerStub($response); 224 | 225 | $result = $this->middleware->process($request, $requestHandler); 226 | 227 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 228 | $this->assertSame($response, $result); 229 | $this->assertSame('ResponseBody', (string) $result->getBody()); 230 | } 231 | 232 | public function testForceNotAttachDebugbarIfAttributePresents(): void 233 | { 234 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); 235 | $request = $request->withAttribute('X-Enable-Debug-Bar', 'false'); 236 | $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); 237 | $response->getBody()->write('ResponseBody'); 238 | $requestHandler = new RequestHandlerStub($response); 239 | 240 | $result = $this->middleware->process($request, $requestHandler); 241 | 242 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 243 | $this->assertSame($response, $result); 244 | $this->assertSame('ResponseBody', (string) $result->getBody()); 245 | } 246 | 247 | public function testAppendsToEndOfHtmlResponse(): void 248 | { 249 | $html = 'FooContent'; 250 | $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); 251 | $response = new Response\HtmlResponse($html); 252 | $requestHandler = new RequestHandlerStub($response); 253 | 254 | $result = $this->middleware->process($request, $requestHandler); 255 | 256 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 257 | $this->assertSame($response, $result); 258 | $this->assertSame($html . 'RenderHeadRenderBody', (string) $result->getBody()); 259 | } 260 | 261 | public function testTryToHandleNotExistingStaticFile(): void 262 | { 263 | $uri = new Uri('http://example.com/phpdebugbar/boo.css'); 264 | $request = new ServerRequest([], [], $uri, null, 'php://memory'); 265 | $response = new Response\HtmlResponse(''); 266 | $requestHandler = new RequestHandlerStub($response); 267 | 268 | $result = $this->middleware->process($request, $requestHandler); 269 | 270 | $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); 271 | $this->assertSame($response, $result); 272 | } 273 | 274 | /** 275 | * @dataProvider getContentTypes 276 | */ 277 | public function testHandleStaticFile(string $extension, string $contentType): void 278 | { 279 | $root = vfsStream::setup('boo'); 280 | 281 | $this->debugbarRenderer->expects($this->any())->method('getBasePath')->willReturn(vfsStream::url('boo')); 282 | 283 | $uri = new Uri(sprintf('http://example.com/phpdebugbar/debugbar.%s', $extension)); 284 | $request = new ServerRequest([], [], $uri, null, 'php://memory'); 285 | $response = new Response\HtmlResponse(''); 286 | 287 | vfsStream::newFile(sprintf('debugbar.%s', $extension))->withContent('filecontent')->at($root); 288 | 289 | $requestHandler = new RequestHandlerStub($response); 290 | 291 | $result = $this->middleware->process($request, $requestHandler); 292 | 293 | $this->assertFalse($requestHandler->isCalled(), 'Request handler is called'); 294 | $this->assertNotSame($response, $result); 295 | $this->assertSame($contentType, $result->getHeaderLine('Content-type')); 296 | $this->assertSame(200, $result->getStatusCode()); 297 | $this->assertSame('filecontent', (string) $result->getBody()); 298 | } 299 | 300 | public function getContentTypes(): array 301 | { 302 | return [ 303 | ['css', 'text/css'], 304 | ['js', 'text/javascript'], 305 | ['html', 'text/plain'], 306 | ]; 307 | } 308 | } 309 | --------------------------------------------------------------------------------