Test Failed
Pull Request — master (#16)
by Divine Niiquaye
02:19
created

RegexGenerator::addRoute()   C

Complexity

Conditions 13
Paths 8

Size

Total Lines 58
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 28
nc 8
nop 2
dl 0
loc 58
rs 6.6166
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Flight Routing.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Flight\Routing\Generator;
19
20
use Flight\Routing\Routes\FastRoute;
21
use Flight\Routing\Interfaces\RouteCompilerInterface;
22
23
/**
24
 * A helper Prefix tree class to help help in the compilation of routes in
25
 * preserving routes order as a full regex excluding modifies.
26
 *
27
 * This class is retrieved from symfony's routing component to add
28
 * high performance into Flight Routing and avoid requiring the whole component.
29
 *
30
 * @author Frank de Jonge <[email protected]>
31
 * @author Nicolas Grekas <[email protected]>
32
 * @author Divine Niiquaye Ibok <[email protected]>
33
 *
34
 * @internal
35
 */
36
class RegexGenerator
37
{
38
    private string $prefix;
39
40
    /** @var string[] */
41
    private array $staticPrefixes = [];
42
43
    /** @var string[] */
44
    private array $prefixes = [];
45
46
    /** @var array[]|self[] */
47
    private array $items = [];
48
49
    public function __construct(string $prefix = '/')
50
    {
51
        $this->prefix = $prefix;
52
    }
53
54
    public function getPrefix(): string
55
    {
56
        return $this->prefix;
57
    }
58
59
    /**
60
     * @return array[]|self[]
61
     */
62
    public function getRoutes(): array
63
    {
64
        return $this->items;
65
    }
66
67
    /**
68
     * This method uses default routes compiler.
69
     *
70
     * @param array<int,FastRoute> $routes
71
     *
72
     * @return array<int,mixed>
73
     */
74
    public static function beforeCaching(RouteCompilerInterface $compiler, array $routes): array
75
    {
76
        $tree = new static();
77
        $indexedRoutes = [];
78
79
        for ($i = 0; $i < \count($routes); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
80
            [$pathRegex, $hostsRegex, $variables] = $compiler->compile($route = $routes[$i]);
81
            $pathRegex = \preg_replace('/\?(?|P<\w+>|<\w+>|\'\w+\')/', '', $pathRegex);
82
83
            $tree->addRoute($pathRegex, [$pathRegex, $i, [$route, $hostsRegex, $variables]]);
84
        }
85
86
        $compiledRegex = '~^(?' . $tree->compile(0, $indexedRoutes) . ')$~u';
87
        \ksort($indexedRoutes);
88
89
        return [$compiledRegex, $indexedRoutes, $compiler];
90
    }
91
92
    /**
93
     * Compiles a regexp tree of sub-patterns that matches nested same-prefix routes.
94
     *
95
     * The route item should contain:
96
     * - pathRegex
97
     * - an id used for (*:MARK)
98
     * - an array of additional/optional values if maybe required.
99
     */
100
    public function compile(int $prefixLen, array &$variables = []): string
101
    {
102
        $code = '';
103
104
        foreach ($this->items as $route) {
105
            if ($route instanceof self) {
106
                $prefix = \substr($route->prefix, $prefixLen);
107
                $code .= '|' . \ltrim($prefix, '?') . '(?' . $route->compile($prefixLen + \strlen($prefix), $variables) . ')';
108
109
                continue;
110
            }
111
112
            $code .= '|' . \ltrim(\substr($route[0], $prefixLen), '?') . '(*:' . $route[1] . ')';
113
            $variables[$route[1]] = $route[2];
114
        }
115
116
        return $code;
117
    }
118
119
    /**
120
     * Adds a route to a group.
121
     *
122
     * @param array|self $route
123
     */
124
    public function addRoute(string $prefix, $route): void
125
    {
126
        [$prefix, $staticPrefix] = $this->getCommonPrefix($prefix, $prefix);
127
128
        for ($i = \count($this->items) - 1; 0 <= $i; --$i) {
129
            $item = $this->items[$i];
130
131
            [$commonPrefix, $commonStaticPrefix] = $this->getCommonPrefix($prefix, $this->prefixes[$i]);
132
133
            if ($this->prefix === $commonPrefix) {
134
                // the new route and a previous one have no common prefix, let's see if they are exclusive to each others
135
136
                if ($this->prefix !== $staticPrefix && $this->prefix !== $this->staticPrefixes[$i]) {
137
                    // the new route and the previous one have exclusive static prefixes
138
                    continue;
139
                }
140
141
                if ($this->prefix === $staticPrefix && $this->prefix === $this->staticPrefixes[$i]) {
142
                    // the new route and the previous one have no static prefix
143
                    break;
144
                }
145
146
                if ($this->prefixes[$i] !== $this->staticPrefixes[$i] && $this->prefix === $this->staticPrefixes[$i]) {
147
                    // the previous route is non-static and has no static prefix
148
                    break;
149
                }
150
151
                if ($prefix !== $staticPrefix && $this->prefix === $staticPrefix) {
152
                    // the new route is non-static and has no static prefix
153
                    break;
154
                }
155
156
                continue;
157
            }
158
159
            if ($item instanceof self && $this->prefixes[$i] === $commonPrefix) {
160
                // the new route is a child of a previous one, let's nest it
161
                $item->addRoute($prefix, $route);
162
            } else {
163
                // the new route and a previous one have a common prefix, let's merge them
164
                $child = new self($commonPrefix);
165
                [$child->prefixes[0], $child->staticPrefixes[0]] = $child->getCommonPrefix($this->prefixes[$i], $this->prefixes[$i]);
166
                [$child->prefixes[1], $child->staticPrefixes[1]] = $child->getCommonPrefix($prefix, $prefix);
167
                $child->items = [$this->items[$i], $route];
168
169
                $this->staticPrefixes[$i] = $commonStaticPrefix;
170
                $this->prefixes[$i] = $commonPrefix;
171
                $this->items[$i] = $child;
172
            }
173
174
            return;
175
        }
176
177
        // No optimised case was found, in this case we simple add the route for possible
178
        // grouping when new routes are added.
179
        $this->staticPrefixes[] = $staticPrefix;
180
        $this->prefixes[] = $prefix;
181
        $this->items[] = $route;
182
    }
183
184
    public static function handleError(int $type, string $msg): bool
185
    {
186
        return false !== \strpos($msg, 'Compilation failed: lookbehind assertion is not fixed length');
187
    }
188
189
    /**
190
     * Gets the full and static common prefixes between two route patterns.
191
     *
192
     * The static prefix stops at last at the first opening bracket.
193
     */
194
    private function getCommonPrefix(string $prefix, string $anotherPrefix): array
195
    {
196
        $baseLength = \strlen($this->prefix);
197
        $end = \min(\strlen($prefix), \strlen($anotherPrefix));
198
        $staticLength = null;
199
        \set_error_handler([__CLASS__, 'handleError']);
200
201
        for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) {
202
            if ('(' === $prefix[$i]) {
203
                $staticLength = $staticLength ?? $i;
204
205
                for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) {
206
                    if ($prefix[$j] !== $anotherPrefix[$j]) {
207
                        break 2;
208
                    }
209
210
                    if ('(' === $prefix[$j]) {
211
                        ++$n;
212
                    } elseif (')' === $prefix[$j]) {
213
                        --$n;
214
                    } elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) {
215
                        --$j;
216
217
                        break;
218
                    }
219
                }
220
221
                if (0 < $n) {
222
                    break;
223
                }
224
225
                if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) {
226
                    break;
227
                }
228
                $subPattern = \substr($prefix, $i, $j - $i);
229
230
                if ($prefix !== $anotherPrefix && !\preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !\preg_match('{(?<!' . $subPattern . ')}', '')) {
231
                    // sub-patterns of variable length are not considered as common prefixes because their greediness would break in-order matching
232
                    break;
233
                }
234
                $i = $j - 1;
235
            } elseif ('\\' === $prefix[$i] && (++$i === $end || $prefix[$i] !== $anotherPrefix[$i])) {
236
                --$i;
237
238
                break;
239
            }
240
        }
241
242
        \restore_error_handler();
243
244
        if ($i < $end && 0b10 === (\ord($prefix[$i]) >> 6) && \preg_match('//u', $prefix . ' ' . $anotherPrefix)) {
245
            do {
246
                // Prevent cutting in the middle of an UTF-8 characters
247
                --$i;
248
            } while (0b10 === (\ord($prefix[$i]) >> 6));
249
        }
250
251
        return [\substr($prefix, 0, $i), \substr($prefix, 0, $staticLength ?? $i)];
252
    }
253
}
254