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

conedevelopment / root / 20656991146

02 Jan 2026 11:33AM UTC coverage: 74.719% (-0.7%) from 75.421%
20656991146

push

github

iamgergo
fix

3458 of 4628 relevant lines covered (74.72%)

32.75 hits per line

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

69.6
/src/Fields/Field.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Cone\Root\Fields;
6

7
use Closure;
8
use Cone\Root\Filters\Filter;
9
use Cone\Root\Filters\RenderableFilter;
10
use Cone\Root\Support\Copyable;
11
use Cone\Root\Traits\Authorizable;
12
use Cone\Root\Traits\HasAttributes;
13
use Cone\Root\Traits\Makeable;
14
use Cone\Root\Traits\ResolvesVisibility;
15
use Illuminate\Contracts\Support\Arrayable;
16
use Illuminate\Contracts\Support\MessageBag;
17
use Illuminate\Database\Eloquent\Builder;
18
use Illuminate\Database\Eloquent\Model;
19
use Illuminate\Http\Request;
20
use Illuminate\Support\Arr;
21
use Illuminate\Support\Str;
22
use Illuminate\Support\Traits\Conditionable;
23
use Illuminate\Support\ViewErrorBag;
24
use JsonSerializable;
25

26
abstract class Field implements Arrayable, JsonSerializable
27
{
28
    use Authorizable;
29
    use Conditionable;
30
    use HasAttributes;
31
    use Makeable;
32
    use ResolvesVisibility;
33

34
    /**
35
     * The Blade template.
36
     */
37
    protected string $template = 'root::fields.input';
38

39
    /**
40
     * The hydrate resolver callback.
41
     */
42
    protected ?Closure $hydrateResolver = null;
43

44
    /**
45
     * The format resolver callback.
46
     */
47
    protected ?Closure $formatResolver = null;
48

49
    /**
50
     * The default value resolver callback.
51
     */
52
    protected ?Closure $defaultValueResolver = null;
53

54
    /**
55
     * The value resolver callback.
56
     */
57
    protected ?Closure $valueResolver = null;
58

59
    /**
60
     * The errors resolver callback.
61
     */
62
    protected ?Closure $errorsResolver = null;
63

64
    /**
65
     * The validation rules.
66
     */
67
    protected array $rules = [
68
        '*' => [],
69
        'create' => [],
70
        'update' => [],
71
    ];
72

73
    /**
74
     * The field label.
75
     */
76
    protected string $label;
77

78
    /**
79
     * The associated model attribute.
80
     */
81
    protected string $modelAttribute;
82

83
    /**
84
     * The field help text.
85
     */
86
    protected ?string $help = null;
87

88
    /**
89
     * The field prefix.
90
     */
91
    protected ?string $prefix = null;
92

93
    /**
94
     * The field suffix.
95
     */
96
    protected ?string $suffix = null;
97

98
    /**
99
     * The field model.
100
     */
101
    protected ?Model $model = null;
102

103
    /**
104
     * Indicates if the field should use the old value.
105
     */
106
    protected bool $withOldValue = true;
107

108
    /**
109
     * Indicates if the field has been hydrated.
110
     */
111
    protected bool $hydrated = false;
112

113
    /**
114
     * Indicates if the field is sortable.
115
     */
116
    protected bool|Closure $sortable = false;
117

118
    /**
119
     * Indicates if the field is searchable.
120
     */
121
    protected bool|Closure $searchable = false;
122

123
    /**
124
     * The filter query resolver callback.
125
     */
126
    protected ?Closure $searchQueryResolver = null;
127

128
    /**
129
     * Indicates if the field is filterable.
130
     */
131
    protected bool|Closure $filterable = false;
132

133
    /**
134
     * The filter query resolver callback.
135
     */
136
    protected ?Closure $filterQueryResolver = null;
137

138
    /**
139
     * Determine if the field is computed.
140
     */
141
    protected bool $computed = false;
142

143
    /**
144
     * Indicates whether the field is translatable.
145
     */
146
    protected bool|Closure $translatable = false;
147

148
    /**
149
     * Indicates whether the field is copyable.
150
     */
151
    protected bool|Closure $copyable = false;
152

153
    /**
154
     * Create a new field instance.
155
     */
156
    public function __construct(string $label, Closure|string|null $modelAttribute = null)
196✔
157
    {
158
        $this->computed = $modelAttribute instanceof Closure;
196✔
159

160
        $this->modelAttribute = $this->computed ? Str::random() : ($modelAttribute ?: Str::of($label)->lower()->snake()->value());
196✔
161

162
        $this->label($label);
196✔
163
        $this->name($this->modelAttribute);
196✔
164
        $this->id($this->modelAttribute);
196✔
165
        $this->class('form-control');
196✔
166
        $this->value($this->computed ? $modelAttribute : null);
196✔
167
    }
168

169
    /**
170
     * Get the model attribute.
171
     */
172
    public function getModelAttribute(): string
196✔
173
    {
174
        return $this->modelAttribute;
196✔
175
    }
176

177
    /**
178
     * Set the model attribute.
179
     */
180
    public function setModelAttribute(string $value): static
196✔
181
    {
182
        $this->modelAttribute = $value;
196✔
183

184
        return $this;
196✔
185
    }
186

187
    /**
188
     * Get the request key.
189
     */
190
    public function getRequestKey(): string
196✔
191
    {
192
        return str_replace('->', '.', $this->getModelAttribute());
196✔
193
    }
194

195
    /**
196
     * Get the validation key.
197
     */
198
    public function getValidationKey(): string
17✔
199
    {
200
        return $this->getRequestKey();
17✔
201
    }
202

203
    /**
204
     * Get the blade template.
205
     */
206
    public function getTemplate(): string
8✔
207
    {
208
        return $this->template;
8✔
209
    }
210

211
    /**
212
     * Set the label attribute.
213
     */
214
    public function label(string $value): static
196✔
215
    {
216
        $this->label = $value;
196✔
217

218
        return $this;
196✔
219
    }
220

221
    /**
222
     * Get the field label.
223
     */
224
    public function getLabel(): string
1✔
225
    {
226
        return $this->label;
1✔
227
    }
228

229
    /**
230
     * Set the "name" HTML attribute attribute.
231
     */
232
    public function name(string|Closure $value): static
196✔
233
    {
234
        $value = $value instanceof Closure ? call_user_func_array($value, [$this]) : $value;
196✔
235

236
        $value = preg_replace('/(?:\->|\.)(.+?)(?=(?:\->|\.)|$)/', '[$1]', $value);
196✔
237

238
        return $this->setAttribute('name', $value);
196✔
239
    }
240

241
    /**
242
     * Set the readonly attribute.
243
     */
244
    public function readonly(bool|Closure $value = true): static
3✔
245
    {
246
        return $this->setAttribute('readonly', $value);
3✔
247
    }
248

249
    /**
250
     * Set the "disabled" HTML attribute.
251
     */
252
    public function disabled(bool|Closure $value = true): static
×
253
    {
254
        return $this->setAttribute('disabled', $value);
×
255
    }
256

257
    /**
258
     * Set the "required" HTML attribute.
259
     */
260
    public function required(bool|Closure $value = true): static
196✔
261
    {
262
        return $this->setAttribute('required', $value);
196✔
263
    }
264

265
    /**
266
     * Set the "type" HTML attribute.
267
     */
268
    public function type(string|Closure $value): static
196✔
269
    {
270
        return $this->setAttribute('type', $value);
196✔
271
    }
272

273
    /**
274
     * Set the "placeholder" HTML attribute.
275
     */
276
    public function placeholder(string|Closure $value): static
3✔
277
    {
278
        return $this->setAttribute('placeholder', $value);
3✔
279
    }
280

281
    /**
282
     * Set the help attribute.
283
     */
284
    public function help(?string $value = null): static
×
285
    {
286
        $this->help = $value;
×
287

288
        return $this;
×
289
    }
290

291
    /**
292
     * Set the prefix attribute.
293
     */
294
    public function prefix(string $value): static
×
295
    {
296
        $this->prefix = $value;
×
297

298
        return $this;
×
299
    }
300

301
    /**
302
     * Set the suffix attribute.
303
     */
304
    public function suffix(string $value): static
196✔
305
    {
306
        $this->suffix = $value;
196✔
307

308
        return $this;
196✔
309
    }
310

311
    /**
312
     * Set the sortable attribute.
313
     */
314
    public function sortable(bool|Closure $value = true): static
196✔
315
    {
316
        $this->sortable = $value;
196✔
317

318
        return $this;
196✔
319
    }
320

321
    /**
322
     * Determine if the field is sortable.
323
     */
324
    public function isSortable(): bool
19✔
325
    {
326
        if ($this->computed) {
19✔
327
            return false;
×
328
        }
329

330
        return $this->sortable instanceof Closure ? call_user_func($this->sortable) : $this->sortable;
19✔
331
    }
332

333
    /**
334
     * Set the searchable attribute.
335
     */
336
    public function searchable(bool|Closure $value = true, ?Closure $callback = null): static
196✔
337
    {
338
        $this->searchable = $value;
196✔
339

340
        $this->searchQueryResolver = $callback ?: function (Request $request, Builder $query, mixed $value, string $attribute): Builder {
196✔
341
            return $query->where($query->qualifyColumn($attribute), 'like', "%{$value}%", 'or');
2✔
342
        };
196✔
343

344
        return $this;
196✔
345
    }
346

347
    /**
348
     * Determine if the field is searchable.
349
     */
350
    public function isSearchable(): bool
20✔
351
    {
352
        if ($this->computed) {
20✔
353
            return false;
×
354
        }
355

356
        return $this->searchable instanceof Closure ? call_user_func($this->searchable) : $this->searchable;
20✔
357
    }
358

359
    /**
360
     * Resolve the search query.
361
     */
362
    public function resolveSearchQuery(Request $request, Builder $query, mixed $value): Builder
2✔
363
    {
364
        return $this->isSearchable()
2✔
365
            ? call_user_func_array($this->searchQueryResolver, [$request, $query, $value, $this->getModelAttribute()])
2✔
366
            : $query;
2✔
367
    }
368

369
    /**
370
     * Set the copyable attribute.
371
     */
372
    public function copyable(bool|Closure $value = true): static
×
373
    {
374
        $this->copyable = $value;
×
375

376
        return $this;
×
377
    }
378

379
    /**
380
     * Determine whether the field value is copyable.
381
     */
382
    public function isCopyable(): bool
20✔
383
    {
384
        return $this->copyable instanceof Closure ? call_user_func($this->copyable) : $this->copyable;
20✔
385
    }
386

387
    /**
388
     * Set the translatable attribute.
389
     */
390
    public function translatable(bool|Closure $value = true): static
×
391
    {
392
        $this->translatable = $value;
×
393

394
        return $this;
×
395
    }
396

397
    /**
398
     * Determine if the field is translatable.
399
     */
400
    public function isTranslatable(): bool
196✔
401
    {
402
        if ($this->computed) {
196✔
403
            return false;
×
404
        }
405

406
        return $this->translatable instanceof Closure ? call_user_func($this->translatable) : $this->translatable;
196✔
407
    }
408

409
    /**
410
     * Set the filterable attribute.
411
     */
412
    public function filterable(bool|Closure $value = true, ?Closure $callback = null): static
×
413
    {
414
        $this->filterable = $value;
×
415

416
        $this->filterQueryResolver = $callback ?: function (Request $request, Builder $query, mixed $value, string $attribute): Builder {
×
417
            return $query->where($query->qualifyColumn($attribute), $value);
×
418
        };
×
419

420
        return $this;
×
421
    }
422

423
    /**
424
     * Determine whether the field is filterable.
425
     */
426
    public function isFilterable(): bool
6✔
427
    {
428
        if ($this->computed) {
6✔
429
            return false;
×
430
        }
431

432
        return $this->filterable instanceof Closure ? call_user_func($this->filterable) : $this->filterable;
6✔
433
    }
434

435
    /**
436
     * Resolve the filter query.
437
     */
438
    public function resolveFilterQuery(Request $request, Builder $query, mixed $value): Builder
×
439
    {
440
        return $this->isFilterable()
×
441
            ? call_user_func_array($this->filterQueryResolver, [$request, $query, $value, $this->getModelAttribute()])
×
442
            : $query;
×
443
    }
444

445
    /**
446
     * Set the default value resolver.
447
     */
448
    public function default(mixed $value): static
×
449
    {
450
        if (! $value instanceof Closure) {
×
451
            $value = fn (): mixed => $value;
×
452
        }
453

454
        $this->defaultValueResolver = $value;
×
455

456
        return $this;
×
457
    }
458

459
    /**
460
     * Set the value resolver.
461
     */
462
    public function value(?Closure $callback = null): static
196✔
463
    {
464
        $this->valueResolver = $callback;
196✔
465

466
        return $this;
196✔
467
    }
468

469
    /**
470
     * Resolve the value.
471
     */
472
    public function resolveValue(Request $request, Model $model): mixed
25✔
473
    {
474
        if (! $this->hydrated && $this->withOldValue && $request->session()->hasOldInput($this->getRequestKey())) {
25✔
475
            $this->resolveHydrate($request, $model, $this->getOldValue($request));
×
476
        }
477

478
        $value = $this->getValue($model);
25✔
479

480
        if (is_null($value) && ! is_null($this->defaultValueResolver)) {
25✔
481
            $value = call_user_func_array($this->defaultValueResolver, [$request, $model]);
×
482
        }
483

484
        if (is_null($this->valueResolver)) {
25✔
485
            return $value;
22✔
486
        }
487

488
        return call_user_func_array($this->valueResolver, [$request, $model, $value]);
7✔
489
    }
490

491
    /**
492
     * Get the old value from the request.
493
     */
494
    public function getOldValue(Request $request): mixed
×
495
    {
496
        return $request->old($this->getRequestKey());
×
497
    }
498

499
    /**
500
     * Set the with old value attribute.
501
     */
502
    public function withOldValue(bool $value = true): static
×
503
    {
504
        $this->withOldValue = $value;
×
505

506
        return $this;
×
507
    }
508

509
    /**
510
     * Set the with old value attribute to false.
511
     */
512
    public function withoutOldValue(): static
×
513
    {
514
        return $this->withOldValue(false);
×
515
    }
516

517
    /**
518
     * Get the default value from the model.
519
     */
520
    public function getValue(Model $model): mixed
23✔
521
    {
522
        $attribute = $this->getModelAttribute();
23✔
523

524
        return match (true) {
525
            str_contains($attribute, '->') => data_get($model, str_replace('->', '.', $attribute)),
23✔
526
            default => $model->getAttribute($this->getModelAttribute()),
23✔
527
        };
528
    }
529

530
    /**
531
     * Set the format resolver.
532
     */
533
    public function format(?Closure $callback = null): static
1✔
534
    {
535
        $this->formatResolver = $callback;
1✔
536

537
        return $this;
1✔
538
    }
539

540
    /**
541
     * Format the value.
542
     */
543
    public function resolveFormat(Request $request, Model $model): ?string
20✔
544
    {
545
        $value = $this->resolveValue($request, $model);
20✔
546

547
        $formattedValue = match (true) {
20✔
548
            ! is_null($this->formatResolver) => call_user_func_array($this->formatResolver, [$request, $model, $value]),
20✔
549
            is_array($value) => json_encode($value),
14✔
550
            is_null($value) => $value,
14✔
551
            default => (string) $value,
10✔
552
        };
20✔
553

554
        if (! $this->isCopyable()) {
20✔
555
            return $formattedValue;
20✔
556
        }
557

558
        return (string) Copyable::make($formattedValue, match (true) {
×
559
            is_array($value) => json_encode($value),
×
560
            is_null($value) => $value,
×
561
            default => (string) $value,
×
562
        });
×
563
    }
564

565
    /**
566
     * Persist the request value on the model.
567
     */
568
    public function persist(Request $request, Model $model, mixed $value): void
9✔
569
    {
570
        $this->resolveHydrate($request, $model, $value);
9✔
571
    }
572

573
    /**
574
     * Get the value for hydrating the model.
575
     */
576
    public function getValueForHydrate(Request $request): mixed
9✔
577
    {
578
        return $request->input($this->getRequestKey());
9✔
579
    }
580

581
    /**
582
     * Set the field to hydrate on change event.
583
     */
584
    public function hydratesOnChange(): static
×
585
    {
586
        return $this->setAttribute(
×
587
            'x-on:change.debounce.250ms',
×
588
            '($event) => $event.target.dispatchEvent(new CustomEvent(\'hydrate\', { bubbles: true }))'
×
589
        );
×
590
    }
591

592
    /**
593
     * Set the hydrate resolver.
594
     */
595
    public function hydrate(Closure $callback): static
196✔
596
    {
597
        $this->hydrateResolver = $callback;
196✔
598

599
        return $this;
196✔
600
    }
601

602
    /**
603
     * Hydrate the model.
604
     */
605
    public function resolveHydrate(Request $request, Model $model, mixed $value): void
15✔
606
    {
607
        if ($this->computed) {
15✔
608
            return;
×
609
        }
610

611
        if (is_null($this->hydrateResolver)) {
15✔
612
            $this->hydrateResolver = function (Request $request, Model $model, $value): void {
10✔
613
                $model->setAttribute($this->getModelAttribute(), $value);
10✔
614
            };
10✔
615
        }
616

617
        call_user_func_array($this->hydrateResolver, [$request, $model, $value]);
15✔
618

619
        $this->hydrated = true;
15✔
620
    }
621

622
    /**
623
     * Set the validation rules.
624
     */
625
    public function rules(array|Closure $rules, string $context = '*'): static
196✔
626
    {
627
        $this->rules[$context] = $rules;
196✔
628

629
        return $this;
196✔
630
    }
631

632
    /**
633
     * Set the create validation rules.
634
     */
635
    public function createRules(array|Closure $rules): static
4✔
636
    {
637
        return $this->rules($rules, 'create');
4✔
638
    }
639

640
    /**
641
     * Set the update validation rules.
642
     */
643
    public function updateRules(array|Closure $rules): static
4✔
644
    {
645
        return $this->rules($rules, 'update');
4✔
646
    }
647

648
    /**
649
     * Get the validation rules.
650
     */
651
    public function getRules(): array
×
652
    {
653
        return $this->rules;
×
654
    }
655

656
    /**
657
     * Set the error resolver callback.
658
     */
659
    public function resolveErrorsUsing(Closure $callback): static
196✔
660
    {
661
        $this->errorsResolver = $callback;
196✔
662

663
        return $this;
196✔
664
    }
665

666
    /**
667
     * Resolve the validation errors.
668
     */
669
    public function resolveErrors(Request $request): MessageBag
9✔
670
    {
671
        return is_null($this->errorsResolver)
9✔
672
            ? $request->session()->get('errors', new ViewErrorBag)->getBag('default')
7✔
673
            : call_user_func_array($this->errorsResolver, [$request]);
9✔
674
    }
675

676
    /**
677
     * Determine if the field is invalid.
678
     */
679
    public function invalid(Request $request): bool
9✔
680
    {
681
        return $this->resolveErrors($request)->has($this->getValidationKey());
9✔
682
    }
683

684
    /**
685
     * Get the validation error from the request.
686
     */
687
    public function error(Request $request): ?string
9✔
688
    {
689
        return $this->resolveErrors($request)->first($this->getValidationKey()) ?: null;
9✔
690
    }
691

692
    /**
693
     * Convert the element to a JSON serializable format.
694
     */
695
    public function jsonSerialize(): array
×
696
    {
697
        return $this->toArray();
×
698
    }
699

700
    /**
701
     * Convert the field to an array.
702
     */
703
    public function toArray(): array
15✔
704
    {
705
        return [
15✔
706
            'attribute' => $this->getModelAttribute(),
15✔
707
            'help' => $this->help,
15✔
708
            'label' => $this->label,
15✔
709
            'prefix' => $this->prefix,
15✔
710
            'suffix' => $this->suffix,
15✔
711
            'template' => $this->template,
15✔
712
            'searchable' => $this->isSearchable(),
15✔
713
            'sortable' => $this->isSortable(),
15✔
714
        ];
15✔
715
    }
716

717
    /**
718
     * Get the form component data.
719
     */
720
    public function toDisplay(Request $request, Model $model): array
15✔
721
    {
722
        return array_merge($this->toArray(), [
15✔
723
            'value' => $this->resolveValue($request, $model),
15✔
724
            'formattedValue' => $this->resolveFormat($request, $model),
15✔
725
        ]);
15✔
726
    }
727

728
    /**
729
     * Get the form component data.
730
     */
731
    public function toInput(Request $request, Model $model): array
9✔
732
    {
733
        if ($this->computed) {
9✔
734
            return [];
×
735
        }
736

737
        return array_merge($this->toDisplay($request, $model), [
9✔
738
            'attrs' => $this->newAttributeBag()->class([
9✔
739
                'form-control--invalid' => $this->invalid($request),
9✔
740
            ]),
9✔
741
            'error' => $this->error($request),
9✔
742
            'invalid' => $this->invalid($request),
9✔
743
        ]);
9✔
744
    }
745

746
    /**
747
     * Get the validation representation of the field.
748
     */
749
    public function toValidate(Request $request, Model $model): array
8✔
750
    {
751
        $key = $model->exists ? 'update' : 'create';
8✔
752

753
        $rules = array_map(
8✔
754
            static fn (array|Closure $rule): array => is_array($rule) ? $rule : call_user_func_array($rule, [$request, $model]),
8✔
755
            Arr::only($this->rules, array_unique(['*', $key]))
8✔
756
        );
8✔
757

758
        return [$this->getValidationKey() => Arr::flatten($rules, 1)];
8✔
759
    }
760

761
    /**
762
     * Get the filter representation of the field.
763
     */
764
    public function toFilter(): Filter
×
765
    {
766
        return new class($this) extends RenderableFilter
×
767
        {
×
768
            protected Field $field;
769

770
            public function __construct(Field $field)
771
            {
772
                parent::__construct($field->getModelAttribute());
×
773

774
                $this->field = $field;
×
775
            }
776

777
            public function apply(Request $request, Builder $query, mixed $value): Builder
778
            {
779
                return $this->field->resolveFilterQuery($request, $query, $value);
×
780
            }
781

782
            public function toField(): Field
783
            {
784
                return Text::make($this->field->getLabel(), $this->getRequestKey())
×
785
                    ->value(fn (Request $request): mixed => $this->getValue($request));
×
786
            }
787
        };
×
788
    }
789

790
    /**
791
     * Clone the field.
792
     */
793
    public function __clone(): void
×
794
    {
795
        //
796
    }
×
797
}
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