Skip to content

Commit 74fd050

Browse files
committed
Extract ResolvesCallbackArguments trait, enable in AuthBasic
The duplicated resolveCallbackArguments() method in AbstractRoute and AbstractSyncedRoutine is extracted into a shared trait. AuthBasic now uses the trait so its callback can type-hint ServerRequestInterface to conditionally skip auth based on HTTP method (e.g. allow reads, require credentials for writes). Backward compatible — existing callbacks without PSR-7 hints behave identically.
1 parent 56231a7 commit 74fd050

5 files changed

Lines changed: 228 additions & 128 deletions

File tree

src/ResolvesCallbackArguments.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Respect\Rest;
6+
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use ReflectionFunctionAbstract;
10+
use ReflectionNamedType;
11+
12+
use function is_a;
13+
14+
/** Shared PSR-7 argument injection for routes and routines */
15+
trait ResolvesCallbackArguments
16+
{
17+
/**
18+
* Resolves callback arguments by inspecting parameter types via reflection.
19+
*
20+
* PSR-7 typed parameters (ServerRequestInterface, ResponseInterface) are
21+
* injected automatically. All other parameters consume URL params positionally.
22+
*
23+
* @param array<int, mixed> $params URL-extracted parameters
24+
*
25+
* @return array<int, mixed> Resolved argument list
26+
*/
27+
protected function resolveCallbackArguments(
28+
ReflectionFunctionAbstract $reflection,
29+
array $params,
30+
DispatchContext $context,
31+
): array {
32+
$refParams = $reflection->getParameters();
33+
34+
// No declared parameters — pass all URL params through (supports func_get_args())
35+
if ($refParams === []) {
36+
return $params;
37+
}
38+
39+
$args = [];
40+
$paramIndex = 0;
41+
$hasPsrInjection = false;
42+
43+
foreach ($refParams as $refParam) {
44+
$type = $refParam->getType();
45+
46+
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
47+
$typeName = $type->getName();
48+
49+
if (is_a($typeName, ServerRequestInterface::class, true)) {
50+
$args[] = $context->request;
51+
$hasPsrInjection = true;
52+
continue;
53+
}
54+
55+
if (is_a($typeName, ResponseInterface::class, true)) {
56+
$args[] = $context->factory->createResponse();
57+
$hasPsrInjection = true;
58+
continue;
59+
}
60+
}
61+
62+
$default = $refParam->isDefaultValueAvailable() ? $refParam->getDefaultValue() : null;
63+
$args[] = $params[$paramIndex] ?? $default;
64+
$paramIndex++;
65+
}
66+
67+
// No PSR-7 injection happened — pass params directly (faster, preserves original behavior)
68+
if (!$hasPsrInjection) {
69+
return $params;
70+
}
71+
72+
return $args;
73+
}
74+
}

src/Routes/AbstractRoute.php

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44

55
namespace Respect\Rest\Routes;
66

7-
use Psr\Http\Message\ResponseInterface;
8-
use Psr\Http\Message\ServerRequestInterface;
97
use ReflectionClass;
108
use ReflectionFunctionAbstract;
11-
use ReflectionNamedType;
129
use Respect\Rest\DispatchContext;
10+
use Respect\Rest\ResolvesCallbackArguments;
1311
use Respect\Rest\Routines\IgnorableFileExtension;
1412
use Respect\Rest\Routines\Routinable;
1513
use Respect\Rest\Routines\Unique;
@@ -21,7 +19,6 @@
2119
use function end;
2220
use function explode;
2321
use function implode;
24-
use function is_a;
2522
use function is_string;
2623
use function ltrim;
2724
use function preg_match;
@@ -58,6 +55,8 @@
5855
// phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix
5956
abstract class AbstractRoute
6057
{
58+
use ResolvesCallbackArguments;
59+
6160
public const string CATCHALL_IDENTIFIER = '/**';
6261

6362
public const array CORE_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'];
@@ -227,64 +226,6 @@ public function match(DispatchContext $context, array &$params = []): bool
227226
return true;
228227
}
229228

230-
/**
231-
* Resolves callback arguments by inspecting parameter types via reflection.
232-
*
233-
* PSR-7 typed parameters (ServerRequestInterface, ResponseInterface) are
234-
* injected automatically. All other parameters consume URL params positionally.
235-
*
236-
* @param array<int, mixed> $params URL-extracted parameters
237-
*
238-
* @return array<int, mixed> Resolved argument list
239-
*/
240-
protected function resolveCallbackArguments(
241-
ReflectionFunctionAbstract $reflection,
242-
array $params,
243-
DispatchContext $context,
244-
): array {
245-
$refParams = $reflection->getParameters();
246-
247-
// No declared parameters — pass all URL params through (supports func_get_args())
248-
if ($refParams === []) {
249-
return $params;
250-
}
251-
252-
$args = [];
253-
$paramIndex = 0;
254-
$hasPsrInjection = false;
255-
256-
foreach ($refParams as $refParam) {
257-
$type = $refParam->getType();
258-
259-
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
260-
$typeName = $type->getName();
261-
262-
if (is_a($typeName, ServerRequestInterface::class, true)) {
263-
$args[] = $context->request;
264-
$hasPsrInjection = true;
265-
continue;
266-
}
267-
268-
if (is_a($typeName, ResponseInterface::class, true)) {
269-
$args[] = $context->factory->createResponse();
270-
$hasPsrInjection = true;
271-
continue;
272-
}
273-
}
274-
275-
$default = $refParam->isDefaultValueAvailable() ? $refParam->getDefaultValue() : null;
276-
$args[] = $params[$paramIndex] ?? $default;
277-
$paramIndex++;
278-
}
279-
280-
// No PSR-7 injection happened — pass params directly (faster, preserves original behavior)
281-
if (!$hasPsrInjection) {
282-
return $params;
283-
}
284-
285-
return $args;
286-
}
287-
288229
/** @return array{string, string} */
289230
protected function createRegexPatterns(string $pattern): array
290231
{

src/Routines/AbstractSyncedRoutine.php

Lines changed: 3 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,17 @@
55
namespace Respect\Rest\Routines;
66

77
use Closure;
8-
use Psr\Http\Message\ResponseInterface;
9-
use Psr\Http\Message\ServerRequestInterface;
108
use ReflectionClass;
119
use ReflectionFunction;
1210
use ReflectionFunctionAbstract;
1311
use ReflectionMethod;
14-
use ReflectionNamedType;
1512
use ReflectionObject;
1613
use ReflectionParameter;
1714
use Reflector;
1815
use Respect\Rest\DispatchContext;
16+
use Respect\Rest\ResolvesCallbackArguments;
1917

2018
use function assert;
21-
use function is_a;
2219
use function is_array;
2320
use function is_callable;
2421
use function is_string;
@@ -27,6 +24,8 @@
2724
// phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix
2825
abstract class AbstractSyncedRoutine extends AbstractRoutine implements ParamSynced
2926
{
27+
use ResolvesCallbackArguments;
28+
3029
protected Reflector|null $reflection = null;
3130

3231
/** @return array<int, ReflectionParameter> */
@@ -64,59 +63,6 @@ public function execute(DispatchContext $context, array $params): mixed
6463
return $callback(...$params);
6564
}
6665

67-
/**
68-
* Resolves callback arguments, injecting PSR-7 objects for type-hinted parameters.
69-
*
70-
* @param array<int, mixed> $params
71-
*
72-
* @return array<int, mixed>
73-
*/
74-
protected function resolveCallbackArguments(
75-
ReflectionFunctionAbstract $reflection,
76-
array $params,
77-
DispatchContext $context,
78-
): array {
79-
$refParams = $reflection->getParameters();
80-
81-
if ($refParams === []) {
82-
return $params;
83-
}
84-
85-
$args = [];
86-
$paramIndex = 0;
87-
$hasPsrInjection = false;
88-
89-
foreach ($refParams as $refParam) {
90-
$type = $refParam->getType();
91-
92-
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
93-
$typeName = $type->getName();
94-
95-
if (is_a($typeName, ServerRequestInterface::class, true)) {
96-
$args[] = $context->request;
97-
$hasPsrInjection = true;
98-
continue;
99-
}
100-
101-
if (is_a($typeName, ResponseInterface::class, true)) {
102-
$args[] = $context->factory->createResponse();
103-
$hasPsrInjection = true;
104-
continue;
105-
}
106-
}
107-
108-
$default = $refParam->isDefaultValueAvailable() ? $refParam->getDefaultValue() : null;
109-
$args[] = $params[$paramIndex] ?? $default;
110-
$paramIndex++;
111-
}
112-
113-
if (!$hasPsrInjection) {
114-
return $params;
115-
}
116-
117-
return $args;
118-
}
119-
12066
protected function getReflection(): Reflector
12167
{
12268
$callback = $this->getCallback();

src/Routines/AuthBasic.php

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,28 @@
44

55
namespace Respect\Rest\Routines;
66

7+
use Closure;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use ReflectionFunction;
11+
use ReflectionFunctionAbstract;
12+
use ReflectionMethod;
13+
use ReflectionNamedType;
714
use Respect\Rest\DispatchContext;
15+
use Respect\Rest\ResolvesCallbackArguments;
816

917
use function array_merge;
1018
use function base64_decode;
1119
use function explode;
20+
use function is_a;
21+
use function is_array;
1222
use function stripos;
1323
use function substr;
1424

1525
final class AuthBasic extends AbstractRoutine implements ProxyableBy
1626
{
27+
use ResolvesCallbackArguments;
28+
1729
public function __construct(public string $realm, mixed $callback)
1830
{
1931
parent::__construct($callback);
@@ -22,22 +34,65 @@ public function __construct(public string $realm, mixed $callback)
2234
/** @param array<int, mixed> $params */
2335
public function by(DispatchContext $context, array $params): mixed
2436
{
25-
$callbackResponse = false;
26-
2737
$authorization = $context->request->getHeaderLine('Authorization');
38+
$hasCredentials = $authorization !== '' && stripos($authorization, 'Basic ') === 0;
2839

29-
if ($authorization !== '' && stripos($authorization, 'Basic ') === 0) {
30-
$callbackResponse = ($this->callback)(
31-
...array_merge(explode(':', base64_decode(substr($authorization, 6))), $params),
32-
);
40+
if ($hasCredentials) {
41+
$credentials = explode(':', base64_decode(substr($authorization, 6)));
42+
} elseif ($this->callbackAcceptsPsr7()) {
43+
$credentials = ['', ''];
44+
} else {
45+
return $this->unauthorizedResponse($context);
3346
}
3447

35-
if ($callbackResponse === false) {
36-
$response = $context->factory->createResponse(401);
48+
$allParams = array_merge($credentials, $params);
49+
$args = $this->resolveCallbackArguments(
50+
$this->getCallbackReflection(),
51+
$allParams,
52+
$context,
53+
);
3754

38-
return $response->withHeader('WWW-Authenticate', 'Basic realm="' . $this->realm . '"');
55+
$callbackResponse = ($this->callback)(...$args);
56+
57+
if ($callbackResponse === false) {
58+
return $this->unauthorizedResponse($context);
3959
}
4060

4161
return $callbackResponse;
4262
}
63+
64+
private function unauthorizedResponse(DispatchContext $context): ResponseInterface
65+
{
66+
$response = $context->factory->createResponse(401);
67+
68+
return $response->withHeader('WWW-Authenticate', 'Basic realm="' . $this->realm . '"');
69+
}
70+
71+
private function callbackAcceptsPsr7(): bool
72+
{
73+
foreach ($this->getCallbackReflection()->getParameters() as $param) {
74+
$type = $param->getType();
75+
76+
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
77+
continue;
78+
}
79+
80+
if (is_a($type->getName(), ServerRequestInterface::class, true)) {
81+
return true;
82+
}
83+
}
84+
85+
return false;
86+
}
87+
88+
private function getCallbackReflection(): ReflectionFunctionAbstract
89+
{
90+
$callback = $this->getCallback();
91+
92+
if (is_array($callback)) {
93+
return new ReflectionMethod($callback[0], $callback[1]);
94+
}
95+
96+
return new ReflectionFunction(Closure::fromCallable($callback));
97+
}
4398
}

0 commit comments

Comments
 (0)