Passed
Push — master ( d894f8...dfe6be )
by Magnar Ovedal
03:04
created

Pspell   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 132
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 43
dl 0
loc 132
ccs 51
cts 51
cp 1
rs 10
c 0
b 0
f 0
wmc 16

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A fromLocale() 0 22 3
A contains() 0 22 4
A getFormattedWords() 0 7 3
A getUniqueWords() 0 10 3
A getWordsToCheck() 0 3 1
A errorHandler() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\WordList;
6
7
use ErrorException;
8
use InvalidArgumentException;
9
use RuntimeException;
10
use Stadly\PasswordPolice\WordFormatter;
11
use Stadly\PasswordPolice\WordList;
12
use Traversable;
13
14
final class Pspell implements WordList
15
{
16
    /**
17
     * @var int Pspell dictionary.
18
     */
19
    private $pspell;
20
21
    /**
22
     * @var WordFormatter[] Word formatters.
23
     */
24
    private $wordFormatters;
25
26
    /**
27
     * Pspell dictionaries are case-sensitive.
28
     * Specify word formatters if tests should also be performed for the word formatted to other cases.
29
     *
30
     * @param int $pspell Pspell dictionary link, as generated by `pspell_new` and friends.
31
     * @param WordFormatter ...$wordFormatters Word formatters.
32
     */
33 2
    public function __construct(int $pspell, WordFormatter ...$wordFormatters)
34
    {
35 2
        $this->pspell = $pspell;
36 2
        $this->wordFormatters = $wordFormatters;
37 2
    }
38
39
    /**
40
     * Pspell dictionaries are case-sensitive.
41
     * Specify word formatters if tests should also be performed for the word formatted to other cases.
42
     *
43
     * @param string $locale Locale of the pspell dictionary to load. For example `en-US` or `de`.
44
     * @param WordFormatter ...$wordFormatters Word formatters.
45
     * @throws RuntimeException If the pspell dictionary could not be loaded.
46
     * @return self Pspell word list.
47
     */
48 4
    public static function fromLocale(string $locale, WordFormatter ...$wordFormatters): self
49
    {
50 4
        if (preg_match('{^[a-z]{2}(?:[-_][A-Z]{2})?$}', $locale) !== 1) {
51 2
            throw new InvalidArgumentException(sprintf('%s is not a valid locale.', $locale));
52
        }
53
54 2
        set_error_handler([self::class, 'errorHandler']);
55
        try {
56 2
            $pspell = pspell_new($locale);
57 1
        } catch (ErrorException $exception) {
58 1
            throw new RuntimeException(
59 1
                'An error occurred while loading the word list: '.$exception->getMessage(),
60 1
                /*code*/0,
61 1
                $exception
62
            );
63 1
        } finally {
64 2
            restore_error_handler();
65
        }
66
67 1
        assert($pspell !== false);
68
69 1
        return new self($pspell, ...$wordFormatters);
70
    }
71
72
    /**
73
     * {@inheritDoc}
74
     */
75 7
    public function contains(string $word): bool
76
    {
77 7
        foreach ($this->getWordsToCheck($word) as $wordVariant) {
78 7
            set_error_handler([self::class, 'errorHandler']);
79
            try {
80 7
                $check = pspell_check($this->pspell, $wordVariant);
81 2
            } catch (ErrorException $exception) {
82 2
                throw new RuntimeException(
83 2
                    'An error occurred while using the word list: '.$exception->getMessage(),
84 2
                    /*code*/0,
85 2
                    $exception
86
                );
87 5
            } finally {
88 7
                restore_error_handler();
89
            }
90
91 5
            if ($check) {
92 5
                return true;
93
            }
94
        }
95
96 3
        return false;
97
    }
98
99
    /**
100
     * @param string $word Word to check.
101
     * @return Traversable<string> Variants of the word to check.
102
     */
103 7
    private function getWordsToCheck(string $word): Traversable
104
    {
105 7
        yield from $this->getUniqueWords($this->getFormattedWords($word));
106 3
    }
107
108
    /**
109
     * @param iterable<string> $words Words to filter.
110
     * @return Traversable<string> Unique words.
111
     */
112 7
    private function getUniqueWords(iterable $words): Traversable
113
    {
114 7
        $checked = [];
115 7
        foreach ($words as $word) {
116 7
            if (isset($checked[$word])) {
117 2
                continue;
118
            }
119
120 7
            $checked[$word] = true;
121 7
            yield $word;
122
        }
123 3
    }
124
125
    /**
126
     * @param string $word Word to format.
127
     * @return Traversable<string> Formatted words. May contain duplicates.
128
     */
129 7
    private function getFormattedWords(string $word): Traversable
130
    {
131 7
        yield $word;
132
133 3
        foreach ($this->wordFormatters as $wordFormatter) {
134 2
            foreach ($wordFormatter->apply([$word]) as $formatted) {
135 2
                yield $formatted;
136
            }
137
        }
138 3
    }
139
140
    /**
141
     * @throws ErrorException Error converted to an exception.
142
     */
143 4
    private static function errorHandler(int $severity, string $message, string $filename, int $line): void
144
    {
145 4
        throw new ErrorException($message, /*code*/0, $severity, $filename, $line);
146
    }
147
}
148