1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of Flight Routing. |
5
|
|
|
* |
6
|
|
|
* PHP version 8.0 and above required |
7
|
|
|
* |
8
|
|
|
* @author Divine Niiquaye Ibok <[email protected]> |
9
|
|
|
* @copyright 2019 Divine Niiquaye Ibok (https://divinenii.com/) |
10
|
|
|
* @license https://opensource.org/licenses/BSD-3-Clause License |
11
|
|
|
* |
12
|
|
|
* For the full copyright and license information, please view the LICENSE |
13
|
|
|
* file that was distributed with this source code. |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
namespace Flight\Routing\Traits; |
17
|
|
|
|
18
|
|
|
use Flight\Routing\Handlers\ResourceHandler; |
19
|
|
|
use Flight\Routing\RouteCollection; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* A default cache implementation for route match. |
23
|
|
|
* |
24
|
|
|
* @author Divine Niiquaye Ibok <[email protected]> |
25
|
|
|
*/ |
26
|
|
|
trait CacheTrait |
27
|
|
|
{ |
28
|
|
|
private ?string $cache = null; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @param string $path file path to store compiled routes |
32
|
|
|
*/ |
33
|
2 |
|
public function setCache(string $path): void |
34
|
|
|
{ |
35
|
2 |
|
$this->cache = $path; |
36
|
|
|
} |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* A well php value formatter, better than (var_export). |
40
|
|
|
*/ |
41
|
3 |
|
public static function export(mixed $value, string $indent = ''): string |
42
|
|
|
{ |
43
|
|
|
switch (true) { |
44
|
3 |
|
case [] === $value: |
45
|
1 |
|
return '[]'; |
46
|
3 |
|
case \is_array($value): |
47
|
3 |
|
if (!($t = \count($value, \COUNT_RECURSIVE) > 15) && \array_is_list($value)) { |
48
|
3 |
|
\array_walk($value, function ($v) use (&$t, $value) { |
49
|
3 |
|
if (\count($value) > 1 && \is_string($v) && \strlen($v) > 100) { |
50
|
2 |
|
$t = true; |
51
|
|
|
} |
52
|
|
|
}); |
53
|
|
|
} |
54
|
|
|
|
55
|
3 |
|
$j = -1; |
56
|
3 |
|
$code = $t ? "[\n" : '['; |
57
|
3 |
|
$subIndent = $t ? $indent.' ' : $indent = ''; |
58
|
|
|
|
59
|
3 |
|
foreach ($value as $k => $v) { |
60
|
3 |
|
$code .= $subIndent; |
61
|
|
|
|
62
|
3 |
|
if (!\is_int($k) || $k !== ++$j) { |
63
|
3 |
|
$code .= self::export($k, $subIndent).' => '; |
64
|
|
|
} |
65
|
|
|
|
66
|
3 |
|
$code .= self::export($v, $subIndent).($t ? ",\n" : ', '); |
67
|
|
|
} |
68
|
|
|
|
69
|
3 |
|
return \rtrim($code, ', ').$indent.']'; |
70
|
3 |
|
case (\is_string($value) && (':' === $value[0] && ':' === $value[-1])): |
71
|
2 |
|
return \substr($value, 1, -1); |
72
|
3 |
|
case $value instanceof ResourceHandler: |
73
|
1 |
|
return $value::class.'('.self::export($value(''), $indent).')'; |
74
|
3 |
|
case $value instanceof \stdClass: |
75
|
1 |
|
return '(object) '.self::export((array) $value, $indent); |
76
|
3 |
|
case $value instanceof RouteCollection: |
77
|
1 |
|
return $value::class.'::__set_state('.self::export([ |
78
|
1 |
|
'routes' => $value->getRoutes(), |
79
|
1 |
|
'defaultIndex' => $value->count() - 1, |
80
|
|
|
'sorted' => true, |
81
|
|
|
], $indent).')'; |
82
|
3 |
|
case \is_object($value): |
83
|
1 |
|
if (\method_exists($value, '__set_state')) { |
84
|
1 |
|
return $value::class.'::__set_state('.self::export( |
85
|
1 |
|
\array_merge(...\array_map(function (\ReflectionProperty $v) use ($value): array { |
86
|
1 |
|
$v->setAccessible(true); |
87
|
|
|
|
88
|
1 |
|
return [$v->getName() => $v->getValue($value)]; |
89
|
1 |
|
}, (new \ReflectionObject($value))->getProperties())) |
90
|
|
|
); |
91
|
|
|
} |
92
|
|
|
|
93
|
1 |
|
return 'unserialize(\''.\serialize($value).'\')'; |
94
|
|
|
} |
95
|
|
|
|
96
|
3 |
|
return \var_export($value, true); |
97
|
|
|
} |
98
|
|
|
|
99
|
4 |
|
protected function doCache(): RouteCollection |
100
|
|
|
{ |
101
|
4 |
|
if (\is_array($a = @include $this->cache)) { |
102
|
3 |
|
$this->optimized = $a; |
|
|
|
|
103
|
|
|
|
104
|
3 |
|
return $this->optimized[2] ??= $this->collection ?? new RouteCollection(); |
105
|
|
|
} |
106
|
|
|
|
107
|
2 |
|
if (\is_callable($collection = $this->collection ?? new RouteCollection())) { |
108
|
1 |
|
$collection($collection = new RouteCollection()); |
109
|
1 |
|
$collection->sort(); |
110
|
1 |
|
$doCache = true; |
111
|
|
|
} |
112
|
|
|
|
113
|
2 |
|
if (!\is_dir($directory = \dirname($this->cache))) { |
|
|
|
|
114
|
2 |
|
@\mkdir($directory, 0775, true); |
|
|
|
|
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
try { |
118
|
2 |
|
return $collection; |
119
|
|
|
} finally { |
120
|
2 |
|
$dumpData = $this->buildCache($collection, $doCache ?? false); |
121
|
2 |
|
\file_put_contents($this->cache, "<?php // auto generated: AVOID MODIFYING\n\nreturn ".$dumpData.";\n"); |
|
|
|
|
122
|
|
|
|
123
|
2 |
|
if (\function_exists('opcache_invalidate') && \filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { |
124
|
2 |
|
@\opcache_invalidate($this->cache, true); |
|
|
|
|
125
|
|
|
} |
126
|
|
|
|
127
|
2 |
|
$this->optimized = require $this->cache; |
128
|
|
|
} |
129
|
|
|
} |
130
|
|
|
|
131
|
2 |
|
protected function buildCache(RouteCollection $collection, bool $doCache): string |
132
|
|
|
{ |
133
|
2 |
|
$dynamicRoutes = $dynamicVar = $staticRoutes = []; |
134
|
2 |
|
$dynamicString = ":static function (string \$path): ?array {\n "; |
135
|
|
|
|
136
|
2 |
|
foreach ($collection->getRoutes() as $i => $route) { |
137
|
2 |
|
$trimmed = \preg_replace('/\W$/', '', $path = '/'.\ltrim($route['path'], '/')); |
138
|
|
|
|
139
|
2 |
|
if (\in_array($prefix = '/'.\ltrim($route['prefix'] ?? '/', '/') ?? '/', [$trimmed, $path], true)) { |
140
|
2 |
|
$staticRoutes[$trimmed ?: '/'][] = $i; |
141
|
2 |
|
continue; |
142
|
|
|
} |
143
|
2 |
|
[$path, $var] = $this->getCompiler()->compile($path, $route['placeholders'] ?? []); |
|
|
|
|
144
|
2 |
|
$path = \str_replace('\/', '/', \substr($path, 1 + \strpos($path, '^'), -(\strlen($path) - \strrpos($path, '$')))); |
145
|
|
|
|
146
|
2 |
|
if (($l = \array_key_last($dynamicRoutes)) && !\in_array($l, ['/', $prefix], true)) { |
147
|
2 |
|
for ($o = 0, $new = ''; $o < \strlen($prefix); ++$o) { |
148
|
2 |
|
if ($prefix[$o] !== ($l[$o] ?? null)) { |
149
|
2 |
|
break; |
150
|
|
|
} |
151
|
2 |
|
$new .= $l[$o]; |
152
|
|
|
} |
153
|
|
|
|
154
|
2 |
|
if ($new && '/' !== $new) { |
155
|
2 |
|
if ($l !== $new) { |
156
|
2 |
|
$dynamicRoutes[$new] = $dynamicRoutes[$l]; |
157
|
2 |
|
unset($dynamicRoutes[$l]); |
158
|
|
|
} |
159
|
2 |
|
$prefix = $new; |
160
|
|
|
} |
161
|
|
|
} |
162
|
2 |
|
$dynamicRoutes[$prefix][] = \preg_replace('#\?(?|P<\w+>|<\w+>|\'\w+\')#', '', $path)."(*:{$i})"; |
163
|
2 |
|
$dynamicVar[$i] = $var; |
164
|
|
|
} |
165
|
2 |
|
\ksort($staticRoutes, \SORT_NATURAL); |
166
|
2 |
|
\uksort($dynamicRoutes, fn (string $a, string $b): int => \in_array('/', [$a, $b], true) ? \strcmp($b, $a) : \strcmp($a, $b)); |
167
|
|
|
|
168
|
2 |
|
foreach ($dynamicRoutes as $offset => $paths) { |
169
|
2 |
|
$numParts = \max(1, \round(($c = \count($paths)) / 30)); |
170
|
2 |
|
$prefix = '/'.\ltrim($offset, '/'); |
171
|
2 |
|
$indent = ' '; |
172
|
2 |
|
$chunks = self::export(\array_map(fn (array $p): string => "~^(?|".implode('|', $p).")$~", \array_chunk($paths, (int) \ceil($c / $numParts))), $indent); |
173
|
|
|
$dynamicString .= <<<PHP |
174
|
|
|
if (\str_starts_with(\$path, '$prefix')) { foreach($chunks as \$p) { if (!\preg_match(\$p, \$path, \$m)) continue; return [\$m]; }} |
175
|
|
|
else |
176
|
|
|
PHP; |
177
|
|
|
} |
178
|
|
|
|
179
|
2 |
|
if (\str_ends_with($dynamicString, 'else')) { |
180
|
2 |
|
$dynamicString = \substr($dynamicString, 0, -4); |
181
|
|
|
} |
182
|
|
|
|
183
|
2 |
|
return self::export([$staticRoutes, [$dynamicString."return null;\n }:", $dynamicVar], $doCache ? $collection : null]); |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|