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