kreme201 / file-base-router
File based router for PHP
Requires
- php: >=8.3
- psr/container: ^2.0
- psr/http-message: ^2.0
Requires (Dev)
- mikey179/vfsstream: ^1.6
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
The File Based Router library is designed to handle dynamic routing based on files.
Install
composer require kreme201/file-base-router
It has been developed and tested on PHP 8.3.
Apache Setting
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
Usage
Project Structure
.
├── pages
│ ├── board
│ │ ├── [id].php
│ │ └── index.php
│ ├── index.php
├── public
│ └── index.php
└── vendor
public/index.php
<?php require '/path/to/vendor/autoload.php'; use Kreme201\FileBaseRouter\Container; use Kreme201\FileBaseRouter\Exception\RouteNotFoundException; use Kreme201\FileBaseRouter\Router; $router = new Router(dirname(__DIR__) . '/pages'); /** @var Container $container built-in container (default) */ $container = $router->getContainer(); $container->bind(App\BoardRepository::class, App\DbBoardRepository::class); // To use another PSR-11 container instead: // $router->setContainer($otherContainer); $router ->on(RouteNotFoundException::class, function (RouteNotFoundException $e, string $path) { http_response_code(404); include dirname(__DIR__) . '/errors/404.php'; }) ->on(Throwable::class, function (Throwable $e) { http_response_code(500); include dirname(__DIR__) . '/errors/500.php'; }) ->handle(); // ServerRequest::fromGlobals()
Router::handle() dispatches the request, renders the template and handles exceptions.
on(ExceptionClass::class, $handler)registers a handler that receives the exception and the request path. Handlers are matched in registration order (the firstinstanceofmatch wins).RouteNotFoundExceptionis thrown when no template matches.InvalidRouteParameterExceptionextends it, so both are handled as 404.- Without a handler,
RouteNotFoundExceptionsets a 404 status and any other exception is rethrown. - Output of a template that throws is discarded, so handlers can render a clean error page.
- The built-in
Containeris used by default. Replace it withsetContainer()(any PSR-11 container);getContainer()returns the current one. DispatcherandRenderercan still be used directly for custom flows.Rendererhas the samesetContainer()/getContainer().
When creating an instance of the Dispatcher, routing is handled based on the provided path.
Dynamic paths can be processed using [{name}] and [...{name}] to capture dynamic data; [...{name}] includes all sub-paths.
dispatch() returns a Route ($route->template, $route->params) or false.
/: {base_path}/index.php
/board: {base_path}/board/index.php
/board/123: {base_path}/board/[id].php, params => ['id' => '123']
/test/slug/example: {base_path}/test/[...slug].php, params => ['slug' => 'slug/example']
Injecting variables with PHPDoc
Renderer reads the @var tags of the template's first docblock and injects each variable.
<?php // pages/board/[id].php use App\BoardRepository; /** * @var int $id route parameter, converted to int * @var BoardRepository $repo resolved from the container * @var ?string $q null when it cannot be resolved */ $post = $repo->find($id); ?> <h1><?= $post->title ?></h1>
- A route parameter with the same name is converted to
string,int,float,boolorarray(string[]splits a catch-all by/). If the conversion fails,InvalidRouteParameterExceptionis thrown (e.g./board/abc), which is usually handled as a 404 (see above). - Class or interface types are resolved from the container. Any PSR-11
ContainerInterfaceimplementation can be passed to$router->setContainer($container);@var ContainerInterface $containerinjects the container itself.useimports and thenamespaceof the template are respected. - Nullable types become
nullwhen nothing matches. OtherwiseContainerExceptionis thrown. - Route parameters are no longer written to
$_GET.
Handling forms (PSR-7)
Router::handle() without an argument builds a PSR-7 ServerRequest from the superglobals (ServerRequest::fromGlobals()).
You can also pass any ServerRequestInterface, or a URI string (a GET request is built).
Templates receive the request by declaring its type; route parameters are also available as request attributes.
<?php // pages/board/[id]/edit.php use Kreme201\FileBaseRouter\Http\ServerRequest; /** * @var int $id * @var ServerRequest $request or Psr\Http\Message\ServerRequestInterface */ if ($request->isMethod('POST')) { $title = $request->getParam('title'); // parsed body, then query string $file = $request->getUploadedFiles()['attachment'] ?? null; $file?->moveTo('/path/to/uploads/' . bin2hex(random_bytes(8))); }
- Implementations in
Kreme201\FileBaseRouter\Http:ServerRequest,Request,Uri,Stream,UploadedFile(psr/http-message 2.0). getParsedBody()is$_POSTfor POST forms. For PUT/PATCH/DELETE,application/x-www-form-urlencodedand JSON bodies are parsed as well.getUploadedFiles()normalizes$_FILES, including nested fields such asphotos[].getParam()andisMethod()are helpers that are not part of PSR-7.- Exception handlers receive the request as the third argument:
function (Throwable $e, string $path, ServerRequestInterface $request).
Non-ASCII paths
Request paths are percent-decoded before matching, so file, directory, parameter and variable names can be written in any language.
/%EC%86%8C%EA%B0%9C → {base_path}/소개.php
/게시판/첫번째%20글 → {base_path}/게시판/[글번호].php, $글번호 => '첫번째 글'
/태그/여행/제주도 → {base_path}/태그/[...이름].php, $이름 => '여행/제주도'
- Each segment is decoded once (
%2525→%25). Empty segments are collapsed (//a/→/a). - Paths are never matched when a segment starts with
.or__(including..), decodes to/,\or NUL, or is not valid UTF-8. Route::$path,RouteNotFoundException::$pathand the handler's$pathcontain the decoded path.- macOS may store file names in Unicode NFD while browsers send NFC. Save page files in NFC when deploying from macOS.
Example
The playground directory is a runnable example app (Korean paths, route parameters, container injection, form submission and file upload).
composer install
composer serve # http://localhost:8000