Completed
Push — master ( 23b0b8...d25f9a )
by Joachim
12:29
created

Api::getLastOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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