Passed
Pull Request — master (#1)
by Tom
07:01
created

AbstractRestfulController::filterValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
nc 1
cc 1
nop 3
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;
11
use Illuminate\Database\Eloquent\Collection;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Relations\Relation;
14
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
15
use Illuminate\Foundation\Validation\ValidatesRequests;
16
use Illuminate\Http\JsonResponse;
17
use Illuminate\Http\RedirectResponse;
18
use Illuminate\Http\Request;
19
use Illuminate\Http\Response;
20
use Illuminate\Routing\Controller as BaseController;
21
use Illuminate\Routing\Redirector;
22
use Illuminate\Routing\Route;
23
use Illuminate\View\View;
24
use Symfony\Component\HttpFoundation\Response as SymResponse;
25
26
abstract class AbstractRestfulController extends BaseController
27
{
28
    use AuthorizesRequests, ValidatesRequests;
29
30
31
    /**
32
     * The views to render.
33
     * @var string[]
34
     */
35
    protected $views = [];
36
37
    /**
38
     * What Model class to search for entities.
39
     * @return string
40
     */
41
    abstract protected function getModelClass(): string;
42
43
    /**
44
     * Return a list of matching models.
45
     * @param Request $request
46
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
47
     */
48
    public function index(Request $request)
49
    {
50
        $builder = $this->createModelQueryBuilder();
51
52
        foreach ((array)$request->input() as $column => $value) {
53
            $this->filterValue($builder, $column, $value);
54
        }
55
56
        $data = $builder->paginate();
57
58
        return $this->return($request, $data, 'index');
59
    }
60
61
    /**
62
     * Handles creating a model. The C of CRUD
63
     * @param Request $request
64
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
65
     */
66 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...
67
    {
68
        $model = $this->newModelInstance();
69
70
        foreach ((array)$request->input() as $column => $value) {
71
            $model->$column = $value;
72
        }
73
74
        $model->save();
75
76
77
        return $this->return($request, $this->findModel($model->getAttribute('id')), 'store');
78
    }
79
80
    /**
81
     * Shows a model. The R of CRUD.
82
     * @param Request $request
83
     * @param int $id
84
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
85
     */
86
    public function show(Request $request, $id)
87
    {
88
        $model = $this->findModel($id, $request);
89
90
        $model = $this->iterateThroughChildren($model, $request);
91
92
        return $this->return($request, $model, 'show');
93
    }
94
95
    /**
96
     * Update a record. The U of CRUD.
97
     * @param Request $request
98
     * @param int $id
99
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
100
     */
101 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...
102
    {
103
        $model = $this->findModel($id);
104
105
        foreach ((array)$request->input() as $column => $value) {
106
            $model->$column = $value;
107
        }
108
109
        $model->save();
110
111
        return $this->return($request, $model, 'update');
112
    }
113
114
    /**
115
     * Destroy a model. The D of CRUD.
116
     * @param Request $request
117
     * @param int $id
118
     * @return ResponseFactory|Response
119
     * @throws Exception
120
     */
121
    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...
122
    {
123
        $model = $this->findModel($id);
124
125
        $model->delete();
126
127
        return response(null, SymResponse::HTTP_NO_CONTENT);
128
    }
129
130
    /**
131
     * Generate a new query builder for the model.
132
     * @return Builder
133
     */
134
    private function createModelQueryBuilder(): Builder
135
    {
136
        $class = $this->newModelInstance();
137
138
        return $class->newQuery();
139
    }
140
141
    /**
142
     * Creates a new model instance.
143
     * @return Model
144
     */
145
    private function newModelInstance(): Model
146
    {
147
        $classFQDN = $this->getModelClass();
148
149
        return app($classFQDN);
150
    }
151
152
    /**
153
     * Looks for an "extra" param in the route, and if it exists, looks for relationships
154
     * based on that route.
155
     * @param Model $model
156
     * @param Request $request
157
     * @return LengthAwarePaginator|Collection|Model|mixed
158
     * @throws BadMethodCallException
159
     */
160
    private function iterateThroughChildren(Model $model, Request $request)
161
    {
162
163
        // If there's no route or extra param, just return.
164
        if (!$request->route() ||
165
            !($request->route() instanceof Route) ||
166
            !$request->route()->parameter('extra')) {
167
            return $model;
168
        }
169
170
        $parts = array_filter(explode('/', (string)$request->route()->parameter('extra')));
171
172
        // Loop through the parts.
173
        foreach ($parts as $part) {
174
            // Look for an array accessor, "children[5]" for example.
175
            preg_match('/\[(\d+)\]$/', $part, $matches);
176
            $offset = false;
177
178
            // If one was found, save the offset and remove it from $part.
179
            if (!empty($matches[0])) {
180
                $part = str_replace(array_shift($matches), '', $part);
181
                $offset = array_shift($matches);
182
            }
183
184
            $model = $model->$part();
185
186
            // If it's a relationship, see if it's paginate-able.
187
            if (method_exists($model, 'paginate')) {
188
                $model = $model->paginate();
189
            } elseif ($model instanceof Relation) {
190
                $model = $model->getResults();
191
            }
192
193
            // If there is an offset, get it.
194
            if ($offset !== false) {
195
                $model = $model[$offset];
196
            }
197
        }
198
199
        return $model;
200
    }
201
202
    /**
203
     * Finds the model instance.
204
     * @param int $id
205
     * @param Request|null $request
206
     * @return Model
207
     */
208
    private function findModel(int $id, Request $request = null): Model
209
    {
210
        /** @var Model|Builder $class */
211
        $class = $this->newModelInstance();
212
213
        if ($request) {
214
            $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...
215
        }
216
217
        $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...
218
        return $class;
219
    }
220
221
    /**
222
     * Apply causes to the builder.
223
     * @param Builder $builder
224
     * @param string $column
225
     * @param mixed $value
226
     */
227
    private function filterValue(Builder $builder, string $column, $value): void
228
    {
229
        $builder->where($column, $value);
230
    }
231
232
    /**
233
     * Build and return a response.
234
     * @param Request $request
235
     * @param mixed $data
236
     * @param string $method
237
     * @return JsonResponse|ResponseFactory|Response|RedirectResponse|Redirector
238
     */
239
    private function return(Request $request, $data, string $method)
240
    {
241
242
        $status = SymResponse::HTTP_OK;
243
        switch ($method) {
244
            case 'store':
245
                $status = SymResponse::HTTP_CREATED;
246
                break;
247
        }
248
249
        if ($request->wantsJson()) {
250
            return app(ResponseFactory::class)->json($data, $status);
251
        }
252
253
        if (isset($this->views[$method]) && app(Factory::class)->exists($this->views[$method])) {
254
            /** @var View $view */
255
            $view = view($this->views[$method], [
256
                'data' => $data
257
            ]);
258
259
            return response($view, $status);
260
        }
261
262
        switch ($method) {
263
            case 'store':
264
            case 'update':
265
                // If it's store/update, and the user isn't asking for JSON, we want to
266
                // try and redirect them to the related show record page.
267
                if (($redirect = $this->redirectToShowRoute($request, $data))) {
268
                    return $redirect;
269
                }
270
                break;
271
        }
272
273
        return app(ResponseFactory::class)->json($data, $status);
274
    }
275
276
    /**
277
     * Redirects to the show route for the model if one exists.
278
     * @param Request $request
279
     * @param mixed $data
280
     * @return RedirectResponse|Redirector|null
281
     */
282
    private function redirectToShowRoute(Request $request, $data)
283
    {
284
        /** @var Route|null $route */
285
        $route = $request->route();
286
        if (!$route) {
287
            return null;
288
        }
289
290
        $name = $route->getName();
291
        if (!$name) {
292
            return null;
293
        }
294
295
        $exploded = explode('.', $name);
296
        array_pop($exploded);
297
        $topLevel = array_pop($exploded);
298
299
        if (!$topLevel) {
300
            return null;
301
        }
302
303
        return redirect(route("$topLevel.show", [
304
            str_replace('-', '_', $topLevel) => $data->id
305
        ]));
306
    }
307
308
    /**
309
     * Preload any relationships required.
310
     * @param Model $class
311
     * @param Request $request
312
     * @return void
313
     */
314
    private function preloadRelationships(Model &$class, Request $request): void
315
    {
316
        $header = $request->headers->get('X-Load-Relationship');
317
        if (!$header) {
318
            return;
319
        }
320
321
        $relationships = explode(',', $header);
322
        if (empty($relationships)) {
323
            return;
324
        }
325
326
        $class = $class->with($relationships);
327
    }
328
}
329