Passed
Pull Request — develop (#509)
by Jens
17:22
created

ClientFactory::addMiddlewares()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 16.6682

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 9
c 1
b 0
f 1
dl 0
loc 15
ccs 3
cts 9
cp 0.3333
rs 9.2222
cc 6
nc 2
nop 2
crap 16.6682
1
<?php
2
3
namespace Commercetools\Core\Client;
4
5
use Commercetools\Core\Cache\CacheAdapterFactory;
6
use Commercetools\Core\Client\OAuth\AnonymousFlowTokenProvider;
7
use Commercetools\Core\Client\OAuth\AnonymousIdProvider;
8
use Commercetools\Core\Client\OAuth\CacheTokenProvider;
9
use Commercetools\Core\Client\OAuth\ClientCredentials;
10
use Commercetools\Core\Client\OAuth\CredentialTokenProvider;
11
use Commercetools\Core\Client\OAuth\OAuth2Handler;
12
use Commercetools\Core\Client\OAuth\PasswordFlowTokenProvider;
13
use Commercetools\Core\Client\OAuth\RefreshFlowTokenProvider;
14
use Commercetools\Core\Client\OAuth\TokenProvider;
15
use Commercetools\Core\Client\OAuth\TokenStorage;
16
use Commercetools\Core\Client\OAuth\TokenStorageProvider;
17
use Commercetools\Core\Config;
18
use Commercetools\Core\Error\ApiException;
19
use Commercetools\Core\Error\DeprecatedException;
20
use Commercetools\Core\Error\InvalidTokenException;
21
use Commercetools\Core\Error\Message;
22
use Commercetools\Core\Helper\CorrelationIdProvider;
23
use Commercetools\Core\Error\InvalidArgumentException;
24
use Commercetools\Core\Model\Common\Context;
25
use Commercetools\Core\Model\Common\ContextAwareInterface;
26
use Commercetools\Core\Response\AbstractApiResponse;
27
use GuzzleHttp\Client;
28
use GuzzleHttp\Exception\RequestException;
29
use GuzzleHttp\HandlerStack;
30
use GuzzleHttp\MessageFormatter;
31
use GuzzleHttp\Middleware;
32
use Psr\Cache\CacheItemPoolInterface;
33
use Psr\Http\Message\RequestInterface;
34
use Psr\Http\Message\ResponseInterface;
35
use Psr\Log\LoggerInterface;
36
use Psr\Log\LogLevel;
37
use Psr\SimpleCache\CacheInterface;
38
39
class ClientFactory
40
{
41
    /**
42
     * @var bool
43
     */
44
    private static $isGuzzle6;
45
46
    /**
47
     * ClientFactory constructor.
48
     * @throws DeprecatedException
49
     */
50 20
    public function __construct()
51
    {
52 20
        if (!self::isGuzzle6()) {
53
            throw new DeprecatedException("ClientFactory doesn't support Guzzle version < 6");
54
        }
55 20
    }
56
57
    /**
58
     * @param string $clientClass
59
     * @param Config|array $config
60
     * @param LoggerInterface $logger
61
     * @param CacheItemPoolInterface|CacheInterface $cache
62
     * @param TokenProvider $provider
63
     * @param CacheAdapterFactory $cacheAdapterFactory
64
     * @param Context|null $context
65
     * @return Client
66
     */
67 19
    public function createCustomClient(
68
        $clientClass,
69
        $config,
70
        LoggerInterface $logger = null,
71
        $cache = null,
72
        TokenProvider $provider = null,
73
        CacheAdapterFactory $cacheAdapterFactory = null,
74
        Context $context = null
75
    ) {
76 19
        $config = $this->createConfig($config);
77
78 19
        if (is_null($cacheAdapterFactory)) {
79 19
            $cacheDir = $config->getCacheDir();
80 19
            $cacheDir = !is_null($cacheDir) ? $cacheDir : realpath(__DIR__ . '/../../..');
0 ignored issues
show
introduced by
The condition is_null($cacheDir) is always false.
Loading history...
81 19
            $cacheAdapterFactory = new CacheAdapterFactory($cacheDir);
82
        }
83
84 19
        $cache = $cacheAdapterFactory->get($cache);
85 19
        if (is_null($cache)) {
86
            throw new InvalidArgumentException(Message::INVALID_CACHE_ADAPTER);
87
        }
88
89 19
        $credentials = $config->getClientCredentials();
90 19
        $oauthHandler = $this->getHandler(
91 19
            $credentials,
92 19
            $config->getOauthUrl(),
93
            $cache,
94
            $provider,
95 19
            $config->getOAuthClientOptions(),
96 19
            $config->getCorrelationIdProvider()
97
        );
98
99 19
        $options = $this->getDefaultOptions($config);
100
101 19
        $client = $this->createGuzzle6Client(
102 19
            $clientClass,
103
            $options,
104
            $oauthHandler,
105
            $logger,
106 19
            $config->getCorrelationIdProvider()
107
        );
108
109 19
        if ($client instanceof ContextAwareInterface) {
110 18
            $client->setContext($context);
111
        }
112 19
        return $client;
113
    }
114
115
    /**
116
     * @param Config|array $config
117
     * @param LoggerInterface $logger
118
     * @param CacheItemPoolInterface|CacheInterface $cache
119
     * @param TokenProvider $provider
120
     * @param CacheAdapterFactory $cacheAdapterFactory
121
     * @param Context|null $context
122
     * @return ApiClient
123
     */
124 17
    public function createClient(
125
        $config,
126
        LoggerInterface $logger = null,
127
        $cache = null,
128
        TokenProvider $provider = null,
129
        CacheAdapterFactory $cacheAdapterFactory = null,
130
        Context $context = null
131
    ) {
132 17
        return $this->createCustomClient(
133 17
            ApiClient::class,
134
            $config,
135
            $logger,
136
            $cache,
137
            $provider,
138
            $cacheAdapterFactory,
139
            $context
140
        );
141
    }
142
143 9
    public function createAuthClient(
144
        array $options = [],
145
        CorrelationIdProvider $correlationIdProvider = null
146
    ) {
147 9
        if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
148
            $handler = $options['handler'];
149
        } else {
150 9
            $handler = HandlerStack::create();
151 9
            $options['handler'] = $handler;
152
        }
153
154 9
        $this->setCorrelationIdMiddleware($handler, $correlationIdProvider);
155
156 9
        $options = $this->addMiddlewares($handler, $options);
157
158 9
        return new Client($options);
159
    }
160
161 19
    private function getDefaultOptions(Config $config)
162
    {
163 19
        $options = $config->getClientOptions();
164 19
        $options['http_errors'] = $config->getThrowExceptions();
165 19
        $options['base_uri'] = $config->getApiUrl() . "/" . $config->getProject() . "/";
166
        $defaultHeaders = [
167 19
            'User-Agent' => (new UserAgentProvider())->getUserAgent()
168
        ];
169 19
        if (!is_null($config->getAcceptEncoding())) {
0 ignored issues
show
introduced by
The condition is_null($config->getAcceptEncoding()) is always false.
Loading history...
170 19
            $defaultHeaders['Accept-Encoding'] = $config->getAcceptEncoding();
171
        }
172 19
        $options['headers'] = array_merge($defaultHeaders, (isset($options['headers']) ? $options['headers'] : []));
173
174 19
        return $options;
175
    }
176
177
    /**
178
     * @param Config|array $config
179
     * @return Config
180
     * @throws InvalidArgumentException
181
     */
182 19
    private function createConfig($config)
183
    {
184 19
        if ($config instanceof Config) {
185 19
            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 19
    private function createGuzzle6Client(
202
        $clientClass,
203
        array $options,
204
        OAuth2Handler $oauthHandler,
205
        LoggerInterface $logger = null,
206
        CorrelationIdProvider $correlationIdProvider = null
207
    ) {
208 19
        if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
209
            $handler = $options['handler'];
210
        } else {
211 19
            $handler = HandlerStack::create();
212 19
            $options['handler'] = $handler;
213
        }
214
215 19
        $options = array_merge(
216
            [
217 19
                'allow_redirects' => false,
218
                'verify' => true,
219
                'timeout' => 60,
220
                'connect_timeout' => 10,
221
                'pool_size' => 25,
222
            ],
223
            $options
224
        );
225
226 19
        if (!is_null($logger)) {
227 16
            $this->setLogger($handler, $logger);
228
        }
229
230 19
        $handler->remove("http_errors");
231 19
        $handler->unshift(self::httpErrors(), 'ctp_http_errors');
232
233 19
        $handler->push(
234 19
            Middleware::mapRequest($oauthHandler),
235 19
            'oauth_2_0'
236
        );
237 19
        if ($oauthHandler->refreshable()) {
238 18
            $handler->push(
239 18
                self::reauthenticate($oauthHandler),
240 18
                'reauthenticate'
241
            );
242
        }
243
244 19
        $this->setCorrelationIdMiddleware($handler, $correlationIdProvider);
245 19
        $options = $this->addMiddlewares($handler, $options);
246
247 19
        $client = new $clientClass($options);
248
249 19
        return $client;
250
    }
251
252 16
    private function setLogger(
253
        HandlerStack $handler,
254
        LoggerInterface $logger,
255
        $logLevel = LogLevel::INFO,
256
        $formatter = null
257
    ) {
258 16
        if (is_null($formatter)) {
259 16
            $formatter = new MessageFormatter();
260
        }
261 16
        $handler->push(self::log($logger, $formatter, $logLevel), 'ctp_logger');
262 16
    }
263
264
    /**
265
     * @param CorrelationIdProvider $correlationIdProvider
266
     * @param HandlerStack $handler
267
     */
268 19
    private function setCorrelationIdMiddleware(
269
        HandlerStack $handler,
270
        CorrelationIdProvider $correlationIdProvider = null
271
    ) {
272 19
        if (!is_null($correlationIdProvider)) {
273
            $handler->push(Middleware::mapRequest(function (RequestInterface $request) use ($correlationIdProvider) {
274 17
                return $request->withAddedHeader(
275 17
                    AbstractApiResponse::X_CORRELATION_ID,
276 17
                    $correlationIdProvider->getCorrelationId()
277
                );
278 19
            }), 'ctp_correlation_id');
279
        }
280 19
    }
281
282
    /**
283
     * @param HandlerStack $handler
284
     * @param array $options
285
     * @return array
286
     */
287 19
    private function addMiddlewares(HandlerStack $handler, array $options)
288
    {
289 19
        if (isset($options['middlewares']) && is_array($options['middlewares'])) {
290
            foreach ($options['middlewares'] as $key => $middleware) {
291
                if (is_callable($middleware)) {
292
                    if (!is_numeric($key)) {
293
                        $handler->remove($key);
294
                        $handler->push($middleware, $key);
295
                    } else {
296
                        $handler->push($middleware);
297
                    }
298
                }
299
            }
300
        }
301 19
        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 18
    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 16
                return $handler($request, $options)->then(
316
                    function (ResponseInterface $response) use (
317 16
                        $request,
318 16
                        $handler,
319 16
                        $oauthHandler,
320 16
                        $options,
321 16
                        $maxRetries
322
                    ) {
323 16
                        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 15
                        return $response;
339 16
                    }
340
                );
341 16
            };
342 18
        };
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 19
    public static function httpErrors()
352
    {
353
        return function (callable $handler) {
354
            return function ($request, array $options) use ($handler) {
355 17
                if (empty($options['http_errors'])) {
356 15
                    return $handler($request, $options);
357
                }
358 2
                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 2
                        $code = $response->getStatusCode();
361 2
                        if ($code < 400) {
362
                            return $response;
363
                        }
364 2
                        throw ApiException::create($request, $response);
365 2
                    }
366
                );
367 17
            };
368 19
        };
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 19
    private function getHandler(
381
        ClientCredentials $credentials,
382
        $accessTokenUrl,
383
        $cache,
384
        TokenProvider $provider = null,
385
        array $authClientOptions = [],
386
        CorrelationIdProvider $correlationIdProvider = null
387
    ) {
388 19
        if (is_null($provider)) {
389 9
            $provider = new CredentialTokenProvider(
390 9
                $this->createAuthClient($authClientOptions, $correlationIdProvider),
391
                $accessTokenUrl,
392
                $credentials
393
            );
394 9
            $cacheKey = sha1($credentials->getClientId() . $credentials->getScope());
395 9
            $provider = new CacheTokenProvider($provider, $cache, $cacheKey);
396
        }
397 19
        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 16
    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 16
                return $handler($request, $options)->then(
415
                    function (ResponseInterface $response) use ($logger, $request, $formatter, $logLevel) {
416 16
                        $message = $formatter->format($request, $response);
417
                        $context = [
418 16
                            AbstractApiResponse::X_CORRELATION_ID => $response->getHeader(
419 16
                                AbstractApiResponse::X_CORRELATION_ID
420
                            )
421
                        ];
422 16
                        $logger->log($logLevel, $message, $context);
423 16
                        return $response;
424 16
                    },
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 16
                    }
440
                );
441 16
            };
442 16
        };
443
    }
444
445
    /**
446
     * @return bool
447
     */
448 20
    private static function isGuzzle6()
449
    {
450 20
        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 20
        return self::$isGuzzle6;
458
    }
459
460
    /**
461
     * @return ClientFactory
462
     */
463 20
    public static function of()
464
    {
465 20
        return new static();
466
    }
467
}
468