summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Light <sam@lightscale.co.uk>2026-06-10 19:00:32 +0100
committerSam Light <sam@lightscale.co.uk>2026-06-10 19:00:32 +0100
commit8c0efd0d9317ad92bd55cd6afcd41bdbab827bf8 (patch)
treec654456753e90ab38b7bcb89ece3381ad7a40f4a
parent5fe7c87967ff29c4a8f03a9186918d8359f4887e (diff)
Make basic routing work
-rw-r--r--composer.json9
-rw-r--r--composer.lock296
-rw-r--r--phpstan.neon8
-rw-r--r--src/BasicStrategy.php23
-rw-r--r--src/Contracts/Strategy.php15
-rw-r--r--src/Route.php11
-rw-r--r--src/RouteCall.php17
-rw-r--r--src/Router.php77
-rw-r--r--tests/Unit/BasicStrategyTest.php44
-rw-r--r--tests/Unit/PathSegmentMatchTest.php17
-rw-r--r--tests/Unit/RouteCallTest.php22
-rw-r--r--tests/Unit/RouteTest.php14
-rw-r--r--tests/Unit/RouterTest.php56
-rw-r--r--tests/Utils/TestCall.php16
-rw-r--r--tests/Utils/TestCallable.php69
15 files changed, 664 insertions, 30 deletions
diff --git a/composer.json b/composer.json
index 6ecde7c..ac85123 100644
--- a/composer.json
+++ b/composer.json
@@ -24,11 +24,16 @@
"phpstan/phpstan": "^2.2",
"friendsofphp/php-cs-fixer": "^3.95",
"ace-of-aces/intellipest": "^1.0",
- "nyholm/psr7": "^1.8"
+ "nyholm/psr7": "^1.8",
+ "mockery/mockery": "^1.6",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan-mockery": "^2.0",
+ "mrpunyapal/peststan": "^0.2.10"
},
"config": {
"allow-plugins": {
- "pestphp/pest-plugin": true
+ "pestphp/pest-plugin": true,
+ "phpstan/extension-installer": true
}
},
"scripts": {
diff --git a/composer.lock b/composer.lock
index 707d8d0..c76f8e8 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "1b580d80d8e453e316c8a11cd22f4157",
+ "content-hash": "207c08be14a5ab20d538c75ead372f1d",
"packages": [
{
"name": "psr/http-message",
@@ -892,6 +892,57 @@
"time": "2026-05-29T20:35:26+00:00"
},
{
+ "name": "hamcrest/hamcrest-php",
+ "version": "v2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/hamcrest/hamcrest-php.git",
+ "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "replace": {
+ "cordoval/hamcrest-php": "*",
+ "davedevelopment/hamcrest-php": "*",
+ "kodova/hamcrest-php": "*"
+ },
+ "require-dev": {
+ "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0",
+ "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "hamcrest"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "This is the PHP port of Hamcrest Matchers",
+ "keywords": [
+ "test"
+ ],
+ "support": {
+ "issues": "https://github.com/hamcrest/hamcrest-php/issues",
+ "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1"
+ },
+ "time": "2025-04-30T06:54:44+00:00"
+ },
+ {
"name": "jean85/pretty-package-versions",
"version": "2.1.1",
"source": {
@@ -952,6 +1003,152 @@
"time": "2025-03-19T14:43:43+00:00"
},
{
+ "name": "mockery/mockery",
+ "version": "1.6.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mockery/mockery.git",
+ "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699",
+ "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699",
+ "shasum": ""
+ },
+ "require": {
+ "hamcrest/hamcrest-php": "^2.0.1",
+ "lib-pcre": ">=7.0",
+ "php": ">=7.3"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.6.17",
+ "symplify/easy-coding-standard": "^12.1.14"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "library/helpers.php",
+ "library/Mockery.php"
+ ],
+ "psr-4": {
+ "Mockery\\": "library/Mockery"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Pádraic Brady",
+ "email": "padraic.brady@gmail.com",
+ "homepage": "https://github.com/padraic",
+ "role": "Author"
+ },
+ {
+ "name": "Dave Marshall",
+ "email": "dave.marshall@atstsolutions.co.uk",
+ "homepage": "https://davedevelopment.co.uk",
+ "role": "Developer"
+ },
+ {
+ "name": "Nathanael Esayeas",
+ "email": "nathanael.esayeas@protonmail.com",
+ "homepage": "https://github.com/ghostwriter",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "Mockery is a simple yet flexible PHP mock object framework",
+ "homepage": "https://github.com/mockery/mockery",
+ "keywords": [
+ "BDD",
+ "TDD",
+ "library",
+ "mock",
+ "mock objects",
+ "mockery",
+ "stub",
+ "test",
+ "test double",
+ "testing"
+ ],
+ "support": {
+ "docs": "https://docs.mockery.io/",
+ "issues": "https://github.com/mockery/mockery/issues",
+ "rss": "https://github.com/mockery/mockery/releases.atom",
+ "security": "https://github.com/mockery/mockery/security/advisories",
+ "source": "https://github.com/mockery/mockery"
+ },
+ "time": "2024-05-16T03:13:13+00:00"
+ },
+ {
+ "name": "mrpunyapal/peststan",
+ "version": "0.2.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MrPunyapal/PestStan.git",
+ "reference": "750859a911050915cb6e3efbfde30e900ad717bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MrPunyapal/PestStan/zipball/750859a911050915cb6e3efbfde30e900ad717bf",
+ "reference": "750859a911050915cb6e3efbfde30e900ad717bf",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2",
+ "phpstan/phpstan": "^2.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.18",
+ "mrpunyapal/rector-pest": "^0.2.0",
+ "nunomaduro/pao": "^0.1.4",
+ "pestphp/pest": "^3.0 || ^4.0 || ^5.0",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "rector/rector": "^2.0"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PestStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan extension for Pest PHP testing framework",
+ "keywords": [
+ "PHPStan",
+ "pest",
+ "static-analysis",
+ "testing"
+ ],
+ "support": {
+ "issues": "https://github.com/MrPunyapal/PestStan/issues",
+ "source": "https://github.com/MrPunyapal/PestStan/tree/0.2.10"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mrpunyapal",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-10T16:55:11+00:00"
+ },
+ {
"name": "myclabs/deep-copy",
"version": "1.13.4",
"source": {
@@ -2016,6 +2213,54 @@
"time": "2026-01-06T21:53:42+00:00"
},
{
+ "name": "phpstan/extension-installer",
+ "version": "1.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/extension-installer.git",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.0",
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpstan": "^1.9.0 || ^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2.0",
+ "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPStan\\ExtensionInstaller\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\ExtensionInstaller\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Composer plugin for automatic installation of PHPStan extensions",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpstan/extension-installer/issues",
+ "source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
+ },
+ "time": "2024-09-04T20:21:43+00:00"
+ },
+ {
"name": "phpstan/phpdoc-parser",
"version": "2.3.2",
"source": {
@@ -2127,6 +2372,55 @@
"time": "2026-05-28T14:44:12+00:00"
},
{
+ "name": "phpstan/phpstan-mockery",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-mockery.git",
+ "reference": "89a949d0ac64298e88b7c7fa00caee565c198394"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-mockery/zipball/89a949d0ac64298e88b7c7fa00caee565c198394",
+ "reference": "89a949d0ac64298e88b7c7fa00caee565c198394",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.6.11",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan Mockery extension",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-mockery/issues",
+ "source": "https://github.com/phpstan/phpstan-mockery/tree/2.0.0"
+ },
+ "time": "2024-10-14T03:18:12+00:00"
+ },
+ {
"name": "phpunit/php-code-coverage",
"version": "12.5.6",
"source": {
diff --git a/phpstan.neon b/phpstan.neon
index 6e02a9e..bbd57ba 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -5,11 +5,7 @@ parameters:
- tests
ignoreErrors:
-
- message: '#Call to an undefined method Pest\\PendingCalls\\TestCall::[a-zA-Z0-9\\_]+#'
- path: tests
- reportUnmatched: false
- -
- message: '#Access to an undefined property Pest\\(Mixins\\)?Expectation#'
- identifier: property.notFound
+ message: '#Call to an undefined method Pest\\Expectation<#'
path: tests
+ identifier: method.notFound
reportUnmatched: false
diff --git a/src/BasicStrategy.php b/src/BasicStrategy.php
new file mode 100644
index 0000000..62bc48e
--- /dev/null
+++ b/src/BasicStrategy.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Lightscale\Router;
+
+use Lightscale\Router\Contracts\Strategy;
+use Lightscale\Router\Exceptions\NotFoundException;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+class BasicStrategy implements Strategy
+{
+ public function runRoute(RouteCall $call): ResponseInterface
+ {
+ return ($call->route->getHandler())($call);
+ }
+
+ public function notFound(RequestInterface $request): ResponseInterface
+ {
+ throw new NotFoundException();
+ }
+}
diff --git a/src/Contracts/Strategy.php b/src/Contracts/Strategy.php
new file mode 100644
index 0000000..81d6ec3
--- /dev/null
+++ b/src/Contracts/Strategy.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Lightscale\Router\Contracts;
+
+use Lightscale\Router\RouteCall;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+interface Strategy
+{
+ public function runRoute(RouteCall $call): ResponseInterface;
+ public function notFound(RequestInterface $request): ResponseInterface;
+}
diff --git a/src/Route.php b/src/Route.php
index aeb3f66..af08ded 100644
--- a/src/Route.php
+++ b/src/Route.php
@@ -6,7 +6,6 @@ namespace Lightscale\Router;
use Closure;
use Lightscale\Router\Enums\HttpMethod;
-use Psr\Http\Message\RequestInterface;
class Route
{
@@ -26,11 +25,6 @@ class Route
}
}
- public function __invoke(RequestInterface $request): mixed
- {
- return ($this->handler)($request);
- }
-
public function getMethod(): HttpMethod
{
return $this->method;
@@ -50,4 +44,9 @@ class Route
{
return $this->segment->getPath();
}
+
+ public function getHandler(): Closure
+ {
+ return $this->handler;
+ }
}
diff --git a/src/RouteCall.php b/src/RouteCall.php
new file mode 100644
index 0000000..b76a780
--- /dev/null
+++ b/src/RouteCall.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Lightscale\Router;
+
+use Psr\Http\Message\RequestInterface;
+
+class RouteCall
+{
+ /** @param array<string, string> $parameters */
+ public function __construct(
+ public RequestInterface $request,
+ public Route $route,
+ public array $parameters,
+ ) {}
+}
diff --git a/src/Router.php b/src/Router.php
index f666fe5..ddd3220 100644
--- a/src/Router.php
+++ b/src/Router.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Lightscale\Router;
+use Lightscale\Router\Contracts\Strategy;
use Lightscale\Router\Enums\HttpMethod;
use Lightscale\Router\Enums\PathSegmentType;
use Lightscale\Router\Exceptions\NotFoundException;
@@ -14,10 +15,22 @@ use Psr\Http\Message\ResponseInterface;
class Router
{
private PathSegment $root;
+ private Strategy $strategy;
public function __construct()
{
$this->root = new PathSegment(type: PathSegmentType::Root);
+ $this->strategy = new BasicStrategy;
+ }
+
+ public function getStrategy(): Strategy
+ {
+ return $this->strategy;
+ }
+
+ public function setStrategy(Strategy $strategy): void
+ {
+ $this->strategy = $strategy;
}
public function root(): PathSegment
@@ -60,21 +73,71 @@ class Router
$match = $this->findSegment($uri->getPath());
if (null === $match) {
- throw new NotFoundException();
+ return $this->strategy->notFound($request);
}
$segment = $match->segment;
$method = $request->getMethod();
- $method = (
- HttpMethod::tryFrom(strtolower($method)) ??
- throw new UnknownMethodException()
- );
+ $method = HttpMethod::tryFrom(strtolower($method));
+
+ if ($method === null) {
+ return $this->strategy->notFound($request);
+ }
+
$route = $segment->getRoute($method);
if (null === $route) {
- throw new NotFoundException();
+ return $this->strategy->notFound($request);
+ }
+
+ $call = new RouteCall(
+ $request,
+ $route,
+ $match->parameters,
+ );
+
+ return $this->strategy->runRoute($call);
+ }
+
+ public function route(HttpMethod $method, string $path, callable $handler): void
+ {
+ $pathSplit = $this->splitPath($path);
+
+ $seg = $this->root();
+ while(($v = array_shift($pathSplit)) !== null) {
+ $seg = $seg->child($v);
}
- return $route($request);
+ $seg->addRoute(new Route($method, $handler));
+ }
+
+ public function get(string $path, callable $handler): void
+ {
+ $this->route(HttpMethod::Get, $path, $handler);
+ }
+
+ public function post(string $path, callable $handler): void
+ {
+ $this->route(HttpMethod::Post, $path, $handler);
+ }
+
+ public function put(string $path, callable $handler): void
+ {
+ $this->route(HttpMethod::Put, $path, $handler);
+ }
+
+ public function patch(string $path, callable $handler): void
+ {
+ $this->route(HttpMethod::Patch, $path, $handler);
+ }
+
+ public function delete(string $path, callable $handler): void
+ {
+ $this->route(HttpMethod::Delete, $path, $handler);
+ }
+
+ public function any(string $path, callable $handler): void
+ {
+ $this->route(HttpMethod::Any, $path, $handler);
}
}
diff --git a/tests/Unit/BasicStrategyTest.php b/tests/Unit/BasicStrategyTest.php
new file mode 100644
index 0000000..2a0b550
--- /dev/null
+++ b/tests/Unit/BasicStrategyTest.php
@@ -0,0 +1,44 @@
+<?php
+
+use Lightscale\Router\BasicStrategy;
+use Lightscale\Router\Enums\HttpMethod;
+use Lightscale\Router\Exceptions\NotFoundException;
+use Lightscale\Router\Route;
+use Lightscale\Router\RouteCall;
+use Lightscale\Router\Test\Utils\TestCallable;
+use Nyholm\Psr7\Factory\Psr17Factory;
+
+it('initialises')
+ ->expect(fn() => new BasicStrategy)
+ ->toBeInstanceOf(BasicStrategy::class);
+
+it('throws on not found', function() {
+ (new BasicStrategy)->notFound(
+ (new Psr17Factory)->createServerRequest(
+ HttpMethod::Get->value,
+ '/testing/testing'
+ )
+ );
+})->throws(NotFoundException::class);
+
+it('calls route', function () {
+ $factory = new Psr17Factory;
+ $response = $factory->createResponse(200, 'OK');
+ $cb = TestCallable::make(fn () => $response);
+ $res = (new BasicStrategy)->runRoute($rc = new RouteCall(
+ $factory->createServerRequest(
+ HttpMethod::Get->value,
+ '/testing/testing'
+ ),
+ new Route(
+ HttpMethod::Get,
+ $cb
+ ),
+ []
+ ));
+
+ $cb->assertIsCalled();
+ $call = $cb->getLastCall();
+ expect($call->args[0] ?? null)->toBe($rc);
+ expect($res)->toBe($response);
+});
diff --git a/tests/Unit/PathSegmentMatchTest.php b/tests/Unit/PathSegmentMatchTest.php
new file mode 100644
index 0000000..9a522a4
--- /dev/null
+++ b/tests/Unit/PathSegmentMatchTest.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+use Lightscale\Router\PathSegment;
+use Lightscale\Router\PathSegmentMatch;
+
+it('initializes with segment and params', function() {
+ $match = new PathSegmentMatch(
+ $seg = new PathSegment('testing'),
+ $params = ['test1' => 'test2'],
+ );
+
+ expect($match)
+ ->segment->toBe($seg)
+ ->parameters->toBe($params);
+});
diff --git a/tests/Unit/RouteCallTest.php b/tests/Unit/RouteCallTest.php
new file mode 100644
index 0000000..70cc50e
--- /dev/null
+++ b/tests/Unit/RouteCallTest.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+use Lightscale\Router\Enums\HttpMethod;
+use Lightscale\Router\Route;
+use Lightscale\Router\RouteCall;
+use Nyholm\Psr7\Factory\Psr17Factory;
+
+it('initializes with data', function () {
+ $factory = new Psr17Factory;
+ $call = new RouteCall(
+ request: $req = $factory->createServerRequest(HttpMethod::Get->value, '/test/test'),
+ route: $route = new Route(HttpMethod::Get, fn () => null),
+ parameters: $params = ['test1' => 'test2'],
+ );
+
+ expect($call)
+ ->request->toBe($req)
+ ->route->toBe($route)
+ ->parameters->toBe($params);
+});
diff --git a/tests/Unit/RouteTest.php b/tests/Unit/RouteTest.php
index 104401b..50513a1 100644
--- a/tests/Unit/RouteTest.php
+++ b/tests/Unit/RouteTest.php
@@ -47,12 +47,10 @@ it('gets method')
))->getMethod())
->toBe(HttpMethod::Get);
-it('can call handler')
- ->expect(fn () => (new Route(
+it('can get handler', function() {
+ $r = new Route(
HttpMethod::Get,
- fn (RequestInterface $req) => $req->getUri()->getPath()
- ))(
- (new Psr17Factory())->createRequest(HttpMethod::Get->value, '/testing')
- )
- )
- ->toBe('/testing');
+ $h = fn (RequestInterface $req) => $req->getUri()->getPath()
+ );
+ expect($r->getHandler())->toBe($h);
+});
diff --git a/tests/Unit/RouterTest.php b/tests/Unit/RouterTest.php
index 5d08441..12e27a4 100644
--- a/tests/Unit/RouterTest.php
+++ b/tests/Unit/RouterTest.php
@@ -2,14 +2,32 @@
declare(strict_types=1);
+use Lightscale\Router\BasicStrategy;
+use Lightscale\Router\Contracts\Strategy;
+use Lightscale\Router\Enums\HttpMethod;
use Lightscale\Router\Enums\PathSegmentType;
use Lightscale\Router\PathSegment;
+use Lightscale\Router\Route;
+use Lightscale\Router\RouteCall;
use Lightscale\Router\Router;
+use Nyholm\Psr7\Factory\Psr17Factory;
it('initializes')
->expect(fn () => new Router())
->toBeInstanceOf(Router::class);
+it('initializes with basic strategy')
+ ->expect((new Router)->getStrategy())
+ ->toBeInstanceOf(BasicStrategy::class);
+
+it('can set strategy', function() {
+ $router = new Router;
+ $s = new BasicStrategy;
+ expect($router->getStrategy())->not->toBe($s);
+ $router->setStrategy($s);
+ expect($router->getStrategy())->toBe($s);
+});
+
it('has root')
->expect(fn () => (new Router())->root())
->toBeInstanceOf(PathSegment::class)
@@ -88,3 +106,41 @@ it('return null when segment not found', function () {
expect($router->findSegment('/testing/testing'))->toBeNull();
});
+
+it('calls strategy notFound with dispatch', function () {
+ $router = new Router();
+ $factory = new Psr17Factory;
+ $request = $factory->createServerRequest(HttpMethod::Get->value, '/testing/testing');
+ $response = $factory->createResponse();
+
+ $strat = Mockery::mock(Strategy::class);
+ $strat->shouldReceive('notFound')
+ ->with($request)
+ ->andReturn($response);
+
+ $router->setStrategy($strat);
+ $result = $router->dispatch($request);
+ expect($result)->toBe($response);
+});
+
+it('calls strategy runRoute with dispatch on match', function() {
+ $router = new Router();
+
+ $router->root()->child('testing')->addRoute(new Route(
+ HttpMethod::Get,
+ fn () => null,
+ ));
+
+ $factory = new Psr17Factory;
+ $request = $factory->createServerRequest(HttpMethod::Get->value, '/testing');
+ $response = $factory->createResponse();
+
+ $strat = Mockery::mock(Strategy::class);
+ $strat->shouldReceive('runRoute')
+ ->with(Mockery::type(RouteCall::class))
+ ->andReturn($response);
+
+ $router->setStrategy($strat);
+ $result = $router->dispatch($request);
+ expect($result)->toBe($response);
+});
diff --git a/tests/Utils/TestCall.php b/tests/Utils/TestCall.php
new file mode 100644
index 0000000..971e6a7
--- /dev/null
+++ b/tests/Utils/TestCall.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Lightscale\Router\Test\Utils;
+
+readonly class TestCall
+{
+ /**
+ * @param mixed[] $args
+ */
+ public function __construct(
+ public array $args,
+ public mixed $return,
+ ){}
+}
diff --git a/tests/Utils/TestCallable.php b/tests/Utils/TestCallable.php
new file mode 100644
index 0000000..42a3bb5
--- /dev/null
+++ b/tests/Utils/TestCallable.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Lightscale\Router\Test\Utils;
+
+use Closure;
+use PHPUnit\Framework\Assert;
+
+class TestCallable
+{
+ private Closure $cb;
+
+ /** @var TestCall[] */
+ private array $calls;
+
+ final public function __construct(callable $cb)
+ {
+ $this->cb = Closure::fromCallable($cb);
+ }
+
+ public static function make(callable $cb): static
+ {
+ return new static($cb);
+ }
+
+ public function __invoke(mixed ...$args): mixed
+ {
+ $call = new TestCall(
+ args: $args,
+ return: ($this->cb)(...$args),
+ );
+ $this->calls[] = $call;
+
+ return $call->return;
+ }
+
+ public function getCallCount(): int
+ {
+ return count($this->calls);
+ }
+
+ /** @return TestCall[] */
+ public function getCalls(): array
+ {
+ return $this->calls;
+ }
+
+ public function getLastCall(): ?TestCall
+ {
+ return $this->calls[$this->getCallCount() - 1] ?? null;
+ }
+
+ public function assertIsCalled(): void
+ {
+ Assert::assertGreaterThan(0, $this->getCallCount(), 'Not been called');
+ }
+
+ public function assertNotCalled(): void
+ {
+ Assert::assertSame(0, $this->getCallCount());
+ }
+
+ public function assertCalledTimes(int $amount): void
+ {
+ Assert::assertSame($amount, $this->getCallCount());
+ }
+
+}