Passed
Push — master ( c54cf2...993f75 )
by Tom
01:10 queued 10s
created

Builder   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 7

Importance

Changes 0
Metric Value
wmc 30
lcom 4
cbo 7
dl 0
loc 335
rs 10
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A model() 0 5 1
A getModelRoute() 0 16 3
A getResponse() 0 11 1
B parseJsonFromResponse() 0 26 6
A isPaginatedResponse() 0 4 1
A extractDataFromJson() 0 8 2
A where() 0 10 1
A whereIn() 0 10 1
A getWheres() 0 4 1
A order() 0 9 1
A getOrder() 0 4 1
A limit() 0 6 1
A getLimit() 0 4 1
A get() 0 25 3
A _call() 0 10 2
A insert() 0 4 1
A update() 0 4 1
A delete() 0 4 1
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),
0 ignored issues
show
Bug introduced by
It seems like $route->getHrefs('absolute', $queryString) targeting TomHart\Restful\Routing\Route::getHrefs() 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?

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.

Loading history...
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\
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...
232
     */
233
    public function order(string $column, string $direction): self
234
    {
235
        $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...
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();
0 ignored issues
show
Bug introduced by
The method hydrate() does not seem to exist on object<TomHart\Restful\Concerns\Restful>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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