Passed
Push — request-validation ( a92c02...eee8a4 )
by Alex
02:54
created

JsonApiController::getRequestQuerySet()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 3
eloc 9
nc 2
nop 3
1
<?php
2
3
namespace Huntie\JsonApi\Http\Controllers;
4
5
use Huntie\JsonApi\Contracts\Model\IncludesRelatedResources;
6
use Huntie\JsonApi\Exceptions\InvalidRelationPathException;
7
use Huntie\JsonApi\Http\JsonApiResponse;
8
use Huntie\JsonApi\Http\Concerns\QueriesResources;
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 QueriesResources;
29
    use AuthorizesRequests;
30
    use ValidatesRequests;
31
32
    /**
33
     * Return the Eloquent Model for the resource.
34
     *
35
     * @return Model
36
     */
37
    abstract protected function getModel();
38
39
    /**
40
     * The model relationships that can be updated.
41
     *
42
     * @return array
43
     */
44
    protected function getModelRelationships()
45
    {
46
        return [];
47
    }
48
49
    /**
50
     * Return a listing of the resource.
51
     *
52
     * @param Request                                    $request
53
     * @param \Illuminate\Database\Eloquent\Builder|null $query   Custom resource query
54
     *
55
     * @return JsonApiResponse
56
     */
57
    public function indexAction(Request $request, $query = null)
58
    {
59
        $records = $query ?: $this->getModel()->newQuery();
60
        $params = $this->getRequestParameters($request);
61
        $this->validateIncludableRelations($params['include']);
62
63
        $records = $this->sortQuery($records, $params['sort']);
64
        $records = $this->filterQuery($records, $params['filter']);
65
66
        try {
67
            $pageSize = min($this->getModel()->getPerPage(), $request->input('page.size'));
68
            $pageNumber = $request->input('page.number') ?: 1;
69
70
            $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...
71
        } catch (QueryException $e) {
72
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
73
        }
74
75
        return new JsonApiResponse(new CollectionSerializer($records, $params['fields'], $params['include']));
76
    }
77
78
    /**
79
     * Store a new record.
80
     *
81
     * @param Request $request
82
     *
83
     * @return JsonApiResponse
84
     */
85
    public function storeAction(Request $request)
86
    {
87
        $record = $this->getModel()->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...
88
89
        if ($relationships = $request->input('data.relationships')) {
90
            $this->updateRecordRelationships($record, (array) $relationships);
91
        }
92
93
        return new JsonApiResponse(new ResourceSerializer($record), Response::HTTP_CREATED);
94
    }
95
96
    /**
97
     * Return a specified record.
98
     *
99
     * @param Request     $request
100
     * @param Model|mixed $record
101
     *
102
     * @return JsonApiResponse
103
     */
104
    public function showAction(Request $request, $record)
105
    {
106
        $record = $this->findModelInstance($record);
107
        $params = $this->getRequestParameters($request);
108
        $this->validateIncludableRelations($params['include']);
109
110
        return new JsonApiResponse(new ResourceSerializer($record, $params['fields'], $params['include']));
111
    }
112
113
    /**
114
     * Update a specified record.
115
     *
116
     * @param Request     $request
117
     * @param Model|mixed $record
118
     *
119
     * @return JsonApiResponse
120
     */
121
    public function updateAction(Request $request, $record)
122
    {
123
        $record = $this->findModelInstance($record);
124
        $record->fill((array) $request->input('data.attributes'));
125
        $record->save();
126
127
        if ($relationships = $request->input('data.relationships')) {
128
            $this->updateRecordRelationships($record, (array) $relationships);
129
        }
130
131
        return new JsonApiResponse(new ResourceSerializer($record));
132
    }
133
134
    /**
135
     * Destroy a specified record.
136
     *
137
     * @param Request     $request
138
     * @param Model|mixed $record
139
     *
140
     * @return JsonApiResponse
141
     */
142
    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...
143
    {
144
        $record = $this->findModelInstance($record);
145
        $record->delete();
146
147
        return new JsonApiResponse(null, Response::HTTP_NO_CONTENT);
148
    }
149
150
    /**
151
     * Return a specified record relationship.
152
     *
153
     * @param Request     $request
154
     * @param Model|mixed $record
155
     * @param string      $relation
156
     *
157
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
158
     *
159
     * @return JsonApiResponse
160
     */
161
    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...
162
    {
163
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
164
165
        $record = $this->findModelInstance($record);
166
167
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
168
    }
169
170
    /**
171
     * Update a named many-to-one relationship association on a specified record.
172
     * http://jsonapi.org/format/#crud-updating-to-one-relationships
173
     *
174
     * @param Request     $request
175
     * @param Model|mixed $record
176
     * @param string      $relation
177
     *
178
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
179
     *
180
     * @return JsonApiResponse
181
     */
182
    public function updateToOneRelationshipAction(Request $request, $record, $relation)
183
    {
184
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
185
186
        $record = $this->findModelInstance($record);
187
        $relation = $this->getModelRelationships()[$relation];
188
        $data = (array) $request->input('data');
189
190
        $record->{$relation->getForeignKey()} = $data['id'];
191
        $record->save();
192
193
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
194
    }
195
196
    /**
197
     * Update named many-to-many relationship entries on a specified record.
198
     * http://jsonapi.org/format/#crud-updating-to-many-relationships
199
     *
200
     * @param Request     $request
201
     * @param Model|mixed $record
202
     * @param string      $relation
203
     *
204
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
205
     *
206
     * @return JsonApiResponse
207
     */
208
    public function updateToManyRelationshipAction(Request $request, $record, $relation)
209
    {
210
        abort_if(!array_key_exists($relation, $this->getModelRelationships()), Response::HTTP_NOT_FOUND);
211
212
        $record = $this->findModelInstance($record);
213
        $relationships = (array) $request->input('data');
214
        $items = [];
215
216
        foreach ($relationships as $item) {
217
            if (isset($item['attributes'])) {
218
                $items[$item['id']] = $item['attributes'];
219
            } else {
220
                $items[] = $item['id'];
221
            }
222
        }
223
224
        switch ($request->method()) {
225
            case 'PATCH':
226
                $record->{$relation}()->sync($items);
227
                break;
228
            case 'POST':
229
                $record->{$relation}()->sync($items, false);
230
                break;
231
            case 'DELETE':
232
                $record->{$relation}()->detach(array_keys($items));
233
        }
234
235
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
236
    }
237
238
    /**
239
     * Return existing instance of the resource or find by primary key.
240
     *
241
     * @param Model|mixed $record
242
     *
243
     * @throws ModelNotFoundException
244
     *
245
     * @return Model
246
     */
247
    protected function findModelInstance($record)
248
    {
249
        if ($record instanceof Model) {
250
            if (is_null($record->getKey())) {
251
                throw new ModelNotFoundException();
252
            }
253
254
            return $record;
255
        }
256
257
        return $this->getModel()->findOrFail($record);
258
    }
259
260
    /**
261
     * Return any JSON API resource parameters from a request.
262
     *
263
     * @param Request $request
264
     *
265
     * @return array
266
     */
267
    protected function getRequestParameters($request)
268
    {
269
        return [
270
            'fields' => $this->getRequestQuerySet($request, 'fields', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
271
            'include' => $this->getRequestQuerySet($request, 'include', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
272
            'sort' => $this->getRequestQuerySet($request, 'sort', '/[A-Za-z_]+/'),
273
            'filter' => (array) $request->input('filter'),
274
        ];
275
    }
276
277
    /**
278
     * Return any comma separated values in a request query field as an array.
279
     *
280
     * @param Request     $request
281
     * @param string      $key
282
     * @param string|null $validate Regular expression to test for each item
283
     *
284
     * @throws \Illuminate\Validation\ValidationException
285
     *
286
     * @return array
287
     */
288
    protected function getRequestQuerySet($request, $key, $validate = null)
289
    {
290
        $values = preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
291
292
        $validator = Validator::make(['param' => $values], [
293
            'param.*' => 'required' . ($validate ? '|regex:' . $validate : ''),
294
        ]);
295
296
        if ($validator->fails()) {
297
            throw new ValidationException($validator, $this->error(
298
                Response::HTTP_BAD_REQUEST,
299
                sprintf('Invalid values for "%s" parameter', $key))
300
            );
301
        }
302
303
        return $values;
304
    }
305
306
    /**
307
     * Validate the requested included relationships against those that are
308
     * allowed on the requested resource type.
309
     *
310
     * @param array|null $relations
311
     *
312
     * @throws InvalidRelationPathException
313
     */
314
    protected function validateIncludableRelations($relations)
315
    {
316
        if (is_null($relations)) {
317
            return;
318
        }
319
320
        $model = $this->getModel();
321
322
        foreach ($relations as $relation) {
323
            if (!$model instanceof IncludesRelatedResources || !in_array($relation, $model->getIncludableRelations())) {
324
                throw new InvalidRelationPathException($relation);
325
            }
326
        }
327
    }
328
329
    /**
330
     * Update one or more relationships on a model instance.
331
     *
332
     * @param Model $record
333
     * @param array $relationships
334
     */
335
    protected function updateRecordRelationships($record, array $relationships)
336
    {
337
        $relationships = array_intersect_key($relationships, $this->getModelRelationships());
338
339
        foreach ($relationships as $name => $relationship) {
340
            $relation = $this->getModelRelationships()[$name];
341
            $data = $relationship['data'];
342
343
            if ($relation instanceof BelongsTo) {
344
                $record->{$relation->getForeignKey()} = $data['id'];
345
                $record->save();
346
            } else if ($relation instanceof BelongsToMany) {
347
                $record->{$name}()->sync(array_pluck($data, 'id'));
348
            }
349
        }
350
    }
351
}
352