Passed
Push — related-record-fixes ( efcb4a...afa92b )
by Alex
03:42
created

JsonApiController   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 357
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
wmc 40
c 0
b 0
f 0
lcom 1
cbo 15
dl 0
loc 357
rs 7.6556

16 Methods

Rating   Name   Duplication   Size   Complexity  
getModel() 0 1 ?
A getModelRelationships() 0 4 1
A indexAction() 0 19 4
A storeAction() 0 10 2
A showAction() 0 7 1
A updateAction() 0 12 2
A destroyAction() 0 7 1
A relationshipAction() 0 8 1
A updateToOneRelationshipAction() 0 13 1
B updateToManyRelationshipAction() 0 29 6
A findModelInstance() 0 12 3
A getRequestParameters() 0 15 4
A getRequestQuerySet() 0 17 3
A sortQuery() 0 10 3
B filterQuery() 0 22 4
A updateRecordRelationships() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like JsonApiController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JsonApiController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Huntie\JsonApi\Http\Controllers;
4
5
use Schema;
6
use Validator;
7
use Huntie\JsonApi\Exceptions\HttpException;
8
use Huntie\JsonApi\Http\JsonApiResponse;
9
use Huntie\JsonApi\Serializers\CollectionSerializer;
10
use Huntie\JsonApi\Serializers\RelationshipSerializer;
11
use Huntie\JsonApi\Serializers\ResourceSerializer;
12
use Huntie\JsonApi\Support\JsonApiErrors;
13
use Illuminate\Database\QueryException;
14
use Illuminate\Database\Eloquent\Model;
15
use Illuminate\Database\Eloquent\ModelNotFoundException;
16
use Illuminate\Database\Eloquent\Relations\BelongsTo;
17
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
18
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
19
use Illuminate\Foundation\Validation\ValidatesRequests;
20
use Illuminate\Http\Request;
21
use Illuminate\Http\Response;
22
use Illuminate\Routing\Controller;
23
use Illuminate\Validation\ValidationException;
24
25
abstract class JsonApiController extends Controller
26
{
27
    use JsonApiErrors;
28
    use AuthorizesRequests;
29
    use ValidatesRequests;
30
31
    /**
32
     * Return the Eloquent Model for the resource.
33
     *
34
     * @return Model
35
     */
36
    abstract protected function getModel();
37
38
    /**
39
     * The model relationships that can be updated.
40
     *
41
     * @return array
42
     */
43
    protected function getModelRelationships()
44
    {
45
        return [];
46
    }
47
48
    /**
49
     * Return a listing of the resource.
50
     *
51
     * @param Request                                    $request
52
     * @param \Illuminate\Database\Eloquent\Builder|null $query   Custom resource query
53
     *
54
     * @return JsonApiResponse
55
     */
56
    public function indexAction(Request $request, $query = null)
57
    {
58
        $records = $query ?: $this->getModel()->newQuery();
59
        $params = $this->getRequestParameters($request);
60
61
        $records = $this->sortQuery($records, $params['sort']);
62
        $records = $this->filterQuery($records, $params['filter']);
63
64
        try {
65
            $pageSize = min($this->getModel()->getPerPage(), $request->input('page.size'));
66
            $pageNumber = $request->input('page.number') ?: 1;
67
68
            $records = $records->paginate($pageSize, null, 'page', $pageNumber);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a array.

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...
69
        } catch (QueryException $e) {
70
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
71
        }
72
73
        return new JsonApiResponse(new CollectionSerializer($records, $params['fields'], $params['include']));
0 ignored issues
show
Documentation introduced by
$records is of type object<Illuminate\Contra...n\LengthAwarePaginator>, but the function expects a object<Illuminate\Suppor...n\LengthAwarePaginator>.

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...
74
    }
75
76
    /**
77
     * Store a new record.
78
     *
79
     * @param Request $request
80
     *
81
     * @return JsonApiResponse
82
     */
83
    public function storeAction(Request $request)
84
    {
85
        $record = $this->getModel()->create((array) $request->input('data.attributes'));
86
87
        if ($relationships = $request->input('data.relationships')) {
88
            $this->updateRecordRelationships($record, (array) $relationships);
89
        }
90
91
        return new JsonApiResponse(new ResourceSerializer($record), Response::HTTP_CREATED);
92
    }
93
94
    /**
95
     * Return a specified record.
96
     *
97
     * @param Request   $request
98
     * @param Model|int $record
99
     *
100
     * @return JsonApiResponse
101
     */
102
    public function showAction(Request $request, $record)
103
    {
104
        $record = $this->findModelInstance($record);
105
        $params = $this->getRequestParameters($request);
106
107
        return new JsonApiResponse(new ResourceSerializer($record, $params['fields'], $params['include']));
108
    }
109
110
    /**
111
     * Update a specified record.
112
     *
113
     * @param Request   $request
114
     * @param Model|int $record
115
     *
116
     * @return JsonApiResponse
117
     */
118
    public function updateAction(Request $request, $record)
119
    {
120
        $record = $this->findModelInstance($record);
121
        $record->fill((array) $request->input('data.attributes'));
122
        $record->save();
123
124
        if ($relationships = $request->input('data.relationships')) {
125
            $this->updateRecordRelationships($record, (array) $relationships);
126
        }
127
128
        return new JsonApiResponse(new ResourceSerializer($record));
129
    }
130
131
    /**
132
     * Destroy a specified record.
133
     *
134
     * @param Request   $request
135
     * @param Model|int $record
136
     *
137
     * @return JsonApiResponse
138
     */
139
    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...
140
    {
141
        $record = $this->findModelInstance($record);
142
        $record->delete();
143
144
        return new JsonApiResponse(null, Response::HTTP_NO_CONTENT);
145
    }
146
147
    /**
148
     * Return a specified record relationship.
149
     *
150
     * @param Request   $request
151
     * @param Model|int $record
152
     * @param string    $relation
153
     *
154
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
155
     *
156
     * @return JsonApiResponse
157
     */
158
    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...
159
    {
160
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
161
162
        $record = $this->findModelInstance($record);
163
164
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
165
    }
166
167
    /**
168
     * Update a named many-to-one relationship association on a specified record.
169
     * http://jsonapi.org/format/#crud-updating-to-one-relationships
170
     *
171
     * @param Request     $request
172
     * @param Model|int   $record
173
     * @param string      $relation
174
     *
175
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
176
     *
177
     * @return JsonApiResponse
178
     */
179
    public function updateToOneRelationshipAction(Request $request, $record, $relation)
180
    {
181
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
182
183
        $record = $this->findModelInstance($record);
184
        $relation = $this->getModelRelationships()[$relation];
185
        $data = (array) $request->input('data');
186
187
        $record->{$relation->getForeignKey()} = $data['id'];
188
        $record->save();
189
190
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
191
    }
192
193
    /**
194
     * Update named many-to-many relationship entries on a specified record.
195
     * http://jsonapi.org/format/#crud-updating-to-many-relationships
196
     *
197
     * @param Request   $request
198
     * @param Model|int $record
199
     * @param string    $relation
200
     *
201
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
202
     *
203
     * @return JsonApiResponse
204
     */
205
    public function updateToManyRelationshipAction(Request $request, $record, $relation)
206
    {
207
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
208
209
        $record = $this->findModelInstance($record);
210
        $relationships = (array) $request->input('data');
211
        $items = [];
212
213
        foreach ($relationships as $item) {
214
            if (isset($item['attributes'])) {
215
                $items[$item['id']] = $item['attributes'];
216
            } else {
217
                $items[] = $item['id'];
218
            }
219
        }
220
221
        switch ($request->method()) {
222
            case 'PATCH':
223
                $record->{$relation}()->sync($items);
224
                break;
225
            case 'POST':
226
                $record->{$relation}()->sync($items, false);
227
                break;
228
            case 'DELETE':
229
                $record->{$relation}()->detach(array_keys($items));
230
        }
231
232
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
233
    }
234
235
    /**
236
     * Return existing instance of the resource or find by primary key.
237
     *
238
     * @param Model|int $record
239
     *
240
     * @throws ModelNotFoundException
241
     *
242
     * @return Model
243
     */
244
    protected function findModelInstance($record)
245
    {
246
        if ($record instanceof Model) {
247
            if (is_null($record->getKey())) {
248
                throw new ModelNotFoundException();
249
            }
250
251
            return $record;
252
        }
253
254
        return $this->getModel()->findOrFail($record);
255
    }
256
257
    /**
258
     * Return any JSON API resource parameters from a request.
259
     *
260
     * @param Request $request
261
     *
262
     * @return array
263
     */
264
    protected function getRequestParameters($request)
265
    {
266
        $enableIncluded = config('jsonapi.enable_included_resources');
267
268
        if ($request->has('include') && is_bool($enableIncluded) && !$enableIncluded) {
269
            throw new HttpException('Inclusion of related resources is not supported');
270
        }
271
272
        return [
273
            'fields' => $this->getRequestQuerySet($request, 'fields', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
274
            'include' => $this->getRequestQuerySet($request, 'include', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
275
            'sort' => $this->getRequestQuerySet($request, 'sort', '/[A-Za-z_]+/'),
276
            'filter' => (array) $request->input('filter'),
277
        ];
278
    }
279
280
    /**
281
     * Return any comma separated values in a request query field as an array.
282
     *
283
     * @param Request     $request
284
     * @param string      $key
285
     * @param string|null $validate Regular expression to test for each item
286
     *
287
     * @throws \Illuminate\Validation\ValidationException
288
     *
289
     * @return array
290
     */
291
    protected function getRequestQuerySet($request, $key, $validate = null)
292
    {
293
        $values = preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
294
295
        $validator = Validator::make(['param' => $values], [
296
            'param.*' => 'required' . ($validate ? '|regex:' . $validate : ''),
297
        ]);
298
299
        if ($validator->fails()) {
300
            throw new ValidationException($validator, $this->error(
301
                Response::HTTP_BAD_REQUEST,
302
                sprintf('Invalid values for "%s" parameter', $key))
303
            );
304
        }
305
306
        return $values;
307
    }
308
309
    /**
310
     * Sort a resource query by one or more attributes.
311
     *
312
     * @param \Illuminate\Database\Eloquent\Builder $query
313
     * @param array                                 $attributes
314
     *
315
     * @return \Illuminate\Database\Eloquent\Builder
316
     */
317
    protected function sortQuery($query, $attributes)
318
    {
319
        foreach ($attributes as $expression) {
320
            $direction = substr($expression, 0, 1) === '-' ? 'desc' : 'asc';
321
            $column = preg_replace('/^\-/', '', $expression);
322
            $query = $query->orderBy($column, $direction);
323
        }
324
325
        return $query;
326
    }
327
328
    /**
329
     * Filter a resource query by one or more attributes.
330
     *
331
     * @param \Illuminate\Database\Eloquent\Builder $query
332
     * @param array                                 $attributes
333
     *
334
     * @return \Illuminate\Database\Eloquent\Builder
335
     */
336
    protected function filterQuery($query, $attributes)
337
    {
338
        $searchableColumns = array_diff(
339
            Schema::getColumnListing($this->getModel()->getTable()),
340
            $this->getModel()->getHidden()
341
        );
342
343
        foreach (array_intersect_key($attributes, array_flip($searchableColumns)) as $column => $value) {
344
            if (is_numeric($value)) {
345
                // Exact numeric match
346
                $query = $query->where($column, $value);
347
            } else if (in_array(strtolower($value), ['true', 'false'])) {
348
                // Boolean match
349
                $query = $query->where($column, filter_var($value, FILTER_VALIDATE_BOOLEAN));
350
            } else {
351
                // Partial string match
352
                $query = $query->where($column, 'LIKE', '%' . $value . '%');
353
            }
354
        }
355
356
        return $query;
357
    }
358
359
    /**
360
     * Update one or more relationships on a model instance.
361
     *
362
     * @param Model $record
363
     * @param array $relationships
364
     */
365
    protected function updateRecordRelationships($record, array $relationships)
366
    {
367
        $relationships = array_intersect_key($relationships, $this->getModelRelationships());
368
369
        foreach ($relationships as $name => $relationship) {
370
            $relation = $this->getModelRelationships()[$name];
371
            $data = $relationship['data'];
372
373
            if ($relation instanceof BelongsTo) {
374
                $record->{$relation->getForeignKey()} = $data['id'];
375
                $record->save();
376
            } else if ($relation instanceof BelongsToMany) {
377
                $record->{$name}()->sync(array_pluck($data, 'id'));
378
            }
379
        }
380
    }
381
}
382