Passed
Push — master ( 0f979d...0862ec )
by Hong
02:33 queued 13s
created

FastRouteParser   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 211
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 74
c 1
b 0
f 0
dl 0
loc 211
rs 10
wmc 21

7 Methods

Rating   Name   Duplication   Size   Complexity  
A fixMatches() 0 10 4
A match() 0 11 3
A getRegexData() 0 20 4
A parse() 0 6 1
A convert() 0 20 2
A getMapData() 0 23 6
A doneProcess() 0 7 1
1
<?php
2
3
/**
4
 * Phoole (PHP7.2+)
5
 *
6
 * @category  Library
7
 * @package   Phoole\Route
8
 * @copyright Copyright (c) 2019 Hong Zhang
9
 */
10
declare(strict_types=1);
11
12
namespace Phoole\Route\Parser;
13
14
/**
15
 * FastRouteParser
16
 *
17
 * @package Phoole\Route
18
 */
19
class FastRouteParser implements ParserInterface
20
{
21
    /**
22
     * flag for new route added.
23
     *
24
     * @var    bool
25
     */
26
    protected $modified = false;
27
28
    /**
29
     * regex storage
30
     *
31
     * @var    string[]
32
     */
33
    protected $regex = [];
34
35
    /**
36
     * group position map
37
     *
38
     * @var    array
39
     */
40
    protected $maps = [];
41
42
    /**
43
     * chunk size 4 - 12 for merging regex
44
     *
45
     * @var    int
46
     */
47
    protected $chunk = 8;
48
49
    /**
50
     * combined regex (cache)
51
     *
52
     * @var    string[]
53
     */
54
    protected $data = [];
55
56
    /**
57
     * another cache
58
     *
59
     * @var    string[]
60
     */
61
    protected $xmap = [];
62
63
    /**
64
     * @var    string
65
     */
66
    const MATCH_GROUP_NAME = "\s*([a-zA-Z][a-zA-Z0-9_]*)\s*";
67
    const MATCH_GROUP_TYPE = ":\s*([^{}]*(?:\{(?-1)\}[^{}]*)*)";
68
    const MATCH_SEGMENT = "[^/]++";
69
    
70
    /**
71
     * pattern shortcuts
72
     *
73
     * @var    string[]
74
     */
75
    protected $shortcuts = [
76
        ':d}'   => ':[0-9]++}',             // digit only
77
        ':l}'   => ':[a-z]++}',             // lower case
78
        ':u}'   => ':[A-Z]++}',             // upper case
79
        ':a}'   => ':[0-9a-zA-Z]++}',       // alphanumeric
80
        ':c}'   => ':[0-9a-zA-Z+_\-\.]++}', // common chars
81
        ':nd}'  => ':[^0-9/]++}',           // not digits
82
        ':xd}'  => ':[^0-9/][^/]*+}',       // no leading digits
83
    ];
84
85
    /**
86
     * {@inheritDoc}
87
     */
88
    public function parse(string $routeName, string $routePattern): string
89
    {
90
        list($regex, $map) = $this->convert($routePattern);
91
        $this->maps[$routeName] = $map;
92
        $this->doneProcess($routeName, $routePattern, $regex);
93
        return $regex;
94
    }
95
96
    /**
97
     * {@inheritDoc}
98
     */
99
    public function match(string $uriPath): array
100
    {
101
        $matches = [];
102
        foreach ($this->getRegexData() as $i => $regex) {
103
            if (preg_match($regex, $uriPath, $matches)) {
104
                $map = array_flip($this->xmap[$i]);
0 ignored issues
show
Bug introduced by
$this->xmap[$i] of type string is incompatible with the type array expected by parameter $array of array_flip(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

104
                $map = array_flip(/** @scrutinizer ignore-type */ $this->xmap[$i]);
Loading history...
105
                $key = $map[count($matches) - 1];
106
                return $this->fixMatches($key, $matches);
107
            }
108
        }
109
        return $matches;
110
    }
111
112
    /**
113
     * Convert to regex
114
     *
115
     * @param  string $pattern pattern to parse
116
     * @return array
117
     */
118
    protected function convert(string $pattern): array
119
    {
120
        $ph = sprintf("\{%s(?:%s)?\}", self::MATCH_GROUP_NAME, self::MATCH_GROUP_TYPE);
121
122
        // count placeholders
123
        $map = $m = [];
124
        if (preg_match_all('~' . $ph . '~x', $pattern, $m)) {
125
            $map = $m[1];
126
        }
127
128
        $result = preg_replace(
129
            [
130
            '~' . $ph . '(*SKIP)(*FAIL) | \[~x', '~' . $ph . '(*SKIP)(*FAIL) | \]~x',
131
            '~\{' . self::MATCH_GROUP_NAME . '\}~x', '~' . $ph . '~x',
132
            ],
133
            ['(?:', ')?', '{\\1:' . self::MATCH_SEGMENT . '}', '(\\2)'],
134
            strtr('/' . trim($pattern, '/'), $this->shortcuts)
135
        );
136
137
        return [$result, $map];
138
    }
139
140
    /**
141
     * Merge several (chunk size) regex into one
142
     *
143
     * @return array
144
     */
145
    protected function getRegexData(): array
146
    {
147
        // load from cache
148
        if (!$this->modified) {
149
            return $this->data;
150
        }
151
152
        // merge
153
        $this->data = array_chunk($this->regex, $this->chunk, true);
154
        foreach ($this->data as $i => $arr) {
155
            $map = $this->getMapData($arr, $this->maps);
156
            $str = '~^(?|';
157
            foreach ($arr as $k => $reg) {
158
                $str .= $reg . str_repeat('()', $map[$k] - count($this->maps[$k])) . '|';
159
            }
160
            $this->data[$i] = substr($str, 0, -1) . ')$~x';
161
            $this->xmap[$i] = $map;
162
        }
163
        $this->modified = false;
164
        return $this->data;
165
    }
166
167
    /**
168
     * @param  array $arr
169
     * @param  array $maps
170
     * @return array
171
     */
172
    protected function getMapData(array $arr, array $maps): array
173
    {
174
        $new1 = [];
175
        $keys = array_keys($arr);
176
        foreach ($keys as $k) {
177
            $new1[$k] = count($maps[$k]) + 1; // # of PH for route $k
178
        }
179
        $new2 = array_flip($new1);
180
        $new3 = array_flip($new2);
181
182
        foreach ($keys as $k) {
183
            if (!isset($new3[$k])) {
184
                foreach (range(1, 200) as $i) {
185
                    $cnt = $new1[$k] + $i;
186
                    if (!isset($new2[$cnt])) {
187
                        $new2[$cnt] = $k;
188
                        $new3[$k] = $cnt;
189
                        break;
190
                    }
191
                }
192
            }
193
        }
194
        return $new3;
195
    }
196
197
    /**
198
     * Fix matched placeholders, return with unique route key
199
     *
200
     * @param  string $name the route key/name
201
     * @param  array $matches desc
202
     * @return array [ $name, $matches ]
203
     */
204
    protected function fixMatches(string $name, array $matches): array
205
    {
206
        $res = [];
207
        $map = $this->maps[$name];
208
        foreach ($matches as $idx => $val) {
209
            if ($idx > 0 && '' !== $val) {
210
                $res[$map[$idx - 1]] = $val;
211
            }
212
        }
213
        return [$name, $res];
214
    }
215
216
    /**
217
     * Update regex pool etc.
218
     *
219
     * @param  string $routeName
220
     * @param  string $routePattern
221
     * @param  string $regex
222
     */
223
    protected function doneProcess(
224
        string $routeName,
225
        string $routePattern,
0 ignored issues
show
Unused Code introduced by
The parameter $routePattern is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

225
        /** @scrutinizer ignore-unused */ string $routePattern,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
226
        string $regex
227
    ) {
228
        $this->regex[$routeName] = $regex;
229
        $this->modified = true;
230
    }
231
}
232