1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace TomHart\Restful; |
4
|
|
|
|
5
|
|
|
use GuzzleHttp\Client; |
6
|
|
|
use Illuminate\Database\Query\Grammars\Grammar; |
7
|
|
|
use Illuminate\Support\Collection; |
8
|
|
|
use InvalidArgumentException; |
9
|
|
|
use Psr\Http\Message\MessageInterface; |
10
|
|
|
use Symfony\Component\HttpFoundation\Response; |
11
|
|
|
use Symfony\Component\Routing\Exception\RouteNotFoundException; |
12
|
|
|
use TomHart\Restful\Concerns\Restful; |
13
|
|
|
use TomHart\Restful\Concerns\Transformer; |
14
|
|
|
use TomHart\Restful\Routing\Route; |
15
|
|
|
|
16
|
|
|
use function GuzzleHttp\json_decode as guzzle_json_decode; |
17
|
|
|
|
18
|
|
|
class Builder |
19
|
|
|
{ |
20
|
|
|
/** |
21
|
|
|
* @var Restful |
22
|
|
|
*/ |
23
|
|
|
protected $model; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var Client |
27
|
|
|
*/ |
28
|
|
|
protected $client; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var Grammar |
32
|
|
|
*/ |
33
|
|
|
protected $grammar; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var mixed[] |
37
|
|
|
*/ |
38
|
|
|
protected $wheres = []; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var string[] |
42
|
|
|
*/ |
43
|
|
|
private $order; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var int |
47
|
|
|
*/ |
48
|
|
|
private $limit; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var int[] |
52
|
|
|
*/ |
53
|
|
|
private $goodStatusCodes = [ |
54
|
|
|
Response::HTTP_OK, |
55
|
|
|
Response::HTTP_CREATED, |
56
|
|
|
Response::HTTP_ACCEPTED |
57
|
|
|
]; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Builder constructor. |
61
|
|
|
* @param Client $client |
62
|
|
|
* @param Restful $model |
63
|
|
|
*/ |
64
|
|
|
public function __construct(Client $client, Restful $model) |
65
|
|
|
{ |
66
|
|
|
$this->model = $model; |
67
|
|
|
$this->client = $client; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* What model are we querying? |
72
|
|
|
* @param string $class |
73
|
|
|
* @return Builder |
74
|
|
|
*/ |
75
|
|
|
public static function model(string $class) |
76
|
|
|
{ |
77
|
|
|
$class = new $class(); |
78
|
|
|
return app(static::class, ['model' => $class]); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Get the index route for the model via the options route. |
83
|
|
|
* @param string $route |
84
|
|
|
* @param null $id |
85
|
|
|
* @return Route |
86
|
|
|
* @throws Exceptions\UndefinedIndexException |
87
|
|
|
* @throws RouteNotFoundException |
88
|
|
|
*/ |
89
|
|
|
private function getModelRoute(string $route, $id = null): Route |
90
|
|
|
{ |
91
|
|
|
$optionsUrl = $this->model->getOptionsUrl(); |
92
|
|
|
$optionRoute = new Route('OPTIONS', ['absolute' => $optionsUrl]); |
93
|
|
|
$links = $this->getResponse($optionRoute, [], ['id' => $id]); |
94
|
|
|
if (!$links) { |
95
|
|
|
throw new RouteNotFoundException('Cannot get options from route'); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
$links = $this->parseJsonFromResponse($links); |
99
|
|
|
if (!isset($links[$route])) { |
100
|
|
|
throw new RouteNotFoundException("Cannot find link to $route route"); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
return Route::fromArray($links[$route]); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Get the JSON from the given URL. |
108
|
|
|
* @param Route $route |
109
|
|
|
* @param array $queryString |
110
|
|
|
* @param array $postData |
111
|
|
|
* @return MessageInterface |
112
|
|
|
* @throws Exceptions\UndefinedIndexException |
113
|
|
|
*/ |
114
|
|
|
private function getResponse(Route $route, array $queryString = [], array $postData = []): ?MessageInterface |
115
|
|
|
{ |
116
|
|
|
return $this->client->request( |
117
|
|
|
$route->getMethod(), |
118
|
|
|
$route->getHrefs('absolute', $queryString), |
|
|
|
|
119
|
|
|
[ |
120
|
|
|
'headers' => config('restful.headers'), |
121
|
|
|
'form_params' => $postData |
122
|
|
|
] |
123
|
|
|
); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Extract JSON from a Response. |
128
|
|
|
* @param MessageInterface $response |
129
|
|
|
* @return mixed |
130
|
|
|
* @throws Exceptions\UndefinedIndexException |
131
|
|
|
*/ |
132
|
|
|
private function parseJsonFromResponse(MessageInterface $response) |
133
|
|
|
{ |
134
|
|
|
$body = $response->getBody()->getContents(); |
135
|
|
|
if (!$body) { |
136
|
|
|
return null; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
$json = guzzle_json_decode($body, true); |
140
|
|
|
|
141
|
|
|
// If the response is paginated, and there is a next page, get that and add the results. |
142
|
|
|
if ($this->isPaginatedResponse($json) && |
143
|
|
|
isset($json['next_page_url'], $json['current_page'], $json['last_page']) && |
144
|
|
|
$json['current_page'] < $json['last_page'] |
145
|
|
|
) { |
146
|
|
|
$nextPageResponse = $this->getResponse(Route::fromUrl($json['next_page_url'], 'GET')); |
147
|
|
|
if ($nextPageResponse) { |
148
|
|
|
$nextPageJson = $this->parseJsonFromResponse($nextPageResponse); |
149
|
|
|
|
150
|
|
|
$json['data'] = array_merge($json['data'], $nextPageJson['data']); |
151
|
|
|
|
152
|
|
|
return $json; |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
return $json; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Is the JSON responses a paginatable one? |
161
|
|
|
* @param mixed[] $json |
162
|
|
|
* @return bool |
163
|
|
|
*/ |
164
|
|
|
private function isPaginatedResponse(array $json): bool |
165
|
|
|
{ |
166
|
|
|
return empty(array_diff(config('restful.pagination_json_keys'), array_keys($json))); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Extract data from the JSON |
171
|
|
|
* @param $json |
172
|
|
|
* @return array |
173
|
|
|
* @throws InvalidArgumentException |
174
|
|
|
*/ |
175
|
|
|
private function extractDataFromJson($json): array |
176
|
|
|
{ |
177
|
|
|
if (!isset($json['data'])) { |
178
|
|
|
throw new InvalidArgumentException('$json doesn\'t contain a data key'); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
return $json['data']; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Add a where clause. |
186
|
|
|
* @param string $column |
187
|
|
|
* @param $value |
188
|
|
|
* @return Builder |
189
|
|
|
*/ |
190
|
|
|
public function where(string $column, $value): self |
191
|
|
|
{ |
192
|
|
|
$this->wheres[] = [ |
193
|
|
|
'column' => $column, |
194
|
|
|
'type' => 'Basic', |
195
|
|
|
'value' => $value |
196
|
|
|
]; |
197
|
|
|
|
198
|
|
|
return $this; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Add a whereIn clause. |
203
|
|
|
* @param string $column |
204
|
|
|
* @param mixed[] $values |
205
|
|
|
* @return Builder |
206
|
|
|
*/ |
207
|
|
|
public function whereIn(string $column, array $values): self |
208
|
|
|
{ |
209
|
|
|
$this->wheres[] = [ |
210
|
|
|
'column' => $column, |
211
|
|
|
'type' => 'In', |
212
|
|
|
'values' => $values |
213
|
|
|
]; |
214
|
|
|
|
215
|
|
|
return $this; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Return all the wheres |
220
|
|
|
* @return mixed[] |
221
|
|
|
*/ |
222
|
|
|
public function getWheres(): array |
223
|
|
|
{ |
224
|
|
|
return $this->wheres; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Set an order |
229
|
|
|
* @param string $column |
230
|
|
|
* @param string $direction |
231
|
|
|
* @return $this\ |
|
|
|
|
232
|
|
|
*/ |
233
|
|
|
public function order(string $column, string $direction): self |
234
|
|
|
{ |
235
|
|
|
$this->order = [ |
|
|
|
|
236
|
|
|
'column' => $column, |
237
|
|
|
'direction' => $direction |
238
|
|
|
]; |
239
|
|
|
|
240
|
|
|
return $this; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Get the order |
245
|
|
|
* @return array|null |
246
|
|
|
*/ |
247
|
|
|
public function getOrder(): ?array |
248
|
|
|
{ |
249
|
|
|
return $this->order; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Specify a limit |
254
|
|
|
* @param int $limit |
255
|
|
|
* @return $this |
256
|
|
|
*/ |
257
|
|
|
public function limit(int $limit): self |
258
|
|
|
{ |
259
|
|
|
$this->limit = $limit; |
260
|
|
|
|
261
|
|
|
return $this; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Get the limit |
266
|
|
|
* @return int|null |
267
|
|
|
*/ |
268
|
|
|
public function getLimit(): ?int |
269
|
|
|
{ |
270
|
|
|
return $this->limit; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Call the API, and return the models. |
275
|
|
|
* @return Collection |
276
|
|
|
* @throws Exceptions\UndefinedIndexException |
277
|
|
|
*/ |
278
|
|
|
public function get(): Collection |
279
|
|
|
{ |
280
|
|
|
$options = $this->getModelRoute('index'); |
281
|
|
|
|
282
|
|
|
/** @var Transformer $transformer */ |
283
|
|
|
$transformer = app(Transformer::class); |
284
|
|
|
|
285
|
|
|
$queryString = $transformer->buildQueryString($this); |
286
|
|
|
|
287
|
|
|
$response = $this->getResponse($options, $queryString); |
288
|
|
|
if (!$response) { |
289
|
|
|
return collect(); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
$json = $this->parseJsonFromResponse($response); |
293
|
|
|
if (!$json) { |
294
|
|
|
return collect(); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
$data = $this->extractDataFromJson($json); |
298
|
|
|
|
299
|
|
|
$models = $this->model->hydrate($data)->all(); |
|
|
|
|
300
|
|
|
|
301
|
|
|
return collect($models); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* @param string $route |
306
|
|
|
* @param mixed[] $data |
307
|
|
|
* @param mixed $id |
308
|
|
|
* @return bool |
309
|
|
|
* @throws Exceptions\UndefinedIndexException |
310
|
|
|
*/ |
311
|
|
|
private function _call(string $route, array $data, $id = null): bool |
312
|
|
|
{ |
313
|
|
|
$route = $this->getModelRoute($route, $id); |
314
|
|
|
$response = $this->getResponse($route, [], $data); |
315
|
|
|
if (!$response) { |
316
|
|
|
return false; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
return in_array($response->getStatusCode(), $this->goodStatusCodes, true); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* @param mixed[] $data |
324
|
|
|
* @return bool |
325
|
|
|
* @throws Exceptions\UndefinedIndexException |
326
|
|
|
*/ |
327
|
|
|
public function insert(array $data): bool |
328
|
|
|
{ |
329
|
|
|
return $this->_call('store', $data); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* @param mixed $id |
334
|
|
|
* @param mixed[] $data |
335
|
|
|
* @return bool |
336
|
|
|
* @throws Exceptions\UndefinedIndexException |
337
|
|
|
*/ |
338
|
|
|
public function update($id, array $data): bool |
339
|
|
|
{ |
340
|
|
|
return $this->_call('update', $data, $id); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* @param mixed $id |
345
|
|
|
* @return bool |
346
|
|
|
* @throws Exceptions\UndefinedIndexException |
347
|
|
|
*/ |
348
|
|
|
public function delete($id): bool |
349
|
|
|
{ |
350
|
|
|
return $this->_call('destroy', [], $id); |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
|
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.