Passed
Push — master ( 50d73a...8cc9a6 )
by Magnar Ovedal
03:19 queued 41s
created

CharacterClassRule::addConstraint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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