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
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
},
"require": {
"php": ">=8.5",
"psr/container": "^2.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^2.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0"
"psr/http-server-middleware": "^1.0",
"respect/parameter": "^1.0"
},
"require-dev": {
"nyholm/psr7": "^1.8",
Expand Down
27 changes: 3 additions & 24 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ $r3->get('/download/*', function(string $file, ResponseInterface $response) {
```

1. Parameters are matched by type, not position. Mix them freely with route parameters.
2. This works with callback routes and class controller methods alike.
2. This works with callback routes, class controller methods, and routine callbacks alike.

## PSR-15 Integration

Expand Down Expand Up @@ -291,11 +291,9 @@ $r3->get('/documents/*', function($documentId) {
```

1. This will match the route only if the callback on *when* is matched.
2. The `$documentId` param must have the same name in the action and the
condition (but does not need to appear in the same order).
2. Route parameters are passed positionally, matching the order of the `/*` segments.
3. You can specify more than one parameter per condition callback.
4. You can chain conditions: `when($cb1)->when($cb2)->when($etc)`
5. Conditions will also sync with parameters on bound classes and instance methods.

This makes it possible to validate parameters using any custom routine and
not just data types such as `int` or `string`.
Expand Down Expand Up @@ -324,7 +322,7 @@ $r3->get('/artists/*/albums/*', function($artistName, $albumName) {
```

1. This will execute the callback defined with *by* before the route action.
2. Parameters are synced by name, not by order, like with `when`.
2. Route parameters are passed positionally, matching the order of the `/*` segments.
3. You can specify more than one parameter per proxy callback.
4. You can chain proxies: `by($cb1)->by($cb2)->by($etc)`
5. A `return false` from a proxy will stop the execution of any following proxies
Expand All @@ -350,7 +348,6 @@ $r3->post('/artists/*/albums/*', function($artistName, $albumName) {
1. `by` proxies will be executed before the route action, `through` proxies
will be executed after.
2. You are free to use them separately or in tandem.
3. `through` can also receive parameters by name.

When processing something after the route has run, it's often desirable to process
its output as well. This can be achieved with a nested closure:
Expand Down Expand Up @@ -386,23 +383,6 @@ A simple way of applying routines to every route on the router is:
$r3->always('By', $logRoutine);
```

You can use the param sync to take advantage of this:
```php
$r3->always('When', function($user=null) {
if ($user) {
return strlen($user) > 3;
}
});

$r3->any('/products', function () { /***/ });
$r3->any('/users/*', function ($user) { /***/ });
$r3->any('/users/*/products', function ($user) { /***/ });
$r3->any('/listeners/*', function ($user) { /***/ });
```

Since there are three routes with the `$user` parameter, `when` will
verify them all automatically by name.

## File Extensions

Use the `fileExtension` routine to map URL extensions to response transformations:
Expand Down Expand Up @@ -556,7 +536,6 @@ appended to the route. Custom routines have the option of several different inte
which can be implemented:

* `IgnorableFileExtension` - Instructs the router to ignore the file extension in requests.
* `ParamSynced` - Syncs parameters with the route function/method.
* `ProxyableBy` - Instructs the router to run method `by()` before the route.
* `ProxyableThrough` - Instructs the router to run method `through()` after the route.
* `ProxyableWhen` - Instructs the router to run method `when()` to validate the route match.
Expand Down
84 changes: 30 additions & 54 deletions src/DispatchContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@

namespace Respect\Rest;

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use ReflectionFunctionAbstract;
use ReflectionParameter;
use Respect\Parameter\Resolver;
use Respect\Rest\Routes\AbstractRoute;
use Respect\Rest\Routines\ParamSynced;
use Respect\Rest\Routines\Routinable;
use Throwable;

use function is_a;
use function rawurldecode;
use function rtrim;
use function set_error_handler;
use function sprintf;
use function strtolower;
use function strtoupper;

/** Internal routing context wrapping a PSR-7 server request */
final class DispatchContext
final class DispatchContext implements ContainerInterface
{
/** @var array<int, mixed> */
public array $params = [];
Expand Down Expand Up @@ -53,6 +52,8 @@ final class DispatchContext
/** @var array<int, AbstractRoute> */
private array $sideRoutes = [];

private Resolver|null $resolver = null;

public function __construct(
public ServerRequestInterface $request,
public ResponseFactoryInterface&StreamFactoryInterface $factory,
Expand Down Expand Up @@ -180,33 +181,6 @@ public function response(): ResponseInterface|null
}
}

/** @param array<int, mixed> $params */
public function routineCall(
string $type,
string $method,
Routinable $routine,
array &$params,
AbstractRoute $route,
): mixed {
$reflection = $route->getTargetReflection($method);

$callbackParameters = [];

if (!$routine instanceof ParamSynced) {
$callbackParameters = $params;
} elseif ($reflection !== null) {
foreach ($routine->getParameters() as $parameter) {
$callbackParameters[] = $this->extractRouteParam(
$reflection,
$parameter,
$params,
);
}
}

return $routine->{$type}($this, $callbackParameters);
}

public function forward(AbstractRoute $route): ResponseInterface|null
{
$this->route = $route;
Expand All @@ -230,6 +204,30 @@ public function setResponder(Responder $responder): void
$this->responder = $responder;
}

public function resolver(): Resolver
{
return $this->resolver ??= new Resolver($this);
}

public function has(string $id): bool
{
return is_a($id, ServerRequestInterface::class, true)
|| is_a($id, ResponseInterface::class, true);
}

public function get(string $id): mixed
{
if (is_a($id, ServerRequestInterface::class, true)) {
return $this->request;
}

if (is_a($id, ResponseInterface::class, true)) {
return $this->ensureResponseDraft();
}

throw new NotFoundException(sprintf('No entry found for "%s"', $id));
}

/** @return callable|null The previous error handler, or null */
protected function prepareForErrorForwards(AbstractRoute $route): callable|null
{
Expand Down Expand Up @@ -310,28 +308,6 @@ protected function forwardToStatusRoute(ResponseInterface $preparedResponse): Re
return null;
}

/** @param array<int, mixed> $params */
protected function extractRouteParam(
ReflectionFunctionAbstract $callback,
ReflectionParameter $routeParam,
array &$params,
): mixed {
foreach ($callback->getParameters() as $callbackParamReflection) {
if (
$callbackParamReflection->getName() === $routeParam->getName()
&& isset($params[$callbackParamReflection->getPosition()])
) {
return $params[$callbackParamReflection->getPosition()];
}
}

if ($routeParam->isDefaultValueAvailable()) {
return $routeParam->getDefaultValue();
}

return null;
}

protected function finalizeResponse(mixed $response): ResponseInterface
{
return $this->responder()->finalize(
Expand Down
12 changes: 12 additions & 0 deletions src/NotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Respect\Rest;

use Exception;
use Psr\Container\NotFoundExceptionInterface;

final class NotFoundException extends Exception implements NotFoundExceptionInterface
{
}
74 changes: 0 additions & 74 deletions src/ResolvesCallbackArguments.php

This file was deleted.

8 changes: 0 additions & 8 deletions src/Routes/AbstractRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use ReflectionClass;
use ReflectionFunctionAbstract;
use Respect\Rest\DispatchContext;
use Respect\Rest\ResolvesCallbackArguments;
use Respect\Rest\Routines\IgnorableFileExtension;
use Respect\Rest\Routines\Routinable;
use Respect\Rest\Routines\Unique;
Expand Down Expand Up @@ -55,8 +54,6 @@
// 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 @@ -135,11 +132,6 @@ public function getTargetMethod(string $method): string
return $method;
}

public function getTargetReflection(string $method): ReflectionFunctionAbstract|null
{
return $this->getReflection($this->getTargetMethod($method));
}

/** @param array<int, mixed> $params */
public function dispatchTarget(string $method, array &$params, DispatchContext $context): mixed
{
Expand Down
13 changes: 3 additions & 10 deletions src/Routes/Callback.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@

namespace Respect\Rest\Routes;

use Closure;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use Respect\Parameter\Resolver;
use Respect\Rest\DispatchContext;

use function array_merge;
use function is_array;

class Callback extends AbstractRoute
{
Expand All @@ -31,11 +28,7 @@ public function __construct(

public function getCallbackReflection(): ReflectionFunctionAbstract
{
if (is_array($this->callback)) {
return new ReflectionMethod($this->callback[0], $this->callback[1]);
}

return new ReflectionFunction(Closure::fromCallable($this->callback));
return Resolver::reflectCallable($this->callback);
}

public function getReflection(string $method): ReflectionFunctionAbstract
Expand All @@ -51,7 +44,7 @@ public function getReflection(string $method): ReflectionFunctionAbstract
public function runTarget(string $method, array &$params, DispatchContext $context): mixed
{
$reflection = $this->getReflection($method);
$args = $this->resolveCallbackArguments($reflection, array_merge($params, $this->arguments), $context);
$args = $context->resolver()->resolve($reflection, array_merge($params, $this->arguments));

return ($this->callback)(...$args);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Routes/ControllerRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ protected function invokeTarget(object $target, string $method, array &$params,
{
$reflection = $this->getReflection($method);
if ($reflection !== null) {
$args = $this->resolveCallbackArguments($reflection, $params, $context);
$args = $context->resolver()->resolve($reflection, $params);

return $target->$method(...$args);
}
Expand Down
Loading