Passed
Push — relationships ( 1f22ce...63580c )
by Alex
02:43
created

updateToManyRelationshipAction()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 25
rs 8.439
cc 6
eloc 17
nc 16
nop 3
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
        foreach ($params['sort'] as $expression) {
59
            $direction = substr($expression, 0, 1) === '-' ? 'desc' : 'asc';
60
            $column = preg_replace('/^\-/', '', $expression);
61
            $records = $records->orderby($column, $direction);
62
        }
63
64
        foreach ($params['filter'] as $attribute => $value) {
65
            if (is_numeric($value)) {
66
                // Exact numeric match
67
                $records = $records->where($attribute, $value);
68
            } else if (in_array(strtolower($value), ['true', 'false'])) {
69
                // Boolean match
70
                $records = $records->where($attribute, filter_var($value, FILTER_VALIDATE_BOOLEAN));
71
            } else {
72
                // Partial string match
73
                $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...
74
            }
75
        }
76
77
        try {
78
            $records = $records->get();
79
        } catch (QueryException $e) {
80
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
81
        }
82
83
        return new JsonApiResponse($this->transformCollection($records, $params['fields']));
84
    }
85
86
    /**
87
     * Store a new record.
88
     *
89
     * @param Request $request
90
     *
91
     * @return JsonApiResponse
92
     */
93
    public function storeAction(Request $request)
94
    {
95
        $record = $this->getModel()->create((array) $request->input('data.attributes'));
96
97
        if ($relationships = $request->input('data.relationships')) {
98
            $this->updateRecordRelationships($record, (array) $relationships);
99
        }
100
101
        return new JsonApiResponse($this->transformRecord($record), Response::HTTP_CREATED);
102
    }
103
104
    /**
105
     * Return a specified record.
106
     *
107
     * @param Request   $request
108
     * @param Model|int $record
109
     *
110
     * @return JsonApiResponse
111
     */
112
    public function showAction(Request $request, $record)
113
    {
114
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
115
        $params = $this->getRequestParameters($request);
116
117
        return new JsonApiResponse($this->transformRecord($record, $params['fields'], $params['include']));
118
    }
119
120
    /**
121
     * Update a specified record.
122
     *
123
     * @param Request   $request
124
     * @param Model|int $record
125
     *
126
     * @return JsonApiResponse
127
     */
128
    public function updateAction(Request $request, $record)
129
    {
130
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
131
        $record->update((array) $request->input('data.attributes'));
132
133
        if ($relationships = $request->input('data.relationships')) {
134
            $this->updateRecordRelationships($record, (array) $relationships);
135
        }
136
137
        return $this->showAction($request, $record);
138
    }
139
140
    /**
141
     * Destroy a specified record.
142
     *
143
     * @param Request   $request
144
     * @param Model|int $record
145
     *
146
     * @return JsonApiResponse
147
     */
148
    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...
149
    {
150
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
151
        $record->delete();
152
153
        return new JsonApiResponse(null, Response::HTTP_NO_CONTENT);
154
    }
155
156
    /**
157
     * Return a specified record relationship.
158
     *
159
     * @param Request   $request
160
     * @param Model|int $record
161
     * @param string    $relation
162
     *
163
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
164
     *
165
     * @return JsonApiResponse
166
     */
167
    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...
168
    {
169
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Reponse::HTTP_NOT_FOUND);
170
171
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
172
        $modelRelation = $this->getModelRelationships()[$relation];
173
174
        if ($modelRelation instanceof BelongsTo) {
175
            $relatedRecord = $record->{$relation};
176
177
            return new JsonApiResponse([
178
                'type' => $relatedRecord->getTable(),
179
                'id' => $relatedRecord->id,
180
            ]);
181
        } else if ($modelRelation instanceof BelongsToMany) {
182
            return new JsonApiResponse($this->transformCollectionIds($record->{$relation}));
183
        }
184
    }
185
186
    /**
187
     * Update a named many-to-one relationship association on a specified record.
188
     * http://jsonapi.org/format/#crud-updating-to-one-relationships
189
     *
190
     * @param Request     $request
191
     * @param Model|int   $record
192
     * @param string      $relation
193
     * @param string|null $foreignKey
194
     *
195
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
196
     *
197
     * @return JsonApiResponse
198
     */
199
    public function updateToOneRelationshipAction(Request $request, $record, $relation, $foreignKey = null)
200
    {
201
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Reponse::HTTP_NOT_FOUND);
202
203
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
204
        $data = (array) $request->input('data');
205
206
        $record->update([($foreignKey ?: $relation . '_id') => $data['id']]);
207
208
        return new JsonApiResponse();
209
    }
210
211
    /**
212
     * Update named many-to-many relationship entries on a specified record.
213
     * http://jsonapi.org/format/#crud-updating-to-many-relationships
214
     *
215
     * @param Request   $request
216
     * @param Model|int $record
217
     * @param string    $relation
218
     *
219
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
220
     *
221
     * @return JsonApiResponse
222
     */
223
    public function updateToManyRelationshipAction(Request $request, $record, $relation)
224
    {
225
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Reponse::HTTP_NOT_FOUND);
226
227
        $record = $record instanceof Model ? $record : $this->findModelInstance($record);
228
        $relationships = (array) $request->input('data');
229
        $items = [];
230
231
        foreach ($relationships as $item) {
232
            $items[$item['id']] = $item['attributes'];
233
        }
234
235
        switch ($request->method()) {
236
            case 'PATCH':
237
                $record->{$relation}()->sync($items);
238
                break;
239
            case 'POST':
240
                $record->{$relation}()->sync($items, false);
241
                break;
242
            case 'DELETE':
243
                $record->{$relation}()->detach(array_keys($items));
244
        }
245
246
        return new JsonApiResponse();
247
    }
248
249
    /**
250
     * Return an instance of the resource by primary key.
251
     *
252
     * @param int $key
253
     *
254
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
255
     *
256
     * @return Model
257
     */
258
    protected function findModelInstance($key)
259
    {
260
        return $this->getModel()->findOrFail($key);
261
    }
262
263
    /**
264
     * Return any JSON API resource parameters from a request.
265
     *
266
     * @param Request $request
267
     *
268
     * @return array
269
     */
270
    protected function getRequestParameters($request)
271
    {
272
        return [
273
            'fields' => $this->getRequestQuerySet($request, 'fields.' . $this->getModelType()),
274
            'include' => $this->getRequestQuerySet($request, 'include'),
275
            'sort' => $this->getRequestQuerySet($request, 'sort'),
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
     *
286
     * @return array
287
     */
288
    protected function getRequestQuerySet($request, $key)
289
    {
290
        return preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
291
    }
292
293
    /**
294
     * Transform a model instance into a JSON API object.
295
     *
296
     * @param Model      $record
297
     * @param array|null $fields  Field names of attributes to limit to
298
     * @param array|null $include Relations to include
299
     *
300
     * @return array
301
     */
302
    protected function transformRecord($record, array $fields = [], array $include = [])
303
    {
304
        $relations = array_unique(array_merge($record->getRelations(), $include));
305
        $attributes = $record->load($relations)->toArray();
306
        $relationships = [];
307
        $included = [];
308
309
        foreach ($relations as $relation) {
310
            $relationships[$relation] = [
311
                'data' => []
312
            ];
313
314
            foreach (array_pull($attributes, $relation) as $relatedRecord) {
315
                $relationships[$relation]['data'][] = [
316
                    'type' => $relation,
317
                    'id' => $relatedRecord['id'],
318
                ];
319
320
                if (in_array($relation, $include)) {
321
                    $included[] = [
322
                        'type' => $relation,
323
                        'id' => $relatedRecord['id'],
324
                        'attributes' => array_except($relatedRecord, ['id', 'pivot']),
325
                    ];
326
                }
327
            }
328
        }
329
330
        if (!empty($fields)) {
331
            $attributes = array_only($attributes, $fields);
332
        }
333
334
        $data = array_filter([
335
            'type' => $record->getTable(),
336
            'id' => $record->id,
337
            'attributes' => array_except($attributes, ['id']),
338
            'relationships' => $relationships,
339
        ]);
340
341
        return array_filter(compact('data', 'included'));
342
    }
343
344
    /**
345
     * Transform a set of models into a JSON API collection.
346
     *
347
     * @param \Illuminate\Support\Collection $records
348
     * @param array                          $fields
349
     *
350
     * @return array
351
     */
352
    protected function transformCollection($records, array $fields = [])
353
    {
354
        $data = $records->map(function ($record) use ($fields) {
355
            return $this->transformRecord($record, $fields)['data'];
356
        });
357
358
        return compact('data');
359
    }
360
361
    /**
362
     * Transform a set of models into a collection of JSON API resource
363
     * identifier objects.
364
     *
365
     * @param \Illuminate\Support\Collection $records
366
     *
367
     * @return array
368
     */
369
    protected function transformCollectionIds($records)
370
    {
371
        $data = $records->map(function ($record) {
372
            return [
373
                'type' => $record->getTable(),
374
                'id' => $record->id,
375
            ];
376
        });
377
378
        return compact('data');
379
    }
380
381
    /**
382
     * Update one or more relationships on a model instance.
383
     *
384
     * @param Model $record
385
     * @param array $relationships
386
     */
387
    protected function updateRecordRelationships($record, array $relationships)
388
    {
389
        foreach ($relationships as $name => $relationship) {
390
            if (array_key_exists($name, $this->getModelRelationships())) {
391
                $relation = $this->getModelRelationships()[$name];
392
                $data = $relationship['data'];
393
394
                if ($relation instanceof BelongsTo) {
395
                    $record->update([$relation->getForeignKey() => $data['id']]);
396
                } else if ($relation instanceof BelongsToMany) {
397
                    $record->{$name}()->sync(array_pluck($data, 'id'));
398
                }
399
            }
400
        }
401
    }
402
}
403