Skip to content

Commit 6c8dc49

Browse files
fbourigaultNyholm
authored andcommitted
add profile client (#136)
1 parent de33f9c commit 6c8dc49

File tree

7 files changed

+321
-1
lines changed

7 files changed

+321
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
## Unreleased
4+
5+
### Added
6+
7+
- The real request method and target url are now displayed in the profiler.
8+
39
## 1.4.0 - 2017-02-21
410

511
### Changed

Collector/ProfileClient.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace Http\HttplugBundle\Collector;
4+
5+
use Http\Client\Common\FlexibleHttpClient;
6+
use Http\Client\HttpAsyncClient;
7+
use Http\Client\HttpClient;
8+
use Psr\Http\Message\RequestInterface;
9+
10+
/**
11+
* The ProfileClient decorates any client that implement both HttpClient and HttpAsyncClient interfaces to gather target
12+
* url and response status code.
13+
*
14+
* @author Fabien Bourigault <[email protected]>
15+
*
16+
* @internal
17+
*/
18+
class ProfileClient implements HttpClient, HttpAsyncClient
19+
{
20+
/**
21+
* @var HttpClient|HttpAsyncClient
22+
*/
23+
private $client;
24+
25+
/**
26+
* @var Collector
27+
*/
28+
private $collector;
29+
30+
/**
31+
* @param HttpClient|HttpAsyncClient $client The client to profile. Client must implement both HttpClient and
32+
* HttpAsyncClient interfaces.
33+
* @param Collector $collector
34+
*/
35+
public function __construct($client, Collector $collector)
36+
{
37+
if (!($client instanceof HttpClient && $client instanceof HttpAsyncClient)) {
38+
throw new \RuntimeException(sprintf(
39+
'%s first argument must implement %s and %s. Consider using %s.',
40+
__METHOD__,
41+
HttpClient::class,
42+
HttpAsyncClient::class,
43+
FlexibleHttpClient::class
44+
));
45+
}
46+
$this->client = $client;
47+
$this->collector = $collector;
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function sendAsyncRequest(RequestInterface $request)
54+
{
55+
$this->collectRequestInformations($request);
56+
57+
return $this->client->sendAsyncRequest($request);
58+
}
59+
60+
/**
61+
* {@inheritdoc}
62+
*/
63+
public function sendRequest(RequestInterface $request)
64+
{
65+
$this->collectRequestInformations($request);
66+
67+
return $this->client->sendRequest($request);
68+
}
69+
70+
/**
71+
* @param RequestInterface $request
72+
*/
73+
private function collectRequestInformations(RequestInterface $request)
74+
{
75+
if (!$stack = $this->collector->getCurrentStack()) {
76+
return;
77+
}
78+
79+
$stack = $this->collector->getCurrentStack();
80+
$stack->setRequestTarget($request->getRequestTarget());
81+
$stack->setRequestMethod($request->getMethod());
82+
}
83+
}

Collector/ProfileClientFactory.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace Http\HttplugBundle\Collector;
4+
5+
use Http\Client\Common\FlexibleHttpClient;
6+
use Http\Client\HttpAsyncClient;
7+
use Http\Client\HttpClient;
8+
use Http\HttplugBundle\ClientFactory\ClientFactory;
9+
10+
/**
11+
* The ProfileClientFactory decorates any ClientFactory and returns the created client decorated by a ProfileClient.
12+
*
13+
* @author Fabien Bourigault <[email protected]>
14+
*
15+
* @internal
16+
*/
17+
class ProfileClientFactory implements ClientFactory
18+
{
19+
/**
20+
* @var ClientFactory|callable
21+
*/
22+
private $factory;
23+
24+
/**
25+
* @var Collector
26+
*/
27+
private $collector;
28+
29+
/**
30+
* @param ClientFactory|callable $factory
31+
* @param Collector $collector
32+
*/
33+
public function __construct($factory, Collector $collector)
34+
{
35+
if (!$factory instanceof ClientFactory && !is_callable($factory)) {
36+
throw new \RuntimeException(sprintf('First argument to ProfileClientFactory::__construct must be a "%s" or a callable.', ClientFactory::class));
37+
}
38+
$this->factory = $factory;
39+
$this->collector = $collector;
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function createClient(array $config = [])
46+
{
47+
$client = is_callable($this->factory) ? $this->factory($config) : $this->factory->createClient($config);
48+
49+
if (!($client instanceof HttpClient && $client instanceof HttpAsyncClient)) {
50+
$client = new FlexibleHttpClient($client);
51+
}
52+
53+
return new ProfileClient($client, $this->collector);
54+
}
55+
}

Collector/Stack.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ final class Stack
3636
*/
3737
private $failed = false;
3838

39+
/**
40+
* @var string
41+
*/
42+
private $requestTarget;
43+
44+
/**
45+
* @var string
46+
*/
47+
private $requestMethod;
48+
3949
/**
4050
* @param string $client
4151
* @param string $request
@@ -109,4 +119,36 @@ public function setFailed($failed)
109119
{
110120
$this->failed = $failed;
111121
}
122+
123+
/**
124+
* @return string
125+
*/
126+
public function getRequestTarget()
127+
{
128+
return $this->requestTarget;
129+
}
130+
131+
/**
132+
* @param string $requestTarget
133+
*/
134+
public function setRequestTarget($requestTarget)
135+
{
136+
$this->requestTarget = $requestTarget;
137+
}
138+
139+
/**
140+
* @return string
141+
*/
142+
public function getRequestMethod()
143+
{
144+
return $this->requestMethod;
145+
}
146+
147+
/**
148+
* @param string $requestMethod
149+
*/
150+
public function setRequestMethod($requestMethod)
151+
{
152+
$this->requestMethod = $requestMethod;
153+
}
112154
}

Resources/config/data-collector.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,31 @@
2929
<service id="httplug.collector.twig.http_message" class="Http\HttplugBundle\Collector\Twig\HttpMessageMarkupExtension" public="false">
3030
<tag name="twig.extension" />
3131
</service>
32+
33+
<!-- ClientFactories -->
34+
<service id="httplug.collector.factory.buzz" class="Http\HttplugBundle\Collector\ProfileClientFactory" decorates="httplug.factory.buzz" public="false">
35+
<argument type="service" id="httplug.collector.factory.buzz.inner"/>
36+
<argument type="service" id="httplug.collector.collector"/>
37+
</service>
38+
<service id="httplug.collector.factory.curl" class="Http\HttplugBundle\Collector\ProfileClientFactory" decorates="httplug.factory.curl" public="false">
39+
<argument type="service" id="httplug.collector.factory.curl.inner"/>
40+
<argument type="service" id="httplug.collector.collector"/>
41+
</service>
42+
<service id="httplug.collector.factory.guzzle5" class="Http\HttplugBundle\Collector\ProfileClientFactory" decorates="httplug.factory.guzzle5" public="false">
43+
<argument type="service" id="httplug.collector.factory.guzzle5.inner"/>
44+
<argument type="service" id="httplug.collector.collector"/>
45+
</service>
46+
<service id="httplug.collector.factory.guzzle6" class="Http\HttplugBundle\Collector\ProfileClientFactory" decorates="httplug.factory.guzzle6" public="false">
47+
<argument type="service" id="httplug.collector.factory.guzzle6.inner"/>
48+
<argument type="service" id="httplug.collector.collector"/>
49+
</service>
50+
<service id="httplug.collector.factory.react" class="Http\HttplugBundle\Collector\ProfileClientFactory" decorates="httplug.factory.react" public="false">
51+
<argument type="service" id="httplug.collector.factory.react.inner"/>
52+
<argument type="service" id="httplug.collector.collector"/>
53+
</service>
54+
<service id="httplug.collector.factory.socket" class="Http\HttplugBundle\Collector\ProfileClientFactory" decorates="httplug.factory.socket" public="false">
55+
<argument type="service" id="httplug.collector.factory.socket.inner"/>
56+
<argument type="service" id="httplug.collector.collector"/>
57+
</service>
3258
</services>
3359
</container>

Resources/views/webprofiler.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262

6363
{% for stack in collector.clientStacks(client) %}
6464
<h3>
65-
Request #{{ loop.index }}
65+
Request #{{ loop.index }} - {{ stack.requestMethod }} {{ stack.requestTarget }}
6666
{% if stack.failed %}
6767
- <span class="httplug-error">Errored</span>
6868
{% endif %}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace Http\HttplugBundle\Tests\Unit\Collector;
4+
5+
use Http\Client\HttpAsyncClient;
6+
use Http\Client\HttpClient;
7+
use Http\HttplugBundle\Collector\Collector;
8+
use Http\HttplugBundle\Collector\ProfileClient;
9+
use Http\HttplugBundle\Collector\Stack;
10+
use Http\Promise\Promise;
11+
use Psr\Http\Message\RequestInterface;
12+
use Psr\Http\Message\ResponseInterface;
13+
14+
class ProfileClientTest extends \PHPUnit_Framework_TestCase
15+
{
16+
/**
17+
* @var Collector
18+
*/
19+
private $collector;
20+
21+
/**
22+
* @var Stack
23+
*/
24+
private $currentStack;
25+
26+
/**
27+
* @var HttpClient
28+
*/
29+
private $client;
30+
31+
/**
32+
* @var RequestInterface
33+
*/
34+
private $request;
35+
36+
/**
37+
* @var ProfileClient
38+
*/
39+
private $subject;
40+
41+
/**
42+
* @var ResponseInterface
43+
*/
44+
private $response;
45+
46+
/**
47+
* @var Promise
48+
*/
49+
private $promise;
50+
51+
public function setUp()
52+
{
53+
$this->collector = $this->getMockBuilder(Collector::class)->disableOriginalConstructor()->getMock();
54+
$this->currentStack = new Stack('default', 'FormattedRequest');
55+
$this->client = $this->getMockBuilder(ClientInterface::class)->getMock();
56+
$this->request = $this->getMockBuilder(RequestInterface::class)->getMock();
57+
$this->subject = new ProfileClient($this->client, $this->collector);
58+
$this->response = $this->getMockBuilder(ResponseInterface::class)->getMock();
59+
$this->promise = $this->getMockBuilder(Promise::class)->getMock();
60+
61+
$this->client->method('sendRequest')->willReturn($this->response);
62+
$this->client->method('sendAsyncRequest')->willReturn($this->promise);
63+
64+
$this->request->method('getMethod')->willReturn('GET');
65+
$this->request->method('getRequestTarget')->willReturn('/target');
66+
67+
$this->collector->method('getCurrentStack')->willReturn($this->currentStack);
68+
}
69+
70+
public function testCallDecoratedClient()
71+
{
72+
$this->client
73+
->expects($this->once())
74+
->method('sendRequest')
75+
->with($this->identicalTo($this->request))
76+
;
77+
78+
$this->client
79+
->expects($this->once())
80+
->method('sendAsyncRequest')
81+
->with($this->identicalTo($this->request))
82+
;
83+
84+
$this->assertEquals($this->response, $this->subject->sendRequest($this->request));
85+
86+
$this->assertEquals($this->promise, $this->subject->sendAsyncRequest($this->request));
87+
}
88+
89+
public function testCollectRequestInformations()
90+
{
91+
$this->subject->sendRequest($this->request);
92+
93+
$this->assertEquals('GET', $this->currentStack->getRequestMethod());
94+
$this->assertEquals('/target', $this->currentStack->getRequestTarget());
95+
}
96+
97+
public function testCollectAsyncRequestInformations()
98+
{
99+
$this->subject->sendAsyncRequest($this->request);
100+
101+
$this->assertEquals('GET', $this->currentStack->getRequestMethod());
102+
$this->assertEquals('/target', $this->currentStack->getRequestTarget());
103+
}
104+
}
105+
106+
interface ClientInterface extends HttpClient, HttpAsyncClient
107+
{
108+
}

0 commit comments

Comments
 (0)