Completed
Push — 1.0.0-alpha ( c04b09...2436c2 )
by Alex
11s
created

src/Http/Controllers/JsonApiController.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\HttpException;
8
use Huntie\JsonApi\Exceptions\InvalidRelationPathException;
9
use Huntie\JsonApi\Http\JsonApiResponse;
10
use Huntie\JsonApi\Http\Concerns\QueriesResources;
11
use Huntie\JsonApi\Http\Concerns\UpdatesModelRelations;
12
use Huntie\JsonApi\Serializers\CollectionSerializer;
13
use Huntie\JsonApi\Serializers\RelationshipSerializer;
14
use Huntie\JsonApi\Serializers\ResourceSerializer;
15
use Huntie\JsonApi\Support\JsonApiErrors;
16
use Illuminate\Database\QueryException;
17
use Illuminate\Database\Eloquent\Model;
18
use Illuminate\Database\Eloquent\ModelNotFoundException;
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 QueriesResources;
30
    use UpdatesModelRelations;
31
    use AuthorizesRequests;
32
    use ValidatesRequests;
33
34
    /**
35
     * The Eloquent Model for the resource.
36
     *
37
     * @var Model|string
38
     */
39
    protected $model;
40
41
    /**
42
     * Create a new JsonApiController instance.
43
     */
44
    public function __construct()
45
    {
46
        if (is_null($this->model) || is_string($this->model)) {
47
            if (!is_subclass_of($this->model, Model::class)) {
48
                $this->model = str_finish(config('jsonapi.model_namespace', app()->getNamespace()), '\\')
49
                    . preg_replace('/Controller$/', '', class_basename($this));
50
            }
51
52
            $this->model = new $this->model;
53
        }
54
    }
55
56
    /**
57
     * Return a listing of the resource.
58
     *
59
     * @param Request                                    $request
60
     * @param \Illuminate\Database\Eloquent\Builder|null $query   Custom resource query
61
     *
62
     * @return JsonApiResponse
63
     */
64
    public function indexAction(Request $request, $query = null)
65
    {
66
        $records = $query ?: $this->model->newQuery();
67
        $params = $this->getRequestParameters($request);
68
        $this->validateIncludableRelations($params['include']);
69
70
        try {
71
            $records = $this->sortQuery($records, $params['sort']);
72
            $records = $this->filterQuery($records, $params['filter']);
73
            $page = $this->resolvePaginationParameters($request);
74
75
            $records = $records->paginate($page['size'], null, null, $page['number']);
0 ignored issues
show
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...
76
        } catch (QueryException $e) {
77
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
78
        }
79
80
        return new JsonApiResponse(new CollectionSerializer($records, $params['fields'], $params['include']));
81
    }
82
83
    /**
84
     * Store a new record.
85
     *
86
     * @param Request $request
87
     *
88
     * @return JsonApiResponse
89
     */
90
    public function storeAction(Request $request)
91
    {
92
        $record = $this->model->create((array) $request->input('data.attributes'));
93
94
        if ($relationships = $request->input('data.relationships')) {
95
            $this->updateResourceRelationships($record, (array) $relationships);
96
        }
97
98
        return new JsonApiResponse(new ResourceSerializer($record), Response::HTTP_CREATED);
99
    }
100
101
    /**
102
     * Return a specified record.
103
     *
104
     * @param Request     $request
105
     * @param Model|mixed $record
106
     *
107
     * @return JsonApiResponse
108
     */
109
    public function showAction(Request $request, $record)
110
    {
111
        $record = $this->findModelInstance($record);
112
        $params = $this->getRequestParameters($request);
113
        $this->validateIncludableRelations($params['include']);
114
115
        return new JsonApiResponse(new ResourceSerializer($record, $params['fields'], $params['include']));
116
    }
117
118
    /**
119
     * Update a specified record.
120
     *
121
     * @param Request     $request
122
     * @param Model|mixed $record
123
     *
124
     * @return JsonApiResponse
125
     */
126
    public function updateAction(Request $request, $record)
127
    {
128
        $record = $this->findModelInstance($record);
129
        $record->fill((array) $request->input('data.attributes'));
130
        $record->save();
131
132
        if ($relationships = $request->input('data.relationships')) {
133
            $this->updateResourceRelationships($record, (array) $relationships);
134
        }
135
136
        return new JsonApiResponse(new ResourceSerializer($record));
137
    }
138
139
    /**
140
     * Destroy a specified record.
141
     *
142
     * @param Request     $request
143
     * @param Model|mixed $record
144
     *
145
     * @return JsonApiResponse
146
     */
147
    public function destroyAction(Request $request, $record)
0 ignored issues
show
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...
148
    {
149
        $record = $this->findModelInstance($record);
150
        $record->delete();
151
152
        return new JsonApiResponse(null, Response::HTTP_NO_CONTENT);
153
    }
154
155
    /**
156
     * Return a specified record relationship.
157
     *
158
     * @param Request     $request
159
     * @param Model|mixed $record
160
     * @param string      $relation
161
     *
162
     * @return JsonApiResponse
163
     */
164
    public function showRelationshipAction(Request $request, $record, $relation)
0 ignored issues
show
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...
165
    {
166
        $record = $this->findModelInstance($record);
167
168
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
169
    }
170
171
    /**
172
     * Update a named relationship on a specified record.
173
     *
174
     * http://jsonapi.org/format/#crud-updating-relationships
175
     *
176
     * @param Request     $request
177
     * @param Model|mixed $record
178
     * @param string      $relation
179
     *
180
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
181
     *
182
     * @return JsonApiResponse
183
     */
184
    public function updateRelationshipAction(Request $request, $record, $relation)
185
    {
186
        $relationType = $this->getRelationType($relation);
187
        abort_unless(is_string($relationType) && $this->isFillableRelation($relation), Response::HTTP_NOT_FOUND);
188
189
        $record = $this->findModelInstance($record);
190
        $data = (array) $request->input('data');
191
192
        if ($relationType === 'To-One') {
193
            $this->updateToOneResourceRelationship($record, $relation, $data);
194
        } else if ($relationType === 'To-Many') {
195
            $this->updateToManyResourceRelationship($record, $relation, $data, $request->method());
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
        $enableIncluded = config('jsonapi.enable_included_resources');
233
234
        if ($request->has('include') && is_bool($enableIncluded) && !$enableIncluded) {
235
            throw new HttpException('Inclusion of related resources is not supported');
236
        }
237
238
        return [
239
            'fields' => $this->getRequestQuerySet($request, 'fields', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
240
            'include' => $this->getRequestQuerySet($request, 'include', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
241
            'sort' => $this->getRequestQuerySet($request, 'sort', '/[A-Za-z_]+/'),
242
            'filter' => (array) json_decode(json_encode($request->input('filter'))),
243
        ];
244
    }
245
246
    /**
247
     * Return any comma separated values in a request query field as an array.
248
     *
249
     * @param Request     $request
250
     * @param string      $key
251
     * @param string|null $validate Regular expression to test for each item
252
     *
253
     * @throws \Illuminate\Validation\ValidationException
254
     *
255
     * @return array
256
     */
257
    protected function getRequestQuerySet($request, $key, $validate = null)
258
    {
259
        $values = preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
260
261
        $validator = Validator::make(['param' => $values], [
262
            'param.*' => 'required' . ($validate ? '|regex:' . $validate : ''),
263
        ]);
264
265
        if ($validator->fails()) {
266
            throw new ValidationException($validator, $this->error(
267
                Response::HTTP_BAD_REQUEST,
268
                sprintf('Invalid values for "%s" parameter', $key))
269
            );
270
        }
271
272
        return $values;
273
    }
274
275
    /**
276
     * Validate the requested included relationships against those that are
277
     * allowed on the requested resource type.
278
     *
279
     * @param array|null $relations
280
     *
281
     * @throws InvalidRelationPathException
282
     */
283
    protected function validateIncludableRelations($relations)
284
    {
285
        if (is_null($relations)) {
286
            return;
287
        }
288
289
        foreach ($relations as $relation) {
290
            if (!$this->model instanceof IncludesRelatedResources || !in_array($relation, $this->model->getIncludableRelations())) {
0 ignored issues
show
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...
291
                throw new InvalidRelationPathException($relation);
292
            }
293
        }
294
    }
295
296
    /**
297
     * Return the page number and page size to use for paginated results.
298
     *
299
     * @param \Illuminate\Http\Request $request
300
     */
301
    protected function resolvePaginationParameters($request): array
302
    {
303
        return [
304
            'number' => $request->input('page.number', $request->input('page.offset', 0) / 10 + 1),
305
            'size' => $request->input('page.size', $request->input('page.limit', $this->model->getPerPage())),
306
        ];
307
    }
308
}
309