Passed
Push — relationships ( 489f2c...b6e256 )
by Alex
03:59 queued 01:21
created

JsonApiController::transformCollectionIds()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 11
rs 9.4285
cc 1
eloc 6
nc 1
nop 1
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
     * Update a named many-to-one relationship association on a specified record.
156
     * http://jsonapi.org/format/#crud-updating-to-one-relationships
157
     *
158
     * @param Request     $request
159
     * @param Model|int   $record
160
     * @param string      $relation
161
     * @param string|null $foreignKey
162
     *
163
     * @return JsonApiResponse
164
     */
165
    public function updateToOneRelationshipAction(Request $request, $record, $relation, $foreignKey = null)
166
    {
167
        if (!in_array($relation, $this->getModelRelationships())) {
168
            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...
169
        }
170
171
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
172
        $data = (array) $request->input('data');
173
174
        $record->update([($foreignKey ?: $relation . '_id') => $data['id']]);
175
176
        return new JsonApiResponse();
177
    }
178
179
    /**
180
     * Update named many-to-many relationship entries on a specified record.
181
     * http://jsonapi.org/format/#crud-updating-to-many-relationships
182
     *
183
     * @param Request   $request
184
     * @param Model|int $record
185
     * @param string    $relation
186
     *
187
     * @return JsonApiResponse
188
     */
189
    public function updateToManyRelationshipAction(Request $request, $record, $relation)
190
    {
191
        if (!in_array($relation, $this->getModelRelationships())) {
192
            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...
193
        }
194
195
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
196
        $relationships = (array) $request->input('data');
197
        $items = [];
198
199
        foreach ($relationships as $item) {
200
            $items[$item['id']] = $item['attributes'];
201
        }
202
203
        switch ($request->method()) {
204
            case 'PATCH':
205
                $record->{$relation}()->sync($items);
206
                break;
207
            case 'POST':
208
                $record->{$relation}()->sync($items, false);
209
                break;
210
            case 'DELETE':
211
                $record->{$relation}()->detach(array_keys($items));
212
        }
213
214
        return new JsonApiResponse();
215
    }
216
217
    /**
218
     * Return an instance of the resource by primary key.
219
     *
220
     * @param int $key
221
     *
222
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
223
     *
224
     * @return Model
225
     */
226
    protected function findModelInstance($key)
227
    {
228
        return $this->getModel()->findOrFail($key);
229
    }
230
231
    /**
232
     * Return any JSON API resource parameters from a request.
233
     *
234
     * @param Request $request
235
     *
236
     * @return array
237
     */
238
    protected function getRequestParameters($request)
239
    {
240
        return [
241
            'fields' => $this->getRequestQuerySet($request, 'fields.' . $this->getModelType()),
242
            'include' => $this->getRequestQuerySet($request, 'include'),
243
            'sort' => $this->getRequestQuerySet($request, 'sort'),
244
            'filter' => (array) $request->input('filter'),
245
        ];
246
    }
247
248
    /**
249
     * Return any comma separated values in a request query field as an array.
250
     *
251
     * @param Request $request
252
     * @param string  $key
253
     *
254
     * @return array
255
     */
256
    protected function getRequestQuerySet($request, $key)
257
    {
258
        return preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
259
    }
260
261
    /**
262
     * Transform a model instance into a JSON API object.
263
     *
264
     * @param Model      $record
265
     * @param array|null $fields  Field names of attributes to limit to
266
     * @param array|null $include Relations to include
267
     *
268
     * @return array
269
     */
270
    protected function transformRecord($record, array $fields = [], array $include = [])
271
    {
272
        $relations = array_unique(array_merge($record->getRelations(), $include));
273
        $attributes = $record->load($relations)->toArray();
274
        $relationships = [];
275
        $included = [];
276
277
        foreach ($relations as $relation) {
278
            $relationships[$relation] = [
279
                'data' => []
280
            ];
281
282
            foreach (array_pull($attributes, $relation) as $relatedRecord) {
283
                $relationships[$relation]['data'][] = [
284
                    'type' => $relation,
285
                    'id' => $relatedRecord['id'],
286
                ];
287
288
                if (in_array($relation, $include)) {
289
                    $included[] = [
290
                        'type' => $relation,
291
                        'id' => $relatedRecord['id'],
292
                        'attributes' => array_except($relatedRecord, ['id', 'pivot']),
293
                    ];
294
                }
295
            }
296
        }
297
298
        if (!empty($fields)) {
299
            $attributes = array_only($attributes, $fields);
300
        }
301
302
        $data = array_filter([
303
            'type' => $record->getTable(),
304
            'id' => $record->id,
305
            'attributes' => array_except($attributes, ['id']),
306
            'relationships' => $relationships,
307
        ]);
308
309
        return array_filter(compact('data', 'included'));
310
    }
311
312
    /**
313
     * Transform a set of models into a JSON API collection.
314
     *
315
     * @param \Illuminate\Support\Collection $records
316
     * @param array                          $fields
317
     *
318
     * @return array
319
     */
320
    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...
321
    {
322
        $data = $records->map(function ($record) {
323
            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...
324
        });
325
326
        return compact('data');
327
    }
328
329
    /**
330
     * Transform a set of models into a collection of JSON API resource
331
     * identifier objects.
332
     *
333
     * @param \Illuminate\Support\Collection $records
334
     *
335
     * @return array
336
     */
337
    protected function transformCollectionIds($records)
338
    {
339
        $data = $records->map(function ($record) {
340
            return [
341
                'type' => $record->getTable(),
342
                'id' => $record->id,
343
            ];
344
        });
345
346
        return compact('data');
347
    }
348
349
    /**
350
     * Update one or more relationships on a model instance.
351
     *
352
     * @param Model $record
353
     * @param array $relationships
354
     */
355
    protected function updateRecordRelationships($record, array $relationships)
356
    {
357
        foreach ($relationships as $name => $relationship) {
358
            if (in_array($name, $this->getModelRelationships())) {
359
                $relation = $this->getModelRelationships()[$name];
360
                $data = $relationship['data'];
361
362
                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...
363
                    $record->update([$relation->getForeignKey() => $data['id']]);
364
                } 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...
365
                    $record->{$name}()->sync(array_pluck($data, 'id'));
366
                }
367
            }
368
        }
369
    }
370
}
371