Completed
Push — master ( 1e83e1...1479a4 )
by Joachim
13:24
created

Api::__get()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.1755

Importance

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