Working with Router
Router and route
In Fano Framework, a route is an association rule between URL path pattern, HTTP method and code that handles it. Router manages one or more routes and match request URL path, extract data in it and select code that handles it (request handler instance, i.e., controller). Router is any class implements IRouter
interface.
Creating route for GET method
var
router : IRouter;
handler : IRequestHandler;
...
router.get('/', handler);
Creating route for POST method
router.post('/submit', handler);
Creating route for PUT method
router.put('/submit', handler);
Creating route for DELETE method
router.delete('/submit', handler);
Creating route for PATCH method
router.patch('/submit', handler);
Creating route for HEAD method
router.head('/', handler);
Creating route for OPTIONS method
router.options('/', handler);
Creating route for multiple methods
router.map(['GET', 'POST'], '/', handler);
Creating route for all methods
router.any('/', handler);
Route with argument
A route can have arguments as shown in following example.
router.get('/user/{username}', myUserHandler);
router.get('/products/{id}/{productSlug}', productList);
Read Getting Route Argument section for information how to read route argument value.
Route matching
If we have following route setup
var
router : IRouter;
myAppHandler, anotherAppHandler : IRequestHandler;
...
//GET request
router.get('/my/app', myAppHandler);
//POST request
router.post('/another/app', anotherAppHandler);
If your application hostname is example.com
and client opens http://example.com/my/app
through browser, our application will receive request GET
method to /my/app
resources. Router will match HTTP method and URL and returns myAppHandler
as code that responsible to handle this request. Read Working with Controllers for more information regarding request handler.
If client opens http://example.com/another/app
through browser, our application will receive request GET
method to /another/app
resources. Router will find match to /another/app
but because it is only registered for POST
request, EMethodNotAllowed
exception will be raised with HTTP 405 error code.
If client opens http://example.com/not/exists
through browser, our application will receive request GET
method to /not/exists
resources. Router will not find any matches. If this happens, ERouteHandlerNotFound
exception will be raised with HTTP 404 error code.
Build application routes with route builder
To build application routes, you need to create class that implements IRouteBuilder
interface.
IRouteBuilder = interface
['{46016416-B249-4258-B76A-7F5B55E8348D}']
(*!----------------------------------------------
* build application routes
* ----------------------------------------------
* @param cntr instance of dependency container
* @param rtr instance of router
*-----------------------------------------------*)
procedure buildRoutes(const cntr : IDependencyContainer; const rtr : IRouter);
end;
Fano Framework provides base abstract class TRouteBuilder
which you can extend and implement its buildRoutes()
method and pass it when creating application instance as shown in following code,
TAppRoutes = class(TRouteBuilder)
public
procedure buildRoutes(
const container : IDependencyContainer;
const router : IRouter
); override;
end;
...
procedure TAppRoutes.buildRoutes(
const container : IDependencyContainer;
const router : IRouter
);
begin
//register all routes here
router.get('/', handler);
end;
And then create its instance and pass it to application constructor.
appInstance := TCgiWebApplication.create(
TAppServiceProvider.create(),
TAppRoutes.create()
);
If you use Fano CLI to scaffold web application, you may notice that routes are separated into one or more include files that are inserted in one class responsible to build application routes as shown in following example,
procedure TAppRoutes.buildRoutes(
const container : IDependencyContainer;
const router : IRouter
);
begin
{$INCLUDE Routes/routes.inc}
end;
This is just convention used by Fano CLI tools because it is simple to generate.
Fano Framework cares that you provide class that implements IRouteBuilder
. It does not care how you implement it. So you are free to compose route builder class the way it suits you.
For example, you can create separate IRouteBuilder
implementation for each feature for better code organization and then compose them using TCompositeRouteBuilder
class as shown in example below
TUsersRoutes = class(TRouteBuilder)
public
procedure buildRoutes(
const container : IDependencyContainer;
const router : IRouter
); override;
end;
...
procedure TUsersRoutes.buildRoutes(
const container : IDependencyContainer;
const router : IRouter
);
begin
//register users related routes here
end;
For product feature routes,
TProductRoutes = class(TRouteBuilder)
public
procedure buildRoutes(
const container : IDependencyContainer;
const router : IRouter
); override;
end;
...
procedure TProductRoutes.buildRoutes(
const container : IDependencyContainer;
const router : IRouter
);
begin
//register products related routes here
end;
And when you build application, you compose them as follows,
appInstance := TCgiWebApplication.create(
TAppServiceProvider.create(),
TCompositeRouteBuilder.create([
TUserRoutes.create(),
TProductRoutes.create()
]);
);
Please note that TCompositeRouteBuilder
is built-in implementation of IRouteBuilder
which composes one or more IRouteBuilder
instances as one.
Set route name or middlewares with IRoute interface
All methods which register request handler such as get()
, post()
, etc., returns
instance of IRoute
interface which you can use to assign name to route or attach a middlewares. Following code lists all methods in this interface.
function setName(const routeName : shortstring) : IRoute;
function getName() : shortstring;
function add(const amiddleware : IMiddleware) : IRoute;
For example to set route name and attach a middleware.
var authOnlyMiddleware : IMiddleware;
...
router.post('/user/submit', handler).setName('create-user').add(authOnlyMiddleware);
Read Middlewares for more information.
Getting route argument
Third parameter of handleRequest()
method of IRequestHandler
interface gives instance of IRouteArgsReader
interface to allow application to retrieve route arguments.
If you have route pattern /myroute/{name}
and you access route via URl http://example.com/myroute/john
then you can get value of name
parameter as follows
function TMyController.handleRequest(
const request : IRequest;
const response : IResponse;
const args : IRouteArgsReader
) : IResponse;
var
arg : TPlaceholder;
begin
arg := args.getArg('name');
//arg.name = 'name', arg.value = 'john'
end;
If you are interested only for its value, you can call getValue()
and pass name of argument. It returns value as string.
var name : string;
...
name := args.getValue('name'); //name = 'john'
or with simplified array-like syntax,
name := args['name']; //name = 'john'
Above codes will print identical output as follows
name := args.getArg('name').value;
You can get all route arguments using getArgs()
method,
function TMyController.handleRequest(
const request : IRequest;
const response : IResponse;
const args : IRouteArgsReader
) : IResponse;
var placeHolders : TArrayOfPlaceholders;
arg : TPlaceholder;
i:integer;
begin
placeHolders := args.getArgs();
for i:=0 to length(placeholders)-1 do
begin
arg := placeholders[i];
end;
end;
See code example how to read route argument.
Create router instance
Fano Framework comes with basic router implementation TRouter
class which implements IRouter
interface.
container.add('router', TSimpleRouterFactory.create());
TSimpleRouterFactory
class builds router instance that supports route argument parsing. Alternatively, you can use TRouterFactory
class which creates router instance that does not support route argument but it is faster when matching request URL.
container.add('router', TRouterFactory.create());
If you need only static URL path pattern, you should use it.
If you create application service provider inherit from TBasicAppServiceProvider
, it will create default router using TSimpleRouterFactory
class which is good enough for most applications.
Replace router instance
If you want to replace router with different implementation, you can override buildRouter()
method of TBasicAppServiceProvider
. For example,
TMyAppProvider = class(TBasicAppServiceProvider)
private
fRouterMatcher : IRouteMatcher;
public
function getRouteMatcher() : IRouteMatcher; override;
function buildRouter(const cntr : IDependencyContainer) : IRouter; override;
end;
...
function TMyAppProvider.buildRouter(const cntr : IDependencyContainer) : IRouter;
begin
ctnr.add('router', TRouterFactory.create());
result := ctnr['router'] as IRouter;
fRouteMatcher := result as IRouteMatcher;
end;
function TMyAppProvider.getRouteMatcher() : IRouteMatcher;
begin
result := fRouteMatcher;
end;
Note that IRouteMatcher
is interface which is responsible to match request URL and TRouter
implements it.