diff --git a/.gitignore b/.gitignore
index 8c81ab2..7fc4a65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,4 +28,4 @@ phpunit.phar
/phpunit.xml
# phpunit cache
.phpunit.result.cache
-/phpunit.cache
+.phpunit.cache
diff --git a/README.md b/README.md
index 8e5b84f..c258eaf 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,18 @@
-
Yii _____
+ File Router
-[![Latest Stable Version](https://poser.pugx.org/yiisoft/_____/v/stable.png)](https://packagist.org/packages/yiisoft/_____)
-[![Total Downloads](https://poser.pugx.org/yiisoft/_____/downloads.png)](https://packagist.org/packages/yiisoft/_____)
-[![Build status](https://github.com/yiisoft/_____/workflows/build/badge.svg)](https://github.com/yiisoft/_____/actions?query=workflow%3Abuild)
-[![Code Coverage](https://codecov.io/gh/yiisoft/_____/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/_____)
-[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2F_____%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/_____/master)
-[![static analysis](https://github.com/yiisoft/_____/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/_____/actions?query=workflow%3A%22static+analysis%22)
-[![type-coverage](https://shepherd.dev/github/yiisoft/_____/coverage.svg)](https://shepherd.dev/github/yiisoft/_____)
-[![psalm-level](https://shepherd.dev/github/yiisoft/_____/level.svg)](https://shepherd.dev/github/yiisoft/_____)
+[![Latest Stable Version](https://poser.pugx.org/yiisoft/File Router/v/stable.png)](https://packagist.org/packages/yiisoft/File Router)
+[![Total Downloads](https://poser.pugx.org/yiisoft/File Router/downloads.png)](https://packagist.org/packages/yiisoft/File Router)
+[![Build status](https://github.com/yiisoft/File Router/workflows/build/badge.svg)](https://github.com/yiisoft/File Router/actions?query=workflow%3Abuild)
+[![Code Coverage](https://codecov.io/gh/yiisoft/File Router/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/File Router)
+[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2FFile Router%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/File Router/master)
+[![static analysis](https://github.com/yiisoft/File Router/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/File Router/actions?query=workflow%3A%22static+analysis%22)
+[![type-coverage](https://shepherd.dev/github/yiisoft/File Router/coverage.svg)](https://shepherd.dev/github/yiisoft/File Router)
+[![psalm-level](https://shepherd.dev/github/yiisoft/File Router/level.svg)](https://shepherd.dev/github/yiisoft/File Router)
The package ...
@@ -26,7 +26,7 @@ The package ...
The package could be installed with composer:
```shell
-composer require yiisoft/_____
+composer require yiisoft/File Router
```
## General usage
@@ -74,7 +74,7 @@ Use [ComposerRequireChecker](https://github.com/maglnet/ComposerRequireChecker)
## License
-The Yii _____ is free software. It is released under the terms of the BSD License.
+The File Router is free software. It is released under the terms of the BSD License.
Please see [`LICENSE`](./LICENSE.md) for more information.
Maintained by [Yii Software](https://www.yiiframework.com/).
diff --git a/composer.json b/composer.json
index 62faeea..48aa138 100644
--- a/composer.json
+++ b/composer.json
@@ -1,19 +1,23 @@
{
- "name": "yiisoft/_____",
+ "name": "yiisoft/file-router",
"type": "library",
- "description": "_____",
+ "description": "File based router",
"keywords": [
- "_____"
+ "file-router",
+ "router",
+ "routing",
+ "convention-router",
+ "structure-router"
],
"homepage": "https://www.yiiframework.com/",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/_____/issues?state=open",
+ "issues": "https://github.com/yiisoft/file-router/issues?state=open",
"forum": "https://www.yiiframework.com/forum/",
"wiki": "https://www.yiiframework.com/wiki/",
"irc": "ircs://irc.libera.chat:6697/yii",
"chat": "https://t.me/yii3en",
- "source": "https://github.com/yiisoft/_____"
+ "source": "https://github.com/yiisoft/file-router"
},
"funding": [
{
@@ -28,24 +32,31 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
- "php": "^8.1"
+ "php": "^8.1",
+ "psr/http-message": "^2.0",
+ "psr/http-server-middleware": "^1.0",
+ "yiisoft/middleware-dispatcher": "^5.2"
},
"require-dev": {
+ "httpsoft/http-message": "^1.1",
+ "httpsoft/http-response": "^1.1",
"maglnet/composer-require-checker": "^4.7",
"phpunit/phpunit": "^10.5",
"rector/rector": "^0.18.11",
"roave/infection-static-analysis-plugin": "^1.34",
"spatie/phpunit-watcher": "^1.23",
- "vimeo/psalm": "^5.16"
+ "vimeo/psalm": "^5.16",
+ "yiisoft/strings": "^2.4",
+ "yiisoft/test-support": "^3.0"
},
"autoload": {
"psr-4": {
- "Yiisoft\\_____\\": "src"
+ "Yiisoft\\FileRouter\\": "src"
}
},
"autoload-dev": {
"psr-4": {
- "Yiisoft\\_____\\Tests\\": "tests"
+ "Yiisoft\\FileRouter\\Tests\\": "tests"
}
},
"config": {
diff --git a/config/web.php b/config/web.php
new file mode 100644
index 0000000..69d387a
--- /dev/null
+++ b/config/web.php
@@ -0,0 +1,21 @@
+ function (ContainerInterface $container) {
+ $eventDispatcher = $container->has(EventDispatcherInterface::class)
+ ? $container->get(EventDispatcherInterface::class)
+ : null;
+
+ $middlewareFactory = $container->get(MiddlewareFactory::class);
+
+ return new FileRouter(new MiddlewareDispatcher($middlewareFactory, $eventDispatcher));
+ },
+];
diff --git a/src/.gitkeep b/src/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/FileRouter.php b/src/FileRouter.php
new file mode 100644
index 0000000..4e8c033
--- /dev/null
+++ b/src/FileRouter.php
@@ -0,0 +1,136 @@
+baseControllerDirectory = $directory;
+
+ return $new;
+ }
+
+ public function withClassPostfix(string $postfix): self
+ {
+ $new = clone $this;
+ $new->classPostfix = $postfix;
+
+ return $new;
+ }
+
+ public function withNamespace(string $namespace): self
+ {
+ $new = clone $this;
+ $new->namespace = $namespace;
+
+ return $new;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $controllerClass = $this->parseController($request);
+ if ($controllerClass === null) {
+ return $handler->handle($request);
+ }
+ $action = $this->parseAction($request);
+
+ if (!method_exists($controllerClass, $action)) {
+ return $handler->handle($request);
+ }
+
+ $middlewares = $controllerClass::$middlewares[$action] ?? [];
+ $middlewares[] = [$controllerClass, $action];
+
+ $middlewareDispatcher = $this->middlewareDispatcher->withMiddlewares($middlewares);
+
+ return $middlewareDispatcher->dispatch($request, $handler);
+ }
+
+ private function parseAction(ServerRequestInterface $request): ?string
+ {
+ switch ($request->getMethod()) {
+ case 'HEAD':
+ case 'GET':
+ $action = 'index';
+ break;
+ case 'POST':
+ $action = 'create';
+ break;
+ case 'PUT':
+ $action = 'update';
+ break;
+ case 'DELETE':
+ $action = 'delete';
+ break;
+ default:
+ throw new Exception('Not implemented.');
+ }
+ return $action;
+ }
+
+ private function parseController(ServerRequestInterface $request): mixed
+ {
+ $path = $request->getUri()->getPath();
+ if ($path === '/') {
+ $controllerName = 'Index';
+ $directoryPath = '';
+ } else {
+ $controllerName = preg_replace_callback(
+ '#(/.)#',
+ fn(array $matches) => strtoupper($matches[1]),
+ str_replace('/', DIRECTORY_SEPARATOR, $path)
+ );
+ $directoryPath = StringHelper::directoryName($controllerName);
+
+ $controllerName = StringHelper::basename($controllerName);
+ }
+
+ $controller = $controllerName . $this->classPostfix;
+ $className = str_replace(
+ ['/', '\\\\'],
+ ['\\', '\\'],
+ $this->namespace . '\\' . $this->baseControllerDirectory . '\\' . $directoryPath . '\\' . $controller
+ );
+
+ if (class_exists($className)) {
+ return $className;
+ }
+
+ // alternative version finding namespace by file
+
+
+ //$controllerDirectory = $this->aliases->get('src') . DIRECTORY_SEPARATOR . $this->baseControllerDirectory;
+ //$classPath = $controllerDirectory . DIRECTORY_SEPARATOR . $directoryPath . DIRECTORY_SEPARATOR . $controller;
+ //$filename = $classPath . '.php';
+ //if (file_exists($filename)) {
+ // $content = file_get_contents($filename);
+ // $namespace = preg_match('#namespace\s+(.+?);#', $content, $matches) ? $matches[1] : '';
+ // if (class_exists($namespace . '\\' . $controller)) {
+ // return $namespace . '\\' . $controller;
+ // }
+ //}
+
+ return null;
+ }
+}
diff --git a/tests/.gitkeep b/tests/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/FileRouterTest.php b/tests/FileRouterTest.php
new file mode 100644
index 0000000..9769798
--- /dev/null
+++ b/tests/FileRouterTest.php
@@ -0,0 +1,172 @@
+createRouter();
+ $router = $router
+ ->withNamespace('Yiisoft\FileRouter\Tests\Support\App1')
+ ->withBaseControllerDirectory('Controller');
+
+ $handler = $this->createExceptionHandler();
+ $request = new ServerRequest(
+ method: 'GET',
+ uri: '/user/blog',
+ );
+
+ $response = $router->process($request, $handler);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('Hello, index!', (string) $response->getBody());
+ $this->assertEquals('x-header-value', $response->getHeaderLine('X-Header-Name'));
+ }
+
+ #[DataProvider('dataRouter')]
+ public function testRouter(string $method, string $uri, string $expectedResponse): void
+ {
+ /**
+ * @var FileRouter $router
+ */
+ $router = $this->createRouter();
+ $router = $router
+ ->withNamespace('Yiisoft\FileRouter\Tests\Support\App1');
+
+ $handler = $this->createExceptionHandler();
+ $request = new ServerRequest(
+ method: $method,
+ uri: $uri,
+ );
+
+ $response = $router->process($request, $handler);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals($expectedResponse, (string) $response->getBody());
+ }
+
+ public static function dataRouter(): iterable
+ {
+ yield 'GET /user' => [
+ 'GET',
+ '/user',
+ 'Hello, index!',
+ ];
+ yield 'POST /user' => [
+ 'POST',
+ '/user',
+ 'Hello, create!',
+ ];
+ yield 'PUT /user' => [
+ 'PUT',
+ '/user',
+ 'Hello, update!',
+ ];
+ yield 'DELETE /user' => [
+ 'DELETE',
+ '/user',
+ 'Hello, delete!',
+ ];
+ }
+
+ public function testUnsupportedMethod(): void
+ {
+ $router = $this->createRouter();
+ $router = $router
+ ->withNamespace('Yiisoft\FileRouter\Tests\Support\App1');
+
+ $handler = $this->createExceptionHandler();
+ $request = new ServerRequest(
+ method: 'DELETE',
+ uri: '/',
+ );
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Not implemented from tests.');
+ $router->process($request, $handler);
+ }
+
+ public function testBaseController(): void
+ {
+ $router = $this->createRouter();
+ $router = $router
+ ->withNamespace('Yiisoft\FileRouter\Tests\Support\App1');
+
+ $handler = $this->createExceptionHandler();
+ $request = new ServerRequest(
+ method: 'GET',
+ uri: '/',
+ );
+
+ $response = $router->process($request, $handler);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('Hello, index!', (string) $response->getBody());
+ }
+
+ public function testUnusualControllerDirectory(): void
+ {
+ $router = $this->createRouter();
+ $router = $router
+ ->withNamespace('Yiisoft\FileRouter\Tests\Support\App2')
+ ->withBaseControllerDirectory('Action')
+ ->withClassPostfix('Action');
+
+ $handler = $this->createExceptionHandler();
+ $request = new ServerRequest(
+ method: 'GET',
+ uri: '/user',
+ );
+
+ $response = $router->process($request, $handler);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('Hello, user action!', (string) $response->getBody());
+ }
+
+ private function createRouter(): FileRouter
+ {
+ $container = new SimpleContainer([
+ HeaderMiddleware::class => new HeaderMiddleware(),
+ BlogController::class => new BlogController(),
+ UserController::class => new UserController(),
+ IndexController::class => new IndexController(),
+ UserAction::class => new UserAction(),
+ ]);
+
+ return new FileRouter(
+ new MiddlewareDispatcher(new MiddlewareFactory($container), null)
+ );
+ }
+
+ private function createExceptionHandler(): RequestHandlerInterface
+ {
+ return new class implements RequestHandlerInterface {
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ throw new \Exception('Not implemented from tests.');
+ }
+ };
+ {
+ }
+ }
+}
diff --git a/tests/Support/App1/Controller/IndexController.php b/tests/Support/App1/Controller/IndexController.php
new file mode 100644
index 0000000..88abb86
--- /dev/null
+++ b/tests/Support/App1/Controller/IndexController.php
@@ -0,0 +1,16 @@
+ 'X-Header-Value']);
+ }
+}
diff --git a/tests/Support/App1/Controller/User/BlogController.php b/tests/Support/App1/Controller/User/BlogController.php
new file mode 100644
index 0000000..5215cda
--- /dev/null
+++ b/tests/Support/App1/Controller/User/BlogController.php
@@ -0,0 +1,23 @@
+ [
+ HeaderMiddleware::class,
+ ],
+ ];
+
+ public function index(): ResponseInterface
+ {
+ return new TextResponse('Hello, index!');
+ }
+}
diff --git a/tests/Support/App1/Controller/UserController.php b/tests/Support/App1/Controller/UserController.php
new file mode 100644
index 0000000..1cc4418
--- /dev/null
+++ b/tests/Support/App1/Controller/UserController.php
@@ -0,0 +1,38 @@
+ [
+ HeaderMiddleware::class,
+ ],
+ ];
+
+ public function index(): ResponseInterface
+ {
+ return new TextResponse('Hello, index!');
+ }
+
+ public function create(): ResponseInterface
+ {
+ return new TextResponse('Hello, create!');
+ }
+
+ public function update(): ResponseInterface
+ {
+ return new TextResponse('Hello, update!');
+ }
+
+ public function delete(): ResponseInterface
+ {
+ return new TextResponse('Hello, delete!');
+ }
+}
diff --git a/tests/Support/App2/Action/UserAction.php b/tests/Support/App2/Action/UserAction.php
new file mode 100644
index 0000000..72cf4f9
--- /dev/null
+++ b/tests/Support/App2/Action/UserAction.php
@@ -0,0 +1,23 @@
+ [
+ HeaderMiddleware::class,
+ ],
+ ];
+
+ public function index(): ResponseInterface
+ {
+ return new TextResponse('Hello, user action!');
+ }
+}
diff --git a/tests/Support/HeaderMiddleware.php b/tests/Support/HeaderMiddleware.php
new file mode 100644
index 0000000..59dbdb5
--- /dev/null
+++ b/tests/Support/HeaderMiddleware.php
@@ -0,0 +1,23 @@
+handle($request);
+
+ return $response->withHeader(self::X_HEADER_NAME, self::X_HEADER_VALUE);
+ }
+}