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

luttje / filament-user-attributes / 24601465173

18 Apr 2026 09:10AM UTC coverage: 76.343% (-2.1%) from 78.478%
24601465173

Pull #29

github

web-flow
Merge 340e263f0 into da87d6f02
Pull Request #29: Support Filament v5

11 of 100 new or added lines in 2 files covered. (11.0%)

3 existing lines in 1 file now uncovered.

2104 of 2756 relevant lines covered (76.34%)

26.82 hits per line

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

80.78
/src/FilamentUserAttributes.php
1
<?php
2

3
namespace Luttje\FilamentUserAttributes;
4

5
use Closure;
6
use Filament\Forms\Components\Field;
7
use Filament\Schemas\Components\Component;
8
use Filament\Schemas\Components\Fieldset;
9
use Filament\Schemas\Components\Section;
10
use Filament\Schemas\Components\Tabs;
11
use Filament\Schemas\Components\Tabs\Tab;
12
use Filament\Tables\Columns\Column;
13
use Illuminate\Support\Collection;
14
use Illuminate\Support\Facades\File;
15
use Illuminate\Support\Str;
16
use Luttje\FilamentUserAttributes\Contracts\ConfiguresUserAttributesContract;
17
use Luttje\FilamentUserAttributes\Contracts\UserAttributesConfigContract;
18
use Luttje\FilamentUserAttributes\Filament\UserAttributeComponentFactoryRegistry;
19
use Luttje\FilamentUserAttributes\Models\UserAttributeConfig;
20
use Luttje\FilamentUserAttributes\Traits\ConfiguresUserAttributes;
21

22
/**
23
 * Class FilamentUserAttributes
24
 *
25
 * This class provides functionality for managing user attributes in Filament admin
26
 * panels. It handles the registration and discovery of resources, user attribute
27
 * components, and facilitates the integration of custom fields and columns in
28
 * Filament resources.
29
 */
30
class FilamentUserAttributes
31
{
32
    /**
33
     * @var array|Closure List of registered resources or a closure that returns resources.
34
     */
35
    protected array | Closure $registeredResources = [];
36

37
    /**
38
     * @var array List of registered user attribute config components.
39
     */
40
    protected array $registeredConfigComponents = [];
41

42
    /**
43
     * @var array|null Cached list of discovered resources.
44
     */
45
    protected ?array $cachedDiscoveredResources = null;
46

47
    /**
48
     * @var string Path to the application directory.
49
     */
50
    protected string $appPath;
51

52
    /**
53
     * @var string Namespace of the application.
54
     */
55
    protected string $appNamespace;
56

57
    /**
58
     * Constructor for FilamentUserAttributes.
59
     *
60
     * @param string|null $appPath       Optional path to the application directory.
61
     * @param string|null $appNamespace  Optional namespace of the application.
62
     */
63
    public function __construct(?string $appPath = null, ?string $appNamespace = null)
228✔
64
    {
65
        $this->appNamespace = $appNamespace ?? app()->getNamespace();
228✔
66
        $this->appPath = $appPath ?? app_path();
228✔
67

68
        $this->appNamespace = rtrim($this->appNamespace, '\\') . '\\';
228✔
69
        $this->appPath = rtrim($this->appPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
228✔
70
    }
71

72
    /**
73
     * Returns whether the component can have child components.
74
     */
75
    public function componentHasChildren(Component $component): bool
48✔
76
    {
77
        return $component instanceof Tabs
48✔
78
            || $component instanceof Tab
48✔
79
            || $component instanceof Section
48✔
80
            || $component instanceof Fieldset;
48✔
81
    }
82

83
    /**
84
     * Register resources that can be configured with user attributes.
85
     * You can provide an associative array of resources, where the key
86
     * is the resource class and the value is the resource label to show
87
     * to users.
88
     *
89
     * You can also provide a closure that returns an array of resources
90
     * in the same format.
91
     *
92
     * Call this in your AppServiceProvider's boot function.
93
     */
94
    public function registerResources(array | Closure $resources): void
6✔
95
    {
96
        if (config('filament-user-attributes.discover_resources') !== false) {
6✔
97
            throw new \Exception("You cannot register resources when the 'filament-user-attributes.discover_resources' config option is enabled. Set it to false.");
3✔
98
        }
99

100
        if (is_array($resources)) {
3✔
101
            $this->registeredResources = array_merge($this->registeredResources, $resources);
3✔
102
        } elseif ($resources instanceof Closure) {
×
103
            $this->registeredResources = $resources;
×
104
        }
105
    }
106

107
    /**
108
     * Registers all types of user attribute field factories.
109
     */
110
    public function registerDefaultUserAttributeComponentFactories(): void
228✔
111
    {
112
        UserAttributeComponentFactoryRegistry::register('text', \Luttje\FilamentUserAttributes\Filament\Factories\TextComponentFactory::class);
228✔
113
        UserAttributeComponentFactoryRegistry::register('number', \Luttje\FilamentUserAttributes\Filament\Factories\NumberInputComponentFactory::class);
228✔
114
        UserAttributeComponentFactoryRegistry::register('textarea', \Luttje\FilamentUserAttributes\Filament\Factories\TextareaComponentFactory::class);
228✔
115
        UserAttributeComponentFactoryRegistry::register('richeditor', \Luttje\FilamentUserAttributes\Filament\Factories\RichEditorComponentFactory::class);
228✔
116
        UserAttributeComponentFactoryRegistry::register('tags', \Luttje\FilamentUserAttributes\Filament\Factories\TagsInputComponentFactory::class);
228✔
117

118
        UserAttributeComponentFactoryRegistry::register('select', \Luttje\FilamentUserAttributes\Filament\Factories\SelectComponentFactory::class);
228✔
119
        UserAttributeComponentFactoryRegistry::register('checkbox', \Luttje\FilamentUserAttributes\Filament\Factories\CheckboxComponentFactory::class);
228✔
120
        UserAttributeComponentFactoryRegistry::register('toggle', \Luttje\FilamentUserAttributes\Filament\Factories\ToggleComponentFactory::class);
228✔
121
        UserAttributeComponentFactoryRegistry::register('radio', \Luttje\FilamentUserAttributes\Filament\Factories\RadioComponentFactory::class);
228✔
122

123
        UserAttributeComponentFactoryRegistry::register('datetime', \Luttje\FilamentUserAttributes\Filament\Factories\DateTimeComponentFactory::class);
228✔
124
    }
125

126
    /**
127
     * Register a custom filament form component to be added to the user attributes configuration form.
128
     */
129
    public function registerUserAttributeConfigComponent(Component|Closure $component): void
×
130
    {
131
        $this->registeredConfigComponents[] = $component;
×
132
    }
133

134
    /**
135
     * Returns all registered user attribute config components.
136
     */
137
    public function getUserAttributeConfigComponents(UserAttributeConfig $configModel): array
36✔
138
    {
139
        $components = [];
36✔
140

141
        foreach ($this->registeredConfigComponents as $component) {
36✔
142
            if ($component instanceof Closure) {
×
143
                $component = $component($configModel);
×
144
            }
145

146
            if (!$component) {
×
147
                continue;
×
148
            }
149

150
            $components[] = $component;
×
151
        }
152

153
        return $components;
36✔
154
    }
155

156
    /**
157
     * Returns the user attribute columns.
158
     */
159
    public function getUserAttributeColumns(string $resource): array
15✔
160
    {
161
        $config = $this->getUserAttributeConfig($resource);
15✔
162

163
        if (!in_array(ConfiguresUserAttributes::class, class_uses_recursive($config))) {
15✔
164
            throw new \Exception("The resource '$resource' does not correctly use the ConfiguresUserAttributes trait");
×
165
        }
166

167
        return $config->getUserAttributeColumns($resource);
15✔
168
    }
169

170
    /**
171
     * Returns the user attribute fields.
172
     */
173
    public function getUserAttributeFields(string $resource): array
3✔
174
    {
175
        $config = $this->getUserAttributeConfig($resource);
3✔
176

177
        if (!in_array(ConfiguresUserAttributes::class, class_uses_recursive($config))) {
3✔
178
            throw new \Exception("The resource '$resource' does not use the ConfiguresUserAttributes trait");
×
179
        }
180

181
        return $config->getUserAttributeFields($resource);
3✔
182
    }
183

184
    /**
185
     * Returns the user attribute configuration model.
186
     */
187
    public function getUserAttributeConfig(string $resource): ConfiguresUserAttributesContract
15✔
188
    {
189
        if (!in_array(UserAttributesConfigContract::class, class_implements($resource))) {
15✔
190
            throw new \Exception("The resource '$resource' does not implement the UserAttributesConfigContract interface.");
×
191
        }
192

193
        /** @var ?UserAttributesConfigContract */
194
        $resource = $resource;
15✔
195
        $config = $resource::getUserAttributesConfig();
15✔
196

197
        if ($config === null) {
15✔
198
            throw new \Exception("The resource '" . strval($resource) . "' did not return a configuration model from the getUserAttributesConfig function (or it was null).");
×
199
        }
200

201
        return $config;
15✔
202
    }
203

204
    public static function normalizePath(string $path): string
48✔
205
    {
206
        return str_replace('/', DIRECTORY_SEPARATOR, $path);
48✔
207
    }
208

209
    public static function normalizePaths(array $paths): array
48✔
210
    {
211
        return array_map(function ($path) {
48✔
212
            return self::normalizePath($path);
48✔
213
        }, $paths);
48✔
214
    }
215

216
    public static function normalizeClassName(string $className): string
24✔
217
    {
218
        return str_replace('/', '\\', $className);
24✔
219
    }
220

221
    public static function normalizeClassNames(array $classNames): array
×
222
    {
223
        return array_map(function ($className) {
×
224
            return self::normalizeClassName($className);
×
225
        }, $classNames);
×
226
    }
227

228
    /**
229
     * Returns all Resource discover paths, normalized
230
     */
231
    public function getResourceDiscoverPaths(): array|false
51✔
232
    {
233
        $discoverPaths = config('filament-user-attributes.discover_resources');
51✔
234

235
        if ($discoverPaths === false) {
51✔
236
            return false;
3✔
237
        }
238

239
        return self::normalizePaths($discoverPaths);
48✔
240
    }
241

242
    /**
243
     * Finds all resources that have the HasUserAttributesContract interface
244
     */
245
    public function getConfigurableResources($configuredOnly = true)
51✔
246
    {
247
        $discoverPaths = $this->getResourceDiscoverPaths();
51✔
248

249
        if ($discoverPaths === false) {
51✔
250
            $resources = $this->registeredResources;
3✔
251

252
            if ($resources instanceof Closure) {
3✔
253
                return $resources();
×
254
            }
255

256
            return $resources;
3✔
257
        }
258

259
        if ($this->cachedDiscoveredResources === null) {
48✔
260
            $this->cachedDiscoveredResources = $this->discoverConfigurableResources($discoverPaths, $configuredOnly);
48✔
261
        }
262

263
        return $this->cachedDiscoveredResources;
48✔
264
    }
265

266
    /**
267
     * Discovers all resources that have the HasUserAttributesContract interface
268
     */
269
    public function discoverConfigurableResources(array $paths, bool $configuredOnly): array
48✔
270
    {
271
        $resources = [];
48✔
272

273
        foreach ($paths as $targetPath) {
48✔
274
            $path = $this->appPath . $targetPath;
48✔
275

276
            if (!File::exists($path)) {
48✔
277
                continue;
33✔
278
            }
279

280
            $nameTransformer = config('filament-user-attributes.discovery_resource_name_transformer');
15✔
281

282
            $resourcesForPath = collect(File::allFiles($path))
15✔
283
                ->map(function ($file) use ($targetPath) {
15✔
284
                    $type = $this->appNamespace . static::normalizeClassName($targetPath) . '\\' . static::normalizeClassName($file->getRelativePathname());
15✔
285
                    $type = substr($type, 0, -strlen('.php'));
15✔
286

287
                    return $type;
15✔
288
                })
15✔
289
                ->filter(function ($type) {
15✔
290
                    // Only include classes that extend Filament's Resource base class,
291
                    // since recursive discovery may find non-resource classes (e.g. Pages, Schemas, Tables).
292
                    return class_exists($type)
15✔
293
                        && is_subclass_of($type, \Filament\Resources\Resource::class);
15✔
294
                });
15✔
295

296
            // Note: this will autoload the models if $configured = true
297
            if ($configuredOnly) {
15✔
298
                $resourcesForPath = $resourcesForPath->filter(function ($type) {
15✔
299
                    if (!in_array(\Luttje\FilamentUserAttributes\Contracts\UserAttributesConfigContract::class, class_implements($type))) {
15✔
300
                        return false;
15✔
301
                    }
302

303
                    return true;
15✔
304
                });
15✔
305
            }
306

307
            $resourcesForPath = $resourcesForPath->mapWithKeys(function ($type) use ($nameTransformer) {
15✔
308
                return [$type => $nameTransformer($type)];
15✔
309
            })
15✔
310
                ->toArray();
15✔
311

312
            $resources = array_merge($resources, $resourcesForPath);
15✔
313
        }
314

315
        return $resources;
48✔
316
    }
317

318
    /**
319
     * Discovers all models that could possibly be configured with user attributes.
320
     */
321
    public function getConfigurableModels($configuredOnly = true)
9✔
322
    {
323
        $discoverPaths = config('filament-user-attributes.discover_models');
9✔
324

325
        if ($discoverPaths === false) {
9✔
326
            return [];
×
327
        }
328

329
        return $this->discoverConfigurableModels($discoverPaths, $configuredOnly);
9✔
330
    }
331

332
    /**
333
     * Discovers all models that could possibly be configured with user attributes.
334
     */
335
    public function discoverConfigurableModels(array $paths, bool $configuredOnly): array
9✔
336
    {
337
        $models = [];
9✔
338

339
        foreach ($paths as $targetPath) {
9✔
340
            $path = $this->appPath . $targetPath;
9✔
341

342
            if (!File::exists($path)) {
9✔
343
                continue;
×
344
            }
345

346
            $modelsForPath = collect(File::allFiles($path))
9✔
347
                ->map(function ($file) use ($targetPath) {
9✔
348
                    $type = $this->appNamespace . static::normalizeClassName($targetPath) . '\\' . static::normalizeClassName($file->getRelativePathname());
9✔
349
                    $type = substr($type, 0, -strlen('.php'));
9✔
350

351
                    return $type;
9✔
352
                });
9✔
353

354
            // Note: this will autoload the models if $configured = true
355
            if ($configuredOnly) {
9✔
356
                $modelsForPath = $modelsForPath->filter(function ($type) {
6✔
357
                    if (!class_exists($type)) {
6✔
358
                        return false;
×
359
                    }
360

361
                    if (!in_array(\Luttje\FilamentUserAttributes\Contracts\HasUserAttributesContract::class, class_implements($type))) {
6✔
362
                        return false;
6✔
363
                    }
364

365
                    return true;
6✔
366
                });
6✔
367
            }
368

369
            $models = array_merge($models, $modelsForPath->toArray());
9✔
370
        }
371

372
        return $models;
9✔
373
    }
374

375
    /**
376
     * Uses configured path discovery information to find the path for the given
377
     * resource class
378
     */
379
    public function findResourceFilePath(string $resource): string
×
380
    {
381
        $discoverPaths = $this->getResourceDiscoverPaths();
×
382

383
        foreach ($discoverPaths as $targetPath) {
×
384
            $path = $this->appPath . $targetPath;
×
385

386
            if (!File::exists($path)) {
×
387
                continue;
×
388
            }
389

NEW
390
            foreach (File::allFiles($path) as $file) {
×
NEW
391
                if ($file->getFilename() === class_basename($resource) . '.php') {
×
NEW
392
                    return $file->getPathname();
×
393
                }
394
            }
395
        }
396

397
        throw new \Exception("Could not find the file for resource '$resource'. Did you forget to add it's directory to the 'filament-user-attributes.discover_resources' config option?");
×
398
    }
399

400
    /**
401
     * Uses configured path discovery information to find the path for the given
402
     * model class
403
     */
404
    public function findModelFilePath(string $model): string
3✔
405
    {
406
        $discoverPaths = config('filament-user-attributes.discover_models');
3✔
407

408
        foreach ($discoverPaths as $targetPath) {
3✔
409
            $path = $this->appPath . $targetPath;
3✔
410

411
            if (!File::exists($path)) {
3✔
412
                continue;
×
413
            }
414

415
            foreach (File::allFiles($path) as $file) {
3✔
416
                if ($file->getFilename() === class_basename($model) . '.php') {
3✔
417
                    return $file->getPathname();
3✔
418
                }
419
            }
420
        }
421

422
        throw new \Exception("Could not find the file for model '$model'. Did you forget to add it's directory to the 'filament-user-attributes.discover_models' config option?");
×
423
    }
424

425
    /**
426
     * Helper function to get label for a component.
427
     */
428
    private function getComponentLabel($component, ?string $parentLabel = null): string
54✔
429
    {
430
        $label = $component->getLabel();
54✔
431

432
        if ($label === null) {
54✔
433
            $label = '';
33✔
434
        }
435

436
        return $parentLabel ? ($parentLabel . ' > ' . $label) : $label;
54✔
437
    }
438

439
    /**
440
     * Gets all components and child components as a flat array of names with labels
441
     */
442
    public function getAllFieldComponents(array $components, ?string $parentLabel = null): array
33✔
443
    {
444
        $namesWithLabels = [];
33✔
445

446
        foreach ($components as $component) {
33✔
447
            $label = $this->getComponentLabel($component, $parentLabel);
33✔
448

449
            if ($component instanceof Field) {
33✔
450
                $namesWithLabels[] = [
33✔
451
                    'name' => $component->getName(),
33✔
452
                    'label' => $label,
33✔
453
                    'statePath' => $component->getStatePath(false),
33✔
454
                ];
33✔
455
            }
456

457
            if ($this->componentHasChildren($component)) {
33✔
458
                try {
459
                    $childComponents = $component->getChildComponents();
33✔
460
                } catch (\Error $e) {
×
461
                    $raw = $component->getDefaultChildComponents();
×
462
                    $childComponents = is_array($raw) ? $raw : [];
×
463
                }
464
                $namesWithLabels = array_merge(
33✔
465
                    $namesWithLabels,
33✔
466
                    $this->getAllFieldComponents(
33✔
467
                        $childComponents,
33✔
468
                        $label
33✔
469
                    )
33✔
470
                );
33✔
471
            }
472
        }
473

474
        return $namesWithLabels;
33✔
475
    }
476

477
    /**
478
     * Gets all columns as a flat array of names with labels
479
     */
480
    public function getAllTableColumns(array $columns): array
33✔
481
    {
482
        $namesWithLabels = [];
33✔
483

484
        /** @var Column $column */
485
        foreach ($columns as $column) {
33✔
486
            $label = $this->getComponentLabel($column);
33✔
487
            $namesWithLabels[] = [
33✔
488
                'name' => $column->getName(),
33✔
489
                'label' => $label,
33✔
490
            ];
33✔
491
        }
492

493
        return $namesWithLabels;
33✔
494
    }
495

496
    /**
497
     * Search the components and child components until the component with the given name is found,
498
     * then add the given component after it.
499
     */
500
    public function addFieldBesidesField(
12✔
501
        array $components,
502
        string $siblingComponentName,
503
        string $position,
504
        Component $componentToAdd,
505
        ?string $parentLabel = null,
506
        &$siblingFound = false
507
    ): array {
508
        $newComponents = [];
12✔
509

510
        foreach ($components as $component) {
12✔
511
            $label = $this->getComponentLabel($component, $parentLabel);
12✔
512

513
            $newComponents[] = $component;
12✔
514

515
            if (
516
                $component instanceof Field
12✔
517
                && $label === $siblingComponentName
12✔
518
            ) {
519
                $siblingFound = true;
9✔
520
                if ($position === 'before') {
9✔
521
                    array_splice($newComponents, count($newComponents) - 1, 0, [$componentToAdd]);
6✔
522
                } elseif ($position === 'after') {
3✔
523
                    $newComponents[] = $componentToAdd;
3✔
524
                }
525
            }
526

527
            if ($this->componentHasChildren($component)) {
12✔
528
                try {
529
                    $containerChildComponents = $component->getChildComponents();
6✔
530
                } catch (\Error $e) {
6✔
531
                    $raw = $component->getDefaultChildComponents();
6✔
532
                    $containerChildComponents = is_array($raw) ? $raw : [];
6✔
533
                }
534
                $childComponents = $this->addFieldBesidesField(
6✔
535
                    $containerChildComponents,
6✔
536
                    $siblingComponentName,
6✔
537
                    $position,
6✔
538
                    $componentToAdd,
6✔
539
                    $label,
6✔
540
                    $siblingFound
6✔
541
                );
6✔
542

543
                $component->childComponents($childComponents);
6✔
544
            }
545
        }
546

547
        if (!$siblingFound && $parentLabel === null) {
12✔
548
            $newComponents[] = $componentToAdd;
3✔
549
        }
550

551
        return $newComponents;
12✔
552
    }
553

554
    /**
555
     * Search the columns and child columns until the column with the given name is found,
556
     * unlike with forms, tables simply have columns in a flat array next to each other.
557
     */
558
    public function addColumnBesidesColumn(array $columns, string $siblingColumnName, string $position, Column $columnToAdd): array
9✔
559
    {
560
        $newColumns = [];
9✔
561

562
        foreach ($columns as $column) {
9✔
563
            $label = $this->getComponentLabel($column);
9✔
564
            $newColumns[] = $column;
9✔
565

566
            if ($label === $siblingColumnName) {
9✔
567
                if ($position === 'before') {
9✔
568
                    array_splice($newColumns, count($newColumns) - 1, 0, [$columnToAdd]);
3✔
569
                } elseif ($position === 'after') {
6✔
570
                    $newColumns[] = $columnToAdd;
6✔
571
                } else {
572
                    throw new \Exception("Invalid position '$position' given.");
×
573
                }
574
            }
575
        }
576

577
        return $newColumns;
9✔
578
    }
579

580
    /**
581
     * Merges the custom fields into the given form schema.
582
     */
583
    public function mergeCustomFormFields(array $fields, string $resource): array
3✔
584
    {
585
        $customFields = collect(FilamentUserAttributes::getUserAttributeFields($resource));
3✔
586
        $customFieldCount = $customFields->count();
3✔
587

588
        $inheritingFieldsMap = $customFields->filter(function ($customField) {
3✔
589
            return $customField['inheritance']['enabled'] === true;
3✔
590
        })->mapWithKeys(function ($customField) {
3✔
591
            $key = $customField['inheritance']['relation'];
×
592

593
            if ($key === '__self') {
×
594
                $key = $customField['inheritance']['attribute'];
×
595
            }
596

597
            return [$key => $customField];
×
598
        });
3✔
599

600
        for ($i = 0; $i < $customFieldCount; $i++) {
3✔
601
            $customField = $customFields->pop();
3✔
602

603
            if (
604
                !isset($customField['ordering'])
3✔
605
                || $customField['ordering']['sibling'] === null
3✔
606
            ) {
607
                $customFields->prepend($customField);
3✔
608
                continue;
3✔
609
            }
610

611
            $fields = $this->addFieldBesidesField(
×
612
                $fields,
×
613
                $customField['ordering']['sibling'],
×
614
                $customField['ordering']['position'],
×
615
                $customField['field']
×
616
            );
×
617
        }
618

619
        $fields = array_merge($fields, $customFields->pluck('field')->toArray());
3✔
620

621
        $this->addStateChangeSignalToInheritedFields($fields, $inheritingFieldsMap);
3✔
622

623
        return $fields;
3✔
624
    }
625

626
    /**
627
     * Adds a state change signal to all fields that inherit from another model.
628
     * This ensures the inherited field updates, when the related field value
629
     * changes.
630
     */
631
    public function addStateChangeSignalToInheritedFields(array $fields, $inheritingFieldsMap): void
3✔
632
    {
633
        /** @var Component $field */
634
        foreach ($fields as $field) {
3✔
635
            if ($this->componentHasChildren($field)) {
3✔
636
                try {
637
                    $childComponents = $field->getChildComponents();
×
638
                } catch (\Error $e) {
×
639
                    // The component's container is not yet initialized (e.g. during schema
640
                    // capture before getComponents() has been invoked on the parent schema).
641
                    // Fall back to the raw stored child components to continue traversal.
642
                    $rawChildren = $field->getDefaultChildComponents();
×
643
                    $childComponents = is_array($rawChildren) ? $rawChildren : [];
×
644
                }
645

646
                $this->addStateChangeSignalToInheritedFields($childComponents, $inheritingFieldsMap);
×
647
            }
648

649
            $statePath = $field->getStatePath(false);
3✔
650
            $statePath = preg_replace('/_id$/', '', $statePath);
3✔
651
            $inheritingField = $inheritingFieldsMap->get($statePath);
3✔
652

653
            if (!$inheritingField) {
3✔
654
                continue;
3✔
655
            }
656

657
            // Ensure that the related field is live, so that state changes are reactive.
658
            if (!$field->isLive()) {
×
659
                $field->live();
×
660
            }
661

662
            $field->afterStateUpdated(static function (Component $component) use ($inheritingField): void {
×
663
                $components = $component->getContainer()
×
664
                    ->getFlatComponents(true);
×
665

666
                foreach ($components as $component) {
×
667
                    if ($component->getId() !== $inheritingField['field']->getId()) {
×
668
                        continue;
×
669
                    }
670

671
                    $component->fill();
×
672
                }
673
            });
×
674
        }
675
    }
676

677
    /**
678
     * Merges the custom columns into the given table schema.
679
     */
680
    public function mergeCustomTableColumns(array $columns, $resource): array
15✔
681
    {
682
        $customColumns = collect(FilamentUserAttributes::getUserAttributeColumns($resource));
15✔
683
        $customColumnCount = $customColumns->count();
15✔
684

685
        for ($i = 0; $i < $customColumnCount; $i++) {
15✔
686
            $customColumn = $customColumns->pop();
9✔
687

688
            if (
689
                !isset($customColumn['ordering'])
9✔
690
                || $customColumn['ordering']['sibling'] === null
9✔
691
            ) {
692
                $customColumns->prepend($customColumn);
6✔
693
                continue;
6✔
694
            }
695

696
            $columns = $this->addColumnBesidesColumn(
3✔
697
                $columns,
3✔
698
                $customColumn['ordering']['sibling'],
3✔
699
                $customColumn['ordering']['position'],
3✔
700
                $customColumn['column']
3✔
701
            );
3✔
702
        }
703

704
        return array_merge($columns, $customColumns->pluck('column')->toArray());
15✔
705
    }
706

707
    /**
708
     * Converts a class name to a human readable label by getting
709
     * the last part of the name and adding spaces between words.
710
     */
711
    public function classNameToLabel(string $className): string
18✔
712
    {
713
        if (method_exists($className, 'getModelLabel')) {
18✔
714
            $label = $className::getModelLabel();
15✔
715

716
            if (!empty($label)) {
15✔
717
                return $label . ucfirst(__('filament-user-attributes::user-attributes.suffix_page'));
15✔
718
            }
719
        }
720

721
        $className = class_basename($className);
3✔
722
        $className = preg_replace('/(?<!^)[A-Z]/', ' $0', $className);
3✔
723
        $className = preg_replace('/Resource$/', ucfirst(__('filament-user-attributes::user-attributes.suffix_page')), $className);
3✔
724

725
        return $className;
3✔
726
    }
727

728
    /**
729
     * Converts a model class name to a human readable label by getting
730
     * the last part of the name and translating it using the validation
731
     * localization file.
732
     */
733
    public function classNameToModelLabel(string $className, int $amount = 1): string
3✔
734
    {
735
        $className = class_basename($className);
3✔
736
        $className = trans_choice('validation.attributes.' . Str::snake($className), $amount);
3✔
737

738
        return $className;
3✔
739
    }
740

741
    /**
742
     * Tries to get a model from the given resource class through the getModel method.
743
     * If the getModel method is not found, the user is informed on how to properly
744
     * implement Livewire components.
745
     */
746
    public function getModelFromResource(string $resource): string
48✔
747
    {
748
        if (!method_exists($resource, 'getModel')) {
48✔
749
            throw new \Exception("The resource '$resource' does not implement the getModel method. If you are using a Livewire component, you need to implement the static getModel method yourself.");
×
750
        }
751

752
        $model = $resource::getModel();
48✔
753

754
        if ($model === null) {
48✔
755
            throw new \Exception("The resource '$resource' did not return a model from the static getModel function (or it was null).");
×
756
        }
757

758
        return $model;
48✔
759
    }
760

761
    /**
762
     * Gets all resources mapped by their models
763
     */
764
    public function getResourcesByModel(): Collection
36✔
765
    {
766
        $resources = $this->getConfigurableResources();
36✔
767
        $modelsMappedToResources = collect($resources)
36✔
768
            ->filter(function ($name, string $class) {
36✔
769
                return method_exists($class, 'getModel');
3✔
770
            })
36✔
771
            ->mapWithKeys(function ($name, string $class) {
36✔
772
                return [$this->getModelFromResource($class) => $class];
3✔
773
            });
36✔
774

775
        return $modelsMappedToResources;
36✔
776
    }
777
}
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

© 2026 Coveralls, Inc