Passed
Push — relationships ( b6e256...1f22ce )
by Alex
03:30 queued 01:11
created

JsonApiController   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 12
Bugs 0 Features 10
Metric Value
wmc 49
c 12
b 0
f 10
lcom 1
cbo 6
dl 0
loc 388
rs 8.5454

18 Methods

Rating   Name   Duplication   Size   Complexity  
getModel() 0 1 ?
A getModelType() 0 4 1
A getModelRelationships() 0 4 1
C indexAction() 0 32 7
A storeAction() 0 10 2
A showAction() 0 7 2
A updateAction() 0 11 3
A destroyAction() 0 7 2
B relationshipAction() 0 20 5
A updateToOneRelationshipAction() 0 13 4
C updateToManyRelationshipAction() 0 27 7
A findModelInstance() 0 4 1
A getRequestParameters() 0 9 1
A getRequestQuerySet() 0 4 1
B transformRecord() 0 41 5
A transformCollection() 0 8 1
A transformCollectionIds() 0 11 1
B updateRecordRelationships() 0 15 5

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 Huntie\JsonApi\Http\JsonApiResponse;
6
use Huntie\JsonApi\Support\JsonApiErrors;
7
use Illuminate\Database\QueryException;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Http\Request;
10
use Illuminate\Http\Response;
11
use Illuminate\Routing\Controller;
12
13
abstract class JsonApiController extends Controller
14
{
15
    use JsonApiErrors;
16
17
    /**
18
     * Return the Eloquent Model for the resource.
19
     *
20
     * @return Model
21
     */
22
    abstract protected function getModel();
23
24
    /**
25
     * Return the type name of the resource.
26
     *
27
     * @return string
28
     */
29
    protected function getModelType()
30
    {
31
        return $this->getModel()->getTable();
32
    }
33
34
    /**
35
     * The model relationships that can be updated.
36
     *
37
     * @return string
38
     */
39
    protected function getModelRelationships()
40
    {
41
        return [];
42
    }
43
44
    /**
45
     * Return a listing of the resource.
46
     *
47
     * @param Request $request
48
     *
49
     * @return JsonApiResponse
50
     */
51
    public function indexAction(Request $request)
52
    {
53
        $records = $this->getModel()->newQuery();
54
        $params = $this->getRequestParameters($request);
55
56
        foreach ($params['sort'] as $expression) {
57
            $direction = substr($expression, 0, 1) === '-' ? 'desc' : 'asc';
58
            $column = preg_replace('/^\-/', '', $expression);
59
            $records = $records->orderby($column, $direction);
60
        }
61
62
        foreach ($params['filter'] as $attribute => $value) {
63
            if (is_numeric($value)) {
64
                // Exact numeric match
65
                $records = $records->where($attribute, $value);
66
            } else if (in_array(strtolower($value), ['true', 'false'])) {
67
                // Boolean match
68
                $records = $records->where($attribute, filter_var($value, FILTER_VALIDATE_BOOLEAN));
69
            } else {
70
                // Partial string match
71
                $records = $records->where($attribute, 'like', "%$value%");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $value instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
72
            }
73
        }
74
75
        try {
76
            $records = $records->get();
77
        } catch (QueryException $e) {
78
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
79
        }
80
81
        return new JsonApiResponse($this->transformCollection($records, $params['fields']));
82
    }
83
84
    /**
85
     * Store a new record.
86
     *
87
     * @param Request $request
88
     *
89
     * @return JsonApiResponse
90
     */
91
    public function storeAction(Request $request)
92
    {
93
        $record = $this->getModel()->create((array) $request->input('data.attributes'));
94
95
        if ($relationships = $request->input('data.relationships')) {
96
            $this->updateRecordRelationships($record, (array) $relationships);
97
        }
98
99
        return new JsonApiResponse($this->transformRecord($record), Response::HTTP_CREATED);
100
    }
101
102
    /**
103
     * Return a specified record.
104
     *
105
     * @param Request   $request
106
     * @param Model|int $record
107
     *
108
     * @return JsonApiResponse
109
     */
110
    public function showAction(Request $request, $record)
111
    {
112
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
113
        $params = $this->getRequestParameters($request);
114
115
        return new JsonApiResponse($this->transformRecord($record, $params['fields'], $params['include']));
116
    }
117
118
    /**
119
     * Update a specified record.
120
     *
121
     * @param Request   $request
122
     * @param Model|int $record
123
     *
124
     * @return JsonApiResponse
125
     */
126
    public function updateAction(Request $request, $record)
127
    {
128
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
129
        $record->update((array) $request->input('data.attributes'));
130
131
        if ($relationships = $request->input('data.relationships')) {
132
            $this->updateRecordRelationships($record, (array) $relationships);
133
        }
134
135
        return $this->showAction($request, $record);
136
    }
137
138
    /**
139
     * Destroy a specified record.
140
     *
141
     * @param Request   $request
142
     * @param Model|int $record
143
     *
144
     * @return JsonApiResponse
145
     */
146
    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...
147
    {
148
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
149
        $record->delete();
150
151
        return new JsonApiResponse(null, Response::HTTP_NO_CONTENT);
152
    }
153
154
    /**
155
     * Return a specified record relationship.
156
     *
157
     * @param Request   $request
158
     * @param Model|int $record
159
     * @param string    $relation
160
     *
161
     * @return JsonApiResponse
162
     */
163
    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...
164
    {
165
        if (!in_array($relation, $this->getModelRelationships())) {
166
            return $this->error(Reponse::HTTP_NOT_FOUND);
0 ignored issues
show
Bug introduced by
The call to error() misses a required argument $title.

This check looks for function calls that miss required arguments.

Loading history...
167
        }
168
169
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
170
        $modelRelation = $this->getModelRelationships()[$relation];
171
172
        if ($modelRelation instanceof BelongsTo) {
0 ignored issues
show
Bug introduced by
The class Huntie\JsonApi\Http\Controllers\BelongsTo does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
173
            $relatedRecord = $record->{$relation};
174
175
            return new JsonApiResponse([
176
                'type' => $relatedRecord->getTable(),
177
                'id' => $relatedRecord->id,
178
            ]);
179
        } else if ($modelRelation instanceof BelongsToMany) {
0 ignored issues
show
Bug introduced by
The class Huntie\JsonApi\Http\Controllers\BelongsToMany does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
180
            return new JsonApiResponse($this->transformCollectionIds($record->{$relation}));
181
        }
182
    }
183
184
    /**
185
     * Update a named many-to-one relationship association on a specified record.
186
     * http://jsonapi.org/format/#crud-updating-to-one-relationships
187
     *
188
     * @param Request     $request
189
     * @param Model|int   $record
190
     * @param string      $relation
191
     * @param string|null $foreignKey
192
     *
193
     * @return JsonApiResponse
194
     */
195
    public function updateToOneRelationshipAction(Request $request, $record, $relation, $foreignKey = null)
196
    {
197
        if (!in_array($relation, $this->getModelRelationships())) {
198
            return $this->error(Reponse::HTTP_NOT_FOUND);
0 ignored issues
show
Bug introduced by
The call to error() misses a required argument $title.

This check looks for function calls that miss required arguments.

Loading history...
199
        }
200
201
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
202
        $data = (array) $request->input('data');
203
204
        $record->update([($foreignKey ?: $relation . '_id') => $data['id']]);
205
206
        return new JsonApiResponse();
207
    }
208
209
    /**
210
     * Update named many-to-many relationship entries on a specified record.
211
     * http://jsonapi.org/format/#crud-updating-to-many-relationships
212
     *
213
     * @param Request   $request
214
     * @param Model|int $record
215
     * @param string    $relation
216
     *
217
     * @return JsonApiResponse
218
     */
219
    public function updateToManyRelationshipAction(Request $request, $record, $relation)
220
    {
221
        if (!in_array($relation, $this->getModelRelationships())) {
222
            return $this->error(Reponse::HTTP_NOT_FOUND);
0 ignored issues
show
Bug introduced by
The call to error() misses a required argument $title.

This check looks for function calls that miss required arguments.

Loading history...
223
        }
224
225
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
226
        $relationships = (array) $request->input('data');
227
        $items = [];
228
229
        foreach ($relationships as $item) {
230
            $items[$item['id']] = $item['attributes'];
231
        }
232
233
        switch ($request->method()) {
234
            case 'PATCH':
235
                $record->{$relation}()->sync($items);
236
                break;
237
            case 'POST':
238
                $record->{$relation}()->sync($items, false);
239
                break;
240
            case 'DELETE':
241
                $record->{$relation}()->detach(array_keys($items));
242
        }
243
244
        return new JsonApiResponse();
245
    }
246
247
    /**
248
     * Return an instance of the resource by primary key.
249
     *
250
     * @param int $key
251
     *
252
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
253
     *
254
     * @return Model
255
     */
256
    protected function findModelInstance($key)
257
    {
258
        return $this->getModel()->findOrFail($key);
259
    }
260
261
    /**
262
     * Return any JSON API resource parameters from a request.
263
     *
264
     * @param Request $request
265
     *
266
     * @return array
267
     */
268
    protected function getRequestParameters($request)
269
    {
270
        return [
271
            'fields' => $this->getRequestQuerySet($request, 'fields.' . $this->getModelType()),
272
            'include' => $this->getRequestQuerySet($request, 'include'),
273
            'sort' => $this->getRequestQuerySet($request, 'sort'),
274
            'filter' => (array) $request->input('filter'),
275
        ];
276
    }
277
278
    /**
279
     * Return any comma separated values in a request query field as an array.
280
     *
281
     * @param Request $request
282
     * @param string  $key
283
     *
284
     * @return array
285
     */
286
    protected function getRequestQuerySet($request, $key)
287
    {
288
        return preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
289
    }
290
291
    /**
292
     * Transform a model instance into a JSON API object.
293
     *
294
     * @param Model      $record
295
     * @param array|null $fields  Field names of attributes to limit to
296
     * @param array|null $include Relations to include
297
     *
298
     * @return array
299
     */
300
    protected function transformRecord($record, array $fields = [], array $include = [])
301
    {
302
        $relations = array_unique(array_merge($record->getRelations(), $include));
303
        $attributes = $record->load($relations)->toArray();
304
        $relationships = [];
305
        $included = [];
306
307
        foreach ($relations as $relation) {
308
            $relationships[$relation] = [
309
                'data' => []
310
            ];
311
312
            foreach (array_pull($attributes, $relation) as $relatedRecord) {
313
                $relationships[$relation]['data'][] = [
314
                    'type' => $relation,
315
                    'id' => $relatedRecord['id'],
316
                ];
317
318
                if (in_array($relation, $include)) {
319
                    $included[] = [
320
                        'type' => $relation,
321
                        'id' => $relatedRecord['id'],
322
                        'attributes' => array_except($relatedRecord, ['id', 'pivot']),
323
                    ];
324
                }
325
            }
326
        }
327
328
        if (!empty($fields)) {
329
            $attributes = array_only($attributes, $fields);
330
        }
331
332
        $data = array_filter([
333
            'type' => $record->getTable(),
334
            'id' => $record->id,
335
            'attributes' => array_except($attributes, ['id']),
336
            'relationships' => $relationships,
337
        ]);
338
339
        return array_filter(compact('data', 'included'));
340
    }
341
342
    /**
343
     * Transform a set of models into a JSON API collection.
344
     *
345
     * @param \Illuminate\Support\Collection $records
346
     * @param array                          $fields
347
     *
348
     * @return array
349
     */
350
    protected function transformCollection($records, array $fields = [])
0 ignored issues
show
Unused Code introduced by
The parameter $fields 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...
351
    {
352
        $data = $records->map(function ($record) {
353
            return $this->transformRecord($record, $fields)['data'];
0 ignored issues
show
Bug introduced by
The variable $fields does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
354
        });
355
356
        return compact('data');
357
    }
358
359
    /**
360
     * Transform a set of models into a collection of JSON API resource
361
     * identifier objects.
362
     *
363
     * @param \Illuminate\Support\Collection $records
364
     *
365
     * @return array
366
     */
367
    protected function transformCollectionIds($records)
368
    {
369
        $data = $records->map(function ($record) {
370
            return [
371
                'type' => $record->getTable(),
372
                'id' => $record->id,
373
            ];
374
        });
375
376
        return compact('data');
377
    }
378
379
    /**
380
     * Update one or more relationships on a model instance.
381
     *
382
     * @param Model $record
383
     * @param array $relationships
384
     */
385
    protected function updateRecordRelationships($record, array $relationships)
386
    {
387
        foreach ($relationships as $name => $relationship) {
388
            if (in_array($name, $this->getModelRelationships())) {
389
                $relation = $this->getModelRelationships()[$name];
390
                $data = $relationship['data'];
391
392
                if ($relation instanceof BelongsTo) {
0 ignored issues
show
Bug introduced by
The class Huntie\JsonApi\Http\Controllers\BelongsTo does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
393
                    $record->update([$relation->getForeignKey() => $data['id']]);
394
                } else if ($relation instanceof BelongsToMany) {
0 ignored issues
show
Bug introduced by
The class Huntie\JsonApi\Http\Controllers\BelongsToMany does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
395
                    $record->{$name}()->sync(array_pluck($data, 'id'));
396
                }
397
            }
398
        }
399
    }
400
}
401