Client::buildUrl()   A
last analyzed

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