Issues (115)

src/Services/FetchService.php (7 issues)

Labels
1
<?php
2
3
namespace VGirol\JsonApi\Services;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Database\Eloquent\ModelNotFoundException;
9
use Illuminate\Database\Eloquent\Relations\Relation;
10
use Illuminate\Http\Request;
11
use Illuminate\Support\Arr;
12
use Illuminate\Support\Collection;
13
use Illuminate\Support\Str;
14
use Spatie\QueryBuilder\QueryBuilder;
15
use VGirol\JsonApi\Exceptions\JsonApi400Exception;
16
use VGirol\JsonApi\Exceptions\JsonApi403Exception;
17
use VGirol\JsonApi\Exceptions\JsonApi404Exception;
18
use VGirol\JsonApi\Exceptions\JsonApiException;
19
use VGirol\JsonApi\Messages\Messages;
20
use VGirol\JsonApi\Model\Related;
21
use VGirol\JsonApi\Services\AliasesService;
22
use VGirol\JsonApi\Services\FieldsService;
23
use VGirol\JsonApi\Services\FilterService;
24
use VGirol\JsonApi\Services\IncludeService;
25
use VGirol\JsonApi\Services\PaginationService;
26
use VGirol\JsonApi\Services\SortService;
27
use VGirol\JsonApiConstant\Members;
28
29
class FetchService
30
{
31
    /**
32
     * Undocumented variable
33
     *
34
     * @var SortService
35
     */
36
    private $sort;
37
38
    /**
39
     * Undocumented variable
40
     *
41
     * @var FilterService
42
     */
43
    private $filter;
44
45
    /**
46
     * Undocumented variable
47
     *
48
     * @var FieldsService
49
     */
50
    private $fields;
51
52
    /**
53
     * Undocumented variable
54
     *
55
     * @var IncludeService
56
     */
57
    private $include;
58
59
    /**
60
     * Undocumented variable
61
     *
62
     * @var AliasesService
63
     */
64
    private $alias;
65
66
    /**
67
     * Undocumented variable
68
     *
69
     * @var PaginationService
70
     */
71
    private $pagination;
72
73
    /**
74
     * Class constructor
75
     */
76
    public function __construct()
77
    {
78
        $this->alias = jsonapiAliases();
79
        $this->sort = jsonapiSort();
80
        $this->filter = jsonapiFilter();
81
        $this->fields = jsonapiFields();
82
        $this->include = jsonapiInclude();
83
        $this->pagination = jsonapiPagination();
84
    }
85
86
    /**
87
     * Find a single resource
88
     *
89
     * @param Request        $request
90
     * @param string|Builder $baseQuery
91
     * @param mixed          $id
92
     *
93
     * @return Model
94
     * @throws JsonApiException
95
     * @throws ModelNotFoundException
96
     */
97
    public function find(Request $request, $baseQuery, $id)
98
    {
99
        if (
100
            $this->sort->hasQuery($request)
101
            || $this->filter->hasQuery($request)
102
            || $this->pagination->hasQuery($request)
103
        ) {
104
            throw new JsonApiException(Messages::ERROR_FETCHING_SINGLE_WITH_NOT_ALLOWED_QUERY_PARAMETERS);
105
        }
106
107
        // Create QueryBuilder object for sorting, filtering, including ...
108
        $queryBuilder = $this->getQueryBuilder($request, $baseQuery);
109
110
        return $queryBuilder->findOrFail($id);
111
    }
112
113
    /**
114
     * Get a collection of resources
115
     *
116
     * @param Request        $request
117
     * @param string|Builder $baseQuery
118
     *
119
     * @return EloquentCollection
120
     * @throws JsonApiException
121
     */
122
    public function get(Request $request, $baseQuery)
123
    {
124
        // Create QueryBuilder object for sorting, filtering, including ...
125
        $queryBuilder = $this->getQueryBuilder($request, $baseQuery);
126
127
        // Get total items before pagination
128
        $itemTotal = $queryBuilder->count();
129
        $this->pagination->setTotalItem($itemTotal);
0 ignored issues
show
$itemTotal of type Spatie\QueryBuilder\QueryBuilder is incompatible with the type integer expected by parameter $total of VGirol\JsonApi\Services\...Service::setTotalItem(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

129
        $this->pagination->setTotalItem(/** @scrutinizer ignore-type */ $itemTotal);
Loading history...
130
131
        if ($this->pagination->allowed()) {
132
            // Pagination method
133
            $method_name = config('json-api-paginate.method_name');
134
135
            // Paginate collection
136
            $builder = call_user_func([$queryBuilder, $method_name]);
137
138
            $collection = $builder->getCollection();
139
        } else {
140
            $collection = $queryBuilder->get();
141
        }
142
143
        $collection->itemTotal = $itemTotal;
144
145
        return $collection;
146
    }
147
148
    /**
149
     * Find a single related resource
150
     *
151
     * @param Request $request
152
     * @param mixed   $parentId
153
     * @param string  $relationship
154
     * @param mixed   $id
155
     *
156
     * @return Model
157
     * @throws JsonApi403Exception
158
     * @throws ModelNotFoundException
159
     */
160
    public function findRelated(Request $request, $parentId, string $relationship, $id)
161
    {
162
        return $this->getRelationFromRequest($request, $parentId, $relationship)
163
            ->getQuery()
0 ignored issues
show
The method getQuery() does not exist on Illuminate\Database\Eloquent\Relations\Relation. It seems like you code against a sub-type of Illuminate\Database\Eloquent\Relations\Relation such as Illuminate\Database\Eloquent\Relations\MorphTo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

163
            ->/** @scrutinizer ignore-call */ getQuery()
Loading history...
164
            ->findOrFail($id);
165
    }
166
167
    /**
168
     * Gets one or more related resources.
169
     *
170
     * @param Request $request
171
     * @param mixed   $parentId
172
     * @param string  $relationship
173
     * @param boolean $asResourceIdentifier
174
     *
175
     * @return EloquentCollection|Model|null
176
     * @throws JsonApiException
177
     * @throws JsonApi400Exception
178
     * @throws JsonApi403Exception
179
     * @throws ModelNotFoundException
180
     */
181
    public function getRelated(Request $request, $parentId, string $relationship)
182
    {
183
        // Gets the relation and its query builder
184
        $relation = $this->getRelationFromRequest($request, $parentId, $relationship);
185
        $query = $relation->getQuery();
186
187
        // Creates resource
188
        if ($relation->isToMany()) {
189
            $resource = $this->get($request, $query);
190
        } else {
191
            if (jsonapiSort()->hasQuery()) {
192
                throw new JsonApi400Exception(Messages::SORTING_IMPOSSIBLE_FOR_TO_ONE_RELATIONSHIP);
193
            }
194
            $id = $query->count() ? $relation->getResults()->getKey() : null;
0 ignored issues
show
The method getResults() does not exist on Illuminate\Database\Eloquent\Relations\Relation. It seems like you code against a sub-type of said class. However, the method does not exist in Illuminate\Database\Eloq...\Relations\HasOneOrMany or Illuminate\Database\Eloq...elations\MorphOneOrMany. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

194
            $id = $query->count() ? $relation->/** @scrutinizer ignore-call */ getResults()->getKey() : null;
Loading history...
195
            $resource = ($id !== null) ? $this->find($request, $query, $id) : null;
196
        }
197
198
        return $resource;
199
    }
200
201
    /**
202
     * Gets all related resources specified in request data
203
     *
204
     * @param array $data
205
     *
206
     * @return Collection|Related
207
     * @throws JsonApi404Exception
208
     */
209
    public function getRelatedFromRequestData(array $data)
210
    {
211
        return $this->extractRelated($data);
212
    }
213
214
    /**
215
     * Undocumented function
216
     *
217
     * @param Collection|Model|Related|array|null $data
218
     *
219
     * @return Collection|Related
220
     * @throws JsonApi404Exception
221
     */
222
    public function extractRelated($data)
223
    {
224
        $isSingle = $this->isSingle($data);
225
226
        if ($isSingle) {
227
            $data = [$data];
228
        }
229
230
        $collection = Collection::make($data)->map(
231
            function ($item) {
232
                return $this->getRelatedInstance($item);
233
            }
234
        );
235
236
        return $isSingle ? $collection->first() : $collection;
237
    }
238
239
    /**
240
     * Get a Relation object.
241
     *
242
     * @param Request $request
243
     * @param mixed   $parentId
244
     * @param string  $relationship
245
     *
246
     * @return Relation
247
     * @throws JsonApi403Exception
248
     * @throws ModelNotFoundException
249
     */
250
    public function getRelationFromRequest(Request $request, $parentId, string $relationship)
251
    {
252
        return $this->getRelationFromModel(
253
            $this->findParent($request, $parentId),
254
            $relationship
255
        );
256
    }
257
258
    /**
259
     * Get a Relation object.
260
     *
261
     * @param Model  $model
262
     * @param string $relationship
263
     *
264
     * @return Relation
265
     * @throws JsonApi403Exception
266
     */
267
    public function getRelationFromModel($model, string $relationship)
268
    {
269
        // Get the relationship
270
        if (!\method_exists($model, $relationship)) {
271
            throw new JsonApi403Exception(sprintf(Messages::NON_EXISTENT_RELATIONSHIP, $relationship));
272
        }
273
274
        return $model->$relationship();
275
    }
276
277
    /**
278
     * Get the model required to fill the server response
279
     *
280
     * @param Request $request
281
     * @param mixed   $id
282
     * @param Model   $model
283
     *
284
     * @return Model
285
     */
286
    public function getRequiredModel(Request $request, $id, $model)
287
    {
288
        // Find model
289
        $requiredModel = $this->find(
290
            $request,
291
            $this->alias->getModelClassName($request),
292
            $id
293
        );
294
295
        // Add tag indicating if automatic changes have been made
296
        $requiredModel->automaticChanges = (\count(\array_diff_assoc(
297
            ($request->method() == 'POST') ? $model->attributesToArray() : $model->getChanges(),
0 ignored issues
show
The method method() does not exist on Illuminate\Http\Request. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

297
            ($request->/** @scrutinizer ignore-call */ method() == 'POST') ? $model->attributesToArray() : $model->getChanges(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
298
            $request->input(Members::DATA . '.' . Members::ATTRIBUTES, [])
0 ignored issues
show
The method input() does not exist on Illuminate\Http\Request. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

298
            $request->/** @scrutinizer ignore-call */ 
299
                      input(Members::DATA . '.' . Members::ATTRIBUTES, [])

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
299
        )) !== 0);
300
301
        $requiredModel->makeHidden('automaticChanges');
302
303
        return $requiredModel;
304
    }
305
306
    /**
307
     * Find the parent of a related resource
308
     *
309
     * @param Request $request
310
     * @param mixed   $id
311
     *
312
     * @return Model
313
     * @throws ModelNotFoundException
314
     */
315
    public function findParent(Request $request, $id)
316
    {
317
        return \call_user_func(
318
            [$this->alias->getParentClassName($request), 'findOrFail'],
319
            $id
320
        );
321
    }
322
323
    /**
324
     * Get an instance of the query builder
325
     *
326
     * @param Request        $request
327
     * @param string|Builder $baseQuery
328
     *
329
     * @return QueryBuilder
330
     * @throws JsonApiException
331
     */
332
    private function getQueryBuilder(Request $request, $baseQuery): QueryBuilder
333
    {
334
        // Create QueryBuilder object for sorting, filtering, ...
335
        $queryBuilder = QueryBuilder::for($baseQuery, $request);
336
337
        $table = $queryBuilder->getModel()->getTable();
338
339
        // Sorting
340
        if (!$this->sort->queryIsValid($table)) {
0 ignored issues
show
$table of type Spatie\QueryBuilder\QueryBuilder is incompatible with the type null|string expected by parameter $tableName of VGirol\JsonApi\Services\...Service::queryIsValid(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

340
        if (!$this->sort->queryIsValid(/** @scrutinizer ignore-type */ $table)) {
Loading history...
341
            throw new JsonApiException(sprintf(Messages::SORTING_BAD_FIELD, $this->sort->implode()));
342
        }
343
        $queryBuilder->allowedSorts($this->sort->allowedSorts());
344
345
        // Filtering
346
        $queryBuilder->allowedFilters($this->filter->allowedFilters());
347
348
        // Selecting fields
349
        $queryBuilder->allowedFields($this->fields->allowedFields($this->alias->getResourceType($request)));
350
        if ($this->fields->hasQuery()) {
351
            $keyName = Str::lower($queryBuilder->getModel()->getKeyName());
0 ignored issues
show
$queryBuilder->getModel()->getKeyName() of type Spatie\QueryBuilder\QueryBuilder is incompatible with the type string expected by parameter $value of Illuminate\Support\Str::lower(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

351
            $keyName = Str::lower(/** @scrutinizer ignore-type */ $queryBuilder->getModel()->getKeyName());
Loading history...
352
            if (!Arr::has($this->fields->getQueryParameter(), $keyName)) {
353
                // Add key name to the selected fields
354
                $queryBuilder->addSelect($table . '.' . $keyName);
355
            }
356
        }
357
358
        // Including relationships
359
        $queryBuilder->allowedIncludes($this->include->allowedIncludes());
360
361
        return $queryBuilder;
362
    }
363
364
    /**
365
     * Undocumented function
366
     *
367
     * @param Related|Model|array $item
368
     *
369
     * @return Related
370
     * @throws JsonApi404Exception
371
     */
372
    private function getRelatedInstance($item): Related
373
    {
374
        if (\is_object($item) && \is_a($item, Related::class)) {
375
            return $item;
376
        }
377
378
        $model = $this->getModelInstance($item);
379
380
        if ($model === null) {
381
            throw new JsonApi404Exception(
382
                sprintf(Messages::UPDATING_REQUEST_RELATED_NOT_FOUND, $item[Members::TYPE])
383
            );
384
        }
385
386
        $rel = new Related($model, $item[Members::META] ?? []);
387
388
        return $rel;
389
    }
390
391
    /**
392
     * Undocumented function
393
     *
394
     * @param Model|array $item
395
     *
396
     * @return Model
397
     */
398
    private function getModelInstance($item)
399
    {
400
        if (\is_object($item) && \is_a($item, Model::class)) {
401
            return $item;
402
        }
403
404
        return call_user_func(
405
            [$this->alias->getModelClassName($item[Members::TYPE]), 'make']
406
        )->find($item[Members::ID]);
407
    }
408
409
    /**
410
     * Undocumented function
411
     *
412
     * @param Collection|Model|Related|array|null $data
413
     *
414
     * @return boolean
415
     */
416
    private function isSingle($data): bool
417
    {
418
        $isSingle = false;
419
420
        if (\is_array($data)) {
421
            $isSingle = Arr::isAssoc($data);
422
        }
423
        if (\is_object($data) && (\is_a($data, Model::class) || \is_a($data, Related::class))) {
424
            $isSingle = true;
425
        }
426
427
        return $isSingle;
428
    }
429
}
430