Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
27 / 27 |
|
100.00% |
12 / 12 |
CRAP | |
100.00% |
1 / 1 |
Regex | |
100.00% |
27 / 27 |
|
100.00% |
12 / 12 |
22 | |
100.00% |
1 / 1 |
setPattern | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getPattern | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isCaseSensitive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCaseSensitive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLabel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
setLabel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMatch | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getMatches | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validatePattern | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
validateDelimiter | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
4 | |||
escapeString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
match | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | /** |
4 | * @package pvcRegex |
5 | * @author Doug Wilbourne (dougwilbourne@gmail.com) |
6 | */ |
7 | |
8 | declare(strict_types=1); |
9 | |
10 | namespace pvc\regex; |
11 | |
12 | use pvc\interfaces\regex\RegexInterface; |
13 | use pvc\regex\err\RegexBadPatternException; |
14 | use pvc\regex\err\RegexInvalidDelimiterException; |
15 | use pvc\regex\err\RegexInvalidMatchIndexException; |
16 | use Throwable; |
17 | |
18 | /** |
19 | * |
20 | * @class Regex |
21 | * |
22 | * Object that wraps preg_match |
23 | * |
24 | */ |
25 | |
26 | class Regex implements RegexInterface |
27 | { |
28 | |
29 | /** |
30 | * Regex pattern to be used in trying to match the subject. Includes delimiters. |
31 | * |
32 | * @var string $pattern |
33 | * |
34 | */ |
35 | |
36 | protected string $pattern; |
37 | |
38 | protected bool $caseSensitive = true; |
39 | |
40 | /** |
41 | * |
42 | * Array that holds the captured pieces of the subject. |
43 | * The matches array is not settable externally - it can only be populated as a result of |
44 | * running the match method in this class. |
45 | * |
46 | * @var array<string> $matches |
47 | * |
48 | */ |
49 | |
50 | protected array $matches = []; |
51 | |
52 | /** |
53 | * @var string |
54 | * |
55 | * describes what the regex is. For example, if this regex determines whether a string is a valid windows |
56 | * filename, then an appropriate label would be "valid windows filename". |
57 | */ |
58 | protected string $label; |
59 | |
60 | /** |
61 | * |
62 | * @function setPattern(); |
63 | * |
64 | * sets a regex pattern |
65 | * |
66 | * @param string $pattern . |
67 | * |
68 | * @return void |
69 | * @throws RegexBadPatternException |
70 | */ |
71 | |
72 | public function setPattern(string $pattern): void |
73 | { |
74 | if (!$this->validatePattern($pattern)) { |
75 | throw new RegexBadPatternException($pattern); |
76 | } else { |
77 | $this->pattern = $pattern; |
78 | } |
79 | } |
80 | |
81 | /** |
82 | * |
83 | * @function getPattern(); |
84 | * |
85 | * gets the regex pattern |
86 | * |
87 | * @returns string; |
88 | * |
89 | */ |
90 | |
91 | public function getPattern(): string |
92 | { |
93 | return $this->pattern ?? ''; |
94 | } |
95 | |
96 | /** |
97 | * @return bool |
98 | */ |
99 | public function isCaseSensitive(): bool |
100 | { |
101 | return $this->caseSensitive; |
102 | } |
103 | |
104 | /** |
105 | * @param bool $caseSensitive |
106 | */ |
107 | public function setCaseSensitive(bool $caseSensitive): void |
108 | { |
109 | $this->caseSensitive = $caseSensitive; |
110 | } |
111 | |
112 | /** |
113 | * @return string |
114 | */ |
115 | public function getLabel(): string |
116 | { |
117 | return $this->label . (!$this->isCaseSensitive() ? '(not case sensitive)' : ''); |
118 | } |
119 | |
120 | /** |
121 | * @param string $label |
122 | */ |
123 | public function setLabel(string $label): void |
124 | { |
125 | $this->label = $label; |
126 | } |
127 | |
128 | /** |
129 | * @function getMatch() |
130 | * |
131 | * @param array-key $index |
132 | * $index can be numeric or a string depending on whether you used a named subpattern or not. |
133 | * @return array<string>|string |
134 | * @throws RegexInvalidMatchIndexException |
135 | */ |
136 | |
137 | public function getMatch(int|string $index): array|string |
138 | { |
139 | if (!isset($this->matches[$index])) { |
140 | throw new RegexInvalidMatchIndexException($index); |
141 | } |
142 | return $this->matches[$index]; |
143 | } |
144 | |
145 | /** |
146 | * @function getMatches() |
147 | * |
148 | * @return array<string> |
149 | * |
150 | */ |
151 | |
152 | public function getMatches(): array |
153 | { |
154 | return $this->matches; |
155 | } |
156 | |
157 | |
158 | /** |
159 | * @function validatePattern |
160 | * @param string $pattern |
161 | * @return bool |
162 | * validates a regex pattern. |
163 | */ |
164 | public static function validatePattern(string $pattern): bool |
165 | { |
166 | // preg_match outputs an error (severity = warning) and returns FALSE if it fails. |
167 | try { |
168 | preg_match($pattern, ''); |
169 | return true; |
170 | } catch (Throwable $e) { |
171 | return false; |
172 | } |
173 | } |
174 | |
175 | /** |
176 | * @function validateDelimiter |
177 | * @param string $delimiter |
178 | * @return bool |
179 | * |
180 | * delimiter must be a single char, not alphanumeric, not whitespace and not a backslash |
181 | */ |
182 | public static function validateDelimiter(string $delimiter): bool |
183 | { |
184 | return !((strlen($delimiter) > 1) || (ctype_alnum($delimiter)) || ('\\' == $delimiter) || (ctype_space( |
185 | $delimiter |
186 | ))); |
187 | } |
188 | |
189 | /** |
190 | * escapeString |
191 | * @param string $pattern - without delimiters |
192 | * @return string |
193 | * |
194 | * preg_quote is kind of mis-named. It does not quote special characters in a pattern, it escapes |
195 | * them using a backslash. Its intended use is for creating a regex pattern from a string generated |
196 | * at runtime. So the idea is that a pattern argument might contain special characters which would |
197 | * need to be escaped, but you don't know because the system generates the text. So to be sure the special |
198 | * characters are escaped, you use preg_quote. |
199 | * |
200 | * Moreover, the system does not know what delimiters you want to use on your regex. And if by chance your |
201 | * runtime-generated pattern should contain the character you intend to use as a delimiter, then |
202 | * your regex will be messed up because if it is not escaped, it will terminate the pattern and the rest of the |
203 | * characters become trailing junk. By supplying your delimiter as the second argument to this function, PHP will |
204 | * escape that character as well in your pattern (before you prepend and append the delimiters of |
205 | * course) so that the delimiter character in the middle of your pattern does not muddle your pattern. |
206 | * |
207 | * So the correct usage of this method is to supply the pattern argument without delimiters and to |
208 | * supply the delimiter argument so that if that character should by chance appear in the pattern, |
209 | * it can be properly escaped. |
210 | * @throws RegexInvalidDelimiterException |
211 | * @throws RegexInvalidDelimiterException |
212 | */ |
213 | public static function escapeString(string $pattern, string $delimiter) : string |
214 | { |
215 | if (!self::validateDelimiter($delimiter)) { |
216 | throw new RegexInvalidDelimiterException(); |
217 | } |
218 | return preg_quote($pattern, $delimiter); |
219 | } |
220 | |
221 | /** |
222 | * All Regex classes use this method in order to test whether a given subject matches the pattern. |
223 | * |
224 | * @function match bool. |
225 | * |
226 | * @param string $subject |
227 | * |
228 | * $matchAll toggles the preg_match_all verb, meaning that all matches are returned, not just the first one. |
229 | * @param bool $matchAll |
230 | * |
231 | * @return bool. Returns true or false, throws an error if preg_match throws an error. |
232 | */ |
233 | |
234 | public function match(string $subject, bool $matchAll = false): bool |
235 | { |
236 | $pattern = $this->getPattern() . (!$this->isCaseSensitive() ? 'i' : ''); |
237 | |
238 | if ($matchAll) { |
239 | $result = preg_match_all($pattern, $subject, $this->matches); |
240 | } else { |
241 | $result = preg_match($pattern, $subject, $this->matches); |
242 | } |
243 | |
244 | // $result should never be false because pattern was validated when it was set |
245 | |
246 | return ($result != 0); |
247 | } |
248 | } |