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

RouteMatcher::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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