Skip to content
This repository was archived by the owner on Oct 24, 2023. It is now read-only.

Commit 40a0bd0

Browse files
committed
feat(Client): add client factory for direct usage of guzzle http client
1 parent 208933d commit 40a0bd0

File tree

11 files changed

+705
-10
lines changed

11 files changed

+705
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.vagrant
22
.idea
33
/vendor
4+
/cache
45
/build/*
56
!/build/theme
67
!/build/apigen.neon

src/Core/Client/ClientFactory.php

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
<?php
2+
3+
namespace Commercetools\Core\Client;
4+
5+
use Cache\Adapter\Filesystem\FilesystemCachePool;
6+
use Commercetools\Core\Client\OAuth\ClientCredentials;
7+
use Commercetools\Core\Client\OAuth\CredentialTokenProvider;
8+
use Commercetools\Core\Client\OAuth\OAuth2Handler;
9+
use Commercetools\Core\Client\OAuth\TokenProvider;
10+
use Commercetools\Core\Config;
11+
use Commercetools\Core\Helper\CorrelationIdProvider;
12+
use Commercetools\Core\Helper\Subscriber\Log\Formatter;
13+
use Commercetools\Core\Helper\Subscriber\Log\LogSubscriber;
14+
use Commercetools\Core\Error\InvalidArgumentException;
15+
use Commercetools\Core\Response\AbstractApiResponse;
16+
use GuzzleHttp\Client as HttpClient;
17+
use GuzzleHttp\Client;
18+
use GuzzleHttp\Event\BeforeEvent;
19+
use GuzzleHttp\Exception\RequestException;
20+
use GuzzleHttp\HandlerStack;
21+
use GuzzleHttp\MessageFormatter;
22+
use GuzzleHttp\Middleware;
23+
use GuzzleHttp\Psr7\Response;
24+
use League\Flysystem\Adapter\Local;
25+
use League\Flysystem\Filesystem;
26+
use Psr\Cache\CacheItemPoolInterface;
27+
use Psr\Http\Message\RequestInterface;
28+
use GuzzleHttp\Message\RequestInterface as GuzzleRequestInterface;
29+
use GuzzleHttp\Message\ResponseInterface as GuzzleResponseInferface;
30+
use Psr\Http\Message\ResponseInterface;
31+
use Psr\Log\LoggerInterface;
32+
use Psr\Log\LogLevel;
33+
34+
class ClientFactory
35+
{
36+
/**
37+
* @var bool
38+
*/
39+
private static $isGuzzle6;
40+
41+
/**
42+
* @param Config|array $config
43+
* @param LoggerInterface $logger
44+
* @param CacheItemPoolInterface $cache
45+
* @param TokenProvider $provider
46+
* @return HttpClient
47+
*/
48+
public function createClient(
49+
$config,
50+
LoggerInterface $logger = null,
51+
CacheItemPoolInterface $cache = null,
52+
TokenProvider $provider = null
53+
) {
54+
$config = $this->createConfig($config);
55+
56+
if (is_null($cache)) {
57+
$cacheDir = $config->getCacheDir();
58+
$cacheDir = !is_null($cacheDir) ? $cacheDir : realpath(__DIR__ . '/../../..');
59+
$filesystemAdapter = new Local($cacheDir);
60+
$filesystem = new Filesystem($filesystemAdapter);
61+
$cache = new FilesystemCachePool($filesystem);
62+
}
63+
$credentials = $config->getClientCredentials();
64+
$oauthHandler = $this->getHandler(
65+
$credentials,
66+
$config->getOauthUrl(),
67+
$cache,
68+
$provider,
69+
$config->getOAuthClientOptions()
70+
);
71+
72+
$options = $this->getDefaultOptions($config);
73+
if (self::isGuzzle6()) {
74+
return $this->createGuzzle6Client($options, $oauthHandler, $logger, $config->getCorrelationIdProvider());
75+
} else {
76+
return $this->createGuzzle5Client($options, $oauthHandler, $logger);
77+
}
78+
}
79+
80+
private function getDefaultOptions(Config $config)
81+
{
82+
$options = $config->getClientOptions();
83+
$options['base_uri'] = $config->getApiUrl() . "/" . $config->getProject();
84+
$defaultHeaders = [
85+
'User-Agent' => (new UserAgentProvider())->getUserAgent()
86+
];
87+
if (!is_null($config->getAcceptEncoding())) {
88+
$defaultHeaders['Accept-Encoding'] = $config->getAcceptEncoding();
89+
}
90+
$options['headers'] = array_merge($defaultHeaders, (isset($options['headers']) ? $options['headers'] : []));
91+
92+
return $options;
93+
}
94+
95+
/**
96+
* @param Config|array $config
97+
* @return Config
98+
* @throws InvalidArgumentException
99+
*/
100+
private function createConfig($config)
101+
{
102+
if ($config instanceof Config) {
103+
return $config;
104+
}
105+
if (is_array($config)) {
106+
return Config::fromArray($config);
107+
}
108+
throw new InvalidArgumentException();
109+
}
110+
111+
/**
112+
* @param array $options
113+
* @param LoggerInterface|null $logger
114+
* @param OAuth2Handler $oauthHandler
115+
* @return HttpClient
116+
*/
117+
private function createGuzzle6Client(
118+
array $options,
119+
OAuth2Handler $oauthHandler,
120+
LoggerInterface $logger = null,
121+
CorrelationIdProvider $correlationIdProvider = null
122+
) {
123+
if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
124+
$handler = $options['handler'];
125+
} else {
126+
$handler = HandlerStack::create();
127+
$options['handler'] = $handler;
128+
}
129+
130+
$options = array_merge(
131+
[
132+
'allow_redirects' => false,
133+
'verify' => true,
134+
'timeout' => 60,
135+
'connect_timeout' => 10,
136+
'pool_size' => 25
137+
],
138+
$options
139+
);
140+
141+
if (!is_null($logger)) {
142+
$this->setLogger($handler, $logger);
143+
}
144+
$handler->push(
145+
Middleware::mapRequest($oauthHandler),
146+
'oauth_2_0'
147+
);
148+
149+
if (!is_null($correlationIdProvider)) {
150+
$handler->push(Middleware::mapRequest(function (RequestInterface $request) use ($correlationIdProvider) {
151+
return $request->withAddedHeader(
152+
AbstractApiResponse::X_CORRELATION_ID,
153+
$correlationIdProvider->getCorrelationId()
154+
);
155+
}), 'ctp_correlation_id');
156+
}
157+
158+
$client = new HttpClient($options);
159+
160+
return $client;
161+
}
162+
163+
private function setLogger(HandlerStack $handler, LoggerInterface $logger, $logLevel = LogLevel::INFO, $formatter = null)
164+
{
165+
if (is_null($formatter)) {
166+
$formatter = new MessageFormatter();
167+
}
168+
$handler->push(self::log($logger, $formatter, $logLevel), 'ctp_logger');
169+
}
170+
171+
/**
172+
* Middleware that logs requests, responses, and errors using a message
173+
* formatter.
174+
*
175+
* @param LoggerInterface $logger Logs messages.
176+
* @param MessageFormatter $formatter Formatter used to create message strings.
177+
* @param string $logLevel Level at which to log requests.
178+
*
179+
* @return callable Returns a function that accepts the next handler.
180+
*/
181+
private static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = LogLevel::INFO)
182+
{
183+
return function (callable $handler) use ($logger, $formatter, $logLevel) {
184+
return function ($request, array $options) use ($handler, $logger, $formatter, $logLevel) {
185+
return $handler($request, $options)->then(
186+
function ($response) use ($logger, $request, $formatter, $logLevel) {
187+
$message = $formatter->format($request, $response);
188+
$context = [
189+
AbstractApiResponse::X_CORRELATION_ID => $response->getHeader(
190+
AbstractApiResponse::X_CORRELATION_ID
191+
)
192+
];
193+
$logger->log($logLevel, $message, $context);
194+
return $response;
195+
},
196+
function ($reason) use ($logger, $request, $formatter) {
197+
$response = null;
198+
$context = [];
199+
if ($reason instanceof RequestException) {
200+
$response = $reason->getResponse();
201+
if (!is_null($response)) {
202+
$context[AbstractApiResponse::X_CORRELATION_ID] = $response->getHeader(
203+
AbstractApiResponse::X_CORRELATION_ID
204+
);
205+
}
206+
}
207+
$message = $formatter->format($request, $response, $reason);
208+
$logger->notice($message, $context);
209+
return \GuzzleHttp\Promise\rejection_for($reason);
210+
}
211+
);
212+
};
213+
};
214+
}
215+
216+
/**
217+
* @param array $options
218+
* @param LoggerInterface|null $logger
219+
* @param OAuth2Handler $oauthHandler
220+
* @return HttpClient
221+
*/
222+
private function createGuzzle5Client(
223+
array $options,
224+
OAuth2Handler $oauthHandler,
225+
LoggerInterface $logger = null
226+
) {
227+
if (isset($options['base_uri'])) {
228+
$options['base_url'] = $options['base_uri'];
229+
unset($options['base_uri']);
230+
}
231+
if (isset($options['headers'])) {
232+
$options['defaults']['headers'] = $options['headers'];
233+
unset($options['headers']);
234+
}
235+
$options = array_merge(
236+
[
237+
'allow_redirects' => false,
238+
'verify' => true,
239+
'timeout' => 60,
240+
'connect_timeout' => 10,
241+
'pool_size' => 25
242+
],
243+
$options
244+
);
245+
$client = new HttpClient($options);
246+
if ($logger instanceof LoggerInterface) {
247+
$formatter = new Formatter();
248+
$client->getEmitter()->attach(new LogSubscriber($logger, $formatter, LogLevel::INFO));
249+
}
250+
251+
$client->getEmitter()->on('before', function (BeforeEvent $e) use ($oauthHandler) {
252+
$e->getRequest()->setHeader('Authorization', $oauthHandler->getAuthorizationHeader());
253+
});
254+
255+
return $client;
256+
}
257+
258+
/**
259+
* @param RequestInterface $psrRequest
260+
* @param HttpClient $client
261+
* @return GuzzleRequestInterface|RequestInterface
262+
*/
263+
public function createRequest(RequestInterface $psrRequest, HttpClient $client)
264+
{
265+
if (self::isGuzzle6()) {
266+
return $psrRequest;
267+
}
268+
$options = [
269+
'headers' => $psrRequest->getHeaders(),
270+
'body' => (string)$psrRequest->getBody()
271+
];
272+
273+
return $client->createRequest($psrRequest->getMethod(), (string)$psrRequest->getUri(), $options);
274+
}
275+
276+
/**
277+
* @param GuzzleResponseInferface|ResponseInterface $response
278+
* @return ResponseInterface
279+
*/
280+
public function createResponse($response)
281+
{
282+
if ($response instanceof ResponseInterface) {
283+
return $response;
284+
}
285+
if ($response instanceof GuzzleResponseInferface) {
286+
return new Response(
287+
$response->getStatusCode(),
288+
$response->getHeaders(),
289+
(string)$response->getBody()
290+
);
291+
}
292+
293+
throw new InvalidArgumentException(
294+
'Argument 1 must be an instance of Psr\Http\Message\ResponseInterface ' .
295+
'or GuzzleHttp\Message\ResponseInterface'
296+
);
297+
}
298+
299+
/**
300+
* @param ClientCredentials $credentials
301+
* @param string $accessTokenUrl
302+
* @param CacheItemPoolInterface $cache
303+
* @param TokenProvider $provider
304+
* @param array $authClientOptions
305+
* @return OAuth2Handler
306+
*/
307+
private function getHandler(
308+
ClientCredentials $credentials,
309+
$accessTokenUrl,
310+
CacheItemPoolInterface $cache = null,
311+
TokenProvider $provider = null,
312+
array $authClientOptions = []
313+
) {
314+
if (is_null($provider)) {
315+
$provider = new CredentialTokenProvider(
316+
new HttpClient($authClientOptions),
317+
$accessTokenUrl,
318+
$credentials
319+
);
320+
}
321+
return new OAuth2Handler($provider, $cache);
322+
}
323+
324+
/**
325+
* @return bool
326+
*/
327+
private static function isGuzzle6()
328+
{
329+
if (is_null(self::$isGuzzle6)) {
330+
if (version_compare(HttpClient::VERSION, '6.0.0', '>=')) {
331+
self::$isGuzzle6 = true;
332+
} else {
333+
self::$isGuzzle6 = false;
334+
}
335+
}
336+
return self::$isGuzzle6;
337+
}
338+
339+
/**
340+
* @return ClientFactory
341+
*/
342+
public static function of()
343+
{
344+
return new static();
345+
}
346+
347+
protected function getUserAgent()
348+
{
349+
if (is_null($this->userAgent)) {
350+
$agent = 'commercetools-php-sdk/' . static::VERSION;
351+
352+
$adapterClass = $this->getAdapterFactory()->getClass($this->getConfig()->getAdapter());
353+
$agent .= ' (' . $adapterClass::getAdapterInfo();
354+
if (extension_loaded('curl') && function_exists('curl_version')) {
355+
$agent .= '; curl/' . \curl_version()['version'];
356+
}
357+
$agent .= ') PHP/' . PHP_VERSION;
358+
$this->userAgent = $agent;
359+
}
360+
361+
return $this->userAgent;
362+
}
363+
}

0 commit comments

Comments
 (0)