diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d7c1298..fbb289cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - The real request method and target url are now displayed in the profiler. +### Changed + +- The profiler design has been updated. + ## 1.4.0 - 2017-02-21 ### Changed diff --git a/Collector/ProfileClient.php b/Collector/ProfileClient.php index 297d6296..aaf0c6b2 100644 --- a/Collector/ProfileClient.php +++ b/Collector/ProfileClient.php @@ -3,9 +3,11 @@ namespace Http\HttplugBundle\Collector; use Http\Client\Common\FlexibleHttpClient; +use Http\Client\Exception\HttpException; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; /** * The ProfileClient decorates any client that implement both HttpClient and HttpAsyncClient interfaces to gather target @@ -27,12 +29,18 @@ class ProfileClient implements HttpClient, HttpAsyncClient */ private $collector; + /** + * @var Formatter + */ + private $formatter; + /** * @param HttpClient|HttpAsyncClient $client The client to profile. Client must implement both HttpClient and * HttpAsyncClient interfaces. * @param Collector $collector + * @param Formatter $formatter */ - public function __construct($client, Collector $collector) + public function __construct($client, Collector $collector, Formatter $formatter) { if (!($client instanceof HttpClient && $client instanceof HttpAsyncClient)) { throw new \RuntimeException(sprintf( @@ -45,6 +53,7 @@ public function __construct($client, Collector $collector) } $this->client = $client; $this->collector = $collector; + $this->formatter = $formatter; } /** @@ -52,9 +61,18 @@ public function __construct($client, Collector $collector) */ public function sendAsyncRequest(RequestInterface $request) { - $this->collectRequestInformations($request); + $stack = $this->collector->getCurrentStack(); + $this->collectRequestInformations($request, $stack); - return $this->client->sendAsyncRequest($request); + return $this->client->sendAsyncRequest($request)->then(function (ResponseInterface $response) use ($stack) { + $this->collectResponseInformations($response, $stack); + + return $response; + }, function (\Exception $exception) use ($stack) { + $this->collectExceptionInformations($exception, $stack); + + throw $exception; + }); } /** @@ -62,22 +80,67 @@ public function sendAsyncRequest(RequestInterface $request) */ public function sendRequest(RequestInterface $request) { - $this->collectRequestInformations($request); + $stack = $this->collector->getCurrentStack(); + $this->collectRequestInformations($request, $stack); + + try { + $response = $this->client->sendRequest($request); - return $this->client->sendRequest($request); + $this->collectResponseInformations($response, $stack); + + return $response; + } catch (\Exception $e) { + $this->collectExceptionInformations($e, $stack); + + throw $e; + } } /** * @param RequestInterface $request + * @param Stack|null $stack */ - private function collectRequestInformations(RequestInterface $request) + private function collectRequestInformations(RequestInterface $request, Stack $stack = null) { - if (!$stack = $this->collector->getCurrentStack()) { + if (!$stack) { return; } - $stack = $this->collector->getCurrentStack(); $stack->setRequestTarget($request->getRequestTarget()); $stack->setRequestMethod($request->getMethod()); + $stack->setRequestScheme($request->getUri()->getScheme()); + $stack->setRequestHost($request->getUri()->getHost()); + $stack->setClientRequest($this->formatter->formatRequest($request)); + } + + /** + * @param ResponseInterface $response + * @param Stack|null $stack + */ + private function collectResponseInformations(ResponseInterface $response, Stack $stack = null) + { + if (!$stack) { + return; + } + + $stack->setResponseCode($response->getStatusCode()); + $stack->setClientResponse($this->formatter->formatResponse($response)); + } + + /** + * @param \Exception $exception + * @param Stack|null $stack + */ + private function collectExceptionInformations(\Exception $exception, Stack $stack = null) + { + if ($exception instanceof HttpException) { + $this->collectResponseInformations($exception->getResponse(), $stack); + } + + if (!$stack) { + return; + } + + $stack->setClientException($this->formatter->formatException($exception)); } } diff --git a/Collector/ProfileClientFactory.php b/Collector/ProfileClientFactory.php index 46ddc1fb..b9422094 100644 --- a/Collector/ProfileClientFactory.php +++ b/Collector/ProfileClientFactory.php @@ -26,17 +26,24 @@ class ProfileClientFactory implements ClientFactory */ private $collector; + /** + * @var Formatter + */ + private $formatter; + /** * @param ClientFactory|callable $factory * @param Collector $collector + * @param Formatter $formatter */ - public function __construct($factory, Collector $collector) + public function __construct($factory, Collector $collector, Formatter $formatter) { if (!$factory instanceof ClientFactory && !is_callable($factory)) { throw new \RuntimeException(sprintf('First argument to ProfileClientFactory::__construct must be a "%s" or a callable.', ClientFactory::class)); } $this->factory = $factory; $this->collector = $collector; + $this->formatter = $formatter; } /** @@ -50,6 +57,6 @@ public function createClient(array $config = []) $client = new FlexibleHttpClient($client); } - return new ProfileClient($client, $this->collector); + return new ProfileClient($client, $this->collector, $this->formatter); } } diff --git a/Collector/Stack.php b/Collector/Stack.php index 77be7c5a..5431525d 100644 --- a/Collector/Stack.php +++ b/Collector/Stack.php @@ -46,6 +46,36 @@ final class Stack */ private $requestMethod; + /** + * @var string + */ + private $requestHost; + + /** + * @var string + */ + private $requestScheme; + + /** + * @var string + */ + private $clientRequest; + + /** + * @var string + */ + private $clientResponse; + + /** + * @var string + */ + private $clientException; + + /** + * @var int + */ + private $responseCode; + /** * @param string $client * @param string $request @@ -151,4 +181,100 @@ public function setRequestMethod($requestMethod) { $this->requestMethod = $requestMethod; } + + /** + * @return string + */ + public function getClientRequest() + { + return $this->clientRequest; + } + + /** + * @param string $clientRequest + */ + public function setClientRequest($clientRequest) + { + $this->clientRequest = $clientRequest; + } + + /** + * @return mixed + */ + public function getClientResponse() + { + return $this->clientResponse; + } + + /** + * @param mixed $clientResponse + */ + public function setClientResponse($clientResponse) + { + $this->clientResponse = $clientResponse; + } + + /** + * @return string + */ + public function getClientException() + { + return $this->clientException; + } + + /** + * @param string $clientException + */ + public function setClientException($clientException) + { + $this->clientException = $clientException; + } + + /** + * @return int + */ + public function getResponseCode() + { + return $this->responseCode; + } + + /** + * @param int $responseCode + */ + public function setResponseCode($responseCode) + { + $this->responseCode = $responseCode; + } + + /** + * @return string + */ + public function getRequestHost() + { + return $this->requestHost; + } + + /** + * @param string $requestHost + */ + public function setRequestHost($requestHost) + { + $this->requestHost = $requestHost; + } + + /** + * @return string + */ + public function getRequestScheme() + { + return $this->requestScheme; + } + + /** + * @param string $requestScheme + */ + public function setRequestScheme($requestScheme) + { + $this->requestScheme = $requestScheme; + } } diff --git a/Collector/Twig/HttpMessageMarkupExtension.php b/Collector/Twig/HttpMessageMarkupExtension.php index b6c5c904..71077f74 100644 --- a/Collector/Twig/HttpMessageMarkupExtension.php +++ b/Collector/Twig/HttpMessageMarkupExtension.php @@ -39,7 +39,7 @@ public function markup($message) // make header names bold $headers = preg_replace("|\n(.*?): |si", "\n$1: ", $parts[0]); - return sprintf("%s\n\n
%s
", $headers, $parts[1]); + return sprintf("%s\n\n
%s
", $headers, $parts[1]); } public function getName() diff --git a/Resources/config/data-collector.xml b/Resources/config/data-collector.xml index 7c814014..dd673066 100644 --- a/Resources/config/data-collector.xml +++ b/Resources/config/data-collector.xml @@ -34,26 +34,32 @@ + + + + + + diff --git a/Resources/public/script/httplug.js b/Resources/public/script/httplug.js index 4740a29f..52ae0bd0 100644 --- a/Resources/public/script/httplug.js +++ b/Resources/public/script/httplug.js @@ -1,130 +1,12 @@ /** - * Toggle hide/show on the message body - */ -function httplug_toggleBody(el) { - var bodies = document.querySelectorAll(".httplug-http-body"); - - httplug_toggleVisibility(bodies); - - var newLabel = el.getAttribute("data-label"); - var oldLabel = el.innerHTML; - el.innerHTML = newLabel; - el.setAttribute("data-label", oldLabel); -} - -function httplug_togglePluginStack(el) { - var requestTable = httplug_getClosest(el, '.httplug-request-table'); - var stacks = requestTable.querySelectorAll('.httplug-request-stack'); - - httplug_toggleVisibility(stacks, "table-row"); - - var newLabel = el.getAttribute("data-label"); - var oldLabel = el.innerHTML; - el.innerHTML = newLabel; - el.setAttribute("data-label", oldLabel); -} - - - -/** - * Get the closest matching element up the DOM tree. - * - * {@link https://gomakethings.com/climbing-up-and-down-the-dom-tree-with-vanilla-javascript/} - * - * @param {Element} elem Starting element - * @param {String} selector Selector to match against (class, ID, data attribute, or tag) - * @return {Boolean|Element} Returns null if not match found - */ -var httplug_getClosest = function ( elem, selector ) { - - // Variables - var firstChar = selector.charAt(0); - var supports = 'classList' in document.documentElement; - var attribute, value; - - // If selector is a data attribute, split attribute from value - if ( firstChar === '[' ) { - selector = selector.substr( 1, selector.length - 2 ); - attribute = selector.split( '=' ); - - if ( attribute.length > 1 ) { - value = true; - attribute[1] = attribute[1].replace( /"/g, '' ).replace( /'/g, '' ); - } - } - - // Get closest match - for ( ; elem && elem !== document && elem.nodeType === 1; elem = elem.parentNode ) { - - // If selector is a class - if ( firstChar === '.' ) { - if ( supports ) { - if ( elem.classList.contains( selector.substr(1) ) ) { - return elem; - } - } else { - if ( new RegExp('(^|\\s)' + selector.substr(1) + '(\\s|$)').test( elem.className ) ) { - return elem; - } - } - } - - // If selector is an ID - if ( firstChar === '#' ) { - if ( elem.id === selector.substr(1) ) { - return elem; - } - } - - // If selector is a data attribute - if ( firstChar === '[' ) { - if ( elem.hasAttribute( attribute[0] ) ) { - if ( value ) { - if ( elem.getAttribute( attribute[0] ) === attribute[1] ) { - return elem; - } - } else { - return elem; - } - } - } - - // If selector is a tag - if ( elem.tagName.toLowerCase() === selector ) { - return elem; - } - - } - - return null; - -}; - -/** - * Check if element is hidden. - * @param el - * @returns {boolean} - */ -var httplug_isHidden = function (el) { - var style = window.getComputedStyle(el); - return (style.display === 'none') -} - -/** - * Toggle visibility on elements - * @param els - * @param display defaults to "block" - */ -var httplug_toggleVisibility = function (els, display) { - if (typeof display === 'undefined') { - display = "block"; - } - - for (var i = 0; i < els.length; i++) { - if (httplug_isHidden(els[i])) { - els[i].style.display = display; - } else { - els[i].style.display = "none"; - } - } -}; + * Toggle visibility on elements. + */ +document.addEventListener("DOMContentLoaded", function() { + Array.prototype.forEach.call(document.getElementsByClassName('httplug-toggle'), function (source) { + source.addEventListener('click', function() { + Array.prototype.forEach.call(document.querySelectorAll(source.getAttribute('data-toggle')), function (target) { + target.classList.toggle('httplug-hidden'); + }); + }); + }); +}); diff --git a/Resources/public/style/httplug.css b/Resources/public/style/httplug.css index 907350ca..a7a06141 100644 --- a/Resources/public/style/httplug.css +++ b/Resources/public/style/httplug.css @@ -1,24 +1,128 @@ - -.push-right { +.httplug-push-right { float: right; } -.httplug-http-body { +.httplug-plugin-name { + font-size: 130%; + font-weight: bold; + text-align: center; +} + +.httplug-hidden { display: none; } -.httplug-request-table { +.httplug-toggle { + cursor: pointer; +} +.httplug-center { + text-align: center; } -.httplug-plugin-name { - font-size: 130%; - font-weight: bold; +.httplug-message { + box-sizing: border-box; + padding: 5px; + flex: 1; + margin: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.httplug-request-stack { +.httplug-stack-header { + display: flex; + justify-content: space-between; + + background: #FFF; + border: 1px solid #E0E0E0; + box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); + margin: 1em 0; + padding: 10px; +} + +.httplug-stack-failed { + color:#B0413E; +} + +.httplug-stack-success { + color: #4F805D; +} + +.httplug-stack-header .httplug-stack-header-target { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + background: white; + color: black; + font-size: 0; /*hide line return spacings*/ +} + +.httplug-messages { + clear: both; + display: flex; +} + +.httplug-scheme-http { display: none; } -.httplug-error { - color: red; + +.httplug-scheme-https { + color: green; +} + +.httplug-target, .httplug-scheme { + font-weight: normal; +} + +.httplug-target, .httplug-host, .httplug-scheme { + font-size: 12px; +} + +/** + * HTTP method colors from swagger-ui. + */ +.httplug-method.label { + color: black; + width: 9ch; + text-align: center; +} + +.httplug-method-post.label { + background: #49cc90; +} + +.httplug-method-get.label { + background: #61affe; +} + +.httplug-method-put.label { + background: #fca130; +} + +.httplug-method-delete.label { + background: #f93e3e; +} + +.httplug-method-head.label { + background: #9012fe; + color: white; +} + +.httplug-method-patch.label { + background: #50e3c2; +} + +.httplug-method-options.label { + background: #0d5aa7; + color: white; +} + +.httplug-method-connect.label { + background: #ebebeb; +} + +.httplug-method-trace.label { + background: #ebebeb; } diff --git a/Resources/views/webprofiler.html.twig b/Resources/views/webprofiler.html.twig index 62d6aa1a..93250cf7 100644 --- a/Resources/views/webprofiler.html.twig +++ b/Resources/views/webprofiler.html.twig @@ -1,7 +1,5 @@ {% extends '@WebProfiler/Profiler/layout.html.twig' %} -{% import _self as macro %} - {% block toolbar %} {% if collector.stacks|length > 0 %} {% set icon %} @@ -48,8 +46,6 @@ {% block panel %}

HTTPlug

- -
{% for client in collector.clients %}
@@ -61,13 +57,66 @@

{% for stack in collector.clientStacks(client) %} -

- Request #{{ loop.index }} - {{ stack.requestMethod }} {{ stack.requestTarget }} - {% if stack.failed %} - - Errored +
+
+ {% if stack.failed %} + + {% else %} + + {% endif %} + {{ stack.requestMethod }} +
+
+ {{ stack.requestScheme }}:// + {{ stack.requestHost }} + {{ stack.requestTarget }} +
+
+ {% if stack.responseCode >= 400 and stack.responseCode <= 599 %} + {{ stack.responseCode }} + {% elseif stack.responseCode >= 300 and stack.responseCode <= 399 %} + {{ stack.responseCode }} + {% else %} + {{ stack.responseCode }} + {% endif %} +
+
+
+ +
+
+

Request

+ {{ stack.clientRequest|httplug_markup|nl2br }} +
+
+

Response

+ {{ stack.clientResponse|httplug_markup|nl2br }} +
+
+ {% if stack.profiles %} +
+ +
+
+ {% for profile in stack.profiles %} +

{{ profile.plugin }}

+
+
+

Request

+ {{ profile.request|httplug_markup|nl2br }} +
+
+

Response

+ {{ profile.response|httplug_markup|nl2br }} +
+
+ {% if not loop.last %} +
+ {% endif %} + {% endfor %} +
{% endif %} -

- {{ macro.printMessages(stack) }} +
{% endfor %}
@@ -76,58 +125,5 @@

No request were sent.

{% endfor %} - - {% endblock %} - -{% macro printMessages(stack) %} - - - - - - - - - - - {% if stack.profiles %} - - - - - - - - {% for profile in stack.profiles %} - - - - - - - - - {% endfor %} - - - - - {% endif %} -
RequestResponse
{{ stack.request|httplug_markup|nl2br }}{{ stack.response|httplug_markup|nl2br }}
- -
↓ Start - End - {% if stack.failed %} - - {% endif %} -
↓ {{ profile.plugin }} ↑ - {% if profile.failed %} - - {% endif %} -
{{ profile.request|httplug_markup|nl2br }}{{ profile.response|httplug_markup|nl2br }}
HTTP client↑ - {#{% if profile.failed %}#} - {##} - {#{% endif %}#} -
-{% endmacro %} diff --git a/Tests/Unit/Collector/ProfileClientTest.php b/Tests/Unit/Collector/ProfileClientTest.php index 0c2e526f..6c1eec53 100644 --- a/Tests/Unit/Collector/ProfileClientTest.php +++ b/Tests/Unit/Collector/ProfileClientTest.php @@ -5,11 +5,13 @@ use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use Http\HttplugBundle\Collector\Collector; +use Http\HttplugBundle\Collector\Formatter; use Http\HttplugBundle\Collector\ProfileClient; use Http\HttplugBundle\Collector\Stack; use Http\Promise\Promise; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriInterface; class ProfileClientTest extends \PHPUnit_Framework_TestCase { @@ -33,6 +35,11 @@ class ProfileClientTest extends \PHPUnit_Framework_TestCase */ private $request; + /** + * @var Formatter + */ + private $formatter; + /** * @var ProfileClient */ @@ -48,21 +55,37 @@ class ProfileClientTest extends \PHPUnit_Framework_TestCase */ private $promise; + /** + * @var UriInterface + */ + private $uri; + public function setUp() { $this->collector = $this->getMockBuilder(Collector::class)->disableOriginalConstructor()->getMock(); $this->currentStack = new Stack('default', 'FormattedRequest'); $this->client = $this->getMockBuilder(ClientInterface::class)->getMock(); $this->request = $this->getMockBuilder(RequestInterface::class)->getMock(); - $this->subject = new ProfileClient($this->client, $this->collector); + $this->formatter = $this->getMockBuilder(Formatter::class)->disableOriginalConstructor()->getMock(); + $this->subject = new ProfileClient($this->client, $this->collector, $this->formatter); $this->response = $this->getMockBuilder(ResponseInterface::class)->getMock(); $this->promise = $this->getMockBuilder(Promise::class)->getMock(); + $this->uri = $this->getMockBuilder(UriInterface::class)->getMock(); $this->client->method('sendRequest')->willReturn($this->response); - $this->client->method('sendAsyncRequest')->willReturn($this->promise); + $this->client->method('sendAsyncRequest')->will($this->returnCallback(function () { + $promise = $this->getMockBuilder(Promise::class)->getMock(); + $promise->method('then')->willReturn($this->promise); + + return $promise; + })); $this->request->method('getMethod')->willReturn('GET'); $this->request->method('getRequestTarget')->willReturn('/target'); + $this->request->method('getUri')->willReturn($this->uri); + + $this->uri->method('getScheme')->willReturn('https'); + $this->uri->method('getHost')->willReturn('example.com'); $this->collector->method('getCurrentStack')->willReturn($this->currentStack); } @@ -82,7 +105,6 @@ public function testCallDecoratedClient() ; $this->assertEquals($this->response, $this->subject->sendRequest($this->request)); - $this->assertEquals($this->promise, $this->subject->sendAsyncRequest($this->request)); } @@ -92,6 +114,8 @@ public function testCollectRequestInformations() $this->assertEquals('GET', $this->currentStack->getRequestMethod()); $this->assertEquals('/target', $this->currentStack->getRequestTarget()); + $this->assertEquals('example.com', $this->currentStack->getRequestHost()); + $this->assertEquals('https', $this->currentStack->getRequestScheme()); } public function testCollectAsyncRequestInformations() @@ -100,6 +124,8 @@ public function testCollectAsyncRequestInformations() $this->assertEquals('GET', $this->currentStack->getRequestMethod()); $this->assertEquals('/target', $this->currentStack->getRequestTarget()); + $this->assertEquals('example.com', $this->currentStack->getRequestHost()); + $this->assertEquals('https', $this->currentStack->getRequestScheme()); } }