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

conedevelopment / root / 20618156642

31 Dec 2025 11:33AM UTC coverage: 75.542% (-0.5%) from 76.05%
20618156642

push

github

iamgergo
fixes

3 of 3 new or added lines in 1 file covered. (100.0%)

93 existing lines in 4 files now uncovered.

3450 of 4567 relevant lines covered (75.54%)

33.14 hits per line

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

71.17
/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
     */
UNCOV
494
    public function getOldValue(Request $request): mixed
×
495
    {
UNCOV
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 hydrate resolver.
583
     */
584
    public function hydrate(Closure $callback): static
196✔
585
    {
586
        $this->hydrateResolver = $callback;
196✔
587

588
        return $this;
196✔
589
    }
590

591
    /**
592
     * Hydrate the model.
593
     */
594
    public function resolveHydrate(Request $request, Model $model, mixed $value): void
15✔
595
    {
596
        if ($this->computed) {
15✔
597
            return;
×
598
        }
599

600
        if (is_null($this->hydrateResolver)) {
15✔
601
            $this->hydrateResolver = function (Request $request, Model $model, $value): void {
10✔
602
                $model->setAttribute($this->getModelAttribute(), $value);
10✔
603
            };
10✔
604
        }
605

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

608
        $this->hydrated = true;
15✔
609
    }
610

611
    /**
612
     * Set the validation rules.
613
     */
614
    public function rules(array|Closure $rules, string $context = '*'): static
196✔
615
    {
616
        $this->rules[$context] = $rules;
196✔
617

618
        return $this;
196✔
619
    }
620

621
    /**
622
     * Set the create validation rules.
623
     */
624
    public function createRules(array|Closure $rules): static
4✔
625
    {
626
        return $this->rules($rules, 'create');
4✔
627
    }
628

629
    /**
630
     * Set the update validation rules.
631
     */
632
    public function updateRules(array|Closure $rules): static
4✔
633
    {
634
        return $this->rules($rules, 'update');
4✔
635
    }
636

637
    /**
638
     * Get the validation rules.
639
     */
640
    public function getRules(): array
×
641
    {
642
        return $this->rules;
×
643
    }
644

645
    /**
646
     * Set the error resolver callback.
647
     */
648
    public function resolveErrorsUsing(Closure $callback): static
196✔
649
    {
650
        $this->errorsResolver = $callback;
196✔
651

652
        return $this;
196✔
653
    }
654

655
    /**
656
     * Resolve the validation errors.
657
     */
658
    public function resolveErrors(Request $request): MessageBag
9✔
659
    {
660
        return is_null($this->errorsResolver)
9✔
661
            ? $request->session()->get('errors', new ViewErrorBag)->getBag('default')
7✔
662
            : call_user_func_array($this->errorsResolver, [$request]);
9✔
663
    }
664

665
    /**
666
     * Determine if the field is invalid.
667
     */
668
    public function invalid(Request $request): bool
9✔
669
    {
670
        return $this->resolveErrors($request)->has($this->getValidationKey());
9✔
671
    }
672

673
    /**
674
     * Get the validation error from the request.
675
     */
676
    public function error(Request $request): ?string
9✔
677
    {
678
        return $this->resolveErrors($request)->first($this->getValidationKey()) ?: null;
9✔
679
    }
680

681
    /**
682
     * Convert the element to a JSON serializable format.
683
     */
684
    public function jsonSerialize(): array
×
685
    {
686
        return $this->toArray();
×
687
    }
688

689
    /**
690
     * Convert the field to an array.
691
     */
692
    public function toArray(): array
15✔
693
    {
694
        return [
15✔
695
            'attribute' => $this->getModelAttribute(),
15✔
696
            'help' => $this->help,
15✔
697
            'label' => $this->label,
15✔
698
            'prefix' => $this->prefix,
15✔
699
            'suffix' => $this->suffix,
15✔
700
            'template' => $this->template,
15✔
701
            'searchable' => $this->isSearchable(),
15✔
702
            'sortable' => $this->isSortable(),
15✔
703
        ];
15✔
704
    }
705

706
    /**
707
     * Get the form component data.
708
     */
709
    public function toDisplay(Request $request, Model $model): array
15✔
710
    {
711
        return array_merge($this->toArray(), [
15✔
712
            'value' => $this->resolveValue($request, $model),
15✔
713
            'formattedValue' => $this->resolveFormat($request, $model),
15✔
714
        ]);
15✔
715
    }
716

717
    /**
718
     * Get the form component data.
719
     */
720
    public function toInput(Request $request, Model $model): array
9✔
721
    {
722
        if ($this->computed) {
9✔
723
            return [];
×
724
        }
725

726
        return array_merge($this->toDisplay($request, $model), [
9✔
727
            'attrs' => $this->newAttributeBag()->class([
9✔
728
                'form-control--invalid' => $this->invalid($request),
9✔
729
            ]),
9✔
730
            'error' => $this->error($request),
9✔
731
            'invalid' => $this->invalid($request),
9✔
732
        ]);
9✔
733
    }
734

735
    /**
736
     * Get the validation representation of the field.
737
     */
738
    public function toValidate(Request $request, Model $model): array
8✔
739
    {
740
        $key = $model->exists ? 'update' : 'create';
8✔
741

742
        $rules = array_map(
8✔
743
            static fn (array|Closure $rule): array => is_array($rule) ? $rule : call_user_func_array($rule, [$request, $model]),
8✔
744
            Arr::only($this->rules, array_unique(['*', $key]))
8✔
745
        );
8✔
746

747
        return [$this->getValidationKey() => Arr::flatten($rules, 1)];
8✔
748
    }
749

750
    /**
751
     * Get the filter representation of the field.
752
     */
753
    public function toFilter(): Filter
×
754
    {
755
        return new class($this) extends RenderableFilter
×
756
        {
×
757
            protected Field $field;
758

759
            public function __construct(Field $field)
760
            {
761
                parent::__construct($field->getModelAttribute());
×
762

763
                $this->field = $field;
×
764
            }
765

766
            public function apply(Request $request, Builder $query, mixed $value): Builder
767
            {
768
                return $this->field->resolveFilterQuery($request, $query, $value);
×
769
            }
770

771
            public function toField(): Field
772
            {
773
                return Text::make($this->field->getLabel(), $this->getRequestKey())
×
774
                    ->value(fn (Request $request): mixed => $this->getValue($request));
×
775
            }
776
        };
×
777
    }
778

779
    /**
780
     * Clone the field.
781
     */
782
    public function __clone(): void
×
783
    {
784
        //
785
    }
×
786
}
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