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

tempestphp / tempest-framework / 14049246919

24 Mar 2025 09:42PM UTC coverage: 79.353% (-0.04%) from 79.391%
14049246919

push

github

web-flow
feat(support): support array parameters in string manipulations (#1073)

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

735 existing lines in 126 files now uncovered.

10492 of 13222 relevant lines covered (79.35%)

90.78 hits per line

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

97.85
/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\str;
29

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

38
    private bool $handleExceptions = true;
39

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

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

50
        return $this;
4✔
51
    }
52

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

59
        $matchedRoute = $this->routeMatcher->match($request);
45✔
60

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

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

70
        $callable = $this->getCallable($matchedRoute);
36✔
71

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

86
        return $response;
34✔
87
    }
88

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

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

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

103
            return $response;
34✔
104
        };
36✔
105

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

108
        $middlewareStack = [...$this->middleware, ...$route->middleware];
36✔
109

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

115
                return $middleware($request, $callable);
29✔
116
            });
31✔
117
        }
118

119
        return $callable;
36✔
120
    }
121

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

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

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

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

147
        $uri = str($uri);
16✔
148
        $queryParams = [];
16✔
149

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

154
                continue;
3✔
155
            }
156

157
            if ($value instanceof BackedEnum) {
7✔
158
                $value = $value->value;
1✔
159
            }
160

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

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

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

173
        return $uri->toString();
14✔
174
    }
175

176
    private function createResponse(Response|View $input): Response
34✔
177
    {
178
        if ($input instanceof View) {
34✔
179
            return new Ok($input);
8✔
180
        }
181

182
        return $input;
26✔
183
    }
184

185
    // TODO: could in theory be moved to a dynamic initializer
186
    private function resolveRequest(\Psr\Http\Message\ServerRequestInterface|\Tempest\Mapper\ObjectFactory $psrRequest, MatchedRoute $matchedRoute): Request
36✔
187
    {
188
        // Let's find out if our input request data matches what the route's action needs
189
        $requestClass = GenericRequest::class;
36✔
190

191
        // We'll loop over all the handler's parameters
192
        foreach ($matchedRoute->route->handler->getParameters() as $parameter) {
36✔
193
            // If the parameter's type is an instance of Request…
194
            if ($parameter->getType()->matches(Request::class)) {
19✔
195
                // We'll use that specific request class
196
                $requestClass = $parameter->getType()->getName();
11✔
197

198
                break;
11✔
199
            }
200
        }
201

202
        // We map the original request we got into this method to the right request class
203
        /** @var \Tempest\Router\GenericRequest $request */
204
        $request = map($psrRequest)->with(PsrRequestToGenericRequestMapper::class)->do();
36✔
205

206
        if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) {
36✔
207
            $request = map($request)->with(RequestToObjectMapper::class)->to($requestClass);
5✔
208
        }
209

210
        // Next, we register this newly created request object in the container
211
        // This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class
212
        // Making it so that we don't need to set any $_SERVER variables and stuff like that
213
        $this->container->singleton(Request::class, fn () => $request);
34✔
214
        $this->container->singleton($request::class, fn () => $request);
34✔
215

216
        return $request;
34✔
217
    }
218

219
    public function addMiddleware(string $middlewareClass): void
660✔
220
    {
221
        $this->middleware[] = $middlewareClass;
660✔
222
    }
223
}
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