Passed
Push — master ( da0795...78880b )
by Jens
14:45 queued 18s
created

ClientFactory::getDefaultOptions()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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