Test Failed
Push — request-validation ( 876935...295ccd )
by Alex
02:34
created

JsonApiController::destroyAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
nc 1
cc 1
eloc 4
nop 2
1
<?php
2
3
namespace Huntie\JsonApi\Http\Controllers;
4
5
use Schema;
6
use Validator;
7
use Huntie\JsonApi\Contracts\Model\IncludesRelatedResources;
8
use Huntie\JsonApi\Exceptions\InvalidRelationPathException;
9
use Huntie\JsonApi\Http\JsonApiResponse;
10
use Huntie\JsonApi\Serializers\CollectionSerializer;
11
use Huntie\JsonApi\Serializers\RelationshipSerializer;
12
use Huntie\JsonApi\Serializers\ResourceSerializer;
13
use Huntie\JsonApi\Support\JsonApiErrors;
14
use Illuminate\Database\QueryException;
15
use Illuminate\Database\Eloquent\Model;
16
use Illuminate\Database\Eloquent\ModelNotFoundException;
17
use Illuminate\Database\Eloquent\Relations\BelongsTo;
18
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
19
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
20
use Illuminate\Foundation\Validation\ValidatesRequests;
21
use Illuminate\Http\Request;
22
use Illuminate\Http\Response;
23
use Illuminate\Routing\Controller;
24
use Illuminate\Validation\ValidationException;
25
26
abstract class JsonApiController extends Controller
27
{
28
    use JsonApiErrors;
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']));
0 ignored issues
show
Documentation introduced by
$records is of type object<Illuminate\Contra...n\LengthAwarePaginator>, but the function expects a object<Illuminate\Suppor...n\LengthAwarePaginator>.

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...
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'));
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|int $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|int $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|int $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|int $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|int   $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|int $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|int $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
     * Sort a resource query by one or more attributes.
331
     *
332
     * @param \Illuminate\Database\Eloquent\Builder $query
333
     * @param array                                 $attributes
334
     *
335
     * @return \Illuminate\Database\Eloquent\Builder
336
     */
337
    protected function sortQuery($query, $attributes)
338
    {
339
        foreach ($attributes as $expression) {
340
            $direction = substr($expression, 0, 1) === '-' ? 'desc' : 'asc';
341
            $column = preg_replace('/^\-/', '', $expression);
342
            $query = $query->orderBy($column, $direction);
343
        }
344
345
        return $query;
346
    }
347
348
    /**
349
     * Filter a resource query by one or more attributes.
350
     *
351
     * @param \Illuminate\Database\Eloquent\Builder $query
352
     * @param array                                 $attributes
353
     *
354
     * @return \Illuminate\Database\Eloquent\Builder
355
     */
356
    protected function filterQuery($query, $attributes)
357
    {
358
        $searchableColumns = array_diff(
359
            Schema::getColumnListing($this->getModel()->getTable()),
360
            $this->getModel()->getHidden()
361
        );
362
363
        foreach (array_intersect_key($attributes, array_flip($searchableColumns)) as $column => $value) {
364
            if (is_numeric($value)) {
365
                // Exact numeric match
366
                $query = $query->where($column, $value);
367
            } else if (in_array(strtolower($value), ['true', 'false'])) {
368
                // Boolean match
369
                $query = $query->where($column, filter_var($value, FILTER_VALIDATE_BOOLEAN));
370
            } else {
371
                // Partial string match
372
                $query = $query->where($column, 'LIKE', '%' . $value . '%');
373
            }
374
        }
375
376
        return $query;
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
        $relationships = array_intersect_key($relationships, $this->getModelRelationships());
388
389
        foreach ($relationships as $name => $relationship) {
390
            $relation = $this->getModelRelationships()[$name];
391
            $data = $relationship['data'];
392
393
            if ($relation instanceof BelongsTo) {
394
                $record->{$relation->getForeignKey()} = $data['id'];
395
                $record->save();
396
            } else if ($relation instanceof BelongsToMany) {
397
                $record->{$name}()->sync(array_pluck($data, 'id'));
398
            }
399
        }
400
    }
401
}
402