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

tempestphp / tempest-framework / 11656281982

02 Nov 2024 07:57PM UTC coverage: 82.594%. Remained the same
11656281982

push

github

web-flow
feat: route enum binding support (#668)

29 of 30 new or added lines in 3 files covered. (96.67%)

5 existing lines in 1 file now uncovered.

7127 of 8629 relevant lines covered (82.59%)

46.49 hits per line

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

93.18
/src/Tempest/Http/src/GenericRouter.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Http;
6

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

27
/**
28
 * @template MiddlewareClass of \Tempest\Http\HttpMiddleware
29
 */
30
final class GenericRouter implements Router
31
{
32
    /** @var class-string<MiddlewareClass>[] */
33
    private array $middleware = [];
34

35
    public function __construct(
326✔
36
        private readonly Container $container,
37
        private readonly RouteMatcher $routeMatcher,
38
        private readonly AppConfig $appConfig,
39
    ) {
40
    }
326✔
41

42
    public function dispatch(Request|PsrRequest $request): Response
27✔
43
    {
44
        if (! $request instanceof PsrRequest) {
27✔
45
            $request = map($request)->with(RequestToPsrRequestMapper::class);
2✔
46
        }
47

48
        $matchedRoute = $this->routeMatcher->match($request);
27✔
49

50
        if ($matchedRoute === null) {
27✔
51
            return new NotFound();
1✔
52
        }
53

54
        $this->container->singleton(
26✔
55
            MatchedRoute::class,
26✔
56
            fn () => $matchedRoute,
26✔
57
        );
26✔
58

59
        $callable = $this->getCallable($matchedRoute);
26✔
60

61
        try {
62
            $request = $this->resolveRequest($request, $matchedRoute);
26✔
63
            $response = $callable($request);
24✔
64
        } catch (NotFoundException) {
3✔
65
            return new NotFound();
1✔
66
        } catch (ValidationException $validationException) {
2✔
67
            return new Invalid($request, $validationException->failingRules);
2✔
68
        }
69

70
        if ($response === null) {
24✔
UNCOV
71
            throw new MissingControllerOutputException(
×
UNCOV
72
                $matchedRoute->route->handler->getDeclaringClass()->getName(),
×
UNCOV
73
                $matchedRoute->route->handler->getName(),
×
74
            );
×
75
        }
76

77
        return $response;
24✔
78
    }
79

80
    private function getCallable(MatchedRoute $matchedRoute): Closure
26✔
81
    {
82
        $route = $matchedRoute->route;
26✔
83

84
        $callControllerAction = function (Request $request) use ($route, $matchedRoute) {
26✔
85
            $response = $this->container->invoke(
24✔
86
                $route->handler,
24✔
87
                ...$matchedRoute->params,
24✔
88
            );
24✔
89

90
            if ($response === null) {
24✔
UNCOV
91
                throw new ControllerActionHasNoReturn($route);
×
92
            }
93

94
            return $response;
24✔
95
        };
26✔
96

97
        $callable = fn (Request $request) => $this->createResponse($callControllerAction($request));
26✔
98

99
        $middlewareStack = [...$this->middleware, ...$route->middleware];
26✔
100

101
        while ($middlewareClass = array_pop($middlewareStack)) {
26✔
102
            $callable = function (Request $request) use ($middlewareClass, $callable) {
21✔
103
                /** @var HttpMiddleware $middleware */
104
                $middleware = $this->container->get($middlewareClass);
19✔
105

106
                return $middleware($request, $callable);
19✔
107
            };
21✔
108
        }
109

110
        return $callable;
26✔
111
    }
112

113
    public function toUri(array|string $action, ...$params): string
14✔
114
    {
115
        try {
116
            if (is_array($action)) {
14✔
117
                $controllerClass = $action[0];
9✔
118
                $reflection = new ClassReflector($controllerClass);
9✔
119
                $controllerMethod = $reflection->getMethod($action[1]);
9✔
120
            } else {
121
                $controllerClass = $action;
6✔
122
                $reflection = new ClassReflector($controllerClass);
6✔
123
                $controllerMethod = $reflection->getMethod('__invoke');
6✔
124
            }
125

126
            $routeAttribute = $controllerMethod->getAttribute(Route::class);
14✔
127

128
            $uri = $routeAttribute->uri;
14✔
129
        } catch (ReflectionException) {
1✔
130
            if (is_array($action)) {
1✔
UNCOV
131
                throw new InvalidRouteException($action[0], $action[1]);
×
132
            }
133

134
            $uri = $action;
1✔
135
        }
136

137
        $queryParams = [];
14✔
138

139

140
        foreach ($params as $key => $value) {
14✔
141
            if (! str_contains($uri, "{$key}")) {
7✔
142
                $queryParams[$key] = $value;
1✔
143

144
                continue;
1✔
145
            }
146

147
            if ($value instanceof BackedEnum) {
7✔
148
                $value = $value->value;
1✔
149
            }
150

151
            $pattern = '#\{' . $key . Route::ROUTE_PARAM_CUSTOM_REGEX . '\}#';
7✔
152
            $uri = preg_replace($pattern, (string)$value, $uri);
7✔
153
        }
154

155
        $uri = rtrim($this->appConfig->baseUri, '/') . $uri;
14✔
156

157
        if ($queryParams !== []) {
14✔
158
            return $uri . '?' . http_build_query($queryParams);
1✔
159
        }
160

161
        return $uri;
14✔
162
    }
163

164
    private function createResponse(Response|View $input): Response
24✔
165
    {
166
        if ($input instanceof View) {
24✔
167
            return new Ok($input);
7✔
168
        }
169

170
        return $input;
17✔
171
    }
172

173
    private function resolveRequest(PsrRequest $psrRequest, MatchedRoute $matchedRoute): Request
26✔
174
    {
175
        // Let's find out if our input request data matches what the route's action needs
176
        $requestClass = GenericRequest::class;
26✔
177

178
        // We'll loop over all the handler's parameters
179
        foreach ($matchedRoute->route->handler->getParameters() as $parameter) {
26✔
180

181
            // If the parameter's type is an instance of Request…
182
            if ($parameter->getType()->matches(Request::class)) {
18✔
183
                // We'll use that specific request class
184
                $requestClass = $parameter->getType()->getName();
10✔
185

186
                break;
10✔
187
            }
188
        }
189

190
        // We map the original request we got into this method to the right request class
191
        /** @var Request $request */
192
        $request = map($psrRequest)->to($requestClass);
26✔
193

194
        // Next, we register this newly created request object in the container
195
        // This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class
196
        // Making it so that we don't need to set any $_SERVER variables and stuff like that
197
        $this->container->singleton(Request::class, fn () => $request);
24✔
198
        $this->container->singleton($request::class, fn () => $request);
24✔
199

200
        // Finally, we validate the request
201
        $request->validate();
24✔
202

203
        return $request;
24✔
204
    }
205

206
    public function addMiddleware(string $middlewareClass): void
326✔
207
    {
208
        $this->middleware[] = $middlewareClass;
326✔
209
    }
210
}
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