1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Laravel\DataTables\Services; |
4
|
|
|
|
5
|
|
|
use Arr; |
6
|
|
|
use Illuminate\Database\Eloquent\Builder; |
7
|
|
|
use Illuminate\Database\Eloquent\Collection; |
8
|
|
|
use Illuminate\Database\Eloquent\Model; |
9
|
|
|
use Illuminate\Database\QueryException; |
10
|
|
|
use Illuminate\Http\Request; |
11
|
|
|
use Illuminate\Support\Traits\Macroable; |
12
|
|
|
use Laravel\DataTables\Contracts\Displayable; |
13
|
|
|
use Laravel\DataTables\Exceptions\EloquentBuilderWasSetToNullException; |
14
|
|
|
use Laravel\DataTables\Exceptions\InvalidColumnSearchException; |
15
|
|
|
use Schema; |
16
|
|
|
|
17
|
|
|
abstract class BaseDataTableService implements Displayable |
18
|
|
|
{ |
19
|
|
|
use Macroable; |
|
|
|
|
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Load the relationships associated with the collection that will be returned. |
23
|
|
|
* |
24
|
|
|
* @var array |
25
|
|
|
*/ |
26
|
|
|
public $relations; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Get/Set the eloquent builder. |
30
|
|
|
* |
31
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
32
|
|
|
*/ |
33
|
|
|
public function builder(): Builder |
34
|
|
|
{ |
35
|
|
|
/** |
36
|
|
|
* @var mixed |
37
|
|
|
*/ |
38
|
|
|
static $builder = null; |
39
|
|
|
|
40
|
|
|
if (! is_null($builder) && ! app()->environment('testing')) { |
|
|
|
|
41
|
|
|
return $builder; |
42
|
|
|
} |
43
|
|
|
$builder = $this->query()->newQuery(); |
44
|
|
|
|
45
|
|
|
return $builder; |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @param $columns |
50
|
|
|
*/ |
51
|
|
|
public function getColumnsWithoutPrimaryKey($columns) |
52
|
|
|
{ |
53
|
|
|
$primaryKey = $this->getModel()->getKeyName(); |
54
|
|
|
|
55
|
|
|
return array_filter($columns, function ($column) use ($primaryKey) { |
56
|
|
|
return $primaryKey !== $column; |
57
|
|
|
}); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Get Custom Column Names. |
62
|
|
|
* |
63
|
|
|
* @return array |
64
|
|
|
*/ |
65
|
|
|
public function getCustomColumnNames(): array |
66
|
|
|
{ |
67
|
|
|
if (method_exists($model = $this->getModel(), 'getCustomColumnNames')) { |
68
|
|
|
return $model->getCustomColumnNames(); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
return []; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Get displayable columns. |
76
|
|
|
* |
77
|
|
|
* @return array |
78
|
|
|
*/ |
79
|
|
|
public function getDisplayableColumns(): array |
80
|
|
|
{ |
81
|
|
|
if (method_exists($model = $this->getModel(), 'getDisplayableColumns')) { |
82
|
|
|
return $model->getDisplayableColumns(); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
return array_diff( |
86
|
|
|
$this->getDatabaseColumnNames(), |
87
|
|
|
$this->getModel()->getHidden() |
88
|
|
|
); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @return mixed |
93
|
|
|
*/ |
94
|
|
|
public function getModel(): Model |
95
|
|
|
{ |
96
|
|
|
/** |
97
|
|
|
* @var mixed |
98
|
|
|
*/ |
99
|
|
|
static $model = null; |
100
|
|
|
if (! is_null($model)) { |
101
|
|
|
return $model; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
return $model = $this->builder()->getModel(); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Fetch records from the database. |
109
|
|
|
* |
110
|
|
|
* @param Request $request |
111
|
|
|
* @param callable $callback |
112
|
|
|
* @return Collection |
113
|
|
|
*/ |
114
|
|
|
public function getRecords(Request $request = null, callable $callback = null): Collection |
115
|
|
|
{ |
116
|
|
|
$builder = $this->builder(); |
117
|
|
|
|
118
|
|
|
// we will check if the request has a query string for search. the query string for searching must contain column, operator which identified at resolveQueryParts method in form of the keys of the array. and the value that user is trying to search for |
119
|
|
|
// example: http://localhost:8000/api/posts?column=title&operator=contains&value=hello |
120
|
|
|
if ($request && $this->hasSearchQuery($request)) { |
121
|
|
|
$builder = $this->buildSearchQuery($builder, $request); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
// Turn on the flexibility for the programmer to apply his own query to chain on the current query then we will retrieve back the query builder after the programmer applies his logic and proceed our own queries. |
125
|
|
|
if ($callback) { |
126
|
|
|
$builder = $callback($builder); |
127
|
|
|
throw_unless($builder, new EloquentBuilderWasSetToNullException); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
// we will try to parse the query and return the output of it, if anything goes wrong, by default we will be returning an empty collection. |
131
|
|
|
try { |
132
|
|
|
// if the request doesn't have a limit, it will return null, and since limit takes an integer value >= 0, then it won't limit |
133
|
|
|
// at all since we will replace the null with a negative number. |
134
|
|
|
return $builder->select(...$this->getSelectableColumns())->limit( |
135
|
|
|
$request->limit ?? -1 |
136
|
|
|
)->get(); |
137
|
|
|
} catch (QueryException $e) { |
138
|
|
|
return collect([]); |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @return mixed |
144
|
|
|
*/ |
145
|
|
|
public function getSelectableColumns(): array |
146
|
|
|
{ |
147
|
|
|
if (method_exists($model = $this->getModel(), 'getSelectableColumns')) { |
148
|
|
|
return $model->getSelectableColumns(); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
return $this->getDisplayableColumns(); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Get The Table Name. |
156
|
|
|
* |
157
|
|
|
* @return string |
158
|
|
|
*/ |
159
|
|
|
public function getTable(): string |
160
|
|
|
{ |
161
|
|
|
return $this->getModel()->getTable(); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* @return array |
166
|
|
|
*/ |
167
|
|
|
public function getUpdatableColumns(): array |
168
|
|
|
{ |
169
|
|
|
if (method_exists($this->getModel(), 'getUpdatableColumns')) { |
170
|
|
|
return $this->getColumnsWithoutPrimaryKey($this->getModel()->getUpdatableColumns()); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
return $this->getColumnsWithoutPrimaryKey($this->getDisplayableColumns()); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Get query source of dataTable. |
178
|
|
|
* |
179
|
|
|
* @return \Illuminate\Database\Eloquent\Builder |
180
|
|
|
*/ |
181
|
|
|
abstract public function query(): Builder; |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Return the response skeleton. |
185
|
|
|
* |
186
|
|
|
* @return array |
187
|
|
|
*/ |
188
|
|
|
public function response(callable $callback = null): array |
189
|
|
|
{ |
190
|
|
|
return [ |
191
|
|
|
'table' => $this->getTable(), |
192
|
|
|
'displayable' => $this->getDisplayableColumns(), |
193
|
|
|
'records' => $this->getRecords(request(), $callback)->load((array) $this->relations), |
194
|
|
|
'updatable' => $this->getUpdatableColumns(), |
195
|
|
|
'custom_columns' => $this->getCustomColumnNames(), |
196
|
|
|
'allow' => [ |
197
|
|
|
'creatable' => $this->allowCreating ?? false, |
|
|
|
|
198
|
|
|
'deletable' => $this->allowDeleting ?? false, |
|
|
|
|
199
|
|
|
'updatable' => $this->allowUpdating ?? false, |
|
|
|
|
200
|
|
|
], |
201
|
|
|
]; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Build Search Query. |
206
|
|
|
* |
207
|
|
|
* @param Builder $builder |
208
|
|
|
* @param Request $request |
209
|
|
|
* @return Builder |
210
|
|
|
*/ |
211
|
|
|
protected function buildSearchQuery(Builder $builder, Request $request): Builder |
212
|
|
|
{ |
213
|
|
|
[ 'operator' => $operator, 'value' => $value ] = $this->resolveQueryParts($request->operator, $request->value); |
214
|
|
|
|
215
|
|
|
throw_unless(in_array($request->column, $this->getDisplayableColumns()), InvalidColumnSearchException::class); |
216
|
|
|
|
217
|
|
|
return $builder->where($request->column, $operator, $value); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Get Database Column Names. |
222
|
|
|
* |
223
|
|
|
* @return array |
224
|
|
|
*/ |
225
|
|
|
protected function getDatabaseColumnNames(): array |
226
|
|
|
{ |
227
|
|
|
return Schema::getColumnListing($this->getTable()); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Check if the request has a search query. |
232
|
|
|
* |
233
|
|
|
* @param \Illuminate\Http\Request $request. |
234
|
|
|
* @return bool |
235
|
|
|
*/ |
236
|
|
|
protected function hasSearchQuery(Request $request): bool |
237
|
|
|
{ |
238
|
|
|
return count(array_filter($request->only(['column', 'operator', 'value']))) === 3; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Resolve Query Parts. |
243
|
|
|
* |
244
|
|
|
* @param string $operator |
245
|
|
|
* @param string $value |
246
|
|
|
* |
247
|
|
|
* @return array |
248
|
|
|
*/ |
249
|
|
|
protected function resolveQueryParts(string $operator, string $value): array |
250
|
|
|
{ |
251
|
|
|
return Arr::get([ |
252
|
|
|
'equals' => [ |
253
|
|
|
'operator' => '=', |
254
|
|
|
'value' => $value, |
255
|
|
|
], |
256
|
|
|
'contains' => [ |
257
|
|
|
'operator' => 'LIKE', |
258
|
|
|
'value' => "%{$value}%", |
259
|
|
|
], |
260
|
|
|
'starts_with' => [ |
261
|
|
|
'operator' => 'LIKE', |
262
|
|
|
'value' => "{$value}%", |
263
|
|
|
], |
264
|
|
|
'ends_with' => [ |
265
|
|
|
'operator' => 'LIKE', |
266
|
|
|
'value' => "%{$value}", |
267
|
|
|
|
268
|
|
|
], |
269
|
|
|
'greater_than' => [ |
270
|
|
|
'operator' => '>', |
271
|
|
|
'value' => $value, |
272
|
|
|
|
273
|
|
|
], |
274
|
|
|
'less_than' => [ |
275
|
|
|
'operator' => '<', |
276
|
|
|
'value' => $value, |
277
|
|
|
|
278
|
|
|
], |
279
|
|
|
], $operator); |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|