CharacterClassRule   A
last analyzed

Complexity

Total Complexity 13

Size/Duplication

Total Lines 135
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 38
c 1
b 0
f 0
dl 0
loc 135
ccs 39
cts 39
cp 1
rs 10
wmc 13

7 Methods

Rating   Name   Duplication   Size   Complexity  
A validate() 0 15 2
A addConstraint() 0 9 1
A getCharacters() 0 3 1
A test() 0 6 1
A __construct() 0 8 2
A getCount() 0 7 1
A getViolation() 0 12 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\Rule;
6
7
use InvalidArgumentException;
8
use StableSort\StableSort;
9
use Stadly\PasswordPolice\Constraint\CountConstraint;
10
use Stadly\PasswordPolice\Password;
11
use Stadly\PasswordPolice\Rule;
12
use Stadly\PasswordPolice\ValidationError;
13
use Symfony\Contracts\Translation\LocaleAwareInterface;
14
use Symfony\Contracts\Translation\TranslatorInterface;
15
16
abstract class CharacterClassRule implements Rule
17
{
18
    /**
19
     * @var string Characters matched by the rule.
20
     */
21
    protected $characters;
22
23
    /**
24
     * @var array<CountConstraint> Rule constraints.
25
     */
26
    private $constraints = [];
27
28
    /**
29
     * @param string $characters Characters matched by the rule.
30
     * @param int $min Minimum number of characters matching the rule.
31
     * @param int|null $max Maximum number of characters matching the rule.
32
     * @param int $weight Constraint weight.
33
     */
34 28
    public function __construct(string $characters, int $min = 1, ?int $max = null, int $weight = 1)
35
    {
36 28
        if ($characters === '') {
37 2
            throw new InvalidArgumentException('At least one character must be specified.');
38
        }
39
40 26
        $this->characters = $characters;
41 26
        $this->addConstraint($min, $max, $weight);
42 22
    }
43
44
    /**
45
     * @param int $min Minimum number of characters matching the rule.
46
     * @param int|null $max Maximum number of characters matching the rule.
47
     * @param int $weight Constraint weight.
48
     * @return $this
49
     */
50 3
    public function addConstraint(int $min = 1, ?int $max = null, int $weight = 1): self
51
    {
52 3
        $this->constraints[] = new CountConstraint($min, $max, $weight);
53
54
        StableSort::usort($this->constraints, static function (CountConstraint $a, CountConstraint $b): int {
55 3
            return $b->getWeight() <=> $a->getWeight();
56 3
        });
57
58 3
        return $this;
59
    }
60
61
    /**
62
     * @return string Characters matched by the rule.
63
     */
64 2
    public function getCharacters(): string
65
    {
66 2
        return $this->characters;
67
    }
68
69
    /**
70
     * Check whether a password is in compliance with the rule.
71
     *
72
     * @param Password|string $password Password to check.
73
     * @param int|null $weight Don't consider constraints with lower weights.
74
     * @return bool Whether the password is in compliance with the rule.
75
     */
76 15
    public function test($password, ?int $weight = null): bool
77
    {
78 15
        $count = $this->getCount((string)$password);
79 15
        $constraint = $this->getViolation($count, $weight);
80
81 15
        return $constraint === null;
82
    }
83
84
    /**
85
     * Validate that a password is in compliance with the rule.
86
     *
87
     * @param Password|string $password Password to validate.
88
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
89
     * @return ValidationError|null Validation error describing why the password is not in compliance with the rule.
90
     */
91 14
    public function validate($password, TranslatorInterface $translator): ?ValidationError
92
    {
93 14
        $count = $this->getCount((string)$password);
94 14
        $constraint = $this->getViolation($count);
95
96 14
        if ($constraint !== null) {
97 11
            return new ValidationError(
98 11
                $this->getMessage($constraint, $count, $translator),
99 11
                $password,
100 11
                $this,
101 11
                $constraint->getWeight()
102
            );
103
        }
104
105 3
        return null;
106
    }
107
108
    /**
109
     * @param int $count Number of characters matching the rule.
110
     * @param int|null $weight Don't consider constraints with lower weights.
111
     * @return CountConstraint|null Constraint violated by the count.
112
     */
113 29
    private function getViolation(int $count, ?int $weight = null): ?CountConstraint
114
    {
115 29
        foreach ($this->constraints as $constraint) {
116 29
            if ($weight !== null && $constraint->getWeight() < $weight) {
117 3
                continue;
118
            }
119 26
            if (!$constraint->test($count)) {
120 26
                return $constraint;
121
            }
122
        }
123
124 12
        return null;
125
    }
126
127
    /**
128
     * @param string $password Password to count characters in.
129
     * @return int Number of characters matching the rule.
130
     */
131 29
    private function getCount(string $password): int
132
    {
133 29
        $escapedCharacters = preg_quote($this->characters);
134 29
        $count = preg_match_all('{[' . $escapedCharacters . ']}u', $password);
135 29
        assert($count !== false);
136
137 29
        return $count;
138
    }
139
140
    /**
141
     * @param CountConstraint $constraint Constraint that is violated.
142
     * @param int $count Count that violates the constraint.
143
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
144
     * @return string Message explaining the violation.
145
     */
146
    abstract protected function getMessage(
147
        CountConstraint $constraint,
148
        int $count,
149
        TranslatorInterface $translator
150
    ): string;
151
}
152