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

conedevelopment / root / 14664184790

25 Apr 2025 12:06PM UTC coverage: 78.732% (-0.2%) from 78.973%
14664184790

push

github

iamgergo
wip

14 of 18 new or added lines in 6 files covered. (77.78%)

7 existing lines in 1 file now uncovered.

2595 of 3296 relevant lines covered (78.73%)

35.42 hits per line

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

89.24
/src/Fields/Relation.php
1
<?php
2

3
namespace Cone\Root\Fields;
4

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

38
/**
39
 * @template TRelation of \Illuminate\Database\Eloquent\Relations\Relation
40
 */
41
abstract class Relation extends Field implements Form
42
{
43
    use AsForm;
44
    use RegistersRoutes {
45
        RegistersRoutes::registerRoutes as __registerRoutes;
46
        RegistersRoutes::routeMatched as __routeMatched;
47
    }
48
    use ResolvesActions;
49
    use ResolvesFields;
50
    use ResolvesFilters;
51

52
    /**
53
     * The relation name on the model.
54
     */
55
    protected Closure|string $relation;
56

57
    /**
58
     * The searchable columns.
59
     */
60
    protected array $searchableColumns = ['id'];
61

62
    /**
63
     * The sortable column.
64
     */
65
    protected string $sortableColumn = 'id';
66

67
    /**
68
     * Indicates if the field should be nullable.
69
     */
70
    protected bool $nullable = false;
71

72
    /**
73
     * The Blade template.
74
     */
75
    protected string $template = 'root::fields.select';
76

77
    /**
78
     * The display resolver callback.
79
     */
80
    protected ?Closure $displayResolver = null;
81

82
    /**
83
     * The query resolver callback.
84
     */
85
    protected ?Closure $queryResolver = null;
86

87
    /**
88
     * Determine if the field is computed.
89
     */
90
    protected ?Closure $aggregateResolver = null;
91

92
    /**
93
     * Determine whether the relation values are aggregated.
94
     */
95
    protected bool $aggregated = false;
96

97
    /**
98
     * The option group resolver.
99
     */
100
    protected string|Closure|null $groupResolver = null;
101

102
    /**
103
     * Indicates whether the relation is a sub resource.
104
     */
105
    protected bool $asSubResource = false;
106

107
    /**
108
     * The relations to eager load on every query.
109
     */
110
    protected array $with = [];
111

112
    /**
113
     * The relations to eager load on every query.
114
     */
115
    protected array $withCount = [];
116

117
    /**
118
     * The query scopes.
119
     */
120
    protected static array $scopes = [];
121

122
    /**
123
     * The route key resolver.
124
     */
125
    protected ?Closure $routeKeyNameResolver = null;
126

127
    /**
128
     * Create a new relation field instance.
129
     */
130
    public function __construct(string $label, Closure|string|null $modelAttribute = null, Closure|string|null $relation = null)
131
    {
132
        parent::__construct($label, $modelAttribute);
198✔
133

134
        $this->relation = $relation ?: $this->getModelAttribute();
198✔
135
    }
136

137
    /**
138
     * Add a new scope for the relation query.
139
     */
140
    public static function scopeQuery(Closure $callback): void
141
    {
142
        static::$scopes[static::class][] = $callback;
×
143
    }
144

145
    /**
146
     * Get the relation instance.
147
     *
148
     * @phpstan-return TRelation
149
     */
150
    public function getRelation(Model $model): EloquentRelation
151
    {
152
        if ($this->relation instanceof Closure) {
24✔
153
            return call_user_func_array($this->relation, [$model]);
×
154
        }
155

156
        return call_user_func([$model, $this->relation]);
24✔
157
    }
158

159
    /**
160
     * Get the related model name.
161
     */
162
    public function getRelatedName(): string
163
    {
164
        return __(Str::of($this->getModelAttribute())->singular()->headline()->value());
198✔
165
    }
166

167
    /**
168
     * Get the relation name.
169
     */
170
    public function getRelationName(): string
171
    {
172
        return $this->relation instanceof Closure
198✔
173
            ? Str::afterLast($this->getModelAttribute(), '.')
198✔
174
            : $this->relation;
198✔
175
    }
176

177
    /**
178
     * Get the URI key.
179
     */
180
    public function getUriKey(): string
181
    {
182
        return str_replace('.', '-', $this->getRequestKey());
198✔
183
    }
184

185
    /**
186
     * Set the route key name resolver.
187
     */
188
    public function resolveRouteKeyNameUsing(Closure $callback): static
189
    {
190
        $this->routeKeyNameResolver = $callback;
198✔
191

192
        return $this;
198✔
193
    }
194

195
    /**
196
     * Get the related model's route key name.
197
     */
198
    public function getRouteKeyName(): string
199
    {
200
        $callback = is_null($this->routeKeyNameResolver)
198✔
201
            ? fn (): string => Str::of($this->getRelationName())->singular()->ucfirst()->prepend('relation')->value()
1✔
202
        : $this->routeKeyNameResolver;
198✔
203

204
        return call_user_func($callback);
198✔
205
    }
206

207
    /**
208
     * Get the route parameter name.
209
     */
210
    public function getRouteParameterName(): string
211
    {
212
        return 'field';
12✔
213
    }
214

215
    /**
216
     * Set the as subresource attribute.
217
     */
218
    public function asSubResource(bool $value = true): static
219
    {
220
        $this->asSubResource = $value;
198✔
221

222
        return $this;
198✔
223
    }
224

225
    /**
226
     * Determine if the relation is a subresource.
227
     */
228
    public function isSubResource(): bool
229
    {
230
        return $this->asSubResource;
198✔
231
    }
232

233
    /**
234
     * Set the nullable attribute.
235
     */
236
    public function nullable(bool $value = true): static
237
    {
238
        $this->nullable = $value;
198✔
239

240
        return $this;
198✔
241
    }
242

243
    /**
244
     * Determine if the field is nullable.
245
     */
246
    public function isNullable(): bool
247
    {
248
        return $this->nullable;
3✔
249
    }
250

251
    /**
252
     * {@inheritdoc}
253
     */
254
    public function searchable(bool|Closure $value = true, ?Closure $callback = null, array $columns = ['id']): static
255
    {
256
        $this->searchableColumns = $columns;
1✔
257

258
        $callback ??= function (Request $request, Builder $query, mixed $value, array $attributes): Builder {
1✔
259
            return $query->has($this->getModelAttribute(), '>=', 1, 'or', static function (Builder $query) use ($attributes, $value): Builder {
1✔
260
                foreach ($attributes as $attribute) {
1✔
261
                    $query->where(
1✔
262
                        $query->qualifyColumn($attribute),
1✔
263
                        'like',
1✔
264
                        "%{$value}%",
1✔
265
                        $attributes[0] === $attribute ? 'and' : 'or'
1✔
266
                    );
1✔
267
                }
268

269
                return $query;
1✔
270
            });
1✔
271
        };
1✔
272

273
        return parent::searchable($value, $callback);
1✔
274
    }
275

276
    /**
277
     * Get the searchable columns.
278
     */
279
    public function getSearchableColumns(): array
280
    {
281
        return $this->searchableColumns;
1✔
282
    }
283

284
    /**
285
     * Resolve the filter query.
286
     */
287
    public function resolveSearchQuery(Request $request, Builder $query, mixed $value): Builder
288
    {
289
        if (! $this->isSearchable()) {
1✔
NEW
290
            return parent::resolveSearchQuery($request, $query, $value);
×
291
        }
292

293
        return call_user_func_array($this->searchQueryResolver, [
1✔
294
            $request, $query, $value, $this->getSearchableColumns(),
1✔
295
        ]);
1✔
296
    }
297

298
    /**
299
     * Set the sortable attribute.
300
     */
301
    public function sortable(bool|Closure $value = true, string $column = 'id'): static
302
    {
303
        $this->sortableColumn = $column;
1✔
304

305
        return parent::sortable($value);
1✔
306
    }
307

308
    /**
309
     * Get the sortable columns.
310
     */
311
    public function getSortableColumn(): string
312
    {
313
        return $this->sortableColumn;
1✔
314
    }
315

316
    /**
317
     * {@inheritdoc}
318
     */
319
    public function isSortable(): bool
320
    {
321
        if ($this->isSubResource()) {
13✔
322
            return false;
10✔
323
        }
324

325
        return parent::isSortable();
9✔
326
    }
327

328
    /**
329
     * Set the translatable attribute.
330
     */
331
    public function translatable(bool|Closure $value = false): static
332
    {
333
        $this->translatable = false;
×
334

335
        return $this;
×
336
    }
337

338
    /**
339
     * Determine if the field is translatable.
340
     */
341
    public function isTranslatable(): bool
342
    {
343
        return false;
198✔
344
    }
345

346
    /**
347
     * Set the display resolver.
348
     */
349
    public function display(Closure|string $callback): static
350
    {
351
        if (is_string($callback)) {
198✔
352
            $callback = static fn (Model $model) => $model->getAttribute($callback);
198✔
353
        }
354

355
        $this->displayResolver = $callback;
198✔
356

357
        return $this;
198✔
358
    }
359

360
    /**
361
     * Resolve the display format or the query result.
362
     */
363
    public function resolveDisplay(Model $related): ?string
364
    {
365
        if (is_null($this->displayResolver)) {
8✔
366
            $this->display($related->getKeyName());
×
367
        }
368

369
        return call_user_func_array($this->displayResolver, [$related]);
8✔
370
    }
371

372
    /**
373
     * {@inheritdoc}
374
     */
375
    public function getValue(Model $model): mixed
376
    {
377
        if ($this->aggregated) {
11✔
378
            return parent::getValue($model);
×
379
        }
380

381
        $name = $this->getRelationName();
11✔
382

383
        if ($this->relation instanceof Closure && ! $model->relationLoaded($name)) {
11✔
384
            $model->setRelation($name, call_user_func_array($this->relation, [$model])->getResults());
3✔
385
        }
386

387
        return $model->getAttribute($name);
11✔
388
    }
389

390
    /**
391
     * {@inheritdoc}
392
     */
393
    public function resolveFormat(Request $request, Model $model): ?string
394
    {
395
        if (is_null($this->formatResolver)) {
6✔
396
            $this->formatResolver = function (Request $request, Model $model): mixed {
6✔
397
                $default = $this->getValue($model);
6✔
398

399
                if ($this->aggregated) {
6✔
400
                    return $default;
×
401
                }
402

403
                return Collection::wrap($default)->map(fn (Model $related): ?string => $this->formatRelated($request, $model, $related))->filter()->join(', ');
6✔
404
            };
6✔
405
        }
406

407
        return parent::resolveFormat($request, $model);
6✔
408
    }
409

410
    /**
411
     * Format the related model.
412
     */
413
    public function formatRelated(Request $request, Model $model, Model $related): ?string
414
    {
415
        $resource = Root::instance()->resources->forModel($related);
1✔
416

417
        $value = $this->resolveDisplay($related);
1✔
418

419
        if (! is_null($resource) && $related->exists && $resource->resolveAbility('view', $request, $related)) {
1✔
420
            $value = sprintf('<a href="%s" data-turbo-frame="_top">%s</a>', $resource->modelUrl($related), $value);
1✔
421
        }
422

423
        return $value;
1✔
424
    }
425

426
    /**
427
     * Define the filters for the object.
428
     */
429
    public function filters(Request $request): array
430
    {
431
        $fields = $this->resolveFields($request)->authorized($request);
2✔
432

433
        $searchables = $fields->searchable();
2✔
434

435
        $sortables = $fields->sortable();
2✔
436

437
        $filterables = $fields->filter(static function (Field $field): bool {
2✔
438
            return $field->isFilterable() && ! $field->isSearchable();
2✔
439
        });
2✔
440

441
        return array_values(array_filter([
2✔
442
            $searchables->isNotEmpty() ? new Search($searchables) : null,
2✔
443
            $sortables->isNotEmpty() ? new Sort($sortables) : null,
2✔
444
            ...$filterables->map->toFilter()->all(),
2✔
445
        ]));
2✔
446
    }
447

448
    /**
449
     * Handle the callback for the field resolution.
450
     */
451
    protected function resolveField(Request $request, Field $field): void
452
    {
453
        if ($this->isSubResource()) {
198✔
454
            $field->setAttribute('form', $this->modelAttribute);
198✔
455
            $field->resolveErrorsUsing(fn (Request $request): MessageBag => $this->errors($request));
198✔
456
        } else {
457
            $field->setAttribute('form', $this->getAttribute('form'));
×
458
            $field->resolveErrorsUsing($this->errorsResolver);
×
459
        }
460

461
        if ($field instanceof Relation) {
198✔
462
            $field->resolveRouteKeyNameUsing(
198✔
463
                fn (): string => Str::of($field->getRelationName())->singular()->ucfirst()->prepend($this->getRouteKeyName())->value()
198✔
464
            );
198✔
465
        }
466
    }
467

468
    /**
469
     * Handle the callback for the field resolution.
470
     */
471
    protected function resolveAction(Request $request, Action $action): void
472
    {
473
        $action->withQuery(function (Request $request): Builder {
×
474
            $model = $request->route('resourceModel');
×
475

476
            return $this->resolveFilters($request)->apply($request, $this->getRelation($model)->getQuery());
×
477
        });
×
478
    }
479

480
    /**
481
     * Handle the callback for the filter resolution.
482
     */
483
    protected function resolveFilter(Request $request, Filter $filter): void
484
    {
485
        $filter->setKey(sprintf('%s_%s', $this->getRequestKey(), $filter->getKey()));
3✔
486
    }
487

488
    /**
489
     * Set the query resolver.
490
     */
491
    public function withRelatableQuery(Closure $callback): static
492
    {
493
        $this->queryResolver = $callback;
198✔
494

495
        return $this;
198✔
496
    }
497

498
    /**
499
     * Resolve the related model's eloquent query.
500
     */
501
    public function resolveRelatableQuery(Request $request, Model $model): Builder
502
    {
503
        $query = $this->getRelation($model)
11✔
504
            ->getRelated()
11✔
505
            ->newQuery()
11✔
506
            ->with($this->with)
11✔
507
            ->withCount($this->withCount);
11✔
508

509
        foreach (static::$scopes[static::class] ?? [] as $scope) {
11✔
510
            $query = call_user_func_array($scope, [$request, $query, $model]);
×
511
        }
512

513
        return $query
11✔
514
            ->when(! is_null($this->queryResolver), fn (Builder $query): Builder => call_user_func_array($this->queryResolver, [$request, $query, $model]));
11✔
515
    }
516

517
    /**
518
     * Aggregate relation values.
519
     */
520
    public function aggregate(string $fn = 'count', string $column = '*'): static
521
    {
522
        $this->aggregateResolver = function (Request $request, Builder $query) use ($fn, $column): Builder {
523
            $this->setModelAttribute(sprintf(
×
524
                '%s_%s%s', $this->getRelationName(),
×
525
                $fn,
×
526
                $column === '*' ? '' : sprintf('_%s', $column)
×
527
            ));
×
528

529
            $this->aggregated = true;
×
530

531
            return $query->withAggregate($this->getRelationName(), $column, $fn);
×
532
        };
533

534
        return $this;
×
535
    }
536

537
    /**
538
     * Resolve the aggregate query.
539
     */
540
    public function resolveAggregate(Request $request, Builder $query): Builder
541
    {
542
        if (! is_null($this->aggregateResolver)) {
1✔
543
            $query = call_user_func_array($this->aggregateResolver, [$request, $query]);
×
544
        }
545

546
        return $query;
1✔
547
    }
548

549
    /**
550
     * Set the group resolver attribute.
551
     */
552
    public function groupOptionsBy(string|Closure $key): static
553
    {
554
        $this->groupResolver = $key;
×
555

556
        return $this;
×
557
    }
558

559
    /**
560
     * Resolve the options for the field.
561
     */
562
    public function resolveOptions(Request $request, Model $model): array
563
    {
564
        return $this->resolveRelatableQuery($request, $model)
3✔
565
            ->get()
3✔
566
            ->when(! is_null($this->groupResolver), fn (Collection $collection): Collection => $collection->groupBy($this->groupResolver)
3✔
567
                ->map(fn (Collection $group, string $key): array => [
3✔
568
                    'label' => $key,
3✔
569
                    'options' => $group->map(fn (Model $related): array => $this->toOption($request, $model, $related))->all(),
3✔
570
                ]), fn (Collection $collection): Collection => $collection->map(fn (Model $related): array => $this->toOption($request, $model, $related)))
3✔
571
            ->toArray();
3✔
572
    }
573

574
    /**
575
     * Make a new option instance.
576
     */
577
    public function newOption(Model $related, string $label): Option
578
    {
579
        return new Option($related->getKey(), $label);
2✔
580
    }
581

582
    /**
583
     * Get the per page options.
584
     */
585
    public function getPerPageOptions(): array
586
    {
587
        return [5, 10, 15, 25];
2✔
588
    }
589

590
    /**
591
     * Get the per page key.
592
     */
593
    public function getPerPageKey(): string
594
    {
595
        return sprintf('%s_per_page', $this->getRequestKey());
2✔
596
    }
597

598
    /**
599
     * Get the sort key.
600
     */
601
    public function getSortKey(): string
602
    {
603
        return sprintf('%s_sort', $this->getRequestKey());
2✔
604
    }
605

606
    /**
607
     * The relations to be eagerload.
608
     */
609
    public function with(array $with): static
610
    {
611
        $this->with = $with;
×
612

613
        return $this;
×
614
    }
615

616
    /**
617
     * The relation counts to be eagerload.
618
     */
619
    public function withCount(array $withCount): static
620
    {
621
        $this->withCount = $withCount;
×
622

623
        return $this;
×
624
    }
625

626
    /**
627
     * Paginate the given query.
628
     */
629
    public function paginate(Request $request, Model $model): LengthAwarePaginator
630
    {
631
        $relation = $this->getRelation($model);
2✔
632

633
        $this->resolveFilters($request)->apply($request, $relation->getQuery());
2✔
634

635
        return $relation
2✔
636
            ->with($this->with)
2✔
637
            ->withCount($this->withCount)
2✔
638
            ->latest()
2✔
639
            ->paginate(
2✔
640
                $request->input(
2✔
641
                    $this->getPerPageKey(),
2✔
642
                    $request->isTurboFrameRequest() ? 5 : $relation->getRelated()->getPerPage()
2✔
643
                )
2✔
644
            )->withQueryString();
2✔
645
    }
646

647
    /**
648
     * Map a related model.
649
     */
650
    public function mapRelated(Request $request, Model $model, Model $related): array
651
    {
652
        return [
2✔
653
            'id' => $related->getKey(),
2✔
654
            'url' => $this->relatedUrl($model, $related),
2✔
655
            'model' => $related->setRelation('related', $model),
2✔
656
            'fields' => $this->resolveFields($request)
2✔
657
                ->subResource(false)
2✔
658
                ->authorized($request, $related)
2✔
659
                ->visible('index')
2✔
660
                ->mapToDisplay($request, $related),
2✔
661
            'abilities' => $this->mapRelatedAbilities($request, $model, $related),
2✔
662
        ];
2✔
663
    }
664

665
    /**
666
     * Get the model URL.
667
     */
668
    public function modelUrl(Model $model): string
669
    {
670
        return str_replace('{resourceModel}', $model->exists ? $model->getKey() : 'create', $this->getUri());
14✔
671
    }
672

673
    /**
674
     * Get the related URL.
675
     */
676
    public function relatedUrl(Model $model, Model $related): string
677
    {
678
        return sprintf('%s/%s', $this->modelUrl($model), $related->getKey());
8✔
679
    }
680

681
    /**
682
     * {@inheritdoc}
683
     */
684
    public function persist(Request $request, Model $model, mixed $value): void
685
    {
686
        if ($this->isSubResource()) {
7✔
687
            $this->resolveFields($request)
4✔
688
                ->authorized($request, $model)
4✔
689
                ->visible($request->isMethod('POST') ? 'create' : 'update')
4✔
690
                ->persist($request, $model);
4✔
691
        } else {
692
            parent::persist($request, $model, $value);
5✔
693
        }
694
    }
695

696
    /**
697
     * Handle the request.
698
     */
699
    public function handleFormRequest(Request $request, Model $model): void
700
    {
701
        $this->validateFormRequest($request, $model);
4✔
702

703
        try {
704
            DB::beginTransaction();
4✔
705

706
            $this->persist($request, $model, $this->getValueForHydrate($request));
4✔
707

708
            $model->save();
4✔
709

710
            if (in_array(HasRootEvents::class, class_uses_recursive($model))) {
4✔
711
                $model->recordRootEvent(
×
712
                    $model->wasRecentlyCreated ? 'Created' : 'Updated',
×
713
                    $request->user()
×
714
                );
×
715
            }
716

717
            $this->saved($request, $model);
4✔
718

719
            DB::commit();
4✔
720
        } catch (Throwable $exception) {
×
721
            report($exception);
×
722

723
            DB::rollBack();
×
724

725
            throw new SaveFormDataException($exception->getMessage());
×
726
        }
727
    }
728

729
    /**
730
     * Handle the saved form event.
731
     */
732
    public function saved(Request $request, Model $model): void
733
    {
734
        //
735
    }
4✔
736

737
    /**
738
     * Resolve the resource model for a bound value.
739
     */
740
    public function resolveRouteBinding(Request $request, string $id): Model
741
    {
742
        return $this->getRelation($request->route()->parentOfParameter($this->getRouteKeyName()))->findOrFail($id);
4✔
743
    }
744

745
    /**
746
     * Register the routes using the given router.
747
     */
748
    public function registerRoutes(Request $request, Router $router): void
749
    {
750
        $this->__registerRoutes($request, $router);
198✔
751

752
        $router->prefix($this->getUriKey())->group(function (Router $router) use ($request): void {
198✔
753
            $this->resolveActions($request)->registerRoutes($request, $router);
198✔
754

755
            $router->prefix("{{$this->getRouteKeyName()}}")->group(function (Router $router) use ($request): void {
198✔
756
                $this->resolveFields($request)->registerRoutes($request, $router);
198✔
757
            });
198✔
758
        });
198✔
759

760
        $this->registerRouteConstraints($request, $router);
198✔
761

762
        $this->routesRegistered($request);
198✔
763
    }
764

765
    /**
766
     * Get the route middleware for the registered routes.
767
     */
768
    public function getRouteMiddleware(): array
769
    {
770
        return [
198✔
771
            sprintf('%s:field,resourceModel,%s', Authorize::class, $this->getRouteKeyName()),
198✔
772
        ];
198✔
773
    }
774

775
    /**
776
     * Handle the routes registered event.
777
     */
778
    protected function routesRegistered(Request $request): void
779
    {
780
        $uri = $this->getUri();
198✔
781
        $routeKeyName = $this->getRouteKeyName();
198✔
782

783
        Root::instance()->breadcrumbs->patterns([
198✔
784
            $this->getUri() => $this->label,
198✔
785
            sprintf('%s/create', $uri) => __('Add'),
198✔
786
            sprintf('%s/{%s}', $uri, $routeKeyName) => fn (Request $request): string => $this->resolveDisplay($request->route($routeKeyName)),
198✔
787
            sprintf('%s/{%s}/edit', $uri, $routeKeyName) => __('Edit'),
198✔
788
        ]);
198✔
789
    }
790

791
    /**
792
     * Handle the route matched event.
793
     */
794
    public function routeMatched(RouteMatched $event): void
795
    {
796
        $this->__routeMatched($event);
15✔
797

798
        $controller = $event->route->getController();
15✔
799

800
        $controller->middleware($this->getRouteMiddleware());
15✔
801

802
        $middleware = function (Request $request, Closure $next) use ($event): mixed {
15✔
803
            $ability = match ($event->route->getActionMethod()) {
15✔
804
                'index' => 'viewAny',
2✔
805
                'show' => 'view',
1✔
806
                'create' => 'create',
1✔
807
                'store' => 'create',
2✔
808
                'edit' => 'update',
1✔
809
                'update' => 'update',
2✔
810
                'destroy' => 'delete',
2✔
811
                default => $event->route->getActionMethod(),
4✔
812
            };
15✔
813

814
            Gate::allowIf($this->resolveAbility(
15✔
815
                $ability, $request, $request->route('resourceModel'), $request->route($this->getRouteParameterName())
15✔
816
            ));
15✔
817

818
            return $next($request);
15✔
819
        };
15✔
820

821
        $controller->middleware([$middleware]);
15✔
822
    }
823

824
    /**
825
     * Resolve the ability.
826
     */
827
    public function resolveAbility(string $ability, Request $request, Model $model, ...$arguments): bool
828
    {
829
        $policy = Gate::getPolicyFor($model);
16✔
830

831
        $ability .= Str::of($this->getModelAttribute())->singular()->studly()->value();
16✔
832

833
        return is_null($policy)
16✔
834
            || ! method_exists($policy, $ability)
16✔
835
            || Gate::allows($ability, [$model, ...$arguments]);
16✔
836
    }
837

838
    /**
839
     * Map the relation abilities.
840
     */
841
    public function mapRelationAbilities(Request $request, Model $model): array
842
    {
843
        return [
6✔
844
            'viewAny' => $this->resolveAbility('viewAny', $request, $model),
6✔
845
            'create' => $this->resolveAbility('create', $request, $model),
6✔
846
        ];
6✔
847
    }
848

849
    /**
850
     * Map the related model abilities.
851
     */
852
    public function mapRelatedAbilities(Request $request, Model $model, Model $related): array
853
    {
854
        return [
4✔
855
            'view' => $this->resolveAbility('view', $request, $model, $related),
4✔
856
            'update' => $this->resolveAbility('update', $request, $model, $related),
4✔
857
            'restore' => $this->resolveAbility('restore', $request, $model, $related),
4✔
858
            'delete' => $this->resolveAbility('delete', $request, $model, $related),
4✔
859
            'forceDelete' => $this->resolveAbility('forceDelete', $request, $model, $related),
4✔
860
        ];
4✔
861
    }
862

863
    /**
864
     * Register the routes.
865
     */
866
    public function routes(Router $router): void
867
    {
868
        if ($this->isSubResource()) {
198✔
869
            $router->get('/', [RelationController::class, 'index']);
198✔
870
            $router->get('/create', [RelationController::class, 'create']);
198✔
871
            $router->get("/{{$this->getRouteKeyName()}}", [RelationController::class, 'show']);
198✔
872
            $router->post('/', [RelationController::class, 'store']);
198✔
873
            $router->get("/{{$this->getRouteKeyName()}}/edit", [RelationController::class, 'edit']);
198✔
874
            $router->patch("/{{$this->getRouteKeyName()}}", [RelationController::class, 'update']);
198✔
875
            $router->delete("/{{$this->getRouteKeyName()}}", [RelationController::class, 'destroy']);
198✔
876
        }
877
    }
878

879
    /**
880
     * Register the route constraints.
881
     */
882
    public function registerRouteConstraints(Request $request, Router $router): void
883
    {
884
        $router->bind($this->getRouteKeyName(), fn (string $id, Route $route): Model => match ($id) {
198✔
885
            'create' => $this->getRelation($route->parentOfParameter($this->getRouteKeyName()))->make(),
6✔
886
            default => $this->resolveRouteBinding($router->getCurrentRequest(), $id),
6✔
887
        });
198✔
888
    }
889

890
    /**
891
     * Parse the given query string.
892
     */
893
    public function parseQueryString(string $url): array
894
    {
895
        $query = parse_url($url, PHP_URL_QUERY);
6✔
896

897
        parse_str($query, $result);
6✔
898

899
        return array_filter($result, fn (string $key): bool => str_starts_with($key, $this->getRequestKey()), ARRAY_FILTER_USE_KEY);
6✔
900
    }
901

902
    /**
903
     * Get the option representation of the model and the related model.
904
     */
905
    public function toOption(Request $request, Model $model, Model $related): array
906
    {
907
        $value = $this->resolveValue($request, $model);
5✔
908

909
        return $this->newOption($related, $this->resolveDisplay($related))
5✔
910
            ->selected(! is_null($value) && ($value instanceof Model ? $value->is($related) : $value->contains($related)))
5✔
911
            ->toArray();
5✔
912
    }
913

914
    /**
915
     * {@inheritdoc}
916
     */
917
    public function toInput(Request $request, Model $model): array
918
    {
919
        return array_merge(parent::toInput($request, $model), [
3✔
920
            'nullable' => $this->isNullable(),
3✔
921
            'options' => $this->resolveOptions($request, $model),
3✔
922
        ]);
3✔
923
    }
924

925
    /**
926
     * Get the sub resource representation of the relation
927
     */
928
    public function toSubResource(Request $request, Model $model): array
929
    {
930
        return array_merge($this->toArray(), [
6✔
931
            'key' => $this->modelAttribute,
6✔
932
            'baseUrl' => $this->modelUrl($model),
6✔
933
            'url' => URL::query($this->modelUrl($model), $this->parseQueryString($request->fullUrl())),
6✔
934
            'modelName' => $this->getRelatedName(),
6✔
935
            'abilities' => $this->mapRelationAbilities($request, $model),
6✔
936
        ]);
6✔
937
    }
938

939
    /**
940
     * Get the index representation of the relation.
941
     */
942
    public function toIndex(Request $request, Model $model): array
943
    {
944
        return array_merge($this->toSubResource($request, $model), [
2✔
945
            'template' => $request->isTurboFrameRequest() ? 'root::resources.relation' : 'root::resources.index',
2✔
946
            'title' => $this->label,
2✔
947
            'model' => $this->getRelation($model)->make()->setRelation('related', $model),
2✔
948
            'standaloneActions' => $this->resolveActions($request)
2✔
949
                ->authorized($request, $model)
2✔
950
                ->standalone()
2✔
951
                ->mapToForms($request, $model),
2✔
952
            'actions' => $this->resolveActions($request)
2✔
953
                ->authorized($request, $model)
2✔
954
                ->visible('index')
2✔
955
                ->standalone(false)
2✔
956
                ->mapToForms($request, $model),
2✔
957
            'data' => $this->paginate($request, $model)->through(fn (Model $related): array => $this->mapRelated($request, $model, $related)),
2✔
958
            'perPageOptions' => $this->getPerPageOptions(),
2✔
959
            'perPageKey' => $this->getPerPageKey(),
2✔
960
            'sortKey' => $this->getSortKey(),
2✔
961
            'filters' => $this->resolveFilters($request)
2✔
962
                ->authorized($request)
2✔
963
                ->renderable()
2✔
964
                ->map(static fn (RenderableFilter $filter): array => $filter->toField()->toInput($request, $model))
2✔
965
                ->all(),
2✔
966
            'activeFilters' => $this->resolveFilters($request)->active($request)->count(),
2✔
967
            'parentUrl' => URL::query($request->server('HTTP_REFERER'), $request->query()),
2✔
968
        ]);
2✔
969
    }
970

971
    /**
972
     * Get the create representation of the resource.
973
     */
974
    public function toCreate(Request $request, Model $model): array
975
    {
976
        return array_merge($this->toSubResource($request, $model), [
1✔
977
            'template' => 'root::resources.form',
1✔
978
            'title' => __('Create :model', ['model' => $this->getRelatedName()]),
1✔
979
            'model' => $related = $this->getRelation($model)->make()->setRelation('related', $model),
1✔
980
            'action' => $this->modelUrl($model),
1✔
981
            'uploads' => $this->hasFileField($request),
1✔
982
            'method' => 'POST',
1✔
983
            'fields' => $this->resolveFields($request)
1✔
984
                ->subResource(false)
1✔
985
                ->authorized($request, $related)
1✔
986
                ->visible('create')
1✔
987
                ->mapToInputs($request, $related),
1✔
988
        ]);
1✔
989
    }
990

991
    /**
992
     * Get the edit representation of the
993
     */
994
    public function toShow(Request $request, Model $model, Model $related): array
995
    {
996
        return array_merge($this->toSubResource($request, $model), [
1✔
997
            'template' => 'root::resources.show',
1✔
998
            'title' => $this->resolveDisplay($related),
1✔
999
            'model' => $related->setRelation('related', $model),
1✔
1000
            'action' => $this->relatedUrl($model, $related),
1✔
1001
            'fields' => $this->resolveFields($request)
1✔
1002
                ->subResource(false)
1✔
1003
                ->authorized($request, $related)
1✔
1004
                ->visible('show')
1✔
1005
                ->mapToDisplay($request, $related),
1✔
1006
            'actions' => $this->resolveActions($request)
1✔
1007
                ->authorized($request, $related)
1✔
1008
                ->visible('show')
1✔
1009
                ->standalone(false)
1✔
1010
                ->mapToForms($request, $related),
1✔
1011
            'abilities' => array_merge(
1✔
1012
                $this->mapRelationAbilities($request, $model),
1✔
1013
                $this->mapRelatedAbilities($request, $model, $related)
1✔
1014
            ),
1✔
1015
        ]);
1✔
1016
    }
1017

1018
    /**
1019
     * Get the edit representation of the
1020
     */
1021
    public function toEdit(Request $request, Model $model, Model $related): array
1022
    {
1023
        return array_merge($this->toSubResource($request, $model), [
1✔
1024
            'template' => 'root::resources.form',
1✔
1025
            'title' => __('Edit :model', ['model' => $this->resolveDisplay($related)]),
1✔
1026
            'model' => $related->setRelation('related', $model),
1✔
1027
            'action' => $this->relatedUrl($model, $related),
1✔
1028
            'method' => 'PATCH',
1✔
1029
            'uploads' => $this->hasFileField($request),
1✔
1030
            'fields' => $this->resolveFields($request)
1✔
1031
                ->subResource(false)
1✔
1032
                ->authorized($request, $related)
1✔
1033
                ->visible('update')
1✔
1034
                ->mapToInputs($request, $related),
1✔
1035
            'abilities' => array_merge(
1✔
1036
                $this->mapRelationAbilities($request, $model),
1✔
1037
                $this->mapRelatedAbilities($request, $model, $related)
1✔
1038
            ),
1✔
1039
        ]);
1✔
1040
    }
1041
}
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

© 2025 Coveralls, Inc