Passed
Push — master ( 199ece...6bc7da )
by y
02:13
created

Api::post()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 4
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
     * @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) {
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...
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, [
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt_array() does only seem to accept resource, 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

96
        curl_setopt_array(/** @scrutinizer ignore-type */ $ch, [
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, 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

108
        $res = explode("\r\n\r\n", curl_exec(/** @scrutinizer ignore-type */ $ch), 2);
Loading history...
109
        $info = curl_getinfo($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_getinfo() does only seem to accept resource, 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

109
        $info = curl_getinfo(/** @scrutinizer ignore-type */ $ch);
Loading history...
110
        switch ($info['http_code']) {
111
            case 0:
112
                throw new ShopifyError(curl_errno($ch), curl_error($ch), $info);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_errno() does only seem to accept resource, 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

112
                throw new ShopifyError(curl_errno(/** @scrutinizer ignore-type */ $ch), curl_error($ch), $info);
Loading history...
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_error() does only seem to accept resource, 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

112
                throw new ShopifyError(curl_errno($ch), curl_error(/** @scrutinizer ignore-type */ $ch), $info);
Loading history...
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) {
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...
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
}