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

luttje / filament-user-attributes / 16222170016

11 Jul 2025 02:13PM UTC coverage: 74.446%. Remained the same
16222170016

push

github

luttje
.

1311 of 1761 relevant lines covered (74.45%)

30.95 hits per line

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

96.72
/src/Traits/HasUserAttributes.php
1
<?php
2

3
namespace Luttje\FilamentUserAttributes\Traits;
4

5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Casts\ArrayObject;
7
use Illuminate\Database\Eloquent\Relations\MorphOne;
8
use Illuminate\Support\Arr;
9
use Luttje\FilamentUserAttributes\Models\UserAttribute;
10

11
/**
12
 * @see Model
13
 * @property object $user_attributes
14
 */
15
trait HasUserAttributes
16
{
17
    /**
18
     * All user attributes that have been set on the model but not yet saved.
19
     */
20
    protected $dirtyUserAttributes = [];
21

22
    /**
23
     * Whether the user attributes should be destroyed when the model is saved.
24
     */
25
    protected $shouldDestroyUserAttributes = false;
26

27
    /**
28
     * Stores an instance of the anonymous class that is created when the user_attributes
29
     */
30
    private $__userAttributesInstance;
31

32
    /**
33
     * Setup the model to make user_attributes fillable (so they reach the 'saving' hook).
34
     *
35
     * Optionally eager load the userAttributes relationship.
36
     */
37
    protected function initializeHasUserAttributes()
172✔
38
    {
39
        if (config('filament-user-attributes.eager_load_user_attributes', false)) {
172✔
40
            $this->with[] = 'userAttribute';
×
41
        }
42

43
        // In case fillable is used, we need to ensure that the user_attributes
44
        // are included in the fillable attributes of the model or they won't
45
        // be saved when the model is saved.
46
        if (!empty($this->fillable)) {
172✔
47
            $this->mergeFillable(['user_attributes']);
168✔
48
        } else {
49
            $version = app()->version();
64✔
50
            $major = (int) explode('.', $version)[0];
64✔
51
            $isHighEnoughLaravel = $major >= 11;
64✔
52

53
            // Laravel 11 and higher consider user_attributes a guardable column
54
            // because we implemented the `setUserAttributesAttribute` mutator.
55
            // So we only need to perform the below hack if the Laravel version is
56
            // lower than 11.
57
            if (!$isHighEnoughLaravel) {
64✔
58
                // Otherwise guarded is used, in that case we need to ensure that all
59
                // attributes not in guarded are fillable, including user_attributes.
60
                $columns = $this->getConnection()
16✔
61
                            ->getSchemaBuilder()
16✔
62
                            ->getColumnListing($this->getTable());
16✔
63

64
                $fillable = array_diff($columns, $this->guarded);
16✔
65
                $fillable[] = 'user_attributes';
16✔
66

67
                // We have to fiddle with the fillable array here, because if any
68
                // attributes are in $guarded, Laravel will consider any attributes
69
                // that don't exist in the database as guarded, unless we explicitly
70
                // add them to the fillable array.
71
                // NOTE: This is quite a hack, so we mention this in the README.md!
72
                $this->mergeFillable($fillable);
16✔
73
            }
74
        }
75

76
        // Ensure that the user attributes are appended to the model when it is serialized.
77
        $this->append('user_attributes');
172✔
78
    }
79

80
    /**
81
     * Boots the trait and adds a saved hook to save the user attributes
82
     * when the model is saved.
83
     *
84
     * Additionally removes user_attributes so it doesn't get saved to
85
     * the database.
86
     *
87
     * @return void
88
     */
89
    protected static function bootHasUserAttributes()
172✔
90
    {
91
        static::saving(function ($model) {
172✔
92
            if (!isset($model->attributes['user_attributes'])) {
168✔
93
                return;
160✔
94
            }
95

96
            $userAttributes = $model->attributes['user_attributes'];
12✔
97

98
            // In some cases (like when Filament is saving a model), the user_attributes
99
            // may not have been added to dirtyUserAttributes yet. In this case, we
100
            // need to add them now.
101
            foreach ($userAttributes as $key => $value) {
12✔
102
                if (!array_key_exists($key, $model->dirtyUserAttributes)) {
12✔
103
                    $model->dirtyUserAttributes[$key] = $value;
12✔
104
                }
105
            }
106

107
            unset($model->attributes['user_attributes']);
12✔
108
        });
172✔
109

110
        static::saved(function ($model) {
172✔
111
            if ($model->shouldDestroyUserAttributes) {
168✔
112
                $model->userAttribute()->delete();
4✔
113
                $model->shouldDestroyUserAttributes = false;
4✔
114

115
                return;
4✔
116
            }
117

118
            if (!empty($model->dirtyUserAttributes)) {
168✔
119
                $model->saveAllUserAttributes($model->dirtyUserAttributes);
80✔
120

121
                // Clear the delayed attributes as they are now saved
122
                $model->dirtyUserAttributes = [];
80✔
123
            }
124
        });
172✔
125
    }
126

127
    /**
128
     * Saves all user attributes.
129
     *
130
     * @param  array  $attributes
131
     */
132
    public function saveAllUserAttributes(array $attributes)
80✔
133
    {
134
        // If the model already has user attributes, merge them, otherwise create a new record
135
        if ($this->userAttribute()->exists()) {
80✔
136
            $newValues = array_merge($this->userAttribute->values->toArray(), $attributes);
4✔
137
            $this->userAttribute->values = $newValues;
4✔
138
            $this->userAttribute->save();
4✔
139
        } else {
140
            $this->userAttribute()->create(['values' => $attributes]);
80✔
141

142
            // Ensure that the user attributes are dirty for the next time the model is used.
143
            //$this->unsetRelation('userAttribute'); ?/ This was here because I accidentally fetched the relationship, just before creating it. Causing it to be set as null (and then not updated after save)
144
        }
145
    }
146

147
    /**
148
     * Accessor for serializing the model including the user attributes.
149
     */
150
    public function getUserAttributesAttribute()
44✔
151
    {
152
        return $this->getUserAttributeValues()->toArray();
44✔
153
    }
154

155
    /**
156
     * Mutator for setting the user attributes.
157
     *
158
     * This is only here so that in Laravel 11 and higher, the user_attributes
159
     * attribute is considered a 'guardable' column. This causes guarded models
160
     * to still save user_attributes, even if that is not a real database column.
161
     */
162
    public function setUserAttributesAttribute($value)
12✔
163
    {
164
        $this->attributes['user_attributes'] = $value;
12✔
165
    }
166

167
    /**
168
     * Relationship to the user attributes model.
169
     */
170
    public function userAttribute(): MorphOne
136✔
171
    {
172
        return $this->morphOne(UserAttribute::class, 'model');
136✔
173
    }
174

175
    public function hasUserAttribute(string $key): bool
4✔
176
    {
177
        return $this->userAttribute()->where('values->' . $key, '!=', null)->exists();
4✔
178
    }
179

180
    public function setUserAttributeValue(string $key, $value)
32✔
181
    {
182
        if ($this->shouldDestroyUserAttributes) {
32✔
183
            throw new \Exception('Cannot set user attribute on a model that has been marked for deletion.');
4✔
184
        }
185

186
        $this->dirtyUserAttributes[$key] = $value;
28✔
187
    }
188

189
    public function setUserAttributeValues(object $values)
48✔
190
    {
191
        if ($this->shouldDestroyUserAttributes) {
48✔
192
            throw new \Exception('Cannot set user attributes on a model that has been marked for deletion.');
4✔
193
        }
194

195
        $this->dirtyUserAttributes = array_merge($this->dirtyUserAttributes, (array) $values);
48✔
196
    }
197

198
    public function destroyUserAttributes()
52✔
199
    {
200
        $this->shouldDestroyUserAttributes = true;
52✔
201
    }
202

203
    public function getUserAttributeValue(string $keyOrPath)
48✔
204
    {
205
        $userAttribute = $this->userAttribute;
48✔
206
        $array = $userAttribute?->values;
48✔
207

208
        if (!$array) {
48✔
209
            return null;
8✔
210
        }
211

212
        return Arr::get($array, $keyOrPath);
40✔
213
    }
214

215
    public function getUserAttributeValues(): ArrayObject
68✔
216
    {
217
        $userAttribute = $this->userAttribute;
68✔
218
        return $userAttribute?->values ?? new ArrayObject([]);
68✔
219
    }
220

221
    /**
222
     * Static Getters
223
     */
224
    public static function getUserAttributeQuery(): Builder
52✔
225
    {
226
        return UserAttribute::query()
52✔
227
            ->where('model_type', static::class);
52✔
228
    }
229

230
    public static function allUserAttributes(string $key)
12✔
231
    {
232
        return static::getUserAttributeQuery()
12✔
233
            ->get()
12✔
234
            ->pluck('values.' . $key);
12✔
235
    }
236

237
    /**
238
     * Aggregates
239
     */
240
    public static function userAttributeSum(string $path): int
4✔
241
    {
242
        return UserAttribute::query()
4✔
243
            ->where('model_type', static::class)
4✔
244
            ->sum('values->' . $path);
4✔
245
    }
246

247
    /**
248
     * Where filters
249
     */
250
    public static function whereUserAttribute(string $key, $value)
4✔
251
    {
252
        return static::whereHas('userAttribute', function ($query) use ($key, $value) {
4✔
253
            $query->where('values->' . $key, $value);
4✔
254
        });
4✔
255
    }
256

257
    public static function whereUserAttributeContains(string $key, $value)
4✔
258
    {
259
        return static::whereHas('userAttribute', function ($query) use ($key, $value) {
4✔
260
            $query->whereJsonContains('values->' . $key, $value);
4✔
261
        });
4✔
262
    }
263

264
    public static function whereUserAttributeLength(string $key, $operator, $value = null)
4✔
265
    {
266
        if (func_num_args() == 2) {
4✔
267
            $value = $operator;
4✔
268
            $operator = '=';
4✔
269
        }
270

271
        return static::whereHas('userAttribute', function ($query) use ($key, $operator, $value) {
4✔
272
            $query->whereJsonLength('values->' . $key, $operator, $value);
4✔
273
        });
4✔
274
    }
275

276
    /**
277
     * Magic Methods
278
     */
279

280
    /**
281
     * We want users of this trait to be able to access the user_attributes property as if it
282
     * were a real property on the model. This makes it easy to get and set user attributes.
283
     *
284
     * Creates an anonymous class when user_attributes is called for the first time and stores it
285
     * in the $__userAttributesInstance property. The anonymous class stores a reference back to
286
     * the owner object and uses the __get and __set magic methods to intercept property accesses.
287
     *
288
     * @return object
289
     */
290
    public function __get($key)
116✔
291
    {
292
        if ($key === 'user_attributes') {
116✔
293
            if (!$this->__userAttributesInstance) {
56✔
294
                $this->__userAttributesInstance = new class ($this) {
56✔
295
                    private $owner;
296

297
                    public function __construct($owner)
298
                    {
299
                        $this->owner = $owner;
56✔
300
                    }
301

302
                    public function __get($key)
303
                    {
304
                        return $this->owner->getUserAttributeValue($key);
44✔
305
                    }
306

307
                    public function __set($key, $value)
308
                    {
309
                        $this->owner->setUserAttributeValue($key, $value);
32✔
310
                    }
311

312
                    public function __unset($key)
313
                    {
314
                        $this->owner->setUserAttributeValue($key, null);
×
315
                    }
316

317
                    public function __isset($key)
318
                    {
319
                        return $this->owner->hasUserAttribute($key);
×
320
                    }
321
                };
56✔
322
            }
323

324
            return $this->__userAttributesInstance;
56✔
325
        }
326

327
        return parent::__get($key);
112✔
328
    }
329

330
    /**
331
     * When the user attempts to directly set the user_attributes property, we intercept it
332
     * and call setUserAttributeValues instead.
333
     *
334
     * @param  mixed  $key
335
     * @param  mixed  $value
336
     */
337
    public function __set($key, $value)
168✔
338
    {
339
        if ($key === 'user_attributes') {
168✔
340
            if (!is_object($value)) {
76✔
341
                // We throw, because if a developer would set `$model->user_attributes = ['key' => 'value']`, it would
342
                // mess with the IDE's ability to recognize `user_attributes` as an object (thanks to the PHPdoc and __get)
343
                throw new \Exception('The user_attributes property must be an object. Be sure to wrap arrays with UserAttribute::make($yourArray) or cast them to an object.');
28✔
344
            }
345

346
            $this->setUserAttributeValues($value);
48✔
347

348
            return;
48✔
349
        }
350

351
        parent::__set($key, $value);
168✔
352
    }
353

354
    /**
355
     * When the user attempts to unset the user_attributes property, we intercept it
356
     * and call destroyUserAttributes instead.
357
     *
358
     * @param  mixed  $key
359
     */
360
    public function __unset($key)
16✔
361
    {
362
        if ($key === 'user_attributes') {
16✔
363
            $this->destroyUserAttributes();
12✔
364

365
            return;
12✔
366
        }
367

368
        parent::__unset($key);
4✔
369
    }
370

371
    /**
372
     * When the user attempts to check if the user_attributes property is set, we intercept it
373
     * for user_attributes.
374
     *
375
     * @param  mixed  $key
376
     */
377
    public function __isset($key)
24✔
378
    {
379
        if ($key === 'user_attributes') {
24✔
380
            return true;
×
381
        }
382

383
        return parent::__isset($key);
24✔
384
    }
385
}
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