Skip to content

Commit 741f695

Browse files
committed
Refactor route method aliases to routes API
1 parent 21793e3 commit 741f695

6 files changed

Lines changed: 85 additions & 93 deletions

File tree

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ curl http://localhost:8000/hello-world?name=Appwrite
143143

144144
It's always recommended to use params instead of getting params or body directly from the request resource. If you do that intentionally, always make sure to run validation right after fetching such a raw input.
145145

146-
### Aliases
146+
### Aliases and Multiple Methods
147147

148-
A route can be registered under additional paths and HTTP methods using aliases. All aliases dispatch to the same route, so the action, params, and hooks are defined only once.
148+
A route can be registered under additional paths and multiple HTTP methods. All matching paths and methods dispatch to the same route, so the action, params, and hooks are defined only once.
149149

150150
Use `alias()` to serve the same route under another path, for example to keep a legacy URL working:
151151

@@ -159,11 +159,10 @@ Http::get('/users/:id')
159159
});
160160
```
161161

162-
Use `aliasMethod()` to serve the same route under another HTTP method. For example, the OpenID Connect UserInfo endpoint must support both GET and POST:
162+
Use `routes()` to serve the same route under multiple HTTP methods. For example, the OpenID Connect UserInfo endpoint must support both GET and POST:
163163

164164
```php
165-
Http::get('/oauth/userinfo')
166-
->aliasMethod(Http::REQUEST_METHOD_POST)
165+
Http::routes([Http::REQUEST_METHOD_GET, Http::REQUEST_METHOD_POST], '/oauth/userinfo')
167166
->inject('request')
168167
->inject('response')
169168
->action(function(Request $request, Response $response) {
@@ -172,7 +171,7 @@ Http::get('/oauth/userinfo')
172171
});
173172
```
174173

175-
Path and method aliases combine: a route with both responds on every method under every path. Note that `getMethod()` on the route keeps returning the primary method it was defined with; use the request resource to tell how a request arrived.
174+
Path aliases and multiple methods combine: a route with both responds on every method under every path. Note that `getMethod()` on the route returns the first method it was defined with; use the request resource to tell how a request arrived.
176175

177176
### Hooks
178177

src/Http/Http.php

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ public function setCompressionSupported(mixed $compressionSupported): void
179179
*/
180180
public static function get(string $url): Route
181181
{
182-
return self::addRoute(self::REQUEST_METHOD_GET, $url);
182+
return self::routes(self::REQUEST_METHOD_GET, $url);
183183
}
184184

185185
/**
@@ -189,7 +189,7 @@ public static function get(string $url): Route
189189
*/
190190
public static function post(string $url): Route
191191
{
192-
return self::addRoute(self::REQUEST_METHOD_POST, $url);
192+
return self::routes(self::REQUEST_METHOD_POST, $url);
193193
}
194194

195195
/**
@@ -199,7 +199,7 @@ public static function post(string $url): Route
199199
*/
200200
public static function put(string $url): Route
201201
{
202-
return self::addRoute(self::REQUEST_METHOD_PUT, $url);
202+
return self::routes(self::REQUEST_METHOD_PUT, $url);
203203
}
204204

205205
/**
@@ -209,7 +209,7 @@ public static function put(string $url): Route
209209
*/
210210
public static function patch(string $url): Route
211211
{
212-
return self::addRoute(self::REQUEST_METHOD_PATCH, $url);
212+
return self::routes(self::REQUEST_METHOD_PATCH, $url);
213213
}
214214

215215
/**
@@ -219,7 +219,29 @@ public static function patch(string $url): Route
219219
*/
220220
public static function delete(string $url): Route
221221
{
222-
return self::addRoute(self::REQUEST_METHOD_DELETE, $url);
222+
return self::routes(self::REQUEST_METHOD_DELETE, $url);
223+
}
224+
225+
/**
226+
* ROUTES
227+
*
228+
* Add one route under one or more request methods
229+
*
230+
* @param string|array<int, string> $methods
231+
*/
232+
public static function routes(string|array $methods, string $url): Route
233+
{
234+
$methods = \is_array($methods) ? $methods : [$methods];
235+
$methods = array_values(array_unique($methods));
236+
237+
if (empty($methods)) {
238+
throw new \Exception('At least one HTTP method is required.');
239+
}
240+
241+
$route = new Route($methods[0], $url, \array_slice($methods, 1));
242+
Router::addRoute($route);
243+
244+
return $route;
223245
}
224246

225247
/**

src/Http/Route.php

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ class Route extends Hook
3636
protected array $aliasPaths = [];
3737

3838
/**
39-
* Alias HTTP methods this route is also registered under.
39+
* Additional HTTP methods this route is also registered under.
4040
*
4141
* @var array<string>
4242
*/
43-
protected array $aliasMethods = [];
43+
protected array $additionalMethods = [];
4444

4545
/**
4646
* Internal counter.
@@ -52,11 +52,15 @@ class Route extends Hook
5252
*/
5353
protected int $order;
5454

55-
public function __construct(string $method, string $path)
55+
/**
56+
* @param array<int, string> $additionalMethods
57+
*/
58+
public function __construct(string $method, string $path, array $additionalMethods = [])
5659
{
5760
parent::__construct();
5861
$this->path($path);
5962
$this->method = $method;
63+
$this->additionalMethods = $additionalMethods;
6064
$this->order = ++self::$counter;
6165
}
6266

@@ -85,7 +89,7 @@ public function alias(string $path): self
8589
{
8690
Router::addRouteAlias($path, $this);
8791

88-
foreach ($this->aliasMethods as $method) {
92+
foreach ($this->additionalMethods as $method) {
8993
Router::addRouteAlias($path, $this, $method);
9094
}
9195

@@ -96,27 +100,6 @@ public function alias(string $path): self
96100
return $this;
97101
}
98102

99-
/**
100-
* Register this route under an additional HTTP method.
101-
*
102-
* The route keeps reporting its primary method via getMethod();
103-
* use the request's method to tell how a request arrived.
104-
*/
105-
public function aliasMethod(string $method): self
106-
{
107-
Router::addRouteMethodAlias($method, $this);
108-
109-
foreach ($this->aliasPaths as $path) {
110-
Router::addRouteAlias($path, $this, $method);
111-
}
112-
113-
if (!\in_array($method, $this->aliasMethods, true)) {
114-
$this->aliasMethods[] = $method;
115-
}
116-
117-
return $this;
118-
}
119-
120103
/**
121104
* When set false, hooks for this route will be skipped.
122105
*/
@@ -162,13 +145,13 @@ public function getAliasPaths(): array
162145
}
163146

164147
/**
165-
* Get alias methods.
148+
* Get methods this route is registered under.
166149
*
167150
* @return array<string>
168151
*/
169-
public function getAliasMethods(): array
152+
public function getMethods(): array
170153
{
171-
return $this->aliasMethods;
154+
return array_merge([$this->method], $this->additionalMethods);
172155
}
173156

174157
/**

src/Http/Router.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ public static function addRoute(Route $route): void
8888
}
8989

9090
self::$routes[$route->getMethod()][$path] = $route;
91+
92+
foreach (\array_slice($route->getMethods(), 1) as $method) {
93+
self::addRouteMethod($method, $route);
94+
}
9195
}
9296

9397
/**
@@ -121,14 +125,14 @@ public static function addRouteAlias(string $path, Route $route, ?string $method
121125
*
122126
* @throws \Exception
123127
*/
124-
public static function addRouteMethodAlias(string $method, Route $route): void
128+
public static function addRouteMethod(string $method, Route $route): void
125129
{
126130
if (!\array_key_exists($method, self::$routes)) {
127131
throw new Exception("Method ({$method}) not supported.");
128132
}
129133

130134
if ($route->getPath() === '') {
131-
throw new Exception('Method aliases are not supported for the wildcard route.');
135+
throw new Exception('Additional route methods are not supported for the wildcard route.');
132136
}
133137

134138
[$path, $params] = self::preparePath($route->getPath());

tests/HttpTest.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,9 @@ public function testCanExecuteRoute(): void
250250
$this->assertSame('init-' . $resource . '-(init-homepage)-param-x*param-y-(shutdown-homepage)-shutdown', $result);
251251
}
252252

253-
public function testCanExecuteRouteWithMethodAlias(): void
253+
public function testCanExecuteRouteWithMultipleMethods(): void
254254
{
255-
$route = Http::get('/v1/oauth/userinfo');
256-
257-
$route
258-
->aliasMethod(Http::REQUEST_METHOD_POST)
255+
Http::routes([Http::REQUEST_METHOD_GET, Http::REQUEST_METHOD_POST], '/v1/oauth/userinfo')
259256
->inject('request')
260257
->action(function ($request) {
261258
echo 'userinfo:' . $request->getMethod();

tests/RouterTest.php

Lines changed: 34 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -142,104 +142,91 @@ public function testCanMatchMix(): void
142142
$this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_GET, '/register/lorem/ipsum')?->route);
143143
}
144144

145-
public function testCanMatchMethodAlias(): void
145+
public function testCanMatchRouteWithMultipleMethods(): void
146146
{
147-
$route = new Route(Http::REQUEST_METHOD_GET, '/userinfo');
148-
$route->aliasMethod(Http::REQUEST_METHOD_POST);
149-
150-
Router::addRoute($route);
147+
$route = Http::routes([Http::REQUEST_METHOD_GET, Http::REQUEST_METHOD_POST], '/userinfo');
151148

152149
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_GET, '/userinfo')?->route);
153150
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_POST, '/userinfo')?->route);
154151
$this->assertNull(Router::match(Http::REQUEST_METHOD_PUT, '/userinfo'));
155152

156153
$this->assertSame(Http::REQUEST_METHOD_GET, $route->getMethod());
157-
$this->assertSame([Http::REQUEST_METHOD_POST], $route->getAliasMethods());
154+
$this->assertSame([Http::REQUEST_METHOD_GET, Http::REQUEST_METHOD_POST], $route->getMethods());
158155
}
159156

160-
public function testCanMatchMethodAliasWithPlaceholder(): void
157+
public function testCanMatchRouteWithStringMethod(): void
161158
{
162-
$route = new Route(Http::REQUEST_METHOD_GET, '/users/:id');
163-
$route->aliasMethod(Http::REQUEST_METHOD_POST);
159+
$route = Http::routes(Http::REQUEST_METHOD_GET, '/userinfo');
164160

165-
Router::addRoute($route);
161+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_GET, '/userinfo')?->route);
162+
$this->assertNull(Router::match(Http::REQUEST_METHOD_POST, '/userinfo'));
163+
$this->assertSame([Http::REQUEST_METHOD_GET], $route->getMethods());
164+
}
165+
166+
public function testCanMatchRouteWithMultipleMethodsAndPlaceholder(): void
167+
{
168+
$route = Http::routes([Http::REQUEST_METHOD_GET, Http::REQUEST_METHOD_POST], '/users/:id');
166169

167170
$match = Router::match(Http::REQUEST_METHOD_POST, '/users/abc-123');
168171

169172
$this->assertEquals($route, $match?->route);
170173
$this->assertSame(['id' => 'abc-123'], $match?->params);
171174
}
172175

173-
public function testMethodAliasCrossesPathAliasesRegardlessOfOrder(): void
176+
public function testRoutesCrossPathAliases(): void
174177
{
175-
$routeA = new Route(Http::REQUEST_METHOD_GET, '/a');
176-
$routeA
177-
->alias('/a-old')
178-
->aliasMethod(Http::REQUEST_METHOD_POST);
178+
$route = Http::routes([Http::REQUEST_METHOD_GET, Http::REQUEST_METHOD_POST], '/a')
179+
->alias('/a-old');
179180

180-
Router::addRoute($routeA);
181-
182-
$routeB = new Route(Http::REQUEST_METHOD_GET, '/b');
183-
$routeB
184-
->aliasMethod(Http::REQUEST_METHOD_POST)
185-
->alias('/b-old');
186-
187-
Router::addRoute($routeB);
188-
189-
$this->assertEquals($routeA, Router::match(Http::REQUEST_METHOD_POST, '/a')?->route);
190-
$this->assertEquals($routeA, Router::match(Http::REQUEST_METHOD_POST, '/a-old')?->route);
191-
$this->assertEquals($routeB, Router::match(Http::REQUEST_METHOD_POST, '/b')?->route);
192-
$this->assertEquals($routeB, Router::match(Http::REQUEST_METHOD_POST, '/b-old')?->route);
181+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_GET, '/a')?->route);
182+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_POST, '/a')?->route);
183+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_GET, '/a-old')?->route);
184+
$this->assertEquals($route, Router::match(Http::REQUEST_METHOD_POST, '/a-old')?->route);
193185
}
194186

195-
public function testCannotRegisterDuplicateMethodAlias(): void
187+
public function testCannotRegisterDuplicateRouteMethod(): void
196188
{
197189
$routePOST = new Route(Http::REQUEST_METHOD_POST, '/userinfo');
198190
Router::addRoute($routePOST);
199191

200-
$routeGET = new Route(Http::REQUEST_METHOD_GET, '/userinfo');
201-
202192
$this->expectException(\Exception::class);
203193
$this->expectExceptionMessage('Route for (POST:userinfo) already registered.');
204-
$routeGET->aliasMethod(Http::REQUEST_METHOD_POST);
194+
Http::routes([Http::REQUEST_METHOD_GET, Http::REQUEST_METHOD_POST], '/userinfo');
205195
}
206196

207-
public function testCanOverrideMethodAlias(): void
197+
public function testCanOverrideRouteMethod(): void
208198
{
209199
Router::setAllowOverride(true);
210200

211201
try {
212202
$routePOST = new Route(Http::REQUEST_METHOD_POST, '/userinfo');
213203
Router::addRoute($routePOST);
214204

215-
$routeGET = new Route(Http::REQUEST_METHOD_GET, '/userinfo');
216-
Router::addRoute($routeGET);
217-
$routeGET->aliasMethod(Http::REQUEST_METHOD_POST);
218-
$routeGET->aliasMethod(Http::REQUEST_METHOD_POST);
205+
$routeGET = Http::routes([
206+
Http::REQUEST_METHOD_GET,
207+
Http::REQUEST_METHOD_POST,
208+
Http::REQUEST_METHOD_POST,
209+
], '/userinfo');
219210

220211
$this->assertEquals($routeGET, Router::match(Http::REQUEST_METHOD_POST, '/userinfo')?->route);
221-
$this->assertSame([Http::REQUEST_METHOD_POST], $routeGET->getAliasMethods());
212+
$this->assertSame([Http::REQUEST_METHOD_GET, Http::REQUEST_METHOD_POST], $routeGET->getMethods());
222213
} finally {
223214
Router::setAllowOverride(false);
224215
}
225216
}
226217

227-
public function testCannotRegisterMethodAliasForUnknownMethod(): void
218+
public function testCannotRegisterRouteForUnknownMethod(): void
228219
{
229-
$route = new Route(Http::REQUEST_METHOD_GET, '/userinfo');
230-
231220
$this->expectException(\Exception::class);
232221
$this->expectExceptionMessage('Method (TRACE) not supported.');
233-
$route->aliasMethod('TRACE');
222+
Http::routes([Http::REQUEST_METHOD_GET, 'TRACE'], '/userinfo');
234223
}
235224

236-
public function testCannotRegisterMethodAliasOnWildcardRoute(): void
225+
public function testCannotRegisterRouteWithoutMethods(): void
237226
{
238-
$route = new Route('', '');
239-
240227
$this->expectException(\Exception::class);
241-
$this->expectExceptionMessage('Method aliases are not supported for the wildcard route.');
242-
$route->aliasMethod(Http::REQUEST_METHOD_POST);
228+
$this->expectExceptionMessage('At least one HTTP method is required.');
229+
Http::routes([], '/userinfo');
243230
}
244231

245232
public function testCanMatchFilename(): void

0 commit comments

Comments
 (0)