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

luttje / filament-user-attributes / 6914439067

18 Nov 2023 02:13PM UTC coverage: 63.165% (-0.3%) from 63.51%
6914439067

push

github

luttje
fix tests by adding model label to resource

914 of 1447 relevant lines covered (63.17%)

19.07 hits per line

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

89.42
/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\Section;
8
use Filament\Forms\Components\Tabs;
9
use Filament\Forms\Components\Tabs\Tab;
10
use Filament\Tables\Columns\Column;
11
use Illuminate\Support\Facades\File;
12
use Luttje\FilamentUserAttributes\Contracts\ConfiguresUserAttributesContract;
13
use Luttje\FilamentUserAttributes\Contracts\UserAttributesConfigContract;
14
use Luttje\FilamentUserAttributes\Filament\UserAttributeComponentFactoryRegistry;
15
use Luttje\FilamentUserAttributes\Traits\ConfiguresUserAttributes;
16

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

32
    /**
33
     * @var array|null Cached list of discovered resources.
34
     */
35
    protected ?array $cachedDiscoveredResources = null;
36

37
    /**
38
     * @var string Path to the application directory.
39
     */
40
    protected string $appPath;
41

42
    /**
43
     * @var string Namespace of the application.
44
     */
45
    protected string $appNamespace;
46

47
    /**
48
     * Constructor for FilamentUserAttributes.
49
     *
50
     * @param string|null $appPath       Optional path to the application directory.
51
     * @param string|null $appNamespace  Optional namespace of the application.
52
     */
53
    public function __construct(string $appPath = null, string $appNamespace = null)
220✔
54
    {
55
        $this->appNamespace = $appNamespace ?? app()->getNamespace();
220✔
56
        $this->appPath = $appPath ?? app_path();
220✔
57

58
        $this->appNamespace = rtrim($this->appNamespace, '\\') . '\\';
220✔
59
        $this->appPath = rtrim($this->appPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
220✔
60
    }
61

62
    /**
63
     * Register resources that can be configured with user attributes.
64
     * You can provide an associative array of resources, where the key
65
     * is the resource class and the value is the resource label to show
66
     * to users.
67
     *
68
     * You can also provide a closure that returns an array of resources
69
     * in the same format.
70
     *
71
     * Call this in your AppServiceProvider's boot function.
72
     */
73
    public function registerResources(array | Closure $resources): void
8✔
74
    {
75
        if (config('filament-user-attributes.discover_resources') !== false) {
8✔
76
            throw new \Exception("You cannot register resources when the 'filament-user-attributes.discover_resources' config option is enabled. Set it to false.");
4✔
77
        }
78

79
        if (is_array($resources)) {
4✔
80
            $this->registeredResources = array_merge($this->registeredResources, $resources);
4✔
81
        } elseif ($resources instanceof Closure) {
×
82
            $this->registeredResources = $resources;
×
83
        }
84
    }
85

86
    /**
87
     * Registers all types of user attribute field factories.
88
     */
89
    public function registerDefaultUserAttributeComponentFactories(): void
220✔
90
    {
91
        UserAttributeComponentFactoryRegistry::register('text', \Luttje\FilamentUserAttributes\Filament\Factories\TextComponentFactory::class);
220✔
92
        UserAttributeComponentFactoryRegistry::register('number', \Luttje\FilamentUserAttributes\Filament\Factories\NumberInputComponentFactory::class);
220✔
93
        UserAttributeComponentFactoryRegistry::register('textarea', \Luttje\FilamentUserAttributes\Filament\Factories\TextareaComponentFactory::class);
220✔
94
        UserAttributeComponentFactoryRegistry::register('richeditor', \Luttje\FilamentUserAttributes\Filament\Factories\RichEditorComponentFactory::class);
220✔
95
        UserAttributeComponentFactoryRegistry::register('tags', \Luttje\FilamentUserAttributes\Filament\Factories\TagsInputComponentFactory::class);
220✔
96

97
        UserAttributeComponentFactoryRegistry::register('select', \Luttje\FilamentUserAttributes\Filament\Factories\SelectComponentFactory::class);
220✔
98
        UserAttributeComponentFactoryRegistry::register('checkbox', \Luttje\FilamentUserAttributes\Filament\Factories\CheckboxComponentFactory::class);
220✔
99
        UserAttributeComponentFactoryRegistry::register('radio', \Luttje\FilamentUserAttributes\Filament\Factories\RadioComponentFactory::class);
220✔
100

101
        UserAttributeComponentFactoryRegistry::register('datetime', \Luttje\FilamentUserAttributes\Filament\Factories\DateTimeComponentFactory::class);
220✔
102
    }
103

104
    /**
105
     * Returns the user attribute columns.
106
     */
107
    public function getUserAttributeColumns(string $resource): array
16✔
108
    {
109
        $config = $this->getUserAttributeConfig($resource);
16✔
110

111
        if (!in_array(ConfiguresUserAttributes::class, class_uses_recursive($config))) {
16✔
112
            throw new \Exception("The resource '$resource' does not correctly use the ConfiguresUserAttributes trait");
×
113
        }
114

115
        return $config->getUserAttributeColumns($resource);
16✔
116
    }
117

118
    /**
119
     * Returns the user attribute fields.
120
     */
121
    public function getUserAttributeFields(string $resource): array
4✔
122
    {
123
        $config = $this->getUserAttributeConfig($resource);
4✔
124

125
        if (!in_array(ConfiguresUserAttributes::class, class_uses_recursive($config))) {
4✔
126
            throw new \Exception("The resource '$resource' does not use the ConfiguresUserAttributes trait");
×
127
        }
128

129
        return $config->getUserAttributeFields($resource);
4✔
130
    }
131

132
    /**
133
     * Returns the user attribute configuration model.
134
     */
135
    public function getUserAttributeConfig(string $resource): ConfiguresUserAttributesContract
16✔
136
    {
137
        if (!in_array(UserAttributesConfigContract::class, class_implements($resource))) {
16✔
138
            throw new \Exception("The resource '$resource' does not implement the UserAttributesConfigContract interface.");
×
139
        }
140

141
        /** @var ?UserAttributesConfigContract */
142
        $resource = $resource;
16✔
143
        $config = $resource::getUserAttributesConfig();
16✔
144

145
        if ($config === null) {
16✔
146
            throw new \Exception("The resource '" . strval($resource) . "' did not return a configuration model from the getUserAttributesConfig function (or it was null).");
×
147
        }
148

149
        return $config;
16✔
150
    }
151

152
    /**
153
     * Finds all resources that have the HasUserAttributesContract interface
154
     */
155
    public function getConfigurableResources()
12✔
156
    {
157
        $discoverPaths = config('filament-user-attributes.discover_resources');
12✔
158

159
        if ($discoverPaths === false) {
12✔
160
            $resources = $this->registeredResources;
4✔
161

162
            if ($resources instanceof Closure) {
4✔
163
                return $resources();
×
164
            }
165

166
            return $resources;
4✔
167
        }
168

169
        if ($this->cachedDiscoveredResources === null) {
8✔
170
            $this->cachedDiscoveredResources = $this->discoverConfigurableResources($discoverPaths);
8✔
171
        }
172

173
        return $this->cachedDiscoveredResources;
8✔
174
    }
175

176
    /**
177
     * Discovers all resources that have the HasUserAttributesContract interface
178
     */
179
    public function discoverConfigurableResources(array $paths): array
8✔
180
    {
181
        $resources = [];
8✔
182

183
        foreach ($paths as $targetPath) {
8✔
184
            $path = $this->appPath . $targetPath;
8✔
185

186
            if (!File::exists($path)) {
8✔
187
                continue;
×
188
            }
189

190
            $nameTransformer = config('filament-user-attributes.discovery_resource_name_transformer');
8✔
191

192
            $resourcesForPath = collect(File::allFiles($path))
8✔
193
                ->map(function ($file) use ($targetPath) {
8✔
194
                    $type = $this->appNamespace . $targetPath . '\\' . $file->getRelativePathName();
8✔
195
                    $type = substr($type, 0, -strlen('.php'));
8✔
196

197
                    return $type;
8✔
198
                })
8✔
199
                ->filter(function ($type) {
8✔
200
                    if (!class_exists($type)) {
8✔
201
                        return false;
8✔
202
                    }
203

204
                    if (!in_array(\Luttje\FilamentUserAttributes\Contracts\UserAttributesConfigContract::class, class_implements($type))) {
8✔
205
                        return false;
×
206
                    }
207

208
                    return true;
8✔
209
                })
8✔
210
                ->mapWithKeys(function ($type) use ($nameTransformer) {
8✔
211
                    return [$type => $nameTransformer($type)];
8✔
212
                })
8✔
213
                ->toArray();
8✔
214

215
            $resources = array_merge($resources, $resourcesForPath);
8✔
216
        }
217

218
        return $resources;
8✔
219
    }
220

221
    /**
222
     * Helper function to get label for a component.
223
     */
224
    private function getComponentLabel($component, ?string $parentLabel = null): string
32✔
225
    {
226
        $label = $component->getLabel();
32✔
227

228
        if (!empty($label)) {
32✔
229
            return $parentLabel ? ($parentLabel . ' > ' . $label) : $label;
32✔
230
        }
231

232
        return $parentLabel ?? '';
4✔
233
    }
234

235
    /**
236
     * Gets all components and child components as a flat array of names with labels
237
     */
238
    public function getAllFieldComponents(array $components, ?string $parentLabel = null): array
4✔
239
    {
240
        $namesWithLabels = [];
4✔
241

242
        foreach ($components as $component) {
4✔
243
            $label = $this->getComponentLabel($component, $parentLabel);
4✔
244

245
            if ($component instanceof \Filament\Forms\Components\Field) {
4✔
246
                $namesWithLabels[] = [
4✔
247
                    'name' => $component->getName(),
4✔
248
                    'label' => $label,
4✔
249
                ];
4✔
250
            }
251

252
            if ($component instanceof Tabs
4✔
253
                || $component instanceof Tab
4✔
254
                || $component instanceof Section
4✔
255
            ) {
256
                $namesWithLabels = array_merge(
4✔
257
                    $namesWithLabels,
4✔
258
                    $this->getAllFieldComponents(
4✔
259
                        $component->getChildComponents(),
4✔
260
                        $label
4✔
261
                    )
4✔
262
                );
4✔
263
            }
264
        }
265

266
        return $namesWithLabels;
4✔
267
    }
268

269
    /**
270
     * Gets all columns as a flat array of names with labels
271
     */
272
    public function getAllTableColumns(array $columns): array
4✔
273
    {
274
        $namesWithLabels = [];
4✔
275

276
        foreach ($columns as $column) {
4✔
277
            $label = $this->getComponentLabel($column);
4✔
278
            $namesWithLabels[] = [
4✔
279
                'name' => $column->getName(),
4✔
280
                'label' => $label,
4✔
281
            ];
4✔
282
        }
283

284
        return $namesWithLabels;
4✔
285
    }
286

287
    /**
288
     * Search the components and child components until the component with the given name is found,
289
     * then add the given component after it.
290
     */
291
    public function addFieldBesidesField(
16✔
292
        array $components,
293
        string $siblingComponentName,
294
        string $position,
295
        Component $componentToAdd,
296
        ?string $parentLabel = null,
297
        &$siblingFound = false
298
    ): array {
299
        $newComponents = [];
16✔
300

301
        foreach ($components as $component) {
16✔
302
            $label = $this->getComponentLabel($component, $parentLabel);
16✔
303

304
            $newComponents[] = $component;
16✔
305

306
            if ($component instanceof \Filament\Forms\Components\Field
16✔
307
            && $label === $siblingComponentName) {
16✔
308
                $siblingFound = true;
12✔
309
                if ($position === 'before') {
12✔
310
                    array_splice($newComponents, count($newComponents) - 1, 0, [$componentToAdd]);
8✔
311
                } elseif($position === 'after') {
4✔
312
                    $newComponents[] = $componentToAdd;
4✔
313
                }
314
            }
315

316
            if ($component instanceof Tabs
16✔
317
                || $component instanceof Tab
16✔
318
                || $component instanceof Section
16✔
319
            ) {
320
                $containerChildComponents = $component->getChildComponents();
8✔
321
                $childComponents = $this->addFieldBesidesField(
8✔
322
                    $containerChildComponents,
8✔
323
                    $siblingComponentName,
8✔
324
                    $position,
8✔
325
                    $componentToAdd,
8✔
326
                    $label,
8✔
327
                    $siblingFound
8✔
328
                );
8✔
329

330
                $component->childComponents($childComponents);
8✔
331
            }
332
        }
333

334
        if (!$siblingFound) {
16✔
335
            $newComponents[] = $componentToAdd;
4✔
336
        }
337

338
        return $newComponents;
16✔
339
    }
340

341
    /**
342
     * Search the columns and child columns until the column with the given name is found,
343
     * unlike with forms, tables simply have columns in a flat array next to each other.
344
     */
345
    public function addColumnBesidesColumn(array $columns, string $siblingColumnName, string $position, Column $columnToAdd): array
12✔
346
    {
347
        $newColumns = [];
12✔
348

349
        foreach ($columns as $column) {
12✔
350
            $label = $this->getComponentLabel($column);
12✔
351
            $newColumns[] = $column;
12✔
352

353
            if ($label === $siblingColumnName) {
12✔
354
                if ($position === 'before') {
12✔
355
                    array_splice($newColumns, count($newColumns) - 1, 0, [$columnToAdd]);
4✔
356
                } elseif ($position === 'after') {
8✔
357
                    $newColumns[] = $columnToAdd;
8✔
358
                } else {
359
                    throw new \Exception("Invalid position '$position' given.");
×
360
                }
361
            }
362
        }
363

364
        return $newColumns;
12✔
365
    }
366

367
    /**
368
     * Merges the custom fields into the given form schema.
369
     */
370
    public function mergeCustomFormFields(array $fields, string $resource): array
4✔
371
    {
372
        $customFields = collect(FilamentUserAttributes::getUserAttributeFields($resource));
4✔
373

374
        for ($i = 0; $i < $customFields->count(); $i++) {
4✔
375
            $customField = $customFields->pop();
4✔
376

377
            if (!isset($customField['ordering'])
4✔
378
                || $customField['ordering']['sibling'] === null) {
4✔
379
                $customFields->prepend($customField);
4✔
380
                continue;
4✔
381
            }
382

383
            $fields = $this->addFieldBesidesField(
×
384
                $fields,
×
385
                $customField['ordering']['sibling'],
×
386
                $customField['ordering']['position'],
×
387
                $customField['field']
×
388
            );
×
389
        }
390

391
        return array_merge($fields, $customFields->pluck('field')->toArray());
4✔
392
    }
393

394
    /**
395
     * Merges the custom columns into the given table schema.
396
     */
397
    public function mergeCustomTableColumns(array $columns, $resource): array
16✔
398
    {
399
        $customColumns = collect(FilamentUserAttributes::getUserAttributeColumns($resource));
16✔
400

401
        for ($i = 0; $i < $customColumns->count(); $i++) {
16✔
402
            $customColumn = $customColumns->pop();
12✔
403

404
            if (!isset($customColumn['ordering'])
12✔
405
                || $customColumn['ordering']['sibling'] === null) {
12✔
406
                $customColumns->prepend($customColumn);
8✔
407
                continue;
8✔
408
            }
409

410
            $columns = $this->addColumnBesidesColumn(
4✔
411
                $columns,
4✔
412
                $customColumn['ordering']['sibling'],
4✔
413
                $customColumn['ordering']['position'],
4✔
414
                $customColumn['column']
4✔
415
            );
4✔
416
        }
417

418
        return array_merge($columns, $customColumns->pluck('column')->toArray());
16✔
419
    }
420

421
    /**
422
     * Converts a class name to a human readable label by getting
423
     * the last part of the name and adding spaces between words.
424
     */
425
    public function classNameToLabel(string $className): string
8✔
426
    {
427
        if (method_exists($className, 'getModelLabel')) {
8✔
428
            $label = $className::getModelLabel();
8✔
429

430
            if (!empty($label)) {
8✔
431
                return $label . ucfirst(__('filament-user-attributes::user-attributes.suffix_page'));
8✔
432
            }
433
        }
434

435
        $className = class_basename($className);
×
436
        $className = preg_replace('/(?<!^)[A-Z]/', ' $0', $className);
×
437
        $className = preg_replace('/Resource$/', ucfirst(__('filament-user-attributes::user-attributes.suffix_page')), $className);
×
438

439
        return $className;
×
440
    }
441
}
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