Passed
Push — master ( 1d5b30...945cac )
by John
02:13
created

Generator::buildCharacterPool()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 25
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 16
nop 4
dl 0
loc 25
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PasswordHelper;
6
7
use Exception;
8
use InvalidArgumentException;
9
10
/**
11
 * Generates secure random passwords based on specified requirements.
12
 *
13
 * This class provides methods to generate passwords with various character types
14
 * and ensures they meet minimum security requirements.
15
 *
16
 * @package PasswordHelper
17
 */
18
class Generator
19
{
20
    /**
21
     * Default minimum length for generated passwords.
22
     */
23
    private const DEFAULT_MIN_LENGTH = 12;
24
25
    /**
26
     * Default maximum length for generated passwords.
27
     */
28
    private const DEFAULT_MAX_LENGTH = 20;
29
30
    /**
31
     * Creates a new password generator.
32
     */
33
    public function __construct(
34
        private int $minLength = self::DEFAULT_MIN_LENGTH,
35
        private int $maxLength = self::DEFAULT_MAX_LENGTH
36
    ) {
37
        if ($this->minLength < 8) {
38
            throw new InvalidArgumentException('Minimum length must be at least 8 characters');
39
        }
40
41
        if ($this->maxLength < $this->minLength) {
42
            throw new InvalidArgumentException('Maximum length must be greater than minimum length');
43
        }
44
    }
45
46
    /**
47
     * Gets the minimum length for generated passwords.
48
     *
49
     * @return int The minimum length
50
     */
51
    public function getMinLength(): int
52
    {
53
        return $this->minLength;
54
    }
55
56
    /**
57
     * Gets the maximum length for generated passwords.
58
     *
59
     * @return int The maximum length
60
     */
61
    public function getMaxLength(): int
62
    {
63
        return $this->maxLength;
64
    }
65
66
    /**
67
     * Generates a random password that meets the specified requirements.
68
     *
69
     * @param bool $includeUppercase Whether to include uppercase letters
70
     * @param bool $includeLowercase Whether to include lowercase letters
71
     * @param bool $includeNumbers Whether to include numbers
72
     * @param bool $includeSpecial Whether to include special characters
73
     * @return string The generated password
74
     * @throws InvalidArgumentException If no character types are selected
75
     * @throws Exception
76
     */
77
    public function generate(
78
        bool $includeUppercase = true,
79
        bool $includeLowercase = true,
80
        bool $includeNumbers = true,
81
        bool $includeSpecial = true
82
    ): string {
83
        $this->validateCharacterTypes($includeUppercase, $includeLowercase, $includeNumbers, $includeSpecial);
84
85
        $chars = $this->buildCharacterPool($includeUppercase, $includeLowercase, $includeNumbers, $includeSpecial);
86
        $length = random_int($this->minLength, $this->maxLength);
87
        
88
        $password = $this->generateRequiredCharacters(
89
            $includeUppercase,
90
            $includeLowercase,
91
            $includeNumbers,
92
            $includeSpecial
93
        );
94
95
        $password = $this->fillRemainingCharacters($password, $chars, $length);
96
        
97
        return str_shuffle($password);
98
    }
99
100
    /**
101
     * Validates that at least one character type is selected.
102
     *
103
     * @param bool $includeUppercase Whether to include uppercase letters
104
     * @param bool $includeLowercase Whether to include lowercase letters
105
     * @param bool $includeNumbers Whether to include numbers
106
     * @param bool $includeSpecial Whether to include special characters
107
     * @throws InvalidArgumentException If no character types are selected
108
     */
109
    private function validateCharacterTypes(
110
        bool $includeUppercase,
111
        bool $includeLowercase,
112
        bool $includeNumbers,
113
        bool $includeSpecial
114
    ): void {
115
        if (!$includeUppercase && !$includeLowercase && !$includeNumbers && !$includeSpecial) {
116
            throw new InvalidArgumentException('At least one character type must be selected');
117
        }
118
    }
119
120
    /**
121
     * Builds the character pool based on selected character types.
122
     *
123
     * @param bool $includeUppercase Whether to include uppercase letters
124
     * @param bool $includeLowercase Whether to include lowercase letters
125
     * @param bool $includeNumbers Whether to include numbers
126
     * @param bool $includeSpecial Whether to include special characters
127
     * @return array<int, string> The character pool
128
     */
129
    private function buildCharacterPool(
130
        bool $includeUppercase,
131
        bool $includeLowercase,
132
        bool $includeNumbers,
133
        bool $includeSpecial
134
    ): array {
135
        $chars = [];
136
137
        if ($includeUppercase) {
138
            $chars = array_merge($chars, range('A', 'Z'));
139
        }
140
141
        if ($includeLowercase) {
142
            $chars = array_merge($chars, range('a', 'z'));
143
        }
144
145
        if ($includeNumbers) {
146
            $chars = array_merge($chars, array_map('strval', range(0, 9)));
147
        }
148
149
        if ($includeSpecial) {
150
            $chars = array_merge($chars, str_split('!@#$%^&*()_+-=[]{}|;:,.<>?'));
0 ignored issues
show
Bug introduced by
It seems like str_split('!@#$%^&*()_+-=[]{}|;:,.<>?') can also be of type true; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

150
            $chars = array_merge($chars, /** @scrutinizer ignore-type */ str_split('!@#$%^&*()_+-=[]{}|;:,.<>?'));
Loading history...
151
        }
152
153
        return $chars;
154
    }
155
156
    /**
157
     * Generates the required characters for each selected character type.
158
     *
159
     * @param bool $includeUppercase Whether to include uppercase letters
160
     * @param bool $includeLowercase Whether to include lowercase letters
161
     * @param bool $includeNumbers Whether to include numbers
162
     * @param bool $includeSpecial Whether to include special characters
163
     * @return string The initial password with required characters
164
     * @throws Exception
165
     */
166
    private function generateRequiredCharacters(
167
        bool $includeUppercase,
168
        bool $includeLowercase,
169
        bool $includeNumbers,
170
        bool $includeSpecial
171
    ): string {
172
        $password = '';
173
174
        if ($includeUppercase) {
175
            $password .= $this->getRandomCharacter(range('A', 'Z'));
176
        }
177
178
        if ($includeLowercase) {
179
            $password .= $this->getRandomCharacter(range('a', 'z'));
180
        }
181
182
        if ($includeNumbers) {
183
            $password .= $this->getRandomCharacter(array_map('strval', range(0, 9)));
184
        }
185
186
        if ($includeSpecial) {
187
            $password .= $this->getRandomCharacter(str_split('!@#$%^&*()_+-=[]{}|;:,.<>?'));
0 ignored issues
show
Bug introduced by
It seems like str_split('!@#$%^&*()_+-=[]{}|;:,.<>?') can also be of type true; however, parameter $chars of PasswordHelper\Generator::getRandomCharacter() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

187
            $password .= $this->getRandomCharacter(/** @scrutinizer ignore-type */ str_split('!@#$%^&*()_+-=[]{}|;:,.<>?'));
Loading history...
188
        }
189
190
        return $password;
191
    }
192
193
    /**
194
     * Fills the remaining characters in the password.
195
     *
196
     * @param string $password The current password
197
     * @param array<int, string> $chars The character pool
198
     * @param int $length The desired password length
199
     * @return string The completed password
200
     * @throws Exception
201
     */
202
    private function fillRemainingCharacters(string $password, array $chars, int $length): string
203
    {
204
        while (strlen($password) < $length) {
205
            $password .= $this->getRandomCharacter($chars);
206
        }
207
208
        return $password;
209
    }
210
211
    /**
212
     * Gets a random character from the given array.
213
     *
214
     * @param array<int, string> $chars Array of characters to choose from
215
     * @return string A random character
216
     * @throws Exception
217
     */
218
    private function getRandomCharacter(array $chars): string
219
    {
220
        return (string) $chars[random_int(0, count($chars) - 1)];
221
    }
222
}
223