Test Failed
Push — develop ( 5182a7...e299a6 )
by Jens
16:32 queued 13s
created

Client::logExceptionResponse()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 20
ccs 10
cts 10
cp 1
rs 9.8333
cc 3
nc 3
nop 1
crap 3
1
<?php
2
/**
3
 * @author @jenschude <[email protected]>
4
 * @created 19.01.15, 14:29
5
 */
6
7
namespace Commercetools\Core;
8
9
use Commercetools\Core\Client\Adapter\CorrelationIdAware;
10
use Commercetools\Core\Client\Adapter\TokenProviderAware;
11
use Commercetools\Core\Helper\CorrelationIdProvider;
12
use Commercetools\Core\Client\Adapter\AdapterOptionInterface;
13
use Commercetools\Core\Response\ApiPromiseGetInterface;
14
use Commercetools\Core\Response\ErrorResponse;
15
use Psr\Http\Message\RequestInterface;
16
use Psr\Http\Message\ResponseInterface;
17
use Psr\Log\LoggerAwareInterface;
18
use Psr\Log\LoggerInterface;
19
use Commercetools\Core\Client\Adapter\AdapterInterface;
20
use Commercetools\Core\Error\InvalidTokenException;
21
use Commercetools\Core\Error\Message;
22
use Commercetools\Core\Error\ApiException;
23
use Commercetools\Core\Model\Common\ContextAwareInterface;
24
use Commercetools\Core\Response\ApiResponseInterface;
25
use Commercetools\Core\Request\ClientRequestInterface;
26
use Commercetools\Core\Client\OAuth\Manager;
27
use function GuzzleHttp\Psr7\stream_for;
28
29
/**
30
 * The client for communicating with the commercetools platform
31
 *
32
 * @description
33
 * ## Instantiation
34
 *
35
 * ```php
36
 * $config = Config::fromArray(
37
 *  ['client_id' => '<client_id>', 'client_secret' => '<client_secret>', 'project' => '<project>']
38
 * );
39
 * $client = Client::ofConfig($config);
40
 * ```
41
 *
42
 * ## Execution
43
 *
44
 * ### Synchronous
45
 * There are two main approaches for retrieving result objects
46
 *
47
 * Client centric:
48
 *
49
 * ```php
50
 * $response = $client->execute(ProductProjectionSearchRequest::of());
51
 * $products = $response->toObject();
52
 * ```
53
 *
54
 * Request centric:
55
 *
56
 * ```php
57
 * $request = ProductProjectionSearchRequest::of();
58
 * $response = $request->executeWithClient($client);
59
 * $products = $request->mapFromResponse($response);
60
 * ```
61
 *
62
 * By using the request centric approach the IDE is capable of resolving the correct classes and give
63
 * a maximum of support with available methods.
64
 *
65
 * ### Asynchronous
66
 * The asynchronous execution will return a promise to fulfill the request.
67
 *
68
 * ```php
69
 * $response = $client->executeAsync(ProductProjectionSearchRequest::of());
70
 * ```
71
 *
72
 * ### Batch
73
 * By filling the batch queue and starting the execution all requests will be executed in parallel.
74
 *
75
 * ```php
76
 * $client->addBatchRequest(ProductProjectionSearchRequest::of())
77
 *     ->addBatchRequest(CartByIdGetRequest::ofId($cartId));
78
 * $responses = $client->executeBatch();
79
 * ```
80
 *
81
 * ## Instantiation options
82
 *
83
 * ### Using a logger
84
 *
85
 * The client uses the PSR-3 logger interface for logging requests and deprecation notices. To enable
86
 * logging provide a PSR-3 compliant logger (e.g. Monolog).
87
 *
88
 * ```php
89
 * $logger = new \Monolog\Logger('name');
90
 * $logger->pushHandler(new StreamHandler('./requests.log'));
91
 * $client = Client::ofConfigAndLogger($config, $logger);
92
 * ```
93
 *
94
 * ### Using a cache adapter ###
95
 *
96
 * The client will automatically request an OAuth token and store the token in the provided cache.
97
 *
98
 * It's also possible to use a different cache adapter. The SDK provides a Doctrine, a Redis and an APCu cache adapter.
99
 * By default the SDK tries to instantiate the APCu or a PSR-6 filesystem cache adapter if there is no cache given.
100
 * E.g. Redis:
101
 *
102
 * ```php
103
 * $redis = new \Redis();
104
 * $redis->connect('localhost');
105
 * $client = Client::ofConfigAndCache($config, $redis);
106
 * ```
107
 *
108
 * #### Using cache and logger ####
109
 *
110
 * ```php
111
 * $client = Client::ofConfigCacheAndLogger($config, $cache, $logger);
112
 * ```
113
 *
114
 * #### Using a custom cache adapter ####
115
 *
116
 * ```php
117
 * class <CacheClass>Adapter implements \Commercetools\Core\Cache\CacheAdapterInterface {
118
 *     protected $cache;
119
 *     public function __construct(<CacheClass> $cache) {
120
 *         $this->cache = $cache;
121
 *     }
122
 * }
123
 *
124
 * $client->getAdapterFactory()->registerCallback(function ($cache) {
125
 *     if ($cache instanceof <CacheClass>) {
126
 *         return new <CacheClass>Adapter($cache);
127
 *     }
128
 *     return null;
129
 * });
130
 * ```
131
 *
132
 * @package Commercetools\Core
133
 */
134
class Client extends AbstractHttpClient implements LoggerAwareInterface
135
{
136
    const DEPRECATION_HEADER = 'X-DEPRECATION-NOTICE';
137
138
    /**
139
     * @sideeffect Test123
140
     * @var LoggerInterface
141
     */
142
    protected $logger;
143
144
    /**
145
     * @var Manager
146
     */
147
    protected $oauthManager;
148
149
    /**
150
     * @var ClientRequestInterface[]
151
     */
152
    protected $batchRequests = [];
153
154
    protected $tokenRefreshed = false;
155
156
    /**
157
     * @param array|Config $config
158
     * @param $cache
159
     * @param LoggerInterface $logger
160 63
     */
161
    public function __construct($config, $cache = null, LoggerInterface $logger = null)
162 63
    {
163
        parent::__construct($config);
164 63
165 63
        $manager = new Manager($config, $cache);
166 63
        $this->setOauthManager($manager);
167 63
        $this->setLogger($logger);
168
    }
169
170
    /**
171
     * @return Manager
172 35
     */
173
    public function getOauthManager()
174 35
    {
175
        return $this->oauthManager;
176
    }
177
178
    /**
179
     * @param Manager $oauthManager
180
     * @return $this
181 63
     */
182
    protected function setOauthManager(Manager $oauthManager)
183 63
    {
184 63
        $this->oauthManager = $oauthManager;
185
        return $this;
186
    }
187
188
    /**
189
     * @param LoggerInterface $logger
190
     * @return $this
191 63
     */
192
    public function setLogger(LoggerInterface $logger = null)
193 63
    {
194 40
        if ($logger instanceof LoggerInterface) {
195
            $this->logger = $logger;
196 63
        }
197
        return $this;
198
    }
199
200
    /**
201
     * @param array $options
202
     * @return AdapterInterface
203 140
     */
204
    public function getHttpClient($options = [])
205 140
    {
206 61
        if (is_null($this->httpClient)) {
207 61
            $clientOptions = $this->config->getClientOptions();
208 3
            if (count($clientOptions) > 0) {
209
                $options = array_merge($clientOptions, $options);
210 61
            }
211 61
            $client = parent::getHttpClient($options);
212 61
            if ($client instanceof TokenProviderAware) {
213
                $client->setOAuthTokenProvider($this->getOauthManager());
214 61
            }
215 40
            if ($this->logger instanceof LoggerInterface) {
0 ignored issues
show
introduced by
$this->logger is always a sub-type of Psr\Log\LoggerInterface.
Loading history...
216 40
                $client->setLogger(
217 40
                    $this->logger,
218 40
                    $this->getConfig()->getLogLevel(),
219
                    $this->getConfig()->getMessageFormatter()
220
                );
221 61
            }
222 61
            if ($this->getConfig()->getCorrelationIdProvider() instanceof CorrelationIdProvider
223
                && $client instanceof CorrelationIdAware
224 60
            ) {
225
                $client->setCorrelationIdProvider($this->getConfig()->getCorrelationIdProvider());
226 61
            }
227
            $this->httpClient = $client;
228
        }
229 140
230
        return $this->httpClient;
231
    }
232
233
234
    /**
235
     * @return string
236 62
     */
237
    protected function getBaseUrl()
238 62
    {
239
        return $this->getConfig()->getApiUrl() . '/' . $this->getConfig()->getProject() . '/';
240
    }
241
242
    /**
243
     * Executes an API request synchronously
244
     *
245
     * @param ClientRequestInterface $request
246
     * @param array $headers
247
     * @param array $clientOptions
248
     * @return ApiResponseInterface
249
     * @throws ApiException
250
     * @throws InvalidTokenException
251 126
     */
252
    public function execute(ClientRequestInterface $request, array $headers = null, array $clientOptions = [])
253 126
    {
254 123
        if ($request instanceof ContextAwareInterface) {
255
            $request->setContextIfNull($this->getConfig()->getContext());
256 126
        }
257
        $httpRequest = $this->createHttpRequest($request, $headers);
258
259 126
        try {
260 126
            $client = $this->getHttpClient();
261 126
            if ($client instanceof AdapterOptionInterface) {
262
                $httpResponse = $client->execute($httpRequest, $clientOptions);
263
            } else {
264
                $httpResponse = $client->execute($httpRequest);
265 110
            }
266 25
            $response = $request->buildResponse($httpResponse);
267 25
        } catch (ApiException $exception) {
268 4
            if ($exception instanceof InvalidTokenException && !$this->tokenRefreshed) {
269 4
                $this->tokenRefreshed = true;
270 3
                $this->getOauthManager()->refreshToken();
271
                return $this->execute($request);
272 22
            }
273 10
            if ($this->getConfig()->getThrowExceptions() || !$exception->getResponse() instanceof ResponseInterface) {
274
                throw $exception;
275 12
            }
276 12
            $httpResponse = $this->logExceptionResponse($exception);
277 12
            $response = new ErrorResponse($exception, $request, $httpResponse);
278
        }
279 116
        $this->logDeprecatedRequest($httpResponse, $httpRequest);
280
281
282 116
        return $response;
283
    }
284
285
    /**
286
     * Executes an API request asynchronously
287
     * @param ClientRequestInterface $request
288
     * @param array $clientOptions
289
     * @return ApiResponseInterface|ApiPromiseGetInterface
290
     */
291 4
    public function executeAsync(ClientRequestInterface $request, array $headers = null, array $clientOptions = [])
292
    {
293 4
        if ($request instanceof ContextAwareInterface) {
294 4
            $request->setContextIfNull($this->getConfig()->getContext());
295
        }
296 4
        $httpRequest = $this->createHttpRequest($request, $headers);
297 4
        $client = $this->getHttpClient();
298 4
        if ($client instanceof AdapterOptionInterface) {
299 4
            $response = $request->buildResponse($client->executeAsync($httpRequest, $clientOptions));
300
        } else {
301
            $response = $request->buildResponse($client->executeAsync($httpRequest));
302
        }
303
304 4
        $response = $response->then(
305
            function ($httpResponse) use ($httpRequest) {
306 4
                $this->logDeprecatedRequest($httpResponse, $httpRequest);
307 4
                return $httpResponse;
308 4
            }
309
        );
310
311 4
        return $response;
312
    }
313
314
    /**
315
     * @param ClientRequestInterface $request
316
     * @param array $headers
317
     * @return RequestInterface
318
     */
319 138
    protected function createHttpRequest(ClientRequestInterface $request, array $headers = null)
320
    {
321 138
        $httpRequest = $request->httpRequest();
322 138
        if (!$this->getHttpClient() instanceof TokenProviderAware) {
323
            $token = $this->getOauthManager()->getToken();
324
            $httpRequest = $httpRequest
325
                ->withHeader('Authorization', 'Bearer ' . $token->getToken());
326
        }
327 138
        if (is_array($headers)) {
328
            foreach ($headers as $headerName => $headerValues) {
329
                $httpRequest = $httpRequest
330
                    ->withAddedHeader($headerName, $headerValues);
331
            }
332
        }
333
334 138
        return $httpRequest;
335
    }
336
337
    /**
338
     * Executes API requests in batch
339
     * @param array $headers
340
     * @param array $clientOptions
341
     * @return ApiResponseInterface[]
342
     * @throws ApiException
343
     */
344 89
    public function executeBatch(array $headers = null, array $clientOptions = [])
345
    {
346 89
        $requests = $this->getBatchHttpRequests($headers);
347 89
        $client = $this->getHttpClient();
348 89
        if ($client instanceof AdapterOptionInterface) {
349 89
            $httpResponses = $client->executeBatch($requests, $clientOptions);
350
        } else {
351
            $httpResponses = $client->executeBatch($requests);
352
        }
353
354 89
        $responses = [];
355 89
        foreach ($httpResponses as $key => $httpResponse) {
356 89
            $request = $this->batchRequests[$key];
357 89
            $httpRequest = $requests[$key];
358 89
            if ($httpResponse instanceof ApiException) {
359 5
                $exception = $httpResponse;
360 5
                if ($this->getConfig()->getThrowExceptions() ||
361 5
                    !$httpResponse->getResponse() instanceof ResponseInterface
362
                ) {
363 1
                    throw $exception;
364
                }
365 4
                $httpResponse = $this->logExceptionResponse($httpResponse);
366 4
                $responses[$request->getIdentifier()] = new ErrorResponse(
367 4
                    $exception,
368 4
                    $request,
369
                    $httpResponse
370
                );
371
            } else {
372
                $responses[$request->getIdentifier()] = $request->buildResponse($httpResponse);
373 84
            }
374
            $this->logDeprecatedRequest($httpResponse, $httpRequest);
375 88
        }
376
        unset($this->batchRequests);
377 88
        $this->batchRequests = [];
378 88
379
        return $responses;
380 88
    }
381
382
    /**
383
     * @deprecated use logExceptionResponse instead
384
     * @param $exception
385
     * @return $this
386
     */
387 16
    protected function logException(ApiException $exception)
388
    {
389 16
        if (is_null($this->logger)) {
390 3
            return $this;
391
        }
392 13
393
        $response = $exception->getResponse();
394 13
395 13
        $context = [];
396
        if ($response instanceof ResponseInterface) {
0 ignored issues
show
introduced by
$response is always a sub-type of Psr\Http\Message\ResponseInterface.
Loading history...
397 13
            $context = [
398 13
                'responseStatusCode' => $response->getStatusCode(),
399 13
                'responseHeaders' => $response->getHeaders(),
400
                'responseBody' => (string)$response->getBody(),
401
            ];
402 13
        }
403
        $this->logger->error($exception->getMessage(), $context);
404 13
405
        return $this;
406
    }
407
408
    /**
409
     * logs the response of a message as context object without destroying the body stream
410
     * @param $exception
411
     * @return ResponseInterface
412 127
     */
413
    protected function logExceptionResponse(ApiException $exception)
414 127
    {
415 8
        if (is_null($this->logger)) {
416
            return $exception->getResponse();
417
        }
418 119
419 2
        $response = $exception->getResponse();
420 2
        $context = [];
421 2
        if ($response instanceof ResponseInterface) {
0 ignored issues
show
introduced by
$response is always a sub-type of Psr\Http\Message\ResponseInterface.
Loading history...
422 2
            $body = $response->getBody()->getContents();
423 2
            $response = $response->withBody(stream_for($body));
424
            $context = [
425 2
                'responseStatusCode' => $response->getStatusCode(),
426
                'responseHeaders' => $response->getHeaders(),
427 119
                'responseBody' => $body,
428
            ];
429
        }
430
        $this->logger->error($exception->getMessage(), $context);
431
432
        return $response;
433
    }
434
435
    /**
436
     * @param ResponseInterface $response
437
     * @param RequestInterface $request
438
     * @return $this
439
     */
440
    protected function logDeprecatedRequest(ResponseInterface $response, RequestInterface $request)
441
    {
442
        if (is_null($this->logger)) {
443
            return $this;
444
        }
445
446
        if ($response->hasHeader(static::DEPRECATION_HEADER)) {
447
            $message = sprintf(
448
                Message::DEPRECATED_METHOD,
449 89
                $request->getUri(),
450
                $request->getMethod(),
451 89
                $response->getHeaderLine(static::DEPRECATION_HEADER)
452
            );
453 89
            $this->logger->warning($message);
454 89
        }
455 89
        return $this;
456
    }
457
458 89
    /**
459
     * @param RequestInterface $request
460
     * @param ResponseInterface $response
461
     * @return string
462
     */
463
    protected function format(RequestInterface $request, ResponseInterface $response)
464
    {
465
        $entries = [
466 89
            $request->getMethod(),
467
            (string)$request->getUri(),
468 89
            $response->getStatusCode()
469 89
        ];
470
        return implode(', ', $entries);
471 89
    }
472 89
473
    /**
474
     * @param array $headers
475
     * @return array
476
     */
477
    protected function getBatchHttpRequests(array $headers = null)
478
    {
479
        $requests = array_map(
480 2
            function ($request) use ($headers) {
481
                return $this->createHttpRequest($request, $headers);
482 2
            },
483
            $this->batchRequests
484
        );
485
486
        return $requests;
487
    }
488
489
    /**
490
     * Adds a request to the batch execution queue
491
     * @param ClientRequestInterface $request
492
     * @return $this
493
     */
494
    public function addBatchRequest(ClientRequestInterface $request)
495
    {
496
        if ($request instanceof ContextAwareInterface) {
497
            $request->setContextIfNull($this->getConfig()->getContext());
498
        }
499
        $this->batchRequests[] = $request;
500
        return $this;
501
    }
502 2
503
    /**
504 2
     * Instantiates a client with the given config
505
     * @param Config $config
506
     * @return static
507
     */
508
    public static function ofConfig(Config $config)
509
    {
510
        return new static($config);
511
    }
512
513
    /**
514 30
     * Instantiates a client with the given config and cache adapter
515
     * @param Config $config
516 30
     * @param $cache
517
     * @return static
518
     */
519
    public static function ofConfigAndCache(Config $config, $cache)
520
    {
521
        return new static($config, $cache);
522
    }
523
524
    /**
525
     * Instantiates a client with the given config and a PSR-3 compliant logger
526
     * @param Config $config
527
     * @param LoggerInterface $logger
528
     * @return static
529
     */
530
    public static function ofConfigAndLogger(Config $config, LoggerInterface $logger)
531
    {
532
        return new static($config, null, $logger);
533
    }
534
535
    /**
536
     * Instantiates a client with the given config, a cache adapter and a PSR-3 compliant logger
537
     * @param Config $config
538
     * @param $cache
539
     * @param LoggerInterface $logger
540
     * @return static
541
     */
542
    public static function ofConfigCacheAndLogger(Config $config, $cache, LoggerInterface $logger)
543
    {
544
        return new static($config, $cache, $logger);
545
    }
546
}
547