LowerCaseRule::getMessage()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 21
c 1
b 0
f 0
nc 5
nop 3
dl 0
loc 35
ccs 19
cts 19
cp 1
crap 5
rs 9.2728
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\Rule;
6
7
use StableSort\StableSort;
8
use Stadly\PasswordPolice\Constraint\CountConstraint;
9
use Stadly\PasswordPolice\Password;
10
use Stadly\PasswordPolice\Rule;
11
use Stadly\PasswordPolice\ValidationError;
12
use Symfony\Contracts\Translation\LocaleAwareInterface;
13
use Symfony\Contracts\Translation\TranslatorInterface;
14
15
final class LowerCaseRule implements Rule
16
{
17
    /**
18
     * @var array<CountConstraint> Rule constraints.
19
     */
20
    private $constraints = [];
21
22
    /**
23
     * @param int $min Minimum number of lower case letters.
24
     * @param int|null $max Maximum number of lower case letters.
25
     * @param int $weight Constraint weight.
26
     */
27 16
    public function __construct(int $min = 1, ?int $max = null, int $weight = 1)
28
    {
29 16
        $this->addConstraint($min, $max, $weight);
30 14
    }
31
32
    /**
33
     * @param int $min Minimum number of lower case letters.
34
     * @param int|null $max Maximum number of lower case letters.
35
     * @param int $weight Constraint weight.
36
     * @return $this
37
     */
38 1
    public function addConstraint(int $min = 1, ?int $max = null, int $weight = 1): self
39
    {
40 1
        $this->constraints[] = new CountConstraint($min, $max, $weight);
41
42
        StableSort::usort($this->constraints, static function (CountConstraint $a, CountConstraint $b): int {
43 1
            return $b->getWeight() <=> $a->getWeight();
44 1
        });
45
46 1
        return $this;
47
    }
48
49
    /**
50
     * Check whether a password is in compliance with the rule.
51
     *
52
     * @param Password|string $password Password to check.
53
     * @param int|null $weight Don't consider constraints with lower weights.
54
     * @return bool Whether the password is in compliance with the rule.
55
     */
56 7
    public function test($password, ?int $weight = null): bool
57
    {
58 7
        $count = $this->getCount((string)$password);
59 7
        $constraint = $this->getViolation($count, $weight);
60
61 7
        return $constraint === null;
62
    }
63
64
    /**
65
     * Validate that a password is in compliance with the rule.
66
     *
67
     * @param Password|string $password Password to validate.
68
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
69
     * @return ValidationError|null Validation error describing why the password is not in compliance with the rule.
70
     */
71 6
    public function validate($password, TranslatorInterface $translator): ?ValidationError
72
    {
73 6
        $count = $this->getCount((string)$password);
74 6
        $constraint = $this->getViolation($count);
75
76 6
        if ($constraint !== null) {
77 5
            return new ValidationError(
78 5
                $this->getMessage($constraint, $count, $translator),
79 5
                $password,
80 5
                $this,
81 5
                $constraint->getWeight()
82
            );
83
        }
84
85 1
        return null;
86
    }
87
88
    /**
89
     * @param int $count Number of lower case characters.
90
     * @param int|null $weight Don't consider constraints with lower weights.
91
     * @return CountConstraint|null Constraint violated by the count.
92
     */
93 13
    private function getViolation(int $count, ?int $weight = null): ?CountConstraint
94
    {
95 13
        foreach ($this->constraints as $constraint) {
96 13
            if ($weight !== null && $constraint->getWeight() < $weight) {
97 1
                continue;
98
            }
99 12
            if (!$constraint->test($count)) {
100 12
                return $constraint;
101
            }
102
        }
103
104 5
        return null;
105
    }
106
107
    /**
108
     * @param string $password Password to count characters in.
109
     * @return int Number of lower case characters.
110
     */
111 13
    private function getCount(string $password): int
112
    {
113 13
        $upperCase = mb_strtoupper($password);
114
115 13
        $passwordCharacters = $this->splitString($password);
116 13
        $upperCaseCharacters = $this->splitString($upperCase);
117 13
        assert(count($passwordCharacters) === count($upperCaseCharacters));
118
119 13
        $count = 0;
120 13
        for ($i = count($passwordCharacters) - 1; $i >= 0; --$i) {
121 13
            if ($passwordCharacters[$i] !== $upperCaseCharacters[$i]) {
122 11
                ++$count;
123
            }
124
        }
125
126 13
        return $count;
127
    }
128
129
    /**
130
     * @param string $string String to split into individual characters.
131
     * @return array<string> Array of characters.
132
     */
133 13
    private function splitString(string $string): array
134
    {
135 13
        $characters = preg_split('{}u', $string, -1, PREG_SPLIT_NO_EMPTY);
136 13
        assert($characters !== false);
137
138 13
        return $characters;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $characters returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
139
    }
140
141
    /**
142
     * @param CountConstraint $constraint Constraint that is violated.
143
     * @param int $count Count that violates the constraint.
144
     * @param TranslatorInterface&LocaleAwareInterface $translator Translator for translating messages.
145
     * @return string Message explaining the violation.
146
     */
147 5
    private function getMessage(CountConstraint $constraint, int $count, TranslatorInterface $translator): string
0 ignored issues
show
Unused Code introduced by
The parameter $count is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

147
    private function getMessage(CountConstraint $constraint, /** @scrutinizer ignore-unused */ int $count, TranslatorInterface $translator): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
148
    {
149 5
        if ($constraint->getMax() === null) {
150 1
            return $translator->trans(
151
                'The password must contain at least one lower case letter.|' .
152 1
                'The password must contain at least %count% lower case letters.',
153 1
                ['%count%' => $constraint->getMin()]
154
            );
155
        }
156
157 4
        if ($constraint->getMax() === 0) {
158 1
            return $translator->trans(
159 1
                'The password cannot contain lower case letters.'
160
            );
161
        }
162
163 3
        if ($constraint->getMin() === 0) {
164 1
            return $translator->trans(
165
                'The password must contain at most one lower case letter.|' .
166 1
                'The password must contain at most %count% lower case letters.',
167 1
                ['%count%' => $constraint->getMax()]
168
            );
169
        }
170
171 2
        if ($constraint->getMin() === $constraint->getMax()) {
172 1
            return $translator->trans(
173
                'The password must contain exactly one lower case letter.|' .
174 1
                'The password must contain exactly %count% lower case letters.',
175 1
                ['%count%' => $constraint->getMin()]
176
            );
177
        }
178
179 1
        return $translator->trans(
180 1
            'The password must contain between %min% and %max% lower case letters.',
181 1
            ['%min%' => $constraint->getMin(), '%max%' => $constraint->getMax()]
182
        );
183
    }
184
}
185