Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.21% covered (warning)
86.21%
50 / 58
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
HasBlamable
86.21% covered (warning)
86.21%
50 / 58
85.71% covered (warning)
85.71%
6 / 7
27.77
0.00% covered (danger)
0.00%
0 / 1
 bootHasBlamable
84.62% covered (warning)
84.62%
44 / 52
0.00% covered (danger)
0.00%
0 / 1
21.46
 getBlamableUserId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCreatedByColumn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUpdatedByColumn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDeletedByColumn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 usesSoftDeletes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 usesBlamable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace DevToolbelt\LaravelEloquentPlus\Concerns;
6
7use DevToolbelt\LaravelEloquentPlus\Exceptions\MissingModelPropertyException;
8use DevToolbelt\LaravelEloquentPlus\ModelBase;
9
10/**
11 * Trait for automatic user audit tracking on Eloquent models.
12 *
13 * Automatically sets the created_by, updated_by, and deleted_by columns
14 * based on the currently authenticated user. Works with Laravel's
15 * authentication system via the auth() helper.
16 *
17 * Required constants in the model:
18 * - CREATED_BY: Column name for tracking who created the record
19 * - UPDATED_BY: Column name for tracking who last updated the record
20 * - DELETED_BY: Column name for tracking who deleted the record (soft deletes)
21 *
22 * @package DevToolbelt\LaravelEloquentPlus\Concerns
23 */
24trait HasBlamable
25{
26    /**
27     * The name of the "created by" column for audit tracking.
28     */
29    public const string CREATED_BY = 'created_by';
30
31    /**
32     * The name of the "updated by" column for audit tracking.
33     */
34    public const string UPDATED_BY = 'updated_by';
35
36    /**
37     * The name of the "deleted by" column for audit tracking.
38     */
39    public const string DELETED_BY = 'deleted_by';
40
41    /**
42     * Indicates if the model should use blamable (audit tracking).
43     *
44     * @var bool
45     */
46    protected bool $usesBlamable = false;
47
48    /**
49     * Boot the HasBlamable trait.
50     *
51     * Registers model event listeners to automatically set audit columns
52     * on creating, updating, deleting, and restoring events.
53     *
54     * @return void
55     * @throws MissingModelPropertyException
56     */
57    protected static function bootHasBlamable(): void
58    {
59        static::creating(static function (ModelBase $model): void {
60            if (!$model->usesBlamable()) {
61                return;
62            }
63
64            if (!$model->hasAttribute($model->getCreatedByColumn())) {
65                throw new MissingModelPropertyException($model::class, $model->getCreatedByColumn());
66            }
67
68            if (!$model->hasAttribute($model->getUpdatedByColumn())) {
69                throw new MissingModelPropertyException($model::class, $model->getUpdatedByColumn());
70            }
71
72            $userId = $model->getBlamableUserId();
73            if ($userId === null) {
74                return;
75            }
76
77            if (!$model->getAttribute($model->getCreatedByColumn())) {
78                $model->setAttribute($model->getCreatedByColumn(), $userId);
79            }
80
81            if (!$model->getAttribute($model->getUpdatedByColumn())) {
82                $model->setAttribute($model->getUpdatedByColumn(), $userId);
83            }
84        });
85
86        static::updating(static function (ModelBase $model): void {
87            if (!$model->usesBlamable()) {
88                return;
89            }
90
91            if (!$model->hasAttribute($model->getUpdatedByColumn())) {
92                throw new MissingModelPropertyException($model::class, $model->getUpdatedByColumn());
93            }
94
95            $userId = $model->getBlamableUserId();
96            if ($userId === null) {
97                return;
98            }
99
100            $model->setAttribute($model->getUpdatedByColumn(), $userId);
101        });
102
103        static::deleting(static function (ModelBase $model): void {
104            if (!$model->usesBlamable()) {
105                return;
106            }
107
108            $userId = $model->getBlamableUserId();
109            if ($userId === null) {
110                return;
111            }
112
113            if ($model->usesSoftDeletes() && !$model->isForceDeleting()) {
114                if (!$model->hasAttribute($model->getDeletedByColumn())) {
115                    throw new MissingModelPropertyException($model::class, $model->getDeletedByColumn());
116                }
117
118                if (!$model->hasAttribute($model->getUpdatedByColumn())) {
119                    throw new MissingModelPropertyException($model::class, $model->getUpdatedByColumn());
120                }
121
122                $model->setAttribute($model->getDeletedByColumn(), $userId);
123                $model->setAttribute($model->getUpdatedByColumn(), $userId);
124            }
125        });
126
127        static::restoring(static function (ModelBase $model): void {
128            if (!$model->usesBlamable()) {
129                return;
130            }
131
132            if (!$model->hasAttribute($model->getDeletedByColumn())) {
133                throw new MissingModelPropertyException($model::class, $model->getDeletedByColumn());
134            }
135
136            if (!$model->hasAttribute($model->getUpdatedByColumn())) {
137                throw new MissingModelPropertyException($model::class, $model->getUpdatedByColumn());
138            }
139
140            $userId = $model->getBlamableUserId();
141            if ($userId === null) {
142                return;
143            }
144
145            $model->setAttribute($model->getDeletedByColumn(), null);
146            $model->setAttribute($model->getUpdatedByColumn(), $userId);
147        });
148    }
149
150    /**
151     * Get the authenticated user's identifier for audit tracking.
152     *
153     * @return int|string|null The user ID, or null if not authenticated
154     */
155    protected function getBlamableUserId(): int|string|null
156    {
157        return auth()->user()?->getAuthIdentifier();
158    }
159
160    /**
161     * Get the name of the "created by" column.
162     *
163     * Looks for the CREATED_BY constant in the model class.
164     *
165     * @return string The column name, or null if constant is not defined
166     */
167    protected function getCreatedByColumn(): string
168    {
169        return $this::CREATED_BY;
170    }
171
172    /**
173     * Get the name of the "updated by" column.
174     *
175     * Looks for the UPDATED_BY constant in the model class.
176     *
177     * @return string The column name, or null if constant is not defined
178     */
179    protected function getUpdatedByColumn(): string
180    {
181        return $this::UPDATED_BY;
182    }
183
184    /**
185     * Get the name of the "deleted by" column.
186     *
187     * Looks for the DELETED_BY constant in the model class.
188     *
189     * @return string The column name, or null if constant is not defined
190     */
191    protected function getDeletedByColumn(): string
192    {
193        return $this::DELETED_BY;
194    }
195
196    /**
197     * Determine if the model uses soft deletes.
198     *
199     * Checks for the presence of the isForceDeleting method,
200     * which is added by the SoftDeletes trait.
201     *
202     * @return bool True if the model uses soft deletes, false otherwise
203     */
204    protected function usesSoftDeletes(): bool
205    {
206        return method_exists($this, 'isForceDeleting');
207    }
208
209    /**
210     * Determine if the model uses blamable (audit tracking).
211     *
212     * @return bool True if the model uses blamable, false otherwise
213     */
214    public function usesBlamable(): bool
215    {
216        return $this->usesBlamable;
217    }
218}