• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tempestphp / tempest-framework / 14073185847

25 Mar 2025 06:56PM UTC coverage: 79.311% (-0.04%) from 79.353%
14073185847

push

github

web-flow
feat(database): store default sqlite database in internal storage (#1075)

1 of 1 new or added line in 1 file covered. (100.0%)

12 existing lines in 2 files now uncovered.

10500 of 13239 relevant lines covered (79.31%)

91.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.0
/src/Tempest/Router/src/GenericRouter.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Router;
6

7
use BackedEnum;
8
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
9
use ReflectionException;
10
use Tempest\Container\Container;
11
use Tempest\Core\AppConfig;
12
use Tempest\Reflection\ClassReflector;
13
use Tempest\Router\Exceptions\ControllerActionHasNoReturn;
14
use Tempest\Router\Exceptions\InvalidRouteException;
15
use Tempest\Router\Exceptions\NotFoundException;
16
use Tempest\Router\Mappers\PsrRequestToGenericRequestMapper;
17
use Tempest\Router\Mappers\RequestToObjectMapper;
18
use Tempest\Router\Mappers\RequestToPsrRequestMapper;
19
use Tempest\Router\Responses\Invalid;
20
use Tempest\Router\Responses\NotFound;
21
use Tempest\Router\Responses\Ok;
22
use Tempest\Router\Routing\Construction\DiscoveredRoute;
23
use Tempest\Router\Routing\Matching\RouteMatcher;
24
use Tempest\Validation\Exceptions\ValidationException;
25
use Tempest\View\View;
26

27
use function Tempest\map;
28
use function Tempest\Support\Regex\replace;
29
use function Tempest\Support\str;
30

31
/**
32
 * @template MiddlewareClass of \Tempest\Router\HttpMiddleware
33
 */
34
final class GenericRouter implements Router
35
{
36
    /** @var class-string<MiddlewareClass>[] */
37
    private array $middleware = [];
38

39
    private bool $handleExceptions = true;
40

41
    public function __construct(
663✔
42
        private readonly Container $container,
43
        private readonly RouteMatcher $routeMatcher,
44
        private readonly AppConfig $appConfig,
45
    ) {}
663✔
46

47
    public function throwExceptions(): self
4✔
48
    {
49
        $this->handleExceptions = false;
4✔
50

51
        return $this;
4✔
52
    }
53

54
    public function dispatch(Request|PsrRequest $request): Response
48✔
55
    {
56
        if (! ($request instanceof PsrRequest)) {
48✔
57
            $request = map($request)->with(RequestToPsrRequestMapper::class)->do();
2✔
58
        }
59

60
        $matchedRoute = $this->routeMatcher->match($request);
48✔
61

62
        if ($matchedRoute === null) {
48✔
63
            return new NotFound();
9✔
64
        }
65

66
        $this->container->singleton(
39✔
67
            MatchedRoute::class,
39✔
68
            fn () => $matchedRoute,
39✔
69
        );
39✔
70

71
        $callable = $this->getCallable($matchedRoute);
39✔
72

73
        if ($this->handleExceptions) {
39✔
74
            try {
75
                $request = $this->resolveRequest($request, $matchedRoute);
35✔
76
                $response = $callable($request);
33✔
77
            } catch (NotFoundException) {
3✔
78
                return new NotFound();
1✔
79
            } catch (ValidationException $validationException) {
2✔
80
                return new Invalid($validationException->object, $validationException->failingRules);
2✔
81
            }
82
        } else {
83
            $request = $this->resolveRequest($request, $matchedRoute);
4✔
84
            $response = $callable($request);
4✔
85
        }
86

87
        return $response;
37✔
88
    }
89

90
    private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable
39✔
91
    {
92
        $route = $matchedRoute->route;
39✔
93

94
        $callControllerAction = function (Request $_) use ($route, $matchedRoute) {
39✔
95
            $response = $this->container->invoke(
37✔
96
                $route->handler,
37✔
97
                ...$matchedRoute->params,
37✔
98
            );
37✔
99

100
            if ($response === null) {
37✔
UNCOV
101
                throw new ControllerActionHasNoReturn($route);
×
102
            }
103

104
            return $response;
37✔
105
        };
39✔
106

107
        $callable = new HttpMiddlewareCallable(fn (Request $request) => $this->createResponse($callControllerAction($request)));
39✔
108

109
        $middlewareStack = [...$this->middleware, ...$route->middleware];
39✔
110

111
        while ($middlewareClass = array_pop($middlewareStack)) {
39✔
112
            $callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) {
34✔
113
                /** @var HttpMiddleware $middleware */
114
                $middleware = $this->container->get($middlewareClass);
32✔
115

116
                return $middleware($request, $callable);
32✔
117
            });
34✔
118
        }
119

120
        return $callable;
39✔
121
    }
122

123
    public function toUri(array|string $action, ...$params): string
19✔
124
    {
125
        try {
126
            if (is_array($action)) {
19✔
127
                $controllerClass = $action[0];
14✔
128
                $reflection = new ClassReflector($controllerClass);
14✔
129
                $controllerMethod = $reflection->getMethod($action[1]);
14✔
130
            } else {
131
                $controllerClass = $action;
7✔
132
                $reflection = new ClassReflector($controllerClass);
7✔
133
                $controllerMethod = $reflection->getMethod('__invoke');
7✔
134
            }
135

136
            /** @var Route|null $routeAttribute */
137
            $routeAttribute = $controllerMethod->getAttribute(Route::class);
19✔
138

139
            $uri = $routeAttribute->uri;
19✔
140
        } catch (ReflectionException) {
1✔
141
            if (is_array($action)) {
1✔
UNCOV
142
                throw new InvalidRouteException($action[0], $action[1]);
×
143
            }
144

145
            $uri = $action;
1✔
146
        }
147

148
        $uri = str($uri);
19✔
149
        $queryParams = [];
19✔
150

151
        foreach ($params as $key => $value) {
19✔
152
            if (! $uri->matches(sprintf('/\{%s(\}|:)/', $key))) {
11✔
153
                $queryParams[$key] = $value;
3✔
154

155
                continue;
3✔
156
            }
157

158
            if ($value instanceof BackedEnum) {
10✔
159
                $value = $value->value;
2✔
160
            }
161

162
            $uri = $uri->replaceRegex(
10✔
163
                '#\{' . $key . DiscoveredRoute::ROUTE_PARAM_CUSTOM_REGEX . '\}#',
10✔
164
                (string) $value,
10✔
165
            );
10✔
166
        }
167

168
        $uri = $uri->prepend(rtrim($this->appConfig->baseUri, '/'));
19✔
169

170
        if ($queryParams !== []) {
19✔
171
            return $uri->append('?' . http_build_query($queryParams))->toString();
3✔
172
        }
173

174
        return $uri->toString();
17✔
175
    }
176

177
    public function isCurrentUri(array|string $action, ...$params): bool
3✔
178
    {
179
        $matchedRoute = $this->container->get(MatchedRoute::class);
3✔
180
        $candidateUri = $this->toUri($action, ...[...$matchedRoute->params, ...$params]);
3✔
181
        $currentUri = $this->toUri([$matchedRoute->route->handler->getDeclaringClass(), $matchedRoute->route->handler->getName()]);
3✔
182

183
        foreach ($matchedRoute->params as $key => $value) {
3✔
184
            $currentUri = replace($currentUri, '/({' . preg_quote($key, '/') . '(?::.*?)?})/', $value);
2✔
185
        }
186

187
        return $currentUri === $candidateUri;
3✔
188
    }
189

190
    private function createResponse(Response|View $input): Response
37✔
191
    {
192
        if ($input instanceof View) {
37✔
193
            return new Ok($input);
8✔
194
        }
195

196
        return $input;
29✔
197
    }
198

199
    // TODO: could in theory be moved to a dynamic initializer
200
    private function resolveRequest(\Psr\Http\Message\ServerRequestInterface|\Tempest\Mapper\ObjectFactory $psrRequest, MatchedRoute $matchedRoute): Request
39✔
201
    {
202
        // Let's find out if our input request data matches what the route's action needs
203
        $requestClass = GenericRequest::class;
39✔
204

205
        // We'll loop over all the handler's parameters
206
        foreach ($matchedRoute->route->handler->getParameters() as $parameter) {
39✔
207
            // If the parameter's type is an instance of Request…
208
            if ($parameter->getType()->matches(Request::class)) {
21✔
209
                // We'll use that specific request class
210
                $requestClass = $parameter->getType()->getName();
11✔
211

212
                break;
11✔
213
            }
214
        }
215

216
        // We map the original request we got into this method to the right request class
217
        /** @var \Tempest\Router\GenericRequest $request */
218
        $request = map($psrRequest)->with(PsrRequestToGenericRequestMapper::class)->do();
39✔
219

220
        if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) {
39✔
221
            $request = map($request)->with(RequestToObjectMapper::class)->to($requestClass);
5✔
222
        }
223

224
        // Next, we register this newly created request object in the container
225
        // This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class
226
        // Making it so that we don't need to set any $_SERVER variables and stuff like that
227
        $this->container->singleton(Request::class, fn () => $request);
37✔
228
        $this->container->singleton($request::class, fn () => $request);
37✔
229

230
        return $request;
37✔
231
    }
232

233
    public function addMiddleware(string $middlewareClass): void
663✔
234
    {
235
        $this->middleware[] = $middlewareClass;
663✔
236
    }
237
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc