|
| 1 | +<?php |
| 2 | +namespace Laravel\Datatables\Services; |
| 3 | + |
| 4 | +use Arr; |
| 5 | +use Schema; |
| 6 | +use Illuminate\Http\Request; |
| 7 | +use Illuminate\Database\QueryException; |
| 8 | +use Illuminate\Support\Traits\Macroable; |
| 9 | +use Illuminate\Database\Eloquent\Builder; |
| 10 | +use Illuminate\Database\Eloquent\Collection; |
| 11 | +use Laravel\DataTables\Contracts\Displayable; |
| 12 | +use Laravel\DataTables\Exceptions\EloquentBuilderWasSetToNullException; |
| 13 | + |
| 14 | +abstract class BaseDatatableService implements Displayable |
| 15 | +{ |
| 16 | + use Macroable; |
| 17 | + |
| 18 | + /** |
| 19 | + * @var Builder |
| 20 | + */ |
| 21 | + public $builder; |
| 22 | + |
| 23 | + /** |
| 24 | + * load the relationships associated with the collection that will be returned. |
| 25 | + * @var array |
| 26 | + */ |
| 27 | + public $relations; |
| 28 | + |
| 29 | + /** |
| 30 | + * get/set the eloquent builder |
| 31 | + * @return Builder |
| 32 | + */ |
| 33 | + public function builder(): Builder |
| 34 | + { |
| 35 | + /** |
| 36 | + * @var mixed |
| 37 | + */ |
| 38 | + static $builder = null; |
| 39 | + |
| 40 | + if (!is_null($builder)) { |
| 41 | + return $builder; |
| 42 | + } |
| 43 | + $builder = $this->query(); |
| 44 | + |
| 45 | + return $builder; |
| 46 | + } |
| 47 | + |
| 48 | + public function filename() |
| 49 | + { |
| 50 | + return vsprintf('%name_%date', [ |
| 51 | + 'name' => ucfirst($this->getTable()), |
| 52 | + 'date' => date('Y-m-d H:i:s'), |
| 53 | + ]); |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * @param $columns |
| 58 | + */ |
| 59 | + public function getColumnsWithoutPrimaryKey($columns) |
| 60 | + { |
| 61 | + $primaryKey = $this->builder()->getModel()->getKeyName(); |
| 62 | + |
| 63 | + return array_filter($columns, fn($column) => $primaryKey !== $column); |
| 64 | + } |
| 65 | + |
| 66 | + /** |
| 67 | + * @return mixed |
| 68 | + */ |
| 69 | + public function getCustomColumnNames(): array |
| 70 | + { |
| 71 | + if (method_exists($this->builder()->getModel(), 'getCustomColumnNames')) { |
| 72 | + return $this->builder()->getModel()->getCustomColumnNames(); |
| 73 | + } |
| 74 | + |
| 75 | + return [ |
| 76 | + |
| 77 | + ]; |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * @return mixed |
| 82 | + */ |
| 83 | + public function getDisplayableColumns(): array |
| 84 | + { |
| 85 | + if (method_exists($this->builder()->getModel(), 'getDisplayableColumns')) { |
| 86 | + return array_values($this->builder()->getModel()->getDisplayableColumns()); |
| 87 | + } |
| 88 | + |
| 89 | + return array_values( |
| 90 | + array_diff( |
| 91 | + $this->getDatabaseColumnNames(), |
| 92 | + $this->builder()->getModel()->getHidden() |
| 93 | + ) |
| 94 | + ); |
| 95 | + } |
| 96 | + |
| 97 | + /** |
| 98 | + * @return Collection |
| 99 | + */ |
| 100 | + |
| 101 | + // |
| 102 | + public function getRecords(Request $request = null, callable $callback = null): Collection |
| 103 | + { |
| 104 | + $builder = $this->builder(); |
| 105 | + // 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 |
| 106 | + // example: http://localhost:8000/api/posts?column=title&operator=contains&value=hello |
| 107 | + if ($request && $this->hasSearchQuery($request)) { |
| 108 | + $builder = $this->buildSearchQuery($builder, $request); |
| 109 | + } |
| 110 | + // 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. |
| 111 | + if ($callback) { |
| 112 | + $builder = $callback($builder); |
| 113 | + throw_unless($builder, new EloquentBuilderWasSetToNullException); |
| 114 | + } |
| 115 | + // 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. |
| 116 | + try { |
| 117 | + return $builder->select(...$this->getSelectableColumns())->limit($request->limit)->get($this->getDisplayableColumns()); |
| 118 | + } catch (QueryException $e) { |
| 119 | + return collect([]); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * @return array |
| 125 | + */ |
| 126 | + public function getSelectableColumns(): array |
| 127 | + { |
| 128 | + return $this->getDatabaseColumnNames(); |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * @return string |
| 133 | + */ |
| 134 | + public function getTable(): string |
| 135 | + { |
| 136 | + return $this->builder()->getModel()->getTable(); |
| 137 | + } |
| 138 | + |
| 139 | + /** |
| 140 | + * get the columns that user can see at the frontend to update. |
| 141 | + * @return array |
| 142 | + */ |
| 143 | + public function getUpdatableColumns(): array |
| 144 | + { |
| 145 | + if (method_exists($this->builder()->getModel(), 'getUpdatableColumns')) { |
| 146 | + return array_values($this->getColumnsWithoutPrimaryKey($this->builder()->getModel()->getUpdatableColumns())); |
| 147 | + } |
| 148 | + |
| 149 | + return array_values($this->getColumnsWithoutPrimaryKey($this->getDisplayableColumns())); |
| 150 | + } |
| 151 | + |
| 152 | + abstract public function query(): Builder; |
| 153 | + |
| 154 | + /** |
| 155 | + * return the response skeleton. |
| 156 | + * @return array |
| 157 | + */ |
| 158 | + public function response(callable $callback = null): array |
| 159 | + { |
| 160 | + |
| 161 | + return [ |
| 162 | + 'table' => $this->getTable(), |
| 163 | + 'displayable' => $this->getDisplayableColumns(), |
| 164 | + 'records' => $this->getRecords(request(), $callback)->load((array) $this->relations), |
| 165 | + 'updatable' => $this->getUpdatableColumns(), |
| 166 | + 'custom_columns' => $this->getCustomColumnNames(), |
| 167 | + 'allow' => [ |
| 168 | + 'creatable' => $this->allowCreating ?? false, |
| 169 | + 'deletable' => $this->allowDeleting ?? false, |
| 170 | + 'updatable' => $this->allowUpdating ?? false, |
| 171 | + ], |
| 172 | + ]; |
| 173 | + } |
| 174 | + |
| 175 | + /** |
| 176 | + * @param Builder $builder |
| 177 | + */ |
| 178 | + protected function buildSearchQuery(Builder $builder, Request $request): Builder |
| 179 | + { |
| 180 | + ['operator' => $operator, 'value' => $value] = $this->resolveQueryParts($request->operator, $request->value); |
| 181 | + |
| 182 | + return $builder->where($request->column, $operator, $value); |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * [getDatabaseColumnNames] |
| 187 | + * @return array |
| 188 | + */ |
| 189 | + protected function getDatabaseColumnNames(): array |
| 190 | + { |
| 191 | + return Schema::getColumnListing($this->getTable()); |
| 192 | + } |
| 193 | + |
| 194 | + /** |
| 195 | + * @param $request |
| 196 | + * @return int |
| 197 | + */ |
| 198 | + protected function hasSearchQuery(Request $request): bool |
| 199 | + { |
| 200 | + return count(array_filter($request->only(['column', 'operator', 'value']))) === 3; |
| 201 | + } |
| 202 | + |
| 203 | + /** |
| 204 | + * @param $operator |
| 205 | + * @param $value |
| 206 | + * @return array |
| 207 | + */ |
| 208 | + protected function resolveQueryParts($operator, $value) |
| 209 | + { |
| 210 | + return Arr::get([ |
| 211 | + 'equals' => [ |
| 212 | + 'operator' => '=', |
| 213 | + 'value' => $value, |
| 214 | + ], |
| 215 | + 'contains' => [ |
| 216 | + 'operator' => 'LIKE', |
| 217 | + 'value' => "%{$value}%", |
| 218 | + ], |
| 219 | + 'starts_with' => [ |
| 220 | + 'operator' => 'LIKE', |
| 221 | + 'value' => "{$value}%", |
| 222 | + ], |
| 223 | + 'ends_with' => [ |
| 224 | + 'operator' => 'LIKE', |
| 225 | + 'value' => "%{$value}", |
| 226 | + |
| 227 | + ], |
| 228 | + 'greater_than' => [ |
| 229 | + 'operator' => '>', |
| 230 | + 'value' => $value, |
| 231 | + |
| 232 | + ], |
| 233 | + 'less_than' => [ |
| 234 | + 'operator' => '<', |
| 235 | + 'value' => $value, |
| 236 | + |
| 237 | + ], |
| 238 | + ], $operator); |
| 239 | + } |
| 240 | +} |
0 commit comments