Passed
Push — master ( 66f51c...e70f10 )
by Tom
04:46
created

AbstractRestfulController::preloadRelationships()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 3
nc 3
nop 2
1
<?php
2
3
namespace TomHart\Restful;
4
5
use Exception;
6
use Illuminate\Contracts\Routing\ResponseFactory;
7
use Illuminate\Contracts\View\Factory;
8
use Illuminate\Database\Eloquent\Builder;
9
use Illuminate\Database\Eloquent\Model;
10
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
11
use Illuminate\Foundation\Bus\DispatchesJobs;
12
use Illuminate\Foundation\Validation\ValidatesRequests;
13
use Illuminate\Http\JsonResponse;
14
use Illuminate\Http\RedirectResponse;
15
use Illuminate\Http\Request;
16
use Illuminate\Http\Response;
17
use Illuminate\Routing\Controller as BaseController;
18
use Illuminate\Routing\Redirector;
19
use Illuminate\Routing\Route;
20
use Illuminate\View\View;
21
use Symfony\Component\HttpFoundation\Response as SymResponse;
22
23
abstract class AbstractRestfulController extends BaseController
24
{
25
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
26
27
28
    /**
29
     * The views to render.
30
     * @var string[]
31
     */
32
    protected $views = [];
33
34
    /**
35
     * What Model class to search for entities.
36
     * @return string
37
     */
38
    abstract protected function getModelClass(): string;
39
40
    /**
41
     * Return a list of matching models.
42
     * @param Request $request
43
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
44
     */
45
    public function index(Request $request)
46
    {
47
        $builder = $this->createModelQueryBuilder();
48
49
        foreach ((array)$request->input() as $column => $value) {
50
            $this->filterValue($builder, $column, $value);
51
        }
52
53
        $data = $builder->paginate();
54
55
        return $this->return($request, $data, 'index');
56
    }
57
58
    /**
59
     * Handles creating a model. The C of CRUD
60
     * @param Request $request
61
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
62
     */
63 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...
64
    {
65
        $model = $this->newModelInstance();
66
67
        foreach ((array)$request->input() as $column => $value) {
68
            $model->$column = $value;
69
        }
70
71
        $model->save();
72
73
74
        return $this->return($request, $this->findModel($model->getAttribute('id')), 'store');
75
    }
76
77
    /**
78
     * Shows a model. The R of CRUD.
79
     * @param Request $request
80
     * @param int $id
81
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
82
     */
83
    public function show(Request $request, $id)
84
    {
85
        $model = $this->findModel($id, $request);
86
87
        return $this->return($request, $model, 'show');
88
    }
89
90
    /**
91
     * Update a record. The U of CRUD.
92
     * @param Request $request
93
     * @param int $id
94
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
95
     */
96 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...
97
    {
98
        $model = $this->findModel($id);
99
100
        foreach ((array)$request->input() as $column => $value) {
101
            $model->$column = $value;
102
        }
103
104
        $model->save();
105
106
        return $this->return($request, $model, 'update');
107
    }
108
109
    /**
110
     * Destroy a model. The D of CRUD.
111
     * @param Request $request
112
     * @param int $id
113
     * @return ResponseFactory|Response
114
     * @throws Exception
115
     */
116
    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...
117
    {
118
        $model = $this->findModel($id);
119
120
        $model->delete();
121
122
        return response(null, SymResponse::HTTP_NO_CONTENT);
123
    }
124
125
    /**
126
     * Apply causes to the builder.
127
     * @param Builder $builder
128
     * @param string $column
129
     * @param mixed $value
130
     */
131
    private function filterValue(Builder $builder, string $column, $value): void
132
    {
133
        $builder->where($column, $value);
134
    }
135
136
137
    /**
138
     * Generate a new query builder for the model.
139
     * @return Builder
140
     */
141
    private function createModelQueryBuilder(): Builder
142
    {
143
        $class = $this->newModelInstance();
144
145
        return $class->newQuery();
146
    }
147
148
    /**
149
     * Creates a new model instance.
150
     * @return Model
151
     */
152
    private function newModelInstance(): Model
153
    {
154
        $classFQDN = $this->getModelClass();
155
156
        return new $classFQDN;
157
    }
158
159
    /**
160
     * Finds the model instance.
161
     * @param int $id
162
     * @param Request|null $request
163
     * @return Model
164
     */
165
    private function findModel($id, Request $request = null): Model
166
    {
167
        /** @var Model|Builder $class */
168
        $class = $this->newModelInstance();
169
170
        if ($request) {
171
            $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...
172
        }
173
174
        $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...
175
        return $class;
176
    }
177
178
    /**
179
     * Build and return a response.
180
     * @param Request $request
181
     * @param mixed $data
182
     * @param string $method
183
     * @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector
184
     */
185
    private function return(Request $request, $data, string $method)
186
    {
187
188
        $status = SymResponse::HTTP_OK;
189
        switch ($method) {
190
            case 'store':
191
                $status = SymResponse::HTTP_CREATED;
192
                break;
193
        }
194
195
        if ($request->wantsJson()) {
196
            return app(ResponseFactory::class)->json($data, $status);
197
        }
198
199
        if (isset($this->views[$method]) && app(Factory::class)->exists($this->views[$method])) {
200
            /** @var View $view */
201
            $view = view($this->views[$method], [
202
                'data' => $data
203
            ]);
204
205
            return response($view, $status);
206
        }
207
208
        switch ($method) {
209
            case 'store':
210
            case 'update':
211
                // If it's store/update, and the user isn't asking for JSON, we want to
212
                // try and redirect them to the related show record page.
213
                /** @var Route|null $route */
214
                $route = $request->route();
215
                if ($route) {
216
                    $name = $route->getName();
217
                    if ($name) {
218
                        $exploded = explode('.', $name);
219
                        array_pop($exploded);
220
                        $topLevel = array_pop($exploded);
221
222
                        if ($topLevel) {
223
                            return redirect(route("$topLevel.show", [
224
                                str_replace('-', '_', $topLevel) => $data->id
225
                            ]));
226
                        }
227
                    }
228
                }
229
                break;
230
        }
231
232
        return app(ResponseFactory::class)->json($data, $status);
233
    }
234
235
    /**
236
     * Preload any relationships required.
237
     * @param Model $class
238
     * @param Request $request
239
     * @return void
240
     */
241
    private function preloadRelationships(Model &$class, Request $request): void
242
    {
243
        $header = $request->headers->get('X-Load-Relationship');
244
        if (!$header) {
245
            return;
246
        }
247
248
        $relationships = explode(',', $header);
249
        if (!$relationships) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $relationships of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
250
            return;
251
        }
252
253
        $class = $class->with($relationships);
254
    }
255
}
256