inroutephp / inroute
Generate http routing and dispatching middleware from docblock annotations
Requires
- php: >=7.2
- aura/router: ^3
- psr/container: ^1
- psr/http-factory: ^1
- psr/http-message: ^1
- psr/http-server-middleware: ^1
- symfony/var-exporter: ^5
Requires (Dev)
Suggests
- doctrine/annotations:1: For compiling
- zircote/swagger-php:3: For building openapi apps
This package is auto-updated.
Last update: 2024-10-25 00:51:34 UTC
README
Generate http routing and dispatching middleware from docblock annotations.
Inroute is a code generator. It scans your source tree for annotated routes and generates a PSR-15 compliant http routing middleware. In addition all routes have a middleware pipeline of their own, making it easy to add behaviour at compile time based on custom annotations.
- See the example-app for a complete example.
- See console for a compiler tool for the command line.
Installation
composer require inroutephp/inroute
Table of contents
- Writing routes
- Piping a route through a middleware
- Compiling
- Dispatching
- Generating route paths
- Creating custom annotations
- Processing routes using compiler passes
- Handling dependencies with a DI container
- Dealing with routing errors
Writing routes
Routes are annotated using annotations, are called with a PSR-7 request object and inroute environment and are expected to return a PSR-7 response.
use inroutephp\inroute\Annotations\BasePath; use inroutephp\inroute\Annotations\GET; use inroutephp\inroute\Runtime\EnvironmentInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\TextResponse; /** * @BasePath(path="/users") */ class UserController { /** * @GET( * path="/{name}", * name="getUser", * attributes={ * "key": "value", * "name": "overwritten by path value" * } * ) */ function getUser( ServerRequestInterface $request, EnvironmentInterface $environment ): ResponseInterface { return new TextResponse( // the name attribute from the request path $request->getAttribute('name') // the custom route attribute . $request->getAttribute('key') ); } }
- The
method
andpath
values are self explanatory. - A route
name
is optional, and defaults toclass:method
(in the exampleUserController:getUser
). Attributes
are custom values that can be accessed at runtime through the request object.- Note that the use of Laminas diactoros as a psr-7 response implementation is used in this example, you may of course use another psr-7 implementation.
Piping a route through a middleware
Each route has a PSR-15 middleware
pipeline of its own. Adding a middleware to a route can be done using the
@Pipe
annotation. In the following example the pipedAction
route is piped
through the AppendingMiddleware
and the text ::Middleware
is appended to the
route response.
use inroutephp\inroute\Annotations\Pipe; class PipedController { /** * @GET(path="/piped") * @Pipe(middlewares={"AppendingMiddleware"}) */ function pipedAction( ServerRequestInterface $request, EnvironmentInterface $environment ): ResponseInterface { return new TextResponse('Controller'); } } use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class AppendingMiddleware implements MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ): ResponseInterface { $response = $handler->handle($request); return new TextResponse( $response->getBody()->getContents() . "::Middleware" ); } }
Compiling
The recommended way of building a project is by using the console build tool. Compiling from pure php involves setting up the compiler something like the following.
use inroutephp\inroute\Compiler\CompilerFacade; use inroutephp\inroute\Compiler\Settings\ArraySettings; $settings = new ArraySettings([ 'source-classes' => [ UserController::CLASS, PipedController::CLASS, ], 'target-namespace' => 'example', 'target-classname' => 'HttpRouter', ]); $facade = new CompilerFacade; $code = $facade->compileProject($settings); eval($code); $router = new example\HttpRouter;
Possible settings include
container
: The classname of a compile time container, specify if needed.bootstrap
: Classname of compile bootstrap, default should normally be fine.source-dir
: Directory to scan for annotated routes.source-prefix
: psr-4 namespace prefix to use when scanning directory.source-classes
: Array of source classnames, use instead of or togheter with directory scanning.ignore-annotations
: Array of annotations to ignore during compilationroute-factory
: Classname of route factory, default should normally be fine.compiler
: Classname of compiler to use, default should normally be fine.core-compiler-passes
: Array of core compiler passes, default should normally be fine.compiler-passes
: Array of custom compiler passes.code-generator
: The code generator to use, default should normally be fine.target-namespace
: The namespace of the generated router (defaults to no namespace).target-classname
: The classname of the generated router (defaults toHttpRouter
).
OpenApi
Please note that reading openapi annotations is still very rudimentary. Please open an issue if you have suggestions on more values that should be parsed.
Instead of using the built in annotations inroute is also able to build openapi projects annotated with swagger-php annotations.
Set the core-compiler-passes
setting to ['inroutephp\inroute\OpenApi\OpenApiCompilerPass']
.
Dispatching
The generated router is a PSR-15 compliant middleware. To dispatch you need to supply an implementation of PSR-7 for request and response objects and some response emitting functionality (of course you should also use a complete middleware pipeline for maximum power).
In this simple example we use
- laminas-diactoros as PSR-15 implementation.
- laminas-httphandlerrunner for emitting responses.
- The built in middleware pipeline for dispatching.
use inroutephp\inroute\Runtime\Middleware\Pipeline; use Laminas\Diactoros\ServerRequestFactory; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; // create a simple middleware pipeline for the entire application $pipeline = new Pipeline($router); // create a psr-7 compliant response emitter $emitter = new SapiEmitter; // fakeing a GET request $request = (new ServerRequestFactory)->createServerRequest('GET', '/users/foo'); // in the real worl you would of course use // $request = ServerRequestFactory::fromGlobals(); // create the response $response = $pipeline->handle($request); // send it $emitter->emit($response);
Or to send to piped example from above
use inroutephp\inroute\Runtime\Middleware\Pipeline; use Laminas\Diactoros\ServerRequestFactory; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; (new SapiEmitter)->emit( (new Pipeline($router))->handle( (new ServerRequestFactory)->createServerRequest('GET', '/piped') ) );
Generating route paths
function getUser(ServerRequestInterface $request, EnvironmentInterface $environment): ResponseInterface { return new TextResponse( $environment->getUrlGenerator()->generateUrl('getUser', ['name' => 'myUserName']) ); }
Creating custom annotations
Inroute uses doctrine to read annotations. Creating custom annotations is as easy as
namespace MyNamespace; /** * @Annotation */ class MyAnnotation { public $value; }
To create annotations that automatically pipes a route through a middleware use something like the following.
namespace MyNamespace; use inroutephp\inroute\Annotations\Pipe; /** * @Annotation */ class AdminRequired extends Pipe { public $middlewares = ['AuthMiddleware', 'RequireUserGroupMiddleware']; public $attributes = ['required_user_group' => 'admin']; }
Note that you need to supply the
AuthMiddleware
to authenticate a user and theRequireUserGroupMiddleware
to check user priviliges for this example to function as expected. See below on how to inject a dependency container that can deliver these middlewares.
And to annotate your controller methods
use MyNamespace\MyAnnotation; use MyNamespace\AdminRequired; class MyController { /** * @MyAnnotation(value="foobar") * @AdminRequired */ public function aRouteThatIsOnlyOpenToAdminUsers() { } }
Processing routes using compiler passes
Custom annotations are most useful pared with custom compiler passes.
use inroutephp\inroute\Compiler\CompilerPassInterface; use inroutephp\inroute\Runtime\RouteInterface; use MyNamespace\MyAnnotation; class MyCompilerPass implements CompilerPassInterface { public function processRoute(RouteInterface $route): RouteInterface { if ($route->hasAnnotation(MyAnnotation::CLASS)) { return $route ->withAttribute('cool-attribute', $route->getAnnotation(MyAnnotation::CLASS)->value) ->withMiddleware(SomeCoolMiddleware::CLASS); } return $route; } }
Each route has a middleware pipeline of its own. In the example above all
routes annotated with MyAnnotation
will be wrapped in SomeCoolMiddleware
.
This makes it easy to add custom behaviour to routes at compile time based
on annotations.
The attribute cool-attribute
can be accessed in middlewares using
$request->getAttribute('cool-attribute')
.
Handling dependencies with a DI container
You may have noted that in the example above SomeCoolMiddleware
was passed
not as an instantiated object but as a class name. The actual object is created
at runtime using a PSR-11 compliant
dependency injection container. The same is true for controller classes.
Create you container as part of your dispatching logic and pass it to the router
using the setContainer()
method.
$container = /* your custom setup */; $router = new example\HttpRouter; $router->setContainer($container); // continue dispatch...
Dealing with routing errors
Route note found (http code 404
) and method not allowed (405
) situations can
be handled in one of two ways.
Using a ResponseFactoryInterface
If you container contains a service Psr\Http\Message\ResponseFactoryInterface
then that factory will be used to create and return a 404
or 405
http response.
Catching exceptions
If no factory is defined a inroutephp\inroute\Runtime\Exception\RouteNotFoundException
or inroutephp\inroute\Runtime\Exception\MethodNotAllowedException
will be
thrown.