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\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, ValidatesRequests; |
38
|
|
|
|
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
|
|
|
foreach (collect($request->input())->except('page')->toArray() as $column => $value) { |
62
|
|
|
$this->filterValue($builder, $column, $value); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
$data = $builder->paginate(); |
66
|
|
|
|
67
|
|
|
return $this->return($request, $data, 'index'); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Handles creating a model. The C of CRUD |
72
|
|
|
* @param Request $request |
73
|
|
|
* @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector |
74
|
|
|
*/ |
75
|
|
View Code Duplication |
public function store(Request $request) |
|
|
|
|
76
|
|
|
{ |
77
|
|
|
$model = $this->newModelInstance(); |
78
|
|
|
|
79
|
|
|
foreach ((array)$request->input() as $column => $value) { |
80
|
|
|
$model->$column = $value; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
$model->save(); |
84
|
|
|
|
85
|
|
|
|
86
|
|
|
return $this->return($request, $this->findModel($model->getAttribute('id')), 'store'); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Shows a model. The R of CRUD. |
91
|
|
|
* @param Request $request |
92
|
|
|
* @param int $id |
93
|
|
|
* @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector |
94
|
|
|
*/ |
95
|
|
|
public function show(Request $request, $id) |
96
|
|
|
{ |
97
|
|
|
$model = $this->findModel($id, $request); |
98
|
|
|
|
99
|
|
|
$model = $this->iterateThroughChildren($model, $request); |
100
|
|
|
|
101
|
|
|
return $this->return($request, $model, 'show'); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Update a record. The U of CRUD. |
106
|
|
|
* @param Request $request |
107
|
|
|
* @param int $id |
108
|
|
|
* @return JsonResponse|RedirectResponse|ResponseFactory|Response|Redirector |
109
|
|
|
*/ |
110
|
|
View Code Duplication |
public function update(Request $request, $id) |
|
|
|
|
111
|
|
|
{ |
112
|
|
|
$model = $this->findModel($id); |
113
|
|
|
|
114
|
|
|
foreach ((array)$request->input() as $column => $value) { |
115
|
|
|
$model->$column = $value; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
$model->save(); |
119
|
|
|
|
120
|
|
|
return $this->return($request, $model, 'update'); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Destroy a model. The D of CRUD. |
125
|
|
|
* @param Request $request |
126
|
|
|
* @param int $id |
127
|
|
|
* @return ResponseFactory|Response |
128
|
|
|
* @throws Exception |
129
|
|
|
*/ |
130
|
|
|
public function destroy(Request $request, $id) |
|
|
|
|
131
|
|
|
{ |
132
|
|
|
$model = $this->findModel($id); |
133
|
|
|
|
134
|
|
|
$model->delete(); |
135
|
|
|
|
136
|
|
|
return response(null, SymResponse::HTTP_NO_CONTENT); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Return the _links |
142
|
|
|
* @param Request $request |
143
|
|
|
* @return ResponseFactory|JsonResponse|RedirectResponse|Response|Redirector |
144
|
|
|
*/ |
145
|
|
|
public function options(Request $request) |
146
|
|
|
{ |
147
|
|
|
$class = $this->newModelInstance(); |
148
|
|
|
|
149
|
|
|
if (!($class instanceof HasLinks)) { |
150
|
|
|
throw new InvalidArgumentException('OPTIONS only works for models implementing HasLinks'); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
return $this->return($request, $class->buildLinks(), 'options'); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Generate a new query builder for the model. |
158
|
|
|
* @return Builder |
159
|
|
|
*/ |
160
|
|
|
private function createModelQueryBuilder(): Builder |
161
|
|
|
{ |
162
|
|
|
$class = $this->newModelInstance(); |
163
|
|
|
|
164
|
|
|
return $class->newQuery(); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Creates a new model instance. |
169
|
|
|
* @return Model |
170
|
|
|
*/ |
171
|
|
|
private function newModelInstance(): Model |
172
|
|
|
{ |
173
|
|
|
$classFQDN = $this->getModelClass(); |
174
|
|
|
|
175
|
|
|
return app($classFQDN); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Looks for an "extra" param in the route, and if it exists, looks for relationships |
180
|
|
|
* based on that route. |
181
|
|
|
* @param Model $model |
182
|
|
|
* @param Request $request |
183
|
|
|
* @return LengthAwarePaginator|Collection|Model|mixed |
184
|
|
|
* @throws BadMethodCallException |
185
|
|
|
*/ |
186
|
|
|
private function iterateThroughChildren(Model $model, Request $request) |
187
|
|
|
{ |
188
|
|
|
|
189
|
|
|
// If there's no route or extra param, just return. |
190
|
|
|
if (!$request->route() || |
191
|
|
|
!($request->route() instanceof Route) || |
192
|
|
|
!$request->route()->parameter('extra')) { |
193
|
|
|
return $model; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
$parts = array_filter(explode('/', (string)$request->route()->parameter('extra'))); |
197
|
|
|
|
198
|
|
|
// Loop through the parts. |
199
|
|
|
foreach ($parts as $part) { |
200
|
|
|
// Look for an array accessor, "children[5]" for example. |
201
|
|
|
preg_match('/\[(\d+)]$/', $part, $matches); |
202
|
|
|
$offset = false; |
203
|
|
|
|
204
|
|
|
// If one was found, save the offset and remove it from $part. |
205
|
|
|
if (!empty($matches[0])) { |
206
|
|
|
$part = str_replace(array_shift($matches), '', $part); |
207
|
|
|
$offset = array_shift($matches); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
$model = $model->$part(); |
211
|
|
|
|
212
|
|
|
// If it's a relationship, see if it's paginate-able. |
213
|
|
|
if (stripos(get_class($model), 'Many') !== false) { |
214
|
|
|
/** @var BelongsToMany|HasMany|HasOneOrMany|MorphMany|MorphOneOrMany|MorphToMany $model */ |
215
|
|
|
$model = $model->paginate(); |
|
|
|
|
216
|
|
|
} elseif ($model instanceof Relation) { |
217
|
|
|
$model = $model->getResults(); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
// If there is an offset, get it. |
221
|
|
|
if ($offset !== false) { |
222
|
|
|
$model = $model[$offset]; |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
return $model; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* Finds the model instance. |
231
|
|
|
* @param int $id |
232
|
|
|
* @param Request|null $request |
233
|
|
|
* @return Model |
234
|
|
|
*/ |
235
|
|
|
private function findModel(int $id, Request $request = null): Model |
236
|
|
|
{ |
237
|
|
|
/** @var Model|Builder $class */ |
238
|
|
|
$class = $this->newModelInstance(); |
239
|
|
|
|
240
|
|
|
if ($request) { |
241
|
|
|
$this->preloadRelationships($class, $request); |
|
|
|
|
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
$class = $class->findOrFail($id); |
|
|
|
|
245
|
|
|
return $class; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Apply causes to the builder. |
250
|
|
|
* @param Builder $builder |
251
|
|
|
* @param string $column |
252
|
|
|
* @param mixed $value |
253
|
|
|
*/ |
254
|
|
|
private function filterValue(Builder $builder, string $column, $value): void |
255
|
|
|
{ |
256
|
|
|
$builder->where($column, $value); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Build and return a response. |
261
|
|
|
* @param Request $request |
262
|
|
|
* @param mixed $data |
263
|
|
|
* @param string $method |
264
|
|
|
* @return JsonResponse|ResponseFactory|Response|RedirectResponse|Redirector |
265
|
|
|
*/ |
266
|
|
|
private function return(Request $request, $data, string $method) |
267
|
|
|
{ |
268
|
|
|
|
269
|
|
|
$status = SymResponse::HTTP_OK; |
270
|
|
|
switch ($method) { |
271
|
|
|
case 'store': |
272
|
|
|
$status = SymResponse::HTTP_CREATED; |
273
|
|
|
break; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
if ($request->wantsJson()) { |
277
|
|
|
return app(ResponseFactory::class)->json($data, $status); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
if (isset($this->views[$method]) && app(Factory::class)->exists($this->views[$method])) { |
281
|
|
|
/** @var View $view */ |
282
|
|
|
$view = view($this->views[$method], [ |
283
|
|
|
'data' => $data |
284
|
|
|
]); |
285
|
|
|
|
286
|
|
|
return response($view, $status); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
switch ($method) { |
290
|
|
|
case 'store': |
291
|
|
|
case 'update': |
292
|
|
|
// If it's store/update, and the user isn't asking for JSON, we want to |
293
|
|
|
// try and redirect them to the related show record page. |
294
|
|
|
if (($redirect = $this->redirectToShowRoute($request, $data))) { |
295
|
|
|
return $redirect; |
296
|
|
|
} |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
return app(ResponseFactory::class)->json($data, $status); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* Redirects to the show route for the model if one exists. |
304
|
|
|
* @param Request $request |
305
|
|
|
* @param mixed $data |
306
|
|
|
* @return RedirectResponse|Redirector|null |
307
|
|
|
*/ |
308
|
|
|
private function redirectToShowRoute(Request $request, $data) |
309
|
|
|
{ |
310
|
|
|
/** @var Route|null $route */ |
311
|
|
|
$route = $request->route(); |
312
|
|
|
if (!$route) { |
313
|
|
|
return null; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
$name = $route->getName(); |
317
|
|
|
if (!$name) { |
318
|
|
|
return null; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
$exploded = explode('.', $name); |
322
|
|
|
array_pop($exploded); |
323
|
|
|
$topLevel = array_pop($exploded); |
324
|
|
|
|
325
|
|
|
if (!$topLevel) { |
326
|
|
|
return null; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
$key = Str::singular(str_replace('-', '_', $topLevel)); |
330
|
|
|
|
331
|
|
|
return redirect(route("$topLevel.show", [ |
332
|
|
|
$key => $data->id |
333
|
|
|
])); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Preload any relationships required. |
338
|
|
|
* @param Model $class |
339
|
|
|
* @param Request $request |
340
|
|
|
* @return void |
341
|
|
|
*/ |
342
|
|
|
private function preloadRelationships(Model &$class, Request $request): void |
343
|
|
|
{ |
344
|
|
|
$header = $request->headers->get('X-Load-Relationship'); |
345
|
|
|
if (!$header || empty($header)) { |
346
|
|
|
return; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
$relationships = array_filter(explode(',', $header)); |
350
|
|
|
|
351
|
|
|
$class = $class->with($relationships); |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
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.