Passed
Push — controller-updates ( c4bb3e )
by Alex
04:42
created

JsonApiController   C

Complexity

Total Complexity 41

Size/Duplication

Total Lines 341
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 17
dl 0
loc 341
rs 6.6122
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 3
A getModelRelationships() 0 4 1
A indexAction() 0 20 4
A storeAction() 0 10 2
A showAction() 0 8 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
B validateIncludableRelations() 0 12 5
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 Validator;
6
use Huntie\JsonApi\Contracts\Model\IncludesRelatedResources;
7
use Huntie\JsonApi\Exceptions\HttpException;
8
use Huntie\JsonApi\Exceptions\InvalidRelationPathException;
9
use Huntie\JsonApi\Http\JsonApiResponse;
10
use Huntie\JsonApi\Http\Concerns\QueriesResources;
11
use Huntie\JsonApi\Serializers\CollectionSerializer;
12
use Huntie\JsonApi\Serializers\RelationshipSerializer;
13
use Huntie\JsonApi\Serializers\ResourceSerializer;
14
use Huntie\JsonApi\Support\JsonApiErrors;
15
use Illuminate\Database\QueryException;
16
use Illuminate\Database\Eloquent\Model;
17
use Illuminate\Database\Eloquent\ModelNotFoundException;
18
use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
20
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
21
use Illuminate\Foundation\Validation\ValidatesRequests;
22
use Illuminate\Http\Request;
23
use Illuminate\Http\Response;
24
use Illuminate\Routing\Controller;
25
use Illuminate\Validation\ValidationException;
26
27
abstract class JsonApiController extends Controller
28
{
29
    use JsonApiErrors;
30
    use QueriesResources;
31
    use AuthorizesRequests;
32
    use ValidatesRequests;
33
34
    /**
35
     * The Eloquent Model for the resource.
36
     *
37
     * @var Model|string
38
     */
39
    protected $model;
40
41
    /**
42
     * Create a new JsonApiController instance.
43
     */
44
    public function __construct()
45
    {
46
        if (is_string($this->model)) {
47
            if (!is_subclass_of($this->model, Model::class)) {
48
                $this->model = str_finish(config('jsonapi.model_namespace', app()->getNamespace()), '\\')
49
                    . preg_replace('/Controller$/', '', class_basename($this));
50
            }
51
52
            $this->model = new $this->model;
53
        }
54
    }
55
56
    protected function getModelRelationships()
57
    {
58
        return [];
59
    }
60
61
    /**
62
     * Return a listing of the resource.
63
     *
64
     * @param Request                                    $request
65
     * @param \Illuminate\Database\Eloquent\Builder|null $query   Custom resource query
66
     *
67
     * @return JsonApiResponse
68
     */
69
    public function indexAction(Request $request, $query = null)
70
    {
71
        $records = $query ?: $this->model->newQuery();
72
        $params = $this->getRequestParameters($request);
73
        $this->validateIncludableRelations($params['include']);
74
75
        $records = $this->sortQuery($records, $params['sort']);
76
        $records = $this->filterQuery($records, $params['filter']);
77
78
        try {
79
            $pageSize = min($this->model->getPerPage(), $request->input('page.size'));
80
            $pageNumber = $request->input('page.number') ?: 1;
81
82
            $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...
83
        } catch (QueryException $e) {
84
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
85
        }
86
87
        return new JsonApiResponse(new CollectionSerializer($records, $params['fields'], $params['include']));
88
    }
89
90
    /**
91
     * Store a new record.
92
     *
93
     * @param Request $request
94
     *
95
     * @return JsonApiResponse
96
     */
97
    public function storeAction(Request $request)
98
    {
99
        $record = $this->model->create((array) $request->input('data.attributes'));
0 ignored issues
show
Bug introduced by
The method create() does not exist on Illuminate\Database\Eloquent\Model. Did you maybe mean created()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
100
101
        if ($relationships = $request->input('data.relationships')) {
102
            $this->updateRecordRelationships($record, (array) $relationships);
103
        }
104
105
        return new JsonApiResponse(new ResourceSerializer($record), Response::HTTP_CREATED);
106
    }
107
108
    /**
109
     * Return a specified record.
110
     *
111
     * @param Request     $request
112
     * @param Model|mixed $record
113
     *
114
     * @return JsonApiResponse
115
     */
116
    public function showAction(Request $request, $record)
117
    {
118
        $record = $this->findModelInstance($record);
119
        $params = $this->getRequestParameters($request);
120
        $this->validateIncludableRelations($params['include']);
121
122
        return new JsonApiResponse(new ResourceSerializer($record, $params['fields'], $params['include']));
123
    }
124
125
    /**
126
     * Update a specified record.
127
     *
128
     * @param Request     $request
129
     * @param Model|mixed $record
130
     *
131
     * @return JsonApiResponse
132
     */
133
    public function updateAction(Request $request, $record)
134
    {
135
        $record = $this->findModelInstance($record);
136
        $record->fill((array) $request->input('data.attributes'));
137
        $record->save();
138
139
        if ($relationships = $request->input('data.relationships')) {
140
            $this->updateRecordRelationships($record, (array) $relationships);
141
        }
142
143
        return new JsonApiResponse(new ResourceSerializer($record));
144
    }
145
146
    /**
147
     * Destroy a specified record.
148
     *
149
     * @param Request     $request
150
     * @param Model|mixed $record
151
     *
152
     * @return JsonApiResponse
153
     */
154
    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...
155
    {
156
        $record = $this->findModelInstance($record);
157
        $record->delete();
158
159
        return new JsonApiResponse(null, Response::HTTP_NO_CONTENT);
160
    }
161
162
    /**
163
     * Return a specified record relationship.
164
     *
165
     * @param Request     $request
166
     * @param Model|mixed $record
167
     * @param string      $relation
168
     *
169
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
170
     *
171
     * @return JsonApiResponse
172
     */
173
    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...
174
    {
175
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
176
177
        $record = $this->findModelInstance($record);
178
179
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
180
    }
181
182
    /**
183
     * Update a named many-to-one relationship association on a specified record.
184
     * http://jsonapi.org/format/#crud-updating-to-one-relationships
185
     *
186
     * @param Request     $request
187
     * @param Model|mixed $record
188
     * @param string      $relation
189
     *
190
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
191
     *
192
     * @return JsonApiResponse
193
     */
194
    public function updateToOneRelationshipAction(Request $request, $record, $relation)
195
    {
196
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
197
198
        $record = $this->findModelInstance($record);
199
        $relation = $this->getModelRelationships()[$relation];
200
        $data = (array) $request->input('data');
201
202
        $record->{$relation->getForeignKey()} = $data['id'];
203
        $record->save();
204
205
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
206
    }
207
208
    /**
209
     * Update named many-to-many relationship entries on a specified record.
210
     * http://jsonapi.org/format/#crud-updating-to-many-relationships
211
     *
212
     * @param Request     $request
213
     * @param Model|mixed $record
214
     * @param string      $relation
215
     *
216
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
217
     *
218
     * @return JsonApiResponse
219
     */
220
    public function updateToManyRelationshipAction(Request $request, $record, $relation)
221
    {
222
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
223
224
        $record = $this->findModelInstance($record);
225
        $relationships = (array) $request->input('data');
226
        $items = [];
227
228
        foreach ($relationships as $item) {
229
            if (isset($item['attributes'])) {
230
                $items[$item['id']] = $item['attributes'];
231
            } else {
232
                $items[] = $item['id'];
233
            }
234
        }
235
236
        switch ($request->method()) {
237
            case 'PATCH':
238
                $record->{$relation}()->sync($items);
239
                break;
240
            case 'POST':
241
                $record->{$relation}()->sync($items, false);
242
                break;
243
            case 'DELETE':
244
                $record->{$relation}()->detach(array_keys($items));
245
        }
246
247
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
248
    }
249
250
    /**
251
     * Return existing instance of the resource or find by primary key.
252
     *
253
     * @param Model|mixed $record
254
     *
255
     * @throws ModelNotFoundException
256
     *
257
     * @return Model
258
     */
259
    protected function findModelInstance($record)
260
    {
261
        if ($record instanceof Model) {
262
            if (is_null($record->getKey())) {
263
                throw new ModelNotFoundException();
264
            }
265
266
            return $record;
267
        }
268
269
        return $this->model->findOrFail($record);
270
    }
271
272
    /**
273
     * Return any JSON API resource parameters from a request.
274
     *
275
     * @param Request $request
276
     *
277
     * @return array
278
     */
279
    protected function getRequestParameters($request)
280
    {
281
        $enableIncluded = config('jsonapi.enable_included_resources');
282
283
        if ($request->has('include') && is_bool($enableIncluded) && !$enableIncluded) {
284
            throw new HttpException('Inclusion of related resources is not supported');
285
        }
286
287
        return [
288
            'fields' => $this->getRequestQuerySet($request, 'fields', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
289
            'include' => $this->getRequestQuerySet($request, 'include', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
290
            'sort' => $this->getRequestQuerySet($request, 'sort', '/[A-Za-z_]+/'),
291
            'filter' => (array) $request->input('filter'),
292
        ];
293
    }
294
295
    /**
296
     * Return any comma separated values in a request query field as an array.
297
     *
298
     * @param Request     $request
299
     * @param string      $key
300
     * @param string|null $validate Regular expression to test for each item
301
     *
302
     * @throws \Illuminate\Validation\ValidationException
303
     *
304
     * @return array
305
     */
306
    protected function getRequestQuerySet($request, $key, $validate = null)
307
    {
308
        $values = preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
309
310
        $validator = Validator::make(['param' => $values], [
311
            'param.*' => 'required' . ($validate ? '|regex:' . $validate : ''),
312
        ]);
313
314
        if ($validator->fails()) {
315
            throw new ValidationException($validator, $this->error(
316
                Response::HTTP_BAD_REQUEST,
317
                sprintf('Invalid values for "%s" parameter', $key))
318
            );
319
        }
320
321
        return $values;
322
    }
323
324
    /**
325
     * Validate the requested included relationships against those that are
326
     * allowed on the requested resource type.
327
     *
328
     * @param array|null $relations
329
     *
330
     * @throws InvalidRelationPathException
331
     */
332
    protected function validateIncludableRelations($relations)
333
    {
334
        if (is_null($relations)) {
335
            return;
336
        }
337
338
        foreach ($relations as $relation) {
339
            if (!$this->model instanceof IncludesRelatedResources || !in_array($relation, $this->model->getIncludableRelations())) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 132 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
340
                throw new InvalidRelationPathException($relation);
341
            }
342
        }
343
    }
344
345
    /**
346
     * Update one or more relationships on a model instance.
347
     *
348
     * @param Model $record
349
     * @param array $relationships
350
     */
351
    protected function updateRecordRelationships($record, array $relationships)
352
    {
353
        $relationships = array_intersect_key($relationships, $this->getModelRelationships());
354
355
        foreach ($relationships as $name => $relationship) {
356
            $relation = $this->getModelRelationships()[$name];
357
            $data = $relationship['data'];
358
359
            if ($relation instanceof BelongsTo) {
360
                $record->{$relation->getForeignKey()} = $data['id'];
361
                $record->save();
362
            } else if ($relation instanceof BelongsToMany) {
363
                $record->{$name}()->sync(array_pluck($data, 'id'));
364
            }
365
        }
366
    }
367
}
368