Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/ResolvesCallbackArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace Respect\Rest;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionFunctionAbstract;
use ReflectionNamedType;

use function is_a;

/** Shared PSR-7 argument injection for routes and routines */
trait ResolvesCallbackArguments
{
/**
* Resolves callback arguments by inspecting parameter types via reflection.
*
* PSR-7 typed parameters (ServerRequestInterface, ResponseInterface) are
* injected automatically. All other parameters consume URL params positionally.
*
* @param array<int, mixed> $params URL-extracted parameters
*
* @return array<int, mixed> Resolved argument list
*/
protected function resolveCallbackArguments(
ReflectionFunctionAbstract $reflection,
array $params,
DispatchContext $context,
): array {
$refParams = $reflection->getParameters();

// No declared parameters — pass all URL params through (supports func_get_args())
if ($refParams === []) {
return $params;
}

$args = [];
$paramIndex = 0;
$hasPsrInjection = false;

foreach ($refParams as $refParam) {
$type = $refParam->getType();

if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$typeName = $type->getName();

if (is_a($typeName, ServerRequestInterface::class, true)) {
$args[] = $context->request;
$hasPsrInjection = true;
continue;
}

if (is_a($typeName, ResponseInterface::class, true)) {
$args[] = $context->factory->createResponse();
$hasPsrInjection = true;
continue;
}
}

$default = $refParam->isDefaultValueAvailable() ? $refParam->getDefaultValue() : null;
$args[] = $params[$paramIndex] ?? $default;
$paramIndex++;
}

// No PSR-7 injection happened — pass params directly (faster, preserves original behavior)
if (!$hasPsrInjection) {
return $params;
}

return $args;
}
}
65 changes: 3 additions & 62 deletions src/Routes/AbstractRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

namespace Respect\Rest\Routes;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionClass;
use ReflectionFunctionAbstract;
use ReflectionNamedType;
use Respect\Rest\DispatchContext;
use Respect\Rest\ResolvesCallbackArguments;
use Respect\Rest\Routines\IgnorableFileExtension;
use Respect\Rest\Routines\Routinable;
use Respect\Rest\Routines\Unique;
Expand All @@ -21,7 +19,6 @@
use function end;
use function explode;
use function implode;
use function is_a;
use function is_string;
use function ltrim;
use function preg_match;
Expand Down Expand Up @@ -58,6 +55,8 @@
// phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix
abstract class AbstractRoute
{
use ResolvesCallbackArguments;

public const string CATCHALL_IDENTIFIER = '/**';

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

/**
* Resolves callback arguments by inspecting parameter types via reflection.
*
* PSR-7 typed parameters (ServerRequestInterface, ResponseInterface) are
* injected automatically. All other parameters consume URL params positionally.
*
* @param array<int, mixed> $params URL-extracted parameters
*
* @return array<int, mixed> Resolved argument list
*/
protected function resolveCallbackArguments(
ReflectionFunctionAbstract $reflection,
array $params,
DispatchContext $context,
): array {
$refParams = $reflection->getParameters();

// No declared parameters — pass all URL params through (supports func_get_args())
if ($refParams === []) {
return $params;
}

$args = [];
$paramIndex = 0;
$hasPsrInjection = false;

foreach ($refParams as $refParam) {
$type = $refParam->getType();

if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$typeName = $type->getName();

if (is_a($typeName, ServerRequestInterface::class, true)) {
$args[] = $context->request;
$hasPsrInjection = true;
continue;
}

if (is_a($typeName, ResponseInterface::class, true)) {
$args[] = $context->factory->createResponse();
$hasPsrInjection = true;
continue;
}
}

$default = $refParam->isDefaultValueAvailable() ? $refParam->getDefaultValue() : null;
$args[] = $params[$paramIndex] ?? $default;
$paramIndex++;
}

// No PSR-7 injection happened — pass params directly (faster, preserves original behavior)
if (!$hasPsrInjection) {
return $params;
}

return $args;
}

/** @return array{string, string} */
protected function createRegexPatterns(string $pattern): array
{
Expand Down
60 changes: 3 additions & 57 deletions src/Routines/AbstractSyncedRoutine.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,17 @@
namespace Respect\Rest\Routines;

use Closure;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionClass;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionObject;
use ReflectionParameter;
use Reflector;
use Respect\Rest\DispatchContext;
use Respect\Rest\ResolvesCallbackArguments;

use function assert;
use function is_a;
use function is_array;
use function is_callable;
use function is_string;
Expand All @@ -27,6 +24,8 @@
// phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix
abstract class AbstractSyncedRoutine extends AbstractRoutine implements ParamSynced
{
use ResolvesCallbackArguments;

protected Reflector|null $reflection = null;

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

/**
* Resolves callback arguments, injecting PSR-7 objects for type-hinted parameters.
*
* @param array<int, mixed> $params
*
* @return array<int, mixed>
*/
protected function resolveCallbackArguments(
ReflectionFunctionAbstract $reflection,
array $params,
DispatchContext $context,
): array {
$refParams = $reflection->getParameters();

if ($refParams === []) {
return $params;
}

$args = [];
$paramIndex = 0;
$hasPsrInjection = false;

foreach ($refParams as $refParam) {
$type = $refParam->getType();

if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$typeName = $type->getName();

if (is_a($typeName, ServerRequestInterface::class, true)) {
$args[] = $context->request;
$hasPsrInjection = true;
continue;
}

if (is_a($typeName, ResponseInterface::class, true)) {
$args[] = $context->factory->createResponse();
$hasPsrInjection = true;
continue;
}
}

$default = $refParam->isDefaultValueAvailable() ? $refParam->getDefaultValue() : null;
$args[] = $params[$paramIndex] ?? $default;
$paramIndex++;
}

if (!$hasPsrInjection) {
return $params;
}

return $args;
}

protected function getReflection(): Reflector
{
$callback = $this->getCallback();
Expand Down
73 changes: 64 additions & 9 deletions src/Routines/AuthBasic.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,28 @@

namespace Respect\Rest\Routines;

use Closure;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionNamedType;
use Respect\Rest\DispatchContext;
use Respect\Rest\ResolvesCallbackArguments;

use function array_merge;
use function base64_decode;
use function explode;
use function is_a;
use function is_array;
use function stripos;
use function substr;

final class AuthBasic extends AbstractRoutine implements ProxyableBy
{
use ResolvesCallbackArguments;

public function __construct(public string $realm, mixed $callback)
{
parent::__construct($callback);
Expand All @@ -22,22 +34,65 @@ public function __construct(public string $realm, mixed $callback)
/** @param array<int, mixed> $params */
public function by(DispatchContext $context, array $params): mixed
{
$callbackResponse = false;

$authorization = $context->request->getHeaderLine('Authorization');
$hasCredentials = $authorization !== '' && stripos($authorization, 'Basic ') === 0;

if ($authorization !== '' && stripos($authorization, 'Basic ') === 0) {
$callbackResponse = ($this->callback)(
...array_merge(explode(':', base64_decode(substr($authorization, 6))), $params),
);
if ($hasCredentials) {
$credentials = explode(':', base64_decode(substr($authorization, 6)));
} elseif ($this->callbackAcceptsPsr7()) {
$credentials = ['', ''];
} else {
return $this->unauthorizedResponse($context);
}

if ($callbackResponse === false) {
$response = $context->factory->createResponse(401);
$allParams = array_merge($credentials, $params);
$args = $this->resolveCallbackArguments(
$this->getCallbackReflection(),
$allParams,
$context,
);

return $response->withHeader('WWW-Authenticate', 'Basic realm="' . $this->realm . '"');
$callbackResponse = ($this->callback)(...$args);

if ($callbackResponse === false) {
return $this->unauthorizedResponse($context);
}

return $callbackResponse;
}

private function unauthorizedResponse(DispatchContext $context): ResponseInterface
{
$response = $context->factory->createResponse(401);

return $response->withHeader('WWW-Authenticate', 'Basic realm="' . $this->realm . '"');
}

private function callbackAcceptsPsr7(): bool
{
foreach ($this->getCallbackReflection()->getParameters() as $param) {
$type = $param->getType();

if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
continue;
}

if (is_a($type->getName(), ServerRequestInterface::class, true)) {
return true;
}
}

return false;
}

private function getCallbackReflection(): ReflectionFunctionAbstract
{
$callback = $this->getCallback();

if (is_array($callback)) {
return new ReflectionMethod($callback[0], $callback[1]);
}

return new ReflectionFunction(Closure::fromCallable($callback));
}
}
Loading