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
61 changes: 54 additions & 7 deletions docs/1-essentials/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ public function docsRedirect(string $path): Redirect

## Generating URIs

Tempest provides a `\Tempest\uri` function that can be used to generate an URI to a controller method. This function accepts the FQCN of the controller or a callable to a method as its first argument, and named parameters as [the rest of its arguments](https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list).
Tempest provides a `\Tempest\uri` function that can be used to generate a URI to a controller method. This function accepts the FQCN of the controller or a callable to a method as its first argument, and named parameters as [the rest of its arguments](https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list).

```php
use function Tempest\uri;
use function Tempest\Router\uri;

// Invokable classes can be referenced directly:
uri(HomeController::class);
Expand All @@ -181,15 +181,62 @@ uri([AircraftController::class, 'show'], id: $aircraft->id);
```

:::info
Note that Tempest does not have named routes, and currently doesn't plan on adding them. However, if you have an argument for them, feel free to hop on our [Discord server](/discord){:ssg-ignore="true"} to discuss it.
URI-related methods are also available by injecting the {b`Tempest\Router\UriGenerator`} class into your controller.
:::

## Matching the current URI
### Signed URIs

To determine whether the current request matches a specific controller action, Tempest provides the `\Tempest\is_current_uri` function. This function accepts the same arguments as `uri`, and returns a boolean.
A signed URI may be used to ensure that the URI was not modified after it was created. This is useful for implementing login links, or other endpoints that need protection against tampering.

To create a signed URI, you may use the `signed_uri` function. This function accepts the same arguments as `uri`, and returns the URI with a `signature` parameter:

```php
use function Tempest\Router\signed_uri;

signed_uri(
action: [MailingListController::class, 'unsubscribe'],
email: $email
);
```

Alternatively, you may use `temporary_signed_uri` to provide a duration after which the signed URI will expire, providing an extra layer of security.

```php
use function Tempest\Router\temporary_signed_uri;

temporary_signed_uri(
action: PasswordlessAuthenticationController::class,
duration: Duration::minutes(10),
userId: $userId
);
```

To ensure the validity of a signed URL, you should call the `hasValidSignature` method on the {`Tempest\Router\UriGenerator`} class.

```php
final class PasswordlessAuthenticationController
{
public function __construct(
private readonly UriGenerator $uri,
) {}

public function __invoke(Request $request): Response
{
if (! $this->uri->hasValidSignature($request)) {
return new Invalid();
}

// ...
}
}
```

### Matching the current URI

To determine whether the current request matches a specific controller action, Tempest provides the `is_current_uri` function. This function accepts the same arguments as `uri`, and returns a boolean.

```php
use function Tempest\is_current_uri;
use function Tempest\Router\is_current_uri;

// Current URI is: /aircraft/1

Expand Down Expand Up @@ -243,7 +290,7 @@ Once you have created a request class, you may simply inject it into a controlle
use Tempest\Router\Post;
use Tempest\Http\Responses\Redirect;
use function Tempest\map;
use function Tempest\uri;
use function Tempest\Router\uri;

final readonly class AirportController
{
Expand Down
4 changes: 2 additions & 2 deletions docs/4-internals/03-view-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Tempest will merge all imports at the top of the compiled view, meaning that eac
```html
<?php
use App\PostController;
use function Tempest\uri;
use function Tempest\Router\uri;
?>

{{ uri([PostController::class, 'show'], post: $post->id) }}
Expand Down Expand Up @@ -608,7 +608,7 @@ Referencing a symbol within a view will automatically import it at the top of th
```html
<?php
use App\PostController;
use function Tempest\uri;
use function Tempest\Router\uri;
?>

{{ uri([PostController::class, 'show'], post: $post->id) }}
Expand Down
79 changes: 0 additions & 79 deletions packages/router/src/GenericRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Tempest\Router;

use BackedEnum;
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
use ReflectionClass;
use Tempest\Container\Container;
Expand All @@ -14,17 +13,12 @@
use Tempest\Http\Request;
use Tempest\Http\Response;
use Tempest\Http\Responses\Ok;
use Tempest\Reflection\ClassReflector;
use Tempest\Router\Exceptions\ControllerActionHadNoReturn;
use Tempest\Router\Exceptions\ControllerMethodHadNoRouteAttribute;
use Tempest\Router\Exceptions\MatchedRouteCouldNotBeResolved;
use Tempest\Router\Routing\Construction\DiscoveredRoute;
use Tempest\Router\Routing\Matching\RouteMatcher;
use Tempest\View\View;

use function Tempest\map;
use function Tempest\Support\Regex\replace;
use function Tempest\Support\str;

final readonly class GenericRouter implements Router
{
Expand Down Expand Up @@ -98,79 +92,6 @@ private function getCallable(): HttpMiddlewareCallable
return $callable;
}

public function toUri(array|string $action, ...$params): string
{
if (is_string($action) && str_starts_with($action, '/')) {
$uri = $action;
} else {
[$controllerClass, $controllerMethod] = is_array($action) ? $action : [$action, '__invoke'];

$routeAttribute = new ClassReflector($controllerClass)
->getMethod($controllerMethod)
->getAttribute(Route::class);

if ($routeAttribute === null) {
throw new ControllerMethodHadNoRouteAttribute($controllerClass, $controllerMethod);
}

$uri = $routeAttribute->uri;
}

$uri = str($uri);
$queryParams = [];

foreach ($params as $key => $value) {
if (! $uri->matches(sprintf('/\{%s(\}|:)/', $key))) {
$queryParams[$key] = $value;

continue;
}

if ($value instanceof BackedEnum) {
$value = $value->value;
} elseif ($value instanceof Bindable) {
foreach (new ClassReflector($value)->getPublicProperties() as $property) {
if (! $property->hasAttribute(IsBindingValue::class)) {
continue;
}

$value = $property->getValue($value);
break;
}
}

$uri = $uri->replaceRegex(
'#\{' . $key . DiscoveredRoute::ROUTE_PARAM_CUSTOM_REGEX . '\}#',
(string) $value,
);
}

$uri = $uri->prepend(rtrim($this->appConfig->baseUri, '/'));

if ($queryParams !== []) {
return $uri->append('?' . http_build_query($queryParams))->toString();
}

return $uri->toString();
}

public function isCurrentUri(array|string $action, ...$params): bool
{
$matchedRoute = $this->container->get(MatchedRoute::class);
$candidateUri = $this->toUri($action, ...[...$matchedRoute->params, ...$params]);
$currentUri = $this->toUri([$matchedRoute->route->handler->getDeclaringClass(), $matchedRoute->route->handler->getName()]);

foreach ($matchedRoute->params as $key => $value) {
if ($value instanceof BackedEnum) {
$value = $value->value;
}

$currentUri = replace($currentUri, '/({' . preg_quote($key, '/') . '(?::.*?)?})/', $value);
}

return $currentUri === $candidateUri;
}

private function createResponse(string|array|Response|View $input): Response
{
if ($input instanceof View || is_array($input) || is_string($input)) {
Expand Down
26 changes: 2 additions & 24 deletions packages/router/src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,12 @@
namespace Tempest\Router;

use Psr\Http\Message\ServerRequestInterface as PsrRequest;
use Tempest\DateTime\DateTime;
use Tempest\DateTime\Duration;
use Tempest\Http\Request;
use Tempest\Http\Response;

interface Router
{
public function dispatch(Request|PsrRequest $request): Response;

/**
* Creates a valid URI to the given `$action`.
*
* `$action` is one of :
* - Controller FQCN and its method as a tuple
* - Invokable controller FQCN
* - URI string starting with `/`
*
* @param array{class-string, string}|class-string|string $action
*/
public function toUri(array|string $action, ...$params): string;

/**
* Checks if the URI to the given `$action` would match the current route.
*
* `$action` is one of :
* - Controller FQCN and its method as a tuple
* - Invokable controller FQCN
* - URI string starting with `/`
*
* @param array{class-string, string}|class-string|string $action
*/
public function isCurrentUri(array|string $action, ...$params): bool;
}
2 changes: 1 addition & 1 deletion packages/router/src/Static/StaticGenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
use Tempest\Vite\Exceptions\ManifestWasNotFound;
use Throwable;

use function Tempest\Router\uri;
use function Tempest\Support\path;
use function Tempest\Support\str;
use function Tempest\uri;

final class StaticGenerateCommand
{
Expand Down
Loading
Loading