Passed
Push — request-validation ( 81884a...ddd2e2 )
by Alex
02:55
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 Validator;
6
use Huntie\JsonApi\Contracts\Model\IncludesRelatedResources;
7
use Huntie\JsonApi\Exceptions\InvalidRelationPathException;
8
use Huntie\JsonApi\Http\JsonApiResponse;
9
use Huntie\JsonApi\Http\Concerns\QueriesResources;
10
use Huntie\JsonApi\Http\Concerns\UpdatesModelRelations;
11
use Huntie\JsonApi\Serializers\CollectionSerializer;
12
use Huntie\JsonApi\Serializers\RelationshipSerializer;
13
use Huntie\JsonApi\Serializers\ResourceSerializer;
14
use Huntie\JsonApi\Support\JsonApiErrors;
15
use Illuminate\Database\QueryException;
16
use Illuminate\Database\Eloquent\Model;
17
use Illuminate\Database\Eloquent\ModelNotFoundException;
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 UpdatesModelRelations;
30
    use AuthorizesRequests;
31
    use ValidatesRequests;
32
33
    /**
34
     * The Eloquent Model for the resource.
35
     *
36
     * @var Model|string
37
     */
38
    protected $model;
39
40
    /**
41
     * Create a new JsonApiController instance.
42
     */
43
    public function __construct()
44
    {
45
        if (is_string($this->model)) {
46
            if (!is_subclass_of($this->model, Model::class)) {
47
                $this->model = str_finish(config('jsonapi.model_namespace', app()->getNamespace()), '\\')
48
                    . preg_replace('/Controller$/', '', class_basename($this));
49
            }
50
51
            $this->model = new $this->model;
52
        }
53
    }
54
55
    /**
56
     * Return a listing of the resource.
57
     *
58
     * @param Request                                    $request
59
     * @param \Illuminate\Database\Eloquent\Builder|null $query   Custom resource query
60
     *
61
     * @return JsonApiResponse
62
     */
63
    public function indexAction(Request $request, $query = null)
64
    {
65
        $records = $query ?: $this->model->newQuery();
66
        $params = $this->getRequestParameters($request);
67
        $this->validateIncludableRelations($params['include']);
68
69
        $records = $this->sortQuery($records, $params['sort']);
70
        $records = $this->filterQuery($records, $params['filter']);
71
72
        try {
73
            $pageSize = min($this->model->getPerPage(), $request->input('page.size'));
74
            $pageNumber = $request->input('page.number') ?: 1;
75
76
            $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...
77
        } catch (QueryException $e) {
78
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
79
        }
80
81
        return new JsonApiResponse(new CollectionSerializer($records, $params['fields'], $params['include']));
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->model->create((array) $request->input('data.attributes'));
94
95
        if ($relationships = $request->input('data.relationships')) {
96
            $this->updateResourceRelationships($record, (array) $relationships);
97
        }
98
99
        return new JsonApiResponse(new ResourceSerializer($record), Response::HTTP_CREATED);
100
    }
101
102
    /**
103
     * Return a specified record.
104
     *
105
     * @param Request     $request
106
     * @param Model|mixed $record
107
     *
108
     * @return JsonApiResponse
109
     */
110
    public function showAction(Request $request, $record)
111
    {
112
        $record = $this->findModelInstance($record);
113
        $params = $this->getRequestParameters($request);
114
        $this->validateIncludableRelations($params['include']);
115
116
        return new JsonApiResponse(new ResourceSerializer($record, $params['fields'], $params['include']));
117
    }
118
119
    /**
120
     * Update a specified record.
121
     *
122
     * @param Request     $request
123
     * @param Model|mixed $record
124
     *
125
     * @return JsonApiResponse
126
     */
127
    public function updateAction(Request $request, $record)
128
    {
129
        $record = $this->findModelInstance($record);
130
        $record->fill((array) $request->input('data.attributes'));
131
        $record->save();
132
133
        if ($relationships = $request->input('data.relationships')) {
134
            $this->updateRecordRelationships($record, (array) $relationships);
0 ignored issues
show
Documentation Bug introduced by
The method updateRecordRelationships does not exist on object<Huntie\JsonApi\Ht...lers\JsonApiController>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
135
        }
136
137
        return new JsonApiResponse(new ResourceSerializer($record));
138
    }
139
140
    /**
141
     * Destroy a specified record.
142
     *
143
     * @param Request     $request
144
     * @param Model|mixed $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 = $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|mixed $record
161
     * @param string      $relation
162
     *
163
     * @return JsonApiResponse
164
     */
165
    public function showRelationshipAction(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...
166
    {
167
        $record = $this->findModelInstance($record);
168
169
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
170
    }
171
172
    /**
173
     * Update a named relationship on a specified record.
174
     *
175
     * http://jsonapi.org/format/#crud-updating-relationships
176
     *
177
     * @param Request     $request
178
     * @param Model|mixed $record
179
     * @param string      $relation
180
     *
181
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
182
     *
183
     * @return JsonApiResponse
184
     */
185
    public function updateRelationshipAction(Request $request, $record, $relation)
186
    {
187
        $record = $this->findModelInstance($record);
188
        $relationType = $this->getRelationType($relation);
189
190
        abort_unless(is_string($relationType) && $this->isFillableRelation($relation), Response::HTTP_NOT_FOUND);
191
192
        if ($relationType === 'To-One') {
193
            $this->updateToOneResourceRelationship($record, $relation, $request->input('data'));
0 ignored issues
show
Bug introduced by
It seems like $request->input('data') targeting Illuminate\Http\Concerns...ractsWithInput::input() can also be of type string; however, Huntie\JsonApi\Http\Conc...eResourceRelationship() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
194
        } else if ($relationType === 'To-Many') {
195
            $this->updateToManyResourceRelationship($record, $relation, $request->input('data'), $request->method());
0 ignored issues
show
Bug introduced by
It seems like $request->input('data') targeting Illuminate\Http\Concerns...ractsWithInput::input() can also be of type string; however, Huntie\JsonApi\Http\Conc...yResourceRelationship() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
196
        }
197
198
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
199
    }
200
201
    /**
202
     * Return existing instance of the resource or find by primary key.
203
     *
204
     * @param Model|mixed $record
205
     *
206
     * @throws ModelNotFoundException
207
     *
208
     * @return Model
209
     */
210
    protected function findModelInstance($record)
211
    {
212
        if ($record instanceof Model) {
213
            if (is_null($record->getKey())) {
214
                throw new ModelNotFoundException();
215
            }
216
217
            return $record;
218
        }
219
220
        return $this->model->findOrFail($record);
221
    }
222
223
    /**
224
     * Return any JSON API resource parameters from a request.
225
     *
226
     * @param Request $request
227
     *
228
     * @return array
229
     */
230
    protected function getRequestParameters($request)
231
    {
232
        return [
233
            'fields' => $this->getRequestQuerySet($request, 'fields', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
234
            'include' => $this->getRequestQuerySet($request, 'include', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
235
            'sort' => $this->getRequestQuerySet($request, 'sort', '/[A-Za-z_]+/'),
236
            'filter' => (array) $request->input('filter'),
237
        ];
238
    }
239
240
    /**
241
     * Return any comma separated values in a request query field as an array.
242
     *
243
     * @param Request     $request
244
     * @param string      $key
245
     * @param string|null $validate Regular expression to test for each item
246
     *
247
     * @throws \Illuminate\Validation\ValidationException
248
     *
249
     * @return array
250
     */
251
    protected function getRequestQuerySet($request, $key, $validate = null)
252
    {
253
        $values = preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
254
255
        $validator = Validator::make(['param' => $values], [
256
            'param.*' => 'required' . ($validate ? '|regex:' . $validate : ''),
257
        ]);
258
259
        if ($validator->fails()) {
260
            throw new ValidationException($validator, $this->error(
261
                Response::HTTP_BAD_REQUEST,
262
                sprintf('Invalid values for "%s" parameter', $key))
263
            );
264
        }
265
266
        return $values;
267
    }
268
269
    /**
270
     * Validate the requested included relationships against those that are
271
     * allowed on the requested resource type.
272
     *
273
     * @param array|null $relations
274
     *
275
     * @throws InvalidRelationPathException
276
     */
277
    protected function validateIncludableRelations($relations)
278
    {
279
        if (is_null($relations)) {
280
            return;
281
        }
282
283
        foreach ($relations as $relation) {
284
            if (!$this->model instanceof IncludesRelatedResources || !in_array($relation, $this->model->getIncludableRelations())) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 132 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
285
                throw new InvalidRelationPathException($relation);
286
            }
287
        }
288
    }
289
}
290