Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
RoleService
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
6 / 6
11
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 all
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 present
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 summary
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 update
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 findByName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Services\Permission;
4
5use Illuminate\Support\Collection;
6use Spatie\Permission\Models\Role;
7use Spatie\Permission\PermissionRegistrar;
8
9/**
10 * The Roles page: show each preset role, what it can do in plain language,
11 * and let the operator tick / untick permission groups. No jargon.
12 */
13class RoleService
14{
15    public function __construct(private PermissionService $permissions) {}
16
17    /** Roles that must never be edited or deleted from the UI. */
18    public const LOCKED = ['Owner'];
19
20    /** @return Collection<int,array> each: name, description, staff_count, locked, abilities, summary */
21    public function all(): Collection
22    {
23        return Role::query()
24            ->with('permissions:id,name')
25            ->withCount('users')
26            ->orderByRaw("FIELD(name, 'Owner') DESC")
27            ->orderBy('name')
28            ->get()
29            ->map(fn (Role $role) => $this->present($role));
30    }
31
32    public function present(Role $role): array
33    {
34        $abilities = $role->permissions->pluck('name')->all();
35
36        return [
37            'name' => $role->name,
38            'description' => config("roles.{$role->name}.description", ''),
39            'staff_count' => $role->users_count ?? $role->users()->count(),
40            'locked' => in_array($role->name, self::LOCKED, true),
41            'abilities' => $abilities,
42            'summary' => $this->summary($abilities),
43        ];
44    }
45
46    /** Plain-language labels for a set of ability keys, in group order. */
47    public function summary(array $abilities): array
48    {
49        $labels = [];
50        foreach ($this->permissions->groups() as $group => $defs) {
51            foreach ($defs as $key => $def) {
52                if (in_array($key, $abilities, true)) {
53                    $labels[$group][] = $def['label'];
54                }
55            }
56        }
57
58        return $labels;
59    }
60
61    /**
62     * Replace a role's abilities. Owner is left untouched (it always has
63     * everything, plus a Gate::before bypass).
64     */
65    public function update(Role $role, array $abilities): void
66    {
67        if (in_array($role->name, self::LOCKED, true)) {
68            return;
69        }
70
71        $valid = array_values(array_intersect($abilities, $this->permissions->abilities()));
72        $before = $role->permissions->pluck('name')->sort()->values()->all();
73
74        $role->syncPermissions($valid);
75
76        app(PermissionRegistrar::class)->forgetCachedPermissions();
77
78        if ($before !== collect($valid)->sort()->values()->all()) {
79            activity('role')
80                ->performedOn($role)
81                ->withProperties(['old' => ['abilities' => $before], 'attributes' => ['abilities' => array_values($valid)]])
82                ->log("Updated permissions for the “{$role->name}” role");
83        }
84    }
85
86    public function findByName(string $name): Role
87    {
88        return Role::query()->where('name', $name)->firstOrFail();
89    }
90}