Passed
Push — develop ( 39b4d5...7a2e88 )
by Jens
17:17
created

ClientFactory::createCustomClient()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 49
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6.0026

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 28
c 1
b 0
f 1
dl 0
loc 49
ccs 23
cts 24
cp 0.9583
rs 8.8497
cc 6
nc 18
nop 7
crap 6.0026
1
<?php
2
3
namespace Commercetools\Core\Client;
4
5
use Commercetools\Core\Cache\CacheAdapterFactory;
6
use Commercetools\Core\Client\OAuth\CacheTokenProvider;
7
use Commercetools\Core\Client\OAuth\ClientCredentials;
8
use Commercetools\Core\Client\OAuth\CredentialTokenProvider;
9
use Commercetools\Core\Client\OAuth\OAuth2Handler;
10
use Commercetools\Core\Client\OAuth\TokenProvider;
11
use Commercetools\Core\Config;
12
use Commercetools\Core\Error\ApiException;
13
use Commercetools\Core\Error\DeprecatedException;
14
use Commercetools\Core\Error\InvalidTokenException;
15
use Commercetools\Core\Error\Message;
16
use Commercetools\Core\Helper\CorrelationIdProvider;
17
use Commercetools\Core\Error\InvalidArgumentException;
18
use Commercetools\Core\Model\Common\Context;
19
use Commercetools\Core\Model\Common\ContextAwareInterface;
20
use Commercetools\Core\Response\AbstractApiResponse;
21
use GuzzleHttp\Client;
22
use GuzzleHttp\Exception\RequestException;
23
use GuzzleHttp\HandlerStack;
24
use GuzzleHttp\MessageFormatter;
25
use GuzzleHttp\Middleware;
26
use Psr\Cache\CacheItemPoolInterface;
27
use Psr\Http\Message\RequestInterface;
28
use Psr\Http\Message\ResponseInterface;
29
use Psr\Log\LoggerInterface;
30
use Psr\Log\LogLevel;
31
use Psr\SimpleCache\CacheInterface;
32
33
class ClientFactory
34
{
35
    /**
36
     * @var bool
37
     */
38
    private static $isGuzzle6;
39
40
    /**
41
     * ClientFactory constructor.
42
     * @throws DeprecatedException
43
     */
44 22
    public function __construct()
45
    {
46 22
        if (!self::isGuzzle6()) {
47
            throw new DeprecatedException("ClientFactory doesn't support Guzzle version < 6");
48
        }
49 22
    }
50
51
    /**
52
     * @param string $clientClass
53
     * @param Config|array $config
54
     * @param LoggerInterface $logger
55
     * @param CacheItemPoolInterface|CacheInterface $cache
56
     * @param TokenProvider $provider
57
     * @param CacheAdapterFactory $cacheAdapterFactory
58
     * @param Context|null $context
59
     * @return Client
60
     */
61 21
    public function createCustomClient(
62
        $clientClass,
63
        $config,
64
        LoggerInterface $logger = null,
65
        $cache = null,
66
        TokenProvider $provider = null,
67
        CacheAdapterFactory $cacheAdapterFactory = null,
68
        Context $context = null
69
    ) {
70 21
        $config = $this->createConfig($config);
71
72 21
        if (is_null($context)) {
73 21
            $context = $config->getContext();
74
        }
75 21
        if (is_null($cacheAdapterFactory)) {
76 21
            $cacheDir = $config->getCacheDir();
77 21
            $cacheDir = !is_null($cacheDir) ? $cacheDir : realpath(__DIR__ . '/../../..');
0 ignored issues
show
introduced by
The condition is_null($cacheDir) is always false.
Loading history...
78 21
            $cacheAdapterFactory = new CacheAdapterFactory($cacheDir);
79
        }
80
81 21
        $cache = $cacheAdapterFactory->get($cache);
82 21
        if (is_null($cache)) {
83
            throw new InvalidArgumentException(Message::INVALID_CACHE_ADAPTER);
84
        }
85
86 21
        $credentials = $config->getClientCredentials();
87 21
        $oauthHandler = $this->getHandler(
88 21
            $credentials,
89 21
            $config->getOauthUrl(),
90
            $cache,
91
            $provider,
92 21
            $config->getOAuthClientOptions(),
93 21
            $config->getCorrelationIdProvider()
94
        );
95
96 21
        $options = $this->getDefaultOptions($config);
97
98 21
        $client = $this->createGuzzle6Client(
99 21
            $clientClass,
100
            $options,
101
            $oauthHandler,
102
            $logger,
103 21
            $config->getCorrelationIdProvider()
104
        );
105
106 21
        if ($client instanceof ContextAwareInterface) {
107 20
            $client->setContext($context);
108
        }
109 21
        return $client;
110
    }
111
112
    /**
113
     * @param Config|array $config
114
     * @param LoggerInterface $logger
115
     * @param CacheItemPoolInterface|CacheInterface $cache
116
     * @param TokenProvider $provider
117
     * @param CacheAdapterFactory $cacheAdapterFactory
118
     * @param Context|null $context
119
     * @return ApiClient
120
     */
121 19
    public function createClient(
122
        $config,
123
        LoggerInterface $logger = null,
124
        $cache = null,
125
        TokenProvider $provider = null,
126
        CacheAdapterFactory $cacheAdapterFactory = null,
127
        Context $context = null
128
    ) {
129 19
        return $this->createCustomClient(
130 19
            ApiClient::class,
131
            $config,
132
            $logger,
133
            $cache,
134
            $provider,
135
            $cacheAdapterFactory,
136
            $context
137
        );
138
    }
139
140 11
    public function createAuthClient(
141
        array $options = [],
142
        CorrelationIdProvider $correlationIdProvider = null
143
    ) {
144 11
        if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
145
            $handler = $options['handler'];
146
        } else {
147 11
            $handler = HandlerStack::create();
148 11
            $options['handler'] = $handler;
149
        }
150
151 11
        $handler->remove("http_errors");
152 11
        $handler->unshift(self::httpErrors(), 'ctp_http_errors');
153
154 11
        $this->setCorrelationIdMiddleware($handler, $correlationIdProvider);
155
156 11
        $options = $this->addMiddlewares($handler, $options);
157
158 11
        return new Client($options);
159
    }
160
161 21
    private function getDefaultOptions(Config $config)
162
    {
163 21
        $options = $config->getClientOptions();
164 21
        $options['http_errors'] = $config->getThrowExceptions();
165 21
        $options['base_uri'] = $config->getApiUrl() . "/" . $config->getProject() . "/";
166
        $defaultHeaders = [
167 21
            'User-Agent' => (new UserAgentProvider())->getUserAgent()
168
        ];
169 21
        if (!is_null($config->getAcceptEncoding())) {
0 ignored issues
show
introduced by
The condition is_null($config->getAcceptEncoding()) is always false.
Loading history...
170 21
            $defaultHeaders['Accept-Encoding'] = $config->getAcceptEncoding();
171
        }
172 21
        $options['headers'] = array_merge($defaultHeaders, (isset($options['headers']) ? $options['headers'] : []));
173
174 21
        return $options;
175
    }
176
177
    /**
178
     * @param Config|array $config
179
     * @return Config
180
     * @throws InvalidArgumentException
181
     */
182 21
    private function createConfig($config)
183
    {
184 21
        if ($config instanceof Config) {
185 21
            return $config;
186
        }
187
        if (is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
188
            return Config::fromArray($config);
189
        }
190
        throw new InvalidArgumentException();
191
    }
192
193
    /**
194
     * @param string $clientClass
195
     * @param array $options
196
     * @param OAuth2Handler $oauthHandler
197
     * @param LoggerInterface|null $logger
198
     * @param CorrelationIdProvider|null $correlationIdProvider
199
     * @return Client
200
     */
201 21
    private function createGuzzle6Client(
202
        $clientClass,
203
        array $options,
204
        OAuth2Handler $oauthHandler,
205
        LoggerInterface $logger = null,
206
        CorrelationIdProvider $correlationIdProvider = null
207
    ) {
208 21
        if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
209
            $handler = $options['handler'];
210
        } else {
211 21
            $handler = HandlerStack::create();
212 21
            $options['handler'] = $handler;
213
        }
214
215 21
        $options = array_merge(
216
            [
217 21
                'allow_redirects' => false,
218
                'verify' => true,
219
                'timeout' => 60,
220
                'connect_timeout' => 10,
221
                'pool_size' => 25,
222
            ],
223
            $options
224
        );
225
226 21
        if (!is_null($logger)) {
227 17
            $this->setLogger($handler, $logger);
228
        }
229
230 21
        $handler->remove("http_errors");
231 21
        $handler->unshift(self::httpErrors(), 'ctp_http_errors');
232
233 21
        $handler->push(
234 21
            Middleware::mapRequest($oauthHandler),
235 21
            'oauth_2_0'
236
        );
237 21
        if ($oauthHandler->refreshable()) {
238 20
            $handler->push(
239 20
                self::reauthenticate($oauthHandler),
240 20
                'reauthenticate'
241
            );
242
        }
243
244 21
        $this->setCorrelationIdMiddleware($handler, $correlationIdProvider);
245 21
        $options = $this->addMiddlewares($handler, $options);
246
247 21
        $client = new $clientClass($options);
248
249 21
        return $client;
250
    }
251
252 17
    private function setLogger(
253
        HandlerStack $handler,
254
        LoggerInterface $logger,
255
        $logLevel = LogLevel::INFO,
256
        $formatter = null
257
    ) {
258 17
        if (is_null($formatter)) {
259 17
            $formatter = new MessageFormatter();
260
        }
261 17
        $handler->push(self::log($logger, $formatter, $logLevel), 'ctp_logger');
262 17
    }
263
264
    /**
265
     * @param CorrelationIdProvider $correlationIdProvider
266
     * @param HandlerStack $handler
267
     */
268 359
    private function setCorrelationIdMiddleware(
269
        HandlerStack $handler,
270
        CorrelationIdProvider $correlationIdProvider = null
271
    ) {
272 21
        if (!is_null($correlationIdProvider)) {
273
            $handler->push(Middleware::mapRequest(function (RequestInterface $request) use ($correlationIdProvider) {
274 359
                return $request->withAddedHeader(
275 359
                    AbstractApiResponse::X_CORRELATION_ID,
276 359
                    $correlationIdProvider->getCorrelationId()
277
                );
278 21
            }), 'ctp_correlation_id');
279
        }
280 21
    }
281
282
    /**
283
     * @param HandlerStack $handler
284
     * @param array $options
285
     * @return array
286
     */
287 21
    private function addMiddlewares(HandlerStack $handler, array $options)
288
    {
289 21
        if (isset($options['middlewares']) && is_array($options['middlewares'])) {
290 1
            foreach ($options['middlewares'] as $key => $middleware) {
291 1
                if (is_callable($middleware)) {
292 1
                    if (!is_numeric($key)) {
293 1
                        $handler->remove($key);
294 1
                        $handler->push($middleware, $key);
295
                    } else {
296
                        $handler->push($middleware);
297
                    }
298
                }
299
            }
300
        }
301 21
        return $options;
302
    }
303
304
    /**
305
     * Middleware that reauthenticates on invalid token error
306
     *
307
     * @param OAuth2Handler $oauthHandler
308
     * @param int $maxRetries
309
     * @return callable Returns a function that accepts the next handler.
310
     */
311 357
    public static function reauthenticate(OAuth2Handler $oauthHandler, $maxRetries = 1)
312
    {
313
        return function (callable $handler) use ($oauthHandler, $maxRetries) {
314
            return function (RequestInterface $request, array $options) use ($handler, $oauthHandler, $maxRetries) {
315 357
                return $handler($request, $options)->then(
316
                    function (ResponseInterface $response) use (
317 357
                        $request,
318 357
                        $handler,
319 357
                        $oauthHandler,
320 357
                        $options,
321 357
                        $maxRetries
322
                    ) {
323 357
                        if ($response->getStatusCode() == 401) {
324 1
                            if (!isset($options['reauth'])) {
325 1
                                $options['reauth'] = 0;
326
                            }
327 1
                            $exception = ApiException::create($request, $response);
328 1
                            if ($options['reauth'] < $maxRetries && $exception instanceof InvalidTokenException) {
329 1
                                $options['reauth']++;
330 1
                                $token = $oauthHandler->refreshToken();
331 1
                                $request = $request->withHeader(
332 1
                                    'Authorization',
333 1
                                    'Bearer ' . $token->getToken()
334
                                );
335 1
                                return $handler($request, $options);
336
                            }
337
                        }
338 356
                        return $response;
339 357
                    }
340
                );
341 18
            };
342 20
        };
343
    }
344
345
    /**
346
     * Middleware that throws exceptions for 4xx or 5xx responses when the
347
     * "http_error" request option is set to true.
348
     *
349
     * @return callable Returns a function that accepts the next handler.
350
     */
351 359
    public static function httpErrors()
352
    {
353
        return function (callable $handler) {
354
            return function ($request, array $options) use ($handler) {
355 359
                if (empty($options['http_errors'])) {
356 16
                    return $handler($request, $options);
357
                }
358 345
                return $handler($request, $options)->then(
359
                    function (ResponseInterface $response) use ($request, $handler) {
0 ignored issues
show
Unused Code introduced by
The import $handler is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
360 345
                        $code = $response->getStatusCode();
361 345
                        if ($code < 400) {
362 340
                            return $response;
363
                        }
364 26
                        throw ApiException::create($request, $response);
365 345
                    }
366
                );
367 19
            };
368 21
        };
369
    }
370
371
    /**
372
     * @param ClientCredentials $credentials
373
     * @param string $accessTokenUrl
374
     * @param CacheItemPoolInterface|CacheInterface $cache
375
     * @param TokenProvider $provider
376
     * @param array $authClientOptions
377
     * @param CorrelationIdProvider|null $correlationIdProvider
378
     * @return OAuth2Handler
379
     */
380 21
    private function getHandler(
381
        ClientCredentials $credentials,
382
        $accessTokenUrl,
383
        $cache,
384
        TokenProvider $provider = null,
385
        array $authClientOptions = [],
386
        CorrelationIdProvider $correlationIdProvider = null
387
    ) {
388 21
        if (is_null($provider)) {
389 11
            $provider = new CredentialTokenProvider(
390 11
                $this->createAuthClient($authClientOptions, $correlationIdProvider),
391
                $accessTokenUrl,
392
                $credentials
393
            );
394 11
            $cacheKey = sha1($credentials->getClientId() . $credentials->getScope());
395 11
            $provider = new CacheTokenProvider($provider, $cache, $cacheKey);
396
        }
397 21
        return new OAuth2Handler($provider);
398
    }
399
400
    /**
401
     * Middleware that logs requests, responses, and errors using a message
402
     * formatter.
403
     *
404
     * @param LoggerInterface  $logger Logs messages.
405
     * @param MessageFormatter $formatter Formatter used to create message strings.
406
     * @param string           $logLevel Level at which to log requests.
407
     *
408
     * @return callable Returns a function that accepts the next handler.
409
     */
410 357
    private static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = LogLevel::INFO)
411
    {
412
        return function (callable $handler) use ($logger, $formatter, $logLevel) {
413
            return function ($request, array $options) use ($handler, $logger, $formatter, $logLevel) {
414 357
                return $handler($request, $options)->then(
415
                    function (ResponseInterface $response) use ($logger, $request, $formatter, $logLevel) {
416 357
                        $message = $formatter->format($request, $response);
417
                        $context = [
418 357
                            AbstractApiResponse::X_CORRELATION_ID => $response->getHeader(
419 357
                                AbstractApiResponse::X_CORRELATION_ID
420
                            )
421
                        ];
422 357
                        $logger->log($logLevel, $message, $context);
423 357
                        return $response;
424 357
                    },
425
                    function ($reason) use ($logger, $request, $formatter) {
426
                        $response = null;
427
                        $context = [];
428
                        if ($reason instanceof RequestException) {
429
                            $response = $reason->getResponse();
430
                            if (!is_null($response)) {
431
                                $context[AbstractApiResponse::X_CORRELATION_ID] = $response->getHeader(
432
                                    AbstractApiResponse::X_CORRELATION_ID
433
                                );
434
                            }
435
                        }
436
                        $message = $formatter->format($request, $response, $reason);
437
                        $logger->notice($message, $context);
438
                        return \GuzzleHttp\Promise\rejection_for($reason);
439 357
                    }
440
                );
441 17
            };
442 17
        };
443
    }
444
445
    /**
446
     * @return bool
447
     */
448 22
    private static function isGuzzle6()
449
    {
450 22
        if (is_null(self::$isGuzzle6)) {
0 ignored issues
show
introduced by
The condition is_null(self::isGuzzle6) is always false.
Loading history...
451 1
            if (version_compare(Client::VERSION, '6.0.0', '>=')) {
452 1
                self::$isGuzzle6 = true;
453
            } else {
454
                self::$isGuzzle6 = false;
455
            }
456
        }
457 22
        return self::$isGuzzle6;
458
    }
459
460
    /**
461
     * @return ClientFactory
462
     */
463 22
    public static function of()
464
    {
465 22
        return new static();
466
    }
467
}
468