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

conedevelopment / root / 15084089635

17 May 2025 10:00AM UTC coverage: 77.93% (+0.04%) from 77.891%
15084089635

push

github

web-flow
Modernize back-end.yml (#240)

3291 of 4223 relevant lines covered (77.93%)

36.04 hits per line

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

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

3
namespace Cone\Root\Fields;
4

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

23
abstract class Field implements Arrayable, JsonSerializable
24
{
25
    use Authorizable;
26
    use Conditionable;
27
    use HasAttributes;
28
    use Makeable;
29
    use ResolvesVisibility;
30

31
    /**
32
     * The Blade template.
33
     */
34
    protected string $template = 'root::fields.input';
35

36
    /**
37
     * The hydrate resolver callback.
38
     */
39
    protected ?Closure $hydrateResolver = null;
40

41
    /**
42
     * The format resolver callback.
43
     */
44
    protected ?Closure $formatResolver = null;
45

46
    /**
47
     * The default value resolver callback.
48
     */
49
    protected ?Closure $defaultValueResolver = null;
50

51
    /**
52
     * The value resolver callback.
53
     */
54
    protected ?Closure $valueResolver = null;
55

56
    /**
57
     * The errors resolver callback.
58
     */
59
    protected ?Closure $errorsResolver = null;
60

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

70
    /**
71
     * The field label.
72
     */
73
    protected string $label;
74

75
    /**
76
     * The associated model attribute.
77
     */
78
    protected string $modelAttribute;
79

80
    /**
81
     * The field help text.
82
     */
83
    protected ?string $help = null;
84

85
    /**
86
     * The field prefix.
87
     */
88
    protected ?string $prefix = null;
89

90
    /**
91
     * The field suffix.
92
     */
93
    protected ?string $suffix = null;
94

95
    /**
96
     * The field model.
97
     */
98
    protected ?Model $model = null;
99

100
    /**
101
     * Indicates if the field should use the old value.
102
     */
103
    protected bool $withOldValue = true;
104

105
    /**
106
     * Indicates if the field has been hydrated.
107
     */
108
    protected bool $hydrated = false;
109

110
    /**
111
     * Indicates if the field is sortable.
112
     */
113
    protected bool|Closure $sortable = false;
114

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

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

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

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

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

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

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

152
        $this->modelAttribute = $this->computed ? Str::random() : ($modelAttribute ?: Str::of($label)->lower()->snake()->value());
198✔
153

154
        $this->label($label);
198✔
155
        $this->name($this->modelAttribute);
198✔
156
        $this->id($this->modelAttribute);
198✔
157
        $this->class('form-control');
198✔
158
        $this->value($this->computed ? $modelAttribute : null);
198✔
159
    }
160

161
    /**
162
     * Get the model attribute.
163
     */
164
    public function getModelAttribute(): string
198✔
165
    {
166
        return $this->modelAttribute;
198✔
167
    }
168

169
    /**
170
     * Set the model attribute.
171
     */
172
    public function setModelAttribute(string $value): static
198✔
173
    {
174
        $this->modelAttribute = $value;
198✔
175

176
        return $this;
198✔
177
    }
178

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

187
    /**
188
     * Get the validation key.
189
     */
190
    public function getValidationKey(): string
18✔
191
    {
192
        return $this->getRequestKey();
18✔
193
    }
194

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

203
    /**
204
     * Set the label attribute.
205
     */
206
    public function label(string $value): static
198✔
207
    {
208
        $this->label = $value;
198✔
209

210
        return $this;
198✔
211
    }
212

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

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

228
        $value = preg_replace('/(?:\->|\.)(.+?)(?=(?:\->|\.)|$)/', '[$1]', $value);
198✔
229

230
        return $this->setAttribute('name', $value);
198✔
231
    }
232

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

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

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

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

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

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

280
        return $this;
×
281
    }
282

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

290
        return $this;
×
291
    }
292

293
    /**
294
     * Set the suffix attribute.
295
     */
296
    public function suffix(string $value): static
198✔
297
    {
298
        $this->suffix = $value;
198✔
299

300
        return $this;
198✔
301
    }
302

303
    /**
304
     * Set the sortable attribute.
305
     */
306
    public function sortable(bool|Closure $value = true): static
198✔
307
    {
308
        $this->sortable = $value;
198✔
309

310
        return $this;
198✔
311
    }
312

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

322
        return $this->sortable instanceof Closure ? call_user_func($this->sortable) : $this->sortable;
20✔
323
    }
324

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

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

336
        return $this;
198✔
337
    }
338

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

348
        return $this->searchable instanceof Closure ? call_user_func($this->searchable) : $this->searchable;
21✔
349
    }
350

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

361
    /**
362
     * Set the translatable attribute.
363
     */
364
    public function translatable(bool|Closure $value = true): static
×
365
    {
366
        $this->translatable = $value;
×
367

368
        return $this;
×
369
    }
370

371
    /**
372
     * Determine if the field is translatable.
373
     */
374
    public function isTranslatable(): bool
198✔
375
    {
376
        if ($this->computed) {
198✔
377
            return false;
×
378
        }
379

380
        return $this->translatable instanceof Closure ? call_user_func($this->translatable) : $this->translatable;
198✔
381
    }
382

383
    /**
384
     * Set the filterable attribute.
385
     */
386
    public function filterable(bool|Closure $value = true, ?Closure $callback = null): static
×
387
    {
388
        $this->filterable = $value;
×
389

390
        $this->filterQueryResolver = $callback ?: function (Request $request, Builder $query, mixed $value, string $attribute): Builder {
×
391
            return $query->where($query->qualifyColumn($attribute), $value);
×
392
        };
×
393

394
        return $this;
×
395
    }
396

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

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

409
    /**
410
     * Resolve the filter query.
411
     */
412
    public function resolveFilterQuery(Request $request, Builder $query, mixed $value): Builder
×
413
    {
414
        return $this->isFilterable()
×
415
            ? call_user_func_array($this->filterQueryResolver, [$request, $query, $value, $this->getModelAttribute()])
×
416
            : $query;
×
417
    }
418

419
    /**
420
     * Set the default value resolver.
421
     */
422
    public function default(mixed $value): static
×
423
    {
424
        if (! $value instanceof Closure) {
×
425
            $value = fn (): mixed => $value;
×
426
        }
427

428
        $this->defaultValueResolver = $value;
×
429

430
        return $this;
×
431
    }
432

433
    /**
434
     * Set the value resolver.
435
     */
436
    public function value(?Closure $callback = null): static
198✔
437
    {
438
        $this->valueResolver = $callback;
198✔
439

440
        return $this;
198✔
441
    }
442

443
    /**
444
     * Resolve the value.
445
     */
446
    public function resolveValue(Request $request, Model $model): mixed
26✔
447
    {
448
        if (! $this->hydrated && $this->withOldValue && $request->session()->hasOldInput($this->getRequestKey())) {
26✔
449
            $this->resolveHydrate($request, $model, $this->getOldValue($request));
×
450
        }
451

452
        $value = $this->getValue($model);
26✔
453

454
        if (is_null($value) && ! is_null($this->defaultValueResolver)) {
26✔
455
            $value = call_user_func_array($this->defaultValueResolver, [$request, $model]);
×
456
        }
457

458
        if (is_null($this->valueResolver)) {
26✔
459
            return $value;
23✔
460
        }
461

462
        return call_user_func_array($this->valueResolver, [$request, $model, $value]);
7✔
463
    }
464

465
    /**
466
     * Get the old value from the request.
467
     */
468
    public function getOldValue(Request $request): mixed
3✔
469
    {
470
        return $request->old($this->getRequestKey());
3✔
471
    }
472

473
    /**
474
     * Set the with old value attribute.
475
     */
476
    public function withOldValue(bool $value = true): static
×
477
    {
478
        $this->withOldValue = $value;
×
479

480
        return $this;
×
481
    }
482

483
    /**
484
     * Set the with old value attribute to false.
485
     */
486
    public function withoutOldValue(): static
×
487
    {
488
        return $this->withOldValue(false);
×
489
    }
490

491
    /**
492
     * Get the default value from the model.
493
     */
494
    public function getValue(Model $model): mixed
23✔
495
    {
496
        $attribute = $this->getModelAttribute();
23✔
497

498
        return match (true) {
499
            str_contains($attribute, '->') => data_get($model, str_replace('->', '.', $attribute)),
23✔
500
            default => $model->getAttribute($this->getModelAttribute()),
23✔
501
        };
502
    }
503

504
    /**
505
     * Set the format resolver.
506
     */
507
    public function format(?Closure $callback = null): static
1✔
508
    {
509
        $this->formatResolver = $callback;
1✔
510

511
        return $this;
1✔
512
    }
513

514
    /**
515
     * Format the value.
516
     */
517
    public function resolveFormat(Request $request, Model $model): ?string
21✔
518
    {
519
        $value = $this->resolveValue($request, $model);
21✔
520

521
        if (is_null($this->formatResolver)) {
21✔
522
            return is_array($value) ? json_encode($value) : $value;
14✔
523
        }
524

525
        return call_user_func_array($this->formatResolver, [$request, $model, $value]);
12✔
526
    }
527

528
    /**
529
     * Persist the request value on the model.
530
     */
531
    public function persist(Request $request, Model $model, mixed $value): void
9✔
532
    {
533
        $this->resolveHydrate($request, $model, $value);
9✔
534
    }
535

536
    /**
537
     * Get the value for hydrating the model.
538
     */
539
    public function getValueForHydrate(Request $request): mixed
9✔
540
    {
541
        return $request->input($this->getRequestKey());
9✔
542
    }
543

544
    /**
545
     * Set the hydrate resolver.
546
     */
547
    public function hydrate(Closure $callback): static
198✔
548
    {
549
        $this->hydrateResolver = $callback;
198✔
550

551
        return $this;
198✔
552
    }
553

554
    /**
555
     * Hydrate the model.
556
     */
557
    public function resolveHydrate(Request $request, Model $model, mixed $value): void
15✔
558
    {
559
        if ($this->computed) {
15✔
560
            return;
×
561
        }
562

563
        if (is_null($this->hydrateResolver)) {
15✔
564
            $this->hydrateResolver = function (Request $request, Model $model, $value): void {
10✔
565
                $model->setAttribute($this->getModelAttribute(), $value);
10✔
566
            };
10✔
567
        }
568

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

571
        $this->hydrated = true;
15✔
572
    }
573

574
    /**
575
     * Set the validation rules.
576
     */
577
    public function rules(array|Closure $rules, string $context = '*'): static
198✔
578
    {
579
        $this->rules[$context] = $rules;
198✔
580

581
        return $this;
198✔
582
    }
583

584
    /**
585
     * Set the create validation rules.
586
     */
587
    public function createRules(array|Closure $rules): static
4✔
588
    {
589
        return $this->rules($rules, 'create');
4✔
590
    }
591

592
    /**
593
     * Set the update validation rules.
594
     */
595
    public function updateRules(array|Closure $rules): static
4✔
596
    {
597
        return $this->rules($rules, 'update');
4✔
598
    }
599

600
    /**
601
     * Get the validation rules.
602
     */
603
    public function getRules(): array
×
604
    {
605
        return $this->rules;
×
606
    }
607

608
    /**
609
     * Set the error resolver callback.
610
     */
611
    public function resolveErrorsUsing(Closure $callback): static
198✔
612
    {
613
        $this->errorsResolver = $callback;
198✔
614

615
        return $this;
198✔
616
    }
617

618
    /**
619
     * Resolve the validation errors.
620
     */
621
    public function resolveErrors(Request $request): MessageBag
10✔
622
    {
623
        return is_null($this->errorsResolver)
10✔
624
            ? $request->session()->get('errors', new ViewErrorBag)->getBag('default')
7✔
625
            : call_user_func_array($this->errorsResolver, [$request]);
10✔
626
    }
627

628
    /**
629
     * Determine if the field is invalid.
630
     */
631
    public function invalid(Request $request): bool
10✔
632
    {
633
        return $this->resolveErrors($request)->has($this->getValidationKey());
10✔
634
    }
635

636
    /**
637
     * Get the validation error from the request.
638
     */
639
    public function error(Request $request): ?string
10✔
640
    {
641
        return $this->resolveErrors($request)->first($this->getValidationKey()) ?: null;
10✔
642
    }
643

644
    /**
645
     * Convert the element to a JSON serializable format.
646
     */
647
    public function jsonSerialize(): array
×
648
    {
649
        return $this->toArray();
×
650
    }
651

652
    /**
653
     * Convert the field to an array.
654
     */
655
    public function toArray(): array
16✔
656
    {
657
        return [
16✔
658
            'attribute' => $this->getModelAttribute(),
16✔
659
            'help' => $this->help,
16✔
660
            'label' => $this->label,
16✔
661
            'prefix' => $this->prefix,
16✔
662
            'suffix' => $this->suffix,
16✔
663
            'template' => $this->template,
16✔
664
            'searchable' => $this->isSearchable(),
16✔
665
            'sortable' => $this->isSortable(),
16✔
666
        ];
16✔
667
    }
668

669
    /**
670
     * Get the form component data.
671
     */
672
    public function toDisplay(Request $request, Model $model): array
16✔
673
    {
674
        return array_merge($this->toArray(), [
16✔
675
            'value' => $this->resolveValue($request, $model),
16✔
676
            'formattedValue' => $this->resolveFormat($request, $model),
16✔
677
        ]);
16✔
678
    }
679

680
    /**
681
     * Get the form component data.
682
     */
683
    public function toInput(Request $request, Model $model): array
10✔
684
    {
685
        if ($this->computed) {
10✔
686
            return [];
×
687
        }
688

689
        return array_merge($this->toDisplay($request, $model), [
10✔
690
            'attrs' => $this->newAttributeBag()->class([
10✔
691
                'form-control--invalid' => $this->invalid($request),
10✔
692
            ]),
10✔
693
            'error' => $this->error($request),
10✔
694
            'invalid' => $this->invalid($request),
10✔
695
        ]);
10✔
696
    }
697

698
    /**
699
     * Get the validation representation of the field.
700
     */
701
    public function toValidate(Request $request, Model $model): array
8✔
702
    {
703
        $key = $model->exists ? 'update' : 'create';
8✔
704

705
        $rules = array_map(
8✔
706
            static fn (array|Closure $rule): array => is_array($rule) ? $rule : call_user_func_array($rule, [$request, $model]),
8✔
707
            Arr::only($this->rules, array_unique(['*', $key]))
8✔
708
        );
8✔
709

710
        return [$this->getValidationKey() => Arr::flatten($rules, 1)];
8✔
711
    }
712

713
    /**
714
     * Get the filter representation of the field.
715
     */
716
    public function toFilter(): Filter
×
717
    {
718
        return new class($this) extends RenderableFilter
×
719
        {
×
720
            protected Field $field;
721

722
            public function __construct(Field $field)
723
            {
724
                parent::__construct($field->getModelAttribute());
×
725

726
                $this->field = $field;
×
727
            }
728

729
            public function apply(Request $request, Builder $query, mixed $value): Builder
730
            {
731
                return $this->field->resolveFilterQuery($request, $query, $value);
×
732
            }
733

734
            public function toField(): Field
735
            {
736
                return Text::make($this->field->getLabel(), $this->getRequestKey())
×
737
                    ->value(fn (Request $request): mixed => $this->getValue($request));
×
738
            }
739
        };
×
740
    }
741

742
    /**
743
     * Clone the field.
744
     */
745
    public function __clone(): void
×
746
    {
747
        //
748
    }
×
749
}
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