Completed
Push — master ( 323b38...f8d35a )
by Magnar Ovedal
03:57
created

CharacterClass::validate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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