diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..0b6a230 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,48 @@ +name: CI +on: + - push +jobs: + phpstan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: 7.3 + + - name: Install dependencies with Composer + uses: ramsey/composer-install@v1 + + - name: Run phpstan + run: vendor/bin/phpstan analyse --level=6 src/ + tests: + strategy: + matrix: + dependencies: + - highest + - lowest + php-versions: + - 7.3 + - 7.4 + - 8.0 + - 8.1 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + + - name: Install dependencies with Composer + uses: ramsey/composer-install@v1 + with: + dependency-versions: ${{ matrix.dependencies }} + + - name: Run unit tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 55940e5..0794651 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor/ -composer.lock \ No newline at end of file +composer.lock +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 573e782..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: php - -php: - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - hhvm - -env: - - DEPS=lowest - - DEPS=latest - -before_script: - - composer self-update - - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable --no-interaction ; fi - - travis_retry composer install --no-interaction - -script: - - ./vendor/bin/phpunit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..301f924 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 PHP Middleware + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bb3edfd..cf9b6ff 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,24 @@ # phpdebugbar middleware [![Build Status](https://travis-ci.org/php-middleware/phpdebugbar.svg?branch=master)](https://travis-ci.org/php-middleware/phpdebugbar) -PHP Debug bar middleware with PSR-7 +[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/). -This middleware provide framework-agnostic possibility to attach [PHP Debug bar](http://phpdebugbar.com/) to your response (html on non-html!). +Framework-agnostic way to attach [PHP Debug Bar](http://phpdebugbar.com/) to your response (html or non-html!). ## Installation ``` -composer require php-middleware/php-debug-bar +composer require --dev php-middleware/php-debug-bar ``` -To build this middleware you need to injecting inside `PhpDebugBarMiddleware` instance `DebugBar\JavascriptRenderer` (you can get it from `DebugBar\StandardDebugBar`) and add middleware to your middleware runner. Or use default factory. +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: ```php $debugbar = new DebugBar\StandardDebugBar(); $debugbarRenderer = $debugbar->getJavascriptRenderer('/phpdebugbar'); -$middleware = new PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware($debugbarRenderer); +$middleware = new PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware($debugbarRenderer, $psr17ResponseFactory, $psr17StreamFactory); -// OR - -$factory = PhpMiddleware\PhpDebugBar\PhpDebugBarMiddlewareFactory(); -$middleware = $factory(); +// or use provided factory +$factory = new PhpMiddleware\PhpDebugBar\PhpDebugBarMiddlewareFactory(); +$middleware = $factory($psr11Container); $app = new MiddlewareRunner(); $app->add($middleware); @@ -28,38 +27,86 @@ $app->run($request, $response); You don't need to copy any static assets from phpdebugbar vendor! -### How to install on Zend Expressive? +### How to force disable or enable PHP Debug Bar? + +Sometimes you want to have control when enable or disable PHP Debug Bar: +* custom content negotiation, +* allow debug redirects responses. + +We allow you to disable attaching phpdebugbar using `X-Enable-Debug-Bar: false` header, cookie or request attribute. +To force enable just send request with `X-Enable-Debug-Bar` header, cookie or request attribute with `true` value. + +### PSR-17 + +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). + +#### ... and PSR-11 + +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/): + +```php +$container[Psr\Http\Message\ResponseInterface::class] = new Laminas\Diactoros\ResponseFactory(); +$container[Psr\Http\Message\StreamFactoryInterface::class] = new Laminas\Diactoros\StreamFactory(); +``` + +### How to install on Mezzio? -Use [mtymek/expressive-config-manager](https://github.com/mtymek/expressive-config-manager) and add -`PhpMiddleware\PhpDebugBar\ConfigProvider` class name: +You need to register `PhpMiddleware\PhpDebugBar\ConfigProvider` and pipe provided middleware: ```php -$configManager = new \Zend\Expressive\ConfigManager\ConfigManager([ - \PhpMiddleware\PhpDebugBar\ConfigProvider::class, - new \Zend\Expressive\ConfigManager\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), -]); +$app->pipe(\PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware::class); ``` -more [about config manager](https://zendframework.github.io/zend-expressive/cookbook/modular-layout/). +For more - follow Mezzio [documentation](https://docs.mezzio.dev/mezzio/v3/features/modular-applications/). ### How to install on Slim 3? -Add existing factory to container: +Register factories in container: ```php -$container['debugbar_middleware'] = new PhpMiddleware\PhpDebugBar\PhpDebugBarMiddlewareFactory(); +foreach (ConfigProvider::getConfig()['dependencies']['factories'] as $key => $factory) { + $container[$key] = new $factory(); +} ``` and add middleware from container to app: ```php -$app->add($app->getContainer()->get('debugbar_middleware')); +$app->add( + $app->getContainer()->get(\PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware::class) +); +``` + +### How to configure using existing factories? + +Put array with a configuration into `PhpMiddleware\PhpDebugBar\ConfigProvider` service in your container: + +```php +return [ + 'phpmiddleware' => [ + 'phpdebugbar' => [ + 'javascript_renderer' => [ + 'base_url' => '/phpdebugbar', + ], + 'collectors' => [ + DebugBar\DataCollector\ConfigCollector::class, // Service names of collectors + ], + 'storage' => null, // Service name of storage + ], + ], +]; +``` + +You can override existing configuration by merge default configuration with your own (example): + +```php +return array_merge(PhpMiddleware\PhpDebugBar\ConfigProvider::getConfig(), $myOverritenConfig); ``` ## It's just works with any modern php framework! Middleware tested on: -* [Zend Expressive](https://github.com/zendframework/zend-expressive) +* [Mezzio](https://github.com/mezzio/mezzio) * [Slim 3.x](https://github.com/slimphp/Slim) -And any other modern framework [supported middlewares and PSR-7](https://mwop.net/blog/2015-01-08-on-http-middleware-and-psr-7.html). +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). diff --git a/composer.json b/composer.json index 6986873..77bcca9 100644 --- a/composer.json +++ b/composer.json @@ -1,26 +1,34 @@ { "name": "php-middleware/php-debug-bar", - "description": "PHP Debug bar middleware with PSR-7", + "description": "PHP Debug Bar PSR-15 middleware with PSR-7", "type": "library", + "license": "MIT", "keywords": [ "debug", "middleware", "psr", - "psr-7" + "psr-7", + "psr-15", + "psr-11" ], "require": { - "php": "^5.5 || ^7.0", - "maximebf/debugbar": "^1.10", - "psr/http-message": "^1.0", - "zendframework/zend-diactoros": "^1.1.3" + "php": "^7.3 || ^8.0", + "maximebf/debugbar": "^1.4", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/container-implementation": "^1.0 || ^2.0", + "psr/http-message-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.6", - "mikey179/vfsStream": "^1.6", + "phpunit/phpunit": "^9.1.4", + "mikey179/vfsstream": "^1.6.8", "slim/slim": "^3.0", - "zendframework/zend-expressive": "^1.0", - "zendframework/zend-expressive-fastroute": "^1.0", - "zendframework/zend-servicemanager": "^3.0" + "mezzio/mezzio": "^3.0", + "mezzio/mezzio-fastroute": "^3.0.1", + "laminas/laminas-servicemanager": "^3.3.2", + "laminas/laminas-diactoros": "^2.0", + "phpstan/phpstan": "^1.4" }, "autoload": { "psr-4": { diff --git a/config/dependency.config.php b/config/dependency.config.php index eeb3ea2..1680268 100644 --- a/config/dependency.config.php +++ b/config/dependency.config.php @@ -2,6 +2,10 @@ return [ 'factories' => [ - PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware::class => PhpMiddleware\PhpDebugBar\PhpDebugBarMiddlewareFactory::class, + \PhpMiddleware\PhpDebugBar\PhpDebugBarMiddleware::class => \PhpMiddleware\PhpDebugBar\PhpDebugBarMiddlewareFactory::class, + \DebugBar\DataCollector\ConfigCollector::class => \PhpMiddleware\PhpDebugBar\ConfigCollectorFactory::class, + \PhpMiddleware\PhpDebugBar\ConfigProvider::class => \PhpMiddleware\PhpDebugBar\ConfigProvider::class, + \DebugBar\DebugBar::class => \PhpMiddleware\PhpDebugBar\StandardDebugBarFactory::class, + \DebugBar\JavascriptRenderer::class => \PhpMiddleware\PhpDebugBar\JavascriptRendererFactory::class, ], ]; diff --git a/config/zend-expressive.middleware_pipeline.config.php b/config/mezzio.middleware_pipeline.config.php similarity index 100% rename from config/zend-expressive.middleware_pipeline.config.php rename to config/mezzio.middleware_pipeline.config.php diff --git a/config/phpdebugbar.config.php b/config/phpdebugbar.config.php new file mode 100644 index 0000000..5a6b9a8 --- /dev/null +++ b/config/phpdebugbar.config.php @@ -0,0 +1,15 @@ + [ + 'phpdebugbar' => [ + 'javascript_renderer' => [ + 'base_url' => '/phpdebugbar', + ], + 'collectors' => [ + DebugBar\DataCollector\ConfigCollector::class, + ], + 'storage' => null, + ], + ], +]; diff --git a/phpunit.xml b/phpunit.xml index 88ba754..fd2da66 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,13 @@ - - - - - ./test - - - - - - ./src - - + + + + ./src + + + + + ./test + + diff --git a/src/ConfigCollectorFactory.php b/src/ConfigCollectorFactory.php new file mode 100644 index 0000000..69e3ffa --- /dev/null +++ b/src/ConfigCollectorFactory.php @@ -0,0 +1,17 @@ +get(ConfigProvider::class); + + return new ConfigCollector($data, 'Config'); + } +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 73b3199..375bad1 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -1,20 +1,27 @@ + */ + public static function getConfig(): array { - $self = new self(); - return $self(); + return (new self())(); } - public function __invoke() + /** + * @return array + */ + public function __invoke(): array { - return [ - 'dependencies' => include __DIR__ . '/../config/dependency.config.php', - 'middleware_pipeline' => include __DIR__ . '/../config/zend-expressive.middleware_pipeline.config.php', - ]; + $config = include __DIR__ . '/../config/phpdebugbar.config.php'; + $config['dependencies'] = include __DIR__ . '/../config/dependency.config.php'; + $config['middleware_pipeline'] = include __DIR__ . '/../config/mezzio.middleware_pipeline.config.php'; + + return $config; } } diff --git a/src/JavascriptRendererFactory.php b/src/JavascriptRendererFactory.php new file mode 100644 index 0000000..3063abc --- /dev/null +++ b/src/JavascriptRendererFactory.php @@ -0,0 +1,23 @@ +get(DebugBar::class); + $config = $container->get(ConfigProvider::class); + $rendererOptions = $config['phpmiddleware']['phpdebugbar']['javascript_renderer']; + + $renderer = new JavascriptRenderer($debugBar); + $renderer->setOptions($rendererOptions); + + return $renderer; + } +} diff --git a/src/PhpDebugBarMiddleware.php b/src/PhpDebugBarMiddleware.php index 210753d..8d4a892 100644 --- a/src/PhpDebugBarMiddleware.php +++ b/src/PhpDebugBarMiddleware.php @@ -1,89 +1,147 @@ */ -class PhpDebugBarMiddleware +final class PhpDebugBarMiddleware implements MiddlewareInterface { + public const FORCE_KEY = 'X-Enable-Debug-Bar'; + /** * @var DebugBarRenderer */ - protected $debugBarRenderer; + private $debugBarRenderer; /** - * @param DebugBarRenderer $debugbarRenderer + * @var ResponseFactoryInterface */ - public function __construct(DebugBarRenderer $debugbarRenderer) - { - $this->debugBarRenderer = $debugbarRenderer; + private $responseFactory; + + /** + * @var StreamFactoryInterface + */ + private $streamFactory; + + public function __construct( + DebugBarRenderer $debugBarRenderer, + ResponseFactoryInterface $responseFactory, + StreamFactoryInterface $streamFactory + ) { + $this->debugBarRenderer = $debugBarRenderer; + $this->responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; } /** - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param callable $next - * - * @return ResponseInterface + * @inheritDoc */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) + public function process(ServerRequest $request, RequestHandler $handler): Response { if ($staticFile = $this->getStaticFile($request->getUri())) { return $staticFile; } - $outResponse = $next($request, $response); + $response = $handler->handle($request); - if (!$this->isHtmlAccepted($request)) { - return $outResponse; + if ($this->shouldReturnResponse($request, $response)) { + return $response; } - $debugBarHead = $this->debugBarRenderer->renderHead(); - $debugBarBody = $this->debugBarRenderer->render(); + if ($this->isHtmlResponse($response)) { + return $this->attachDebugBarToHtmlResponse($response); + } - if ($this->isHtmlResponse($outResponse)) { - $body = $outResponse->getBody(); - if (! $body->eof() && $body->isSeekable()) { - $body->seek(0, SEEK_END); + return $this->prepareHtmlResponseWithDebugBar($response); + } + + public function __invoke(ServerRequest $request, Response $response, callable $next): Response + { + $handler = new class($next, $response) implements RequestHandler { + /** + * @var callable + */ + private $next; + + /** + * @var Response + */ + private $response; + + public function __construct(callable $next, Response $response) + { + $this->next = $next; + $this->response = $response; } - $body->write($debugBarHead . $debugBarBody); - return $outResponse; - } + public function handle(ServerRequest $request): Response + { + return ($this->next)($request, $this->response); + } + }; + return $this->process($request, $handler); + } - $outResponseBody = Serializer::toString($outResponse); + private function shouldReturnResponse(ServerRequest $request, Response $response): bool + { + $forceHeaderValue = $request->getHeaderLine(self::FORCE_KEY); + $forceCookieValue = $request->getCookieParams()[self::FORCE_KEY] ?? ''; + $forceAttributeValue = $request->getAttribute(self::FORCE_KEY, ''); + $isForceEnable = in_array('true', [$forceHeaderValue, $forceCookieValue, $forceAttributeValue], true); + $isForceDisable = in_array('false', [$forceHeaderValue, $forceCookieValue, $forceAttributeValue], true); + + return $isForceDisable || (!$isForceEnable && ($this->isRedirect($response) || !$this->isHtmlAccepted($request))); + } + + private function prepareHtmlResponseWithDebugBar(Response $response): Response + { + $head = $this->debugBarRenderer->renderHead(); + $body = $this->debugBarRenderer->render(); + $outResponseBody = $this->serializeResponse($response); $template = '%s

DebugBar

Response:

%s
%s'; $escapedOutResponseBody = htmlspecialchars($outResponseBody); - $result = sprintf($template, $debugBarHead, $escapedOutResponseBody, $debugBarBody); + $result = sprintf($template, $head, $escapedOutResponseBody, $body); + + $stream = $this->streamFactory->createStream($result); - return new HtmlResponse($result); + return $this->responseFactory->createResponse() + ->withBody($stream) + ->withAddedHeader('Content-type', 'text/html'); } - /** - * @param UriInterface $uri - * - * @return ResponseInterface|null - */ - private function getStaticFile(UriInterface $uri) + private function attachDebugBarToHtmlResponse(Response $response): Response + { + $head = $this->debugBarRenderer->renderHead(); + $body = $this->debugBarRenderer->render(); + $responseBody = $response->getBody(); + + if (! $responseBody->eof() && $responseBody->isSeekable()) { + $responseBody->seek(0, SEEK_END); + } + $responseBody->write($head . $body); + + return $response; + } + + private function getStaticFile(UriInterface $uri): ?Response { $path = $this->extractPath($uri); if (strpos($path, $this->debugBarRenderer->getBaseUrl()) !== 0) { - return; + return null; } $pathToFile = substr($path, strlen($this->debugBarRenderer->getBaseUrl())); @@ -91,26 +149,21 @@ private function getStaticFile(UriInterface $uri) $fullPathToFile = $this->debugBarRenderer->getBasePath() . $pathToFile; if (!file_exists($fullPathToFile)) { - return; + return null; } $contentType = $this->getContentTypeByFileName($fullPathToFile); - $stream = new Stream($fullPathToFile, 'r'); + $stream = $this->streamFactory->createStreamFromResource(fopen($fullPathToFile, 'rb')); - return new Response($stream, 200, [ - 'Content-type' => $contentType, - ]); + return $this->responseFactory->createResponse() + ->withBody($stream) + ->withAddedHeader('Content-type', $contentType); } - /** - * @param UriInterface $uri - * - * @return string - */ - private function extractPath(UriInterface $uri) + private function extractPath(UriInterface $uri): string { // Slim3 compatibility - if ($uri instanceof Uri) { + if ($uri instanceof SlimUri) { $basePath = $uri->getBasePath(); if (!empty($basePath)) { return $basePath; @@ -119,12 +172,7 @@ private function extractPath(UriInterface $uri) return $uri->getPath(); } - /** - * @param string $filename - * - * @return string - */ - private function getContentTypeByFileName($filename) + private function getContentTypeByFileName(string $filename): string { $ext = pathinfo($filename, PATHINFO_EXTENSION); @@ -139,42 +187,73 @@ private function getContentTypeByFileName($filename) 'woff2' => 'application/font-woff2', ]; - if (isset($map[$ext])) { - return $map[$ext]; - } + return $map[$ext] ?? 'text/plain'; + } + + private function isHtmlResponse(Response $response): bool + { + return $this->isHtml($response, 'Content-Type'); + } - return 'text/plain'; + private function isHtmlAccepted(ServerRequest $request): bool + { + return $this->isHtml($request, 'Accept'); } - /** - * @param ResponseInterface $response - * - * @return bool - */ - private function isHtmlResponse(ResponseInterface $response) + private function isHtml(MessageInterface $message, string $headerName): bool { - return $this->hasHeaderContains($response, 'Content-Type', 'text/html'); + return strpos($message->getHeaderLine($headerName), 'text/html') !== false; } - /** - * @param ServerRequestInterface $request - * - * @return bool - */ - private function isHtmlAccepted(ServerRequestInterface $request) + private function isRedirect(Response $response): bool { - return $this->hasHeaderContains($request, 'Accept', 'text/html'); + $statusCode = $response->getStatusCode(); + + return $statusCode >= 300 && $statusCode < 400 && $response->getHeaderLine('Location') !== ''; + } + + private function serializeResponse(Response $response) : string + { + $reasonPhrase = $response->getReasonPhrase(); + $headers = $this->serializeHeaders($response->getHeaders()); + $format = 'HTTP/%s %d%s%s%s'; + + if (! empty($headers)) { + $headers = "\r\n" . $headers; + } + + $headers .= "\r\n\r\n"; + + return sprintf( + $format, + $response->getProtocolVersion(), + $response->getStatusCode(), + ($reasonPhrase ? ' ' . $reasonPhrase : ''), + $headers, + $response->getBody() + ); } /** - * @param MessageInterface $message - * @param string $headerName - * @param string $value - * - * @return bool + * @param array> $headers */ - private function hasHeaderContains(MessageInterface $message, $headerName, $value) + private function serializeHeaders(array $headers) : string + { + $lines = []; + foreach ($headers as $header => $values) { + $normalized = $this->filterHeader($header); + foreach ($values as $value) { + $lines[] = sprintf('%s: %s', $normalized, $value); + } + } + + return implode("\r\n", $lines); + } + + private function filterHeader(string $header) : string { - return strpos($message->getHeaderLine($headerName), $value) !== false; + $filtered = str_replace('-', ' ', $header); + $filtered = ucwords($filtered); + return str_replace(' ', '-', $filtered); } } diff --git a/src/PhpDebugBarMiddlewareFactory.php b/src/PhpDebugBarMiddlewareFactory.php index 38c7019..0e0cabc 100644 --- a/src/PhpDebugBarMiddlewareFactory.php +++ b/src/PhpDebugBarMiddlewareFactory.php @@ -1,21 +1,21 @@ - */ final class PhpDebugBarMiddlewareFactory { - public function __invoke() + public function __invoke(ContainerInterface $container): PhpDebugBarMiddleware { - $debugbar = new StandardDebugBar(); - $renderer = $debugbar->getJavascriptRenderer('/phpdebugbar'); + $renderer = $container->get(JavascriptRenderer::class); + $responseFactory = $container->get(ResponseFactoryInterface::class); + $streamFactory = $container->get(StreamFactoryInterface::class); - return new PhpDebugBarMiddleware($renderer); + return new PhpDebugBarMiddleware($renderer, $responseFactory, $streamFactory); } } diff --git a/src/StandardDebugBarFactory.php b/src/StandardDebugBarFactory.php new file mode 100644 index 0000000..21f331f --- /dev/null +++ b/src/StandardDebugBarFactory.php @@ -0,0 +1,34 @@ +get(ConfigProvider::class); + + $collectors = $config['phpmiddleware']['phpdebugbar']['collectors']; + + foreach ($collectors as $collectorName) { + $collector = $container->get($collectorName); + $debugBar->addCollector($collector); + } + + $storage = $config['phpmiddleware']['phpdebugbar']['storage']; + + if (is_string($storage)) { + $debugBar->setStorage( + $container->get($storage) + ); + } + + return $debugBar; + } +} diff --git a/test/AbstractMiddlewareRunnerTest.php b/test/AbstractMiddlewareRunnerTest.php index 44bbd4e..a7dd572 100644 --- a/test/AbstractMiddlewareRunnerTest.php +++ b/test/AbstractMiddlewareRunnerTest.php @@ -1,22 +1,25 @@ dispatchApplication([ 'REQUEST_URI' => '/hello', 'REQUEST_METHOD' => 'GET', 'HTTP_ACCEPT' => 'text/html', ], [ - '/hello' => function (ServerRequestInterface $request, ResponseInterface $response, $next) { + '/hello' => function (ServerRequestInterface $request) { + $response = new Response(); $response->getBody()->write('Hello!'); return $response; }, @@ -24,12 +27,12 @@ final public function testAppendJsIntoHtmlContent() $responseBody = (string) $response->getBody(); - $this->assertContains('var phpdebugbar = new PhpDebugBar.DebugBar();', $responseBody); - $this->assertContains('Hello!', $responseBody); - $this->assertContains('"/phpdebugbar/debugbar.js"', $responseBody); + $this->assertStringContainsString('var phpdebugbar = new PhpDebugBar.DebugBar();', $responseBody); + $this->assertStringContainsString('Hello!', $responseBody); + $this->assertStringContainsString('"/phpdebugbar/debugbar.js"', $responseBody); } - final public function testGetStatics() + final public function testGetStatics(): void { $response = $this->dispatchApplication([ 'DOCUMENT_ROOT' => __DIR__, @@ -50,11 +53,8 @@ final public function testGetStatics() $contentType = $response->getHeaderLine('Content-type'); - $this->assertContains('text/javascript', $contentType); + $this->assertStringContainsString('text/javascript', $contentType); } - /** - * @return ResponseInterface - */ - abstract protected function dispatchApplication(array $server, array $pipe = []); + abstract protected function dispatchApplication(array $server, array $pipe = []): ResponseInterface; } diff --git a/test/MezzioTest.php b/test/MezzioTest.php new file mode 100644 index 0000000..f3e340d --- /dev/null +++ b/test/MezzioTest.php @@ -0,0 +1,114 @@ +dispatchApplication([ + 'REQUEST_URI' => '/hello', + 'REQUEST_METHOD' => 'GET', + 'HTTP_ACCEPT' => 'text/html', + ], [ + '/hello' => function (ServerRequestInterface $request) { + $response = new Response(); + $response->getBody()->write('Hello!'); + return $response; + }, + ]); + + $responseBody = (string) $response->getBody(); + + $this->assertStringContainsString('DebugBar\\\DataCollector\\\ConfigCollector', $responseBody); + } + + protected function dispatchApplication(array $server, array $pipe = []): ResponseInterface + { + $container = $this->createContainer($server); + + $appFactory = new ApplicationFactory(); + $app = $appFactory($container); + + $app->pipe(RouteMiddleware::class); + + $app->pipe(PhpDebugBarMiddleware::class); + + $app->pipe(DispatchMiddleware::class); + + foreach ($pipe as $pattern => $middleware) { + $app->get($pattern, $middleware); + } + + $app->run(); + + return $container->get(EmitterInterface::class)->getResponse(); + } + + private function createContainer(array $server): ContainerInterface + { + $config = ConfigProvider::getConfig(); + $config['debug'] = true; + + $serviceManagerConfig = $config['dependencies']; + $serviceManagerConfig['services']['config'] = $config; + $serviceManagerConfig['services'][EmitterInterface::class] = new TestEmitter(); + $serviceManagerConfig['services'][ServerRequestInterface::class] = function() use ($server) { + return ServerRequestFactory::fromGlobals($server, [], [], [], []); + }; + $serviceManagerConfig['factories'][MiddlewareFactory::class] = MiddlewareFactoryFactory::class; + $serviceManagerConfig['factories'][MiddlewareContainer::class] = MiddlewareContainerFactory::class; + $serviceManagerConfig['factories'][MiddlewarePipe::class] = InvokableFactory::class; + $serviceManagerConfig['factories'][RouteCollector::class] = RouteCollectorFactory::class; + $serviceManagerConfig['factories'][FastRouteRouter::class] = FastRouteRouterFactory::class; + $serviceManagerConfig['factories'][RequestHandlerRunner::class] = RequestHandlerRunnerFactory::class; + $serviceManagerConfig['factories'][ServerRequestErrorResponseGenerator::class] = ServerRequestErrorResponseGeneratorFactory::class; + $serviceManagerConfig['factories'][ResponseInterface::class] = ResponseFactoryFactory::class; + $serviceManagerConfig['factories'][RouteMiddleware::class] = RouteMiddlewareFactory::class; + $serviceManagerConfig['factories'][DispatchMiddleware::class] = DispatchMiddlewareFactory::class; + $serviceManagerConfig['factories'][ResponseFactory::class] = InvokableFactory::class; + $serviceManagerConfig['factories'][StreamFactory::class] = InvokableFactory::class; + $serviceManagerConfig['aliases'][RouterInterface::class] = FastRouteRouter::class; + $serviceManagerConfig['aliases'][\Mezzio\ApplicationPipeline::class] = MiddlewarePipe::class; + $serviceManagerConfig['aliases'][ResponseFactoryInterface::class] = ResponseFactory::class; + $serviceManagerConfig['aliases'][StreamFactoryInterface::class] = StreamFactory::class; + + return new ServiceManager($serviceManagerConfig); + } +} diff --git a/test/PhpDebugBarMiddlewareFactoryTest.php b/test/PhpDebugBarMiddlewareFactoryTest.php deleted file mode 100644 index 215bef4..0000000 --- a/test/PhpDebugBarMiddlewareFactoryTest.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -class PhpDebugBarMiddlewareFactoryTest extends PHPUnit_Framework_TestCase -{ - public function testFactory() - { - $factory = new PhpDebugBarMiddlewareFactory(); - - $result = $factory(); - - $this->assertInstanceOf(PhpDebugBarMiddleware::class, $result); - } -} diff --git a/test/PhpDebugBarMiddlewareTest.php b/test/PhpDebugBarMiddlewareTest.php index f474d81..423b195 100644 --- a/test/PhpDebugBarMiddlewareTest.php +++ b/test/PhpDebugBarMiddlewareTest.php @@ -1,35 +1,46 @@ */ -class PhpDebugBarMiddlewareTest extends PHPUnit_Framework_TestCase +class PhpDebugBarMiddlewareTest extends TestCase { protected $debugbarRenderer; + /** @var PhpDebugBarMiddleware */ protected $middleware; - protected function setUp() + protected function setUp(): void { $this->debugbarRenderer = $this->getMockBuilder(JavascriptRenderer::class)->disableOriginalConstructor()->getMock(); - $this->middleware = new PhpDebugBarMiddleware($this->debugbarRenderer); + $this->debugbarRenderer->method('renderHead')->willReturn('RenderHead'); + $this->debugbarRenderer->method('getBaseUrl')->willReturn('/phpdebugbar'); + $this->debugbarRenderer->method('render')->willReturn('RenderBody'); + $responseFactory = new ResponseFactory(); + $streamFactory = new StreamFactory(); + + $this->middleware = new PhpDebugBarMiddleware($this->debugbarRenderer, $responseFactory, $streamFactory); } - public function testNotAttachIfNotAccept() + public function testTwoPassCallingForCompatibility(): void { $request = new ServerRequest(); $response = new Response(); + $response->getBody()->write('ResponseBody'); $calledOut = false; $outFunction = function ($request, $response) use (&$calledOut) { $calledOut = true; @@ -39,99 +50,234 @@ public function testNotAttachIfNotAccept() $result = call_user_func($this->middleware, $request, $response, $outFunction); $this->assertTrue($calledOut, 'Out is not called'); + $this->assertSame('ResponseBody', (string) $result->getBody()); $this->assertSame($response, $result); } - public function testAttachToNoneHtmlResponse() + public function testNotAttachIfNotAccept(): void { - $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $request = new ServerRequest(); $response = new Response(); $response->getBody()->write('ResponseBody'); - $calledOut = false; - $outFunction = function ($request, $response) use (&$calledOut) { - $calledOut = true; - return $response; - }; + $requestHandler = new RequestHandlerStub($response); - $this->debugbarRenderer->expects($this->once())->method('renderHead')->willReturn('RenderHead'); - $this->debugbarRenderer->expects($this->once())->method('render')->willReturn('RenderBody'); + $result = $this->middleware->process($request, $requestHandler); - $result = call_user_func($this->middleware, $request, $response, $outFunction); + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame('ResponseBody', (string) $result->getBody()); + $this->assertSame($response, $result); + } + + public function testForceAttachDebugbarIfHeaderPresents(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json', 'X-Enable-Debug-Bar' => 'true']); + $response = new Response(); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); + } + + public function testForceAttachDebugbarIfCookiePresents(): void + { + $cookies = ['X-Enable-Debug-Bar' => 'true']; + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json'], $cookies); + $response = new Response(); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); + } + + public function testForceAttachDebugbarIfAttributePresents(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json']); + $request = $request->withAttribute('X-Enable-Debug-Bar', 'true'); + $response = new Response(); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); - $this->assertTrue($calledOut, 'Out is not called'); - $this->assertNotSame($response, $result); $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); } - public function testAttachToHtmlResponse() + public function testAttachToNoneHtmlResponse(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $response = (new Response())->withHeader('test-header', 'value'); + $response->getBody()->write('ResponseBody'); + + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertNotSame($response, $result); + $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\nTest-Header: value\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); + } + + public function testNotAttachToRedirectResponse(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $response = (new Response())->withStatus(300)->withAddedHeader('Location', 'some-location'); + + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertSame($response, $result); + } + + public function testAttachToNonRedirectResponse(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $response = (new Response())->withStatus(299)->withAddedHeader('Location', 'some-location'); + + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertNotSame($response, $result); + } + + public function testAttachToNonRedirectResponse2(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $response = (new Response())->withStatus(400)->withAddedHeader('Location', 'some-location'); + + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertNotSame($response, $result); + } + + public function testAttachToRedirectResponseWithoutLocation(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $response = (new Response())->withStatus(302); + + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertNotSame($response, $result); + $this->assertSame("RenderHead

DebugBar

Response:

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

DebugBar

Response:

HTTP/1.1 302 Found\r\nLocation: some-location\r\n\r\n
RenderBody", (string) $result->getBody()); + } + + public function testAttachToHtmlResponse(): void { $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); $response->getBody()->write('ResponseBody'); - $calledOut = false; - $outFunction = function ($request, $response) use (&$calledOut) { - $calledOut = true; - return $response; - }; + $requestHandler = new RequestHandlerStub($response); - $this->debugbarRenderer->expects($this->once())->method('renderHead')->willReturn('RenderHead'); - $this->debugbarRenderer->expects($this->once())->method('render')->willReturn('RenderBody'); + $result = $this->middleware->process($request, $requestHandler); - $result = call_user_func($this->middleware, $request, $response, $outFunction); + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame($response, $result); + $this->assertSame('ResponseBodyRenderHeadRenderBody', (string) $result->getBody()); + } - $this->assertTrue($calledOut, 'Out is not called'); + public function testForceNotAttachDebugbarIfHeaderPresents(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html', 'X-Enable-Debug-Bar' => 'false']); + $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame($response, $result); + $this->assertSame('ResponseBody', (string) $result->getBody()); + } + + public function testForceNotAttachDebugbarIfCookiePresents(): void + { + $cookie = ['X-Enable-Debug-Bar' => 'false']; + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html'], $cookie); + $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); $this->assertSame($response, $result); - $this->assertSame("ResponseBodyRenderHeadRenderBody", (string) $result->getBody()); + $this->assertSame('ResponseBody', (string) $result->getBody()); } - public function testAppendsToEndOfHtmlResponse() + public function testForceNotAttachDebugbarIfAttributePresents(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $request = $request->withAttribute('X-Enable-Debug-Bar', 'false'); + $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame($response, $result); + $this->assertSame('ResponseBody', (string) $result->getBody()); + } + + public function testAppendsToEndOfHtmlResponse(): void { $html = 'FooContent'; $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); $response = new Response\HtmlResponse($html); - $calledOut = false; - $outFunction = function ($request, $response) use (&$calledOut) { - $calledOut = true; - return $response; - }; - - $this->debugbarRenderer->expects($this->once())->method('renderHead')->willReturn('RenderHead'); - $this->debugbarRenderer->expects($this->once())->method('render')->willReturn('RenderBody'); + $requestHandler = new RequestHandlerStub($response); - $result = call_user_func($this->middleware, $request, $response, $outFunction); + $result = $this->middleware->process($request, $requestHandler); - $this->assertTrue($calledOut, 'Out is not called'); + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); $this->assertSame($response, $result); $this->assertSame($html . 'RenderHeadRenderBody', (string) $result->getBody()); } - public function testTryToHandleNotExistingStaticFile() + public function testTryToHandleNotExistingStaticFile(): void { - $this->debugbarRenderer->expects($this->any())->method('getBaseUrl')->willReturn('/phpdebugbar'); - $uri = new Uri('http://example.com/phpdebugbar/boo.css'); $request = new ServerRequest([], [], $uri, null, 'php://memory'); $response = new Response\HtmlResponse(''); + $requestHandler = new RequestHandlerStub($response); - $calledOut = false; - $outFunction = function ($request, $response) use (&$calledOut) { - $calledOut = true; - return $response; - }; + $result = $this->middleware->process($request, $requestHandler); - $result = call_user_func($this->middleware, $request, $response, $outFunction); - $this->assertTrue($calledOut, 'Out is not called'); + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); $this->assertSame($response, $result); } /** * @dataProvider getContentTypes */ - public function testHandleStaticFile($extension, $contentType) + public function testHandleStaticFile(string $extension, string $contentType): void { $root = vfsStream::setup('boo'); - $this->debugbarRenderer->expects($this->any())->method('getBaseUrl')->willReturn('/phpdebugbar'); $this->debugbarRenderer->expects($this->any())->method('getBasePath')->willReturn(vfsStream::url('boo')); $uri = new Uri(sprintf('http://example.com/phpdebugbar/debugbar.%s', $extension)); @@ -140,20 +286,18 @@ public function testHandleStaticFile($extension, $contentType) vfsStream::newFile(sprintf('debugbar.%s', $extension))->withContent('filecontent')->at($root); - $calledOut = false; - $outFunction = function ($request, $response) use (&$calledOut) { - $calledOut = true; - return $response; - }; + $requestHandler = new RequestHandlerStub($response); - $result = call_user_func($this->middleware, $request, $response, $outFunction); - $this->assertFalse($calledOut, 'Out is called'); + $result = $this->middleware->process($request, $requestHandler); + + $this->assertFalse($requestHandler->isCalled(), 'Request handler is called'); $this->assertNotSame($response, $result); $this->assertSame($contentType, $result->getHeaderLine('Content-type')); + $this->assertSame(200, $result->getStatusCode()); $this->assertSame('filecontent', (string) $result->getBody()); } - public function getContentTypes() + public function getContentTypes(): array { return [ ['css', 'text/css'], diff --git a/test/RequestHandlerStub.php b/test/RequestHandlerStub.php new file mode 100644 index 0000000..2fb41da --- /dev/null +++ b/test/RequestHandlerStub.php @@ -0,0 +1,32 @@ +response = $response; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->called = true; + + return $this->response; + } + + public function isCalled(): bool + { + return $this->called; + } +} \ No newline at end of file diff --git a/test/Slim3Test.php b/test/Slim3Test.php index fc67b7e..9a7cefd 100644 --- a/test/Slim3Test.php +++ b/test/Slim3Test.php @@ -1,22 +1,37 @@ getContainer()['environment'] = function() use ($server) { + $container = $app->getContainer(); + $container[ResponseFactoryInterface::class] = new ResponseFactory(); + $container[StreamFactoryInterface::class] = new StreamFactory(); + $container['environment'] = function() use ($server) { return new Environment($server); }; - $middlewareFactory = new PhpDebugBarMiddlewareFactory(); - $middleware = $middlewareFactory(); + $config = ConfigProvider::getConfig(); + + foreach ($config['dependencies']['factories'] as $key => $factory) { + $container[$key] = new $factory(); + } + + $middleware = $container->get(PhpDebugBarMiddleware::class); $app->add($middleware); diff --git a/test/TestEmitter.php b/test/TestEmitter.php index 6bc5b18..fae807b 100644 --- a/test/TestEmitter.php +++ b/test/TestEmitter.php @@ -1,23 +1,24 @@ response = $response; - return $response; + return true; } - public function getResponse() + public function getResponse(): ResponseInterface { if ($this->response instanceof ResponseInterface) { return $this->response; diff --git a/test/ZendExpressiveTest.php b/test/ZendExpressiveTest.php deleted file mode 100644 index 7296c2c..0000000 --- a/test/ZendExpressiveTest.php +++ /dev/null @@ -1,58 +0,0 @@ -testEmitter = new TestEmitter(); - } - - protected function dispatchApplication(array $server, array $pipe = []) - { - $container = $this->createContainer(); - - $appFactory = new ApplicationFactory(); - $app = $appFactory($container); - - foreach ($pipe as $pattern => $middleware) { - $app->get($pattern, $middleware); - } - - $app->pipeRoutingMiddleware(); - $app->pipeDispatchMiddleware(); - - $serverRequest = ServerRequestFactory::fromGlobals($server); - - $app->run($serverRequest); - - return $this->testEmitter->getResponse(); - } - - /** - * - * @return ContainerInterface - */ - private function createContainer() - { - $config = ConfigProvider::getConfig(); - - $serviceManagerConfig = $config['dependencies']; - $serviceManagerConfig['services']['config'] = $config; - $serviceManagerConfig['services'][EmitterInterface::class] = $this->testEmitter; - - return new ServiceManager($serviceManagerConfig); - } -}