diff --git a/composer.json b/composer.json index 5675f8c..3517ba0 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "require": { "php": ">=8.3", "utopia-php/di": "0.3.*", - "utopia-php/servers": "0.3.*", + "utopia-php/servers": "0.4.0", "utopia-php/compression": "0.1.*", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" diff --git a/composer.lock b/composer.lock index b88d836..f617c48 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ffcf218a03a3c0e154ccd9029a626747", + "content-hash": "288cea8b8a10ec454331ba22f82753fa", "packages": [ { "name": "brick/math", @@ -1966,16 +1966,16 @@ }, { "name": "utopia-php/servers", - "version": "0.3.0", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/servers.git", - "reference": "235be31200df9437fc96a1c270ffef4c64fafe52" + "reference": "7db346ef377503efe0acafe0791085270cd9ed70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52", - "reference": "235be31200df9437fc96a1c270ffef4c64fafe52", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/7db346ef377503efe0acafe0791085270cd9ed70", + "reference": "7db346ef377503efe0acafe0791085270cd9ed70", "shasum": "" }, "require": { @@ -2014,9 +2014,9 @@ ], "support": { "issues": "https://github.com/utopia-php/servers/issues", - "source": "https://github.com/utopia-php/servers/tree/0.3.0" + "source": "https://github.com/utopia-php/servers/tree/0.4.0" }, - "time": "2026-03-13T11:31:42+00:00" + "time": "2026-05-05T04:08:30+00:00" }, { "name": "utopia-php/telemetry", diff --git a/src/Http/Http.php b/src/Http/Http.php index 74965fc..e059a91 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -699,15 +699,35 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) { $arguments = []; foreach ($hook->getParams() as $key => $param) { // Get value from route or request object - $existsInRequest = \array_key_exists($key, $requestParams); - $existsInValues = \array_key_exists($key, $values); + $requestKey = $key; + if (!\array_key_exists($key, $requestParams) && !empty($param['aliases'])) { + foreach ($param['aliases'] as $alias) { + if (\array_key_exists($alias, $requestParams)) { + $requestKey = $alias; + break; + } + } + } + + $valuesKey = $key; + if (!\array_key_exists($key, $values) && !empty($param['aliases'])) { + foreach ($param['aliases'] as $alias) { + if (\array_key_exists($alias, $values)) { + $valuesKey = $alias; + break; + } + } + } + + $existsInRequest = \array_key_exists($requestKey, $requestParams); + $existsInValues = \array_key_exists($valuesKey, $values); $paramExists = $existsInRequest || $existsInValues; - $arg = $existsInRequest ? $requestParams[$key] : $param['default']; + $arg = $existsInRequest ? $requestParams[$requestKey] : $param['default']; if (\is_callable($arg) && !\is_string($arg)) { $arg = \call_user_func_array($arg, array_values($this->getResources($param['injections']))); } - $value = $existsInValues ? $values[$key] : $arg; + $value = $existsInValues ? $values[$valuesKey] : $arg; if (!$param['skipValidation']) { if (!$paramExists && !$param['optional']) { diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 6535df0..2836f40 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -290,6 +290,194 @@ public function testCanAddAndExecuteHooks(): void $this->assertSame('x-def', $result); } + public function testCanResolveParamAliases(): void + { + $this->http + ->error() + ->inject('error') + ->action(function ($error) { + echo 'error-' . $error->getMessage(); + }); + + $savedGet = $_GET; + $savedPost = $_POST; + $savedMethod = $_SERVER['REQUEST_METHOD'] ?? null; + + try { + // GET request: alias resolves from $_GET when canonical key is absent + $_GET = ['xAlias' => 'from-alias']; + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $route = new Route('GET', '/path'); + $route + ->param('x', 'x-def', new Text(200), 'x param', true, aliases: ['xAlias', 'xLegacy']) + ->action(function ($x) { + echo $x; + }); + + ob_start(); + $this->http->execute($route, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('from-alias', $result); + + // GET request: canonical key wins when both are present in $_GET + $_GET = ['x' => 'canonical', 'xAlias' => 'aliased']; + + $route = new Route('GET', '/path'); + $route + ->param('x', 'x-def', new Text(200), 'x param', true, aliases: ['xAlias']) + ->action(function ($x) { + echo $x; + }); + + ob_start(); + $this->http->execute($route, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('canonical', $result); + + // GET request: first matching alias wins when multiple are present in $_GET + $_GET = ['xAlias2' => 'second', 'xAlias1' => 'first']; + + $route = new Route('GET', '/path'); + $route + ->param('x', 'x-def', new Text(200), 'x param', true, aliases: ['xAlias1', 'xAlias2']) + ->action(function ($x) { + echo $x; + }); + + ob_start(); + $this->http->execute($route, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('first', $result); + + // GET request: falls back to default when neither canonical nor any alias is in $_GET + $_GET = ['unrelated' => 'value']; + + $route = new Route('GET', '/path'); + $route + ->param('x', 'x-def', new Text(200), 'x param', true, aliases: ['xAlias']) + ->action(function ($x) { + echo $x; + }); + + ob_start(); + $this->http->execute($route, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('x-def', $result); + + // GET request: required param throws when neither canonical nor any alias is in $_GET + $_GET = ['unrelated' => 'value']; + + $route = new Route('GET', '/path'); + $route + ->param('x', '', new Text(200), 'x param', false, aliases: ['xAlias']) + ->action(function ($x) { + echo $x; + }); + + ob_start(); + $this->http->execute($route, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('error-Param "x" is not optional.', $result); + + // GET request: validation runs against the aliased value and reports the canonical key + $_GET = ['xAlias' => 'too-long']; + + $route = new Route('GET', '/path'); + $route + ->param('x', '', new Text(1, min: 0), 'x param', false, aliases: ['xAlias']) + ->action(function ($x) { + echo $x; + }); + + ob_start(); + $this->http->execute($route, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('error-Invalid `x` param: Value must be a valid string and no longer than 1 chars', $result); + + // POST request: alias resolves from $_POST body + $_GET = []; + $_POST = ['xAlias' => 'posted-alias']; + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $route = new Route('POST', '/path'); + $route + ->param('x', 'x-def', new Text(200), 'x param', true, aliases: ['xAlias']) + ->action(function ($x) { + echo $x; + }); + + ob_start(); + $this->http->execute($route, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('posted-alias', $result); + + // URL path: alias resolves the placeholder name to the canonical param key + $_GET = []; + $_POST = []; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/users/abc-123'; + + $route = Http::get('/users/:userId') + ->param('user_id', '', new Text(200), 'user id', false, aliases: ['userId']) + ->action(function ($user_id) { + echo $user_id; + }); + + $matched = $this->http->match(new Request()); + $this->assertSame($route, $matched); + + ob_start(); + $this->http->execute($matched, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('abc-123', $result); + + // URL path value beats request param when both are present (path-level override) + $_GET = ['user_id' => 'from-query']; + $_SERVER['REQUEST_URI'] = '/users-2/from-path'; + + $route = Http::get('/users-2/:userId') + ->param('user_id', '', new Text(200), 'user id', false, aliases: ['userId']) + ->action(function ($user_id) { + echo $user_id; + }); + + $matched = $this->http->match(new Request()); + $this->assertSame($route, $matched); + + ob_start(); + $this->http->execute($matched, new Request(), new Response()); + $result = ob_get_contents(); + ob_end_clean(); + + $this->assertSame('from-path', $result); + } finally { + $_GET = $savedGet; + $_POST = $savedPost; + if ($savedMethod === null) { + unset($_SERVER['REQUEST_METHOD']); + } else { + $_SERVER['REQUEST_METHOD'] = $savedMethod; + } + } + } + public function testAllowRouteOverrides(): void { Http::setAllowOverride(false);