Levels::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jasny\Auth\Authz;
6
7
use DomainException;
8
use Improved as i;
9
use Improved\IteratorPipeline\Pipeline;
10
use Jasny\Auth\AuthzInterface;
11
use Jasny\Auth\AuthzInterface as Authz;
12
use Jasny\Auth\ContextInterface as Context;
13
use Jasny\Auth\User\PartiallyLoggedIn;
14
use Jasny\Auth\UserInterface as User;
15
use UnexpectedValueException;
16
17
/**
18
 * Authorize by access level.
19
 * @immutable
20
 *
21
 * <code>
22
 *   $authz = new Authz\Levels([
23
 *     'user' => 1,
24
 *     'moderator' => 100,
25
 *     'admin' => 1000
26
 *   ]);
27
 *
28
 *   $auth = new Auth($authz);
29
 * </code>
30
 *
31
 * Levels should be positive integers (greater than 0).
32
 */
33
class Levels implements AuthzInterface
34
{
35
    use StateTrait;
36
37
    /** @var array<string,int> */
38
    protected array $levels;
39
40
    /**
41
     * Cached user level. Service has an immutable state.
42
     */
43
    protected int $userLevel = 0;
44
45
    /**
46
     * AuthzByLevel constructor.
47
     *
48
     * @param array<string,int> $levels
49
     */
50 20
    public function __construct(array $levels)
51
    {
52 20
        $this->levels = $levels;
53
    }
54
55
56
    /**
57
     * Get a copy of the service with a modified property and recalculated
58
     * Returns $this if authz hasn't changed.
59
     */
60 16
    protected function withProperty(string $property, mixed $value): static
61
    {
62 16
        $clone = clone $this;
63 16
        $clone->{$property} = $value;
64
65 16
        $clone->calcUserLevel();
66
67 14
        $isSame = $clone->{$property} === $this->{$property} && $clone->userLevel === $this->userLevel;
68
69 14
        return $isSame ? $this : $clone;
70
    }
71
72
    /**
73
     * Get all available authorization roles (for the current context)
74
     *
75
     * @return string[]
76
     */
77 1
    public function getAvailableRoles(): array
78
    {
79 1
        return array_keys($this->levels);
80
    }
81
82
83
    /**
84
     * Check if the current user is logged in and has specified role.
85
     */
86 10
    public function is(string $role): bool
87
    {
88 10
        if (!isset($this->levels[$role])) {
89 1
            trigger_error("Unknown authz role '$role'", E_USER_WARNING); // Catch typos
90 1
            return false;
91
        }
92
93 9
        return $this->userLevel >= $this->levels[$role];
94
    }
95
96
97
    /**
98
     * Get a copy, recalculating the authz level of the user.
99
     * Returns $this if authz hasn't changed.
100
     */
101 3
    public function recalc(): static
102
    {
103 3
        $clone = clone $this;
104 3
        $clone->calcUserLevel();
105
106 3
        return $clone->userLevel === $this->userLevel ? $this : $clone;
107
    }
108
109
    /**
110
     * Get access level of the current user.
111
     *
112
     * @throws DomainException for unknown level names
113
     */
114 16
    private function calcUserLevel(): void
115
    {
116 16
        if ($this->user === null || $this->user instanceof PartiallyLoggedIn) {
117 4
            $this->userLevel = 0;
118 4
            return;
119
        }
120
121 12
        $uid = $this->user->getAuthId();
122
123 12
        $role = i\type_check(
124 12
            $this->user->getAuthRole($this->context),
125 12
            ['int', 'string'],
126 12
            new UnexpectedValueException("For authz levels the role should be string|int, %s returned (uid:$uid)")
127 12
        );
128
129 11
        if (is_string($role) && !isset($this->levels[$role])) {
130 1
            throw new DomainException("Authorization level '$role' isn't defined (uid:$uid)");
131
        }
132
133 10
        $this->userLevel = is_string($role) ? $this->levels[$role] : (int)$role;
134
    }
135
}
136