Passed
Push — master ( 4b3c6f...540a68 )
by Tom
04:41
created

AbstractRestfulController::redirectToShowRoute()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 46
rs 8.867
c 0
b 0
f 0
cc 5
nc 5
nop 2
1
<?php
2
3
namespace TomHart\Restful;
4
5
use BadMethodCallException;
6
use Exception;
7
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
8
use Illuminate\Contracts\Routing\ResponseFactory;
9
use Illuminate\Contracts\View\Factory;
10
use Illuminate\Database\Eloquent\Builder;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, TomHart\Restful\Builder.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
11
use Illuminate\Database\Eloquent\Collection;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
14
use Illuminate\Database\Eloquent\Relations\HasMany;
15
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
16
use Illuminate\Database\Eloquent\Relations\MorphMany;
17
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
18
use Illuminate\Database\Eloquent\Relations\MorphToMany;
19
use Illuminate\Database\Eloquent\Relations\Relation;
20
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
21
use Illuminate\Foundation\Validation\ValidatesRequests;
22
use Illuminate\Http\JsonResponse;
23
use Illuminate\Http\RedirectResponse;
24
use Illuminate\Http\Request;
25
use Illuminate\Http\Response;
26
use Illuminate\Routing\Controller as BaseController;
27
use Illuminate\Routing\Redirector;
28
use Illuminate\Routing\Route;
29
use Illuminate\Support\Str;
30
use Illuminate\View\View;
31
use InvalidArgumentException;
32
use Symfony\Component\HttpFoundation\Response as SymResponse;
33
use TomHart\Restful\Concerns\HasLinks;
34
35
abstract class AbstractRestfulController extends BaseController
36
{
37
    use AuthorizesRequests;
38
    use ValidatesRequests;
39
40
    /**
41
     * The views to render.
42
     * @var string[]
43
     */
44
    protected $views = [];
45
46
    /**
47
     * What Model class to search for entities.
48
     * @return string
49
     */
50
    abstract protected function getModelClass(): string;
51
52
    /**
53
     * Return a list of matching models.
54
     * @param Request $request
55
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
56
     */
57
    public function index(Request $request)
58
    {
59
        $builder = $this->createModelQueryBuilder();
60
61
        $input = collect($request->input())->except(config('restful.query_string_keys.page'))->toArray();
62
        foreach ($input as $column => $value) {
63
            $this->filterValue($builder, $column, $value);
64
        }
65
66
        $data = $builder->paginate(
67
            $request->input('limit', null),
68
            ['*'],
69
            config('restful.query_string_keys.page')
70
        );
71
72
        return $this->return($request, $data, 'index');
73
    }
74
75
    /**
76
     * Handles creating a model. The C of CRUD
77
     * @param Request $request
78
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
79
     */
80 View Code Duplication
    public function store(Request $request)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
81
    {
82
        $model = $this->newModelInstance();
83
84
        foreach ((array)$request->input() as $column => $value) {
85
            $model->$column = $value;
86
        }
87
88
        $this->saveModel($model);
89
90
        return $this->return($request, $this->findModel($model->getAttribute('id')), 'store');
91
    }
92
93
    /**
94
     * Shows a model. The R of CRUD.
95
     * @param Request $request
96
     * @param int $id
97
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
98
     */
99
    public function show(Request $request, $id)
100
    {
101
        $model = $this->findModel($id, $request);
102
103
        $model = $this->iterateThroughChildren($model, $request);
104
105
        return $this->return($request, $model, 'show');
106
    }
107
108
    /**
109
     * Update a record. The U of CRUD.
110
     * @param Request $request
111
     * @param int $id
112
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
113
     */
114 View Code Duplication
    public function update(Request $request, $id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
115
    {
116
        $model = $this->findModel($id);
117
118
        foreach ((array)$request->input() as $column => $value) {
119
            $model->$column = $value;
120
        }
121
122
        $this->saveModel($model);
123
124
        return $this->return($request, $model, 'update');
125
    }
126
127
    /**
128
     * Destroy a model. The D of CRUD.
129
     * @param Request $request
130
     * @param int $id
131
     * @return ResponseFactory|Response
132
     * @throws Exception
133
     */
134
    public function destroy(Request $request, $id)
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...
135
    {
136
        $model = $this->findModel($id);
137
138
        $model->delete();
139
140
        return response(null, SymResponse::HTTP_NO_CONTENT);
141
    }
142
143
144
    /**
145
     * Return the _links. The O of CRUD.....
146
     * @param Request $request
147
     * @return ResponseFactory|JsonResponse|RedirectResponse|Response|Redirector
148
     */
149
    public function options(Request $request)
150
    {
151
        $class = $this->newModelInstance();
152
153
        if (!($class instanceof HasLinks)) {
154
            throw new InvalidArgumentException('OPTIONS only works for models implementing HasLinks');
155
        }
156
157
        foreach ($request->input() as $key => $value) {
158
            $class->$key = $value;
159
        }
160
161
        return $this->return($request, $class->buildLinks(), 'options');
162
    }
163
164
    /**
165
     * Generate a new query builder for the model.
166
     * @return Builder
167
     */
168
    protected function createModelQueryBuilder(): Builder
169
    {
170
        $class = $this->newModelInstance();
171
172
        return $class->newQuery();
173
    }
174
175
    /**
176
     * Creates a new model instance.
177
     * @return Model
178
     */
179
    protected function newModelInstance(): Model
180
    {
181
        $classFQDN = $this->getModelClass();
182
183
        return app($classFQDN);
184
    }
185
186
    /**
187
     * Looks for an "extra" param in the route, and if it exists, looks for relationships
188
     * based on that route.
189
     * @param Model $model
190
     * @param Request $request
191
     * @return LengthAwarePaginator|Collection|Model|mixed
192
     * @throws BadMethodCallException
193
     */
194
    protected function iterateThroughChildren(Model $model, Request $request)
195
    {
196
        // If there's no route or extra param, just return.
197
        if (!$request->route() ||
198
            !($request->route() instanceof Route) ||
199
            !$request->route()->parameter('extra')) {
200
            return $model;
201
        }
202
203
        $parts = array_filter(explode('/', (string)$request->route()->parameter('extra')));
204
205
        // Loop through the parts.
206
        foreach ($parts as $part) {
207
            // Look for an array accessor, "children[5]" for example.
208
            preg_match('/\[(\d+)]$/', $part, $matches);
209
            $offset = false;
210
211
            // If one was found, save the offset and remove it from $part.
212
            if (!empty($matches[0])) {
213
                $part = str_replace(array_shift($matches), '', $part);
214
                $offset = array_shift($matches);
215
            }
216
217
            $model = $model->$part();
218
219
            // If it's a relationship, see if it's paginate-able.
220
            if (stripos(get_class($model), 'Many') !== false) {
221
                /** @var BelongsToMany|HasMany|HasOneOrMany|MorphMany|MorphOneOrMany|MorphToMany $model */
222
                $model = $model->paginate();
0 ignored issues
show
Bug introduced by
The method paginate does only exist in Illuminate\Database\Eloq...Relations\BelongsToMany, but not in Illuminate\Database\Eloq...\Relations\HasOneOrMany.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
223
            } elseif ($model instanceof Relation) {
224
                $model = $model->getResults();
225
            }
226
227
            // If there is an offset, get it.
228
            if ($offset !== false) {
229
                $model = $model[$offset];
230
            }
231
        }
232
233
        return $model;
234
    }
235
236
    /**
237
     * Finds the model instance.
238
     * @param int|string $id
239
     * @param Request|null $request
240
     * @return Model
241
     */
242
    protected function findModel($id, Request $request = null): Model
243
    {
244
        /** @var Model|Builder $class */
245
        $class = $this->newModelInstance();
246
247
        if ($request) {
248
            $this->preloadRelationships($class, $request);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type object<Illuminate\Database\Eloquent\Builder>; however, TomHart\Restful\Abstract...:preloadRelationships() does only seem to accept object<Illuminate\Database\Eloquent\Model>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
249
        }
250
251
        $class = $class->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
252
        return $class;
253
    }
254
255
    /**
256
     * Apply causes to the builder.
257
     * @param Builder $builder
258
     * @param string $column
259
     * @param mixed $value
260
     */
261
    protected function filterValue(Builder $builder, string $column, $value): void
262
    {
263
        $builder->where($column, $value);
264
    }
265
266
    /**
267
     * Build and return a response.
268
     * @param Request $request
269
     * @param mixed $data
270
     * @param string $method
271
     * @return JsonResponse|ResponseFactory|Response|RedirectResponse|Redirector
272
     */
273
    protected function return(Request $request, $data, string $method)
274
    {
275
        $status = SymResponse::HTTP_OK;
276
        switch ($method) {
277
            case 'store':
278
                $status = SymResponse::HTTP_CREATED;
279
                break;
280
        }
281
282
        if ($request->wantsJson()) {
283
            return app(ResponseFactory::class)->json($data, $status);
284
        }
285
286
        if (isset($this->views[$method]) && app(Factory::class)->exists($this->views[$method])) {
287
            /** @var View $view */
288
            $view = view(
289
                $this->views[$method],
290
                [
291
                    'data' => $data
292
                ]
293
            );
294
295
            return response($view, $status);
296
        }
297
298
        switch ($method) {
299
            case 'store':
300
            case 'update':
301
                // If it's store/update, and the user isn't asking for JSON, we want to
302
                // try and redirect them to the related show record page.
303
                if (($redirect = $this->redirectToShowRoute($request, $data))) {
304
                    return $redirect;
305
                }
306
        }
307
308
        return app(ResponseFactory::class)->json($data, $status);
309
    }
310
311
    /**
312
     * Redirects to the show route for the model if one exists.
313
     * @param Request $request
314
     * @param mixed $data
315
     * @return RedirectResponse|Redirector|null
316
     */
317
    protected function redirectToShowRoute(Request $request, $data)
318
    {
319
        if ($data instanceof HasLinks) {
320
            $route = $data->getRouteName() . '.show';
321
            $key = $data->getRouteKey();
322
323
            return redirect(
324
                route(
325
                    $route,
326
                    [
327
                        $key => $data->id
0 ignored issues
show
Bug introduced by
Accessing id on the interface TomHart\Restful\Concerns\HasLinks suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
328
                    ]
329
                )
330
            );
331
        }
332
333
        /** @var Route|null $route */
334
        $route = $request->route();
335
        if (!$route) {
336
            return null;
337
        }
338
339
        $name = $route->getName();
340
        if (!$name) {
341
            return null;
342
        }
343
344
        $exploded = explode('.', $name);
345
        array_pop($exploded);
346
        $topLevel = array_pop($exploded);
347
348
        if (!$topLevel) {
349
            return null;
350
        }
351
352
        $key = Str::singular(str_replace('-', '_', $topLevel));
353
354
        return redirect(
355
            route(
356
                "$topLevel.show",
357
                [
358
                    $key => $data->id
359
                ]
360
            )
361
        );
362
    }
363
364
    /**
365
     * Preload any relationships required.
366
     * @param Model $class
367
     * @param Request $request
368
     * @return void
369
     */
370
    protected function preloadRelationships(Model &$class, Request $request): void
371
    {
372
        $headers = $request->headers;
373
        if (!$headers) {
374
            return;
375
        }
376
377
        $header = $headers->get('X-Load-Relationship');
378
        if (!$header || empty($header)) {
379
            return;
380
        }
381
382
        $relationships = array_filter(explode(',', $header));
383
384
        $class = $class->with($relationships);
385
    }
386
387
    /**
388
     * Save a model, either from a store or an update.
389
     * @param Model $model
390
     * @return bool
391
     */
392
    private function saveModel(Model $model)
393
    {
394
        return $model->save();
395
    }
396
}
397