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

SimpleRouteDumper::exportRoute()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.049

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 7
nop 1
dl 0
loc 26
ccs 9
cts 10
cp 0.9
crap 7.049
rs 8.8333
c 0
b 0
f 0
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\CompiledRoute;
21
use Flight\Routing\Interfaces\RouteCompilerInterface;
22
use Flight\Routing\Route;
23
24
/**
25
 * The routes dumper for any kind of route compiler.
26
 *
27
 * @author Divine Niiquaye Ibok <[email protected]>
28
 */
29
class SimpleRouteDumper
30
{
31
    /** @var array<string,mixed> */
32
    private $staticRoutes = [];
33
34
    /** @var mixed[] */
35
    private $regexpList = [];
36
37
    /** @var array<string,mixed> */
38
    private $urlsList = [];
39
40
    /** @var Route[] */
41 2
    private $routeList = [];
42
43 2
    /** @var string */
44
    private $cacheFile;
45
46
    public function __construct(string $cacheFile)
47 2
    {
48 2
        $this->cacheFile = $cacheFile;
49
    }
50
51
    /**
52
     * Dumps a set of routes to a string representation of executable code
53 2
     * that can then be used to match a request against these routes.
54
     *
55 2
     * @param \Traversable<int,Route> $collection
56
     */
57
    public function dump(\Traversable $collection, RouteCompilerInterface $compiler): void
58
    {
59
        // Warm up routes for export to $cacheFile.
60
        $this->warmCompiler($collection, $compiler);
61 2
62
        $generatedCode = <<<EOF
63 2
<?php
64 2
65 2
/**
66
 * This file has been auto-generated by the Flight Routing.
67
 */
68 2
return [
69 2
{$this->generateCompiledRoutes()}];
70 2
71
EOF;
72
        \file_put_contents($this->cacheFile, $generatedCode);
73
74 2
        if (\function_exists('opcache_invalidate') && \filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
75
            @\opcache_invalidate($this->cacheFile, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for opcache_invalidate(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

75
            /** @scrutinizer ignore-unhandled */ @\opcache_invalidate($this->cacheFile, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
76 2
        }
77 2
    }
78
79 2
    protected static function exportRoute(Route $route): string
80
    {
81
        $properties = $route->get('all');
82
        $properties['methods'] = \array_keys($properties['methods']);
83
        $controller = $properties['controller'];
84
        $exported = '';
85
86
        if ($controller instanceof \Closure) {
87
            $closureRef = new \ReflectionFunction($controller);
88 2
89
            if ('{closure}' === $closureRef->name) {
90 2
                throw new \RuntimeException(\sprintf('Caching route handler as an anonymous function for "%s" is unspported.', $properties['name']));
91
            }
92
93 2
            $properties['controller'] = $closureRef->name;
94
        } elseif (\is_object($controller) || (\is_array($controller) && \is_object($controller[0]))) {
95
            $properties['controller'] = \sprintf('unserialize(\'%s\')', \serialize($controller));
96 2
        }
97 2
98
        foreach ($properties as $key => $value) {
99
            $exported .= \sprintf('        %s => ', self::export($key));
100 2
            $exported .= self::export($value);
101 2
            $exported .= ",\n";
102
        }
103 2
104
        return "[\n{$exported}    ]";
105
    }
106 2
107 2
    private static function indent(string $code, int $level = 1): string
108 2
    {
109
        return (string) \preg_replace('/^./m', \str_repeat('    ', $level) . '$0', $code);
110 2
    }
111 2
112 2
    /**
113 2
     * @internal
114
     *
115 2
     * @param mixed $value
116
     */
117 2
    private static function export($value, int $level = 2): string
0 ignored issues
show
Unused Code introduced by
The parameter $level 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

117
    private static function export($value, /** @scrutinizer ignore-unused */ int $level = 2): string

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...
118 2
    {
119
        if (null === $value) {
120
            return 'null';
121
        }
122
123 2
        if (!\is_array($value)) {
124
            if ($value instanceof Route) {
125
                return self::exportRoute($value);
126 2
            } elseif ($value instanceof CompiledRoute) {
127
                return "'" . \serialize($value) . "'";
128
            }
129
130
            return \str_replace("\n", '\'."\n".\'', \var_export($value, true));
131
        }
132
133
        if (!$value) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
134
            return '[]';
135
        }
136
137
        $i = 0;
138
        $export = '[';
139 2
140
        foreach ($value as $k => $v) {
141
            if ($i === $k) {
142
                ++$i;
143 2
            } else {
144 2
                $export .= self::export($k) . ' => ';
145 2
146
                if (\is_int($k) && $i < $k) {
147 2
                    $i = 1 + $k;
148 2
                }
149
            }
150
151 2
            if (\is_string($v) && 0 === \strpos($v, 'unserialize')) {
152
                $v = '\\' . $v . ', ';
153 2
            } else {
154
                $v = self::export($v) . ', ';
155
            }
156
157 2
            $export .= $v;
158
        }
159
160 2
        return \substr_replace($export, ']', -2);
0 ignored issues
show
Bug Best Practice introduced by
The expression return substr_replace($export, ']', -2) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
161 2
    }
162 2
163
    /**
164 2
     * @param \Traversable<int,Route> $routes
165
     */
166
    private function warmCompiler(\Traversable $routes, RouteCompilerInterface $compiler): void
167
    {
168
        $regexpList = [];
169
170 2
        foreach ($routes as $index => $route) {
171
            $this->routeList[$index] = $route;
172 2
173
            // Reserved routes pattern to url ...
174
            $this->urlsList[$route->get('name')] = $compiler->compile($route, true);
175
176
            // Compile the route ...
177
            $compiledRoute = $compiler->compile($route);
178 2
179
            if (null !== $url = $compiledRoute->getStatic()) {
180 2
                $this->staticRoutes[$url] = [$index, $compiledRoute->getHostsRegex(), $compiledRoute->getVariables()];
181 2
182
                continue;
183 2
            }
184
185
            $regexpList[$compiledRoute->getRegex()] = [$index, $compiledRoute->getHostsRegex(), $compiledRoute->getVariables()];
186
        }
187 2
188 2
        $this->regexpList = $this->generateExpressions($regexpList);
189
    }
190 2
191 2
    /**
192
     * @param mixed[] $expressions
193 2
     * @param mixed[] $dynamicRoutes
194
     *
195 2
     * @return mixed[]
196
     */
197 2
    private function generateExpressions(array $expressions)
198 2
    {
199
        $variables = [];
200
        $tree = new ExpressionCollection();
201 2
202
        foreach ($expressions as $expression => $dynamicRoute) {
203
            [$pattern, $vars] = $this->filterExpression($expression);
204 2
205 2
            if (null === $pattern) {
206 2
                continue;
207
            }
208
209
            $dynamicRoute[] = $vars;
210
            \array_unshift($dynamicRoute, $pattern); // Prepend the $pattern ...
211 2
212
            $tree->addRoute($pattern, $dynamicRoute);
213 2
        }
214
215 2
        $code = '\'#^(?\'';
216
        $code .= $this->compileExpressionCollection($tree, 0, $variables);
217
        $code .= "\n    .')/?$#sD'";
218
219 2
        return [$code, $variables];
220 2
    }
221 2
222
    /**
223 2
     * Compiles a regexp tree of subpatterns that matches nested same-prefix routes.
224 2
     *
225 2
     * @param array<string,string> $vars
226 2
     */
227
    private function compileExpressionCollection(ExpressionCollection $tree, int $prefixLen, array &$vars): string
228 2
    {
229 2
        $code = '';
230 2
        $routes = $tree->getRoutes();
231
232
        foreach ($routes as $route) {
233 2
            if ($route instanceof ExpressionCollection) {
234
                $prefix = \substr($route->getPrefix(), $prefixLen);
235
                $regexpCode = $this->compileExpressionCollection($route, $prefixLen + \strlen($prefix), $vars);
236
237
                $code .= "\n        ." . self::export("|{$prefix}(?") . self::indent($regexpCode) . "\n        .')'";
238
239
                continue;
240
            }
241
242
            $code .= "\n        .";
243
            $code .= self::export(\sprintf('|%s(*:%s)', \substr(\array_shift($route), $prefixLen), $name = \array_shift($route)));
244
245 2
            $vars[$name] = $route;
246
        }
247 2
248 2
        return $code;
249
    }
250
251
    /**
252
     * @return mixed[]
253 2
     */
254 2
    private function filterExpression(string $expression): array
255 2
    {
256
        \preg_match('/\^(.*)\$/', $expression, $matches);
257 2
258 2
        if (!isset($matches[1])) {
259
            return [null, []];
260 2
        }
261
262
        $modifiers = [];
263 2
        $pattern = \preg_replace_callback(
264
            '/\?P<(\w+)>/',
265
            static function (array $matches) use (&$modifiers): string {
266
                $modifiers[] = $matches[1];
267
268
                return '';
269
            },
270
            $matches[1]
271
        );
272
273
        return [$pattern, $modifiers];
274
    }
275
276
    /**
277
     * @internal
278
     */
279
    private function generateCompiledRoutes(): string
280
    {
281
        $code = '[ // $staticRoutes' . "\n";
282
283
        foreach ($this->staticRoutes as $path => $route) {
284
            $code .= \sprintf('    %s => ', self::export($path));
285
            $code .= self::export($route);
286
            $code .= ",\n";
287
        }
288
        $code .= "],\n";
289
290
        [$regex, $variables] = $this->regexpList;
291
        $regexpCode = "    {$regex},\n    [\n";
292
293
        foreach ($variables as $key => $value) {
294
            $regexpCode .= \sprintf('        %s => ', self::export($key));
295
            $regexpCode .= self::export($value, 3);
296
            $regexpCode .= ",\n";
297
        }
298
299
        $code .= \sprintf("[ // \$regexpList\n%s    ],\n],\n", $regexpCode);
300
301
        $code .= '[ // $reversedRoutes' . "\n";
302
303
        foreach ($this->urlsList as $path => $route) {
304
            $code .= \sprintf('    %s => ', self::export($path));
305
            $code .= self::export($route);
306
            $code .= ",\n";
307
        }
308
        $code .= "],\n";
309
310
        $code .= '[ // $routeCollection' . "\n";
311
312
        foreach ($this->routeList as $name => $route) {
313
            $code .= \sprintf('    %s => ', self::export($name));
314
            $code .= self::export($route);
315
            $code .= ",\n";
316
        }
317
        $code .= "],\n";
318
319
        return self::indent($code);
320
    }
321
}
322