Test Failed
Push — master ( d3660e...c7a4a9 )
by Divine Niiquaye
10:08
created

SimpleRouteDumper::warmCompiler()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 20
c 1
b 0
f 0
dl 0
loc 35
rs 8.9777
cc 6
nc 6
nop 1
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\Matchers;
19
20
use Flight\Routing\Interfaces\MatcherDumperInterface;
21
use Flight\Routing\Route;
22
use Flight\Routing\RouteCollection;
23
use Flight\Routing\Traits\DumperTrait;
24
use Psr\Http\Message\ServerRequestInterface;
25
26
/**
27
 * The routes dumper for any kind of route compiler.
28
 *
29
 * @author Divine Niiquaye Ibok <[email protected]>
30
 */
31
class SimpleRouteDumper extends SimpleRouteMatcher implements MatcherDumperInterface
32
{
33
    use DumperTrait;
34
35
    /** @var string[] */
36
    private $dynamicRoutes = [];
37
38
    /** @var array<string,string|null> */
39
    private $staticRoutes = [];
40
41
    /** @var mixed[] */
42
    private $regexpList = [];
43
44
    /**
45
     * @param RouteCollection|string $collection
46
     */
47
    public function __construct($collection)
48
    {
49
        parent::__construct($collection);
0 ignored issues
show
Bug introduced by
It seems like $collection can also be of type string; however, parameter $collection of Flight\Routing\Matchers\...eMatcher::__construct() does only seem to accept Flight\Routing\Route[]&F...Routing\RouteCollection, maybe add an additional type check? ( Ignorable by Annotation )

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

49
        parent::__construct(/** @scrutinizer ignore-type */ $collection);
Loading history...
50
51
        if (!$collection instanceof RouteCollection) {
52
            $this->export = false;
53
        }
54
55
        $this->warmCompiler($this->routes);
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function dump()
62
    {
63
        return $this->generateCompiledRoutes();
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function match(ServerRequestInterface $request): ?Route
70
    {
71
        $requestUri    = $request->getUri();
72
        $requestMethod = $request->getMethod();
73
        $resolvedPath  = \rawurldecode($this->resolvePath($request));
74
75
        // To prevent breaks when routes are writing to cache file.
76
        if ($this->export) {
77
            $this->export = false;
78
            $this->warmCompiler($this->routes);
79
        }
80
81
        // FInd the matched route ...
82
        $matchedRoute = $this->getCompiledRoute($resolvedPath);
83
84
        if ($matchedRoute instanceof Route) {
0 ignored issues
show
introduced by
$matchedRoute is always a sub-type of Flight\Routing\Route.
Loading history...
85
            $matchDomain = $matchedRoute->getDefaults()['_domain'] ?? [[], []];
86
87
            return $this->matchRoute($matchedRoute, $requestUri, $requestMethod, $matchDomain);
88
        }
89
90
        return null;
91
    }
92
93
    /**
94
     * @return mixed[]
95
     */
96
    public function getCompiledRoutes()
97
    {
98
        return [$this->staticRoutes, $this->dynamicRoutes, $this->regexpList, $this->routes];
99
    }
100
101
    protected function getCompiledRoute(string $resolvedPath): ?Route
102
    {
103
        // Find static route ...
104
        if (isset($this->staticRoutes[$resolvedPath])) {
105
            return $this->routes[$this->staticRoutes[$resolvedPath]];
106
        }
107
108
        static $matchedRoute;
109
        $urlVariables = [];
110
111
        [$regexpList, $parameters] = $this->regexpList;
112
113
        // https://tools.ietf.org/html/rfc7231#section-6.5.5
114
        if ($this->compareUri($regexpList, $resolvedPath, $urlVariables)) {
115
            $routeId = $urlVariables['MARK'];
116
            unset($urlVariables[0], $urlVariables['MARK']);
117
118
            if (isset($this->dynamicRoutes[$routeId])) {
119
                $countVars    = 0;
120
                $matchedRoute = $this->routes[$this->dynamicRoutes[$routeId]];
121
                $parameters   = $parameters[$routeId];
122
123
                foreach ($matchedRoute->getArguments() as $key => $value) {
124
                    if (
125
                        \in_array($key, $parameters, true) &&
126
                        (null === $value && isset($urlVariables[$countVars]))
127
                    ) {
128
                        $matchedRoute->argument($key, $urlVariables[$countVars]);
129
                    }
130
131
                    $countVars++;
132
                }
133
134
                return $matchedRoute;
135
            }
136
        }
137
138
        return $matchedRoute;
139
    }
140
141
    /**
142
     * @param mixed[] $expressions
143
     * @param mixed[] $names
144
     *
145
     * @return mixed[]
146
     */
147
    private function generateExpressions(array $expressions, array $names)
148
    {
149
        // $namesCount For keeping track of the names for sub-matches.
150
        // $captureCount For re-adjust backreferences.
151
        $namesCount = $captureCount = 0;
152
        $variables  = [];
153
        $tree       = new ExpressionCollection();
154
155
        foreach ($expressions as $expression) {
156
            $name = $names[$namesCount++];
157
158
            // Get delimiters and vars:
159
            [$pattern, $vars] = $this->filterExpression($expression, $captureCount);
160
161
            if (false === $pattern) {
162
                return [[], []];
163
            }
164
165
            $tree->addRoute($pattern, [$name, $pattern, $vars]);
166
        }
167
168
        $code = $this->export ? '\'#^(?\'' : '#^(?';
169
        $code .= $this->compileExpressionCollection($tree, 0, $variables);
170
        $code .= $this->export ? "\n    .')/?$#sD'" : ')/?$#sD';
171
172
        return [$code, $variables];
173
    }
174
175
    /**
176
     * @param Route[]|string $routes
177
     */
178
    private function warmCompiler($routes): void
179
    {
180
        if (\is_string($routes)) {
181
            [$this->staticRoutes, $this->dynamicRoutes, $this->regexpList, $this->routes] = require $routes;
182
183
            return;
184
        }
185
186
        $regexpList = $newRoutes = [];
187
188
        foreach ($routes as $route) {
189
            $compiledRoute = clone $this->getCompiler()->compile($route);
190
191
            $routeName     = $route->getName();
192
            $pathVariables = $compiledRoute->getPathVariables();
193
194
            if (!empty($compiledRoute->getHostVariables())) {
195
                $route->default('_domain', [$compiledRoute->getHostsRegex(), $compiledRoute->getHostVariables()]);
196
            }
197
198
            if (empty($pathVariables)) {
199
                $url  = \rtrim($route->getPath(), '/') ?: '/';
200
201
                $this->staticRoutes[$url] = $routeName;
202
            } else {
203
                $route->arguments($pathVariables);
204
205
                $this->dynamicRoutes[] = $routeName;
206
                $regexpList[]          = $compiledRoute->getRegex();
207
            }
208
209
            $newRoutes[$routeName] = $route;
210
        }
211
        $this->routes     = $newRoutes; // Set the new routes.
212
        $this->regexpList = $this->generateExpressions($regexpList, \array_keys($this->dynamicRoutes));
213
    }
214
215
    /**
216
     * @return mixed[]
217
     */
218
    private function filterExpression(string $expression, int &$captureCount): array
219
    {
220
        \preg_match('/^(.)\^(.*)\$.([a-zA-Z]*$)/', $expression, $matches);
221
222
        if (empty($matches)) {
223
            return [false, []];
224
        }
225
226
        $modifiers = [];
227
        $delimeter = $matches[1];
228
        $pattern   = $matches[2];
229
230
        $pattern = \preg_replace_callback(
231
            '/\?P<([^>]++)>/',
232
            static function (array $matches) use (&$modifiers): string {
233
                $modifiers[] = $matches[1];
234
235
                return '';
236
            },
237
            $pattern
238
        );
239
240
        if ($delimeter !== '/') {
241
            // Replace occurrences by the escaped delimiter by its unescaped
242
            // version and escape new delimiter.
243
            $pattern = \str_replace("\\$delimeter", $delimeter, $pattern);
244
            $pattern = \str_replace('/', '\\/', $pattern);
245
        }
246
247
        // Re-adjust backreferences:
248
        // TODO What about \R backreferences (\0 isn't allowed, though)?
249
250
        // We assume that the expression is correct and therefore don't check
251
        // for matching parentheses.
252
        $captures = \preg_match_all('/\([^?]|\(\?[^:]/', $pattern);
253
254
        if ($captures > 0) {
255
            $backref = '/
256
                (?<!\\\\)        # Not preceded by a backslash,
257
                ((?:\\\\\\\\)*?) # zero or more escaped backslashes,
258
                \\\\ (\d+)       # followed by backslash plus digits.
259
            /x';
260
            $pattern = \preg_replace_callback(
261
                $backref,
262
                static function (array $m) use ($captureCount): string {
263
                    return $m[1] . '\\\\' . ((int) $m[2] + $captureCount);
264
                },
265
                $pattern
266
            );
267
            $captureCount += $captures;
268
        }
269
270
        return [$pattern, $modifiers];
271
    }
272
}
273