diff --git a/ClientFactory/PluginClientFactory.php b/ClientFactory/PluginClientFactory.php index 120066e3..f0ff28dc 100644 --- a/ClientFactory/PluginClientFactory.php +++ b/ClientFactory/PluginClientFactory.php @@ -15,12 +15,13 @@ final class PluginClientFactory /** * @param Plugin[] $plugins * @param ClientFactory $factory - * @param array $config + * @param array $config config to the client factory + * @param array $pluginClientOptions config forwarded to the PluginClient * * @return PluginClient */ - public static function createPluginClient(array $plugins, ClientFactory $factory, array $config) + public static function createPluginClient(array $plugins, ClientFactory $factory, array $config, array $pluginClientOptions = []) { - return new PluginClient($factory->createClient($config), $plugins); + return new PluginClient($factory->createClient($config), $plugins, $pluginClientOptions); } } diff --git a/Collector/DebugPlugin.php b/Collector/DebugPlugin.php new file mode 100644 index 00000000..224b3bef --- /dev/null +++ b/Collector/DebugPlugin.php @@ -0,0 +1,63 @@ + + */ +final class DebugPlugin implements Plugin +{ + /** + * @var DebugPluginCollector + */ + private $collector; + + /** + * @var string + */ + private $clientName; + + /** + * @var int + */ + private $depth = -1; + + /** + * @param DebugPluginCollector $collector + * @param string $clientName + */ + public function __construct(DebugPluginCollector $collector, $clientName) + { + $this->collector = $collector; + $this->clientName = $clientName; + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first) + { + $collector = $this->collector; + $clientName = $this->clientName; + $depth = &$this->depth; + + $collector->addRequest($request, $clientName, ++$depth); + + return $next($request)->then(function (ResponseInterface $response) use ($collector, $clientName, &$depth) { + $collector->addResponse($response, $clientName, $depth--); + + return $response; + }, function (Exception $exception) use ($collector, $clientName, &$depth) { + $collector->addFailure($exception, $clientName, $depth--); + + throw $exception; + }); + } +} diff --git a/Collector/DebugPluginCollector.php b/Collector/DebugPluginCollector.php new file mode 100644 index 00000000..220557b5 --- /dev/null +++ b/Collector/DebugPluginCollector.php @@ -0,0 +1,181 @@ + + */ +final class DebugPluginCollector extends DataCollector +{ + /** + * @var Formatter + */ + private $formatter; + + /** + * @var PluginJournal + */ + private $journal; + + /** + * @param Formatter $formatter + * @param PluginJournal $journal + */ + public function __construct(Formatter $formatter, PluginJournal $journal) + { + $this->formatter = $formatter; + $this->journal = $journal; + } + + /** + * @param RequestInterface $request + * @param string $clientName + * @param int $depth + */ + public function addRequest(RequestInterface $request, $clientName, $depth) + { + $this->data[$clientName]['request'][$depth][] = $this->formatter->formatRequest($request); + } + + /** + * @param ResponseInterface $response + * @param string $clientName + * @param int $depth + */ + public function addResponse(ResponseInterface $response, $clientName, $depth) + { + $this->data[$clientName]['response'][$depth][] = $this->formatter->formatResponse($response); + $this->data[$clientName]['failure'][$depth][] = false; + } + + /** + * @param Exception $exception + * @param string $clientName + * @param int $depth + */ + public function addFailure(Exception $exception, $clientName, $depth) + { + if ($exception instanceof Exception\HttpException) { + $formattedResponse = $this->formatter->formatResponse($exception->getResponse()); + } elseif ($exception instanceof Exception\TransferException) { + $formattedResponse = $exception->getMessage(); + } else { + $formattedResponse = sprintf('Unexpected exception of type "%s"', get_class($exception)); + } + + $this->data[$clientName]['response'][$depth][] = $formattedResponse; + $this->data[$clientName]['failure'][$depth][] = true; + } + + /** + * Returns the successful request-resonse pairs. + * + * @return array + */ + public function getSucessfulRequests() + { + $count = 0; + foreach ($this->data as $client) { + if (isset($client['failure'])) { + foreach ($client['failure'][0] as $failure) { + if (!$failure) { + ++$count; + } + } + } + } + + return $count; + } + + /** + * Returns the failed request-resonse pairs. + * + * @return array + */ + public function getFailedRequests() + { + $count = 0; + foreach ($this->data as $client) { + if (isset($client['failure'])) { + foreach ($client['failure'][0] as $failure) { + if ($failure) { + ++$count; + } + } + } + } + + return $count; + } + + /** + * Returns the total number of request made. + * + * @return int + */ + public function getTotalRequests() + { + return $this->getSucessfulRequests() + $this->getFailedRequests(); + } + + /** + * Return a RequestStackProvider for each client. + * + * @return RequestStackProvider[] + */ + public function getClients() + { + return RequestStackProvider::createFromCollectedData($this->data); + } + + /** + * @return PluginJournal + */ + public function getJournal() + { + return $this->journal; + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + // We do not need to collect any data from the Symfony Request and Response + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'httplug'; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize([$this->data, $this->journal]); + } + + /** + * {@inheritdoc} + */ + public function unserialize($data) + { + list($this->data, $this->journal) = unserialize($data); + } +} diff --git a/Collector/MessageJournal.php b/Collector/MessageJournal.php deleted file mode 100644 index 8930d085..00000000 --- a/Collector/MessageJournal.php +++ /dev/null @@ -1,109 +0,0 @@ - - */ -class MessageJournal extends DataCollector implements Journal -{ - /** - * @var Formatter - */ - private $formatter; - - /** - * @param Formatter $formatter - */ - public function __construct(Formatter $formatter = null) - { - $this->formatter = $formatter ?: new SimpleFormatter(); - $this->data = ['success' => [], 'failure' => []]; - } - - /** - * {@inheritdoc} - */ - public function addSuccess(RequestInterface $request, ResponseInterface $response) - { - $this->data['success'][] = [ - 'request' => $this->formatter->formatRequest($request), - 'response' => $this->formatter->formatResponse($response), - ]; - } - - /** - * {@inheritdoc} - */ - public function addFailure(RequestInterface $request, Exception $exception) - { - if ($exception instanceof Exception\HttpException) { - $formattedResponse = $this->formatter->formatResponse($exception->getResponse()); - } elseif ($exception instanceof Exception\TransferException) { - $formattedResponse = $exception->getMessage(); - } else { - $formattedResponse = sprintf('Unexpected exception of type "%s"', get_class($exception)); - } - - $this->data['failure'][] = [ - 'request' => $this->formatter->formatRequest($request), - 'response' => $formattedResponse, - ]; - } - - /** - * Returns the successful request-resonse pairs. - * - * @return array - */ - public function getSucessfulRequests() - { - return $this->data['success']; - } - - /** - * Returns the failed request-resonse pairs. - * - * @return array - */ - public function getFailedRequests() - { - return $this->data['failure']; - } - - /** - * Returns the total number of request made. - * - * @return int - */ - public function getTotalRequests() - { - return count($this->data['success']) + count($this->data['failure']); - } - - /** - * {@inheritdoc} - */ - public function collect(Request $request, Response $response, \Exception $exception = null) - { - // We do not need to collect any data from the Symfony Request and Response - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'httplug'; - } -} diff --git a/Collector/PluginJournal.php b/Collector/PluginJournal.php new file mode 100644 index 00000000..cb53d37d --- /dev/null +++ b/Collector/PluginJournal.php @@ -0,0 +1,50 @@ + + */ +final class PluginJournal +{ + /** + * @var array ['clientName'=>['index' => 'PluginName'] + */ + private $data; + + /** + * @param string $clientName + * + * @return array + */ + public function getPlugins($clientName) + { + return $this->data[$clientName]; + } + + /** + * @param string $clientName + * @param int $idx + * + * @return string|null + */ + public function getPluginName($clientName, $idx) + { + if (isset($this->data[$clientName][$idx])) { + return $this->data[$clientName][$idx]; + } + + return; + } + + /** + * @param string $clientName + * @param array $plugins + */ + public function setPlugins($clientName, array $plugins) + { + $this->data[$clientName] = $plugins; + } +} diff --git a/Collector/RequestStackProvider.php b/Collector/RequestStackProvider.php new file mode 100644 index 00000000..c5a52e5d --- /dev/null +++ b/Collector/RequestStackProvider.php @@ -0,0 +1,141 @@ + + */ +final class RequestStackProvider +{ + /** + * Array that tell if a request errored or not. true = success, false = failure. + * + * @var array + */ + private $failures; + + /** + * A multidimensional array with requests. + * $requests[0][0] is the first request before all plugins. + * $requests[0][1] is the first request after the first plugin. + * + * @var array + */ + private $requests; + + /** + * A multidimensional array with responses. + * $responses[0][0] is the first responses before all plugins. + * $responses[0][1] is the first responses after the first plugin. + * + * @var array + */ + private $responses; + + /** + * @param array $failures if the response was successful or not + * @param array $requests + * @param array $responses + */ + public function __construct(array $failures, array $requests, array $responses) + { + $this->failures = $failures; + $this->requests = $requests; + $this->responses = $responses; + } + + /** + * Create an array of ClientDataCollector from collected data. + * + * @param array $data + * + * @return RequestStackProvider[] + */ + public static function createFromCollectedData(array $data) + { + $clientData = []; + foreach ($data as $clientName => $messages) { + $clientData[$clientName] = static::createOne($messages); + } + + return $clientData; + } + + /** + * @param array $messages is an array with keys 'failure', 'request' and 'response' which hold requests for each call to + * sendRequest and for each depth + * + * @return RequestStackProvider + */ + private static function createOne($messages) + { + $orderedFaulure = []; + $orderedRequests = []; + $orderedResponses = []; + + foreach ($messages['failure'] as $depth => $failures) { + foreach ($failures as $idx => $failure) { + $orderedFaulure[$idx][$depth] = $failure; + } + } + + foreach ($messages['request'] as $depth => $requests) { + foreach ($requests as $idx => $request) { + $orderedRequests[$idx][$depth] = $request; + } + } + + foreach ($messages['response'] as $depth => $responses) { + foreach ($responses as $idx => $response) { + $orderedResponses[$idx][$depth] = $response; + } + } + + return new self($orderedFaulure, $orderedRequests, $orderedResponses); + } + + /** + * Get the index keys for the request and response stacks. + * + * @return array + */ + public function getStackIndexKeys() + { + return array_keys($this->requests); + } + + /** + * @param int $idx + * + * @return array + */ + public function getRequstStack($idx) + { + return $this->requests[$idx]; + } + + /** + * @param int $idx + * + * @return array + */ + public function getResponseStack($idx) + { + return $this->responses[$idx]; + } + + /** + * @param int $idx + * + * @return array + */ + public function getFailureStack($idx) + { + return $this->failures[$idx]; + } +} diff --git a/Collector/Twig/HttpMessageMarkupExtension.php b/Collector/Twig/HttpMessageMarkupExtension.php new file mode 100644 index 00000000..b6c5c904 --- /dev/null +++ b/Collector/Twig/HttpMessageMarkupExtension.php @@ -0,0 +1,49 @@ + + */ +class HttpMessageMarkupExtension extends \Twig_Extension +{ + /** + * {@inheritdoc} + * + * @return array + */ + public function getFilters() + { + return [ + new \Twig_SimpleFilter('httplug_markup', [$this, 'markup'], ['is_safe' => ['html']]), + ]; + } + + /** + * @param string $message http message + */ + public function markup($message) + { + $safeMessage = htmlentities($message); + $parts = preg_split('|\\r?\\n\\r?\\n|', $safeMessage, 2); + + if (!isset($parts[1])) { + // This is not a HTTP message + return $safeMessage; + } + + if (empty($parts[1])) { + $parts[1] = '(This message has no captured body)'; + } + + // make header names bold + $headers = preg_replace("|\n(.*?): |si", "\n$1: ", $parts[0]); + + return sprintf("%s\n\n
+ These messages are sent by client named "{{ name }}". +
-No request were sent.
+ {% for stackIndex in client.stackIndexKeys %} + {% set failureStack = client.failureStack(stackIndex) %} +No request were sent.
+Request | -Response | +Request | +Response |
---|---|---|---|
{{ requestStack[responseStack|length-1]|httplug_markup|nl2br }} | +{{ responseStack[0]|httplug_markup|nl2br }} | +||
{{ message['request'] }} | -{{ message['response'] }} | ++ + | |
↓ Start | +- End + {% if failureStack[idx] %} + ☓ + {% endif %} + | +||
↓ {{ pluginNames[idx-1] }} | +↑ + {% if failureStack[idx] %} + ☓ + {% endif %} + | +||
{{ requestStack[idx]|httplug_markup|nl2br }} | +{{ responseStack[idx]|httplug_markup|nl2br }} | +||
⟶ HTTP client | +↑ | +