FastRouteParser::fixMatches()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 6
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 10
rs 10
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
     * @var    string
23
     */
24
    const MATCH_GROUP_NAME = "\s*([a-zA-Z][a-zA-Z0-9_]*)\s*";
25
    const MATCH_GROUP_TYPE = ":\s*([^{}]*(?:\{(?-1)\}[^{}]*)*)";
26
    const MATCH_SEGMENT    = "[^/]++";
27
28
    /**
29
     * flag for new route added.
30
     *
31
     * @var    bool
32
     */
33
    protected $modified = FALSE;
34
35
    /**
36
     * regex storage
37
     *
38
     * @var    string[]
39
     */
40
    protected $regex = [];
41
42
    /**
43
     * group position map
44
     *
45
     * @var    array
46
     */
47
    protected $maps = [];
48
49
    /**
50
     * chunk size 4 - 12 for merging regex
51
     *
52
     * @var    int
53
     */
54
    protected $chunk = 8;
55
56
    /**
57
     * combined regex (cache)
58
     *
59
     * @var    string[]
60
     */
61
    protected $data = [];
62
63
    /**
64
     * another cache
65
     *
66
     * @var    string[][]
67
     */
68
    protected $xmap = [];
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, $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]);
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
        // count placeholders
122
        $map = $m = [];
123
        if (preg_match_all('~' . $ph . '~x', $pattern, $m)) {
124
            $map = $m[1];
125
        }
126
        $result = preg_replace(
127
            [
128
                '~' . $ph . '(*SKIP)(*FAIL) | \[~x', '~' . $ph . '(*SKIP)(*FAIL) | \]~x',
129
                '~\{' . self::MATCH_GROUP_NAME . '\}~x', '~' . $ph . '~x',
130
            ],
131
            ['(?:', ')?', '{\\1:' . self::MATCH_SEGMENT . '}', '(\\2)'],
132
            strtr('/' . trim($pattern, '/'), $this->shortcuts)
133
        );
134
        return [$result, $map];
135
    }
136
137
    /**
138
     * Update regex pool etc.
139
     *
140
     * @param  string $routeName
141
     * @param  string $regex
142
     */
143
    protected function doneProcess(
144
        string $routeName,
145
        string $regex
146
    ) {
147
        $this->regex[$routeName] = $regex;
148
        $this->modified = TRUE;
149
    }
150
151
    /**
152
     * Merge several (chunk size) regex into one
153
     *
154
     * @return array
155
     */
156
    protected function getRegexData(): array
157
    {
158
        // load from cache
159
        if (!$this->modified) {
160
            return $this->data;
161
        }
162
        // merge
163
        $this->data = array_chunk($this->regex, $this->chunk, TRUE);
164
        foreach ($this->data as $i => $arr) {
165
            $map = $this->getMapData($arr, $this->maps);
166
            $str = '~^(?|';
167
            foreach ($arr as $k => $reg) {
168
                $str .= $reg . str_repeat('()', $map[$k] - count($this->maps[$k])) . '|';
169
            }
170
            $this->data[$i] = substr($str, 0, -1) . ')$~x';
171
            $this->xmap[$i] = $map;
172
        }
173
        $this->modified = FALSE;
174
        return $this->data;
175
    }
176
177
    /**
178
     * @param  array $arr
179
     * @param  array $maps
180
     * @return array
181
     */
182
    protected function getMapData(array $arr, array $maps): array
183
    {
184
        $new1 = [];
185
        $keys = array_keys($arr);
186
        foreach ($keys as $k) {
187
            $new1[$k] = count($maps[$k]) + 1; // # of PH for route $k
188
        }
189
        $new2 = array_flip($new1);
190
        $new3 = array_flip($new2);
191
        foreach ($keys as $k) {
192
            if (!isset($new3[$k])) {
193
                foreach (range(1, 200) as $i) {
194
                    $cnt = $new1[$k] + $i;
195
                    if (!isset($new2[$cnt])) {
196
                        $new2[$cnt] = $k;
197
                        $new3[$k] = $cnt;
198
                        break;
199
                    }
200
                }
201
            }
202
        }
203
        return $new3;
204
    }
205
206
    /**
207
     * Fix matched placeholders, return with unique route key
208
     *
209
     * @param  string $name     the route key/name
210
     * @param  array  $matches  desc
211
     * @return array [ $name, $matches ]
212
     */
213
    protected function fixMatches(string $name, array $matches): array
214
    {
215
        $res = [];
216
        $map = $this->maps[$name];
217
        foreach ($matches as $idx => $val) {
218
            if ($idx > 0 && '' !== $val) {
219
                $res[$map[$idx - 1]] = $val;
220
            }
221
        }
222
        return [$name, $res];
223
    }
224
}