Test Failed
Branch master (effa58)
by Divine Niiquaye
02:13
created

RouteMatcher::match()   B

Complexity

Conditions 9
Paths 10

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 9
Bugs 1 Features 0
Metric Value
cc 9
eloc 15
nc 10
nop 2
dl 0
loc 28
rs 8.0555
c 9
b 1
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.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;
19
20
use Flight\Routing\Routes\{FastRoute as Route, Route as BaseRoute};
21
use Flight\Routing\Exceptions\{UriHandlerException, UrlGenerationException};
22
use Flight\Routing\Generator\GeneratedUri;
23
use Flight\Routing\Interfaces\{RouteCompilerInterface, RouteMatcherInterface};
24
use Psr\Http\Message\{ServerRequestInterface, UriInterface};
25
26
/**
27
 * The bidirectional route matcher responsible for matching
28
 * HTTP request and generating url from routes.
29
 *
30
 * @author Divine Niiquaye Ibok <[email protected]>
31
 */
32
final class RouteMatcher implements RouteMatcherInterface
33
{
34
    /** @var array<int,Route>|array<int,array<int,mixed>> */
35
    private array $routes;
36
37
    private RouteCompilerInterface $compiler;
38
39
    private ?string $generatedRegex = null;
40
41
    /**
42
     * @var callable
43
     *
44
     * @internal returns an optimised routes data
45
     */
46
    private $beforeSerialization = [Generator\RegexGenerator::class, 'beforeCaching'];
47
48
    public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null)
49
    {
50
        $this->compiler = $compiler ?? new RouteCompiler();
51
        $this->routes = $collection->getRoutes();
52
    }
53
54
    /**
55
     * @internal
56
     */
57
    public function __serialize(): array
58
    {
59
        return ($this->beforeSerialization)($this->compiler, $this->getRoutes());
60
    }
61
62
    /**
63
     * @internal
64
     *
65
     * @param array<int,mixed> $data
66
     */
67
    public function __unserialize(array $data): void
68
    {
69
        [$this->generatedRegex, $this->routes, $this->compiler] = $data;
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75
    public function matchRequest(ServerRequestInterface $request): ?Route
76
    {
77
        $requestUri = $request->getUri();
78
79
        // Resolve request path to match sub-directory or /index.php/path
80
        if ('' !== ($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '') && $pathInfo !== $requestUri->getPath()) {
81
            $requestUri = $requestUri->withPath($pathInfo);
82
        }
83
84
        return $this->match($request->getMethod(), $requestUri);
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function match(string $method, UriInterface $uri): ?Route
91
    {
92
        $requestPath = $uri->getPath();
93
94
        if (\array_key_exists($requestPath[-1], BaseRoute::URL_PREFIX_SLASHES)) {
95
            $requestPath = \substr($requestPath, 0, -1) ?: '/';
96
        }
97
98
        if (null === $this->generatedRegex) {
99
            /** @var Route $route */
100
            foreach ($this->routes as $route) {
101
                [$pathRegex, $hostsRegex, $variables] = $this->compiler->compile($route);
102
103
                if ($pathRegex === $requestPath || 1 === \preg_match('#^' . $pathRegex . '$#u', $requestPath, $matches)) {
104
                    if (empty($variables)) {
105
                        return $route->match($method, $uri);
106
                    }
107
108
                    return self::doMatch($route, $uri, $hostsRegex, [$method, $variables, $matches ?? []]);
109
                }
110
            }
111
        } elseif (1 === \preg_match($this->generatedRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
112
            [$route, $hostsRegex, $variables] = $this->routes[$matches['MARK']];
113
114
            return self::doMatch($route, $uri, $hostsRegex, [$method, $variables, \array_filter($matches)]);
115
        }
116
117
        return null;
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
124
    {
125
        foreach ($this->routes as $route) {
126
            if (!$route instanceof Route) {
127
                $route = $route[0];
128
            }
129
130
            if ($routeName === $route->get('name')) {
131
                return $this->compiler->generateUri($route, $parameters);
132
            }
133
        }
134
135
        throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
136
    }
137
138
    /**
139
     * Get the compiler associated with this class.
140
     */
141
    public function getCompiler(): RouteCompilerInterface
142
    {
143
        return $this->compiler;
144
    }
145
146
    /**
147
     * Get the routes associated with this class.
148
     *
149
     * @return Route[]
150
     */
151
    public function getRoutes(): array
152
    {
153
        $routes = $this->routes;
154
155
        if (null !== $this->generatedRegex) {
156
            \array_walk($routes, static function (&$data): void {
157
                $data = $data[0];
158
            });
159
        }
160
161
        return $routes;
162
    }
163
164
    /**
165
     * @param array<int,mixed> $routeData
166
     */
167
    private static function doMatch(Route $route, UriInterface $uri, ?string $hostsRegex, array $routeData): Route
168
    {
169
        [$method, $variables, $matches] = $routeData;
170
        $matchVar = 0;
171
172
        if (!empty($hostsRegex)) {
173
            $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
174
175
            if (1 !== \preg_match('#^' . $hostsRegex . '$#i', $hostAndPost, $hostsVar)) {
176
                throw new UriHandlerException(\sprintf('Unfortunately current host "%s" is not allowed on requested path [%s].', $hostAndPost, $uri->getPath()), 400);
177
            }
178
        }
179
180
        foreach ($variables as $key => $value) {
181
            $route->argument($key, $matches[++$matchVar] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
182
        }
183
184
        return $route->match($method, $uri);
185
    }
186
}
187