Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.74% covered (warning)
89.74%
35 / 39
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ModelBase
89.74% covered (warning)
89.74%
35 / 39
88.89% covered (warning)
88.89%
8 / 9
22.52
0.00% covered (danger)
0.00%
0 / 1
 bootSoftDeletes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 initializeSoftDeletes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fill
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getAttribute
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 setAttribute
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
4.12
 hasAttribute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 toArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 toSoftArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 returnOnlyFields
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace DevToolbelt\LaravelEloquentPlus;
6
7use DevToolbelt\LaravelEloquentPlus\Concerns\HasAutoCasting;
8use DevToolbelt\LaravelEloquentPlus\Concerns\HasBlamable;
9use DevToolbelt\LaravelEloquentPlus\Concerns\HasCastAliases;
10use DevToolbelt\LaravelEloquentPlus\Concerns\HasDateFormatting;
11use DevToolbelt\LaravelEloquentPlus\Concerns\HasExternalId;
12use DevToolbelt\LaravelEloquentPlus\Concerns\HasHiddenAttributes;
13use DevToolbelt\LaravelEloquentPlus\Concerns\HasLifecycleHooks;
14use DevToolbelt\LaravelEloquentPlus\Concerns\HasValidation;
15use DevToolbelt\LaravelEloquentPlus\Exceptions\ValidationException;
16use Illuminate\Database\Eloquent\Factories\HasFactory;
17use Illuminate\Database\Eloquent\Model;
18use Illuminate\Database\Eloquent\SoftDeletes;
19use Illuminate\Database\Eloquent\SoftDeletingScope;
20use Illuminate\Support\Carbon;
21use Illuminate\Support\Str;
22use Throwable;
23use ValueError;
24
25/**
26 * Abstract base model class that extends Laravel's Eloquent Model.
27 *
28 * Provides additional functionality including:
29 * - Automatic validation based on defined rules (HasValidation)
30 * - Automatic type casting inferred from validation rules (HasAutoCasting)
31 * - Date formatting control with string/Carbon output (HasDateFormatting)
32 * - Lifecycle hooks for custom logic (HasLifecycleHooks)
33 * - Automatic hidden attributes for soft deletes (HasHiddenAttributes)
34 * - Built-in soft deletes with user tracking (HasBlamable)
35 * - Optional external ID (UUID) support (HasExternalId)
36 * - Automatic snake_case attribute conversion on fill
37 *
38 * @package DevToolbelt\LaravelEloquentPlus
39 *
40 * @phpstan-consistent-constructor
41 */
42abstract class ModelBase extends Model
43{
44    use HasFactory;
45    use SoftDeletes;
46    use HasBlamable;
47    use HasCastAliases;
48    use HasExternalId;
49    use HasValidation;
50    use HasDateFormatting;
51    use HasAutoCasting;
52    use HasLifecycleHooks;
53    use HasHiddenAttributes;
54
55    /**
56     * The name of the "created at" column.
57     */
58    public const string CREATED_AT = 'created_at';
59
60    /**
61     * The name of the "updated at" column.
62     */
63    public const string UPDATED_AT = 'updated_at';
64
65    /**
66     * The name of the "deleted at" column.
67     */
68    public const string DELETED_AT = 'deleted_at';
69
70    /**
71     * Indicates if the model should be timestamped.
72     *
73     * @var bool
74     */
75    public $timestamps = true;
76
77    /**
78     * The storage format of the model's date columns.
79     *
80     * @var string
81     */
82    public $dateFormat = 'Y-m-d H:i:s.u';
83
84    /**
85     * Indicates whether attributes are snake cased on arrays.
86     *
87     * @var bool
88     */
89    public static $snakeAttributes = false;
90
91    /**
92     * The primary key for the model.
93     *
94     * @var string
95     */
96    protected $primaryKey = 'id';
97
98    /**
99     * The "type" of the primary key ID.
100     *
101     * @var string
102     */
103    protected $keyType = 'int';
104
105    /**
106     * Indicates if the IDs are auto-incrementing.
107     *
108     * @var bool
109     */
110    public $incrementing = true;
111
112    /**
113     * Boot the soft deleting trait for a model.
114     *
115     * @return void
116     */
117    public static function bootSoftDeletes()
118    {
119        /** @phpstan-ignore new.static */
120        $model = new static();
121
122        if ($model->hasAttribute($model->getDeletedAtColumn())) {
123            static::addGlobalScope(new SoftDeletingScope());
124        }
125    }
126
127    /**
128     * Initialize the soft deleting trait for an instance.
129     *
130     * @return void
131     */
132    public function initializeSoftDeletes()
133    {
134    }
135
136    /**
137     * Fill the model with an array of attributes.
138     *
139     * Extends the default fill behavior to automatically convert
140     * camelCase attribute names to snake_case before filling.
141     *
142     * @param array<string, mixed> $attributes Key-value pairs of attributes to fill
143     * @return static
144     */
145    public function fill(array $attributes): self
146    {
147        if (empty($attributes)) {
148            return parent::fill($attributes);
149        }
150
151        foreach ($attributes as $attributeName => $value) {
152            $snakeCaseAttr = Str::snake($attributeName);
153
154            if (!$this->hasAttribute($snakeCaseAttr)) {
155                continue;
156            }
157
158            $attributes[$snakeCaseAttr] = $value;
159        }
160
161        return parent::fill($attributes);
162    }
163
164    /**
165     * Get an attribute from the model.
166     *
167     * Overrides the default behavior to return formatted date strings
168     * instead of Carbon instances for date/datetime fields.
169     * This behavior can be disabled by setting $carbonInstanceInFieldDates to true.
170     *
171     * @param string $key The attribute name
172     * @return mixed The attribute value (string for dates when $carbonInstanceInFieldDates is false)
173     */
174    public function getAttribute($key): mixed
175    {
176        $value = parent::getAttribute($key);
177
178        if ($value instanceof Carbon && isset($this->dateFormats[$key])) {
179            if ($this->carbonInstanceInFieldDates) {
180                return $value;
181            }
182
183            return $value->format($this->dateFormats[$key]);
184        }
185
186        return $value;
187    }
188
189    /**
190     * Set a given attribute on the model.
191     *
192     * @param string $key
193     * @param mixed $value
194     * @return mixed
195     * @throws Throwable
196     */
197    public function setAttribute($key, $value)
198    {
199        try {
200            return parent::setAttribute($key, $value);
201        } catch (ValueError $e) {
202            throw new ValidationException(
203                [['field' => $key, 'value' => $value, 'error' => 'enum', 'message' => $e->getMessage()]],
204                $e->getMessage()
205            );
206        } catch (Throwable $e) {
207            throw $e;
208        }
209    }
210
211    /**
212     * Determine whether an attribute exists on the model.
213     *
214     * @param string $key
215     * @return bool
216     */
217    public function hasAttribute($key)
218    {
219        return parent::hasAttribute($key)
220            || in_array($key, $this->fillable)
221            || array_key_exists($key, $this->rules);
222    }
223
224    /**
225     * Convert the model instance to an array.
226     *
227     * When external ID is enabled, exposes 'id' with the external ID value
228     * instead of the numeric primary key for public-facing output.
229     *
230     * @return array<string, mixed>
231     */
232    public function toArray(): array
233    {
234        $data = parent::toArray();
235
236        if ($this->usesExternalId()) {
237            $data[$this->primaryKey] = $this->getExternalId();
238        }
239
240        return $data;
241    }
242
243    /**
244     * Convert the model to a minimal array representation.
245     *
246     * Returns only the primary key field (or external ID if enabled),
247     * useful for references or lightweight serialization.
248     *
249     * @return array<string, mixed> Array containing only the identifier
250     */
251    public function toSoftArray(): array
252    {
253        if ($this->usesExternalId()) {
254            return [$this->primaryKey => $this->getExternalId()];
255        }
256
257        return $this->returnOnlyFields([$this->primaryKey]);
258    }
259
260    /**
261     * Filter the model's array representation to include only specified fields.
262     *
263     * @param string[] $fieldsToReturn List of field names to include in the output
264     * @return array<string, mixed> Filtered array containing only the specified fields
265     */
266    protected function returnOnlyFields(array $fieldsToReturn): array
267    {
268        return array_filter($this->toArray(), function ($key) use ($fieldsToReturn) {
269            return in_array($key, $fieldsToReturn);
270        }, ARRAY_FILTER_USE_KEY);
271    }
272}