1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Helix\Shopify; |
4
|
|
|
|
5
|
|
|
use Generator; |
6
|
|
|
use Helix\Shopify\Api\Pool; |
7
|
|
|
use Helix\Shopify\Api\ShopifyError; |
8
|
|
|
use Helix\Shopify\Base\AbstractEntity; |
9
|
|
|
use Helix\Shopify\Base\Data; |
10
|
|
|
use Psr\Log\LoggerInterface; |
11
|
|
|
use Psr\Log\NullLogger; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* API access. |
15
|
|
|
* |
16
|
|
|
* @see https://shopify.dev/docs/admin-api/rest/reference |
17
|
|
|
*/ |
18
|
|
|
class Api { |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var string |
22
|
|
|
*/ |
23
|
|
|
protected $domain; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var string |
27
|
|
|
*/ |
28
|
|
|
protected $key; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var LoggerInterface |
32
|
|
|
*/ |
33
|
|
|
protected $logger; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
protected $password; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var Pool |
42
|
|
|
*/ |
43
|
|
|
protected $pool; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @param string $domain |
47
|
|
|
* @param string $key |
48
|
|
|
* @param string $password |
49
|
|
|
* @param null|Pool $pool |
50
|
|
|
*/ |
51
|
|
|
public function __construct (string $domain, string $key, string $password, Pool $pool = null) { |
52
|
|
|
$this->domain = $domain; |
53
|
|
|
$this->key = $key; |
54
|
|
|
$this->password = $password; |
55
|
|
|
$this->pool = $pool ?? new Pool(); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @param string $class |
60
|
|
|
* @param array $query |
61
|
|
|
* @return Generator|mixed|AbstractEntity[] |
62
|
|
|
*/ |
63
|
|
|
public function advancedSearch (string $class, array $query) { |
64
|
|
|
$continue = !isset($query['limit']); |
65
|
|
|
$query['limit'] += ['limit' => 250]; |
66
|
|
|
do { |
67
|
|
|
$remote = $this->get($class::TYPE . '/search', $query); |
68
|
|
|
foreach ($remote[$class::DIR] as $data) { |
69
|
|
|
yield $this->factory($this, $class, $data); |
70
|
|
|
$query['since_id'] = $data['id']; |
71
|
|
|
} |
72
|
|
|
} while ($continue and count($remote) == $query['limit']); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @param string $path |
77
|
|
|
* @param array $query |
78
|
|
|
*/ |
79
|
|
|
public function delete (string $path, array $query = []): void { |
80
|
|
|
$path .= '.json'; |
81
|
|
|
if ($query) { |
|
|
|
|
82
|
|
|
$path .= '?' . http_build_query($query); |
83
|
|
|
} |
84
|
|
|
$this->exec('DELETE', $path); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* @param string $method |
89
|
|
|
* @param string $path |
90
|
|
|
* @param array $curlOpts |
91
|
|
|
* @return null|array |
92
|
|
|
*/ |
93
|
|
|
public function exec (string $method, string $path, array $curlOpts = []) { |
94
|
|
|
$this->getLogger()->log(LOG_DEBUG, "{$method} {$path}", $curlOpts); |
95
|
|
|
$ch = curl_init(); |
96
|
|
|
curl_setopt_array($ch, [ |
|
|
|
|
97
|
|
|
CURLOPT_CUSTOMREQUEST => $method, |
98
|
|
|
CURLOPT_URL => "https://{$this->key}:{$this->password}@{$this->domain}/admin/api/2020-04/{$path}", |
99
|
|
|
CURLOPT_FOLLOWLOCATION => true, |
100
|
|
|
CURLOPT_HEADER => true, |
101
|
|
|
CURLOPT_RETURNTRANSFER => true, |
102
|
|
|
CURLOPT_USERAGENT => 'hfw/shopify' |
103
|
|
|
]); |
104
|
|
|
$curlOpts[CURLOPT_HTTPHEADER][] = 'Accept: application/json'; |
105
|
|
|
$curlOpts[CURLOPT_HTTPHEADER][] = 'Expect:'; // prevent http 100 |
106
|
|
|
curl_setopt_array($ch, $curlOpts); |
107
|
|
|
RETRY: |
108
|
|
|
$res = explode("\r\n\r\n", curl_exec($ch), 2); |
|
|
|
|
109
|
|
|
$info = curl_getinfo($ch); |
|
|
|
|
110
|
|
|
switch ($info['http_code']) { |
111
|
|
|
case 0: |
112
|
|
|
throw new ShopifyError(curl_errno($ch), curl_error($ch), $info); |
|
|
|
|
113
|
|
|
case 200: |
114
|
|
|
case 201: |
115
|
|
|
case 202: |
116
|
|
|
return json_decode($res[1], true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); |
117
|
|
|
case 404: |
118
|
|
|
return null; |
119
|
|
|
case 429: |
120
|
|
|
preg_match('/^Retry-After:\h*(\d+)/im', $res[0], $retry); |
121
|
|
|
$this->getLogger()->log(LOG_DEBUG, $retry[0]); |
122
|
|
|
sleep($retry[1]); |
123
|
|
|
goto RETRY; |
124
|
|
|
default: |
125
|
|
|
$error = new ShopifyError($info['http_code'], $res[1], $info); |
126
|
|
|
$this->getLogger()->log(LOG_ERR, "Shopify {$info['http_code']}: {$error->getMessage()}"); |
127
|
|
|
throw $error; |
128
|
|
|
} |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @param Api|Data $caller |
133
|
|
|
* @param string $class |
134
|
|
|
* @param array $data |
135
|
|
|
* @return mixed |
136
|
|
|
*/ |
137
|
|
|
public function factory ($caller, string $class, array $data = []) { |
138
|
|
|
return new $class($caller, $data); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* @param Api|Data $caller |
143
|
|
|
* @param string $class |
144
|
|
|
* @param array[] $list |
145
|
|
|
* @return array |
146
|
|
|
*/ |
147
|
|
|
public function factoryAll ($caller, string $class, array $list) { |
148
|
|
|
return array_map(function(array $each) use ($caller, $class) { |
149
|
|
|
return $this->factory($caller, $class, $each); |
150
|
|
|
}, $list); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @param string $path |
155
|
|
|
* @param array $query |
156
|
|
|
* @return null|array |
157
|
|
|
*/ |
158
|
|
|
public function get (string $path, array $query = []) { |
159
|
|
|
$path .= '.json'; |
160
|
|
|
if ($query) { |
|
|
|
|
161
|
|
|
$path .= '?' . http_build_query($query); |
162
|
|
|
} |
163
|
|
|
return $this->exec('GET', $path); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* @param string $id |
168
|
|
|
* @return null|Location |
169
|
|
|
*/ |
170
|
|
|
public function getLocation (string $id) { |
171
|
|
|
return $this->load($this, Location::class, "locations/{$id}"); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* @return LoggerInterface |
176
|
|
|
*/ |
177
|
|
|
public function getLogger (): LoggerInterface { |
178
|
|
|
return $this->logger ?? $this->logger = new NullLogger(); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* @return User |
183
|
|
|
*/ |
184
|
|
|
public function getMe () { |
185
|
|
|
return $this->load($this, User::class, 'users/current'); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* @return Pool |
190
|
|
|
*/ |
191
|
|
|
public function getPool () { |
192
|
|
|
return $this->pool; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* @return Shop |
197
|
|
|
*/ |
198
|
|
|
public function getShop () { |
199
|
|
|
return $this->load($this, Shop::class, 'shop'); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* @param Api|Data $caller |
204
|
|
|
* @param string $class |
205
|
|
|
* @param string $path |
206
|
|
|
* @param array $query |
207
|
|
|
* @return null|mixed|AbstractEntity |
208
|
|
|
*/ |
209
|
|
|
public function load ($caller, string $class, string $path, array $query = []) { |
210
|
|
|
return $this->pool->get($path, $caller, function($caller) use ($class, $path, $query) { |
211
|
|
|
if ($remote = $this->get($path, $query)) { |
212
|
|
|
return $this->factory($caller, $class, $remote[$class::TYPE]); |
213
|
|
|
} |
214
|
|
|
return null; |
215
|
|
|
}); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* @param Api|Data $caller |
220
|
|
|
* @param string $class |
221
|
|
|
* @param string $path |
222
|
|
|
* @param array $query |
223
|
|
|
* @return array|Data[] |
224
|
|
|
*/ |
225
|
|
|
public function loadAll ($caller, string $class, string $path, array $query = []) { |
226
|
|
|
return array_map(function(array $each) use ($caller, $class) { |
227
|
|
|
return $this->pool->get($each['id'], $caller, function($caller) use ($class, $each) { |
228
|
|
|
return $this->factory($caller, $class, $each); |
229
|
|
|
}); |
230
|
|
|
}, $this->get($path, $query)[$class::DIR] ?? []); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* @param string $path |
235
|
|
|
* @param array $data |
236
|
|
|
* @return null|array |
237
|
|
|
*/ |
238
|
|
|
public function post (string $path, array $data = []) { |
239
|
|
|
return $this->exec('POST', "{$path}.json", [ |
240
|
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'], |
241
|
|
|
CURLOPT_POSTFIELDS => json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) |
242
|
|
|
]); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* @param string $path |
247
|
|
|
* @param array $data |
248
|
|
|
* @return null|array |
249
|
|
|
*/ |
250
|
|
|
public function put (string $path, array $data = []) { |
251
|
|
|
return $this->exec('PUT', "{$path}.json", [ |
252
|
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'], |
253
|
|
|
CURLOPT_POSTFIELDS => json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) |
254
|
|
|
]); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* @param LoggerInterface $logger |
259
|
|
|
* @return $this |
260
|
|
|
*/ |
261
|
|
|
final public function setLogger (LoggerInterface $logger) { |
262
|
|
|
$this->logger = $logger; |
263
|
|
|
return $this; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
} |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.