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
+
+
+
+
+
+
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) %}
-
-
- Request |
- Response |
-
-
-
- {{ stack.request|httplug_markup|nl2br }} |
- {{ stack.response|httplug_markup|nl2br }} |
-
- {% if stack.profiles %}
-
-
-
- |
-
-
- ↓ Start |
- - End
- {% if stack.failed %}
- ☓
- {% endif %}
- |
-
- {% for profile in stack.profiles %}
-
- ↓ {{ profile.plugin }} |
- ↑
- {% if profile.failed %}
- ☓
- {% endif %}
- |
-
-
- {{ profile.request|httplug_markup|nl2br }} |
- {{ profile.response|httplug_markup|nl2br }} |
-
- {% endfor %}
-
- ⟶ HTTP client |
- ↑
- {#{% if profile.failed %}#}
- {#☓#}
- {#{% endif %}#}
- |
-
- {% 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());
}
}