Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Types
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
7 / 7
10
100.00% covered (success)
100.00%
1 / 1
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 handles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 class
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 label
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 roles
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 handleOf
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\User;
6
7use Cosray\Exception\RuntimeException;
8use Cosray\Schema\Handle;
9use Cosray\Schema\Label;
10use Cosray\Schema\Roles;
11use Cosray\Security\Policy;
12use Cosray\User;
13use ReflectionClass;
14
15final class Types
16{
17    public const string DEFAULT = 'user';
18
19    /** @var array<string, class-string<User>> */
20    private array $classes = [self::DEFAULT => User::class];
21
22    /**
23     * A class whose handle is already taken replaces the earlier one, which
24     * is how an app gives the default user type fields of its own.
25     *
26     * @param class-string<User> $class
27     */
28    public function add(string $class): void
29    {
30        if (!is_a($class, User::class, true)) {
31            throw new RuntimeException('User types must extend ' . User::class . ": {$class}");
32        }
33
34        $this->classes[self::handleOf($class)] = $class;
35    }
36
37    /** @return list<string> */
38    public function handles(): array
39    {
40        return array_keys($this->classes);
41    }
42
43    public function has(string $handle): bool
44    {
45        return isset($this->classes[$handle]);
46    }
47
48    /**
49     * A stored type the app no longer registers falls back to the plain
50     * user, so the account stays usable and editable.
51     *
52     * @return class-string<User>
53     */
54    public function class(string $handle): string
55    {
56        return $this->classes[$handle] ?? User::class;
57    }
58
59    public function label(string $handle): string
60    {
61        $class = $this->class($handle);
62        $label = new ReflectionClass($class)->getAttributes(Label::class)[0] ?? null;
63
64        return $label?->newInstance()->label ?? new ReflectionClass($class)->getShortName();
65    }
66
67    /** @return array<string, string> role name â†’ label */
68    public function roles(string $handle, Policy $policy): array
69    {
70        $roles = $policy->roles();
71        $limit = new ReflectionClass($this->class($handle))->getAttributes(Roles::class)[0] ?? null;
72
73        if ($limit === null) {
74            return $roles;
75        }
76
77        return array_intersect_key($roles, array_flip($limit->newInstance()->roles));
78    }
79
80    /** @param class-string<User> $class */
81    private static function handleOf(string $class): string
82    {
83        $reflection = new ReflectionClass($class);
84        $handle = $reflection->getAttributes(Handle::class)[0] ?? null;
85
86        if ($handle !== null) {
87            return $handle->newInstance()->value;
88        }
89
90        return ltrim(strtolower((string) preg_replace('/[A-Z]/', '-$0', $reflection->getShortName())), '-');
91    }
92}