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

luttje / filament-user-attributes / 7254443569

18 Dec 2023 10:14PM UTC coverage: 73.72% (-0.2%) from 73.873%
7254443569

push

github

web-flow
Feature/custom config field support (#6)

* refactor so configs related to user attributes can be fetched + better livewire support

* let devs add custom config fields

* remove changelog workflow

43 of 67 new or added lines in 8 files covered. (64.18%)

1 existing line in 1 file now uncovered.

1296 of 1758 relevant lines covered (73.72%)

30.13 hits per line

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

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

3
namespace Luttje\FilamentUserAttributes;
4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
149
            $components[] = $component;
×
150
        }
151

152
        return $components;
36✔
153
    }
154

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

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

166
        return $config->getUserAttributeColumns($resource);
20✔
167
    }
168

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

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

180
        return $config->getUserAttributeFields($resource);
8✔
181
    }
182

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

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

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

200
        return $config;
24✔
201
    }
202

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

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

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

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

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

234
        if ($discoverPaths === false) {
56✔
235
            return false;
4✔
236
        }
237

238
        return self::normalizePaths($discoverPaths);
52✔
239
    }
240

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

248
        if ($discoverPaths === false) {
56✔
249
            $resources = $this->registeredResources;
4✔
250

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

255
            return $resources;
4✔
256
        }
257

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

262
        return $this->cachedDiscoveredResources;
52✔
263
    }
264

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

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

275
            if (!File::exists($path)) {
52✔
276
                continue;
40✔
277
            }
278

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

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

286
                    return $type;
12✔
287
                });
12✔
288

289
            // Note: this will autoload the models if $configured = true
290
            if ($configuredOnly) {
12✔
291
                $resourcesForPath = $resourcesForPath->filter(function ($type) {
12✔
292
                    if (!class_exists($type)) {
12✔
293
                        return false;
×
294
                    }
295

296
                    if (!in_array(\Luttje\FilamentUserAttributes\Contracts\UserAttributesConfigContract::class, class_implements($type))) {
12✔
297
                        return false;
12✔
298
                    }
299

300
                    return true;
12✔
301
                });
12✔
302
            }
303

304
            $resourcesForPath = $resourcesForPath->mapWithKeys(function ($type) use ($nameTransformer) {
12✔
305
                return [$type => $nameTransformer($type)];
12✔
306
            })
12✔
307
                ->toArray();
12✔
308

309
            $resources = array_merge($resources, $resourcesForPath);
12✔
310
        }
311

312
        return $resources;
52✔
313
    }
314

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

322
        if ($discoverPaths === false) {
12✔
323
            return [];
×
324
        }
325

326
        return $this->discoverConfigurableModels($discoverPaths, $configuredOnly);
12✔
327
    }
328

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

336
        foreach ($paths as $targetPath) {
12✔
337
            $path = $this->appPath . $targetPath;
12✔
338

339
            if (!File::exists($path)) {
12✔
340
                continue;
×
341
            }
342

343
            $modelsForPath = collect(File::files($path))
12✔
344
                ->map(function ($file) use ($targetPath) {
12✔
345
                    $type = $this->appNamespace . static::normalizeClassName($targetPath) . '\\' . $file->getRelativePathName();
12✔
346
                    $type = substr($type, 0, -strlen('.php'));
12✔
347

348
                    return $type;
12✔
349
                });
12✔
350

351
            // Note: this will autoload the models if $configured = true
352
            if ($configuredOnly) {
12✔
353
                $modelsForPath = $modelsForPath->filter(function ($type) {
8✔
354
                    if (!class_exists($type)) {
8✔
355
                        return false;
×
356
                    }
357

358
                    if (!in_array(\Luttje\FilamentUserAttributes\Contracts\HasUserAttributesContract::class, class_implements($type))) {
8✔
359
                        return false;
8✔
360
                    }
361

362
                    return true;
8✔
363
                });
8✔
364
            }
365

366
            $models = array_merge($models, $modelsForPath->toArray());
12✔
367
        }
368

369
        return $models;
12✔
370
    }
371

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

380
        foreach ($discoverPaths as $targetPath) {
×
381
            $path = $this->appPath . $targetPath;
×
382

383
            if (!File::exists($path)) {
×
384
                continue;
×
385
            }
386

387
            $file = $path . DIRECTORY_SEPARATOR . class_basename($resource) . '.php';
×
388

389
            if (File::exists($file)) {
×
390
                return $file;
×
391
            }
392
        }
393

394
        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?");
×
395
    }
396

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

405
        foreach ($discoverPaths as $targetPath) {
4✔
406
            $path = $this->appPath . $targetPath;
4✔
407

408
            if (!File::exists($path)) {
4✔
409
                continue;
×
410
            }
411

412
            $file = $path . DIRECTORY_SEPARATOR . class_basename($model) . '.php';
4✔
413

414
            if (File::exists($file)) {
4✔
415
                return $file;
4✔
416
            }
417
        }
418

419
        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?");
×
420
    }
421

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

429
        if ($label === null) {
68✔
430
            $label = '';
40✔
431
        }
432

433
        return $parentLabel ? ($parentLabel . ' > ' . $label) : $label;
68✔
434
    }
435

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

443
        foreach ($components as $component) {
36✔
444
            $label = $this->getComponentLabel($component, $parentLabel);
36✔
445

446
            if ($component instanceof \Filament\Forms\Components\Field) {
36✔
447
                $namesWithLabels[] = [
36✔
448
                    'name' => $component->getName(),
36✔
449
                    'label' => $label,
36✔
450
                    'statePath' => $component->getStatePath(false),
36✔
451
                ];
36✔
452
            }
453

454
            if ($this->componentHasChildren($component)) {
36✔
455
                $namesWithLabels = array_merge(
36✔
456
                    $namesWithLabels,
36✔
457
                    $this->getAllFieldComponents(
36✔
458
                        $component->getChildComponents(),
36✔
459
                        $label
36✔
460
                    )
36✔
461
                );
36✔
462
            }
463
        }
464

465
        return $namesWithLabels;
36✔
466
    }
467

468
    /**
469
     * Gets all columns as a flat array of names with labels
470
     */
471
    public function getAllTableColumns(array $columns): array
36✔
472
    {
473
        $namesWithLabels = [];
36✔
474

475
        /** @var Column $column */
476
        foreach ($columns as $column) {
36✔
477
            $label = $this->getComponentLabel($column);
36✔
478
            $namesWithLabels[] = [
36✔
479
                'name' => $column->getName(),
36✔
480
                'label' => $label,
36✔
481
            ];
36✔
482
        }
483

484
        return $namesWithLabels;
36✔
485
    }
486

487
    /**
488
     * Search the components and child components until the component with the given name is found,
489
     * then add the given component after it.
490
     */
491
    public function addFieldBesidesField(
20✔
492
        array $components,
493
        string $siblingComponentName,
494
        string $position,
495
        Component $componentToAdd,
496
        ?string $parentLabel = null,
497
        &$siblingFound = false
498
    ): array {
499
        $newComponents = [];
20✔
500

501
        foreach ($components as $component) {
20✔
502
            $label = $this->getComponentLabel($component, $parentLabel);
20✔
503

504
            $newComponents[] = $component;
20✔
505

506
            if ($component instanceof \Filament\Forms\Components\Field
20✔
507
            && $label === $siblingComponentName) {
20✔
508
                $siblingFound = true;
16✔
509
                if ($position === 'before') {
16✔
510
                    array_splice($newComponents, count($newComponents) - 1, 0, [$componentToAdd]);
12✔
511
                } elseif($position === 'after') {
4✔
512
                    $newComponents[] = $componentToAdd;
4✔
513
                }
514
            }
515

516
            if ($this->componentHasChildren($component)) {
20✔
517
                $containerChildComponents = $component->getChildComponents();
12✔
518
                $childComponents = $this->addFieldBesidesField(
12✔
519
                    $containerChildComponents,
12✔
520
                    $siblingComponentName,
12✔
521
                    $position,
12✔
522
                    $componentToAdd,
12✔
523
                    $label,
12✔
524
                    $siblingFound
12✔
525
                );
12✔
526

527
                $component->childComponents($childComponents);
12✔
528
            }
529
        }
530

531
        if (!$siblingFound && $parentLabel === null) {
20✔
532
            $newComponents[] = $componentToAdd;
4✔
533
        }
534

535
        return $newComponents;
20✔
536
    }
537

538
    /**
539
     * Search the columns and child columns until the column with the given name is found,
540
     * unlike with forms, tables simply have columns in a flat array next to each other.
541
     */
542
    public function addColumnBesidesColumn(array $columns, string $siblingColumnName, string $position, Column $columnToAdd): array
12✔
543
    {
544
        $newColumns = [];
12✔
545

546
        foreach ($columns as $column) {
12✔
547
            $label = $this->getComponentLabel($column);
12✔
548
            $newColumns[] = $column;
12✔
549

550
            if ($label === $siblingColumnName) {
12✔
551
                if ($position === 'before') {
12✔
552
                    array_splice($newColumns, count($newColumns) - 1, 0, [$columnToAdd]);
4✔
553
                } elseif ($position === 'after') {
8✔
554
                    $newColumns[] = $columnToAdd;
8✔
555
                } else {
556
                    throw new \Exception("Invalid position '$position' given.");
×
557
                }
558
            }
559
        }
560

561
        return $newColumns;
12✔
562
    }
563

564
    /**
565
     * Merges the custom fields into the given form schema.
566
     */
567
    public function mergeCustomFormFields(array $fields, string $resource): array
8✔
568
    {
569
        $customFields = collect(FilamentUserAttributes::getUserAttributeFields($resource));
8✔
570
        $customFieldCount = $customFields->count();
8✔
571

572
        $inheritingFieldsMap = $customFields->filter(function ($customField) {
8✔
573
            return $customField['inheritance']['enabled'] === true;
8✔
574
        })->mapWithKeys(function ($customField) {
8✔
575
            $key = $customField['inheritance']['relation'];
×
576

577
            if ($key === '__self') {
×
578
                $key = $customField['inheritance']['attribute'];
×
579
            }
580

581
            return [$key => $customField];
×
582
        });
8✔
583

584
        for ($i = 0; $i < $customFieldCount; $i++) {
8✔
585
            $customField = $customFields->pop();
8✔
586

587
            if (!isset($customField['ordering'])
8✔
588
                || $customField['ordering']['sibling'] === null) {
8✔
589
                $customFields->prepend($customField);
8✔
590
                continue;
8✔
591
            }
592

593
            $fields = $this->addFieldBesidesField(
4✔
594
                $fields,
4✔
595
                $customField['ordering']['sibling'],
4✔
596
                $customField['ordering']['position'],
4✔
597
                $customField['field']
4✔
598
            );
4✔
599
        }
600

601
        $fields = array_merge($fields, $customFields->pluck('field')->toArray());
8✔
602

603
        $this->addStateChangeSignalToInheritedFields($fields, $inheritingFieldsMap);
8✔
604

605
        return $fields;
8✔
606
    }
607

608
    /**
609
     * Adds a state change signal to all fields that inherit from another model.
610
     * This ensures the inherited field updates, when the related field value
611
     * changes.
612
     */
613
    public function addStateChangeSignalToInheritedFields(array $fields, $inheritingFieldsMap): void
8✔
614
    {
615
        /** @var Component $field */
616
        foreach ($fields as $field) {
8✔
617
            if ($this->componentHasChildren($field)) {
8✔
618
                $this->addStateChangeSignalToInheritedFields(
4✔
619
                    $field->getChildComponents(),
4✔
620
                    $inheritingFieldsMap
4✔
621
                );
4✔
622
            }
623

624
            $statePath = $field->getStatePath(false);
8✔
625
            $statePath = preg_replace('/_id$/', '', $statePath);
8✔
626
            $inheritingField = $inheritingFieldsMap->get($statePath);
8✔
627

628
            if (!$inheritingField) {
8✔
629
                continue;
8✔
630
            }
631

632
            // Ensure that the related field is live, so that state changes are reactive.
633
            if (!$field->isLive()) {
×
634
                $field->live();
×
635
            }
636

637
            $field->afterStateUpdated(static function (Component $component) use ($inheritingField): void {
×
638
                $components = $component->getContainer()
×
639
                    ->getFlatComponents(true);
×
640

641
                foreach ($components as $component) {
×
642
                    if ($component->getId() !== $inheritingField['field']->getId()) {
×
643
                        continue;
×
644
                    }
645

646
                    $component->fill();
×
647
                }
648
            });
×
649
        }
650
    }
651

652
    /**
653
     * Merges the custom columns into the given table schema.
654
     */
655
    public function mergeCustomTableColumns(array $columns, $resource): array
20✔
656
    {
657
        $customColumns = collect(FilamentUserAttributes::getUserAttributeColumns($resource));
20✔
658
        $customColumnCount = $customColumns->count();
20✔
659

660
        for ($i = 0; $i < $customColumnCount; $i++) {
20✔
661
            $customColumn = $customColumns->pop();
12✔
662

663
            if (!isset($customColumn['ordering'])
12✔
664
                || $customColumn['ordering']['sibling'] === null) {
12✔
665
                $customColumns->prepend($customColumn);
8✔
666
                continue;
8✔
667
            }
668

669
            $columns = $this->addColumnBesidesColumn(
4✔
670
                $columns,
4✔
671
                $customColumn['ordering']['sibling'],
4✔
672
                $customColumn['ordering']['position'],
4✔
673
                $customColumn['column']
4✔
674
            );
4✔
675
        }
676

677
        return array_merge($columns, $customColumns->pluck('column')->toArray());
20✔
678
    }
679

680
    /**
681
     * Converts a class name to a human readable label by getting
682
     * the last part of the name and adding spaces between words.
683
     */
684
    public function classNameToLabel(string $className): string
16✔
685
    {
686
        if (method_exists($className, 'getModelLabel')) {
16✔
687
            $label = $className::getModelLabel();
12✔
688

689
            if (!empty($label)) {
12✔
690
                return $label . ucfirst(__('filament-user-attributes::user-attributes.suffix_page'));
12✔
691
            }
692
        }
693

694
        $className = class_basename($className);
4✔
695
        $className = preg_replace('/(?<!^)[A-Z]/', ' $0', $className);
4✔
696
        $className = preg_replace('/Resource$/', ucfirst(__('filament-user-attributes::user-attributes.suffix_page')), $className);
4✔
697

698
        return $className;
4✔
699
    }
700

701
    /**
702
     * Converts a model class name to a human readable label by getting
703
     * the last part of the name and translating it using the validation
704
     * localization file.
705
     */
706
    public function classNameToModelLabel(string $className, int $amount = 1): string
×
707
    {
708
        $className = class_basename($className);
×
709
        $className = trans_choice('validation.attributes.' . Str::snake($className), $amount);
×
710

711
        return $className;
×
712
    }
713

714
    /**
715
     * Tries to get a model from the given resource class through the getModel method.
716
     * If the getModel method is not found, the user is informed on how to properly
717
     * implement Livewire components.
718
     */
719
    public function getModelFromResource(string $resource): string
56✔
720
    {
721
        if (!method_exists($resource, 'getModel')) {
56✔
NEW
722
            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.");
×
723
        }
724

725
        $model = $resource::getModel();
56✔
726

727
        if ($model === null) {
56✔
NEW
728
            throw new \Exception("The resource '$resource' did not return a model from the static getModel function (or it was null).");
×
729
        }
730

731
        return $model;
56✔
732
    }
733

734
    /**
735
     * Gets all resources mapped by their models
736
     */
737
    public function getResourcesByModel(): Collection
36✔
738
    {
739
        $resources = $this->getConfigurableResources();
36✔
740
        $modelsMappedToResources = collect($resources)
36✔
741
            ->filter(function ($name, string $class) {
36✔
NEW
742
                return method_exists($class, 'getModel');
×
743
            })
36✔
744
            ->mapWithKeys(function ($name, string $class) {
36✔
NEW
745
                return [$this->getModelFromResource($class) => $class];
×
746
            });
36✔
747

748
        return $modelsMappedToResources;
36✔
749
    }
750
}
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