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

luttje / filament-user-attributes / 6909724102

17 Nov 2023 10:28PM UTC coverage: 59.278% (+7.6%) from 51.704%
6909724102

push

github

luttje
fix basename not working for linux

1 of 2 new or added lines in 2 files covered. (50.0%)

103 existing lines in 8 files now uncovered.

837 of 1412 relevant lines covered (59.28%)

16.41 hits per line

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

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

3
namespace Luttje\FilamentUserAttributes;
4

5
use Closure;
6
use Filament\Forms\Components\Component;
7
use Filament\Tables\Columns\Column;
8
use Illuminate\Support\Facades\File;
9
use Luttje\FilamentUserAttributes\Contracts\ConfiguresUserAttributesContract;
10
use Luttje\FilamentUserAttributes\Contracts\UserAttributesConfigContract;
11
use Luttje\FilamentUserAttributes\Filament\UserAttributeComponentFactoryRegistry;
12
use Luttje\FilamentUserAttributes\Traits\ConfiguresUserAttributes;
13

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

29
    /**
30
     * @var array|null Cached list of discovered resources.
31
     */
32
    protected ?array $cachedDiscoveredResources = null;
33

34
    /**
35
     * @var string Path to the application directory.
36
     */
37
    protected string $appPath;
38

39
    /**
40
     * @var string Namespace of the application.
41
     */
42
    protected string $appNamespace;
43

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

55
        $this->appNamespace = rtrim($this->appNamespace, '\\') . '\\';
188✔
56
        $this->appPath = rtrim($this->appPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
188✔
57
    }
58

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

76
        if (is_array($resources)) {
4✔
77
            $this->registeredResources = array_merge($this->registeredResources, $resources);
4✔
UNCOV
78
        } elseif ($resources instanceof Closure) {
×
UNCOV
79
            $this->registeredResources = $resources;
×
80
        }
81
    }
82

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

94
        UserAttributeComponentFactoryRegistry::register('select', \Luttje\FilamentUserAttributes\Filament\Factories\SelectComponentFactory::class);
188✔
95
        UserAttributeComponentFactoryRegistry::register('checkbox', \Luttje\FilamentUserAttributes\Filament\Factories\CheckboxComponentFactory::class);
188✔
96
        UserAttributeComponentFactoryRegistry::register('radio', \Luttje\FilamentUserAttributes\Filament\Factories\RadioComponentFactory::class);
188✔
97

98
        UserAttributeComponentFactoryRegistry::register('datetime', \Luttje\FilamentUserAttributes\Filament\Factories\DateTimeComponentFactory::class);
188✔
99
    }
100

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

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

112
        return $config->getUserAttributeColumns($resource);
16✔
113
    }
114

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

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

126
        return $config->getUserAttributeFields($resource);
4✔
127
    }
128

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

138
        /** @var ?UserAttributesConfigContract */
139
        $resource = $resource;
16✔
140
        $config = $resource::getUserAttributesConfig();
16✔
141

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

146
        return $config;
16✔
147
    }
148

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

156
        if ($discoverPaths === false) {
12✔
157
            $resources = $this->registeredResources;
4✔
158

159
            if ($resources instanceof Closure) {
4✔
UNCOV
160
                return $resources();
×
161
            }
162

163
            return $resources;
4✔
164
        }
165

166
        if ($this->cachedDiscoveredResources === null) {
8✔
167
            $this->cachedDiscoveredResources = $this->discoverConfigurableResources($discoverPaths);
8✔
168
        }
169

170
        return $this->cachedDiscoveredResources;
8✔
171
    }
172

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

180
        foreach ($paths as $targetPath) {
8✔
181
            $path = $this->appPath . $targetPath;
8✔
182

183
            if (!File::exists($path)) {
8✔
UNCOV
184
                continue;
×
185
            }
186

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

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

194
                    return $type;
8✔
195
                })
8✔
196
                ->filter(function ($type) {
8✔
197
                    if (!class_exists($type)) {
8✔
198
                        return false;
8✔
199
                    }
200

201
                    if (!in_array(\Luttje\FilamentUserAttributes\Contracts\UserAttributesConfigContract::class, class_implements($type))) {
8✔
UNCOV
202
                        return false;
×
203
                    }
204

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

212
            $resources = array_merge($resources, $resourcesForPath);
8✔
213
        }
214

215
        return $resources;
8✔
216
    }
217

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

225
        if (!empty($label)) {
32✔
226
            return $parentLabel ? ($parentLabel . ' > ' . $label) : $label;
32✔
227
        }
228

229
        return $parentLabel ?? '';
4✔
230
    }
231

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

239
        foreach ($components as $component) {
4✔
240
            $label = $this->getComponentLabel($component, $parentLabel);
4✔
241

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

249
            if ($component instanceof Component) {
4✔
250
                $namesWithLabels = array_merge(
4✔
251
                    $namesWithLabels,
4✔
252
                    $this->getAllFieldComponents(
4✔
253
                        $component->getChildComponents(),
4✔
254
                        $label
4✔
255
                    )
4✔
256
                );
4✔
257
            }
258
        }
259

260
        return $namesWithLabels;
4✔
261
    }
262

263
    /**
264
     * Gets all columns as a flat array of names with labels
265
     */
266
    public function getAllTableColumns(array $columns): array
4✔
267
    {
268
        $namesWithLabels = [];
4✔
269

270
        foreach ($columns as $column) {
4✔
271
            $label = $this->getComponentLabel($column);
4✔
272
            $namesWithLabels[] = [
4✔
273
                'name' => $column->getName(),
4✔
274
                'label' => $label,
4✔
275
            ];
4✔
276
        }
277

278
        return $namesWithLabels;
4✔
279
    }
280

281
    /**
282
     * Search the components and child components until the component with the given name is found,
283
     * then add the given component after it.
284
     */
285
    public function addFieldBesidesField(array $components, string $siblingComponentName, string $position, Component $componentToAdd, ?string $parentLabel = null): array
16✔
286
    {
287
        $newComponents = [];
16✔
288
        $siblingFound = false;
16✔
289

290
        foreach ($components as $component) {
16✔
291
            $label = $this->getComponentLabel($component, $parentLabel);
16✔
292

293
            $newComponents[] = $component;
16✔
294

295
            if ($component instanceof \Filament\Forms\Components\Field
16✔
296
            && $label === $siblingComponentName) {
16✔
297
                $siblingFound = true;
12✔
298
                if ($position === 'before') {
12✔
299
                    array_splice($newComponents, count($newComponents) - 1, 0, [$componentToAdd]);
8✔
300
                } elseif($position === 'after') {
4✔
301
                    $newComponents[] = $componentToAdd;
4✔
302
                } else {
UNCOV
303
                    throw new \Exception("Invalid position '$position' given.");
×
304
                }
305
            }
306

307
            if ($component instanceof Component) {
16✔
308
                $childComponents = $this->addFieldBesidesField(
16✔
309
                    $component->getChildComponents(),
16✔
310
                    $siblingComponentName,
16✔
311
                    $position,
16✔
312
                    $componentToAdd,
16✔
313
                    $label
16✔
314
                );
16✔
315

316
                $component->childComponents($childComponents);
16✔
317
            }
318
        }
319

320
        if (!$siblingFound) {
16✔
321
            $newComponents[] = $componentToAdd;
16✔
322
        }
323

324
        return $newComponents;
16✔
325
    }
326

327
    /**
328
     * Search the columns and child columns until the column with the given name is found,
329
     * unlike with forms, tables simply have columns in a flat array next to each other.
330
     */
331
    public function addColumnBesidesColumn(array $columns, string $siblingColumnName, string $position, Column $columnToAdd): array
12✔
332
    {
333
        $newColumns = [];
12✔
334

335
        foreach ($columns as $column) {
12✔
336
            $label = $this->getComponentLabel($column);
12✔
337
            $newColumns[] = $column;
12✔
338

339
            if ($label === $siblingColumnName) {
12✔
340
                if ($position === 'before') {
12✔
341
                    array_splice($newColumns, count($newColumns) - 1, 0, [$columnToAdd]);
4✔
342
                } elseif ($position === 'after') {
8✔
343
                    $newColumns[] = $columnToAdd;
8✔
344
                } else {
UNCOV
345
                    throw new \Exception("Invalid position '$position' given.");
×
346
                }
347
            }
348
        }
349

350
        return $newColumns;
12✔
351
    }
352

353
    /**
354
     * Merges the custom fields into the given form schema.
355
     */
356
    public function mergeCustomFormFields(array $fields, string $resource): array
4✔
357
    {
358
        $customFields = collect(FilamentUserAttributes::getUserAttributeFields($resource));
4✔
359

360
        for ($i = 0; $i < $customFields->count(); $i++) {
4✔
361
            $customField = $customFields->pop();
4✔
362

363
            if (!isset($customField['ordering'])
4✔
364
                || $customField['ordering']['sibling'] === null) {
4✔
365
                $customFields->prepend($customField);
4✔
366
                continue;
4✔
367
            }
368

UNCOV
369
            $fields = $this->addFieldBesidesField(
×
UNCOV
370
                $fields,
×
UNCOV
371
                $customField['ordering']['sibling'],
×
UNCOV
372
                $customField['ordering']['position'],
×
UNCOV
373
                $customField['field']
×
UNCOV
374
            );
×
375
        }
376

377
        return array_merge($fields, $customFields->pluck('field')->toArray());
4✔
378
    }
379

380
    /**
381
     * Merges the custom columns into the given table schema.
382
     */
383
    public function mergeCustomTableColumns(array $columns, $resource): array
16✔
384
    {
385
        $customColumns = collect(FilamentUserAttributes::getUserAttributeColumns($resource));
16✔
386

387
        for ($i = 0; $i < $customColumns->count(); $i++) {
16✔
388
            $customColumn = $customColumns->pop();
12✔
389

390
            if (!isset($customColumn['ordering'])
12✔
391
                || $customColumn['ordering']['sibling'] === null) {
12✔
392
                $customColumns->prepend($customColumn);
8✔
393
                continue;
8✔
394
            }
395

396
            $columns = $this->addColumnBesidesColumn(
4✔
397
                $columns,
4✔
398
                $customColumn['ordering']['sibling'],
4✔
399
                $customColumn['ordering']['position'],
4✔
400
                $customColumn['column']
4✔
401
            );
4✔
402
        }
403

404
        return array_merge($columns, $customColumns->pluck('column')->toArray());
16✔
405
    }
406

407
    /**
408
     * Converts a class name to a human readable label by getting
409
     * the last part of the name and adding spaces between words.
410
     */
411
    public function classNameToLabel(string $className): string
8✔
412
    {
413
        $className = class_basename($className);
8✔
414
        $className = preg_replace('/(?<!^)[A-Z]/', ' $0', $className);
8✔
415
        $className = preg_replace('/Resource$/', 'Page', $className);
8✔
416

417
        return $className;
8✔
418
    }
419
}
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