• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

conedevelopment / root / 8345888516

19 Mar 2024 03:35PM UTC coverage: 68.889% (-0.2%) from 69.122%
8345888516

push

github

iamgergo
wip

9 of 14 new or added lines in 3 files covered. (64.29%)

40 existing lines in 2 files now uncovered.

2108 of 3060 relevant lines covered (68.89%)

21.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.58
/src/Resources/Resource.php
1
<?php
2

3
namespace Cone\Root\Resources;
4

5
use Cone\Root\Actions\Action;
6
use Cone\Root\Exceptions\SaveFormDataException;
7
use Cone\Root\Fields\Field;
8
use Cone\Root\Fields\Relation;
9
use Cone\Root\Filters\Filter;
10
use Cone\Root\Filters\RenderableFilter;
11
use Cone\Root\Filters\Search;
12
use Cone\Root\Filters\Sort;
13
use Cone\Root\Filters\TrashStatus;
14
use Cone\Root\Http\Middleware\Authorize;
15
use Cone\Root\Interfaces\Form;
16
use Cone\Root\Root;
17
use Cone\Root\Traits\AsForm;
18
use Cone\Root\Traits\Authorizable;
19
use Cone\Root\Traits\RegistersRoutes;
20
use Cone\Root\Traits\ResolvesActions;
21
use Cone\Root\Traits\ResolvesFilters;
22
use Cone\Root\Traits\ResolvesWidgets;
23
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
24
use Illuminate\Contracts\Support\Arrayable;
25
use Illuminate\Contracts\Support\MessageBag;
26
use Illuminate\Database\Eloquent\Builder;
27
use Illuminate\Database\Eloquent\Model;
28
use Illuminate\Database\Eloquent\SoftDeletes;
29
use Illuminate\Http\Request;
30
use Illuminate\Routing\Events\RouteMatched;
31
use Illuminate\Routing\Router;
32
use Illuminate\Support\Collection;
33
use Illuminate\Support\Facades\DB;
34
use Illuminate\Support\Facades\Gate;
35
use Illuminate\Support\Str;
36
use Throwable;
37

38
abstract class Resource implements Arrayable, Form
39
{
40
    use AsForm;
41
    use Authorizable;
42
    use RegistersRoutes {
43
        RegistersRoutes::registerRoutes as __registerRoutes;
44
        RegistersRoutes::routeMatched as __routeMatched;
45
    }
46
    use ResolvesActions;
47
    use ResolvesFilters;
48
    use ResolvesWidgets;
49

50
    /**
51
     * The model class.
52
     */
53
    protected string $model;
54

55
    /**
56
     * The relations to eager load on every query.
57
     */
58
    protected array $with = [];
59

60
    /**
61
     * The relations to eager load on every query.
62
     */
63
    protected array $withCount = [];
64

65
    /**
66
     * The icon for the resource.
67
     */
68
    protected string $icon = 'archive';
69

70
    /**
71
     * The group for the resource.
72
     */
73
    protected string $group = 'General';
74

75
    /**
76
     * Boot the resource.
77
     */
78
    public function boot(Root $root): void
79
    {
80
        $root->routes(function (Router $router) use ($root): void {
155✔
81
            $this->registerRoutes($root->app['request'], $router);
155✔
82
        });
155✔
83

84
        $root->navigation->location('sidebar')->new(
155✔
85
            $this->getUri(),
155✔
86
            $this->getName(),
155✔
87
            ['icon' => $this->getIcon(), 'group' => __($this->group)],
155✔
88
        );
155✔
89
    }
90

91
    /**
92
     * Get the model for the resource.
93
     */
94
    public function getModel(): string
95
    {
96
        return $this->model;
155✔
97
    }
98

99
    /**
100
     * Get the key.
101
     */
102
    public function getKey(): string
103
    {
104
        return Str::of($this->getModel())->classBasename()->plural()->kebab()->value();
155✔
105
    }
106

107
    /**
108
     * Get the URI key.
109
     */
110
    public function getUriKey(): string
111
    {
112
        return $this->getKey();
155✔
113
    }
114

115
    /**
116
     * Get the route parameter name.
117
     */
118
    public function getRouteParameterName(): string
119
    {
120
        return '_resource';
19✔
121
    }
122

123
    /**
124
     * Get the name.
125
     */
126
    public function getName(): string
127
    {
128
        return __(Str::of($this->getModel())->classBasename()->headline()->plural()->value());
155✔
129
    }
130

131
    /**
132
     * Get the model name.
133
     */
134
    public function getModelName(): string
135
    {
136
        return __(Str::of($this->getModel())->classBasename()->value());
5✔
137
    }
138

139
    /**
140
     * Get the model instance.
141
     */
142
    public function getModelInstance(): Model
143
    {
144
        return new ($this->getModel());
21✔
145
    }
146

147
    /**
148
     * Set the resource icon.
149
     */
150
    public function icon(string $icon): static
151
    {
152
        $this->icon = $icon;
1✔
153

154
        return $this;
1✔
155
    }
156

157
    /**
158
     * Get the resource icon.
159
     */
160
    public function getIcon(): string
161
    {
162
        return $this->icon;
155✔
163
    }
164

165
    /**
166
     * Get the policy for the model.
167
     */
168
    public function getPolicy(): mixed
169
    {
170
        return Gate::getPolicyFor($this->getModel());
18✔
171
    }
172

173
    /**
174
     * Resolve the ability.
175
     */
176
    public function resolveAbility(string $ability, Request $request, Model $model, ...$arguments): bool
177
    {
178
        $policy = $this->getPolicy();
4✔
179

180
        return is_null($policy)
4✔
181
            || ! method_exists($policy, $ability)
4✔
182
            || call_user_func_array([$policy, $ability], [$request->user(), $model, ...$arguments]);
4✔
183
    }
184

185
    /**
186
     * Map the resource abilities.
187
     */
188
    public function mapResourceAbilities(Request $request): array
189
    {
190
        return [
4✔
191
            'viewAny' => $this->resolveAbility('viewAny', $request, $this->getModelInstance()),
4✔
192
            'create' => $this->resolveAbility('create', $request, $this->getModelInstance()),
4✔
193
        ];
4✔
194
    }
195

196
    /**
197
     * Map the model abilities.
198
     */
199
    public function mapModelAbilities(Request $request, Model $model): array
200
    {
201
        return [
3✔
202
            'view' => $this->resolveAbility('view', $request, $model),
3✔
203
            'update' => $this->resolveAbility('update', $request, $model),
3✔
204
            'restore' => $this->resolveAbility('restore', $request, $model),
3✔
205
            'delete' => $this->resolveAbility('delete', $request, $model),
3✔
206
            'forceDelete' => $this->resolveAbility('forceDelete', $request, $model),
3✔
207
        ];
3✔
208
    }
209

210
    /**
211
     * Set the relations to eagerload.
212
     */
213
    public function with(array $relations): static
214
    {
215
        $this->with = $relations;
1✔
216

217
        return $this;
1✔
218
    }
219

220
    /**
221
     * Set the relation counts to eagerload.
222
     */
223
    public function withCount(array $relations): static
224
    {
UNCOV
225
        $this->withCount = $relations;
×
226

227
        return $this;
×
228
    }
229

230
    /**
231
     * Make a new Eloquent query instance.
232
     */
233
    public function query(): Builder
234
    {
235
        return $this->getModelInstance()->newQuery()->with($this->with)->withCount($this->withCount);
18✔
236
    }
237

238
    /**
239
     * Resolve the query for the given request.
240
     */
241
    public function resolveQuery(Request $request): Builder
242
    {
243
        return $this->query();
18✔
244
    }
245

246
    /**
247
     * Resolve the filtered query for the given request.
248
     */
249
    public function resolveFilteredQuery(Request $request): Builder
250
    {
251
        return $this->resolveFilters($request)->apply($request, $this->resolveQuery($request));
3✔
252
    }
253

254
    /**
255
     * Resolve the route binding query.
256
     */
257
    public function resolveRouteBindingQuery(Request $request): Builder
258
    {
259
        return $this->resolveQuery($request)
14✔
260
            ->withoutEagerLoads()
14✔
261
            ->when(
14✔
262
                $this->isSoftDeletable(),
14✔
263
                static function (Builder $query): Builder {
14✔
UNCOV
264
                    return $query->withTrashed();
×
265
                }
14✔
266
            );
14✔
267
    }
268

269
    /**
270
     * Resolve the resource model for a bound value.
271
     */
272
    public function resolveRouteBinding(Request $request, string $id): Model
273
    {
274
        return $this->resolveRouteBindingQuery($request)->findOrFail($id);
14✔
275
    }
276

277
    /**
278
     * Determine if the model soft deletable.
279
     */
280
    public function isSoftDeletable(): bool
281
    {
282
        return in_array(SoftDeletes::class, class_uses_recursive($this->getModel()));
18✔
283
    }
284

285
    /**
286
     * Get the URL for the given model.
287
     */
288
    public function modelUrl(Model $model): string
289
    {
290
        return sprintf('%s/%s', $this->getUri(), $model->exists ? $model->getKey() : '');
5✔
291
    }
292

293
    /**
294
     * Get the title for the model.
295
     */
296
    public function modelTitle(Model $model): string
297
    {
298
        return $model->getKey();
6✔
299
    }
300

301
    /**
302
     * Define the filters for the object.
303
     */
304
    public function filters(Request $request): array
305
    {
306
        $fields = $this->resolveFields($request)->authorized($request);
4✔
307

308
        $searchables = $fields->searchable();
4✔
309

310
        $sortables = $fields->sortable();
4✔
311

312
        return array_values(array_filter([
4✔
313
            $searchables->isNotEmpty() ? new Search($searchables) : null,
4✔
314
            $sortables->isNotEmpty() ? new Sort($sortables) : null,
4✔
315
            $this->isSoftDeletable() ? new TrashStatus() : null,
4✔
316
        ]));
4✔
317
    }
318

319
    /**
320
     * Handle the callback for the field resolution.
321
     */
322
    protected function resolveField(Request $request, Field $field): void
323
    {
324
        $field->setAttribute('form', $this->getKey());
155✔
325
        $field->resolveErrorsUsing(fn (Request $request): MessageBag => $this->errors($request));
155✔
326

327
        if ($field instanceof Relation) {
155✔
328
            $field->resolveRouteKeyNameUsing(function () use ($field): string {
155✔
329
                return Str::of($field->getRelationName())->singular()->ucfirst()->prepend($this->getKey())->value();
155✔
330
            });
155✔
331
        }
332
    }
333

334
    /**
335
     * Handle the callback for the filter resolution.
336
     */
337
    protected function resolveFilter(Request $request, Filter $filter): void
338
    {
339
        $filter->setKey(sprintf('%s_%s', $this->getKey(), $filter->getKey()));
4✔
340
    }
341

342
    /**
343
     * Handle the callback for the action resolution.
344
     */
345
    protected function resolveAction(Request $request, Action $action): void
346
    {
347
        $action->withQuery(fn (): Builder => $this->resolveFilteredQuery($request));
155✔
348
    }
349

350
    /**
351
     * Get the per page options.
352
     */
353
    public function getPerPageOptions(): array
354
    {
355
        return Collection::make([$this->getModelInstance()->getPerPage()])
1✔
356
            ->merge([15, 25, 50, 100])
1✔
357
            ->filter()
1✔
358
            ->unique()
1✔
359
            ->values()
1✔
360
            ->toArray();
1✔
361
    }
362

363
    /**
364
     * Get the per page key.
365
     */
366
    public function getPerPageKey(): string
367
    {
368
        return sprintf('%s_per_page', $this->getKey());
1✔
369
    }
370

371
    /**
372
     * Get the sort key.
373
     */
374
    public function getSortKey(): string
375
    {
376
        return sprintf('%s_sort', $this->getKey());
1✔
377
    }
378

379
    /**
380
     * Perform the query and the pagination.
381
     */
382
    public function paginate(Request $request): LengthAwarePaginator
383
    {
384
        return $this->resolveFilteredQuery($request)
1✔
385
            ->latest()
1✔
386
            ->paginate($request->input($this->getPerPageKey()))
1✔
387
            ->withQueryString()
1✔
388
            ->through(function (Model $model) use ($request): array {
1✔
389
                return $this->mapModel($request, $model);
1✔
390
            });
1✔
391
    }
392

393
    /**
394
     * Map the model.
395
     */
396
    public function mapModel(Request $request, Model $model): array
397
    {
398
        return [
1✔
399
            'id' => $model->getKey(),
1✔
400
            'url' => $this->modelUrl($model),
1✔
401
            'model' => $model,
1✔
402
            'abilities' => $this->mapModelAbilities($request, $model),
1✔
403
            'fields' => $this->resolveFields($request)
1✔
404
                ->subResource(false)
1✔
405
                ->authorized($request, $model)
1✔
406
                ->visible('index')
1✔
407
                ->mapToDisplay($request, $model),
1✔
408
        ];
1✔
409
    }
410

411
    /**
412
     * Handle the request.
413
     */
414
    public function handleFormRequest(Request $request, Model $model): void
415
    {
416
        $this->validateFormRequest($request, $model);
3✔
417

418
        try {
419
            DB::beginTransaction();
3✔
420

421
            $this->resolveFields($request)
3✔
422
                ->authorized($request, $model)
3✔
423
                ->visible($request->isMethod('POST') ? 'create' : 'update')
3✔
424
                ->persist($request, $model);
3✔
425

426
            $this->saving($request, $model);
3✔
427

428
            $model->save();
3✔
429

430
            $this->saved($request, $model);
3✔
431

432
            DB::commit();
3✔
UNCOV
433
        } catch (Throwable $exception) {
×
UNCOV
434
            report($exception);
×
435

UNCOV
436
            DB::rollBack();
×
437

UNCOV
438
            throw new SaveFormDataException($exception->getMessage());
×
439
        }
440
    }
441

442
    /**
443
     * Handle the saving form event.
444
     */
445
    public function saving(Request $request, Model $model): void
446
    {
447
        //
448
    }
3✔
449

450
    /**
451
     * Handle the saved form event.
452
     */
453
    public function saved(Request $request, Model $model): void
454
    {
455
        //
456
    }
3✔
457

458
    /**
459
     * Register the routes.
460
     */
461
    public function registerRoutes(Request $request, Router $router): void
462
    {
463
        $this->__registerRoutes($request, $router);
155✔
464

465
        $router->group([
155✔
466
            'prefix' => $this->getUriKey(),
155✔
467
            'middleware' => $this->getRouteMiddleware(),
155✔
468
        ], function (Router $router) use ($request): void {
155✔
469
            $this->resolveActions($request)->registerRoutes($request, $router);
155✔
470
            $this->resolveWidgets($request)->registerRoutes($request, $router);
155✔
471

472
            $router->prefix('{resourceModel}')->group(function (Router $router) use ($request): void {
155✔
473
                $this->resolveFields($request)->registerRoutes($request, $router);
155✔
474
            });
155✔
475
        });
155✔
476
    }
477

478
    /**
479
     * Get the route middleware for the registered routes.
480
     */
481
    public function getRouteMiddleware(): array
482
    {
483
        return [
155✔
484
            Authorize::class.':_resource',
155✔
485
        ];
155✔
486
    }
487

488
    /**
489
     * Handle the route matched event.
490
     */
491
    public function routeMatched(RouteMatched $event): void
492
    {
493
        $event->route->defaults('resource', $this->getKey());
18✔
494

495
        $controller = $event->route->getController();
18✔
496

497
        $controller->middleware($this->getRouteMiddleware());
18✔
498

499
        if (! is_null($this->getPolicy())) {
18✔
UNCOV
500
            $controller->authorizeResource($this->getModel(), 'resourceModel');
×
501
        }
502

503
        $this->__routeMatched($event);
18✔
504
    }
505

506
    /**
507
     * Get the instance as an array.
508
     */
509
    public function toArray(): array
510
    {
511
        return [
4✔
512
            'icon' => $this->getIcon(),
4✔
513
            'key' => $this->getKey(),
4✔
514
            'model' => $this->getModel(),
4✔
515
            'modelName' => $this->getModelName(),
4✔
516
            'name' => $this->getName(),
4✔
517
            'uriKey' => $this->getUriKey(),
4✔
518
            'url' => $this->getUri(),
4✔
519
        ];
4✔
520
    }
521

522
    /**
523
     * Get the index representation of the resource.
524
     */
525
    public function toIndex(Request $request): array
526
    {
527
        return array_merge($this->toArray(), [
1✔
528
            'template' => 'root::resources.index',
1✔
529
            'title' => $this->getName(),
1✔
530
            'standaloneActions' => $this->resolveActions($request)
1✔
531
                ->authorized($request, $model = $this->getModelInstance())
1✔
532
                ->standalone()
1✔
533
                ->mapToForms($request, $model),
1✔
534
            'actions' => $this->resolveActions($request)
1✔
535
                ->authorized($request, $model = $this->getModelInstance())
1✔
536
                ->visible('index')
1✔
537
                ->standalone(false)
1✔
538
                ->mapToForms($request, $model),
1✔
539
            'data' => $this->paginate($request),
1✔
540
            'widgets' => $this->resolveWidgets($request)
1✔
541
                ->authorized($request)
1✔
542
                ->visible('index')
1✔
543
                ->mapToDisplay($request),
1✔
544
            'perPageOptions' => $this->getPerPageOptions(),
1✔
545
            'perPageKey' => $this->getPerPageKey(),
1✔
546
            'sortKey' => $this->getSortKey(),
1✔
547
            'filters' => $this->resolveFilters($request)
1✔
548
                ->authorized($request)
1✔
549
                ->renderable()
1✔
550
                ->map(function (RenderableFilter $filter) use ($request, $model): array {
1✔
551
                    return $filter->toField()->toInput($request, $model);
1✔
552
                })
1✔
553
                ->all(),
1✔
554
            'activeFilters' => $this->resolveFilters($request)->active($request)->count(),
1✔
555
            'url' => $this->getUri(),
1✔
556
            'abilities' => $this->mapResourceAbilities($request),
1✔
557
        ]);
1✔
558
    }
559

560
    /**
561
     * Get the create representation of the resource.
562
     */
563
    public function toCreate(Request $request): array
564
    {
565
        return array_merge($this->toArray(), [
1✔
566
            'template' => 'root::resources.form',
1✔
567
            'title' => __('Create :resource', ['resource' => $this->getModelName()]),
1✔
568
            'model' => $model = $this->getModelInstance(),
1✔
569
            'action' => $this->getUri(),
1✔
570
            'method' => 'POST',
1✔
571
            'uploads' => $this->hasFileField($request),
1✔
572
            'fields' => $this->resolveFields($request)
1✔
573
                ->subResource(false)
1✔
574
                ->authorized($request, $model)
1✔
575
                ->visible('create')
1✔
576
                ->mapToInputs($request, $model),
1✔
577
            'abilities' => $this->mapResourceAbilities($request),
1✔
578
        ]);
1✔
579
    }
580

581
    /**
582
     * Get the edit representation of the resource.
583
     */
584
    public function toShow(Request $request, Model $model): array
585
    {
586
        return array_merge($this->toArray(), [
1✔
587
            'template' => 'root::resources.show',
1✔
588
            'title' => sprintf('%s: %s', $this->getModelName(), $this->modelTitle($model)),
1✔
589
            'model' => $model,
1✔
590
            'action' => $this->modelUrl($model),
1✔
591
            'fields' => $this->resolveFields($request)
1✔
592
                ->subResource(false)
1✔
593
                ->authorized($request, $model)
1✔
594
                ->visible('show')
1✔
595
                ->mapToDisplay($request, $model),
1✔
596
            'actions' => $this->resolveActions($request)
1✔
597
                ->authorized($request, $model)
1✔
598
                ->visible('show')
1✔
599
                ->standalone(false)
1✔
600
                ->mapToForms($request, $model),
1✔
601
            'widgets' => $this->resolveWidgets($request)
1✔
602
                ->authorized($request, $model)
1✔
603
                ->visible('show')
1✔
604
                ->mapToDisplay($request),
1✔
605
            'relations' => $this->resolveFields($request)
1✔
606
                ->subResource()
1✔
607
                ->authorized($request, $model)
1✔
608
                ->map(static function (Relation $relation) use ($request, $model): array {
1✔
609
                    return array_merge($relation->toSubResource($request, $model), [
1✔
610
                        'url' => trim(sprintf('%s?%s', $relation->modelUrl($model), $request->getQueryString()), '?'),
1✔
611
                    ]);
1✔
612
                }),
1✔
613
            'abilities' => array_merge(
1✔
614
                $this->mapResourceAbilities($request),
1✔
615
                $this->mapModelAbilities($request, $model)
1✔
616
            ),
1✔
617
        ]);
1✔
618
    }
619

620
    /**
621
     * Get the edit representation of the resource.
622
     */
623
    public function toEdit(Request $request, Model $model): array
624
    {
625
        return array_merge($this->toArray(), [
1✔
626
            'template' => 'root::resources.form',
1✔
627
            'title' => __('Edit :resource: :model', ['resource' => $this->getModelName(), 'model' => $this->modelTitle($model)]),
1✔
628
            'model' => $model,
1✔
629
            'action' => $this->modelUrl($model),
1✔
630
            'method' => 'PATCH',
1✔
631
            'uploads' => $this->hasFileField($request),
1✔
632
            'fields' => $this->resolveFields($request)
1✔
633
                ->subResource(false)
1✔
634
                ->authorized($request, $model)
1✔
635
                ->visible('update')
1✔
636
                ->mapToInputs($request, $model),
1✔
637
            'abilities' => array_merge(
1✔
638
                $this->mapResourceAbilities($request),
1✔
639
                $this->mapModelAbilities($request, $model)
1✔
640
            ),
1✔
641
        ]);
1✔
642
    }
643
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc