Api::loadAll()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 4
dl 0
loc 7
rs 10
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
    /**
22
     * @var string
23
     */
24
    protected $domain;
25
26
    /**
27
     * @var string
28
     */
29
    protected $key;
30
31
    /**
32
     * @var LoggerInterface
33
     */
34
    protected $logger;
35
36
    /**
37
     * @var string
38
     */
39
    protected $password;
40
41
    /**
42
     * @var Pool
43
     */
44
    protected $pool;
45
46
    /**
47
     * @param string $domain
48
     * @param string $key
49
     * @param string $password
50
     * @param null|Pool $pool
51
     */
52
    public function __construct(string $domain, string $key, string $password, Pool $pool = null)
53
    {
54
        $this->domain = $domain;
55
        $this->key = $key;
56
        $this->password = $password;
57
        $this->pool = $pool ?? new Pool();
58
    }
59
60
    /**
61
     * @param string $class
62
     * @param array $query
63
     * @return Generator|mixed|AbstractEntity[]
64
     */
65
    public function advancedSearch(string $class, array $query)
66
    {
67
        $continue = !isset($query['limit']);
68
        $query['limit'] += ['limit' => 250];
69
        do {
70
            $remote = $this->get($class::TYPE . '/search', $query);
71
            foreach ($remote[$class::DIR] as $data) {
72
                yield $this->factory($this, $class, $data);
73
                $query['since_id'] = $data['id'];
74
            }
75
        } while ($continue and count($remote) == $query['limit']);
0 ignored issues
show
Bug introduced by
It seems like $remote can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

75
        } while ($continue and count(/** @scrutinizer ignore-type */ $remote) == $query['limit']);
Loading history...
76
    }
77
78
    /**
79
     * @param string $path
80
     * @param array $query
81
     */
82
    public function delete(string $path, array $query = []): void
83
    {
84
        $path .= '.json';
85
        if ($query) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $query of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
86
            $path .= '?' . http_build_query($query);
87
        }
88
        $this->exec('DELETE', $path);
89
    }
90
91
    /**
92
     * @param string $method
93
     * @param string $path
94
     * @param array $curlOpts
95
     * @return null|array
96
     */
97
    public function exec(string $method, string $path, array $curlOpts = [])
98
    {
99
        $this->getLogger()->log(LOG_DEBUG, "{$method} {$path}", $curlOpts);
100
        $ch = curl_init();
101
        curl_setopt_array($ch, [
102
            CURLOPT_CUSTOMREQUEST => $method,
103
            CURLOPT_URL => "https://{$this->key}:{$this->password}@{$this->domain}/admin/api/2020-04/{$path}",
104
            CURLOPT_FOLLOWLOCATION => true,
105
            CURLOPT_HEADER => true,
106
            CURLOPT_RETURNTRANSFER => true,
107
            CURLOPT_USERAGENT => 'hfw/shopify'
108
        ]);
109
        $curlOpts[CURLOPT_HTTPHEADER][] = 'Accept: application/json';
110
        $curlOpts[CURLOPT_HTTPHEADER][] = 'Expect:'; // prevent http 100
111
        curl_setopt_array($ch, $curlOpts);
112
        RETRY:
113
        $res = explode("\r\n\r\n", curl_exec($ch), 2);
0 ignored issues
show
Bug introduced by
It seems like curl_exec($ch) can also be of type true; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

113
        $res = explode("\r\n\r\n", /** @scrutinizer ignore-type */ curl_exec($ch), 2);
Loading history...
114
        $info = curl_getinfo($ch);
115
        switch ($info['http_code']) {
116
            case 0:
117
                throw new ShopifyError(curl_errno($ch), curl_error($ch), $info);
118
            case 200:
119
            case 201:
120
            case 202:
121
                return json_decode($res[1], true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR);
122
            case 404:
123
                return null;
124
            case 429:
125
                preg_match('/^Retry-After:\h*(\d+)/im', $res[0], $retry);
126
                $this->getLogger()->log(LOG_DEBUG, $retry[0]);
127
                sleep($retry[1]);
128
                goto RETRY;
129
            default:
130
                $error = new ShopifyError($info['http_code'], $res[1], $info);
131
                $this->getLogger()->log(LOG_ERR, "Shopify {$info['http_code']}: {$error->getMessage()}");
132
                throw $error;
133
        }
134
    }
135
136
    /**
137
     * @param Api|Data $caller
138
     * @param string $class
139
     * @param array $data
140
     * @return mixed
141
     */
142
    public function factory($caller, string $class, array $data = [])
143
    {
144
        return new $class($caller, $data);
145
    }
146
147
    /**
148
     * @param Api|Data $caller
149
     * @param string $class
150
     * @param array[] $list
151
     * @return array
152
     */
153
    public function factoryAll($caller, string $class, array $list)
154
    {
155
        return array_map(function (array $each) use ($caller, $class) {
156
            return $this->factory($caller, $class, $each);
157
        }, $list);
158
    }
159
160
    /**
161
     * @param string $path
162
     * @param array $query
163
     * @return null|array
164
     */
165
    public function get(string $path, array $query = [])
166
    {
167
        $path .= '.json';
168
        if ($query) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $query of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
169
            $path .= '?' . http_build_query($query);
170
        }
171
        return $this->exec('GET', $path);
172
    }
173
174
    /**
175
     * @param string $id
176
     * @return null|Location
177
     */
178
    public function getLocation(string $id)
179
    {
180
        return $this->load($this, Location::class, "locations/{$id}");
181
    }
182
183
    /**
184
     * @return LoggerInterface
185
     */
186
    public function getLogger(): LoggerInterface
187
    {
188
        return $this->logger ?? $this->logger = new NullLogger();
189
    }
190
191
    /**
192
     * @return User
193
     */
194
    public function getMe()
195
    {
196
        return $this->load($this, User::class, 'users/current');
197
    }
198
199
    /**
200
     * @return Pool
201
     */
202
    public function getPool()
203
    {
204
        return $this->pool;
205
    }
206
207
    /**
208
     * @return Shop
209
     */
210
    public function getShop()
211
    {
212
        return $this->load($this, Shop::class, 'shop');
213
    }
214
215
    /**
216
     * @param Api|Data $caller
217
     * @param string $class
218
     * @param string $path
219
     * @param array $query
220
     * @return null|mixed|AbstractEntity
221
     */
222
    public function load($caller, string $class, string $path, array $query = [])
223
    {
224
        return $this->pool->get($path, $caller, function ($caller) use ($class, $path, $query) {
225
            if ($remote = $this->get($path, $query)) {
226
                return $this->factory($caller, $class, $remote[$class::TYPE]);
227
            }
228
            return null;
229
        });
230
    }
231
232
    /**
233
     * @param Api|Data $caller
234
     * @param string $class
235
     * @param string $path
236
     * @param array $query
237
     * @return array|Data[]
238
     */
239
    public function loadAll($caller, string $class, string $path, array $query = [])
240
    {
241
        return array_map(function (array $each) use ($caller, $class) {
242
            return $this->pool->get($each['id'], $caller, function ($caller) use ($class, $each) {
243
                return $this->factory($caller, $class, $each);
244
            });
245
        }, $this->get($path, $query)[$class::DIR] ?? []);
246
    }
247
248
    /**
249
     * @param string $path
250
     * @param array $data
251
     * @return null|array
252
     */
253
    public function post(string $path, array $data = [])
254
    {
255
        return $this->exec('POST', "{$path}.json", [
256
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
257
            CURLOPT_POSTFIELDS => json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)
258
        ]);
259
    }
260
261
    /**
262
     * @param string $path
263
     * @param array $data
264
     * @return null|array
265
     */
266
    public function put(string $path, array $data = [])
267
    {
268
        return $this->exec('PUT', "{$path}.json", [
269
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
270
            CURLOPT_POSTFIELDS => json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)
271
        ]);
272
    }
273
274
    /**
275
     * @param LoggerInterface $logger
276
     * @return $this
277
     */
278
    final public function setLogger(LoggerInterface $logger)
279
    {
280
        $this->logger = $logger;
281
        return $this;
282
    }
283
284
}