Passed
Push — master ( 53eead...218820 )
by Tom
03:04
created

Builder::getLimit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace TomHart\Restful;
4
5
use GuzzleHttp\Client;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Query\Grammars\Grammar;
8
use Illuminate\Support\Collection;
9
use InvalidArgumentException;
10
use Psr\Http\Message\MessageInterface;
11
use Psr\Log\LoggerInterface;
12
use Symfony\Component\HttpFoundation\Response;
13
use Symfony\Component\Routing\Exception\RouteNotFoundException;
14
use TomHart\Restful\Concerns\Restful;
15
use TomHart\Restful\Concerns\Transformer;
16
use TomHart\Restful\Routing\Route;
17
18
use function GuzzleHttp\json_decode as guzzle_json_decode;
19
20
class Builder
21
{
22
    /**
23
     * @var Restful
24
     */
25
    protected $model;
26
27
    /**
28
     * @var Client
29
     */
30
    protected $client;
31
32
    /**
33
     * @var Grammar
34
     */
35
    protected $grammar;
36
37
    /**
38
     * @var mixed[]
39
     */
40
    protected $wheres = [];
41
42
    /**
43
     * @var string[]
44
     */
45
    private $order;
46
47
    /**
48
     * @var int
49
     */
50
    private $limit;
51
52
    /**
53
     * @var int[]
54
     */
55
    private $goodStatusCodes = [
56
        Response::HTTP_OK,
57
        Response::HTTP_CREATED,
58
        Response::HTTP_ACCEPTED
59
    ];
60
    /**
61
     * @var LoggerInterface
62
     */
63
    private $logger;
64
65
    /**
66
     * Builder constructor.
67
     * @param Client $client
68
     * @param LoggerInterface $logger
69
     * @param Restful $model
70
     */
71
    public function __construct(Client $client, LoggerInterface $logger, Restful $model)
72
    {
73
        $this->model = $model;
74
        $this->client = $client;
75
        $this->logger = $logger;
76
    }
77
78
    /**
79
     * What model are we querying?
80
     * @param string $class
81
     * @return Builder
82
     */
83
    public static function model(string $class)
84
    {
85
        $class = new $class();
86
        return app(static::class, ['model' => $class]);
87
    }
88
89
    private function newModelInstance(): Model
90
    {
91
        return new $this->model();
92
    }
93
94
    /**
95
     * Get the index route for the model via the options route.
96
     * @param string $route
97
     * @param null $id
98
     * @return Route
99
     * @throws Exceptions\UndefinedIndexException
100
     * @throws RouteNotFoundException
101
     */
102
    private function getModelRoute(string $route, $id = null): Route
103
    {
104
        $optionRoute = $this->model->getOptionsRoute();
105
        $links = $this->getResponse($optionRoute, ['id' => $id]);
106
        if (!$links) {
107
            throw new RouteNotFoundException('Cannot get options from route');
108
        }
109
110
        $links = $this->parseJsonFromResponse($links);
111
        if (!isset($links[$route])) {
112
            throw new RouteNotFoundException("Cannot find link to $route route");
113
        }
114
115
        return Route::fromArray($links[$route]);
116
    }
117
118
    /**
119
     * Get the JSON from the given URL.
120
     * @param Route $route
121
     * @param array $queryString
122
     * @param array $postData
123
     * @return MessageInterface
124
     * @throws Exceptions\UndefinedIndexException
125
     */
126
    private function getResponse(Route $route, array $queryString = [], array $postData = []): ?MessageInterface
127
    {
128
        if ($domain = config('restful.api_domain')) {
129
            $url = $domain . $route->getHrefs('relative', $queryString);
130
        } else {
131
            $url = $route->getHrefs('absolute', $queryString);
132
        }
133
134
        if ($this->shouldLog()) {
135
            $this->logger->info(
136
                sprintf('REST-CALL: %s to %s', $route->getMethod(), $url)
137
            );
138
        }
139
140
        return $this->client->request(
141
            $route->getMethod(),
142
            $url,
0 ignored issues
show
Bug introduced by
It seems like $url defined by $route->getHrefs('absolute', $queryString) on line 131 can also be of type array<integer,string>; however, GuzzleHttp\Client::request() does only seem to accept string|object<Psr\Http\Message\UriInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
143
            [
144
                'headers' => config('restful.headers'),
145
                'form_params' => $postData
146
            ]
147
        );
148
    }
149
150
    /**
151
     * Extract JSON from a Response.
152
     * @param MessageInterface $response
153
     * @return mixed
154
     * @throws Exceptions\UndefinedIndexException
155
     */
156
    private function parseJsonFromResponse(MessageInterface $response)
157
    {
158
        $body = $response->getBody()->getContents();
159
        if (!$body) {
160
            return null;
161
        }
162
163
        $json = guzzle_json_decode($body, true);
164
165
        // If the response is paginated, and there is a next page, get that and add the results.
166
        if ($this->isPaginatedResponse($json) &&
167
            isset($json['next_page_url'], $json['current_page'], $json['last_page']) &&
168
            $json['current_page'] < $json['last_page']
169
        ) {
170
            $nextPageResponse = $this->getResponse(Route::fromUrl($json['next_page_url'], 'GET'));
171
            if ($nextPageResponse) {
172
                $nextPageJson = $this->parseJsonFromResponse($nextPageResponse);
173
174
                $json['data'] = array_merge($json['data'], $nextPageJson['data']);
175
176
                return $json;
177
            }
178
        }
179
180
        return $json;
181
    }
182
183
    /**
184
     * Is the JSON responses a paginatable one?
185
     * @param mixed[] $json
186
     * @return bool
187
     */
188
    private function isPaginatedResponse(array $json): bool
189
    {
190
        return empty(array_diff(config('restful.pagination_json_keys'), array_keys($json)));
191
    }
192
193
    /**
194
     * Extract data from the JSON
195
     * @param $json
196
     * @return array
197
     * @throws InvalidArgumentException
198
     */
199
    private function extractDataFromJson($json): array
200
    {
201
        if (!isset($json['data'])) {
202
            throw new InvalidArgumentException('$json doesn\'t contain a data key');
203
        }
204
205
        return $json['data'];
206
    }
207
208
    /**
209
     * Add a where clause.
210
     * @param string $column
211
     * @param $value
212
     * @return Builder
213
     */
214
    public function where(string $column, $value): self
215
    {
216
        $this->wheres[] = [
217
            'column' => $column,
218
            'type' => 'Basic',
219
            'value' => $value
220
        ];
221
222
        return $this;
223
    }
224
225
    /**
226
     * Add a whereIn clause.
227
     * @param string $column
228
     * @param mixed[] $values
229
     * @return Builder
230
     */
231
    public function whereIn(string $column, array $values): self
232
    {
233
        $this->wheres[] = [
234
            'column' => $column,
235
            'type' => 'In',
236
            'values' => $values
237
        ];
238
239
        return $this;
240
    }
241
242
    /**
243
     * Return all the wheres
244
     * @return mixed[]
245
     */
246
    public function getWheres(): array
247
    {
248
        return $this->wheres;
249
    }
250
251
    /**
252
     * Set an order
253
     * @param string $column
254
     * @param string $direction
255
     * @return $this\
0 ignored issues
show
Documentation introduced by
The doc-type $this\ could not be parsed: Unknown type name "$this\" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
256
     */
257
    public function order(string $column, string $direction): self
258
    {
259
        $this->order = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('column' => $colum...rection' => $direction) of type array<string,string,{"co...,"direction":"string"}> is incompatible with the declared type array<integer,string> of property $order.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
260
            'column' => $column,
261
            'direction' => $direction
262
        ];
263
264
        return $this;
265
    }
266
267
    /**
268
     * Get the order
269
     * @return array|null
270
     */
271
    public function getOrder(): ?array
272
    {
273
        return $this->order;
274
    }
275
276
    /**
277
     * Specify a limit
278
     * @param int $limit
279
     * @return $this
280
     */
281
    public function limit(int $limit): self
282
    {
283
        $this->limit = $limit;
284
285
        return $this;
286
    }
287
288
    /**
289
     * Get the limit
290
     * @return int|null
291
     */
292
    public function getLimit(): ?int
293
    {
294
        return $this->limit;
295
    }
296
297
    /**
298
     * Call the API, and return the models.
299
     * @return Collection
300
     * @throws Exceptions\UndefinedIndexException
301
     */
302
    public function get(): Collection
303
    {
304
        $options = $this->getModelRoute('index');
305
306
        /** @var Transformer $transformer */
307
        $transformer = app(Transformer::class);
308
309
        $queryString = $transformer->buildQueryString($this);
310
311
        $response = $this->getResponse($options, $queryString);
312
        if (!$response) {
313
            return collect();
314
        }
315
316
        $json = $this->parseJsonFromResponse($response);
317
        if (!$json) {
318
            return collect();
319
        }
320
321
        $data = $this->extractDataFromJson($json);
322
323
        $instance = $this->newModelInstance();
324
325
        return $instance->newCollection(
326
            array_map(
327
                function ($item) use ($instance) {
328
                    return $instance->newFromBuilder($item);
329
                },
330
                $data
331
            )
332
        );
333
    }
334
335
    /**
336
     * @param string $route
337
     * @param mixed[] $data
338
     * @param mixed $id
339
     * @return bool
340
     * @throws Exceptions\UndefinedIndexException
341
     */
342
    private function _call(string $route, array $data, $id = null): bool
343
    {
344
        $route = $this->getModelRoute($route, $id);
345
        $response = $this->getResponse($route, [], $data);
346
        if (!$response) {
347
            return false;
348
        }
349
350
        return in_array($response->getStatusCode(), $this->goodStatusCodes, true);
351
    }
352
353
    /**
354
     * @param mixed[] $data
355
     * @return bool
356
     * @throws Exceptions\UndefinedIndexException
357
     */
358
    public function insert(array $data): bool
359
    {
360
        return $this->_call('store', $data);
361
    }
362
363
    /**
364
     * @param mixed $id
365
     * @param mixed[] $data
366
     * @return bool
367
     * @throws Exceptions\UndefinedIndexException
368
     */
369
    public function update($id, array $data): bool
370
    {
371
        return $this->_call('update', $data, $id);
372
    }
373
374
    /**
375
     * @param mixed $id
376
     * @return bool
377
     * @throws Exceptions\UndefinedIndexException
378
     */
379
    public function delete($id): bool
380
    {
381
        return $this->_call('destroy', [], $id);
382
    }
383
384
    /**
385
     * Should we log requests?
386
     * @return bool
387
     */
388
    private function shouldLog(): bool
389
    {
390
        return !!config('restful.logging', false);
391
    }
392
}
393