Passed
Push — develop ( 1bb7ea...fa0beb )
by Jens
10:39 queued 14s
created

ClientFactory::of()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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