Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.20% covered (success)
94.20%
65 / 69
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Regex
94.20% covered (success)
94.20%
65 / 69
62.50% covered (warning)
62.50%
5 / 8
34.23
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 normalizePattern
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
12
 escape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMatch
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 matches
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
8
 replace
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 replaceCallback
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 split
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core;
6
7use PeServer\Core\Collection\Arr;
8use PeServer\Core\Collection\OrderBy;
9use PeServer\Core\Errors\ErrorHandler;
10use PeServer\Core\Throws\ArgumentException;
11use PeServer\Core\Throws\RegexDelimiterException;
12use PeServer\Core\Throws\RegexException;
13use PeServer\Core\Throws\RegexPatternException;
14
15/**
16 * 正規表現ラッパー。
17 */
18class Regex
19{
20    #region define
21
22    public const UNLIMITED = -1;
23    private const DELIMITER_CLOSE_START_INDEX = 2;
24
25    #endregion
26
27    #region variable
28
29    private static ?Encoding $firstDefaultEncoding = null;
30    private readonly Encoding $encoding;
31
32    #endregion
33
34    /**
35     * 生成。
36     *
37     * @param Encoding|null $encoding UTF-8の場合 /pattern/u の u を追加する用。標準エンコーディング(mb_internal_encoding: 基本UTF-8)を想定。
38     */
39    public function __construct(?Encoding $encoding = null)
40    {
41        if ($encoding === null) {
42            if (self::$firstDefaultEncoding === null) {
43                self::$firstDefaultEncoding = Encoding::getDefaultEncoding();
44            }
45            $this->encoding = self::$firstDefaultEncoding;
46        } else {
47            $this->encoding = $encoding;
48        }
49    }
50
51    #region function
52
53    private function normalizePattern(string $pattern): string
54    {
55        $byteLength = strlen($pattern);
56        if ($byteLength < 3) {
57            throw new RegexPatternException($pattern);
58        }
59
60        $open = $pattern[0];
61        $closeIndex = match ($open) {
62            '(' => strrpos($pattern, ')', self::DELIMITER_CLOSE_START_INDEX),
63            '{' => strrpos($pattern, '}', self::DELIMITER_CLOSE_START_INDEX),
64            '[' => strrpos($pattern, ']', self::DELIMITER_CLOSE_START_INDEX),
65            '<' => strrpos($pattern, '>', self::DELIMITER_CLOSE_START_INDEX),
66            default => strrpos($pattern, $open, self::DELIMITER_CLOSE_START_INDEX),
67        };
68        if ($closeIndex === false || $closeIndex === 0) {
69            throw new RegexDelimiterException();
70        }
71
72        //UTF8対応
73        if ($this->encoding->name === Encoding::ENCODE_UTF8) {
74            if ($closeIndex === ($byteLength - 1)) {
75                return $pattern . 'u';
76            }
77            if (strpos($pattern, 'u', $closeIndex) === false) {
78                return $pattern . 'u';
79            }
80        }
81
82        return $pattern;
83    }
84
85    /**
86     * 正規表現パターンをエスケープコードに変換。
87     *
88     * `preg_quote` ラッパー。
89     *
90     * @param string $s 正規表現パターン。
91     * @param string|null $delimiter デリミタ(null: `/`)。
92     * @return string
93     * @see https://www.php.net/manual/function.preg-quote.php
94     */
95    public function escape(string $s, ?string $delimiter = null): string
96    {
97        return preg_quote($s, $delimiter);
98    }
99
100    /**
101     * パターンにマッチするか。
102     *
103     * @param string $input 対象文字列。
104     * @param string $pattern 正規表現パターン。
105     * @phpstan-param literal-string $pattern
106     * @return boolean マッチしたか。
107     * @throws RegexException 正規表現処理失敗。
108     */
109    public function isMatch(string $input, string $pattern): bool
110    {
111        $result = ErrorHandler::trap(fn () => preg_match($this->normalizePattern($pattern), $input));
112        if ($result->isFailureOrFalse()) {
113            throw new RegexException(preg_last_error_msg(), preg_last_error());
114        }
115
116        return (bool)$result->value;
117    }
118
119    /**
120     * パターンマッチ。
121     *
122     * @param string $input
123     * @param string $pattern
124     * @phpstan-param literal-string $pattern
125     * @return array<int|string,string>
126     * @phpstan-return array<array-key,string>
127     */
128    public function matches(string $input, string $pattern): array
129    {
130        $matches = [];
131        $result = ErrorHandler::trap(function () use ($pattern, $input, &$matches) {
132            return preg_match_all($this->normalizePattern($pattern), $input, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE);
133        });
134        if ($result->isFailureOrFalse()) {
135            throw new RegexException(preg_last_error_msg(), preg_last_error());
136        }
137        if ($result->value === 0) {
138            return [];
139        }
140
141        // 最初のやつは無かったことにする
142        array_shift($matches);
143
144        $items = [];
145        foreach ($matches as $key => $match) {
146            if (is_int($key)) {
147                foreach ($match as $v) {
148                    $items[$v[1]] = $v[0];
149                }
150            } else {
151                $items[$key] = $match[0][0];
152            }
153        }
154        $items = Arr::sortByKey($items, OrderBy::Ascending);
155
156        $resultMatches = [
157            0 => $input,
158        ];
159
160        foreach ($items as $key => $item) {
161            if (is_int($key)) {
162                $resultMatches[] = $item;
163            } else {
164                $resultMatches[$key] = $item;
165            }
166        }
167
168        return $resultMatches;
169    }
170
171    /**
172     * 正規表現置き換え。
173     *
174     * @param string $source 一致する対象を検索する文字列。
175     * @param string $pattern 一致させる正規表現パターン。
176     * @param string $replacement 置換文字列。
177     * @param int $limit 各パターンによる 置換を行う最大回数。
178     * @throws ArgumentException 引数がおかしい。
179     * @throws RegexException 正規表現処理失敗。
180     * @return string
181     * @see https://www.php.net/manual/function.preg-replace.php
182     */
183    public function replace(string $source, string $pattern, string $replacement, int $limit = self::UNLIMITED): string
184    {
185        if (!$limit) {
186            throw new ArgumentException();
187        }
188
189        $result = ErrorHandler::trap(fn () => preg_replace($this->normalizePattern($pattern), $replacement, $source, $limit));
190        if ($result->isFailureOrFailValue(null)) {
191            throw new RegexException(preg_last_error_msg(), preg_last_error());
192        }
193
194        return $result->value;
195    }
196
197
198    /**
199     * 正規表現置き換え。
200     *
201     * @param string $source 一致する対象を検索する文字列。
202     * @param string $pattern 一致させる正規表現パターン。
203     * @param callable $replacement 置換処理。
204     * @phpstan-param callable(array<array-key,string>):string $replacement
205     * @param int $limit 各パターンによる 置換を行う最大回数。
206     * @throws ArgumentException 引数がおかしい。
207     * @throws RegexException 正規表現処理失敗。
208     * @return string
209     * @see https://www.php.net/manual/function.preg-replace-callback.php
210     */
211    public function replaceCallback(string $source, string $pattern, callable $replacement, int $limit = self::UNLIMITED): string
212    {
213        if (!$limit) {
214            throw new ArgumentException();
215        }
216
217        $result = preg_replace_callback($this->normalizePattern($pattern), $replacement, $source, $limit);
218        if ($result === null) {
219            throw new RegexException(preg_last_error_msg(), preg_last_error());
220        }
221
222        return $result;
223    }
224
225    /**
226     * 分割処理。
227     *
228     * @param string $source 入力文字列。
229     * @param string $pattern パターン。
230     * @return string[]
231     */
232    public function split(string $source, string $pattern): array
233    {
234        $result = preg_split($this->normalizePattern($pattern), $source);
235        if ($result === false) {
236            throw new RegexException(preg_last_error_msg(), preg_last_error());
237        }
238
239        return $result;
240    }
241
242    #endregion
243}