[HttpClient] Added TraceableHttpClient and WebProfiler panel

Co-authored-by: Jérémy Romey <jeremy@free-agent.fr>
Co-authored-by: Timothée Barray <tim@amicalement-web.net>
This commit is contained in:
Jérémy Romey 2019-03-08 17:59:13 +01:00 committed by Nicolas Grekas
parent 0fa1246e30
commit 51640012f1
17 changed files with 727 additions and 10 deletions

View File

@ -35,6 +35,7 @@ class UnusedTagsPass implements CompilerPassInterface
'form.type',
'form.type_extension',
'form.type_guesser',
'http_client.client',
'kernel.cache_clearer',
'kernel.cache_warmer',
'kernel.event_listener',

View File

@ -150,6 +150,7 @@ class FrameworkExtension extends Extension
private $validatorConfigEnabled = false;
private $messengerConfigEnabled = false;
private $mailerConfigEnabled = false;
private $httpClientConfigEnabled = false;
/**
* Responds to the app.config configuration parameter.
@ -311,6 +312,10 @@ class FrameworkExtension extends Extension
$container->removeDefinition('console.command.messenger_failed_messages_remove');
}
if ($this->httpClientConfigEnabled = $this->isConfigEnabled($container, $config['http_client'])) {
$this->registerHttpClientConfiguration($config['http_client'], $container, $loader, $config['profiler']);
}
$propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']);
$this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled);
$this->registerEsiConfiguration($config['esi'], $container, $loader);
@ -341,10 +346,6 @@ class FrameworkExtension extends Extension
$this->registerLockConfiguration($config['lock'], $container, $loader);
}
if ($this->isConfigEnabled($container, $config['http_client'])) {
$this->registerHttpClientConfiguration($config['http_client'], $container, $loader);
}
if ($this->mailerConfigEnabled = $this->isConfigEnabled($container, $config['mailer'])) {
$this->registerMailerConfiguration($config['mailer'], $container, $loader);
}
@ -562,6 +563,10 @@ class FrameworkExtension extends Extension
$loader->load('mailer_debug.xml');
}
if ($this->httpClientConfigEnabled) {
$loader->load('http_client_debug.xml');
}
$container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']);
$container->setParameter('profiler_listener.only_master_requests', $config['only_master_requests']);
@ -1915,7 +1920,7 @@ class FrameworkExtension extends Extension
}
}
private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $profilerConfig)
{
$loader->load('http_client.xml');
@ -1930,6 +1935,8 @@ class FrameworkExtension extends Extension
$container->removeDefinition(HttpClient::class);
}
$httpClientId = $this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client';
foreach ($config['scoped_clients'] as $name => $scopeConfig) {
if ('http_client' === $name) {
throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name));
@ -1941,10 +1948,14 @@ class FrameworkExtension extends Extension
if (null === $scope) {
$container->register($name, ScopingHttpClient::class)
->setFactory([ScopingHttpClient::class, 'forBaseUri'])
->setArguments([new Reference('http_client'), $scopeConfig['base_uri'], $scopeConfig]);
->setArguments([new Reference($httpClientId), $scopeConfig['base_uri'], $scopeConfig])
->addTag('http_client.client')
;
} else {
$container->register($name, ScopingHttpClient::class)
->setArguments([new Reference('http_client'), [$scope => $scopeConfig], $scope]);
->setArguments([new Reference($httpClientId), [$scope => $scopeConfig], $scope])
->addTag('http_client.client')
;
}
$container->registerAliasForArgument($name, HttpClientInterface::class);

View File

@ -36,6 +36,7 @@ use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\ErrorRenderer\DependencyInjection\ErrorRendererPass;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\Form\DependencyInjection\FormPass;
use Symfony\Component\HttpClient\DependencyInjection\HttpClientPass;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass;
@ -129,6 +130,7 @@ class FrameworkBundle extends Bundle
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
$this->addCompilerPassIfExists($container, MessengerPass::class);
$this->addCompilerPassIfExists($container, HttpClientPass::class);
$this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class);
$container->addCompilerPass(new RegisterReverseContainerPass(true));
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);

View File

@ -7,6 +7,7 @@
<services>
<service id="http_client" class="Symfony\Contracts\HttpClient\HttpClientInterface">
<tag name="monolog.logger" channel="http_client" />
<tag name="http_client.client" />
<factory class="Symfony\Component\HttpClient\HttpClient" method="create" />
<argument type="collection" /> <!-- default options -->
<argument /> <!-- max host connections -->

View File

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="data_collector.http_client" class="Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector">
<tag name="data_collector" template="@WebProfiler/Collector/http_client.html.twig" id="http_client" priority="250" />
</service>
</services>
</container>

View File

@ -40,7 +40,7 @@
"symfony/polyfill-intl-icu": "~1.0",
"symfony/form": "^4.3.4|^5.0",
"symfony/expression-language": "^3.4|^4.0|^5.0",
"symfony/http-client": "^4.3|^5.0",
"symfony/http-client": "^4.4|^5.0",
"symfony/lock": "^4.4|^5.0",
"symfony/mailer": "^4.4|^5.0",
"symfony/messenger": "^4.3|^5.0",
@ -71,6 +71,7 @@
"symfony/console": "<4.3",
"symfony/dotenv": "<4.2",
"symfony/dom-crawler": "<4.3",
"symfony/http-client": "<4.4",
"symfony/form": "<4.3",
"symfony/lock": "<4.4",
"symfony/mailer": "<4.4",

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* added support for the Mailer component
* added support for the HttpClient component
* added button to clear the ajax request tab
* deprecated the `ExceptionController::templateExists()` method
* deprecated the `TemplateManager::templateExists()` method

View File

@ -0,0 +1,98 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.requestCount %}
{% set icon %}
{{ include('@WebProfiler/Icon/http-client.svg') }}
{% set status_color = '' %}
<span class="sf-toolbar-value">{{ collector.requestCount }}</span>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.requestCount == 0 ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/http-client.svg') }}</span>
<strong>HTTP Client</strong>
{% if collector.requestCount %}
<span class="count">
{{ collector.requestCount }}
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>HTTP Client</h2>
{% if collector.requestCount == 0 %}
<div class="empty">
<p>No HTTP requests were made.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.requestCount }}</span>
<span class="label">Total requests</span>
</div>
<div class="metric">
<span class="value">{{ collector.errorCount }}</span>
<span class="label">HTTP errors</span>
</div>
</div>
<h2>Clients</h2>
<div class="sf-tabs">
{% for name, client in collector.clients %}
<div class="tab {{ client.traces|length == 0 ? 'disabled' }}">
<h3 class="tab-title">{{ name }} <span class="badge">{{ client.traces|length }}</span></h3>
<div class="tab-content">
{% if client.traces|length == 0 %}
<div class="empty">
<p>No requests were made with the "{{ name }}" service.</p>
</div>
{% else %}
<h4>Requests</h4>
{% for trace in client.traces %}
<table>
<thead>
<tr>
<th>
<span class="label">{{ trace.method }}</span>
</th>
<th class="full-width">
{{ trace.url }}
{% if trace.options is not empty %}
{{ profiler_dump(trace.options, maxDepth=1) }}
{% endif %}
</th>
</tr>
</thead>
<tbody>
<tr>
<th>
{% if trace.http_code >= 500 %}
{% set responseStatus = 'error' %}
{% elseif trace.http_code >= 400 %}
{% set responseStatus = 'warning' %}
{% else %}
{% set responseStatus = 'success' %}
{% endif %}
<span class="label status-{{ responseStatus }}">
{{ trace.http_code }}
</span>
</th>
<td>
{{ profiler_dump(trace.info, maxDepth=1) }}
</td>
</tr>
</tbody>
</table>
{% endfor %}
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M20.4 12c-1 0-1.8.6-2.2 1.4l-2.6-.9c.1-.3.1-.5.1-.8 0-1.2-.6-2.2-1.5-2.9l1.5-2.6c.3.1.6.2 1 .2 1.4 0 2.5-1.1 2.5-2.5s-1.1-2.5-2.5-2.5-2.5 1.1-2.5 2.5c0 .8.4 1.5.9 1.9l-1.5 2.6c-.5-.3-1-.4-1.6-.4-.9 0-1.7.3-2.3.9L7.4 6.6c.3-.4.5-.9.5-1.5 0-1.4-1.1-2.5-2.5-2.5S2.7 3.7 2.7 5.1s1.1 2.5 2.5 2.5c.6 0 1.1-.2 1.5-.5L9 9.4c-.5.6-.8 1.4-.8 2.3 0 .7.2 1.4.6 2l-3.9 3.8c-.4-.3-.9-.5-1.5-.5C2 17 .9 18.1.9 19.5S2.2 22 3.6 22s2.5-1.1 2.5-2.5c0-.5-.2-1-.5-1.5l3.8-3.7c.7.7 1.6 1.1 2.6 1.1h.2l.4 2.4c-1 .3-1.7 1.3-1.7 2.4 0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5-1.1-2.5-2.5-2.5l-.4-2.5c1-.3 1.9-1 2.3-2l2.6.9v.4c0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5c.1-1.4-1.1-2.5-2.5-2.5z"/></svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -6,10 +6,11 @@ CHANGELOG
* added `StreamWrapper`
* added `HttplugClient`
* added `max_duration` option
* added support for NTLM authentication
* added `$response->toStream()` to cast responses to regular PHP streams
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
* added `max_duration` option
* added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler
4.3.0
-----

View File

@ -0,0 +1,142 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
*/
final class HttpClientDataCollector extends DataCollector
{
/**
* @var TraceableHttpClient[]
*/
private $clients = [];
public function registerClient(string $name, TraceableHttpClient $client)
{
$this->clients[$name] = $client;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
$this->initData();
foreach ($this->clients as $name => $client) {
[$errorCount, $traces] = $this->collectOnClient($client);
$this->data['clients'][$name] = [
'traces' => $traces,
'error_count' => $errorCount,
];
$this->data['request_count'] += \count($traces);
$this->data['error_count'] += $errorCount;
}
}
public function getClients(): array
{
return $this->data['clients'] ?? [];
}
public function getRequestCount(): int
{
return $this->data['request_count'] ?? 0;
}
public function getErrorCount(): int
{
return $this->data['error_count'] ?? 0;
}
/**
* {@inheritdoc}
*/
public function reset()
{
$this->initData();
foreach ($this->clients as $client) {
$client->reset();
}
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'http_client';
}
private function initData()
{
$this->data = [
'clients' => [],
'request_count' => 0,
'error_count' => 0,
];
}
private function collectOnClient(TraceableHttpClient $client): array
{
$traces = $client->getTracedRequests();
$errorCount = 0;
$baseInfo = [
'response_headers' => 1,
'redirect_count' => 1,
'redirect_url' => 1,
'user_data' => 1,
'error' => 1,
'url' => 1,
];
foreach ($traces as $i => $trace) {
if (400 <= ($trace['info']['http_code'] ?? 0)) {
++$errorCount;
}
$info = $trace['info'];
$traces[$i]['http_code'] = $info['http_code'];
unset($info['filetime'], $info['http_code'], $info['ssl_verify_result'], $info['content_type']);
if ($trace['method'] === $info['http_method']) {
unset($info['http_method']);
}
if ($trace['url'] === $info['url']) {
unset($info['url']);
}
foreach ($info as $k => $v) {
if (!$v || (is_numeric($v) && 0 > $v)) {
unset($info[$k]);
}
}
$debugInfo = array_diff_key($info, $baseInfo);
$info = array_diff_key($info, $debugInfo) + ['debug_info' => $debugInfo];
$traces[$i]['info'] = $this->cloneVar($info);
$traces[$i]['options'] = $this->cloneVar($trace['options']);
}
return [$errorCount, $traces];
}
}

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpClient\TraceableHttpClient;
final class HttpClientPass implements CompilerPassInterface
{
private $clientTag;
public function __construct(string $clientTag = 'http_client.client')
{
$this->clientTag = $clientTag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('data_collector.http_client')) {
return;
}
foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
$container->register('.debug.'.$id, TraceableHttpClient::class)
->setArguments([new Reference('.debug.'.$id.'.inner')])
->setDecoratedService($id);
$container->getDefinition('data_collector.http_client')
->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]);
}
}
}

View File

@ -0,0 +1,178 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector;
use Symfony\Component\HttpClient\NativeHttpClient;
use Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\Test\TestHttpServer;
class HttpClientDataCollectorTest extends TestCase
{
public function testItCollectsRequestCount()
{
TestHttpServer::start();
$httpClient1 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => 'http://localhost:8057/',
],
[
'method' => 'GET',
'url' => 'http://localhost:8057/301',
],
]);
$httpClient2 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => 'http://localhost:8057/404',
],
]);
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
$sut = new HttpClientDataCollector();
$sut->registerClient('http_client1', $httpClient1);
$sut->registerClient('http_client2', $httpClient2);
$sut->registerClient('http_client3', $httpClient3);
$this->assertEquals(0, $sut->getRequestCount());
$sut->collect(new Request(), new Response());
$this->assertEquals(3, $sut->getRequestCount());
}
public function testItCollectsErrorCount()
{
TestHttpServer::start();
$httpClient1 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => 'http://localhost:8057/',
],
[
'method' => 'GET',
'url' => 'http://localhost:8057/301',
],
]);
$httpClient2 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => '/404',
'options' => ['base_uri' => 'http://localhost:8057/'],
],
]);
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
$sut = new HttpClientDataCollector();
$sut->registerClient('http_client1', $httpClient1);
$sut->registerClient('http_client2', $httpClient2);
$sut->registerClient('http_client3', $httpClient3);
$this->assertEquals(0, $sut->getErrorCount());
$sut->collect(new Request(), new Response());
$this->assertEquals(1, $sut->getErrorCount());
}
public function testItCollectsErrorCountByClient()
{
TestHttpServer::start();
$httpClient1 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => 'http://localhost:8057/',
],
[
'method' => 'GET',
'url' => 'http://localhost:8057/301',
],
]);
$httpClient2 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => '/404',
'options' => ['base_uri' => 'http://localhost:8057/'],
],
]);
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
$sut = new HttpClientDataCollector();
$sut->registerClient('http_client1', $httpClient1);
$sut->registerClient('http_client2', $httpClient2);
$sut->registerClient('http_client3', $httpClient3);
$this->assertEquals([], $sut->getClients());
$sut->collect(new Request(), new Response());
$collectedData = $sut->getClients();
$this->assertEquals(0, $collectedData['http_client1']['error_count']);
$this->assertEquals(1, $collectedData['http_client2']['error_count']);
$this->assertEquals(0, $collectedData['http_client3']['error_count']);
}
public function testItCollectsTracesByClient()
{
TestHttpServer::start();
$httpClient1 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => 'http://localhost:8057/',
],
[
'method' => 'GET',
'url' => 'http://localhost:8057/301',
],
]);
$httpClient2 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => '/404',
'options' => ['base_uri' => 'http://localhost:8057/'],
],
]);
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
$sut = new HttpClientDataCollector();
$sut->registerClient('http_client1', $httpClient1);
$sut->registerClient('http_client2', $httpClient2);
$sut->registerClient('http_client3', $httpClient3);
$this->assertEquals([], $sut->getClients());
$sut->collect(new Request(), new Response());
$collectedData = $sut->getClients();
$this->assertCount(2, $collectedData['http_client1']['traces']);
$this->assertCount(1, $collectedData['http_client2']['traces']);
$this->assertCount(0, $collectedData['http_client3']['traces']);
}
public function testItIsEmptyAfterReset()
{
TestHttpServer::start();
$httpClient1 = $this->httpClientThatHasTracedRequests([
[
'method' => 'GET',
'url' => 'http://localhost:8057/',
],
]);
$sut = new HttpClientDataCollector();
$sut->registerClient('http_client1', $httpClient1);
$sut->collect(new Request(), new Response());
$collectedData = $sut->getClients();
$this->assertCount(1, $collectedData['http_client1']['traces']);
$sut->reset();
$this->assertEquals([], $sut->getClients());
$this->assertEquals(0, $sut->getErrorCount());
$this->assertEquals(0, $sut->getRequestCount());
}
private function httpClientThatHasTracedRequests($tracedRequests)
{
$httpClient = new TraceableHttpClient(new NativeHttpClient());
foreach ($tracedRequests as $request) {
$response = $httpClient->request($request['method'], $request['url'], $request['options'] ?? []);
$response->getContent(false); // To avoid exception in ResponseTrait::doDestruct
}
return $httpClient;
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector;
use Symfony\Component\HttpClient\DependencyInjection\HttpClientPass;
use Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class HttpClientPassTest extends TestCase
{
public function testItRequiresDataCollector()
{
$container = $this->buildContainerBuilder('http_client');
$sut = new HttpClientPass();
$sut->process($container);
$this->assertFalse($container->hasDefinition('.debug.http_client'));
}
public function testItDecoratesHttpClientWithTraceableHttpClient()
{
$container = $this->buildContainerBuilder('foo');
$container->register('data_collector.http_client', HttpClientDataCollector::class);
$sut = new HttpClientPass();
$sut->process($container);
$this->assertTrue($container->hasDefinition('.debug.foo'));
$this->assertSame(TraceableHttpClient::class, $container->getDefinition('.debug.foo')->getClass());
$this->assertSame(['foo', null, 0], $container->getDefinition('.debug.foo')->getDecoratedService());
}
public function testItRegistersDebugHttpClientToCollector()
{
$container = $this->buildContainerBuilder('foo_client');
$container->register('data_collector.http_client', HttpClientDataCollector::class);
$sut = new HttpClientPass();
$sut->process($container);
$this->assertEquals(
[['registerClient', ['foo_client', new Reference('.debug.foo_client')]]],
$container->getDefinition('data_collector.http_client')->getMethodCalls()
);
}
private function buildContainerBuilder(string $clientId = 'http_client'): ContainerBuilder
{
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', true);
$container->register($clientId, HttpClientInterface::class)->addTag('http_client.client')->setArgument(0, []);
return $container;
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class TraceableHttpClientTest extends TestCase
{
public function testItTracesRequest()
{
$httpClient = $this->getMockBuilder(HttpClientInterface::class)->getMock();
$httpClient
->expects($this->once())
->method('request')
->with(
'GET',
'/foo/bar',
$this->callback(function ($subject) {
$onprogress = $subject['on_progress'];
unset($subject['on_progress']);
$this->assertEquals(['options1' => 'foo'], $subject);
return true;
})
)
->willReturn(MockResponse::fromRequest('GET', '/foo/bar', ['options1' => 'foo'], new MockResponse()))
;
$sut = new TraceableHttpClient($httpClient);
$sut->request('GET', '/foo/bar', ['options1' => 'foo']);
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
$actualTracedRequest = $tracedRequests[0];
$this->assertEquals([
'method' => 'GET',
'url' => '/foo/bar',
'options' => ['options1' => 'foo'],
'info' => [],
], $actualTracedRequest);
}
public function testItCollectsInfoOnRealRequest()
{
$sut = new TraceableHttpClient(new MockHttpClient());
$sut->request('GET', 'http://localhost:8057');
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
$actualTracedRequest = $tracedRequests[0];
$this->assertSame('GET', $actualTracedRequest['info']['http_method']);
$this->assertSame('http://localhost:8057/', $actualTracedRequest['info']['url']);
}
public function testItExecutesOnProgressOption()
{
$sut = new TraceableHttpClient(new MockHttpClient());
$foo = 0;
$sut->request('GET', 'http://localhost:8057', ['on_progress' => function (int $dlNow, int $dlSize, array $info) use (&$foo) {
++$foo;
}]);
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
$actualTracedRequest = $tracedRequests[0];
$this->assertGreaterThan(0, $foo);
}
public function testItResetsTraces()
{
$sut = new TraceableHttpClient(new MockHttpClient());
$sut->request('GET', 'https://example.com/foo/bar');
$sut->reset();
$this->assertCount(0, $sut->getTracedRequests());
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
*/
final class TraceableHttpClient implements HttpClientInterface
{
private $client;
private $tracedRequests = [];
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$traceInfo = [];
$this->tracedRequests[] = [
'method' => $method,
'url' => $url,
'options' => $options,
'info' => &$traceInfo,
];
$onProgress = $options['on_progress'] ?? null;
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) {
$traceInfo = $info;
if (null !== $onProgress) {
$onProgress($dlNow, $dlSize, $info);
}
};
return $this->client->request($method, $url, $options);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
}
public function getTracedRequests(): array
{
return $this->tracedRequests;
}
public function reset()
{
$this->tracedRequests = [];
}
}

View File

@ -29,8 +29,11 @@
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^4.3|^5.0",
"symfony/http-kernel": "^4.3|^5.0",
"symfony/process": "^4.2|^5.0"
"symfony/process": "^4.2|^5.0",
"symfony/service-contracts": "^1.0",
"symfony/var-dumper": "^4.3|^5.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },