Skip to content

Commit

Permalink
backwards-compatible reverse routing
Browse files Browse the repository at this point in the history
  • Loading branch information
pmjones committed Jan 20, 2024
1 parent 4012884 commit dd4d0e9
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 4 deletions.
126 changes: 126 additions & 0 deletions src/FastRoute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace FastRoute;

use RuntimeException;

class FastRoute
{
/**
* @var callable
*/
protected $routes;

/**
* @var array
*/
protected $options = [];

/**
* @var Dispatcher
*/
protected $dispatcher;

/**
* @var RouteGenerator
*/
protected $routeGenerator;

public function __construct(callable $routes, array $options = [])
{
$this->routes = $routes;

$options += [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'routeGenerator' => 'FastRoute\\RouteGenerator',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
'routeCollector' => 'FastRoute\\RouteCollector',
'cacheDisabled' => false,
'cacheFile' => null,
];

$this->options = $options;
}

/**
* @return Dispatcher
*/
public function getDispatcher()
{
if (!$this->dispatcher) {
$this->build();
}

return $this->dispatcher;
}

/**
* @return RouteGenerator
*/
public function getRouteGenerator()
{
if (!$this->routeGenerator) {
$this->build();
}

return $this->routeGenerator;
}

protected function build()
{
if (
!$this->options['cacheDisabled']
&& $this->options['cacheFile']
&& file_exists($this->options['cacheFile'])
) {
$this->buildCached();
return;
}

$this->routeGenerator = new $this->options['routeGenerator'];

/** @var RouteCollector $routeCollector */
$routeCollector = new $this->options['routeCollector'](
new $this->options['routeParser'],
new $this->options['dataGenerator'],
$this->routeGenerator
);

call_user_func($this->routes, $routeCollector);

$dispatchData = $routeCollector->getData();

if (!$this->options['cacheDisabled'] && $this->options['cacheFile']) {
file_put_contents(
$this->options['cacheFile'],
'<?php return ' . var_export($dispatchData, true) . ';'
);

file_put_contents(
$this->options['cacheFile'] . '.generator',
'<?php return ' . var_export($this->routeGenerator->getData(), true) . ';'
);
}

$this->dispatcher = new $this->options['dispatcher']($dispatchData);
}

protected function buildCached()
{
$dispatchData = require $this->options['cacheFile'];

if (!is_array($dispatchData)) {
throw new RuntimeException('Invalid cache file "' . $this->options['cacheFile'] . '"');
}

$this->dispatcher = $this->options['dispatcher']($dispatchData);
$generatorData = require $this->options['cacheFile'] . '.generator';

if (!is_array($generatorData)) {
throw new RuntimeException('Invalid cache file "' . $this->options['cacheFile'] . '.generator"');
}

$this->routeGenerator = new $this->options['generator']($generatorData);
}
}
19 changes: 15 additions & 4 deletions src/RouteCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,27 @@ class RouteCollector
/** @var DataGenerator */
protected $dataGenerator;

/** @var ?RouteGenerator */
protected $routeGenerator;

/** @var string */
protected $currentGroupPrefix;

/**
* Constructs a route collector.
*
* @param RouteParser $routeParser
* @param DataGenerator $dataGenerator
* @param RouteParser $routeParser
* @param DataGenerator $dataGenerator
* @param ?RouteGenerator $routeGenerator
*/
public function __construct(RouteParser $routeParser, DataGenerator $dataGenerator)
{
public function __construct(
RouteParser $routeParser,
DataGenerator $dataGenerator,
RouteGenerator $routeGenerator = null
) {
$this->routeParser = $routeParser;
$this->dataGenerator = $dataGenerator;
$this->routeGenerator = $routeGenerator;
$this->currentGroupPrefix = '';
}

Expand All @@ -44,6 +52,9 @@ public function addRoute($httpMethod, $route, $handler)
$this->dataGenerator->addRoute($method, $routeData, $handler);
}
}
if (is_string($handler) && $this->routeGenerator) {
$this->routeGenerator->setInfo($handler, $route, $routeDatas);
}
}

/**
Expand Down
131 changes: 131 additions & 0 deletions src/RouteGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

namespace FastRoute;

use RuntimeException;

class RouteGenerator
{
/**
* @var array
*/
protected $data = [];

public function __construct(array $data = [])
{
$this->data = $data;
}

public function getData()
{
return $this->data;
}

/**
* @param string $handler
* @param string $route The route as collected.
* @param string $routeDatas The route as parsed.
*/
public function setInfo($handler, $route, $routeDatas)
{
$this->data[$handler] = [$route, $routeDatas];
}

/**
* @return array|null
*/
public function getInfo($handler)
{
return array_key_exists($handler, $this->data)
? $this->data[$handler]
: null;
}

/**
* @param string $handler
* @return string
*/
public function generate($handler, array $values = [])
{
$url = '';
$info = $this->getInfo($handler);

if ($info === null) {
throw new RuntimeException("No such handler: '{$handler}'");
}

list($route, $routeDatas) = $info;
$startAtSegment = 0;

$this->generateRequired(
array_shift($routeDatas),
$values,
$url,
$startAtSegment
);

while ($values && $routeDatas && $startAtSegment) {
$this->generateOptional(
array_shift($routeDatas),
$values,
$url,
$startAtSegment
);
}

return $url;
}

protected function generateRequired($required, &$values, &$url, &$startAtSegment)
{
foreach ($required as $pos => $segment) {
if (is_string($segment)) {
$url .= $segment;
continue;
}

list($name, $regex) = $segment;

if (! array_key_exists($name, $values)) {
throw new RuntimeException(
"Missing {{$name}} value for {$handler}"
);
}

$url .= strval($values[$name]);
unset($values[$name]);
}

$startAtSegment = count($required);
}

protected function generateOptional($optional, &$values, &$url, &$startAtSegment)
{
$append = '';

foreach ($optional as $pos => $segment) {
if ($pos < $startAtSegment) {
continue;
}

if (is_string($segment)) {
$append .= $segment;
continue;
}

list($name, $regex) = $segment;

if (! array_key_exists($name, $values)) {
// cannot complete this optional, nor any later optionals.
$startAtSegment = 0;
return;
}

$append .= strval($values[$name]);
unset($values[$name]);
}

$url .= $append;
$startAtSegment = count($optional);
}
}
60 changes: 60 additions & 0 deletions test/RouteGeneratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace FastRoute;

use PHPUnit\Framework\TestCase;

class RouteGeneratorTest extends TestCase
{
public function test()
{
$this->fastRoute = new FastRoute(function ($r) {
$r->get('/archive/{username}[/{year}[/{month}[/{day}]]]', 'GetArchiveAction');
});

$routeGenerator = $this->fastRoute->getRouteGenerator();

// has only required values
$expect = '/archive/bolivar';
$actual = $routeGenerator->generate('GetArchiveAction', [
'username' => 'bolivar',
]);
$this->assertSame($expect, $actual);

// has optional value for year
$expect = '/archive/bolivar/1979';
$actual = $routeGenerator->generate('GetArchiveAction', [
'username' => 'bolivar',
'year' => 1979,
]);
$this->assertSame($expect, $actual);

// has optional values for year and month
$expect = '/archive/bolivar/1979/11';
$actual = $routeGenerator->generate('GetArchiveAction', [
'username' => 'bolivar',
'year' => 1979,
'month' => 11,
]);
$this->assertSame($expect, $actual);

// has optional values for year, month, and day
$expect = '/archive/bolivar/1979/11/07';
$actual = $routeGenerator->generate('GetArchiveAction', [
'username' => 'bolivar',
'year' => 1979,
'month' => 11,
'day' => '07',
]);
$this->assertSame($expect, $actual);

// has optional values for year and day, but not month
$expect = '/archive/bolivar/1979';
$actual = $routeGenerator->generate('GetArchiveAction', [
'username' => 'bolivar',
'year' => 1979,
'day' => '07',
]);
$this->assertSame($expect, $actual);
}
}

0 comments on commit dd4d0e9

Please sign in to comment.