Passed
Push — develop ( 38ffd9...9f7101 )
by Jens
31:57 queued 05:22
created

Client::createHttpRequest()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5.4042

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 16
ccs 5
cts 9
cp 0.5556
rs 9.9332
c 0
b 0
f 0
cc 4
nc 4
nop 2
crap 5.4042
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
     */
161 63
    public function __construct($config, $cache = null, LoggerInterface $logger = null)
162
    {
163 63
        parent::__construct($config);
164
165 63
        $manager = new Manager($config, $cache);
166 63
        $this->setOauthManager($manager);
167 63
        $this->setLogger($logger);
168 63
    }
169
170
    /**
171
     * @return Manager
172
     */
173 35
    public function getOauthManager()
174
    {
175 35
        return $this->oauthManager;
176
    }
177
178
    /**
179
     * @param Manager $oauthManager
180
     * @return $this
181
     */
182 63
    protected function setOauthManager(Manager $oauthManager)
183
    {
184 63
        $this->oauthManager = $oauthManager;
185 63
        return $this;
186
    }
187
188
    /**
189
     * @param LoggerInterface $logger
190
     * @return $this
191
     */
192 63
    public function setLogger(LoggerInterface $logger = null)
193
    {
194 63
        if ($logger instanceof LoggerInterface) {
195 40
            $this->logger = $logger;
196
        }
197 63
        return $this;
198
    }
199
200
    /**
201
     * @param array $options
202
     * @return AdapterInterface
203
     */
204 140
    public function getHttpClient($options = [])
205
    {
206 140
        if (is_null($this->httpClient)) {
207 61
            $clientOptions = $this->config->getClientOptions();
208 61
            if (count($clientOptions) > 0) {
209 3
                $options = array_merge($clientOptions, $options);
210
            }
211 61
            $client = parent::getHttpClient($options);
212 61
            if ($client instanceof TokenProviderAware) {
213 61
                $client->setOAuthTokenProvider($this->getOauthManager());
214
            }
215 61
            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 40
                    $this->getConfig()->getMessageFormatter()
220
                );
221
            }
222 61
            if ($this->getConfig()->getCorrelationIdProvider() instanceof CorrelationIdProvider
223 61
                && $client instanceof CorrelationIdAware
224
            ) {
225 60
                $client->setCorrelationIdProvider($this->getConfig()->getCorrelationIdProvider());
226
            }
227 61
            $this->httpClient = $client;
228
        }
229
230 140
        return $this->httpClient;
231
    }
232
233
234
    /**
235
     * @return string
236
     */
237 62
    protected function getBaseUrl()
238
    {
239 62
        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
     */
252 126
    public function execute(ClientRequestInterface $request, array $headers = null, array $clientOptions = [])
253
    {
254 126
        if ($request instanceof ContextAwareInterface) {
255 123
            $request->setContextIfNull($this->getConfig()->getContext());
256
        }
257 126
        $httpRequest = $this->createHttpRequest($request, $headers);
258
259
        try {
260 126
            $client = $this->getHttpClient();
261 126
            if ($client instanceof AdapterOptionInterface) {
262 126
                $httpResponse = $client->execute($httpRequest, $clientOptions);
263
            } else {
264
                $httpResponse = $client->execute($httpRequest);
265
            }
266 110
            $response = $request->buildResponse($httpResponse);
267 25
        } catch (ApiException $exception) {
268 25
            if ($exception instanceof InvalidTokenException && !$this->tokenRefreshed) {
269 4
                $this->tokenRefreshed = true;
270 4
                $this->getOauthManager()->refreshToken();
271 3
                return $this->execute($request);
272
            }
273 22
            if ($this->getConfig()->getThrowExceptions() || !$exception->getResponse() instanceof ResponseInterface) {
274 10
                throw $exception;
275
            }
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
                    $request,
369
                    $httpResponse
370
                );
371
            } else {
372 84
                $responses[$request->getIdentifier()] = $request->buildResponse($httpResponse);
373
            }
374 88
            $this->logDeprecatedRequest($httpResponse, $httpRequest);
375
        }
376 88
        unset($this->batchRequests);
377 88
        $this->batchRequests = [];
378
379 88
        return $responses;
380
    }
381
382
    /**
383
     * @deprecated use logExceptionResponse instead
384
     * @param $exception
385
     * @return $this
386
     */
387
    protected function logException(ApiException $exception)
388
    {
389
        if (is_null($this->logger)) {
390
            return $this;
391
        }
392
393
        $response = $exception->getResponse();
394
395
        $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
            $context = [
398
                'responseStatusCode' => $response->getStatusCode(),
399
                'responseHeaders' => $response->getHeaders(),
400
                'responseBody' => (string)$response->getBody(),
401
            ];
402
        }
403
        $this->logger->error($exception->getMessage(), $context);
404
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
     */
413 16
    protected function logExceptionResponse(ApiException $exception)
414
    {
415 16
        if (is_null($this->logger)) {
416 3
            return $exception->getResponse();
417
        }
418
419 13
        $response = $exception->getResponse();
420 13
        $context = [];
421 13
        if ($response instanceof ResponseInterface) {
0 ignored issues
show
introduced by
$response is always a sub-type of Psr\Http\Message\ResponseInterface.
Loading history...
422 13
            $body = $response->getBody()->getContents();
423 13
            $response = $response->withBody(stream_for($body));
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Psr7\stream_for() has been deprecated: stream_for will be removed in guzzlehttp/psr7:2.0. Use Utils::streamFor instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

423
            $response = $response->withBody(/** @scrutinizer ignore-deprecated */ stream_for($body));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
424
            $context = [
425 13
                'responseStatusCode' => $response->getStatusCode(),
426 13
                'responseHeaders' => $response->getHeaders(),
427 13
                'responseBody' => $body,
428
            ];
429
        }
430 13
        $this->logger->error($exception->getMessage(), $context);
431
432 13
        return $response;
433
    }
434
435
    /**
436
     * @param ResponseInterface $response
437
     * @param RequestInterface $request
438
     * @return $this
439
     */
440 127
    protected function logDeprecatedRequest(ResponseInterface $response, RequestInterface $request)
441
    {
442 127
        if (is_null($this->logger)) {
443 8
            return $this;
444
        }
445
446 119
        if ($response->hasHeader(static::DEPRECATION_HEADER)) {
447 2
            $message = sprintf(
448 2
                Message::DEPRECATED_METHOD,
449 2
                $request->getUri(),
450 2
                $request->getMethod(),
451 2
                $response->getHeaderLine(static::DEPRECATION_HEADER)
452
            );
453 2
            $this->logger->warning($message);
454
        }
455 119
        return $this;
456
    }
457
458
    /**
459
     * @param RequestInterface $request
460
     * @param ResponseInterface $response
461
     * @return string
462
     */
463
    protected function format(RequestInterface $request, ResponseInterface $response)
464
    {
465
        $entries = [
466
            $request->getMethod(),
467
            (string)$request->getUri(),
468
            $response->getStatusCode()
469
        ];
470
        return implode(', ', $entries);
471
    }
472
473
    /**
474
     * @param array $headers
475
     * @return array
476
     */
477 89
    protected function getBatchHttpRequests(array $headers = null)
478
    {
479 89
        $requests = array_map(
480
            function ($request) use ($headers) {
481 89
                return $this->createHttpRequest($request, $headers);
482 89
            },
483 89
            $this->batchRequests
484
        );
485
486 89
        return $requests;
487
    }
488
489
    /**
490
     * Adds a request to the batch execution queue
491
     * @param ClientRequestInterface $request
492
     * @return $this
493
     */
494 89
    public function addBatchRequest(ClientRequestInterface $request)
495
    {
496 89
        if ($request instanceof ContextAwareInterface) {
497 89
            $request->setContextIfNull($this->getConfig()->getContext());
498
        }
499 89
        $this->batchRequests[] = $request;
500 89
        return $this;
501
    }
502
503
    /**
504
     * Instantiates a client with the given config
505
     * @param Config $config
506
     * @return static
507
     */
508 2
    public static function ofConfig(Config $config)
509
    {
510 2
        return new static($config);
511
    }
512
513
    /**
514
     * Instantiates a client with the given config and cache adapter
515
     * @param Config $config
516
     * @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 2
    public static function ofConfigAndLogger(Config $config, LoggerInterface $logger)
531
    {
532 2
        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 30
    public static function ofConfigCacheAndLogger(Config $config, $cache, LoggerInterface $logger)
543
    {
544 30
        return new static($config, $cache, $logger);
545
    }
546
}
547