Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement URI generation for named routes #277

Merged
merged 10 commits into from
Mar 4, 2024
91 changes: 91 additions & 0 deletions benchmark/UriGenerationBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);

namespace FastRoute\Benchmark;

use FastRoute\ConfigureRoutes;
use FastRoute\FastRoute;
use FastRoute\GenerateUri;
use PhpBench\Attributes as Bench;

/** @phpstan-import-type UriSubstitutions from GenerateUri */
#[Bench\Iterations(5)]
#[Bench\Revs(250)]
#[Bench\Warmup(3)]
#[Bench\BeforeMethods(['registerGenerator'])]
final class UriGenerationBench
{
private GenerateUri $generator;

public function registerGenerator(): void
{
$loader = static function (ConfigureRoutes $routes): void {
$routes->addRoute('GET', '/', 'do-something', ['_name' => 'home']);
$routes->addRoute('GET', '/page/{page_slug:[a-zA-Z0-9\-]+}', 'do-something', ['_name' => 'page.show']);
$routes->addRoute('GET', '/about-us', 'do-something', ['_name' => 'about-us']);
$routes->addRoute('GET', '/contact-us', 'do-something', ['_name' => 'contact-us']);
$routes->addRoute('POST', '/contact-us', 'do-something', ['_name' => 'contact-us.submit']);
$routes->addRoute('GET', '/blog', 'do-something', ['_name' => 'blog.index']);
$routes->addRoute('GET', '/blog/recent', 'do-something', ['_name' => 'blog.recent']);
$routes->addRoute('GET', '/blog/{year}[/{month}[/{day}]]', 'do-something', ['_name' => 'blog.archive']);
$routes->addRoute('GET', '/blog/post/{post_slug:[a-zA-Z0-9\-]+}', 'do-something', ['_name' => 'blog.post.show']);
$routes->addRoute('POST', '/blog/post/{post_slug:[a-zA-Z0-9\-]+}/comment', 'do-something', ['_name' => 'blog.post.comment']);
$routes->addRoute('GET', '/shop', 'do-something', ['_name' => 'shop.index']);
$routes->addRoute('GET', '/shop/category', 'do-something', ['_name' => 'shop.category.index']);
$routes->addRoute('GET', '/shop/category/{category_id:\d+}/product/search/{filter_by:[a-zA-Z]+}:{filter_value}', 'do-something', ['_name' => 'shop.category.product.search']);
};

$this->generator = FastRoute::recommendedSettings($loader, 'cache')
->disableCache()
->uriGenerator();
}

/** @param array{name: non-empty-string} $params */
#[Bench\Subject]
#[Bench\ParamProviders(['allStaticRoutes'])]
public function staticRoutes(array $params): void
{
$this->generator->forRoute($params['name']);
}

/** @return iterable<non-empty-string, array{name: non-empty-string}> */
public static function allStaticRoutes(): iterable
{
$staticRoutes = [
'home',
'about-us',
'contact-us',
'contact-us.submit',
'blog.index',
'blog.recent',
'shop.index',
'shop.category.index',
];

foreach ($staticRoutes as $route) {
yield $route => ['name' => $route];
}
}

/** @param array{name: non-empty-string, substitutions: UriSubstitutions} $params */
#[Bench\Subject]
#[Bench\ParamProviders(['allDynamicRoutes'])]
public function dynamicRoutes(array $params): void
{
$this->generator->forRoute($params['name'], $params['substitutions']);
}

/** @return iterable<non-empty-string, array{name: non-empty-string, substitutions: UriSubstitutions}> */
public static function allDynamicRoutes(): iterable
{
yield 'page.show' => ['name' => 'page.show', 'substitutions' => ['page_slug' => 'testing-one-two-three']];

yield 'blog.post.show' => ['name' => 'blog.post.show', 'substitutions' => ['post_slug' => 'testing-one-two-three']];
yield 'blog.post.comment' => ['name' => 'blog.post.comment', 'substitutions' => ['post_slug' => 'testing-one-two-three']];
yield 'blog.archive-year' => ['name' => 'blog.archive', 'substitutions' => ['year' => '2014']];
yield 'blog.archive-year-month' => ['name' => 'blog.archive', 'substitutions' => ['year' => '2014', 'month' => '03']];
yield 'blog.archive-year-day' => ['name' => 'blog.archive', 'substitutions' => ['year' => '2014', 'month' => '03', 'day' => '15']];

yield 'shop.category.product.search' => ['name' => 'shop.category.product.search', 'substitutions' => ['category_id' => '1', 'filter_by' => 'name', 'filter_value' => 'testing']];
}
}
22 changes: 11 additions & 11 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion src/BadRouteException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,26 @@
use LogicException;

use function sprintf;
use function var_export;

/** @final */
class BadRouteException extends LogicException
class BadRouteException extends LogicException implements Exception
{
public static function alreadyRegistered(string $route, string $method): self
{
return new self(sprintf('Cannot register two routes matching "%s" for method "%s"', $route, $method));
}

public static function namedRouteAlreadyDefined(string $name): self
{
return new self(sprintf('Cannot register two routes under the name "%s"', $name));
}

public static function invalidRouteName(mixed $name): self
{
return new self(sprintf('Route name must be a non-empty string, "%s" given', var_export($name, true)));
}

public static function shadowedByVariableRoute(string $route, string $shadowedRegex, string $method): self
{
return new self(
Expand Down
6 changes: 3 additions & 3 deletions src/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

namespace FastRoute;

/** @phpstan-import-type RouteData from DataGenerator */
/** @phpstan-import-type ProcessedData from ConfigureRoutes */
interface Cache
{
/**
* @param callable():RouteData $loader
* @param callable():ProcessedData $loader
*
* @return RouteData
* @return ProcessedData
*/
public function get(string $key, callable $loader): array;
}
6 changes: 3 additions & 3 deletions src/Cache/FileCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use Closure;
use FastRoute\Cache;
use FastRoute\DataGenerator;
use FastRoute\ConfigureRoutes;
use RuntimeException;

use function chmod;
Expand All @@ -23,7 +23,7 @@

use const LOCK_EX;

/** @phpstan-import-type RouteData from DataGenerator */
/** @phpstan-import-type ProcessedData from ConfigureRoutes */
final class FileCache implements Cache
{
private const DIRECTORY_PERMISSIONS = 0775;
Expand Down Expand Up @@ -55,7 +55,7 @@ public function get(string $key, callable $loader): array
return $data;
}

/** @return RouteData|null */
/** @return ProcessedData|null */
private static function readFileContents(string $path): ?array
{
// error suppression is faster than calling `file_exists()` + `is_file()` + `is_readable()`, especially because there's no need to error here
Expand Down
2 changes: 0 additions & 2 deletions src/Cache/Psr16Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
namespace FastRoute\Cache;

use FastRoute\Cache;
use FastRoute\DataGenerator;
use Psr\SimpleCache\CacheInterface;

use function is_array;

/** @phpstan-import-type RouteData from DataGenerator */
final class Psr16Cache implements Cache
{
public function __construct(private readonly CacheInterface $cache)
Expand Down
10 changes: 8 additions & 2 deletions src/ConfigureRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
namespace FastRoute;

/**
* @phpstan-import-type RouteData from DataGenerator
* @phpstan-import-type StaticRoutes from DataGenerator
* @phpstan-import-type DynamicRoutes from DataGenerator
* @phpstan-import-type ExtraParameters from DataGenerator
* @phpstan-import-type RoutesForUriGeneration from GenerateUri
* @phpstan-type ProcessedData array{StaticRoutes, DynamicRoutes, RoutesForUriGeneration}
*/
interface ConfigureRoutes
{
public const ROUTE_NAME = '_name';
public const ROUTE_REGEX = '_route';

/**
* Registers a new route.
*
Expand Down Expand Up @@ -101,7 +107,7 @@ public function options(string $route, mixed $handler, array $extraParameters =
/**
* Returns the processed aggregated route data.
*
* @return RouteData
* @return ProcessedData
*/
public function processedRoutes(): array;
}
5 changes: 3 additions & 2 deletions src/DataGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace FastRoute;

/**
* @phpstan-import-type ParsedRoute from RouteParser
* @phpstan-type ExtraParameters array<string, string|int|bool|float>
* @phpstan-type StaticRoutes array<string, array<string, array{mixed, ExtraParameters}>>
* @phpstan-type DynamicRouteChunk array{regex: string, suffix?: string, routeMap: array<int|string, array{mixed, array<string, string>, ExtraParameters}>}
Expand All @@ -21,8 +22,8 @@ interface DataGenerator
* can be arbitrary data that will be returned when the route
* matches.
*
* @param array<string|array{0: string, 1:string}> $routeData
* @param ExtraParameters $extraParameters
* @param ParsedRoute $routeData
* @param ExtraParameters $extraParameters
*/
public function addRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters = []): void;

Expand Down
12 changes: 7 additions & 5 deletions src/DataGenerator/RegexBasedAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use FastRoute\BadRouteException;
use FastRoute\DataGenerator;
use FastRoute\Route;
use FastRoute\RouteParser;

use function array_chunk;
use function array_map;
Expand All @@ -24,6 +25,7 @@
* @phpstan-import-type DynamicRoutes from DataGenerator
* @phpstan-import-type RouteData from DataGenerator
* @phpstan-import-type ExtraParameters from DataGenerator
* @phpstan-import-type ParsedRoute from RouteParser
*/
abstract class RegexBasedAbstract implements DataGenerator
{
Expand Down Expand Up @@ -85,15 +87,15 @@ private function computeChunkSize(int $count): int
return $size;
}

/** @param array<string|array{0: string, 1:string}> $routeData */
/** @param ParsedRoute $routeData */
private function isStaticRoute(array $routeData): bool
{
return count($routeData) === 1 && is_string($routeData[0]);
}

/**
* @param array<string|array{0: string, 1:string}> $routeData
* @param ExtraParameters $extraParameters
* @param ParsedRoute $routeData
* @param ExtraParameters $extraParameters
*/
private function addStaticRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters): void
{
Expand All @@ -116,8 +118,8 @@ private function addStaticRoute(string $httpMethod, array $routeData, mixed $han
}

/**
* @param array<string|array{0: string, 1:string}> $routeData
* @param ExtraParameters $extraParameters
* @param ParsedRoute $routeData
* @param ExtraParameters $extraParameters
*/
private function addVariableRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters): void
{
Expand Down
10 changes: 10 additions & 0 deletions src/Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);

namespace FastRoute;

use Throwable;

interface Exception extends Throwable
{
}
Loading
Loading