Builder::getWheres()   A
last analyzed

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 DebugBar\DataCollector\MessagesCollector;
6
use DebugBar\DebugBar;
7
use DebugBar\DebugBarException;
8
use GuzzleHttp\Client;
9
use Illuminate\Contracts\Container\BindingResolutionException;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Query\Grammars\Grammar;
12
use Illuminate\Support\Collection;
13
use InvalidArgumentException;
14
use Psr\Http\Message\MessageInterface;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\HttpFoundation\Response;
17
use Symfony\Component\Routing\Exception\RouteNotFoundException;
18
use TomHart\Restful\Concerns\Restful;
19
use TomHart\Restful\Concerns\Transformer;
20
use TomHart\Restful\Routing\Route;
21
22
use function GuzzleHttp\json_decode as guzzle_json_decode;
23
24
class Builder
25
{
26
    /**
27
     * @var Restful
28
     */
29
    protected $model;
30
31
    /**
32
     * @var Client
33
     */
34
    protected $client;
35
36
    /**
37
     * @var Grammar
38
     */
39
    protected $grammar;
40
41
    /**
42
     * @var mixed[]
43
     */
44
    protected $wheres = [];
45
46
    /**
47
     * @var string[]
48
     */
49
    private $order;
50
51
    /**
52
     * @var int
53
     */
54
    private $limit;
55
56
    /**
57
     * @var int[]
58
     */
59
    private $goodStatusCodes = [
60
        Response::HTTP_OK,
61
        Response::HTTP_CREATED,
62
        Response::HTTP_ACCEPTED
63
    ];
64
    /**
65
     * @var LoggerInterface
66
     */
67
    private $logger;
68
69
    /**
70
     * Builder constructor.
71
     * @param Client $client
72
     * @param LoggerInterface $logger
73
     * @param Restful $model
74
     */
75
    public function __construct(Client $client, LoggerInterface $logger, Restful $model)
76
    {
77
        $this->model = $model;
78
        $this->client = $client;
79
        $this->logger = $logger;
80
    }
81
82
    /**
83
     * What model are we querying?
84
     * @param string $class
85
     * @return Builder
86
     */
87
    public static function model(string $class)
88
    {
89
        $class = new $class();
90
        return app(static::class, ['model' => $class]);
91
    }
92
93
    /**
94
     * Returns a new instance of the model
95
     * @return Model
96
     */
97
    private function newModelInstance(): Model
98
    {
99
        return new $this->model();
100
    }
101
102
    /**
103
     * Get the index route for the model via the options route.
104
     * @param string $route
105
     * @param null $id
106
     * @return Route
107
     * @throws Exceptions\UndefinedIndexException
108
     * @throws RouteNotFoundException
109
     */
110
    private function getModelRoute(string $route, $id = null): Route
111
    {
112
        $optionRoute = $this->model->getOptionsRoute();
113
        $links = $this->getResponse($optionRoute, ['id' => $id]);
114
        if (!$links) {
115
            throw new RouteNotFoundException('Cannot get options from route');
116
        }
117
118
        $links = $this->parseJsonFromResponse($links);
119
        if (!isset($links[$route])) {
120
            throw new RouteNotFoundException("Cannot find link to $route route");
121
        }
122
123
        return Route::fromArray($links[$route]);
124
    }
125
126
    /**
127
     * Get the JSON from the given URL.
128
     * @param Route $route
129
     * @param array $queryString
130
     * @param array $postData
131
     * @return MessageInterface
132
     * @throws Exceptions\UndefinedIndexException
133
     */
134
    private function getResponse(Route $route, array $queryString = [], array $postData = []): ?MessageInterface
135
    {
136
        if ($domain = config('restful.api_domain')) {
137
            $url = $domain . $route->getHrefs('relative', $queryString);
138
        } else {
139
            $url = $route->getHrefs('absolute', $queryString);
140
        }
141
142
        if ($this->shouldLog()) {
143
            $this->logger->info(sprintf('REST-CALL: %s to %s', $route->getMethod(), $url));
144
145
            try {
146
                /** @var DebugBar $debugBar */
147
                $debugBar = app('debugbar');
148
149
                /** @var MessagesCollector $collector */
150
                $collector = $debugBar->getCollector('restful_calls');
151
                $collector->addMessage(sprintf('REST-CALL: %s to %s', $route->getMethod(), $url));
152
            } catch (DebugBarException|BindingResolutionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
153
            }
154
        }
155
156
        return $this->client->request(
157
            $route->getMethod(),
158
            $url,
0 ignored issues
show
Bug introduced by
It seems like $url defined by $route->getHrefs('absolute', $queryString) on line 139 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...
159
            [
160
                'headers' => config('restful.headers'),
161
                'form_params' => $postData
162
            ]
163
        );
164
    }
165
166
    /**
167
     * Extract JSON from a Response.
168
     * @param MessageInterface $response
169
     * @return mixed
170
     * @throws Exceptions\UndefinedIndexException
171
     */
172
    private function parseJsonFromResponse(MessageInterface $response)
173
    {
174
        $body = $response->getBody()->getContents();
175
        if (!$body) {
176
            return null;
177
        }
178
179
        $json = guzzle_json_decode($body, true);
180
181
        // If the response is paginated, and there is a next page, get that and add the results.
182
        if ($this->isPaginatedResponse($json) &&
183
            isset($json['next_page_url'], $json['current_page'], $json['last_page']) &&
184
            $json['current_page'] < $json['last_page']
185
        ) {
186
            $nextPageResponse = $this->getResponse(Route::fromUrl($json['next_page_url'], 'GET'));
187
            if ($nextPageResponse) {
188
                $nextPageJson = $this->parseJsonFromResponse($nextPageResponse);
189
190
                $json['data'] = array_merge($json['data'], $nextPageJson['data']);
191
192
                return $json;
193
            }
194
        }
195
196
        return $json;
197
    }
198
199
    /**
200
     * Is the JSON responses a paginatable one?
201
     * @param mixed[] $json
202
     * @return bool
203
     */
204
    private function isPaginatedResponse(array $json): bool
205
    {
206
        return empty(array_diff(config('restful.pagination_json_keys'), array_keys($json)));
207
    }
208
209
    /**
210
     * Extract data from the JSON
211
     * @param $json
212
     * @return array
213
     * @throws InvalidArgumentException
214
     */
215
    private function extractDataFromJson($json): array
216
    {
217
        if (!isset($json['data'])) {
218
            throw new InvalidArgumentException('$json doesn\'t contain a data key');
219
        }
220
221
        return $json['data'];
222
    }
223
224
    /**
225
     * Add a where clause.
226
     * @param string $column
227
     * @param $value
228
     * @return Builder
229
     */
230
    public function where(string $column, $value): self
231
    {
232
        $this->wheres[] = [
233
            'column' => $column,
234
            'type' => 'Basic',
235
            'value' => $value
236
        ];
237
238
        return $this;
239
    }
240
241
    /**
242
     * Add a whereIn clause.
243
     * @param string $column
244
     * @param mixed[] $values
245
     * @return Builder
246
     */
247
    public function whereIn(string $column, array $values): self
248
    {
249
        $this->wheres[] = [
250
            'column' => $column,
251
            'type' => 'In',
252
            'values' => $values
253
        ];
254
255
        return $this;
256
    }
257
258
    /**
259
     * Return all the wheres
260
     * @return mixed[]
261
     */
262
    public function getWheres(): array
263
    {
264
        return $this->wheres;
265
    }
266
267
    /**
268
     * Set an order
269
     * @param string $column
270
     * @param string $direction
271
     * @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...
272
     */
273
    public function order(string $column, string $direction): self
274
    {
275
        $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...
276
            'column' => $column,
277
            'direction' => $direction
278
        ];
279
280
        return $this;
281
    }
282
283
    /**
284
     * Get the order
285
     * @return array|null
286
     */
287
    public function getOrder(): ?array
288
    {
289
        return $this->order;
290
    }
291
292
    /**
293
     * Specify a limit
294
     * @param int $limit
295
     * @return $this
296
     */
297
    public function limit(int $limit): self
298
    {
299
        $this->limit = $limit;
300
301
        return $this;
302
    }
303
304
    /**
305
     * Get the limit
306
     * @return int|null
307
     */
308
    public function getLimit(): ?int
309
    {
310
        return $this->limit;
311
    }
312
313
    /**
314
     * Call the API, and return the models.
315
     * @return Collection
316
     * @throws Exceptions\UndefinedIndexException
317
     */
318
    public function get(): Collection
319
    {
320
        $options = $this->getModelRoute('index');
321
322
        /** @var Transformer $transformer */
323
        $transformer = app(Transformer::class);
324
325
        $queryString = $transformer->buildQueryString($this);
326
327
        $response = $this->getResponse($options, $queryString);
328
        if (!$response) {
329
            return collect();
330
        }
331
332
        $json = $this->parseJsonFromResponse($response);
333
        if (!$json) {
334
            return collect();
335
        }
336
337
        $data = $this->extractDataFromJson($json);
338
339
        $instance = $this->newModelInstance();
340
341
        return $instance->newCollection(
342
            array_map(
343
                function ($item) use ($instance) {
344
                    return $instance->newFromBuilder($item);
345
                },
346
                $data
347
            )
348
        );
349
    }
350
351
    /**
352
     * @param string $route
353
     * @param mixed[] $data
354
     * @param mixed $id
355
     * @return bool
356
     * @throws Exceptions\UndefinedIndexException
357
     */
358
    private function _call(string $route, array $data, $id = null): bool
359
    {
360
        $route = $this->getModelRoute($route, $id);
361
        $response = $this->getResponse($route, [], $data);
362
        if (!$response) {
363
            return false;
364
        }
365
366
        return in_array($response->getStatusCode(), $this->goodStatusCodes, true);
367
    }
368
369
    /**
370
     * @param mixed[] $data
371
     * @return bool
372
     * @throws Exceptions\UndefinedIndexException
373
     */
374
    public function insert(array $data): bool
375
    {
376
        return $this->_call('store', $data);
377
    }
378
379
    /**
380
     * @param mixed $id
381
     * @param mixed[] $data
382
     * @return bool
383
     * @throws Exceptions\UndefinedIndexException
384
     */
385
    public function update($id, array $data): bool
386
    {
387
        return $this->_call('update', $data, $id);
388
    }
389
390
    /**
391
     * @param mixed $id
392
     * @return bool
393
     * @throws Exceptions\UndefinedIndexException
394
     */
395
    public function delete($id): bool
396
    {
397
        return $this->_call('destroy', [], $id);
398
    }
399
400
    /**
401
     * Should we log requests?
402
     * @return bool
403
     */
404
    private function shouldLog(): bool
405
    {
406
        return !!config('restful.logging', false);
407
    }
408
}
409