Passed
Push — controller-updates ( d6e495...032841 )
by Alex
02:51 queued 10s
created

JsonApiController::showRelationshipAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
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\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 many-to-one relationship association on a specified record.
175
     * http://jsonapi.org/format/#crud-updating-to-one-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 updateToOneRelationshipAction(Request $request, $record, $relation)
186
    {
187
        abort_unless($this->isFillableRelation($relation), Response::HTTP_NOT_FOUND);
188
189
        $record = $this->findModelInstance($record);
190
        $relation = $this->getModelRelationships()[$relation];
0 ignored issues
show
Documentation Bug introduced by
The method getModelRelationships 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...
191
        $data = (array) $request->input('data');
192
193
        $record->{$relation->getForeignKey()} = $data['id'];
194
        $record->save();
195
196
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
197
    }
198
199
    /**
200
     * Update named many-to-many relationship entries on a specified record.
201
     * http://jsonapi.org/format/#crud-updating-to-many-relationships
202
     *
203
     * @param Request     $request
204
     * @param Model|mixed $record
205
     * @param string      $relation
206
     *
207
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
208
     *
209
     * @return JsonApiResponse
210
     */
211
    public function updateToManyRelationshipAction(Request $request, $record, $relation)
212
    {
213
        abort_unless($this->isFillableRelation($relation), Response::HTTP_NOT_FOUND);
214
215
        $record = $this->findModelInstance($record);
216
        $relationships = (array) $request->input('data');
217
        $items = [];
218
219
        foreach ($relationships as $item) {
220
            if (isset($item['attributes'])) {
221
                $items[$item['id']] = $item['attributes'];
222
            } else {
223
                $items[] = $item['id'];
224
            }
225
        }
226
227
        switch ($request->method()) {
228
            case 'PATCH':
229
                $record->{$relation}()->sync($items);
230
                break;
231
            case 'POST':
232
                $record->{$relation}()->sync($items, false);
233
                break;
234
            case 'DELETE':
235
                $record->{$relation}()->detach(array_keys($items));
236
        }
237
238
        return new JsonApiResponse(new RelationshipSerializer($record, $relation));
239
    }
240
241
    /**
242
     * Return existing instance of the resource or find by primary key.
243
     *
244
     * @param Model|mixed $record
245
     *
246
     * @throws ModelNotFoundException
247
     *
248
     * @return Model
249
     */
250
    protected function findModelInstance($record)
251
    {
252
        if ($record instanceof Model) {
253
            if (is_null($record->getKey())) {
254
                throw new ModelNotFoundException();
255
            }
256
257
            return $record;
258
        }
259
260
        return $this->model->findOrFail($record);
261
    }
262
263
    /**
264
     * Return any JSON API resource parameters from a request.
265
     *
266
     * @param Request $request
267
     *
268
     * @return array
269
     */
270
    protected function getRequestParameters($request)
271
    {
272
        $enableIncluded = config('jsonapi.enable_included_resources');
273
274
        if ($request->has('include') && is_bool($enableIncluded) && !$enableIncluded) {
275
            throw new HttpException('Inclusion of related resources is not supported');
276
        }
277
278
        return [
279
            'fields' => $this->getRequestQuerySet($request, 'fields', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
280
            'include' => $this->getRequestQuerySet($request, 'include', '/^([A-Za-z]+.?)+[A-Za-z]+$/'),
281
            'sort' => $this->getRequestQuerySet($request, 'sort', '/[A-Za-z_]+/'),
282
            'filter' => (array) $request->input('filter'),
283
        ];
284
    }
285
286
    /**
287
     * Return any comma separated values in a request query field as an array.
288
     *
289
     * @param Request     $request
290
     * @param string      $key
291
     * @param string|null $validate Regular expression to test for each item
292
     *
293
     * @throws \Illuminate\Validation\ValidationException
294
     *
295
     * @return array
296
     */
297
    protected function getRequestQuerySet($request, $key, $validate = null)
298
    {
299
        $values = preg_split('/,/', $request->input($key), null, PREG_SPLIT_NO_EMPTY);
300
301
        $validator = Validator::make(['param' => $values], [
302
            'param.*' => 'required' . ($validate ? '|regex:' . $validate : ''),
303
        ]);
304
305
        if ($validator->fails()) {
306
            throw new ValidationException($validator, $this->error(
307
                Response::HTTP_BAD_REQUEST,
308
                sprintf('Invalid values for "%s" parameter', $key))
309
            );
310
        }
311
312
        return $values;
313
    }
314
315
    /**
316
     * Validate the requested included relationships against those that are
317
     * allowed on the requested resource type.
318
     *
319
     * @param array|null $relations
320
     *
321
     * @throws InvalidRelationPathException
322
     */
323
    protected function validateIncludableRelations($relations)
324
    {
325
        if (is_null($relations)) {
326
            return;
327
        }
328
329
        foreach ($relations as $relation) {
330
            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...
331
                throw new InvalidRelationPathException($relation);
332
            }
333
        }
334
    }
335
}
336