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

conedevelopment / root / 20665784697

02 Jan 2026 08:09PM UTC coverage: 75.022% (-0.05%) from 75.076%
20665784697

push

github

iamgergo
fix

9 of 13 new or added lines in 2 files covered. (69.23%)

1 existing line in 1 file now uncovered.

3472 of 4628 relevant lines covered (75.02%)

32.79 hits per line

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

70.09
/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 is sortable.
110
     */
111
    protected bool|Closure $sortable = false;
112

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

118
    /**
119
     * The filter query resolver callback.
120
     */
121
    protected ?Closure $searchQueryResolver = null;
122

123
    /**
124
     * Indicates if the field is filterable.
125
     */
126
    protected bool|Closure $filterable = false;
127

128
    /**
129
     * The filter query resolver callback.
130
     */
131
    protected ?Closure $filterQueryResolver = null;
132

133
    /**
134
     * Determine if the field is computed.
135
     */
136
    protected bool $computed = false;
137

138
    /**
139
     * Indicates whether the field is translatable.
140
     */
141
    protected bool|Closure $translatable = false;
142

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

148
    /**
149
     * Create a new field instance.
150
     */
151
    public function __construct(string $label, Closure|string|null $modelAttribute = null)
196✔
152
    {
153
        $this->computed = $modelAttribute instanceof Closure;
196✔
154

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

157
        $this->label($label);
196✔
158
        $this->name($this->modelAttribute);
196✔
159
        $this->id($this->modelAttribute);
196✔
160
        $this->class('form-control');
196✔
161
        $this->value($this->computed ? $modelAttribute : null);
196✔
162
    }
163

164
    /**
165
     * Get the model attribute.
166
     */
167
    public function getModelAttribute(): string
196✔
168
    {
169
        return $this->modelAttribute;
196✔
170
    }
171

172
    /**
173
     * Set the model attribute.
174
     */
175
    public function setModelAttribute(string $value): static
196✔
176
    {
177
        $this->modelAttribute = $value;
196✔
178

179
        return $this;
196✔
180
    }
181

182
    /**
183
     * Get the request key.
184
     */
185
    public function getRequestKey(): string
196✔
186
    {
187
        return str_replace('->', '.', $this->getModelAttribute());
196✔
188
    }
189

190
    /**
191
     * Get the validation key.
192
     */
193
    public function getValidationKey(): string
17✔
194
    {
195
        return $this->getRequestKey();
17✔
196
    }
197

198
    /**
199
     * Get the blade template.
200
     */
201
    public function getTemplate(): string
8✔
202
    {
203
        return $this->template;
8✔
204
    }
205

206
    /**
207
     * Set the label attribute.
208
     */
209
    public function label(string $value): static
196✔
210
    {
211
        $this->label = $value;
196✔
212

213
        return $this;
196✔
214
    }
215

216
    /**
217
     * Get the field label.
218
     */
219
    public function getLabel(): string
1✔
220
    {
221
        return $this->label;
1✔
222
    }
223

224
    /**
225
     * Set the "name" HTML attribute attribute.
226
     */
227
    public function name(string|Closure $value): static
196✔
228
    {
229
        $value = $value instanceof Closure ? call_user_func_array($value, [$this]) : $value;
196✔
230

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

233
        return $this->setAttribute('name', $value);
196✔
234
    }
235

236
    /**
237
     * Set the readonly attribute.
238
     */
239
    public function readonly(bool|Closure $value = true): static
3✔
240
    {
241
        return $this->setAttribute('readonly', $value);
3✔
242
    }
243

244
    /**
245
     * Set the "disabled" HTML attribute.
246
     */
247
    public function disabled(bool|Closure $value = true): static
×
248
    {
249
        return $this->setAttribute('disabled', $value);
×
250
    }
251

252
    /**
253
     * Set the "required" HTML attribute.
254
     */
255
    public function required(bool|Closure $value = true): static
196✔
256
    {
257
        return $this->setAttribute('required', $value);
196✔
258
    }
259

260
    /**
261
     * Set the "type" HTML attribute.
262
     */
263
    public function type(string|Closure $value): static
196✔
264
    {
265
        return $this->setAttribute('type', $value);
196✔
266
    }
267

268
    /**
269
     * Set the "placeholder" HTML attribute.
270
     */
271
    public function placeholder(string|Closure $value): static
3✔
272
    {
273
        return $this->setAttribute('placeholder', $value);
3✔
274
    }
275

276
    /**
277
     * Set the help attribute.
278
     */
279
    public function help(?string $value = null): static
×
280
    {
281
        $this->help = $value;
×
282

283
        return $this;
×
284
    }
285

286
    /**
287
     * Set the prefix attribute.
288
     */
289
    public function prefix(string $value): static
×
290
    {
291
        $this->prefix = $value;
×
292

293
        return $this;
×
294
    }
295

296
    /**
297
     * Set the suffix attribute.
298
     */
299
    public function suffix(string $value): static
196✔
300
    {
301
        $this->suffix = $value;
196✔
302

303
        return $this;
196✔
304
    }
305

306
    /**
307
     * Set the sortable attribute.
308
     */
309
    public function sortable(bool|Closure $value = true): static
196✔
310
    {
311
        $this->sortable = $value;
196✔
312

313
        return $this;
196✔
314
    }
315

316
    /**
317
     * Determine if the field is sortable.
318
     */
319
    public function isSortable(): bool
19✔
320
    {
321
        if ($this->computed) {
19✔
322
            return false;
×
323
        }
324

325
        return $this->sortable instanceof Closure ? call_user_func($this->sortable) : $this->sortable;
19✔
326
    }
327

328
    /**
329
     * Set the searchable attribute.
330
     */
331
    public function searchable(bool|Closure $value = true, ?Closure $callback = null): static
196✔
332
    {
333
        $this->searchable = $value;
196✔
334

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

339
        return $this;
196✔
340
    }
341

342
    /**
343
     * Determine if the field is searchable.
344
     */
345
    public function isSearchable(): bool
20✔
346
    {
347
        if ($this->computed) {
20✔
348
            return false;
×
349
        }
350

351
        return $this->searchable instanceof Closure ? call_user_func($this->searchable) : $this->searchable;
20✔
352
    }
353

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

364
    /**
365
     * Set the copyable attribute.
366
     */
367
    public function copyable(bool|Closure $value = true): static
×
368
    {
369
        $this->copyable = $value;
×
370

371
        return $this;
×
372
    }
373

374
    /**
375
     * Determine whether the field value is copyable.
376
     */
377
    public function isCopyable(): bool
20✔
378
    {
379
        return $this->copyable instanceof Closure ? call_user_func($this->copyable) : $this->copyable;
20✔
380
    }
381

382
    /**
383
     * Set the translatable attribute.
384
     */
385
    public function translatable(bool|Closure $value = true): static
×
386
    {
387
        $this->translatable = $value;
×
388

389
        return $this;
×
390
    }
391

392
    /**
393
     * Determine if the field is translatable.
394
     */
395
    public function isTranslatable(): bool
196✔
396
    {
397
        if ($this->computed) {
196✔
398
            return false;
×
399
        }
400

401
        return $this->translatable instanceof Closure ? call_user_func($this->translatable) : $this->translatable;
196✔
402
    }
403

404
    /**
405
     * Set the filterable attribute.
406
     */
407
    public function filterable(bool|Closure $value = true, ?Closure $callback = null): static
×
408
    {
409
        $this->filterable = $value;
×
410

411
        $this->filterQueryResolver = $callback ?: function (Request $request, Builder $query, mixed $value, string $attribute): Builder {
×
412
            return $query->where($query->qualifyColumn($attribute), $value);
×
413
        };
×
414

415
        return $this;
×
416
    }
417

418
    /**
419
     * Determine whether the field is filterable.
420
     */
421
    public function isFilterable(): bool
6✔
422
    {
423
        if ($this->computed) {
6✔
424
            return false;
×
425
        }
426

427
        return $this->filterable instanceof Closure ? call_user_func($this->filterable) : $this->filterable;
6✔
428
    }
429

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

440
    /**
441
     * Set the default value resolver.
442
     */
443
    public function default(mixed $value): static
×
444
    {
445
        if (! $value instanceof Closure) {
×
446
            $value = fn (): mixed => $value;
×
447
        }
448

449
        $this->defaultValueResolver = $value;
×
450

451
        return $this;
×
452
    }
453

454
    /**
455
     * Set the value resolver.
456
     */
457
    public function value(?Closure $callback = null): static
196✔
458
    {
459
        $this->valueResolver = $callback;
196✔
460

461
        return $this;
196✔
462
    }
463

464
    /**
465
     * Hydrate the model from the old value.
466
     */
467
    public function hydrateFromRequest(Request $request, Model $model): void
25✔
468
    {
469
        once(function () use ($request, $model): void {
25✔
470
            if (
471
                in_array($request->method(), ['POST', 'PATCH'])
25✔
472
                && $request->isTurboFrameRequest()
25✔
473
                && $request->has($this->getRequestKey())
25✔
474
            ) {
UNCOV
475
                $this->resolveHydrate($request, $model, $this->getValueForHydrate($request));
×
476
            } elseif ($this->withOldValue && $request->session()->hasOldInput($this->getRequestKey())) {
25✔
477
                $this->resolveHydrate($request, $model, $this->getOldValue($request));
×
478
            }
479
        });
25✔
480
    }
481

482
    /**
483
     * Resolve the value.
484
     */
485
    public function resolveValue(Request $request, Model $model): mixed
25✔
486
    {
487
        $this->hydrateFromRequest($request, $model);
25✔
488

489
        $value = $this->getValue($model);
25✔
490

491
        if (is_null($value) && ! is_null($this->defaultValueResolver)) {
25✔
492
            $value = call_user_func_array($this->defaultValueResolver, [$request, $model]);
×
493
        }
494

495
        if (is_null($this->valueResolver)) {
25✔
496
            return $value;
23✔
497
        }
498

499
        return call_user_func_array($this->valueResolver, [$request, $model, $value]);
7✔
500
    }
501

502
    /**
503
     * Get the old value from the request.
504
     */
505
    public function getOldValue(Request $request): mixed
×
506
    {
507
        return $request->old($this->getRequestKey());
×
508
    }
509

510
    /**
511
     * Set the with old value attribute.
512
     */
513
    public function withOldValue(bool $value = true): static
×
514
    {
515
        $this->withOldValue = $value;
×
516

517
        return $this;
×
518
    }
519

520
    /**
521
     * Set the with old value attribute to false.
522
     */
523
    public function withoutOldValue(): static
×
524
    {
525
        return $this->withOldValue(false);
×
526
    }
527

528
    /**
529
     * Get the default value from the model.
530
     */
531
    public function getValue(Model $model): mixed
23✔
532
    {
533
        $attribute = $this->getModelAttribute();
23✔
534

535
        return match (true) {
536
            str_contains($attribute, '->') => data_get($model, str_replace('->', '.', $attribute)),
23✔
537
            default => $model->getAttribute($this->getModelAttribute()),
23✔
538
        };
539
    }
540

541
    /**
542
     * Set the format resolver.
543
     */
544
    public function format(?Closure $callback = null): static
1✔
545
    {
546
        $this->formatResolver = $callback;
1✔
547

548
        return $this;
1✔
549
    }
550

551
    /**
552
     * Format the value.
553
     */
554
    public function resolveFormat(Request $request, Model $model): ?string
20✔
555
    {
556
        $value = $this->resolveValue($request, $model);
20✔
557

558
        $formattedValue = match (true) {
20✔
559
            ! is_null($this->formatResolver) => call_user_func_array($this->formatResolver, [$request, $model, $value]),
20✔
560
            is_array($value) => json_encode($value),
14✔
561
            is_null($value) => $value,
14✔
562
            default => (string) $value,
10✔
563
        };
20✔
564

565
        if (! $this->isCopyable()) {
20✔
566
            return $formattedValue;
20✔
567
        }
568

569
        return (string) Copyable::make($formattedValue, match (true) {
×
570
            is_array($value) => json_encode($value),
×
571
            is_null($value) => $value,
×
572
            default => (string) $value,
×
573
        });
×
574
    }
575

576
    /**
577
     * Persist the request value on the model.
578
     */
579
    public function persist(Request $request, Model $model, mixed $value): void
9✔
580
    {
581
        $this->resolveHydrate($request, $model, $value);
9✔
582
    }
583

584
    /**
585
     * Get the value for hydrating the model.
586
     */
587
    public function getValueForHydrate(Request $request): mixed
9✔
588
    {
589
        return $request->input($this->getRequestKey());
9✔
590
    }
591

592
    /**
593
     * Set the field to hydrate on change event.
594
     */
595
    public function hydratesOnChange(): static
×
596
    {
597
        return $this->setAttribute(
×
598
            'x-on:change.debounce.250ms',
×
599
            '($event) => $event.target.dispatchEvent(new CustomEvent(\'hydrate\', { bubbles: true }))'
×
600
        );
×
601
    }
602

603
    /**
604
     * Set the hydrate resolver.
605
     */
606
    public function hydrate(Closure $callback): static
196✔
607
    {
608
        $this->hydrateResolver = $callback;
196✔
609

610
        return $this;
196✔
611
    }
612

613
    /**
614
     * Hydrate the model.
615
     */
616
    public function resolveHydrate(Request $request, Model $model, mixed $value): void
15✔
617
    {
618
        if ($this->computed) {
15✔
619
            return;
×
620
        }
621

622
        if (is_null($this->hydrateResolver)) {
15✔
623
            $this->hydrateResolver = function (Request $request, Model $model, $value): void {
10✔
624
                $model->setAttribute($this->getModelAttribute(), $value);
10✔
625
            };
10✔
626
        }
627

628
        call_user_func_array($this->hydrateResolver, [$request, $model, $value]);
15✔
629
    }
630

631
    /**
632
     * Set the validation rules.
633
     */
634
    public function rules(array|Closure $rules, string $context = '*'): static
196✔
635
    {
636
        $this->rules[$context] = $rules;
196✔
637

638
        return $this;
196✔
639
    }
640

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

649
    /**
650
     * Set the update validation rules.
651
     */
652
    public function updateRules(array|Closure $rules): static
4✔
653
    {
654
        return $this->rules($rules, 'update');
4✔
655
    }
656

657
    /**
658
     * Get the validation rules.
659
     */
660
    public function getRules(): array
×
661
    {
662
        return $this->rules;
×
663
    }
664

665
    /**
666
     * Set the error resolver callback.
667
     */
668
    public function resolveErrorsUsing(Closure $callback): static
196✔
669
    {
670
        $this->errorsResolver = $callback;
196✔
671

672
        return $this;
196✔
673
    }
674

675
    /**
676
     * Resolve the validation errors.
677
     */
678
    public function resolveErrors(Request $request): MessageBag
9✔
679
    {
680
        return is_null($this->errorsResolver)
9✔
681
            ? $request->session()->get('errors', new ViewErrorBag)->getBag('default')
7✔
682
            : call_user_func_array($this->errorsResolver, [$request]);
9✔
683
    }
684

685
    /**
686
     * Determine if the field is invalid.
687
     */
688
    public function invalid(Request $request): bool
9✔
689
    {
690
        return $this->resolveErrors($request)->has($this->getValidationKey());
9✔
691
    }
692

693
    /**
694
     * Get the validation error from the request.
695
     */
696
    public function error(Request $request): ?string
9✔
697
    {
698
        return $this->resolveErrors($request)->first($this->getValidationKey()) ?: null;
9✔
699
    }
700

701
    /**
702
     * Convert the element to a JSON serializable format.
703
     */
704
    public function jsonSerialize(): array
×
705
    {
706
        return $this->toArray();
×
707
    }
708

709
    /**
710
     * Convert the field to an array.
711
     */
712
    public function toArray(): array
15✔
713
    {
714
        return [
15✔
715
            'attribute' => $this->getModelAttribute(),
15✔
716
            'help' => $this->help,
15✔
717
            'label' => $this->label,
15✔
718
            'prefix' => $this->prefix,
15✔
719
            'suffix' => $this->suffix,
15✔
720
            'template' => $this->template,
15✔
721
            'searchable' => $this->isSearchable(),
15✔
722
            'sortable' => $this->isSortable(),
15✔
723
        ];
15✔
724
    }
725

726
    /**
727
     * Get the form component data.
728
     */
729
    public function toDisplay(Request $request, Model $model): array
15✔
730
    {
731
        return array_merge($this->toArray(), [
15✔
732
            'value' => $this->resolveValue($request, $model),
15✔
733
            'formattedValue' => $this->resolveFormat($request, $model),
15✔
734
        ]);
15✔
735
    }
736

737
    /**
738
     * Get the form component data.
739
     */
740
    public function toInput(Request $request, Model $model): array
9✔
741
    {
742
        if ($this->computed) {
9✔
743
            return [];
×
744
        }
745

746
        return array_merge($this->toDisplay($request, $model), [
9✔
747
            'attrs' => $this->newAttributeBag()->class([
9✔
748
                'form-control--invalid' => $this->invalid($request),
9✔
749
            ]),
9✔
750
            'error' => $this->error($request),
9✔
751
            'invalid' => $this->invalid($request),
9✔
752
        ]);
9✔
753
    }
754

755
    /**
756
     * Get the validation representation of the field.
757
     */
758
    public function toValidate(Request $request, Model $model): array
8✔
759
    {
760
        $key = $model->exists ? 'update' : 'create';
8✔
761

762
        $rules = array_map(
8✔
763
            static fn (array|Closure $rule): array => is_array($rule) ? $rule : call_user_func_array($rule, [$request, $model]),
8✔
764
            Arr::only($this->rules, array_unique(['*', $key]))
8✔
765
        );
8✔
766

767
        return [$this->getValidationKey() => Arr::flatten($rules, 1)];
8✔
768
    }
769

770
    /**
771
     * Get the filter representation of the field.
772
     */
773
    public function toFilter(): Filter
×
774
    {
775
        return new class($this) extends RenderableFilter
×
776
        {
×
777
            protected Field $field;
778

779
            public function __construct(Field $field)
780
            {
781
                parent::__construct($field->getModelAttribute());
×
782

783
                $this->field = $field;
×
784
            }
785

786
            public function apply(Request $request, Builder $query, mixed $value): Builder
787
            {
788
                return $this->field->resolveFilterQuery($request, $query, $value);
×
789
            }
790

791
            public function toField(): Field
792
            {
793
                return Text::make($this->field->getLabel(), $this->getRequestKey())
×
794
                    ->value(fn (Request $request): mixed => $this->getValue($request));
×
795
            }
796
        };
×
797
    }
798

799
    /**
800
     * Clone the field.
801
     */
802
    public function __clone(): void
×
803
    {
804
        //
805
    }
×
806
}
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