Api::resolveOptions()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 9.7998
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
namespace Loevgaard\Dandomain\Api;
3
4
use Assert\Assert;
5
use GuzzleHttp\Client;
6
use GuzzleHttp\ClientInterface;
7
use GuzzleHttp\Exception\GuzzleException;
8
use GuzzleHttp\Psr7\Response;
9
use GuzzleHttp\RequestOptions;
10
use Psr\Http\Message\ResponseInterface;
11
use Symfony\Component\OptionsResolver\OptionsResolver;
12
13
/**
14
 * @property Endpoint\Customer $customer
15
 * @property Endpoint\Discount $discount
16
 * @property Endpoint\Order $order
17
 * @property Endpoint\Product $product
18
 * @property Endpoint\ProductData $productData
19
 * @property Endpoint\ProductTag $productTag
20
 * @property Endpoint\RelatedData $relatedData
21
 * @property Endpoint\Settings $settings
22
 * @property Endpoint\Plugin $plugin
23
 */
24
class Api
25
{
26
    /**
27
     * Example: http://www.example.com
28
     *
29
     * @var string
30
     */
31
    protected $host;
32
33
    /**
34
     * The API key from your Dandomain admin
35
     *
36
     * @var string
37
     */
38
    protected $apiKey;
39
40
    /**
41
     * This is the HTTP client
42
     *
43
     * @var ClientInterface
44
     */
45
    protected $client;
46
47
    /**
48
     * This is the last response
49
     *
50
     * @var Response
51
     */
52
    protected $response;
53
54
    /**
55
     * These are the options used for the next request
56
     * After that request, these options are reset
57
     *
58
     * @var array
59
     */
60
    protected $options;
61
62
    /**
63
     * These are default options used for every request
64
     *
65
     * @var array
66
     */
67
    protected $defaultOptions;
68
69
    /**
70
     * These are the resolved options used in the last request
71
     *
72
     * @var array
73
     */
74
    protected $lastOptions;
75
76
    /*
77
     * These are endpoints in the Dandomain API
78
     */
79
    /**
80
     * @var Endpoint\Customer
81
     */
82
    protected $customer;
83
84
    /**
85
     * @var Endpoint\Discount
86
     */
87
    protected $discount;
88
89
    /**
90
     * @var Endpoint\Order;
91
     */
92
    protected $order;
93
94
    /**
95
     * @var Endpoint\Product;
96
     */
97
    protected $product;
98
99
    /**
100
     * @var Endpoint\ProductData;
101
     */
102
    protected $productData;
103
104
    /**
105
     * @var Endpoint\ProductTag;
106
     */
107
    protected $productTag;
108
109
    /**
110
     * @var Endpoint\RelatedData;
111
     */
112
    protected $relatedData;
113
114
    /**
115
     * @var Endpoint\Settings;
116
     */
117
    protected $settings;
118
    
119
    /**
120
     * @var Endpoint\Plugin;
121
     */
122
    protected $plugin;
123
124 4
    public function __construct(string $host, string $apiKey, array $defaultOptions = [])
125
    {
126 4
        $host = rtrim($host, '/');
127
128 4
        Assert::that($host)->url('`$host` is not a valid URL');
129 4
        Assert::that($apiKey)->length(36, '`$apiKey` is not a valid api key. It must be 36 characters');
130
131 4
        $this->host = $host;
132 4
        $this->apiKey = $apiKey;
133 4
        $this->options = [];
134 4
        $this->defaultOptions = $defaultOptions;
135 4
    }
136
137
    /**
138
     * This ensures lazy loading of the endpoint classes
139
     *
140
     * @param string $name
141
     * @return Endpoint\Endpoint
142
     */
143 4
    public function __get($name)
144
    {
145 4
        if (!property_exists(self::class, $name)) {
146
            throw new \InvalidArgumentException('The property `'.$name.'` does not exist on `'.self::class.'`');
147
        }
148
149 4
        if (!$this->{$name}) {
150 4
            $className = 'Loevgaard\\Dandomain\\Api\\Endpoint\\'.ucfirst($name);
151
152 4
            if (!class_exists($className)) {
153
                throw new \InvalidArgumentException('Class `'.$className.'` does not exist or could not be autoloaded');
154
            }
155
156 4
            $this->{$name} = new $className($this);
157
        }
158
159 4
        return $this->{$name};
160
    }
161
162
    /**
163
     * Will always return a JSON result contrary to Dandomains API
164
     * Errors are formatted as described here: http://jsonapi.org/format/#errors
165
     *
166
     * @param string $method
167
     * @param string $uri
168
     * @param array|\stdClass $body The body is sent as JSON
169
     * @param array $options
170
     * @return mixed
171
     */
172 2
    public function doRequest(string $method, string $uri, $body = null, array $options = [])
173
    {
174 2
        $parsedResponse = [];
175
176
        try {
177
            // merge all options
178
            // the priority is
179
            // 1. options supplied by the user
180
            // 2. options supplied by the method calling
181
            // 3. the default options
182 2
            $options = $this->resolveOptions($this->defaultOptions, $options, $this->options);
183
184 2
            if (!empty($body)) {
185
                $body = $this->objectToArray($body);
186
                Assert::that($body)->notEmpty('The body of the request cannot be empty');
187
188
                // the body will always override any other data sent
189
                $options['json'] = $body;
190
            }
191
192
            // save the resolved options
193 2
            $this->lastOptions = $options;
194
195
            // replace the {KEY} placeholder with the api key
196 2
            $url = $this->host . str_replace('{KEY}', $this->apiKey, $uri);
197
198
            // do request
199 2
            $this->response = $this->getClient()->request($method, $url, $options);
200
201
            // parse response and create error object if the json decode throws an exception
202
            try {
203 2
                $parsedResponse = \GuzzleHttp\json_decode((string)$this->response->getBody(), true);
204
            } catch (\InvalidArgumentException $e) {
205
                $parsedResponse['errors'][] = [
206
                    'status' => $this->response->getStatusCode(),
207
                    'title' => 'JSON parse error',
208
                    'detail' => $e->getMessage()
209
                ];
210
            }
211
212 2
            $statusCode = $this->response->getStatusCode();
213 2
            if ($statusCode > 299 || $statusCode < 200) {
214
                if (!is_array($parsedResponse)) {
215
                    $parsedResponse = [];
216
                }
217
                $parsedResponse['errors'] = [];
218
                $parsedResponse['errors'][] = [
219
                    'status' => $this->response->getStatusCode(),
220 2
                    'detail' => 'See Api::$response for details'
221
                ];
222
            }
223
        } catch (GuzzleException $e) {
224
            $parsedResponse['errors'] = [];
225
            $parsedResponse['errors'][] = [
226
                'title' => 'Unexpected error',
227
                'detail' => $e->getMessage()
228
            ];
229 2
        } finally {
230
            // reset request options
231 2
            $this->options = [];
232
        }
233
234 2
        return $parsedResponse;
235
    }
236
237
    /**
238
     * @return ClientInterface
239
     */
240 2
    public function getClient() : ClientInterface
241
    {
242 2
        if (!$this->client) {
243
            $this->client = new Client();
244
        }
245
246 2
        return $this->client;
247
    }
248
249
    /**
250
     * @param ClientInterface $client
251
     * @return Api
252
     */
253 2
    public function setClient(ClientInterface $client) : Api
254
    {
255 2
        $this->client = $client;
256 2
        return $this;
257
    }
258
259
    /**
260
     * Returns the latest response
261
     *
262
     * @return ResponseInterface
263
     */
264
    public function getResponse() : ResponseInterface
265
    {
266
        return $this->response;
267
    }
268
269
    /**
270
     * Sets request options for the next request
271
     *
272
     * @param array $options
273
     * @return Api
274
     */
275
    public function setOptions(array $options) : Api
276
    {
277
        $this->options = $options;
278
        return $this;
279
    }
280
281
    /**
282
     * Sets default request options
283
     *
284
     * @param array $defaultOptions
285
     * @return Api
286
     */
287 1
    public function setDefaultOptions(array $defaultOptions) : Api
288
    {
289 1
        $this->defaultOptions = $defaultOptions;
290 1
        return $this;
291
    }
292
293
    /**
294
     * @return array
295
     */
296 1
    public function getLastOptions(): array
297
    {
298 1
        return $this->lastOptions;
299
    }
300
301
    /**
302
     * Helper method to convert a \stdClass into an array
303
     *
304
     * @param $obj
305
     * @return array
306
     */
307
    protected function objectToArray($obj) : array
308
    {
309
        if ($obj instanceof \stdClass) {
310
            $obj = json_decode(json_encode($obj), true);
311
        }
312
313
        return (array)$obj;
314
    }
315
316 2
    protected function configureOptions(OptionsResolver $resolver)
317
    {
318 2
        $refl = new \ReflectionClass(RequestOptions::class);
319
320 2
        $resolver->setDefined(array_values($refl->getConstants()));
0 ignored issues
show
Documentation introduced by
array_values($refl->getConstants()) is of type array<integer,integer|double|string|boolean>, but the function expects a string|array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
321
322 2
        $resolver->setDefaults([
323 2
            RequestOptions::HEADERS => [
324
                'Accept' => 'application/json',
325
            ],
326 2
            RequestOptions::CONNECT_TIMEOUT => 15,
327 2
            RequestOptions::TIMEOUT => 60,
328 2
            RequestOptions::HTTP_ERRORS => false
329
        ]);
330 2
    }
331
332 2
    protected function resolveOptions(array ...$options) : array
333
    {
334 2
        $computedOptions = [];
335
336 2
        foreach ($options as $arr) {
337 2
            $computedOptions = array_replace_recursive($computedOptions, $arr);
338
        }
339
340 2
        $resolver = new OptionsResolver();
341 2
        $this->configureOptions($resolver);
342 2
        $options = $resolver->resolve($computedOptions);
343
344 2
        return $options;
345
    }
346
}
347