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::