Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
Regex
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
12 / 12
22
100.00% covered (success)
100.00%
1 / 1
 setPattern
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPattern
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCaseSensitive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCaseSensitive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLabel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 setLabel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMatch
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getMatches
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validatePattern
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 validateDelimiter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 escapeString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 match
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/**
4 * @package pvcRegex
5 * @author Doug Wilbourne (dougwilbourne@gmail.com)
6 */
7
8declare(strict_types=1);
9
10namespace pvc\regex;
11
12use pvc\interfaces\regex\RegexInterface;
13use pvc\regex\err\RegexBadPatternException;
14use pvc\regex\err\RegexInvalidDelimiterException;
15use pvc\regex\err\RegexInvalidMatchIndexException;
16use Throwable;
17
18/**
19 *
20 * @class Regex
21 *
22 * Object that wraps preg_match
23 *
24 */
25
26class 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}