Client::executeBatch()   B
last analyzed

Complexity

Conditions 6
Paths 8

Size

Total Lines 36
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6.0029

Importance

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