Passed
Pull Request — 1.0.0-release (#16)
by Alex
02:46
created

JsonApiController::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 0
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_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
        $records = $this->sortQuery($records, $params['sort']);
71
        $records = $this->filterQuery($records, $params['filter']);
72
73
        try {
74
            $pageSize = min($this->model->getPerPage(), $request->input('page.size'));
75
            $pageNumber = $request->input('page.number') ?: 1;
76
77
            $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...
78
        } catch (QueryException $e) {
79
            return $this->error(Response::HTTP_BAD_REQUEST, 'Invalid query parameters');
80
        }
81
82
        return new JsonApiResponse(new CollectionSerializer($records, $params['fields'], $params['include']));
83
    }
84
85
    /**
86
     * Store a new record.
87
     *
88
     * @param Request $request
89
     *
90
     * @return JsonApiResponse
91
     */
92
    public function storeAction(Request $request)
93
    {
94
        $record = $this->model->create((array) $request->input('data.attributes'));
95
96
        if ($relationships = $request->input('data.relationships')) {
97
            $this->updateResourceRelationships($record, (array) $relationships);
98
        }
99
100
        return new JsonApiResponse(new ResourceSerializer($record), Response::HTTP_CREATED);
101
    }
102
103
    /**
104
     * Return a specified record.
105
     *
106
     * @param Request     $request
107
     * @param Model|mixed $record
108
     *
109
     * @return JsonApiResponse
110
     */
111
    public function showAction(Request $request, $record)
112
    {
113
        $record = $this->findModelInstance($record);
114
        $params = $this->getRequestParameters($request);
115
        $this->validateIncludableRelations($params['include']);
116
117
        return new JsonApiResponse(new ResourceSerializer($record, $params['fields'], $params['include']));
118
    }
119
120
    /**
121
     * Update a specified record.
122
     *
123
     * @param Request     $request
124
     * @param Model|mixed $record
125
     *
126
     * @return JsonApiResponse
127
     */
128
    public function updateAction(Request $request, $record)
129
    {
130
        $record = $this->findModelInstance($record);
131
        $record->fill((array) $request->input('data.attributes'));
132
        $record->save();
133
134
        if ($relationships = $request->input('data.relationships')) {
135
            $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...
136
        }
137
138
        return new JsonApiResponse(new ResourceSerializer($record));
139
    }
140
141
    /**
142
     * Destroy a specified record.
143
     *
144
     * @param Request     $request
145
     * @param Model|mixed $record
146
     *
147
     * @return JsonApiResponse
148
     */
149
    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...
150
    {
151
        $record = $this->findModelInstance($record);
152
        $record->delete();
153
154
        return new JsonApiResponse(null, Response::HTTP_NO_CONTENT);
155
    }
156
157
    /**
158
     * Return a specified record relationship.
159
     *
160
     * @param Request     $request
161
     * @param Model|mixed $record
162
     * @param string      $relation
163
     *
164
     * @return JsonApiResponse
165
     */
166
    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...
167
    {
168
        $record = $this->findModelInstance($record);
169
170
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
171
    }
172
173
    /**
174
     * Update a named relationship on a specified record.
175
     *
176
     * http://jsonapi.org/format/#crud-updating-relationships
177
     *
178
     * @param Request     $request
179
     * @param Model|mixed $record
180
     * @param string      $relation
181
     *
182
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
183
     *
184
     * @return JsonApiResponse
185
     */
186
    public function updateRelationshipAction(Request $request, $record, $relation)
187
    {
188
        $record = $this->findModelInstance($record);
189
        $relationType = $this->getRelationType($relation);
190
191
        abort_unless(is_string($relationType) && $this->isFillableRelation($relation), Response::HTTP_NOT_FOUND);
192
193
        if ($relationType === 'To-One') {
194
            $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...
195
        } else if ($relationType === 'To-Many') {
196
            $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...
197
        }
198
199
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
200
    }
201
202
    /**
203
     * Return existing instance of the resource or find by primary key.
204
     *
205
     * @param Model|mixed $record
206
     *
207
     * @throws ModelNotFoundException
208
     *
209
     * @return Model
210
     */
211
    protected function findModelInstance($record)
212
    {
213
        if ($record instanceof Model) {
214
            if (is_null($record->getKey())) {
215
                throw new ModelNotFoundException();
216
            }
217
218
            return $record;
219
        }
220
221
        return $this->model->findOrFail($record);
222
    }
223
224
    /**
225
     * Return any JSON API resource parameters from a request.
226
     *
227
     * @param Request $request
228
     *
229
     * @return array
230
     */
231
    protected function getRequestParameters($request)
232
    {
233
        $enableIncluded = config('jsonapi.enable_included_resources');
234
235
        if ($request->has('include') && is_bool($enableIncluded) && !$enableIncluded) {
236
            throw new HttpException('Inclusion of related resources is not supported');
237
        }
238
239
        return [
240
            'fields' => $this->getRequestQuerySet($request, 'fields', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
241
            'include' => $this->getRequestQuerySet($request, 'include', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
242
            'sort' => $this->getRequestQuerySet($request, 'sort', '/[A-Za-z_]+/'),
243
            'filter' => (array) $request->input('filter'),
244
        ];
245
    }
246
247
    /**
248
     * Return any comma separated values in a request query field as an array.
249
     *
250
     * @param Request     $request
251
     * @param string      $key
252
     * @param string|null $validate Regular expression to test for each item
253
     *
254
     * @throws \Illuminate\Validation\ValidationException
255
     *
256
     * @return array
257
     */
258
    protected function getRequestQuerySet($request, $key, $validate = null)
259
    {
260
        $values = preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
261
262
        $validator = Validator::make(['param' => $values], [
263
            'param.*' => 'required' . ($validate ? '|regex:' . $validate : ''),
264
        ]);
265
266
        if ($validator->fails()) {
267
            throw new ValidationException($validator, $this->error(
268
                Response::HTTP_BAD_REQUEST,
269
                sprintf('Invalid values for "%s" parameter', $key))
270
            );
271
        }
272
273
        return $values;
274
    }
275
276
    /**
277
     * Validate the requested included relationships against those that are
278
     * allowed on the requested resource type.
279
     *
280
     * @param array|null $relations
281
     *
282
     * @throws InvalidRelationPathException
283
     */
284
    protected function validateIncludableRelations($relations)
285
    {
286
        if (is_null($relations)) {
287
            return;
288
        }
289
290
        foreach ($relations as $relation) {
291
            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...
292
                throw new InvalidRelationPathException($relation);
293
            }
294
        }
295
    }
296
}
297