Completed
Push — development ( 1fb984...7ea9e3 )
by Nils
08:29
created

HumanPasswordGenerator   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 428
Duplicated Lines 10.75 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 46
loc 428
rs 6.5957
c 0
b 0
f 0
wmc 56
lcom 1
cbo 1

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
B generateWordList() 0 33 6
A findWordListLength() 0 14 2
A generateWordListSubset() 0 17 4
C generatePassword() 0 62 12
A randomWord() 0 19 4
A getWordCount() 0 4 1
A setWordCount() 10 10 3
A getMaxWordLength() 0 11 2
A setMaxWordLength() 13 13 3
A getMinWordLength() 0 7 1
A setMinWordLength() 13 13 3
A setWordList() 0 13 3
A getWordList() 0 8 2
A getWordSeparator() 0 4 1
A setWordSeparator() 0 10 2
A getLength() 0 4 1
A setLength() 10 10 3
A getMinPasswordLength() 0 6 1
A getMaxPasswordLength() 0 6 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like HumanPasswordGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HumanPasswordGenerator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PasswordGenerator\Generator;
4
5
use PasswordGenerator\Exception\FileNotFoundException;
6
use PasswordGenerator\Exception\ImpossiblePasswordLengthException;
7
use PasswordGenerator\Exception\NotEnoughWordsException;
8
use PasswordGenerator\Exception\WordsNotFoundException;
9
use PasswordGenerator\Model\Option\Option;
10
11
require_once(dirname(__FILE__).'/../Model/Option/Option.php');
12
13
class HumanPasswordGenerator extends AbstractPasswordGenerator
14
{
15
    const OPTION_WORDS = 'WORDS';
16
    const OPTION_MIN_WORD_LENGTH = 'MIN';
17
    const OPTION_MAX_WORD_LENGTH = 'MAX';
18
    const OPTION_LENGTH = 'LENGTH';
19
20
    const PARAMETER_DICTIONARY_FILE = 'DICTIONARY';
21
    const PARAMETER_WORD_CACHE = 'CACHE';
22
    const PARAMETER_WORD_SEPARATOR = 'SEPARATOR';
23
24
    private $minWordLength;
25
    private $maxWordLength;
26
27
    public function __construct()
28
    {
29
        $this
30
            ->setOption(self::OPTION_LENGTH, array('type' => Option::TYPE_INTEGER, 'default' => null))
31
            ->setOption(self::OPTION_WORDS, array('type' => Option::TYPE_INTEGER, 'default' => 4))
32
            ->setOption(self::OPTION_MIN_WORD_LENGTH, array('type' => Option::TYPE_INTEGER, 'default' => 3))
33
            ->setOption(self::OPTION_MAX_WORD_LENGTH, array('type' => Option::TYPE_INTEGER, 'default' => 20))
34
            ->setParameter(self::PARAMETER_WORD_SEPARATOR, '')
35
        ;
36
    }
37
38
    /**
39
     * Generate word list for us in generating passwords.
40
     *
41
     * @return string[] Words
42
     *
43
     * @throws WordsNotFoundException
44
     */
45
    public function generateWordList()
46
    {
47
        if ($this->getParameter(self::PARAMETER_WORD_CACHE) !== null) {
48
            $this->findWordListLength();
49
50
            return $this->getParameter(self::PARAMETER_WORD_CACHE);
51
        }
52
53
        $words = explode("\n", \file_get_contents($this->getWordList()));
54
55
        $minWordLength = $this->getOptionValue(self::OPTION_MIN_WORD_LENGTH);
56
        $maxWordLength = $this->getOptionValue(self::OPTION_MAX_WORD_LENGTH);
57
58
        foreach ($words as $i => $word) {
59
            $words[$i] = trim($word);
60
            $wordLength = \strlen($word);
61
62
            if ($wordLength > $maxWordLength || $wordLength < $minWordLength) {
63
                unset($words[$i]);
64
            }
65
        }
66
67
        $words = \array_values($words);
68
69
        if (!$words) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $words of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
70
            throw new WordsNotFoundException('No words selected.');
71
        }
72
73
        $this->setParameter(self::PARAMETER_WORD_CACHE, $words);
74
        $this->findWordListLength();
75
76
        return $words;
77
    }
78
79
    private function findWordListLength()
80
    {
81
        $words = $this->getParameter(self::PARAMETER_WORD_CACHE);
82
83
        $this->minWordLength = INF;
84
        $this->maxWordLength = 0;
85
86
        foreach ($words as $word) {
87
            $wordLength = \strlen($word);
88
89
            $this->minWordLength = min($wordLength, $this->minWordLength);
90
            $this->maxWordLength = max($wordLength, $this->maxWordLength);
91
        }
92
    }
93
94
    private function generateWordListSubset($min, $max)
95
    {
96
        $wordList = $this->generateWordList();
97
        $newWordList = array();
98
99
        foreach ($wordList as $word) {
100
            $wordLength = strlen($word);
101
102
            if ($wordLength < $min || $wordLength > $max) {
103
                continue;
104
            }
105
106
            $newWordList[] = $word;
107
        }
108
109
        return $newWordList;
110
    }
111
112
    /**
113
     * Generate one password based on options.
114
     *
115
     * @return string password
116
     *
117
     * @throws WordsNotFoundException
118
     * @throws ImpossiblePasswordLengthException
119
     */
120
    public function generatePassword()
121
    {
122
        $wordList = $this->generateWordList();
123
124
        $words = \count($wordList);
125
126
        if (!$words) {
127
            throw new WordsNotFoundException('No words selected.');
128
        }
129
130
        $password = '';
131
        $wordCount = $this->getWordCount();
132
133
        if (
134
            $this->getLength() > 0 &&
135
            (
136
                $this->getMinPasswordLength() > $this->getLength()
137
                ||
138
                $this->getMaxPasswordLength() < $this->getLength()
139
            )
140
        ) {
141
            throw new ImpossiblePasswordLengthException();
142
        }
143
144
        if (!$this->getLength()) {
145
            for ($i = 0; $i < $wordCount; $i++) {
146
                if ($i) {
147
                    $password .= $this->getWordSeparator();
148
                }
149
150
                $password .= $this->randomWord();
151
            }
152
153
            return $password;
154
        }
155
156
        while(--$wordCount) {
157
            $thisMin = $this->getLength() - strlen($password) - ($wordCount * $this->getMaxWordLength()) - (strlen($this->getWordSeparator()) * $wordCount);
158
            $thisMax = $this->getLength() - strlen($password) - ($wordCount * $this->getMinWordLength()) - (strlen($this->getWordSeparator()) * $wordCount);
159
160
            if ($thisMin < 1) {
161
                $thisMin = $this->getMinWordLength();
162
            }
163
164
            if ($thisMax > $this->getMaxWordLength()) {
165
                $thisMax = $this->getMaxWordLength();
166
            }
167
168
            $length = $this->randomInteger($thisMin, $thisMax);
169
170
            $password .= $this->randomWord($length, $length);
171
172
            if ($wordCount) {
173
                $password .= $this->getWordSeparator();
174
            }
175
        }
176
177
        $desiredLength = $this->getLength() - strlen($password);
178
        $password .= $this->randomWord($desiredLength, $desiredLength);
179
180
        return $password;
181
    }
182
183
    /**
184
     * @param null|int $minLength
185
     * @param null|int $maxLength
186
     *
187
     * @return string
188
     *
189
     * @throws NotEnoughWordsException
190
     */
191
    public function randomWord($minLength = null, $maxLength = null)
192
    {
193
        if (is_null($minLength)) {
194
            $minLength = $this->getMinWordLength();
195
        }
196
197
        if (is_null($maxLength)) {
198
            $maxLength = $this->getMaxWordLength();
199
        }
200
201
        $wordList = $this->generateWordListSubset($minLength, $maxLength);
202
        $words = \count($wordList);
203
204
        if (!$words) {
205
            throw new NotEnoughWordsException(sprintf('No words with a length between %d and %d', $minLength, $maxLength));
206
        }
207
208
        return $wordList[$this->randomInteger(0, $words - 1)];
209
    }
210
211
    /**
212
     * Get number of words in desired password.
213
     *
214
     * @return int
215
     */
216
    public function getWordCount()
217
    {
218
        return $this->getOptionValue(self::OPTION_WORDS);
219
    }
220
221
    /**
222
     * Set number of words in desired password(s).
223
     *
224
     * @param int $characterCount
225
     *
226
     * @return $this
227
     *
228
     * @throws \InvalidArgumentException
229
     */
230 View Code Duplication
    public function setWordCount($characterCount)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231
    {
232
        if (!is_int($characterCount) || $characterCount < 1) {
233
            throw new \InvalidArgumentException('Expected positive integer');
234
        }
235
236
        $this->setOptionValue(self::OPTION_WORDS, $characterCount);
237
238
        return $this;
239
    }
240
241
    /**
242
     * get max word length.
243
     *
244
     * @return int
245
     */
246
    public function getMaxWordLength()
247
    {
248
        if (is_null($this->maxWordLength)) {
249
            return $this->getOptionValue(self::OPTION_MAX_WORD_LENGTH);
250
        }
251
252
        return min(
253
            $this->maxWordLength,
254
            $this->getOptionValue(self::OPTION_MAX_WORD_LENGTH)
255
        );
256
    }
257
258
    /**
259
     * set max word length.
260
     *
261
     * @param int $length
262
     *
263
     * @return $this
264
     *
265
     * @throws \InvalidArgumentException
266
     */
267 View Code Duplication
    public function setMaxWordLength($length)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
268
    {
269
        if (!is_int($length) || $length < 1) {
270
            throw new \InvalidArgumentException('Expected positive integer');
271
        }
272
273
        $this->setOptionValue(self::OPTION_MAX_WORD_LENGTH, $length);
274
        $this->setParameter(self::PARAMETER_WORD_CACHE, null);
275
        $this->minWordLength = null;
276
        $this->maxWordLength = null;
277
278
        return $this;
279
    }
280
281
    /**
282
     * get min word length.
283
     *
284
     * @return int
285
     */
286
    public function getMinWordLength()
287
    {
288
        return max(
289
            $this->minWordLength,
290
            $this->getOptionValue(self::OPTION_MIN_WORD_LENGTH)
291
        );
292
    }
293
294
    /**
295
     * set min word length.
296
     *
297
     * @param int $length
298
     *
299
     * @return $this
300
     *
301
     * @throws \InvalidArgumentException
302
     */
303 View Code Duplication
    public function setMinWordLength($length)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
304
    {
305
        if (!is_int($length) || $length < 1) {
306
            throw new \InvalidArgumentException('Expected positive integer');
307
        }
308
309
        $this->setOptionValue(self::OPTION_MIN_WORD_LENGTH, $length);
310
        $this->setParameter(self::PARAMETER_WORD_CACHE, null);
311
        $this->minWordLength = null;
312
        $this->maxWordLength = null;
313
314
        return $this;
315
    }
316
317
    /**
318
     * Set word list.
319
     *
320
     * @param string $filename
321
     *
322
     * @return $this
323
     *
324
     * @throws \InvalidArgumentException
325
     * @throws FileNotFoundException
326
     */
327
    public function setWordList($filename)
328
    {
329
        if (!is_string($filename)) {
330
            throw new \InvalidArgumentException('Expected string');
331
        } elseif (!file_exists($filename)) {
332
            throw new FileNotFoundException('File not found');
333
        }
334
335
        $this->setParameter(self::PARAMETER_DICTIONARY_FILE, $filename);
336
        $this->setParameter(self::PARAMETER_WORD_CACHE, null);
337
338
        return $this;
339
    }
340
341
    /**
342
     * Get word list filename.
343
     *
344
     * @throws FileNotFoundException
345
     *
346
     * @return string
347
     */
348
    public function getWordList()
349
    {
350
        if (!file_exists($this->getParameter(self::PARAMETER_DICTIONARY_FILE))) {
351
            throw new FileNotFoundException();
352
        }
353
354
        return $this->getParameter(self::PARAMETER_DICTIONARY_FILE);
355
    }
356
357
    /**
358
     * Get word separator.
359
     *
360
     * @return string
361
     */
362
    public function getWordSeparator()
363
    {
364
        return $this->getParameter(self::PARAMETER_WORD_SEPARATOR);
365
    }
366
367
    /**
368
     * Set word separator.
369
     *
370
     * @param string $separator
371
     *
372
     * @return $this
373
     *
374
     * @throws \InvalidArgumentException
375
     */
376
    public function setWordSeparator($separator)
377
    {
378
        if (!is_string($separator)) {
379
            throw new \InvalidArgumentException('Expected string');
380
        }
381
382
        $this->setParameter(self::PARAMETER_WORD_SEPARATOR, $separator);
383
384
        return $this;
385
    }
386
387
    /**
388
     * Password length
389
     *
390
     * @return integer
391
     */
392
    public function getLength()
393
    {
394
        return $this->getOptionValue(self::OPTION_LENGTH);
395
    }
396
397
    /**
398
     * Set length of desired password(s)
399
     *
400
     * @param integer $characterCount
401
     *
402
     * @return $this
403
     *
404
     * @throws \InvalidArgumentException
405
     */
406 View Code Duplication
    public function setLength($characterCount)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
407
    {
408
        if (!is_int($characterCount) || $characterCount < 1) {
409
            throw new \InvalidArgumentException('Expected positive integer');
410
        }
411
412
        $this->setOptionValue(self::OPTION_LENGTH, $characterCount);
413
414
        return $this;
415
    }
416
417
    /**
418
     * Calculate how long the password would be using minimum word length
419
     *
420
     * @return int
421
     */
422
    public function getMinPasswordLength()
423
    {
424
        $wordCount = $this->getWordCount();
425
426
        return ($this->getMinWordLength() * $wordCount) + (strlen($this->getWordSeparator()) * ($wordCount - 1));
427
    }
428
429
    /**
430
     * Calculate how long the password would be using maximum word length
431
     *
432
     * @return int
433
     */
434
    public function getMaxPasswordLength()
435
    {
436
        $wordCount = $this->getWordCount();
437
438
        return ($this->getMaxWordLength() * $wordCount) + (strlen($this->getWordSeparator()) * ($wordCount - 1));
439
    }
440
}
441