Passed
Push — develop ( 45c669...883e42 )
by Jens
33:56 queued 16:54
created

ClientFactory::setLogger()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 2
Bugs 1 Features 1
Metric Value
eloc 3
c 2
b 1
f 1
dl 0
loc 10
ccs 4
cts 4
cp 1
rs 10
cc 2
nc 2
nop 4
crap 2
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 21
    public function __construct()
51
    {
52 21
        if (!self::isGuzzle6()) {
53
            throw new DeprecatedException("ClientFactory doesn't support Guzzle version < 6");
54
        }
55 21
    }
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 20
    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 20
        $config = $this->createConfig($config);
77
78 20
        if (is_null($context)) {
79 20
            $context = $config->getContext();
80
        }
81 20
        if (is_null($cacheAdapterFactory)) {
82 20
            $cacheDir = $config->getCacheDir();
83 20
            $cacheDir = !is_null($cacheDir) ? $cacheDir : realpath(__DIR__ . '/../../..');
0 ignored issues
show
introduced by
The condition is_null($cacheDir) is always false.
Loading history...
84 20
            $cacheAdapterFactory = new CacheAdapterFactory($cacheDir);
85
        }
86
87 20
        $cache = $cacheAdapterFactory->get($cache);
88 20
        if (is_null($cache)) {
89
            throw new InvalidArgumentException(Message::INVALID_CACHE_ADAPTER);
90
        }
91
92 20
        $credentials = $config->getClientCredentials();
93 20
        $oauthHandler = $this->getHandler(
94 20
            $credentials,
95 20
            $config->getOauthUrl(),
96
            $cache,
97
            $provider,
98 20
            $config->getOAuthClientOptions(),
99 20
            $config->getCorrelationIdProvider()
100
        );
101
102 20
        $options = $this->getDefaultOptions($config);
103
104 20
        $client = $this->createGuzzle6Client(
105 20
            $clientClass,
106
            $options,
107
            $oauthHandler,
108
            $logger,
109 20
            $config->getCorrelationIdProvider()
110
        );
111
112 20
        if ($client instanceof ContextAwareInterface) {
113 19
            $client->setContext($context);
114
        }
115 20
        return $client;
116
    }
117
118
    /**
119
     * @param Config|array $config
120
     * @param LoggerInterface $logger
121
     * @param CacheItemPoolInterface|CacheInterface $cache
122
     * @param TokenProvider $provider
123
     * @param CacheAdapterFactory $cacheAdapterFactory
124
     * @param Context|null $context
125
     * @return ApiClient
126
     */
127 18
    public function createClient(
128
        $config,
129
        LoggerInterface $logger = null,
130
        $cache = null,
131
        TokenProvider $provider = null,
132
        CacheAdapterFactory $cacheAdapterFactory = null,
133
        Context $context = null
134
    ) {
135 18
        return $this->createCustomClient(
136 18
            ApiClient::class,
137
            $config,
138
            $logger,
139
            $cache,
140
            $provider,
141
            $cacheAdapterFactory,
142
            $context
143
        );
144
    }
145
146 10
    public function createAuthClient(
147
        array $options = [],
148
        CorrelationIdProvider $correlationIdProvider = null
149
    ) {
150 10
        if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
151
            $handler = $options['handler'];
152
        } else {
153 10
            $handler = HandlerStack::create();
154 10
            $options['handler'] = $handler;
155
        }
156
157 10
        $this->setCorrelationIdMiddleware($handler, $correlationIdProvider);
158
159 10
        $options = $this->addMiddlewares($handler, $options);
160
161 10
        return new Client($options);
162
    }
163
164 20
    private function getDefaultOptions(Config $config)
165
    {
166 20
        $options = $config->getClientOptions();
167 20
        $options['http_errors'] = $config->getThrowExceptions();
168 20
        $options['base_uri'] = $config->getApiUrl() . "/" . $config->getProject() . "/";
169
        $defaultHeaders = [
170 20
            'User-Agent' => (new UserAgentProvider())->getUserAgent()
171
        ];
172 20
        if (!is_null($config->getAcceptEncoding())) {
0 ignored issues
show
introduced by
The condition is_null($config->getAcceptEncoding()) is always false.
Loading history...
173 20
            $defaultHeaders['Accept-Encoding'] = $config->getAcceptEncoding();
174
        }
175 20
        $options['headers'] = array_merge($defaultHeaders, (isset($options['headers']) ? $options['headers'] : []));
176
177 20
        return $options;
178
    }
179
180
    /**
181
     * @param Config|array $config
182
     * @return Config
183
     * @throws InvalidArgumentException
184
     */
185 20
    private function createConfig($config)
186
    {
187 20
        if ($config instanceof Config) {
188 20
            return $config;
189
        }
190
        if (is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
191
            return Config::fromArray($config);
192
        }
193
        throw new InvalidArgumentException();
194
    }
195
196
    /**
197
     * @param string $clientClass
198
     * @param array $options
199
     * @param OAuth2Handler $oauthHandler
200
     * @param LoggerInterface|null $logger
201
     * @param CorrelationIdProvider|null $correlationIdProvider
202
     * @return Client
203
     */
204 20
    private function createGuzzle6Client(
205
        $clientClass,
206
        array $options,
207
        OAuth2Handler $oauthHandler,
208
        LoggerInterface $logger = null,
209
        CorrelationIdProvider $correlationIdProvider = null
210
    ) {
211 20
        if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
212
            $handler = $options['handler'];
213
        } else {
214 20
            $handler = HandlerStack::create();
215 20
            $options['handler'] = $handler;
216
        }
217
218 20
        $options = array_merge(
219
            [
220 20
                'allow_redirects' => false,
221
                'verify' => true,
222
                'timeout' => 60,
223
                'connect_timeout' => 10,
224
                'pool_size' => 25,
225
            ],
226
            $options
227
        );
228
229 20
        if (!is_null($logger)) {
230 17
            $this->setLogger($handler, $logger);
231
        }
232
233 20
        $handler->remove("http_errors");
234 20
        $handler->unshift(self::httpErrors(), 'ctp_http_errors');
235
236 20
        $handler->push(
237 20
            Middleware::mapRequest($oauthHandler),
238 20
            'oauth_2_0'
239
        );
240 20
        if ($oauthHandler->refreshable()) {
241 19
            $handler->push(
242 19
                self::reauthenticate($oauthHandler),
243 19
                'reauthenticate'
244
            );
245
        }
246
247 20
        $this->setCorrelationIdMiddleware($handler, $correlationIdProvider);
248 20
        $options = $this->addMiddlewares($handler, $options);
249
250 20
        $client = new $clientClass($options);
251
252 20
        return $client;
253
    }
254
255 17
    private function setLogger(
256
        HandlerStack $handler,
257
        LoggerInterface $logger,
258
        $logLevel = LogLevel::INFO,
259
        $formatter = null
260
    ) {
261 17
        if (is_null($formatter)) {
262 17
            $formatter = new MessageFormatter();
263
        }
264 17
        $handler->push(self::log($logger, $formatter, $logLevel), 'ctp_logger');
265 17
    }
266
267
    /**
268
     * @param CorrelationIdProvider $correlationIdProvider
269
     * @param HandlerStack $handler
270
     */
271 341
    private function setCorrelationIdMiddleware(
272
        HandlerStack $handler,
273
        CorrelationIdProvider $correlationIdProvider = null
274
    ) {
275 20
        if (!is_null($correlationIdProvider)) {
276
            $handler->push(Middleware::mapRequest(function (RequestInterface $request) use ($correlationIdProvider) {
277 341
                return $request->withAddedHeader(
278 341
                    AbstractApiResponse::X_CORRELATION_ID,
279 341
                    $correlationIdProvider->getCorrelationId()
280
                );
281 20
            }), 'ctp_correlation_id');
282
        }
283 20
    }
284
285
    /**
286
     * @param HandlerStack $handler
287
     * @param array $options
288
     * @return array
289
     */
290 20
    private function addMiddlewares(HandlerStack $handler, array $options)
291
    {
292 20
        if (isset($options['middlewares']) && is_array($options['middlewares'])) {
293 1
            foreach ($options['middlewares'] as $key => $middleware) {
294 1
                if (is_callable($middleware)) {
295 1
                    if (!is_numeric($key)) {
296 1
                        $handler->remove($key);
297 1
                        $handler->push($middleware, $key);
298
                    } else {
299
                        $handler->push($middleware);
300
                    }
301
                }
302
            }
303
        }
304 20
        return $options;
305
    }
306
307
    /**
308
     * Middleware that reauthenticates on invalid token error
309
     *
310
     * @param OAuth2Handler $oauthHandler
311
     * @param int $maxRetries
312
     * @return callable Returns a function that accepts the next handler.
313
     */
314 340
    public static function reauthenticate(OAuth2Handler $oauthHandler, $maxRetries = 1)
315
    {
316
        return function (callable $handler) use ($oauthHandler, $maxRetries) {
317
            return function (RequestInterface $request, array $options) use ($handler, $oauthHandler, $maxRetries) {
318 340
                return $handler($request, $options)->then(
319
                    function (ResponseInterface $response) use (
320 340
                        $request,
321 340
                        $handler,
322 340
                        $oauthHandler,
323 340
                        $options,
324 340
                        $maxRetries
325
                    ) {
326 340
                        if ($response->getStatusCode() == 401) {
327 1
                            if (!isset($options['reauth'])) {
328 1
                                $options['reauth'] = 0;
329
                            }
330 1
                            $exception = ApiException::create($request, $response);
331 1
                            if ($options['reauth'] < $maxRetries && $exception instanceof InvalidTokenException) {
332 1
                                $options['reauth']++;
333 1
                                $token = $oauthHandler->refreshToken();
334 1
                                $request = $request->withHeader(
335 1
                                    'Authorization',
336 1
                                    'Bearer ' . $token->getToken()
337
                                );
338 1
                                return $handler($request, $options);
339
                            }
340
                        }
341 339
                        return $response;
342 340
                    }
343
                );
344 17
            };
345 19
        };
346
    }
347
348
    /**
349
     * Middleware that throws exceptions for 4xx or 5xx responses when the
350
     * "http_error" request option is set to true.
351
     *
352
     * @return callable Returns a function that accepts the next handler.
353
     */
354 341
    public static function httpErrors()
355
    {
356
        return function (callable $handler) {
357
            return function ($request, array $options) use ($handler) {
358 341
                if (empty($options['http_errors'])) {
359 15
                    return $handler($request, $options);
360
                }
361 326
                return $handler($request, $options)->then(
362
                    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...
363 326
                        $code = $response->getStatusCode();
364 326
                        if ($code < 400) {
365 322
                            return $response;
366
                        }
367 23
                        throw ApiException::create($request, $response);
368 326
                    }
369
                );
370 18
            };
371 20
        };
372
    }
373
374
    /**
375
     * @param ClientCredentials $credentials
376
     * @param string $accessTokenUrl
377
     * @param CacheItemPoolInterface|CacheInterface $cache
378
     * @param TokenProvider $provider
379
     * @param array $authClientOptions
380
     * @param CorrelationIdProvider|null $correlationIdProvider
381
     * @return OAuth2Handler
382
     */
383 20
    private function getHandler(
384
        ClientCredentials $credentials,
385
        $accessTokenUrl,
386
        $cache,
387
        TokenProvider $provider = null,
388
        array $authClientOptions = [],
389
        CorrelationIdProvider $correlationIdProvider = null
390
    ) {
391 20
        if (is_null($provider)) {
392 10
            $provider = new CredentialTokenProvider(
393 10
                $this->createAuthClient($authClientOptions, $correlationIdProvider),
394
                $accessTokenUrl,
395
                $credentials
396
            );
397 10
            $cacheKey = sha1($credentials->getClientId() . $credentials->getScope());
398 10
            $provider = new CacheTokenProvider($provider, $cache, $cacheKey);
399
        }
400 20
        return new OAuth2Handler($provider);
401
    }
402
403
    /**
404
     * Middleware that logs requests, responses, and errors using a message
405
     * formatter.
406
     *
407
     * @param LoggerInterface  $logger Logs messages.
408
     * @param MessageFormatter $formatter Formatter used to create message strings.
409
     * @param string           $logLevel Level at which to log requests.
410
     *
411
     * @return callable Returns a function that accepts the next handler.
412
     */
413 340
    private static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = LogLevel::INFO)
414
    {
415
        return function (callable $handler) use ($logger, $formatter, $logLevel) {
416
            return function ($request, array $options) use ($handler, $logger, $formatter, $logLevel) {
417 340
                return $handler($request, $options)->then(
418
                    function (ResponseInterface $response) use ($logger, $request, $formatter, $logLevel) {
419 340
                        $message = $formatter->format($request, $response);
420
                        $context = [
421 340
                            AbstractApiResponse::X_CORRELATION_ID => $response->getHeader(
422 340
                                AbstractApiResponse::X_CORRELATION_ID
423
                            )
424
                        ];
425 340
                        $logger->log($logLevel, $message, $context);
426 340
                        return $response;
427 340
                    },
428
                    function ($reason) use ($logger, $request, $formatter) {
429
                        $response = null;
430
                        $context = [];
431
                        if ($reason instanceof RequestException) {
432
                            $response = $reason->getResponse();
433
                            if (!is_null($response)) {
434
                                $context[AbstractApiResponse::X_CORRELATION_ID] = $response->getHeader(
435
                                    AbstractApiResponse::X_CORRELATION_ID
436
                                );
437
                            }
438
                        }
439
                        $message = $formatter->format($request, $response, $reason);
440
                        $logger->notice($message, $context);
441
                        return \GuzzleHttp\Promise\rejection_for($reason);
442 340
                    }
443
                );
444 17
            };
445 17
        };
446
    }
447
448
    /**
449
     * @return bool
450
     */
451 21
    private static function isGuzzle6()
452
    {
453 21
        if (is_null(self::$isGuzzle6)) {
0 ignored issues
show
introduced by
The condition is_null(self::isGuzzle6) is always false.
Loading history...
454 1
            if (version_compare(Client::VERSION, '6.0.0', '>=')) {
455 1
                self::$isGuzzle6 = true;
456
            } else {
457
                self::$isGuzzle6 = false;
458
            }
459
        }
460 21
        return self::$isGuzzle6;
461
    }
462
463
    /**
464
     * @return ClientFactory
465
     */
466 21
    public static function of()
467
    {
468 21
        return new static();
469
    }
470
}
471