From 51640012f14a3533d9d1887be68470a2edeededa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Romey?= Date: Fri, 8 Mar 2019 17:59:13 +0100 Subject: [PATCH] [HttpClient] Added TraceableHttpClient and WebProfiler panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémy Romey Co-authored-by: Timothée Barray --- .../Compiler/UnusedTagsPass.php | 1 + .../FrameworkExtension.php | 25 ++- .../FrameworkBundle/FrameworkBundle.php | 2 + .../Resources/config/http_client.xml | 1 + .../Resources/config/http_client_debug.xml | 12 ++ .../Bundle/FrameworkBundle/composer.json | 3 +- .../Bundle/WebProfilerBundle/CHANGELOG.md | 1 + .../views/Collector/http_client.html.twig | 98 ++++++++++ .../Resources/views/Icon/http-client.svg | 1 + src/Symfony/Component/HttpClient/CHANGELOG.md | 3 +- .../DataCollector/HttpClientDataCollector.php | 142 ++++++++++++++ .../DependencyInjection/HttpClientPass.php | 45 +++++ .../HttpClientDataCollectorTest.php | 178 ++++++++++++++++++ .../HttpClientPassTest.php | 65 +++++++ .../Tests/TraceableHttpClientTest.php | 82 ++++++++ .../HttpClient/TraceableHttpClient.php | 73 +++++++ .../Component/HttpClient/composer.json | 5 +- 17 files changed, 727 insertions(+), 10 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/http-client.svg create mode 100644 src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php create mode 100755 src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php create mode 100755 src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php create mode 100755 src/Symfony/Component/HttpClient/Tests/DependencyInjection/HttpClientPassTest.php create mode 100755 src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php create mode 100644 src/Symfony/Component/HttpClient/TraceableHttpClient.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 00fc826cf2096..532b0af819a37 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -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', diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index e458a0b623618..268643556dcb9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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 @@ public function load(array $configs, ContainerBuilder $container) $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 @@ public function load(array $configs, ContainerBuilder $container) $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 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $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 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con } } - 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 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $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 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder 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); diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index f1cb0fe14dea3..173165b03eb4a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -36,6 +36,7 @@ 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 @@ public function build(ContainerBuilder $container) $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); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml index a3f0884365b0a..766e9f6d33d31 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml @@ -7,6 +7,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml new file mode 100644 index 0000000000000..6d6ae4b729093 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 4c8a5ef30254c..7807c8aac144f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -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", diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index ebf52ea3e267b..257924f0aad55 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -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 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..68716153dafd5 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig @@ -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 = '' %} + {{ 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.errorCount }} + HTTP errors +
+
+

Clients

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

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

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

No requests were made with the "{{ name }}" service.

+
+ {% else %} +

Requests

+ {% for trace in client.traces %} + + + + + + + + + + + + + +
+ {{ trace.method }} + + {{ trace.url }} + {% if trace.options is not empty %} + {{ profiler_dump(trace.options, maxDepth=1) }} + {% endif %} +
+ {% if trace.http_code >= 500 %} + {% set responseStatus = 'error' %} + {% elseif trace.http_code >= 400 %} + {% set responseStatus = 'warning' %} + {% else %} + {% set responseStatus = 'success' %} + {% endif %} + + {{ trace.http_code }} + + + {{ profiler_dump(trace.info, maxDepth=1) }} +
+ {% endfor %} + {% endif %} +
+
+ {% endfor %} + {% endif %} +
+{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/http-client.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/http-client.svg new file mode 100644 index 0000000000000..e6b1fb2fe903c --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/http-client.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index b61ceddf7515c..9fcfa7ee9a561 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php new file mode 100644 index 0000000000000..f9fb1af2a3ff5 --- /dev/null +++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php @@ -0,0 +1,142 @@ + + * + * 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 + */ +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]; + } +} diff --git a/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php b/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php new file mode 100755 index 0000000000000..e19779786bd65 --- /dev/null +++ b/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php @@ -0,0 +1,45 @@ + + * + * 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)]); + } + } +} diff --git a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php new file mode 100755 index 0000000000000..f4f94156a3204 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php @@ -0,0 +1,178 @@ + + * + * 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; + } +} diff --git a/src/Symfony/Component/HttpClient/Tests/DependencyInjection/HttpClientPassTest.php b/src/Symfony/Component/HttpClient/Tests/DependencyInjection/HttpClientPassTest.php new file mode 100755 index 0000000000000..eb04f88226d1f --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/DependencyInjection/HttpClientPassTest.php @@ -0,0 +1,65 @@ + + * + * 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; + } +} diff --git a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php new file mode 100755 index 0000000000000..949d8afcff85a --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php @@ -0,0 +1,82 @@ + + * + * 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()); + } +} diff --git a/src/Symfony/Component/HttpClient/TraceableHttpClient.php b/src/Symfony/Component/HttpClient/TraceableHttpClient.php new file mode 100644 index 0000000000000..4acbc8ee42df8 --- /dev/null +++ b/src/Symfony/Component/HttpClient/TraceableHttpClient.php @@ -0,0 +1,73 @@ + + * + * 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 + */ +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 = []; + } +} diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index 9ecab88de642a..c010dc06ba21d 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -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\\": "" }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy