diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/HttpClientDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/HttpClientDataCollector.php deleted file mode 100644 index 475e38a208c53..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DataCollector/HttpClientDataCollector.php +++ /dev/null @@ -1,101 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DataCollector; - -use Symfony\Bundle\FrameworkBundle\HttpClient\TraceableHttpClient; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\DataCollector\DataCollector; -use Symfony\Component\VarDumper\Caster\CutStub; -use Symfony\Component\VarDumper\Cloner\ClonerInterface; -use Symfony\Component\VarDumper\Cloner\Data; -use Symfony\Component\VarDumper\Cloner\VarCloner; -use Symfony\Contracts\HttpClient\HttpClientInterface; - -/** - * @author Jérémy Romey - */ -final class HttpClientDataCollector extends DataCollector -{ - protected $httpClient; - - /** - * @var ClonerInterface - */ - private $cloner; - - public function __construct(HttpClientInterface $httpClient = null) - { - $this->httpClient = $httpClient; - } - - /** - * {@inheritdoc} - */ - public function collect(Request $request, Response $response, \Exception $exception = null) - { - if ($this->httpClient instanceof TraceableHttpClient) { - $this->data['traces'] = $this->httpClient->getTraces(); - foreach ($this->data['traces'] as $key => $trace) { - $this->data['traces'][$key]['request']['options'] = $this->cloneVar($trace['request']['options']); - } - } - } - - /** - * Converts the variable into a serializable Data instance. - * - * This array can be displayed in the template using - * the VarDumper component. - * - * @param mixed $var - * - * @return Data - */ - protected function cloneVar($var) - { - if ($var instanceof Data) { - return $var; - } - if (null === $this->cloner) { - if (!class_exists(CutStub::class)) { - throw new \LogicException(sprintf('The VarDumper component is needed for the %s() method. Install symfony/var-dumper version 3.4 or above.', __METHOD__)); - } - $this->cloner = new VarCloner(); - $this->cloner->setMaxItems(-1); - $this->cloner->addCasters($this->getCasters()); - } - - return $this->cloner->cloneVar($var); - } - - public function getTraces(): array - { - return $this->data['traces'] ?? []; - } - - /** - * {@inheritdoc} - */ - public function reset() - { - $this->data = []; - } - - /** - * {@inheritdoc} - */ - public function getName(): string - { - return 'http_client'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f3491d35695b7..2f03f2beec68c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -62,6 +62,7 @@ use Symfony\Component\Form\FormTypeInterface; use Symfony\Component\HttpClient\Psr18Client; use Symfony\Component\HttpClient\ScopingHttpClient; +use Symfony\Component\HttpClient\TraceableHttpClient; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -1800,6 +1801,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder { $loader->load('http_client.xml'); + $debug = $container->getParameter('kernel.debug'); + $container->getDefinition('http_client')->setArguments([$config['default_options'] ?? [], $config['max_host_connections'] ?? 6]); if (!$hasPsr18 = interface_exists(ClientInterface::class)) { @@ -1807,6 +1810,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container->removeAlias(ClientInterface::class); } + $collectorDefinition = $container->getDefinition('data_collector.http_client'); + foreach ($config['scoped_clients'] as $name => $scopeConfig) { if ('http_client' === $name) { throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name)); @@ -1818,6 +1823,19 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container->register($name, ScopingHttpClient::class) ->setArguments([new Reference('http_client'), [$scope => $scopeConfig], $scope]); + if ($debug) { + $definition = $container->getDefinition($name); + $traceableDefinition = new Definition(TraceableHttpClient::class); + $traceableDefinition->setArguments([new Reference($innerId = $name.'.inner')]); + $traceableDefinition->setPublic($definition->isPublic()); + $definition->setPublic(false); + $definition->replaceArgument(0, new Reference('debug.http_client.inner')); + $container->setDefinition($innerId, $definition); + $container->setDefinition($name, $traceableDefinition); + + $collectorDefinition->addMethodCall('addClient', [$name, new Reference($name)]); + } + $container->registerAliasForArgument($name, HttpClientInterface::class); if ($hasPsr18) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index 4393bc0edcbe8..fef8c97d5403e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -54,9 +54,12 @@ - - - + + + http_client + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml index abd0733e0cbd3..82a539b8a6e78 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml @@ -29,5 +29,9 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml index 6b15a87f407f2..bf7b7b9f64055 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml @@ -20,9 +20,5 @@ - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Collector/http_client.html.twig b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Collector/http_client.html.twig deleted file mode 100644 index a8a5fc3fa3290..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Collector/http_client.html.twig +++ /dev/null @@ -1,70 +0,0 @@ -{% extends '@WebProfiler/Profiler/layout.html.twig' %} - -{% block toolbar %} - {% if collector.traces|length %} - {% set icon %} - {{ include('@Framework/Icon/http-client.svg') }} - {% set status_color = '' %} - {{ collector.traces|length }} - {% endset %} - - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }} - {% endif %} -{% endblock %} - -{% block menu %} - - {{ include('@Framework/Icon/http-client.svg') }} - HTTP Client - -{% endblock %} - -{% block panel %} -

HTTP Requests

- {% for trace in collector.traces %} - - - - - - - - - - - - - - - - - - - - -
- {{ trace.request.method }} {{ trace.request.url }} -
Response Status Code - {% if trace.response.statusCode >= 500 %} - {% set responseStatus = 'error' %} - {% elseif trace.response.statusCode >= 400 %} - {% set responseStatus = 'warning' %} - {% else %} - {% set responseStatus = 'success' %} - {% endif %} - - {{ trace.response.statusCode }} - -
Response Headers - {% for header in trace.response.headers %} -
{{ header }}
- {% endfor %} -
Options - {{ trace.request.options is empty ? 'none' : profiler_dump(trace.request.options, maxDepth=1) }} -
- {% else %} -
-

No HTTP requests have been made.

-
- {% endfor %} -{% endblock %} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Icon/http-client.svg b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Icon/http-client.svg deleted file mode 100644 index 271ac98c89e2f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Icon/http-client.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig new file mode 100644 index 0000000000000..7f3cacd9e3515 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig @@ -0,0 +1,104 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% if collector.requestCount %} + {% set icon %} + {{ include('@WebProfiler/Icon/http-client.svg') }} + {% set status_color = '' %} + {{ collector.requestCount }} + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }} + {% endif %} +{% endblock %} + +{% block menu %} + + {{ include('@WebProfiler/Icon/http-client.svg') }} + HTTP Client + {% if collector.requestCount %} + + {{ collector.requestCount }} + + {% endif %} + +{% endblock %} + +{% block panel %} +

HTTP Client

+ {% if collector.requestCount == 0 %} +
+

No HTTP requests were made.

+
+ {% else %} +
+
+ {{ collector.requestCount }} + Total requests +
+
+ {{ collector.errors }} + Total errors +
+
+

Clients

+
+ {% for name, client in collector.clients %} +
+

{{ name }} {{ client.traces|length }}

+
+ {% if client.traces|length == 0 %} +
+

No requests were made for {{ name }} client.

+
+ {% else %} +

Requests

+ {% for trace in client.traces %} + + + + + + + + + + + + + + + + + + + + +
+ {{ trace.request.method }} {{ trace.request.url }} +
Response Status Code + {% if trace.response.statusCode >= 500 %} + {% set responseStatus = 'error' %} + {% elseif trace.response.statusCode >= 400 %} + {% set responseStatus = 'warning' %} + {% else %} + {% set responseStatus = 'success' %} + {% endif %} + + {{ trace.response.statusCode }} + +
Response Headers + {% for header in trace.response.headers %} +
{{ header }}
+ {% endfor %} +
Options + {{ trace.request.options is empty ? 'none' : profiler_dump(trace.request.options, maxDepth=1) }} +
+ {% endfor %} + {% endif %} +
+
+ {% endfor %} + {% endif %} +
+{% endblock %} diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php new file mode 100644 index 0000000000000..8eb223e4337bc --- /dev/null +++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\DataCollector; + +use Symfony\Component\HttpClient\TraceableHttpClient; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Jérémy Romey + */ +class HttpClientDataCollector extends DataCollector +{ + /** + * @var TraceableHttpClient[] + */ + protected $clients = []; + + public function addClient(string $name, HttpClientInterface $client) + { + if ($client) { + $this->clients[$name] = $client; + } + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + $this->data['clients'] = []; + foreach ($this->clients as $name => $client) { + $clientTraces = $client instanceof TraceableHttpClient ? $client->getTraces() : []; + + foreach ($clientTraces as $key => $trace) { + $clientTraces[$key]['request']['options'] = $this->cloneVar($trace['request']['options']); + } + + $this->data['clients'][$name] = [ + 'traces' => $clientTraces, + 'stats' => $this->calculateStatistics($clientTraces), + ]; + } + $this->data['total'] = $this->calculateTotalStatistics(); + } + + public function getClients(): array + { + return $this->data['clients']; + } + + public function getRequestCount(): int + { + return $this->data['total']['traces']; + } + + public function getErrors(): int + { + return $this->data['total']['errors']; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->data = []; + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'http_client'; + } + + private function calculateStatistics(array $traces) + { + $stats = [ + 'errors' => 0, + ]; + + foreach ($traces as $trace) { + if (400 <= $trace['response']['statusCode']) { + ++$stats['errors']; + } + } + + return $stats; + } + + private function calculateTotalStatistics(): array + { + $totals = [ + 'traces' => 0, + 'errors' => 0, + ]; + + foreach ($this->getClients() as $client) { + $totals['traces'] += \count($client['traces']); + $totals['errors'] += $client['stats']['errors']; + } + + return $totals; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpClient/TraceableHttpClient.php b/src/Symfony/Component/HttpClient/TraceableHttpClient.php similarity index 92% rename from src/Symfony/Bundle/FrameworkBundle/HttpClient/TraceableHttpClient.php rename to src/Symfony/Component/HttpClient/TraceableHttpClient.php index e94404b74f73b..820c603a901ab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpClient/TraceableHttpClient.php +++ b/src/Symfony/Component/HttpClient/TraceableHttpClient.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FrameworkBundle\HttpClient; +namespace Symfony\Component\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -20,7 +20,14 @@ */ final class TraceableHttpClient implements HttpClientInterface { + /** + * @var HttpClientInterface + */ protected $httpClient; + + /** + * @var array + */ protected $traces = []; /** @@ -64,7 +71,7 @@ public function request(string $method, string $url, array $options = []): Respo */ public function stream($responses, float $timeout = null): ResponseStreamInterface { - $this->httpClient->stream($responses, $timeout); + return $this->httpClient->stream($responses, $timeout); } public function getTraces(): array