Passed
Push — relationships ( d37974...d3f308 )
by Alex
02:50
created

JsonApiController::sortQuery()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 3
eloc 6
nc 3
nop 2
1
<?php
2
3
namespace Huntie\JsonApi\Http\Controllers;
4
5
use Huntie\JsonApi\Http\JsonApiResponse;
6
use Huntie\JsonApi\Support\JsonApiErrors;
7
use Illuminate\Database\QueryException;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Relations\BelongsTo;
10
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
11
use Illuminate\Http\Request;
12
use Illuminate\Http\Response;
13
use Illuminate\Routing\Controller;
14
15
abstract class JsonApiController extends Controller
16
{
17
    use JsonApiErrors;
18
19
    /**
20
     * Return the Eloquent Model for the resource.
21
     *
22
     * @return Model
23
     */
24
    abstract protected function getModel();
25
26
    /**
27
     * Return the type name of the resource.
28
     *
29
     * @return string
30
     */
31
    protected function getModelType()
32
    {
33
        return $this->getModel()->getTable();
34
    }
35
36
    /**
37
     * The model relationships that can be updated.
38
     *
39
     * @return string
40
     */
41
    protected function getModelRelationships()
42
    {
43
        return [];
44
    }
45
46
    /**
47
     * Return a listing of the resource.
48
     *
49
     * @param Request $request
50
     *
51
     * @return JsonApiResponse
52
     */
53
    public function indexAction(Request $request)
54
    {
55
        $records = $this->getModel()->newQuery();
56
        $params = $this->getRequestParameters($request);
57
58
        $records = $this->sortQuery($records, $params['sort']);
0 ignored issues
show
Documentation introduced by
$records is of type object<Illuminate\Database\Eloquent\Builder>, but the function expects a object<Huntie\JsonApi\Ht...abase\Eloquent\Builder>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
59
        $records = $this->filterQuery($records, $params['filter']);
60
61
        try {
62
            $records = $records->get();
63
        } catch (QueryException $e) {
64
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
65
        }
66
67
        return new JsonApiResponse($this->transformCollection($records, $params['fields']));
68
    }
69
70
    /**
71
     * Store a new record.
72
     *
73
     * @param Request $request
74
     *
75
     * @return JsonApiResponse
76
     */
77
    public function storeAction(Request $request)
78
    {
79
        $record = $this->getModel()->create((array) $request->input('data.attributes'));
80
81
        if ($relationships = $request->input('data.relationships')) {
82
            $this->updateRecordRelationships($record, (array) $relationships);
83
        }
84
85
        return new JsonApiResponse($this->transformRecord($record), Response::HTTP_CREATED);
86
    }
87
88
    /**
89
     * Return a specified record.
90
     *
91
     * @param Request   $request
92
     * @param Model|int $record
93
     *
94
     * @return JsonApiResponse
95
     */
96
    public function showAction(Request $request, $record)
97
    {
98
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
99
        $params = $this->getRequestParameters($request);
100
101
        return new JsonApiResponse($this->transformRecord($record, $params['fields'], $params['include']));
102
    }
103
104
    /**
105
     * Update a specified record.
106
     *
107
     * @param Request   $request
108
     * @param Model|int $record
109
     *
110
     * @return JsonApiResponse
111
     */
112
    public function updateAction(Request $request, $record)
113
    {
114
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
115
        $record->update((array) $request->input('data.attributes'));
116
117
        if ($relationships = $request->input('data.relationships')) {
118
            $this->updateRecordRelationships($record, (array) $relationships);
119
        }
120
121
        return $this->showAction($request, $record);
122
    }
123
124
    /**
125
     * Destroy a specified record.
126
     *
127
     * @param Request   $request
128
     * @param Model|int $record
129
     *
130
     * @return JsonApiResponse
131
     */
132
    public function destroyAction(Request $request, $record)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
133
    {
134
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
135
        $record->delete();
136
137
        return new JsonApiResponse(null, Response::HTTP_NO_CONTENT);
138
    }
139
140
    /**
141
     * Return a specified record relationship.
142
     *
143
     * @param Request   $request
144
     * @param Model|int $record
145
     * @param string    $relation
146
     *
147
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
148
     *
149
     * @return JsonApiResponse
150
     */
151
    public function relationshipAction(Request $request, $record, $relation)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
152
    {
153
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Reponse::HTTP_NOT_FOUND);
154
155
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
156
        $modelRelation = $this->getModelRelationships()[$relation];
157
158
        if ($modelRelation instanceof BelongsTo) {
159
            $relatedRecord = $record->{$relation};
160
161
            return new JsonApiResponse([
162
                'type' => $relatedRecord->getTable(),
163
                'id' => $relatedRecord->id,
164
            ]);
165
        } else if ($modelRelation instanceof BelongsToMany) {
166
            return new JsonApiResponse($this->transformCollectionIds($record->{$relation}));
167
        }
168
    }
169
170
    /**
171
     * Update a named many-to-one relationship association on a specified record.
172
     * http://jsonapi.org/format/#crud-updating-to-one-relationships
173
     *
174
     * @param Request     $request
175
     * @param Model|int   $record
176
     * @param string      $relation
177
     * @param string|null $foreignKey
178
     *
179
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
180
     *
181
     * @return JsonApiResponse
182
     */
183
    public function updateToOneRelationshipAction(Request $request, $record, $relation, $foreignKey = null)
184
    {
185
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Reponse::HTTP_NOT_FOUND);
186
187
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
188
        $data = (array) $request->input('data');
189
190
        $record->update([($foreignKey ?: $relation . '_id') => $data['id']]);
191
192
        return new JsonApiResponse();
193
    }
194
195
    /**
196
     * Update named many-to-many relationship entries on a specified record.
197
     * http://jsonapi.org/format/#crud-updating-to-many-relationships
198
     *
199
     * @param Request   $request
200
     * @param Model|int $record
201
     * @param string    $relation
202
     *
203
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
204
     *
205
     * @return JsonApiResponse
206
     */
207
    public function updateToManyRelationshipAction(Request $request, $record, $relation)
208
    {
209
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Reponse::HTTP_NOT_FOUND);
210
211
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
212
        $relationships = (array) $request->input('data');
213
        $items = [];
214
215
        foreach ($relationships as $item) {
216
            $items[$item['id']] = $item['attributes'];
217
        }
218
219
        switch ($request->method()) {
220
            case 'PATCH':
221
                $record->{$relation}()->sync($items);
222
                break;
223
            case 'POST':
224
                $record->{$relation}()->sync($items, false);
225
                break;
226
            case 'DELETE':
227
                $record->{$relation}()->detach(array_keys($items));
228
        }
229
230
        return new JsonApiResponse();
231
    }
232
233
    /**
234
     * Return an instance of the resource by primary key.
235
     *
236
     * @param int $key
237
     *
238
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
239
     *
240
     * @return Model
241
     */
242
    protected function findModelInstance($key)
243
    {
244
        return $this->getModel()->findOrFail($key);
245
    }
246
247
    /**
248
     * Return any JSON API resource parameters from a request.
249
     *
250
     * @param Request $request
251
     *
252
     * @return array
253
     */
254
    protected function getRequestParameters($request)
255
    {
256
        return [
257
            'fields' => $this->getRequestQuerySet($request, 'fields.' . $this->getModelType()),
258
            'include' => $this->getRequestQuerySet($request, 'include'),
259
            'sort' => $this->getRequestQuerySet($request, 'sort'),
260
            'filter' => (array) $request->input('filter'),
261
        ];
262
    }
263
264
    /**
265
     * Return any comma separated values in a request query field as an array.
266
     *
267
     * @param Request $request
268
     * @param string  $key
269
     *
270
     * @return array
271
     */
272
    protected function getRequestQuerySet($request, $key)
273
    {
274
        return preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
275
    }
276
277
    /**
278
     * Sort a resource query by one or more attributes.
279
     *
280
     * @param Illuminate\Database\Eloquent\Builder $query
281
     * @param array                                $attributes
282
     *
283
     * @return Illuminate\Database\Eloquent\Builder
284
     */
285
    public function sortQuery($query, $attributes)
286
    {
287
        foreach ($attributes as $expression) {
288
            $direction = substr($expression, 0, 1) === '-' ? 'desc' : 'asc';
289
            $column = preg_replace('/^\-/', '', $expression);
290
            $query = $query->orderBy($column, $direction);
291
        }
292
293
        return $query;
294
    }
295
296
    /**
297
     * Filter a resource query by one or more attributes.
298
     *
299
     * @param Illuminate\Database\Eloquent\Builder $query
300
     * @param array                                $attributes
301
     *
302
     * @return Illuminate\Database\Eloquent\Builder
303
     */
304
    public function filterQuery($query, $attributes)
305
    {
306
        foreach ($attributes as $column => $value) {
307
            if (is_numeric($value)) {
308
                // Exact numeric match
309
                $query = $query->where($column, $value);
310
            } else if (in_array(strtolower($value), ['true', 'false'])) {
311
                // Boolean match
312
                $query = $query->where($column, filter_var($value, FILTER_VALIDATE_BOOLEAN));
313
            } else {
314
                // Partial string match
315
                $query = $query->where($column, 'like', '%' . $value . '%');
316
            }
317
        }
318
319
        return $query;
320
    }
321
322
    /**
323
     * Transform a model instance into a JSON API object.
324
     *
325
     * @param Model      $record
326
     * @param array|null $fields  Field names of attributes to limit to
327
     * @param array|null $include Relations to include
328
     *
329
     * @return array
330
     */
331
    protected function transformRecord($record, array $fields = [], array $include = [])
332
    {
333
        $relations = array_unique(array_merge($record->getRelations(), $include));
334
        $attributes = $record->load($relations)->toArray();
335
        $relationships = [];
336
        $included = [];
337
338
        foreach ($relations as $relation) {
339
            $relationships[$relation] = [
340
                'data' => []
341
            ];
342
343
            foreach (array_pull($attributes, $relation) as $relatedRecord) {
344
                $relationships[$relation]['data'][] = [
345
                    'type' => $relation,
346
                    'id' => $relatedRecord['id'],
347
                ];
348
349
                if (in_array($relation, $include)) {
350
                    $included[] = [
351
                        'type' => $relation,
352
                        'id' => $relatedRecord['id'],
353
                        'attributes' => array_except($relatedRecord, ['id', 'pivot']),
354
                    ];
355
                }
356
            }
357
        }
358
359
        if (!empty($fields)) {
360
            $attributes = array_only($attributes, $fields);
361
        }
362
363
        $data = array_filter([
364
            'type' => $record->getTable(),
365
            'id' => $record->id,
366
            'attributes' => array_except($attributes, ['id']),
367
            'relationships' => $relationships,
368
        ]);
369
370
        return array_filter(compact('data', 'included'));
371
    }
372
373
    /**
374
     * Transform a set of models into a JSON API collection.
375
     *
376
     * @param \Illuminate\Support\Collection $records
377
     * @param array                          $fields
378
     *
379
     * @return array
380
     */
381
    protected function transformCollection($records, array $fields = [])
382
    {
383
        $data = $records->map(function ($record) use ($fields) {
384
            return $this->transformRecord($record, $fields)['data'];
385
        });
386
387
        return compact('data');
388
    }
389
390
    /**
391
     * Transform a set of models into a collection of JSON API resource
392
     * identifier objects.
393
     *
394
     * @param \Illuminate\Support\Collection $records
395
     *
396
     * @return array
397
     */
398
    protected function transformCollectionIds($records)
399
    {
400
        $data = $records->map(function ($record) {
401
            return [
402
                'type' => $record->getTable(),
403
                'id' => $record->id,
404
            ];
405
        });
406
407
        return compact('data');
408
    }
409
410
    /**
411
     * Update one or more relationships on a model instance.
412
     *
413
     * @param Model $record
414
     * @param array $relationships
415
     */
416
    protected function updateRecordRelationships($record, array $relationships)
417
    {
418
        $relationships = array_intersect_key($relationships, $this->getModelRelationships());
419
420
        foreach ($relationships as $name => $relationship) {
421
            $relation = $this->getModelRelationships()[$name];
422
            $data = $relationship['data'];
423
424
            if ($relation instanceof BelongsTo) {
425
                $record->update([$relation->getForeignKey() => $data['id']]);
426
            } else if ($relation instanceof BelongsToMany) {
427
                $record->{$name}()->sync(array_pluck($data, 'id'));
428
            }
429
        }
430
    }
431
}
432