Completed
Push — master ( 93b826...c2eec1 )
by Taosikai
11:32
created

Client::getLastResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/*
4
 * This file is part of the slince/shopify-api-php
5
 *
6
 * (c) Slince <[email protected]>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Slince\Shopify;
13
14
use Doctrine\Common\Inflector\Inflector;
15
use GuzzleHttp\Exception\GuzzleException;
16
use GuzzleHttp\Psr7\Request;
17
use Slince\Di\Container;
18
use GuzzleHttp\Client as HttpClient;
19
use Psr\Http\Message\RequestInterface;
20
use Psr\Http\Message\ResponseInterface;
21
use Slince\Shopify\Common\Manager\ManagerInterface;
22
use Slince\Shopify\Exception\InvalidArgumentException;
23
use GuzzleHttp\Exception\RequestException;
24
use Slince\Shopify\Exception\ClientException;
25
use Slince\Shopify\Exception\RuntimeException;
26
use Slince\Shopify\Hydrator\Hydrator;
27
28
/**
29
 * @method Manager\Article\ArticleManagerInterface getArticleManager
30
 * @method Manager\Asset\AssetManagerInterface getAssetManager
31
 * @method Manager\Blog\BlogManagerInterface getBlogManager
32
 * @method Manager\CarrierService\CarrierServiceManagerInterface getCarrierServiceManager
33
 * @method Manager\Collect\CollectManagerInterface getCollectManager
34
 * @method Manager\Comment\CommentManagerInterface getCommentManager
35
 * @method Manager\Country\CountryManagerInterface getCountryManager
36
 * @method Manager\CustomCollection\CustomCollectionManagerInterface getCustomCollectionManager
37
 * @method Manager\Customer\CustomerManagerInterface getCustomerManager
38
 * @method Manager\CustomerAddress\AddressManagerInterface getCustomerAddressManager
39
 * @method Manager\CustomerSavedSearch\CustomerSavedSearchManagerInterface getCustomerSavedSearchManager
40
 * @method Manager\DiscountCode\DiscountCodeManagerInterface getDiscountCodeManager
41
 * @method Manager\Fulfillment\FulfillmentManagerInterface getFulfillmentManager
42
 * @method Manager\FulfillmentService\FulfillmentServiceManagerInterface getFulfillmentServiceManager
43
 * @method Manager\InventoryItem\InventoryItemManagerInterface getInventoryItemManager
44
 * @method Manager\InventoryLevel\InventoryLevelManagerInterface getInventoryLevelManager
45
 * @method Manager\Location\LocationManagerInterface getLocationManager
46
 * @method Manager\Order\OrderManagerInterface getOrderManager
47
 * @method Manager\OrderRisk\RiskManagerInterface getOrderRiskManager
48
 * @method Manager\Page\PageManagerInterface getPageManager
49
 * @method Manager\Policy\PolicyManagerInterface getPolicyManager
50
 * @method Manager\PriceRule\PriceRuleManagerInterface getPriceRuleManager
51
 * @method Manager\Product\ProductManagerInterface getProductManager
52
 * @method Manager\ProductImage\ImageManagerInterface getProductImageManager
53
 * @method Manager\ProductVariant\VariantManagerInterface getProductVariantManager
54
 * @method Manager\Province\ProvinceManagerInterface getProvinceManager
55
 * @method Manager\RecurringApplicationCharge\RecurringApplicationChargeManagerInterface getRecurringApplicationChargeManager
56
 * @method Manager\Redirect\RedirectManagerInterface getRedirectManager
57
 * @method Manager\Refund\RefundManagerInterface getRefundManager
58
 * @method Manager\ShippingZone\ShippingZoneManagerInterface getShippingZoneManager
59
 * @method Manager\Shop\ShopManagerInterface getShopManager
60
 * @method Manager\SmartCollection\SmartCollectionManager getSmartCollectionManager
61
 * @method Manager\Theme\ThemeManagerInterface getThemeManager
62
 * @method Manager\Transaction\TransactionManagerInterface getTransactionManager
63
 * @method Manager\Webhook\WebhookManagerInterface getWebhookManager
64
 */
65
class Client
66
{
67
    const NAME = 'SlinceShopifyClient';
68
    const VERSION = '2.3.0';
69
70
    /**
71
     * @var HttpClient
72
     */
73
    protected $httpClient;
74
75
    /**
76
     * @var Container
77
     */
78
    protected $container;
79
80
    /**
81
     * @var CredentialInterface
82
     */
83
    protected $credential;
84
85
    /**
86
     * The shop.
87
     *
88
     * @var string
89
     */
90
    protected $shop;
91
92
    /**
93
     * @var string
94
     */
95
    protected $apiVersion = '2019-10';
96
97
    /**
98
     * @var ResponseInterface
99
     */
100
    protected $lastResponse;
101
102
    /**
103
     * Array of services classes.
104
     *
105
     * @var array
106
     */
107
    public $serviceClass = [
108
        Manager\Article\ArticleManager::class,
109
        Manager\Asset\AssetManager::class,
110
        Manager\Blog\BlogManager::class,
111
        Manager\CarrierService\CarrierServiceManager::class,
112
        Manager\Collect\CollectManager::class,
113
        Manager\Comment\CommentManager::class,
114
        Manager\Country\CountryManager::class,
115
        Manager\CustomCollection\CustomCollectionManager::class,
116
        Manager\Customer\CustomerManager::class,
117
        Manager\CustomerAddress\AddressManager::class,
118
        Manager\CustomerSavedSearch\CustomerSavedSearchManager::class,
119
        Manager\DiscountCode\DiscountCodeManager::class,
120
        Manager\Fulfillment\FulfillmentManager::class,
121
        Manager\FulfillmentService\FulfillmentServiceManager::class,
122
        Manager\InventoryItem\InventoryItemManager::class,
123
        Manager\InventoryLevel\InventoryLevelManager::class,
124
        Manager\Location\LocationManager::class,
125
        Manager\Order\OrderManager::class,
126
        Manager\OrderRisk\RiskManager::class,
127
        Manager\Page\PageManager::class,
128
        Manager\Policy\PolicyManager::class,
129
        Manager\PriceRule\PriceRuleManager::class,
130
        Manager\Product\ProductManager::class,
131
        Manager\ProductImage\ImageManager::class,
132
        Manager\ProductVariant\VariantManager::class,
133
        Manager\Province\ProvinceManager::class,
134
        Manager\RecurringApplicationCharge\RecurringApplicationChargeManager::class,
135
        Manager\Redirect\RedirectManager::class,
136
        Manager\Refund\RefundManager::class,
137
        Manager\ScriptTag\ScriptTagManager::class,
138
        Manager\ShippingZone\ShippingZoneManager::class,
139
        Manager\Shop\ShopManager::class,
140
        Manager\SmartCollection\SmartCollectionManager::class,
141
        Manager\Theme\ThemeManager::class,
142
        Manager\Transaction\TransactionManager::class,
143
        Manager\Webhook\WebhookManager::class,
144
    ];
145
146
    protected $metaDirs = [
147
        'Slince\Shopify' => __DIR__.'/../config/serializer'
148
    ];
149
150
    /**
151
     * Whether delay the next request.
152
     *
153
     * @var bool
154
     */
155
    protected static $delayNextRequest = false;
156
157
    /**
158
     * @var string
159
     */
160
    protected $metaCacheDir;
161
162
    /**
163
     * @var Hydrator
164
     */
165
    protected $hydrator;
166
167
    public function __construct(CredentialInterface $credential, $shop, array $options = [])
168
    {
169
        $this->container = new Container();
170
        $this->container->register($this);
171
        $this->credential = $credential;
172
        $this->setShop($shop);
173
        $this->applyOptions($options);
174
        $this->initializeBaseServices();
175
    }
176
177
    public function __call($name, $arguments)
178
    {
179
        if ('Manager' === substr($name, -7)) {
180
            $serviceName = substr($name, 3, -7);
181
182
            return $this->container->get(Inflector::tableize(Inflector::pluralize($serviceName)));
183
        }
184
        throw new \InvalidArgumentException(sprintf('The method "%s" is not exists', $name));
185
    }
186
187
    /**
188
     * Gets the credential.
189
     *
190
     * @return CredentialInterface
191
     */
192
    public function getCredential()
193
    {
194
        return $this->credential;
195
    }
196
197
    /**
198
     * sets the shop name for the client.
199
     *
200
     * @param string $shop
201
     */
202
    public function setShop($shop)
203
    {
204
        if (!preg_match('/^[a-zA-Z0-9\-]{3,100}\.myshopify\.(?:com|io)$/', $shop)) {
205
            throw new InvalidArgumentException(
206
                'Shop name should be 3-100 letters, numbers, or hyphens e.g. your-store.myshopify.com'
207
            );
208
        }
209
        $this->shop = $shop;
210
    }
211
212
    /**
213
     * Gets the shop.
214
     *
215
     * @return string
216
     */
217
    public function getShop()
218
    {
219
        return $this->shop;
220
    }
221
222
    /**
223
     * Sets the http client for the client.
224
     *
225
     * @param HttpClient $httpClient
226
     */
227
    public function setHttpClient($httpClient)
228
    {
229
        $this->httpClient = $httpClient;
230
    }
231
232
    /**
233
     * Gets the http client.
234
     *
235
     * @return HttpClient
236
     */
237
    public function getHttpClient()
238
    {
239
        if ($this->httpClient) {
240
            return $this->httpClient;
241
        }
242
        return $this->httpClient = new HttpClient([
243
            'verify' => false,
244
        ]);
245
    }
246
247
    /**
248
     * Perform a GET request.
249
     *
250
     * @param string $resource
251
     * @param array  $query
252
     *
253
     * @return array
254
     */
255
    public function get($resource, $query = [])
256
    {
257
        return $this->doRequest('GET', $resource, [
258
            'query' => $query,
259
        ]);
260
    }
261
262
    /**
263
     * Perform a POST request.
264
     *
265
     * @param string $resource
266
     * @param array  $data
267
     * @param array  $query
268
     *
269
     * @return array
270
     */
271
    public function post($resource, $data, $query = [])
272
    {
273
        return $this->doRequest('POST', $resource, [
274
            'query' => $query,
275
            'json' => $data,
276
        ]);
277
    }
278
279
    /**
280
     * Perform a PUT request.
281
     *
282
     * @param string $resource
283
     * @param array  $data
284
     * @param array  $query
285
     *
286
     * @return array
287
     */
288
    public function put($resource, $data, $query = [])
289
    {
290
        return $this->doRequest('PUT', $resource, [
291
            'query' => $query,
292
            'json' => $data,
293
        ]);
294
    }
295
296
    /**
297
     * Perform a DELETE request.
298
     *
299
     * @param string $resource
300
     * @param array  $query
301
     */
302
    public function delete($resource, $query = [])
303
    {
304
        $this->doRequest('DELETE', $resource, [
305
            'query' => $query
306
        ]);
307
    }
308
309
    /**
310
     * Send an HTTP request
311
     *
312
     * @param string $method
313
     * @param string $resource
314
     * @param array $options
315
     * @return array
316
     */
317
    protected function doRequest($method, $resource, $options = [])
318
    {
319
        $request = new Request($method, $this->buildUrl($resource), [
320
            'Content-Type' => 'application/json',
321
        ]);
322
        $response = $this->sendRequest($request, $options);
323
        $body = $response->getBody();
324
325
        return $body->getSize()
326
            ? \GuzzleHttp\json_decode($body, true)
327
            : [];
328
    }
329
330
    /**
331
     * Send a request.
332
     *
333
     * @param RequestInterface $request
334
     * @param array $options
335
     *
336
     * @return ResponseInterface
337
     * @throws GuzzleException
338
     * @codeCoverageIgnore
339
     */
340
    public function sendRequest(RequestInterface $request, array $options = [])
341
    {
342
        if (static::$delayNextRequest) {
343
            usleep(1000000 * rand(3, 10));
344
        }
345
        $request = $request->withHeader('User-Agent', static::NAME . '/' . static::VERSION);
346
        $request = $this->credential->applyToRequest($request);
347
        try {
348
            $response = $this->getHttpClient()->send($request, $options);
349
            $this->lastResponse = $response;
350
        } catch (RequestException $exception) {
351
            $exception = new ClientException($request, $exception->getResponse(), $exception->getMessage());
352
            throw $exception;
353
        }
354
        list($callsMade, $callsLimit) = explode('/', $response->getHeaderLine('http_x_shopify_shop_api_call_limit'));
355
        static::$delayNextRequest = $callsMade / $callsLimit >= 0.8;
356
        return $response;
357
    }
358
359
    /**
360
     * Gets the latest http response.
361
     *
362
     * @return ResponseInterface
363
     */
364
    public function getLastResponse()
365
    {
366
        return $this->lastResponse;
367
    }
368
369
    /**
370
     * Builds an url by given resource name.
371
     *
372
     * @param string $resource
373
     *
374
     * @return string
375
     */
376
    protected function buildUrl($resource)
377
    {
378
        return sprintf('https://%s/admin/api/%s/%s.json', $this->shop, $this->apiVersion, $resource);
379
    }
380
381
    /**
382
     * Applies the array of request options to the client.
383
     *
384
     * @param array $options
385
     */
386
    protected function applyOptions(array $options)
387
    {
388
        isset($options['httpClient']) && $this->httpClient = $options['httpClient'];
389
        if (!isset($options['metaCacheDir'])) {
390
            throw new InvalidArgumentException('You must provide option "metaCacheDir"');
391
        }
392
        $this->metaCacheDir = $options['metaCacheDir'];
393
        if (isset($options['apiVersion'])) {
394
            if (!preg_match('/^([0-9]{4}-[0-9]{2})|unstable%/', $options['apiVersion'])) {
395
                throw new InvalidArgumentException('Version string must be of YYYY-MM or unstable');
396
            }
397
            $this->apiVersion = $options['apiVersion'];
398
        }
399
    }
400
401
    /**
402
     * Gets the hydrator instance.
403
     *
404
     * @return Hydrator
405
     */
406
    public function getHydrator()
407
    {
408
        if ($this->hydrator) {
409
            return $this->hydrator;
410
        }
411
        return $this->hydrator = new Hydrator($this->metaCacheDir, $this->metaDirs);
412
    }
413
414
    /**
415
     * Add a custom meta dir.
416
     *
417
     * @param string $namespace
418
     * @param string $path
419
     * @throws RuntimeException
420
     */
421
    public function addMetaDir($namespace, $path)
422
    {
423
        if ($this->hydrator) {
424
            throw new RuntimeException(sprintf('The hydrator has been built, you should add meta dir before getting manager.'));
425
        }
426
        $this->metaDirs[$namespace] = $path;
427
    }
428
429
    /**
430
     * Add a custom service class.
431
     *
432
     * @param string $serviceClass
433
     * @throws InvalidArgumentException
434
     */
435
    public function addServiceClass($serviceClass)
436
    {
437
        if (!is_subclass_of($serviceClass, ManagerInterface::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Slince\Shopify\Common\M...ManagerInterface::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
438
            throw new InvalidArgumentException(sprintf('The service class "%s" should implement "ManagerInterface"', $serviceClass));
439
        }
440
        $this->serviceClass[] = $serviceClass;
441
        $this->container->register($serviceClass::getServiceName(), $serviceClass);
442
    }
443
444
    /**
445
     * Initialize base services.
446
     */
447
    protected function initializeBaseServices()
448
    {
449
        foreach ($this->serviceClass as $serviceClass) {
450
            $this->container->register($serviceClass::getServiceName(), $serviceClass);
451
        }
452
    }
453
}
454