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

tempestphp / tempest-framework / 11714560237

06 Nov 2024 06:46PM UTC coverage: 82.608% (+0.003%) from 82.605%
11714560237

push

github

web-flow
feat(container): support injecting properties using `#[Inject]` (#690)

6 of 6 new or added lines in 2 files covered. (100.0%)

95 existing lines in 9 files now uncovered.

7210 of 8728 relevant lines covered (82.61%)

49.34 hits per line

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

97.67
/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 Psr\Http\Message\ServerRequestInterface as PsrRequest;
9
use ReflectionException;
10
use Tempest\Container\Container;
11
use Tempest\Core\AppConfig;
12
use Tempest\Http\Exceptions\ControllerActionHasNoReturn;
13
use Tempest\Http\Exceptions\InvalidRouteException;
14
use Tempest\Http\Exceptions\NotFoundException;
15
use Tempest\Http\Mappers\RequestToPsrRequestMapper;
16
use Tempest\Http\Responses\Invalid;
17
use Tempest\Http\Responses\NotFound;
18
use Tempest\Http\Responses\Ok;
19
use Tempest\Http\Routing\Matching\RouteMatcher;
20
use function Tempest\map;
21
use Tempest\Reflection\ClassReflector;
22
use function Tempest\Support\str;
23
use Tempest\Validation\Exceptions\ValidationException;
24
use Tempest\View\View;
25

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

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

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

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

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

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

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

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

69
        return $response;
24✔
70
    }
71

72
    private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable
26✔
73
    {
74
        $route = $matchedRoute->route;
26✔
75

76
        $callControllerAction = function (Request $request) use ($route, $matchedRoute) {
26✔
77
            $response = $this->container->invoke(
24✔
78
                $route->handler,
24✔
79
                ...$matchedRoute->params,
24✔
80
            );
24✔
81

82
            if ($response === null) {
24✔
UNCOV
83
                throw new ControllerActionHasNoReturn($route);
×
84
            }
85

86
            return $response;
24✔
87
        };
26✔
88

89
        $callable = new HttpMiddlewareCallable(fn (Request $request) => $this->createResponse($callControllerAction($request)));
26✔
90

91
        $middlewareStack = [...$this->middleware, ...$route->middleware];
26✔
92

93
        while ($middlewareClass = array_pop($middlewareStack)) {
26✔
94
            $callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) {
21✔
95
                /** @var HttpMiddleware $middleware */
96
                $middleware = $this->container->get($middlewareClass);
19✔
97

98
                return $middleware($request, $callable);
19✔
99
            });
21✔
100
        }
101

102
        return $callable;
26✔
103
    }
104

105
    public function toUri(array|string $action, ...$params): string
16✔
106
    {
107
        try {
108
            if (is_array($action)) {
16✔
109
                $controllerClass = $action[0];
10✔
110
                $reflection = new ClassReflector($controllerClass);
10✔
111
                $controllerMethod = $reflection->getMethod($action[1]);
10✔
112
            } else {
113
                $controllerClass = $action;
7✔
114
                $reflection = new ClassReflector($controllerClass);
7✔
115
                $controllerMethod = $reflection->getMethod('__invoke');
7✔
116
            }
117

118
            $routeAttribute = $controllerMethod->getAttribute(Route::class);
16✔
119

120
            $uri = $routeAttribute->uri;
16✔
121
        } catch (ReflectionException) {
1✔
122
            if (is_array($action)) {
1✔
UNCOV
123
                throw new InvalidRouteException($action[0], $action[1]);
×
124
            }
125

126
            $uri = $action;
1✔
127
        }
128

129
        $uri = str($uri);
16✔
130
        $queryParams = [];
16✔
131

132
        foreach ($params as $key => $value) {
16✔
133
            if (! $uri->matches(sprintf('/\{%s(\}|:)/', $key))) {
9✔
134
                $queryParams[$key] = $value;
3✔
135

136
                continue;
3✔
137
            }
138

139
            if ($value instanceof BackedEnum) {
8✔
140
                $value = $value->value;
1✔
141
            }
142

143
            $uri = $uri->replaceRegex(
8✔
144
                '#\{' . $key . Route::ROUTE_PARAM_CUSTOM_REGEX . '\}#',
8✔
145
                (string)$value
8✔
146
            );
8✔
147
        }
148

149
        $uri = $uri->prepend(rtrim($this->appConfig->baseUri, '/'));
16✔
150

151
        if ($queryParams !== []) {
16✔
152
            return $uri->append('?' . http_build_query($queryParams))->toString();
3✔
153
        }
154

155
        return $uri->toString();
14✔
156
    }
157

158
    private function createResponse(Response|View $input): Response
24✔
159
    {
160
        if ($input instanceof View) {
24✔
161
            return new Ok($input);
7✔
162
        }
163

164
        return $input;
17✔
165
    }
166

167
    private function resolveRequest(PsrRequest $psrRequest, MatchedRoute $matchedRoute): Request
26✔
168
    {
169
        // Let's find out if our input request data matches what the route's action needs
170
        $requestClass = GenericRequest::class;
26✔
171

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

175
            // If the parameter's type is an instance of Request…
176
            if ($parameter->getType()->matches(Request::class)) {
18✔
177
                // We'll use that specific request class
178
                $requestClass = $parameter->getType()->getName();
10✔
179

180
                break;
10✔
181
            }
182
        }
183

184
        // We map the original request we got into this method to the right request class
185
        /** @var Request $request */
186
        $request = map($psrRequest)->to($requestClass);
26✔
187

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

194
        // Finally, we validate the request
195
        $request->validate();
24✔
196

197
        return $request;
24✔
198
    }
199

200
    public function addMiddleware(string $middlewareClass): void
340✔
201
    {
202
        $this->middleware[] = $middlewareClass;
340✔
203
    }
204
}
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