|null */ private ?array $inUse = null; public function __construct( private readonly PermissionChecker $permissions, private readonly CapabilityRegistry $capabilities, ) {} /** * Every ability key this user may be granted, as strings. * * @return list */ public function availableFor(User $user): array { $granted = $this->permissions->grantedKeys($user); return array_values(array_filter( $granted, fn (string $key): bool => $this->isAvailable($key) && $this->isImplemented($key), )); } /** * The same list as Permission cases, for a UI that needs labels and * categories rather than bare keys. * * @return list */ public function casesFor(User $user): array { $available = $this->availableFor($user); return array_values(array_filter( Permission::cases(), fn (Permission $permission): bool => in_array($permission->value, $available, true), )); } /** * Whether the ability is usable in this edition at all, ignoring who is * asking. An unknown key is not available — a token may only ever carry * abilities drawn from the Permission vocabulary. * * Note this does NOT consider whether an endpoint exists: it is the * check EnsureTokenCan makes, and there the question is already settled * — a route asking for an ability is itself the endpoint. */ public function isAvailable(string $key): bool { $permission = Permission::tryFrom($key); if ($permission === null) { return false; } $capability = $permission->capability(); return $capability === null || $this->capabilities->has($capability); } /** * Whether any API endpoint consumes this ability today. */ public function isImplemented(string $key): bool { return in_array($key, $this->inUse(), true); } /** * Abilities named by a `token-can:` middleware on any registered * /api/v1 route — core or module. * * Read off the route table rather than declared in a list, so this can * never disagree with what the endpoints actually require. Route * caching preserves middleware, so it is correct on a cached install * too. * * @return list */ public function inUse(): array { if ($this->inUse !== null) { return $this->inUse; } $abilities = []; foreach (Router::getRoutes()->getRoutes() as $route) { if (! str_starts_with($route->uri(), 'api/')) { continue; } foreach ($route->gatherMiddleware() as $middleware) { if (! is_string($middleware) || ! str_starts_with($middleware, 'token-can:')) { continue; } foreach (explode(',', substr($middleware, strlen('token-can:'))) as $ability) { $ability = trim($ability); if ($ability !== '') { $abilities[$ability] = true; } } } } return $this->inUse = array_keys($abilities); } }