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

tempestphp / tempest-framework / 14140550176

28 Mar 2025 10:29PM UTC coverage: 80.716% (+1.4%) from 79.334%
14140550176

push

github

web-flow
feat(support): support `$default` on array `first` and `last` methods (#1096)

11 of 12 new or added lines in 2 files covered. (91.67%)

135 existing lines in 16 files now uncovered.

10941 of 13555 relevant lines covered (80.72%)

100.32 hits per line

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

98.17
/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(
707✔
42
        private readonly Container $container,
43
        private readonly RouteMatcher $routeMatcher,
44
        private readonly AppConfig $appConfig,
45
        private readonly RouteConfig $routeConfig,
46
    ) {}
707✔
47

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

52
        return $this;
4✔
53
    }
54

55
    public function dispatch(Request|PsrRequest $request): Response
50✔
56
    {
57
        return $this->processResponse(
50✔
58
            $this->processRequest($request),
50✔
59
        );
50✔
60
    }
61

62
    private function processRequest(Request|PsrRequest $request): Response
50✔
63
    {
64
        if (! ($request instanceof PsrRequest)) {
50✔
65
            $request = map($request)->with(RequestToPsrRequestMapper::class)->do();
2✔
66
        }
67

68
        $matchedRoute = $this->routeMatcher->match($request);
50✔
69

70
        if ($matchedRoute === null) {
50✔
71
            return new NotFound();
9✔
72
        }
73

74
        $this->container->singleton(
41✔
75
            MatchedRoute::class,
41✔
76
            fn () => $matchedRoute,
41✔
77
        );
41✔
78

79
        $callable = $this->getCallable($matchedRoute);
41✔
80

81
        if ($this->handleExceptions) {
41✔
82
            try {
83
                $request = $this->resolveRequest($request, $matchedRoute);
37✔
84
                $response = $callable($request);
35✔
85
            } catch (NotFoundException) {
3✔
86
                return new NotFound();
1✔
87
            } catch (ValidationException $validationException) {
2✔
88
                return new Invalid($validationException->object, $validationException->failingRules);
2✔
89
            }
90
        } else {
91
            $request = $this->resolveRequest($request, $matchedRoute);
4✔
92
            $response = $callable($request);
4✔
93
        }
94

95
        return $response;
39✔
96
    }
97

98
    private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable
41✔
99
    {
100
        $route = $matchedRoute->route;
41✔
101

102
        $callControllerAction = function (Request $_) use ($route, $matchedRoute) {
41✔
103
            $response = $this->container->invoke(
39✔
104
                $route->handler,
39✔
105
                ...$matchedRoute->params,
39✔
106
            );
39✔
107

108
            if ($response === null) {
39✔
UNCOV
109
                throw new ControllerActionHasNoReturn($route);
×
110
            }
111

112
            return $response;
39✔
113
        };
41✔
114

115
        $callable = new HttpMiddlewareCallable(fn (Request $request) => $this->createResponse($callControllerAction($request)));
41✔
116

117
        $middlewareStack = [...$this->middleware, ...$route->middleware];
41✔
118

119
        while ($middlewareClass = array_pop($middlewareStack)) {
41✔
120
            $callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) {
36✔
121
                /** @var HttpMiddleware $middleware */
122
                $middleware = $this->container->get($middlewareClass);
34✔
123

124
                return $middleware($request, $callable);
34✔
125
            });
36✔
126
        }
127

128
        return $callable;
41✔
129
    }
130

131
    public function toUri(array|string $action, ...$params): string
19✔
132
    {
133
        try {
134
            if (is_array($action)) {
19✔
135
                $controllerClass = $action[0];
14✔
136
                $reflection = new ClassReflector($controllerClass);
14✔
137
                $controllerMethod = $reflection->getMethod($action[1]);
14✔
138
            } else {
139
                $controllerClass = $action;
7✔
140
                $reflection = new ClassReflector($controllerClass);
7✔
141
                $controllerMethod = $reflection->getMethod('__invoke');
7✔
142
            }
143

144
            /** @var Route|null $routeAttribute */
145
            $routeAttribute = $controllerMethod->getAttribute(Route::class);
19✔
146

147
            $uri = $routeAttribute->uri;
19✔
148
        } catch (ReflectionException) {
1✔
149
            if (is_array($action)) {
1✔
UNCOV
150
                throw new InvalidRouteException($action[0], $action[1]);
×
151
            }
152

153
            $uri = $action;
1✔
154
        }
155

156
        $uri = str($uri);
19✔
157
        $queryParams = [];
19✔
158

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

163
                continue;
3✔
164
            }
165

166
            if ($value instanceof BackedEnum) {
10✔
167
                $value = $value->value;
2✔
168
            }
169

170
            $uri = $uri->replaceRegex(
10✔
171
                '#\{' . $key . DiscoveredRoute::ROUTE_PARAM_CUSTOM_REGEX . '\}#',
10✔
172
                (string) $value,
10✔
173
            );
10✔
174
        }
175

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

178
        if ($queryParams !== []) {
19✔
179
            return $uri->append('?' . http_build_query($queryParams))->toString();
3✔
180
        }
181

182
        return $uri->toString();
17✔
183
    }
184

185
    public function isCurrentUri(array|string $action, ...$params): bool
3✔
186
    {
187
        $matchedRoute = $this->container->get(MatchedRoute::class);
3✔
188
        $candidateUri = $this->toUri($action, ...[...$matchedRoute->params, ...$params]);
3✔
189
        $currentUri = $this->toUri([$matchedRoute->route->handler->getDeclaringClass(), $matchedRoute->route->handler->getName()]);
3✔
190

191
        foreach ($matchedRoute->params as $key => $value) {
3✔
192
            $currentUri = replace($currentUri, '/({' . preg_quote($key, '/') . '(?::.*?)?})/', $value);
2✔
193
        }
194

195
        return $currentUri === $candidateUri;
3✔
196
    }
197

198
    private function createResponse(string|array|Response|View $input): Response
39✔
199
    {
200
        if ($input instanceof View || is_array($input) || is_string($input)) {
39✔
201
            return new Ok($input);
10✔
202
        }
203

204
        return $input;
29✔
205
    }
206

207
    private function processResponse(Response $response): Response
50✔
208
    {
209
        foreach ($this->routeConfig->responseProcessors as $responseProcessorClass) {
50✔
210
            /** @var \Tempest\Router\ResponseProcessor $responseProcessor */
211
            $responseProcessor = $this->container->get($responseProcessorClass);
50✔
212

213
            $response = $responseProcessor->process($response);
50✔
214
        }
215

216
        return $response;
50✔
217
    }
218

219
    // TODO: could in theory be moved to a dynamic initializer
220
    private function resolveRequest(\Psr\Http\Message\ServerRequestInterface|\Tempest\Mapper\ObjectFactory $psrRequest, MatchedRoute $matchedRoute): Request
41✔
221
    {
222
        // Let's find out if our input request data matches what the route's action needs
223
        $requestClass = GenericRequest::class;
41✔
224

225
        // We'll loop over all the handler's parameters
226
        foreach ($matchedRoute->route->handler->getParameters() as $parameter) {
41✔
227
            // If the parameter's type is an instance of Request…
228
            if ($parameter->getType()->matches(Request::class)) {
23✔
229
                // We'll use that specific request class
230
                $requestClass = $parameter->getType()->getName();
13✔
231

232
                break;
13✔
233
            }
234
        }
235

236
        // We map the original request we got into this method to the right request class
237
        /** @var \Tempest\Router\GenericRequest $request */
238
        $request = map($psrRequest)->with(PsrRequestToGenericRequestMapper::class)->do();
41✔
239

240
        if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) {
41✔
241
            $request = map($request)->with(RequestToObjectMapper::class)->to($requestClass);
5✔
242
        }
243

244
        // Next, we register this newly created request object in the container
245
        // This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class
246
        // Making it so that we don't need to set any $_SERVER variables and stuff like that
247
        $this->container->singleton(Request::class, fn () => $request);
39✔
248
        $this->container->singleton($request::class, fn () => $request);
39✔
249

250
        return $request;
39✔
251
    }
252

253
    public function addMiddleware(string $middlewareClass): void
707✔
254
    {
255
        $this->middleware[] = $middlewareClass;
707✔
256
    }
257
}
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