Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
HasAutoCasting
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
3 / 3
17
100.00% covered (success)
100.00%
1 / 1
 initializeHasAutoCasting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setupCasts
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
12
 extractEnumCast
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace DevToolbelt\LaravelEloquentPlus\Concerns;
6
7use DevToolbelt\LaravelEloquentPlus\Exceptions\LaravelEloquentPlusException;
8use Illuminate\Validation\Rules\Enum as ValidationEnum;
9use ReflectionClass;
10
11/**
12 * Trait for automatic type casting based on validation rules.
13 *
14 * Infers and applies Eloquent casts automatically from validation rules:
15 * - 'boolean' rule -> boolean cast
16 * - 'integer' rule -> integer cast
17 * - 'numeric' rule -> float cast
18 * - 'date' or 'date_format' rules -> date/datetime cast
19 * - 'array' rule -> array cast
20 * - Enum validation rule -> enum class cast
21 *
22 * @package DevToolbelt\LaravelEloquentPlus\Concerns
23 */
24trait HasAutoCasting
25{
26    /**
27     * Initialize the HasAutoCasting trait.
28     *
29     * @return void
30     * @throws LaravelEloquentPlusException
31     */
32    protected function initializeHasAutoCasting(): void
33    {
34        $this->setupCasts();
35    }
36
37    /**
38     * Set up automatic type casts based on validation rules.
39     *
40     * Infers the appropriate cast type from validation rules.
41     * Custom casts defined in the model are merged after inferred casts,
42     * allowing them to override automatic casts.
43     *
44     * @return void
45     * @throws LaravelEloquentPlusException
46     */
47    private function setupCasts(): void
48    {
49        if (empty($this->getRules())) {
50            return;
51        }
52
53        $defaultCasts = [];
54
55        foreach ($this->rules as $attribute => $rules) {
56            if (!is_array($rules)) {
57                throw new LaravelEloquentPlusException(
58                    'Use the list of validators in array format for validation by the model.'
59                );
60            }
61
62            if (in_array('boolean', $rules, true)) {
63                $defaultCasts[$attribute] = 'boolean';
64            }
65
66            if (in_array('integer', $rules, true)) {
67                $defaultCasts[$attribute] = 'integer';
68            }
69
70            if (in_array('numeric', $rules, true)) {
71                $defaultCasts[$attribute] = 'float';
72            }
73
74            $dateCast = $this->resolveDateCast($attribute, $rules);
75            if ($dateCast !== null) {
76                $defaultCasts[$attribute] = $dateCast;
77            }
78
79            if (in_array('array', $rules, true)) {
80                $defaultCasts[$attribute] = 'array';
81            }
82
83            foreach ($rules as $rule) {
84                if ($rule instanceof ValidationEnum) {
85                    $enumClass = $this->extractEnumCast($rule);
86                    if ($enumClass !== null) {
87                        $defaultCasts[$attribute] = $enumClass;
88                    }
89                }
90            }
91        }
92
93        $this->casts = [...$defaultCasts, ...$this->casts];
94    }
95
96    /**
97     * Extract the enum class name from a ValidationEnum rule.
98     *
99     * Uses reflection to access the protected type property of the
100     * Illuminate\Validation\Rules\Enum class.
101     *
102     * @param ValidationEnum $rule The enum validation rule instance
103     * @return class-string|null The fully qualified enum class name, or null if not found
104     */
105    private function extractEnumCast(ValidationEnum $rule): ?string
106    {
107        $reflection = new ReflectionClass($rule);
108
109        foreach ($reflection->getProperties() as $property) {
110            $property->setAccessible(true);
111            $value = $property->getValue($rule);
112
113            if (is_string($value) && enum_exists($value)) {
114                return $value;
115            }
116        }
117
118        return null;
119    }
120}