mblarsen /
laravel-repository
| 1 | <?php |
||
| 2 | |||
| 3 | namespace Mblarsen\LaravelRepository; |
||
| 4 | |||
| 5 | use BadMethodCallException; |
||
| 6 | use Illuminate\Contracts\Auth\Access\Gate; |
||
| 7 | use Illuminate\Contracts\Pagination\LengthAwarePaginator; |
||
| 8 | use Illuminate\Database\Eloquent\Builder; |
||
| 9 | use Illuminate\Database\Eloquent\Collection; |
||
| 10 | use Illuminate\Database\Eloquent\Model; |
||
| 11 | use Illuminate\Pagination\LengthAwarePaginator as PaginationLengthAwarePaginator; |
||
| 12 | use Illuminate\Support\Str; |
||
| 13 | use InvalidArgumentException; |
||
| 14 | use Mblarsen\LaravelRepository\Traits\Filters; |
||
| 15 | use Mblarsen\LaravelRepository\Traits\IncludesRelations; |
||
| 16 | use Mblarsen\LaravelRepository\Traits\Sorts; |
||
| 17 | use Mblarsen\LaravelRepository\Traits\WrapsInResource; |
||
| 18 | |||
| 19 | /** |
||
| 20 | * @method allQuery($query = null): Builder |
||
| 21 | * @method allResources($query = null): \Illuminate\Http\Resources\Json\ResourceCollection |
||
| 22 | * @method createQuery(array $data): JsonBuilder |
||
| 23 | * @method createResource(array $data): JsonResource |
||
| 24 | * @method findQuery($id, $query = null): Builder |
||
| 25 | * @method findResource($id, $query = null): JsonResource |
||
| 26 | * @method listQuery($column = null, $query = null): Builder |
||
| 27 | * @method updateQuery(Model $model, array $data): Builder |
||
| 28 | * @method updateResource(Model $model, array $data): JsonResource |
||
| 29 | */ |
||
| 30 | class Repository |
||
| 31 | { |
||
| 32 | use Filters; |
||
| 33 | use IncludesRelations; |
||
| 34 | use Sorts; |
||
| 35 | use WrapsInResource; |
||
| 36 | |||
| 37 | /** @var string */ |
||
| 38 | protected $model; |
||
| 39 | |||
| 40 | /** @var ResourceContext $resource_context */ |
||
| 41 | protected $resource_context; |
||
| 42 | |||
| 43 | /** @var bool */ |
||
| 44 | protected $should_authorize = false; |
||
| 45 | |||
| 46 | /** @var string|callable $list_column */ |
||
| 47 | protected $default_list_column; |
||
| 48 | |||
| 49 | /** @var bool $only_query */ |
||
| 50 | protected $only_query = false; |
||
| 51 | |||
| 52 | 58 | public function __construct(ResourceContext $resource_context) |
|
| 53 | { |
||
| 54 | 58 | $this->resource_context = $resource_context; |
|
| 55 | 58 | $this->register(); |
|
| 56 | 58 | } |
|
| 57 | |||
| 58 | 54 | protected function register() |
|
| 59 | { |
||
| 60 | // Allows you to set default_list_column |
||
| 61 | 54 | } |
|
| 62 | |||
| 63 | /** |
||
| 64 | * Creates a new repository for a model. |
||
| 65 | * |
||
| 66 | * @param string $model model name |
||
| 67 | * @param array|ResourceContext $context |
||
| 68 | */ |
||
| 69 | 53 | public static function for(string $model, $context = null): self |
|
| 70 | { |
||
| 71 | /** @var Repository $repository */ |
||
| 72 | 53 | $repository = resolve(static::class); |
|
| 73 | 53 | $repository->setModel($model); |
|
| 74 | 53 | if ($context) { |
|
| 75 | 10 | $repository->setContext( |
|
| 76 | 10 | is_array($context) |
|
| 77 | 9 | ? ArrayResourceContext::create($context) |
|
| 78 | 10 | : $context |
|
| 79 | ); |
||
| 80 | } |
||
| 81 | 53 | return $repository; |
|
| 82 | } |
||
| 83 | |||
| 84 | 10 | public function __call($name, $arguments) |
|
| 85 | { |
||
| 86 | 10 | if ($this->canWrapInResource($name)) { |
|
| 87 | 5 | return $this->wrapInResource( |
|
| 88 | 5 | call_user_func_array([&$this, Str::before($name, 'Resource')], $arguments) |
|
| 89 | ); |
||
| 90 | } |
||
| 91 | |||
| 92 | 5 | if ($this->canReturnAsQuery($name)) { |
|
| 93 | try { |
||
| 94 | 3 | $this->only_query = true; |
|
| 95 | 3 | return call_user_func_array([&$this, Str::before($name, 'Query')], $arguments); |
|
| 96 | } finally { |
||
| 97 | 3 | $this->only_query = false; |
|
| 98 | } |
||
| 99 | } |
||
| 100 | |||
| 101 | 2 | throw new BadMethodCallException(); |
|
| 102 | } |
||
| 103 | |||
| 104 | 5 | private function canReturnAsQuery($name) |
|
| 105 | { |
||
| 106 | 5 | return Str::endsWith($name, 'Query') && |
|
| 107 | 3 | in_array( |
|
| 108 | 3 | Str::before($name, 'Query'), |
|
| 109 | 5 | ['all', 'find', 'list'] |
|
| 110 | ); |
||
| 111 | } |
||
| 112 | |||
| 113 | 10 | private function canWrapInResource($name) |
|
| 114 | { |
||
| 115 | 10 | return (Str::endsWith($name, 'Resource') || |
|
| 116 | 10 | Str::endsWith($name, 'Resources')) && |
|
| 117 | 6 | in_array( |
|
| 118 | 6 | Str::before($name, 'Resource'), |
|
| 119 | 10 | ['all', 'find', 'create', 'update', 'destroy'] |
|
| 120 | ); |
||
| 121 | } |
||
| 122 | |||
| 123 | /** |
||
| 124 | * Get the currenct resource context |
||
| 125 | */ |
||
| 126 | 4 | public function getContext(): ResourceContext |
|
| 127 | { |
||
| 128 | 4 | return $this->resource_context; |
|
| 129 | } |
||
| 130 | |||
| 131 | /** |
||
| 132 | * Set or replace the resource context |
||
| 133 | * |
||
| 134 | * @param ResourceContext|array $resource_context |
||
| 135 | * @param bool $set_allowed_with when true 'with' of the context is set to |
||
| 136 | * allowed with |
||
| 137 | * @return $this |
||
| 138 | */ |
||
| 139 | 32 | public function setContext($resource_context, bool $set_allowed_with = false) |
|
| 140 | { |
||
| 141 | 32 | $this->resource_context = is_array($resource_context) |
|
| 142 | 21 | ? ArrayResourceContext::create($resource_context) |
|
| 143 | 11 | : $resource_context; |
|
| 144 | |||
| 145 | 32 | if ($set_allowed_with) { |
|
| 146 | 1 | $this->setAllowedWith($this->resource_context->with()); |
|
| 147 | } |
||
| 148 | |||
| 149 | 32 | return $this; |
|
| 150 | } |
||
| 151 | |||
| 152 | /** |
||
| 153 | * Set the model |
||
| 154 | * |
||
| 155 | * @return $this |
||
| 156 | */ |
||
| 157 | 53 | public function setModel(string $model) |
|
| 158 | { |
||
| 159 | 53 | $this->model = $model; |
|
| 160 | |||
| 161 | 53 | return $this; |
|
| 162 | } |
||
| 163 | |||
| 164 | /** |
||
| 165 | * Toggle authorization |
||
| 166 | * |
||
| 167 | * @return $this |
||
| 168 | */ |
||
| 169 | 2 | public function shouldAuthorize(bool $value = true) |
|
| 170 | { |
||
| 171 | 2 | $this->should_authorize = $value; |
|
| 172 | |||
| 173 | 2 | return $this; |
|
| 174 | } |
||
| 175 | |||
| 176 | /** |
||
| 177 | * Return all models based on resource context and query |
||
| 178 | * |
||
| 179 | * The ResourceContext determines if the result is a Collection or a |
||
| 180 | * LengthAwarePaginator. |
||
| 181 | * |
||
| 182 | * @return LengthAwarePaginator|Collection|Builder |
||
| 183 | */ |
||
| 184 | 37 | public function all($query = null) |
|
| 185 | { |
||
| 186 | 37 | $this->authorize('viewAny', $this->model); |
|
| 187 | |||
| 188 | 37 | $query = $this->modelQuery($query); |
|
| 189 | |||
| 190 | $this |
||
| 191 | 37 | ->validateQurey($query) |
|
| 192 | 36 | ->applyWith($query) |
|
| 193 | 36 | ->applySort($query) |
|
| 194 | 35 | ->applyFilters($query); |
|
| 195 | |||
| 196 | 35 | if ($this->only_query) { |
|
| 197 | 3 | return $query; |
|
| 198 | } |
||
| 199 | |||
| 200 | 32 | return $this->execute($query); |
|
| 201 | } |
||
| 202 | |||
| 203 | /** |
||
| 204 | * Execute query |
||
| 205 | * |
||
| 206 | * @param Builder $query |
||
| 207 | * @return LengthAwarePaginator|Collection |
||
| 208 | */ |
||
| 209 | 32 | private function execute($query) |
|
| 210 | { |
||
| 211 | 32 | $page = $this->resource_context->page(); |
|
| 212 | 32 | $per_page = $this->resource_context->perPage(); |
|
| 213 | 32 | $should_paginate = $this->resource_context->paginate(); |
|
| 214 | |||
| 215 | 32 | return $should_paginate |
|
| 216 | 3 | ? $query->paginate($per_page, ['*'], 'page', $page) |
|
| 217 | 32 | : $query->get(); |
|
| 218 | } |
||
| 219 | |||
| 220 | /** |
||
| 221 | * Produces a result suitable for selects, lists, and autocomplete. All |
||
| 222 | * entries that has a 'value' and a 'label' key. |
||
| 223 | * |
||
| 224 | * Note: if a callable is used the mapping is performed in memory, while a |
||
| 225 | * string is done in the database layer. |
||
| 226 | * |
||
| 227 | * @param callable|string $column |
||
| 228 | * @param Builder $query |
||
| 229 | * @return Collection|Builder|LengthAwarePaginator |
||
| 230 | */ |
||
| 231 | 8 | public function list($column = null, $query = null) |
|
| 232 | { |
||
| 233 | 8 | $this->authorize('viewAny', $this->model); |
|
| 234 | |||
| 235 | 8 | $query = $this->modelQuery($query); |
|
| 236 | |||
| 237 | 8 | $column = $column |
|
| 238 | 2 | ?: $this->default_list_column |
|
| 239 | 8 | ?: $this->default_sort_by; |
|
| 240 | |||
| 241 | 8 | if (is_string($column)) { |
|
| 242 | 4 | $query->select([$query->getModel()->getKeyName() . " AS value", "$column AS label"]); |
|
| 243 | 4 | return $this->all($query); |
|
| 244 | } |
||
| 245 | |||
| 246 | $mapper = function (Model $model) use ($column) { |
||
| 247 | return [ |
||
| 248 | 3 | 'value' => $model->getKey(), |
|
| 249 | 3 | 'label' => $column($model) |
|
| 250 | ]; |
||
| 251 | 5 | }; |
|
| 252 | |||
| 253 | 5 | if (is_callable($column)) { |
|
| 254 | 4 | $all = $this->all($query); |
|
| 255 | |||
| 256 | 4 | if ($all instanceof Builder) { |
|
| 257 | 1 | return $all; |
|
| 258 | } |
||
| 259 | 3 | if ($all instanceof Collection) { |
|
| 260 | 2 | return $all->map($mapper); |
|
| 261 | } |
||
| 262 | 1 | if ($all instanceof LengthAwarePaginator) { |
|
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 263 | 1 | $items = collect($all->items())->map($mapper)->toArray(); |
|
| 264 | 1 | $all = new PaginationLengthAwarePaginator( |
|
| 265 | 1 | $items, |
|
| 266 | 1 | $all->total(), |
|
| 267 | 1 | $all->perPage(), |
|
| 268 | 1 | $all->currentPage() |
|
| 269 | ); |
||
| 270 | 1 | return $all; |
|
| 271 | } |
||
| 272 | } |
||
| 273 | |||
| 274 | 1 | throw new InvalidArgumentException("'column' should be a string or callable"); |
|
| 275 | } |
||
| 276 | |||
| 277 | /** |
||
| 278 | * @return Model|Builder |
||
| 279 | */ |
||
| 280 | 7 | public function find($id, $query = null) |
|
| 281 | { |
||
| 282 | 7 | $this->authorize('viewAny', $this->model); |
|
| 283 | |||
| 284 | 7 | $query = $this->modelQuery($query); |
|
| 285 | |||
| 286 | $this |
||
| 287 | 7 | ->validateQurey($query) |
|
| 288 | 7 | ->applyWith($query); |
|
| 289 | |||
| 290 | 7 | $query->whereId($id); |
|
| 291 | |||
| 292 | 7 | if ($this->only_query) { |
|
| 293 | 1 | return $query; |
|
| 294 | } |
||
| 295 | |||
| 296 | 6 | return $query->firstOrFail(); |
|
| 297 | } |
||
| 298 | |||
| 299 | 6 | public function create(array $data): Model |
|
| 300 | { |
||
| 301 | 6 | $this->authorize('create', $this->model); |
|
| 302 | |||
| 303 | 5 | return $this->model::create($data); |
|
| 304 | } |
||
| 305 | |||
| 306 | 1 | public function update(Model $model, array $data): Model |
|
| 307 | { |
||
| 308 | 1 | $this->authorize('update', $model); |
|
| 309 | |||
| 310 | 1 | $model->update($data); |
|
| 311 | |||
| 312 | 1 | return $model; |
|
| 313 | } |
||
| 314 | |||
| 315 | 1 | public function destroy(Model $model) |
|
| 316 | { |
||
| 317 | 1 | $this->authorize('destroy', $model); |
|
| 318 | |||
| 319 | 1 | $model->delete(); |
|
| 320 | 1 | } |
|
| 321 | |||
| 322 | 45 | protected function modelQuery($query = null) |
|
| 323 | { |
||
| 324 | 45 | return $query ?? $this->model::query(); |
|
| 325 | } |
||
| 326 | |||
| 327 | 43 | protected function validateQurey(Builder $query) |
|
| 328 | { |
||
| 329 | 43 | $model = $this->model; |
|
| 330 | 43 | $query_model = get_class($query->getModel()); |
|
| 331 | |||
| 332 | 43 | if ($model !== $query_model) { |
|
| 333 | 1 | throw new InvalidArgumentException("The input query and model does not match"); |
|
| 334 | } |
||
| 335 | |||
| 336 | 42 | return $this; |
|
| 337 | } |
||
| 338 | |||
| 339 | 50 | protected function authorize($ability, $arguments) |
|
| 340 | { |
||
| 341 | 50 | if ($this->should_authorize) { |
|
| 342 | 2 | app(Gate::class)->authorize($ability, $arguments); |
|
| 343 | } |
||
| 344 | |||
| 345 | 49 | return $this; |
|
| 346 | } |
||
| 347 | } |
||
| 348 |