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

Introduce result objects and improve static analysis #267

Merged
merged 11 commits into from
Jan 8, 2024
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
/.gitattributes export-ignore
/.gitignore export-ignore
/*.dist export-ignore
/psalm.xml export-ignore
/phpbench.json export-ignore
/composer.lock export-ignore
/Makefile export-ignore
/.editorconfig export-ignore
/composer-req*.json export-ignore
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ phpstan:

.PHONY: phpbench
phpbench:
@vendor/bin/phpbench run -l dots --report aggregate $(PHPBENCH_OPTIONS)
@vendor/bin/phpbench run -l dots --report aggregate --retry-threshold=15 $(PHPBENCH_OPTIONS)
13 changes: 13 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,17 @@
<rule ref="Lcobucci">
<exclude name="Generic.Formatting.MultipleStatementAlignment" />
</rule>

<rule ref="SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousSuffix">
<exclude-pattern>src/Dispatcher/RegexBasedAbstract*</exclude-pattern>
<exclude-pattern>src/DataGenerator/RegexBasedAbstract*</exclude-pattern>
</rule>

<rule ref="SlevomatCodingStandard.Functions.UnusedParameter">
<exclude-pattern>src/Dispatcher/Result/*</exclude-pattern>
</rule>

<rule ref="SlevomatCodingStandard.Classes.SuperfluousExceptionNaming.SuperfluousSuffix">
<exclude-pattern>src/BadRouteException*</exclude-pattern>
</rule>
</ruleset>
11 changes: 1 addition & 10 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
parameters:
level: 6
level: max
paths:
- benchmark
- src
- test

ignoreErrors:
# We don't really want to call the constructor here, so this is fine
- '#FastRoute\\Test\\DummyRouteCollector::__construct\(\) does not call parent constructor#'

# These are false-positives
-
message: '#Variable \$i might not be defined.#'
path: 'src/Dispatcher/GroupPosBased.php'
28 changes: 0 additions & 28 deletions psalm.xml

This file was deleted.

1 change: 0 additions & 1 deletion src/BadRouteException.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

use function sprintf;

// phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousExceptionNaming.SuperfluousSuffix
class BadRouteException extends LogicException
{
public static function alreadyRegistered(string $route, string $method): self
Expand Down
5 changes: 3 additions & 2 deletions src/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

namespace FastRoute;

/** @phpstan-import-type RouteData from DataGenerator */
interface Cache
{
/**
* @param callable():array{0: array<string, array<string, mixed>>, 1: array<string, array<array{regex: string, suffix?: string, routeMap: array<int|string, array{0: mixed, 1: array<string, string>}>}>>} $loader
* @param callable():RouteData $loader
*
* @return array{0: array<string, array<string, mixed>>, 1: array<string, array<array{regex: string, suffix?: string, routeMap: array<int|string, array{0: mixed, 1: array<string, string>}>}>>}
* @return RouteData
*/
public function get(string $key, callable $loader): array;
}
1 change: 1 addition & 0 deletions src/Cache/ApcuCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function get(string $key, callable $loader): array
$result = apcu_fetch($key, $itemFetched);

if ($itemFetched && is_array($result)) {
// @phpstan-ignore-next-line because we won´t be able to validate the array shape in a performant way
return $result;
}

Expand Down
1 change: 1 addition & 0 deletions src/Cache/FileCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ private static function readFileContents(string $path): ?array
return null;
}

// @phpstan-ignore-next-line because we won´t be able to validate the array shape in a performant way
return $value;
}

Expand Down
8 changes: 7 additions & 1 deletion src/DataGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

namespace FastRoute;

/**
* @phpstan-type StaticRoutes array<string, array<string, mixed>>
* @phpstan-type DynamicRouteChunk array{regex: string, suffix?: string, routeMap: array<int|string, array{0: mixed, 1: array<string, string>}>}
* @phpstan-type DynamicRoutes array<string, list<DynamicRouteChunk>>
* @phpstan-type RouteData array{StaticRoutes, DynamicRoutes}
*/
interface DataGenerator
{
/**
Expand All @@ -21,7 +27,7 @@ public function addRoute(string $httpMethod, array $routeData, mixed $handler):
* Returns dispatcher data in some unspecified format, which
* depends on the used method of dispatch.
*
* @return array{0: array<string, array<string, mixed>>, 1: array<string, array<array{regex: string, suffix?: string, routeMap: array<int|string, array{0: mixed, 1: array<string, string>}>}>>}
* @return RouteData
*/
public function getData(): array;
}
20 changes: 15 additions & 5 deletions src/DataGenerator/RegexBasedAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@

use function array_chunk;
use function array_map;
use function assert;
use function ceil;
use function count;
use function is_string;
use function max;
use function round;

// phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousSuffix
/**
* @phpstan-import-type StaticRoutes from DataGenerator
* @phpstan-import-type DynamicRouteChunk from DataGenerator
* @phpstan-import-type DynamicRoutes from DataGenerator
* @phpstan-import-type RouteData from DataGenerator
*/
abstract class RegexBasedAbstract implements DataGenerator
{
/** @var array<string, array<string, mixed>> */
/** @var StaticRoutes */
protected array $staticRoutes = [];

/** @var array<string, array<string, Route>> */
Expand All @@ -29,7 +35,7 @@ abstract protected function getApproxChunkSize(): int;
/**
* @param array<string, Route> $regexToRoutesMap
*
* @return array{regex: string, suffix?: string, routeMap: array<int|string, array{0: mixed, 1: array<string, string>}>}
* @return DynamicRouteChunk
*/
abstract protected function processChunk(array $regexToRoutesMap): array;

Expand All @@ -53,7 +59,7 @@ public function getData(): array
return [$this->staticRoutes, $this->generateVariableRouteData()];
}

/** @return array<string, array<array{regex: string, suffix?: string, routeMap: array<int|string, array{0: mixed, 1: array<string, string>}>}>> */
/** @return DynamicRoutes */
private function generateVariableRouteData(): array
{
$data = [];
Expand All @@ -66,11 +72,14 @@ private function generateVariableRouteData(): array
return $data;
}

/** @return positive-int */
private function computeChunkSize(int $count): int
{
$numParts = max(1, round($count / $this->getApproxChunkSize()));
$size = (int) ceil($count / $numParts);
assert($size > 0);

return (int) ceil($count / $numParts);
return $size;
}

/** @param array<string|array{0: string, 1:string}> $routeData */
Expand All @@ -83,6 +92,7 @@ private function isStaticRoute(array $routeData): bool
private function addStaticRoute(string $httpMethod, array $routeData, mixed $handler): void
{
$routeStr = $routeData[0];
assert(is_string($routeStr));

if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
throw BadRouteException::alreadyRegistered($routeStr, $httpMethod);
Expand Down
8 changes: 5 additions & 3 deletions src/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

namespace FastRoute;

use FastRoute\Dispatcher\Result\Matched;
use FastRoute\Dispatcher\Result\MethodNotAllowed;
use FastRoute\Dispatcher\Result\NotMatched;

interface Dispatcher
{
public const NOT_FOUND = 0;
Expand All @@ -17,8 +21,6 @@
* [self::NOT_FOUND]
* [self::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
* [self::FOUND, $handler, ['varName' => 'value', ...]]
*
* @return array{0: int, 1?: list<string>|mixed, 2?: array<string, string>}
*/
public function dispatch(string $httpMethod, string $uri): array;
public function dispatch(string $httpMethod, string $uri): Matched|NotMatched|MethodNotAllowed;

Check failure on line 25 in src/Dispatcher.php

View workflow job for this annotation

GitHub Actions / Backwards compatibility check

The return type of FastRoute\Dispatcher#dispatch() changed from array to the non-covariant FastRoute\Dispatcher\Result\Matched|FastRoute\Dispatcher\Result\NotMatched|FastRoute\Dispatcher\Result\MethodNotAllowed

Check failure on line 25 in src/Dispatcher.php

View workflow job for this annotation

GitHub Actions / Backwards compatibility check

The return type of FastRoute\Dispatcher#dispatch() changed from array to FastRoute\Dispatcher\Result\Matched|FastRoute\Dispatcher\Result\NotMatched|FastRoute\Dispatcher\Result\MethodNotAllowed
}
15 changes: 12 additions & 3 deletions src/Dispatcher/CharCountBased.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@

namespace FastRoute\Dispatcher;

use FastRoute\Dispatcher\Result\Matched;

use function assert;
use function end;
use function preg_match;

class CharCountBased extends RegexBasedAbstract
{
/** @inheritDoc */
protected function dispatchVariableRoute(array $routeData, string $uri): ?array
protected function dispatchVariableRoute(array $routeData, string $uri): ?Matched

Check failure on line 15 in src/Dispatcher/CharCountBased.php

View workflow job for this annotation

GitHub Actions / Backwards compatibility check

The return type of FastRoute\Dispatcher\CharCountBased#dispatchVariableRoute() changed from array|null to the non-covariant FastRoute\Dispatcher\Result\Matched|null

Check failure on line 15 in src/Dispatcher/CharCountBased.php

View workflow job for this annotation

GitHub Actions / Backwards compatibility check

The return type of FastRoute\Dispatcher\CharCountBased#dispatchVariableRoute() changed from array|null to FastRoute\Dispatcher\Result\Matched|null
{
foreach ($routeData as $data) {
if (! preg_match($data['regex'], $uri . $data['suffix'], $matches)) {
assert(isset($data['suffix']));

if (preg_match($data['regex'], $uri . $data['suffix'], $matches) !== 1) {
continue;
}

Expand All @@ -24,7 +29,11 @@
$vars[$varName] = $matches[++$i];
}

return [self::FOUND, $handler, $vars];
$result = new Matched();
$result->handler = $handler;
$result->variables = $vars;

return $result;
}

return null;
Expand Down
12 changes: 9 additions & 3 deletions src/Dispatcher/GroupCountBased.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@

namespace FastRoute\Dispatcher;

use FastRoute\Dispatcher\Result\Matched;

use function count;
use function preg_match;

class GroupCountBased extends RegexBasedAbstract
{
/** @inheritDoc */
protected function dispatchVariableRoute(array $routeData, string $uri): ?array
protected function dispatchVariableRoute(array $routeData, string $uri): ?Matched

Check failure on line 14 in src/Dispatcher/GroupCountBased.php

View workflow job for this annotation

GitHub Actions / Backwards compatibility check

The return type of FastRoute\Dispatcher\GroupCountBased#dispatchVariableRoute() changed from array|null to the non-covariant FastRoute\Dispatcher\Result\Matched|null

Check failure on line 14 in src/Dispatcher/GroupCountBased.php

View workflow job for this annotation

GitHub Actions / Backwards compatibility check

The return type of FastRoute\Dispatcher\GroupCountBased#dispatchVariableRoute() changed from array|null to FastRoute\Dispatcher\Result\Matched|null
{
foreach ($routeData as $data) {
if (! preg_match($data['regex'], $uri, $matches)) {
if (preg_match($data['regex'], $uri, $matches) !== 1) {
continue;
}

Expand All @@ -24,7 +26,11 @@
$vars[$varName] = $matches[++$i];
}

return [self::FOUND, $handler, $vars];
$result = new Matched();
$result->handler = $handler;
$result->variables = $vars;

return $result;
}

return null;
Expand Down
15 changes: 12 additions & 3 deletions src/Dispatcher/GroupPosBased.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@

namespace FastRoute\Dispatcher;

use FastRoute\Dispatcher\Result\Matched;

use function assert;
use function preg_match;

class GroupPosBased extends RegexBasedAbstract
{
/** @inheritDoc */
protected function dispatchVariableRoute(array $routeData, string $uri): ?array
protected function dispatchVariableRoute(array $routeData, string $uri): ?Matched
{
foreach ($routeData as $data) {
if (! preg_match($data['regex'], $uri, $matches)) {
if (preg_match($data['regex'], $uri, $matches) !== 1) {
continue;
}

Expand All @@ -20,14 +23,20 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?array
for ($i = 1; $matches[$i] === ''; ++$i) {
}

assert(isset($i));

[$handler, $varNames] = $data['routeMap'][$i];

$vars = [];
foreach ($varNames as $varName) {
$vars[$varName] = $matches[$i++];
}

return [self::FOUND, $handler, $vars];
$result = new Matched();
$result->handler = $handler;
$result->variables = $vars;

return $result;
}

return null;
Expand Down
12 changes: 9 additions & 3 deletions src/Dispatcher/MarkBased.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@

namespace FastRoute\Dispatcher;

use FastRoute\Dispatcher\Result\Matched;

use function preg_match;

class MarkBased extends RegexBasedAbstract
{
/** @inheritDoc */
protected function dispatchVariableRoute(array $routeData, string $uri): ?array
protected function dispatchVariableRoute(array $routeData, string $uri): ?Matched
{
foreach ($routeData as $data) {
if (! preg_match($data['regex'], $uri, $matches)) {
if (preg_match($data['regex'], $uri, $matches) !== 1) {
continue;
}

Expand All @@ -23,7 +25,11 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?array
$vars[$varName] = $matches[++$i];
}

return [self::FOUND, $handler, $vars];
$result = new Matched();
$result->handler = $handler;
$result->variables = $vars;

return $result;
}

return null;
Expand Down
Loading