Test Failed
Push — develop ( b3c44f...3a55d2 )
by Jens
10:53
created

ClientFactory::createCustomClient()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 3

Importance

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