Passed
Push — master ( 3f8301...c7321f )
by Jens
42:49 queued 17:59
created

ClientFactory   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 433
Duplicated Lines 0 %

Test Coverage

Coverage 88.69%

Importance

Changes 6
Bugs 3 Features 4
Metric Value
wmc 50
eloc 176
dl 0
loc 433
ccs 149
cts 168
cp 0.8869
rs 8.4
c 6
b 3
f 4

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A log() 0 29 3
A httpErrors() 0 14 3
A createAuthClient() 0 19 3
A addMiddlewares() 0 15 6
A createGuzzle6Client() 0 49 5
A reauthenticate() 0 28 5
A of() 0 3 1
A setCorrelationIdMiddleware() 0 11 2
A getDefaultOptions() 0 14 3
A createClient() 0 16 1
A createConfig() 0 9 3
A getHandler() 0 18 2
A isGuzzle6() 0 10 3
A setLogger() 0 10 2
B createCustomClient() 0 49 6

How to fix   Complexity   

Complex Class

Complex classes like ClientFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClientFactory, and based on these observations, apply Extract Interface, too.

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
/**
34
 * The factory to create a Client for communicating with the commercetools platform
35
 *
36
 * @description
37
 * This factory will create a Guzzle HTTP Client preconfigured for talking to the commercetools platform
38
 *
39
 * ## Instantiation
40
 *
41
 * ```php
42
 * $config = Config::fromArray(
43
 *  ['client_id' => '<client_id>', 'client_secret' => '<client_secret>', 'project' => '<project>']
44
 * );
45
 * $client = ClientFactory::of()->createClient($config);
46
 * ```
47
 *
48
 * ## Execution
49
 *
50
 * ### Synchronous
51
 *
52
 * ```php
53
 * $request = ProductProjectionSearchRequest::of();
54
 * $response = $client->execute($request);
55
 * $products = $request->mapFromResponse($response);
56
 * ```
57
 *
58
 * ### Asynchronous
59
 * The asynchronous execution will return a promise to fulfill the request.
60
 *
61
 * ```php
62
 * $request = ProductProjectionSearchRequest::of();
63
 * $response = $client->executeAsync($request);
64
 * $products = $request->mapFromResponse($response->wait());
65
 * ```
66
 *
67
 * ### Batch
68
 * By filling the batch queue and starting the execution all requests will be executed in parallel.
69
 *
70
 * ```php
71
 * $responses = Pool::batch(
72
 *     $client,
73
 *     [ProductProjectionSearchRequest::of()->httpRequest(), CartByIdGetRequest::ofId($cartId)->httpRequest()]
74
 * );
75
 * ```
76
 *
77
 * ## Instantiation options
78
 *
79
 * ### Using a logger
80
 *
81
 * The client uses the PSR-3 logger interface for logging requests and deprecation notices. To enable
82
 * logging provide a PSR-3 compliant logger (e.g. Monolog).
83
 *
84
 * ```php
85
 * $logger = new \Monolog\Logger('name');
86
 * $logger->pushHandler(new StreamHandler('./requests.log'));
87
 * $client = ClientFactory::of()->createClient($config, $logger);
88
 * ```
89
 *
90
 * ### Using a cache adapter ###
91
 *
92
 * The client will automatically request an OAuth token and store the token in the provided cache.
93
 *
94
 * It's also possible to use a different cache adapter. The SDK provides a Doctrine, a Redis and an APCu cache adapter.
95
 * By default the SDK tries to instantiate the APCu or a PSR-6 filesystem cache adapter if there is no cache given.
96
 * E.g. Redis:
97
 *
98
 * ```php
99
 * $redis = new \Redis();
100
 * $redis->connect('localhost');
101
 * $client = ClientFactory::of()->createClient($config, null, $cache);
102
 * ```
103
 *
104
 * #### Using cache and logger ####
105
 *
106
 * ```php
107
 * $client = ClientFactory::of()->createClient($config, $logger, $cache);
108
 * ```
109
 *
110
 * #### Using a custom cache adapter ####
111
 *
112
 * ```php
113
 * class <CacheClass>Adapter implements \Psr\Cache\CacheItemPoolInterface {
114
 *     protected $cache;
115
 *     public function __construct(<CacheClass> $cache) {
116
 *         $this->cache = $cache;
117
 *     }
118
 * }
119
 *
120
 * $client->getAdapterFactory()->registerCallback(function ($cache) {
121
 *     if ($cache instanceof <CacheClass>) {
122
 *         return new <CacheClass>Adapter($cache);
123
 *     }
124
 *     return null;
125
 * });
126
 * ```
127
 *
128
 * ### Using a custom client class
129
 *
130
 * If some additional configuration is needed or the client should have custom logic you could provide a class name
131
 * to be used for the client instance. This class has to be an extended Guzzle client.
132
 *
133
 * ```php
134
 * $client = ClientFactory::of()->createCustomClient(MyCustomClient::class, $config);
135
 * ```
136
 *
137
 * ## Middlewares
138
 *
139
 * Adding middlewares to the clients for platform as well for the authentication can be done using the config
140
 * by setting client options.
141
 *
142
 * ### Using a HandlerStack
143
 *
144
 * ```php
145
 * $handler = HandlerStack::create();
146
 * $handler->push(Middleware::mapRequest(function (RequestInterface $request) {
147
 *     ...
148
 *     return $request; })
149
 * );
150
 * $config = Config::of()->setClientOptions(['handler' => $handler])
151
 * ```
152
 *
153
 * ### Using a middleware array
154
 *
155
 * ```php
156
 * $middlewares = [
157
 *     Middleware::mapRequest(function (RequestInterface $request) {
158
 *     ...
159
 *     return $request; }),
160
 *     ...
161
 * ]
162
 * $config = Config::of()->setClientOptions(['middlewares' => $middlewares])
163
 * ```
164
 *
165
 * @package Commercetools\Core\Client
166
 */
167
class ClientFactory
168
{
169
    /**
170
     * @var bool
171
     */
172
    private static $isGuzzle6;
173
174
    /**
175
     * ClientFactory constructor.
176
     * @throws DeprecatedException
177
     */
178 25
    public function __construct()
179
    {
180 25
        if (!self::isGuzzle6()) {
181
            throw new DeprecatedException("ClientFactory doesn't support Guzzle version < 6");
182
        }
183 25
    }
184
185
    /**
186
     * @param string $clientClass
187
     * @param Config|array $config
188
     * @param LoggerInterface $logger
189
     * @param CacheItemPoolInterface|CacheInterface $cache
190
     * @param TokenProvider $provider
191
     * @param CacheAdapterFactory $cacheAdapterFactory
192
     * @param Context|null $context
193
     * @return Client
194
     */
195 24
    public function createCustomClient(
196
        $clientClass,
197
        $config,
198
        LoggerInterface $logger = null,
199
        $cache = null,
200
        TokenProvider $provider = null,
201
        CacheAdapterFactory $cacheAdapterFactory = null,
202
        Context $context = null
203
    ) {
204 24
        $config = $this->createConfig($config);
205
206 24
        if (is_null($context)) {
207 24
            $context = $config->getContext();
208
        }
209 24
        if (is_null($cacheAdapterFactory)) {
210 24
            $cacheDir = $config->getCacheDir();
211 24
            $cacheDir = !is_null($cacheDir) ? $cacheDir : realpath(__DIR__ . '/../../..');
0 ignored issues
show
introduced by
The condition is_null($cacheDir) is always false.
Loading history...
212 24
            $cacheAdapterFactory = new CacheAdapterFactory($cacheDir);
213
        }
214
215 24
        $cache = $cacheAdapterFactory->get($cache);
216 24
        if (is_null($cache)) {
217
            throw new InvalidArgumentException(Message::INVALID_CACHE_ADAPTER);
218
        }
219
220 24
        $credentials = $config->getClientCredentials();
221 24
        $oauthHandler = $this->getHandler(
222 24
            $credentials,
223 24
            $config->getOauthUrl(),
224
            $cache,
225
            $provider,
226 24
            $config->getOAuthClientOptions(),
227 24
            $config->getCorrelationIdProvider()
228
        );
229
230 24
        $options = $this->getDefaultOptions($config);
231
232 24
        $client = $this->createGuzzle6Client(
233 24
            $clientClass,
234
            $options,
235
            $oauthHandler,
236
            $logger,
237 24
            $config->getCorrelationIdProvider()
238
        );
239
240 24
        if ($client instanceof ContextAwareInterface) {
241 23
            $client->setContext($context);
242
        }
243 24
        return $client;
244
    }
245
246
    /**
247
     * @param Config|array $config
248
     * @param LoggerInterface $logger
249
     * @param CacheItemPoolInterface|CacheInterface $cache
250
     * @param TokenProvider $provider
251
     * @param CacheAdapterFactory $cacheAdapterFactory
252
     * @param Context|null $context
253
     * @return ApiClient
254
     */
255 22
    public function createClient(
256
        $config,
257
        LoggerInterface $logger = null,
258
        $cache = null,
259
        TokenProvider $provider = null,
260
        CacheAdapterFactory $cacheAdapterFactory = null,
261
        Context $context = null
262
    ) {
263 22
        return $this->createCustomClient(
264 22
            ApiClient::class,
265
            $config,
266
            $logger,
267
            $cache,
268
            $provider,
269
            $cacheAdapterFactory,
270
            $context
271
        );
272
    }
273
274 14
    public function createAuthClient(
275
        array $options = [],
276
        CorrelationIdProvider $correlationIdProvider = null
277
    ) {
278 14
        if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
279
            $handler = $options['handler'];
280
        } else {
281 14
            $handler = HandlerStack::create();
282 14
            $options['handler'] = $handler;
283
        }
284
285 14
        $handler->remove("http_errors");
286 14
        $handler->unshift(self::httpErrors(), 'ctp_http_errors');
287
288 14
        $this->setCorrelationIdMiddleware($handler, $correlationIdProvider);
289
290 14
        $options = $this->addMiddlewares($handler, $options);
291
292 14
        return new Client($options);
293
    }
294
295 24
    private function getDefaultOptions(Config $config)
296
    {
297 24
        $options = $config->getClientOptions();
298 24
        $options['http_errors'] = $config->getThrowExceptions();
299 24
        $options['base_uri'] = $config->getApiUrl() . "/" . $config->getProject() . "/";
300
        $defaultHeaders = [
301 24
            'User-Agent' => (new UserAgentProvider())->getUserAgent()
302
        ];
303 24
        if (!is_null($config->getAcceptEncoding())) {
0 ignored issues
show
introduced by
The condition is_null($config->getAcceptEncoding()) is always false.
Loading history...
304 24
            $defaultHeaders['Accept-Encoding'] = $config->getAcceptEncoding();
305
        }
306 24
        $options['headers'] = array_merge($defaultHeaders, (isset($options['headers']) ? $options['headers'] : []));
307
308 24
        return $options;
309
    }
310
311
    /**
312
     * @param Config|array $config
313
     * @return Config
314
     * @throws InvalidArgumentException
315
     */
316 24
    private function createConfig($config)
317
    {
318 24
        if ($config instanceof Config) {
319 24
            return $config;
320
        }
321
        if (is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
322
            return Config::fromArray($config);
323
        }
324
        throw new InvalidArgumentException();
325
    }
326
327
    /**
328
     * @param string $clientClass
329
     * @param array $options
330
     * @param OAuth2Handler $oauthHandler
331
     * @param LoggerInterface|null $logger
332
     * @param CorrelationIdProvider|null $correlationIdProvider
333
     * @return Client
334
     */
335 24
    private function createGuzzle6Client(
336
        $clientClass,
337
        array $options,
338
        OAuth2Handler $oauthHandler,
339
        LoggerInterface $logger = null,
340
        CorrelationIdProvider $correlationIdProvider = null
341
    ) {
342 24
        if (isset($options['handler']) && $options['handler'] instanceof HandlerStack) {
343
            $handler = $options['handler'];
344
        } else {
345 24
            $handler = HandlerStack::create();
346 24
            $options['handler'] = $handler;
347
        }
348
349 24
        $options = array_merge(
350
            [
351 24
                'allow_redirects' => false,
352
                'verify' => true,
353
                'timeout' => 60,
354
                'connect_timeout' => 10,
355
                'pool_size' => 25,
356
            ],
357
            $options
358
        );
359
360 24
        if (!is_null($logger)) {
361 20
            $this->setLogger($handler, $logger);
362
        }
363
364 24
        $handler->remove("http_errors");
365 24
        $handler->unshift(self::httpErrors(), 'ctp_http_errors');
366
367 24
        $handler->push(
368 24
            Middleware::mapRequest($oauthHandler),
369 24
            'oauth_2_0'
370
        );
371 24
        if ($oauthHandler->refreshable()) {
372 23
            $handler->push(
373 23
                self::reauthenticate($oauthHandler),
374 23
                'reauthenticate'
375
            );
376
        }
377
378 24
        $this->setCorrelationIdMiddleware($handler, $correlationIdProvider);
379 24
        $options = $this->addMiddlewares($handler, $options);
380
381 24
        $client = new $clientClass($options);
382
383 24
        return $client;
384
    }
385
386 20
    private function setLogger(
387
        HandlerStack $handler,
388
        LoggerInterface $logger,
389
        $logLevel = LogLevel::INFO,
390
        $formatter = null
391
    ) {
392 20
        if (is_null($formatter)) {
393 20
            $formatter = new MessageFormatter();
394
        }
395 20
        $handler->push(self::log($logger, $formatter, $logLevel), 'ctp_logger');
396 20
    }
397
398
    /**
399
     * @param CorrelationIdProvider $correlationIdProvider
400
     * @param HandlerStack $handler
401
     */
402 564
    private function setCorrelationIdMiddleware(
403
        HandlerStack $handler,
404
        CorrelationIdProvider $correlationIdProvider = null
405
    ) {
406 24
        if (!is_null($correlationIdProvider)) {
407
            $handler->push(Middleware::mapRequest(function (RequestInterface $request) use ($correlationIdProvider) {
408 564
                return $request->withAddedHeader(
409 564
                    AbstractApiResponse::X_CORRELATION_ID,
410 564
                    $correlationIdProvider->getCorrelationId()
411
                );
412 24
            }), 'ctp_correlation_id');
413
        }
414 24
    }
415
416
    /**
417
     * @param HandlerStack $handler
418
     * @param array $options
419
     * @return array
420
     */
421 24
    private function addMiddlewares(HandlerStack $handler, array $options)
422
    {
423 24
        if (isset($options['middlewares']) && is_array($options['middlewares'])) {
424 4
            foreach ($options['middlewares'] as $key => $middleware) {
425 4
                if (is_callable($middleware)) {
426 4
                    if (!is_numeric($key)) {
427 4
                        $handler->remove($key);
428 4
                        $handler->push($middleware, $key);
429
                    } else {
430
                        $handler->push($middleware);
431
                    }
432
                }
433
            }
434
        }
435 24
        return $options;
436
    }
437
438
    /**
439
     * Middleware that reauthenticates on invalid token error
440
     *
441
     * @param OAuth2Handler $oauthHandler
442
     * @param int $maxRetries
443
     * @return callable Returns a function that accepts the next handler.
444
     */
445 562
    public static function reauthenticate(OAuth2Handler $oauthHandler, $maxRetries = 1)
446
    {
447
        return function (callable $handler) use ($oauthHandler, $maxRetries) {
448
            return function (RequestInterface $request, array $options) use ($handler, $oauthHandler, $maxRetries) {
449 562
                return $handler($request, $options)->then(
450
                    function (ResponseInterface $response) use (
451 562
                        $request,
452 562
                        $handler,
453 562
                        $oauthHandler,
454 562
                        $options,
455 562
                        $maxRetries
456
                    ) {
457 562
                        if ($response->getStatusCode() == 401) {
458 1
                            if (!isset($options['reauth'])) {
459 1
                                $options['reauth'] = 0;
460
                            }
461 1
                            $exception = ApiException::create($request, $response);
462 1
                            if ($options['reauth'] < $maxRetries && $exception instanceof InvalidTokenException) {
463 1
                                $options['reauth']++;
464 1
                                $token = $oauthHandler->refreshToken();
465 1
                                $request = $request->withHeader(
466 1
                                    'Authorization',
467 1
                                    'Bearer ' . $token->getToken()
468
                                );
469 1
                                return $handler($request, $options);
470
                            }
471
                        }
472 561
                        return $response;
473 562
                    }
474
                );
475 21
            };
476 23
        };
477
    }
478
479
    /**
480
     * Middleware that throws exceptions for 4xx or 5xx responses when the
481
     * "http_error" request option is set to true.
482
     *
483
     * @return callable Returns a function that accepts the next handler.
484
     */
485 564
    public static function httpErrors()
486
    {
487
        return function (callable $handler) {
488
            return function ($request, array $options) use ($handler) {
489 564
                if (empty($options['http_errors'])) {
490 17
                    return $handler($request, $options);
491
                }
492 549
                return $handler($request, $options)->then(
493
                    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...
494 549
                        $code = $response->getStatusCode();
495 549
                        if ($code < 400) {
496 539
                            return $response;
497
                        }
498 96
                        throw ApiException::create($request, $response);
499 549
                    }
500
                );
501 22
            };
502 24
        };
503
    }
504
505
    /**
506
     * @param ClientCredentials $credentials
507
     * @param string $accessTokenUrl
508
     * @param CacheItemPoolInterface|CacheInterface $cache
509
     * @param TokenProvider $provider
510
     * @param array $authClientOptions
511
     * @param CorrelationIdProvider|null $correlationIdProvider
512
     * @return OAuth2Handler
513
     */
514 24
    private function getHandler(
515
        ClientCredentials $credentials,
516
        $accessTokenUrl,
517
        $cache,
518
        TokenProvider $provider = null,
519
        array $authClientOptions = [],
520
        CorrelationIdProvider $correlationIdProvider = null
521
    ) {
522 24
        if (is_null($provider)) {
523 14
            $provider = new CredentialTokenProvider(
524 14
                $this->createAuthClient($authClientOptions, $correlationIdProvider),
525
                $accessTokenUrl,
526
                $credentials
527
            );
528 14
            $cacheKey = sha1($credentials->getClientId() . $credentials->getScope());
529 14
            $provider = new CacheTokenProvider($provider, $cache, $cacheKey);
530
        }
531 24
        return new OAuth2Handler($provider);
532
    }
533
534
    /**
535
     * Middleware that logs requests, responses, and errors using a message
536
     * formatter.
537
     *
538
     * @param LoggerInterface  $logger Logs messages.
539
     * @param MessageFormatter $formatter Formatter used to create message strings.
540
     * @param string           $logLevel Level at which to log requests.
541
     *
542
     * @return callable Returns a function that accepts the next handler.
543
     */
544 562
    private static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = LogLevel::INFO)
545
    {
546
        return function (callable $handler) use ($logger, $formatter, $logLevel) {
547
            return function ($request, array $options) use ($handler, $logger, $formatter, $logLevel) {
548 562
                return $handler($request, $options)->then(
549
                    function (ResponseInterface $response) use ($logger, $request, $formatter, $logLevel) {
550 562
                        $message = $formatter->format($request, $response);
551
                        $context = [
552 562
                            AbstractApiResponse::X_CORRELATION_ID => $response->getHeader(
553 562
                                AbstractApiResponse::X_CORRELATION_ID
554
                            )
555
                        ];
556 562
                        $logger->log($logLevel, $message, $context);
557 562
                        return $response;
558 562
                    },
559
                    function ($reason) use ($logger, $request, $formatter) {
560
                        $response = null;
561
                        $context = [];
562
                        if ($reason instanceof RequestException) {
563
                            $response = $reason->getResponse();
564
                            if (!is_null($response)) {
565
                                $context[AbstractApiResponse::X_CORRELATION_ID] = $response->getHeader(
566
                                    AbstractApiResponse::X_CORRELATION_ID
567
                                );
568
                            }
569
                        }
570
                        $message = $formatter->format($request, $response, $reason);
571
                        $logger->notice($message, $context);
572
                        return \GuzzleHttp\Promise\rejection_for($reason);
573 562
                    }
574
                );
575 20
            };
576 20
        };
577
    }
578
579
    /**
580
     * @return bool
581
     */
582 25
    private static function isGuzzle6()
583
    {
584 25
        if (is_null(self::$isGuzzle6)) {
0 ignored issues
show
introduced by
The condition is_null(self::isGuzzle6) is always false.
Loading history...
585 1
            if (version_compare(Client::VERSION, '6.0.0', '>=')) {
586 1
                self::$isGuzzle6 = true;
587
            } else {
588
                self::$isGuzzle6 = false;
589
            }
590
        }
591 25
        return self::$isGuzzle6;
592
    }
593
594
    /**
595
     * @return ClientFactory
596
     */
597 25
    public static function of()
598
    {
599 25
        return new static();
600
    }
601
}
602