Routing
Initialize the router
After the frigate FrigateApp
has been initialized, the router can be initialized.
The router is initialized with the init
method. The init
method takes:
- debug
- a boolean value to enable debug mode for the router.
<?php
// We use the Frigate Base class to help with environment variables.
// This is not required but it is recommended.
Router::init(
load_request : true, // (bool) Load the request immediately. Default is true.
debug : null, // (null|bool) Enable debug mode. Default is null which will use the environment variable.
use_request : null, // (null|string) Use a specific request object. Default is null which will use the default request object.
use_response : null, // (null|string) Use a specific response object. Default is null which will use the default response object.
);
// If the request is not loaded immediately we can load it later:
Router::loadRequest();
The router is now initialized and ready to define routes. The router parses the request and matches it against the defined routes. After the execution of the route, the response is sent to the client.
Defining Routes
Routes are defined using the define
method. The define
method takes:
- method
- the HTTP method of the route. This can be a string or an array of strings.
- route
- the Route
object that defines the route logic.
<?php
// index.php file
// ..... Initialize the App and Router
use Frigate\Routing\Http\Methods;
use Frigate\Routing\Routes\Route;
/*
Define a route - In this case a GET route that matches the path "/me/{some_name}""
and executes the MyEndpoint class.
*/
Router::define(Methods::GET, new Route(
path: "/me/?{name}", // The path of the route.
context: [ // The context that will be passed to the endpoint.
"name" => "John" // The default value of the name parameter.
],
exp: new MyEndpoint( debug: null ), // The endpoint class this route will execute.
returns: ["application/json"], // The supported return types.
middleware: [], // Additional middleware to execute before the route.
avoid_middleware: [], // Middleware to avoid.
request_mutator: null // The request mutator.
));
// ..... Run the router
This is the most basic way to define a route. The route will match the path /me/
and /me/John
etc... and will execute the MyEndpoint
class which is a class that extends the Endpoint
class - about endpoints.
Generic Routes
For convenience, the Router
class has dome predefined methods for the most common HTTP methods and for the most common route types. These methods are:
get
- defines a GET route.post
- defines a POST route.put
- defines a PUT route.patch
- defines a PATCH route.delete
- defines a DELETE route.options
- defines an OPTIONS route.head
- defines a HEAD route.any
- defines a route that matches any HTTP method.static
- defines a static route that serves a file.error
- defines an error route that will be executed when an error occurs.
For all the generic routes and the any
route you can simply use them as a method of the Router
class. For example:
<?php
// index.php file
// ..... Initialize the App and Router
Router::get( // Or any other method like post, put, patch, delete, options, head, any
path : "/me/?{name}",
context: [
"name" => "John"
],
exp : new MyEndpoint( debug: null)
);
This is the same as the previous example but with a more convenient way to define the route.
static
and error
routes are special routes that have predefined behavior. The static
route will serve a file from the file system and the error
route will be executed when an error occurs. The error
route is described in the error handling section.
Static Routes
Static routes are routes that serve files from the file system. The static
route is defined with the static
method of the Router
class. The static
method takes:
- path
- (string) the path of the route - using the path syntax described below.
- directory
- (string) the absolute path to the directory that contains the files.
- types
- (string|array) the file types that will be served. This can be a string or an array of strings. The default is ["*/*"]
which will serve all files.
Here is an example of a static route:
<?php
// index.php file
// ..... Initialize the App and Router
Router::static(
path : "/storage", // The path of the route.
directory : __DIR__ . DIRECTORY_SEPARATOR . "files", // The directory that contains the files.
types : ["image/*", "text/*", "video/*"] // The types of files that will be served.
);
This will serve files from the files
directory when the path /storage/...
is requested. The files will be served with the correct mime type based on the file extension.
How do I set the file disposition?
By default, the file disposition is set to inline
. This means that the file will be displayed in the browser. Which means that if the browser can display the file it will. If you want to force the download of the file you can set the file disposition to attachment
. This can be done by passing the types
parameter as an associative array where the key is the mime type and the value is the file disposition. For example:
Note
- You can use the path syntax described below to define the path of the static route. But the path should not contain
path parameters
. - All the logic of the static route is handled by a custom handler that is provided by Frigate. This
Endpoint
is calledStaticEndpoint
and is described in the endpoints section.
Auto loading routes
When the application grows, the number of routes will grow as well. To make the code more readable and maintainable, the routes can be defined in separate files and auto-loaded by the router. This can be done by defining the routes in a separate file and then loading the file with the defineFrom
method of the Router
class. The defineFrom
method takes:
- path
- (string) the path to the folder that contains the route files.
Each route file should define a single class that extends the DefineRoute
class. The class should have a single constructor that takes no arguments and should define the routes in the constructor.
Here is an example of a route file:
<?php
// MyNameRoute.php file in the routes folder - /routes/MyNameRoute.php
use Frigate\Routing\Routes\DefineRoute;
use Frigate\Routing\Http\Methods;
use Frigate\Routing\Http\ResponseInterface;
use Frigate\Routing\Http\RequestInterface;
class MyNameRoute extends DefineRoute
{
public function __construct()
{
// Define the route method(s):
$this->method = [Methods::GET, Methods::POST];
// Define the route path:
$this->path = "/say/?{name:string}";
// Define the route context:
$this->context = [
"name" => "John Doe"
];
// Define the supported return types:
$this->returns = [
"application/json"
];
// Bind the route endpoint or expression:
$this->exp = function(array $context, RequestInterface $request, ResponseInterface $response) {
// Set the response data:
$response->setBodyJson([
"message" => "success",
"name" => $context["name"],
"context" => $context
]);
// return the response:
return $response;
};
// Define the route additional middleware:
$this->middleware = [];
// Define the route middleware to avoid:
$this->avoid_middleware = [];
// Define the request mutator:
$this->request_mutator = null;
}
}
Note
- The class name should be the same as the file name without the
.php
extension.
This is how all the route files inside the routes will be auto-loaded by the router:
<?php
// index.php file
// ..... Initialize the App and Router
Router::defineFrom("routes"); // The path is relative so for a full path use __DIR__ . DIRECTORY_SEPARATOR . "routes"
// After this line all the routes defined in the routes folder will be loaded an registered with the router.
Path Syntax
The route path is the URI that the router will match against. The route path can contain path parameters, and path parameters can have a type:
"/"
- will match the root path."users"
- will match the path "/users""users/{id}"
- will match the path "/users/1" and "/users/2" etc..."users/{id:int}"
- will match the path "/users/1" and "/users/2" and will place the id in the context as an integer.
Note
- The path is case insensitive. This means that the path
/users
will match/Users
and/USERS
etc... - When defining the same path with the same method twice, the last definition will override the previous one.
Path parameters
Path parameters are defined by wrapping the parameter name with curly brackets {}
.
The parameter name can be any alphanumeric string and can contain underscores _
. It is recommended to use a descriptive name.
The parameter name can also contain a type. The type is used to convert the path parameter to a specific type in the context.
int, integer
- will convert to an integer.float, double
- will convert to a float.-
bool, boolean
- will convert to a boolean. (1)- The boolean type will convert the following values to true:
1, true, on, yes
and the following values to false:0, false, off, no
.
- The boolean type will convert the following values to true:
-
string, str
- will match any string. Which is the default type. path
- will be a string containing the rest of the path.
The way we define the type is by adding a colon :
after the parameter name and then the type name:
"users/{id:int}"
- id will be anint
."users/{height:float}"
- height will be afloat
."users/{is_active:bool}"
- is_active will be abool
."users/{name}"
- name will be astring
."users/{name:string}/{id:int}/{action}"
- we can mix types and non types."users/{storage:path}"
- storage will be astring
containing the rest of the path.
Exceptions and limitations
- An exception will be thrown if the several path parameters are defined on the same level.
- i.e. defining
"users/{id:int}"
and"users/{name:string}"
will throw an exception.
- i.e. defining
- The
path
parameter cannot be extended with other parameters.- i.e.
"users/{storage:path}/{id:int}"
will throw an exception.
- i.e.
Variation Macro
In Frigate we can define multiple levels of a path with the variation macro ?
. The variation macro will expand to a several paths:
"users/storage/?{find}/?{term}"
- will expand to:"users/storage/"
"users/storage/{find}/"
"users/storage/{find}/{term}"
Obviously, this can be done manually but it is a lot easier to use the variation macro. Also, the variation macro can be used to mimic default values in path parameters. For example:
<?php
// ...
Router::define(Methods::GET, new Route("users/storage/?find/?{term}",
context : [
"find" => "all",
"term" => "avatar.png",
],
// ... rest of the route definition
));
All the expanded paths will be matched and the context will be merged with the context defined in the route definition. This behavior will result in 3 paths that points to the same route and have "default" values for the find
and term
parameters.
"users/storage/"
- will have the context:["find" => "all", "term" => "avatar.png"]
"users/storage/picture/"
- will have the context:["find" => "picture", "term" => "avatar.png"]
"users/storage/picture/profie.png"
- will have the context:["find" => "picture", "term" => "profie.png"]
Default parameter values
Default values are not supported in path parameters. They don't make sense in the context of a path parameter. Some frameworks support default values (sort of) by looking ahead in the path and matching the next path part. This is not supported in Frigate.
The best way to handle this is to define several paths that point to the same route and have different levels of path parameters.
This can be done easily with the variation macro ?
as described above.
Why not???
The reason for this is that it is not clear what the default value should be. For example, if we have the following path: "users/{id:int}/{action}"
and we want to set the default value of action
to view
. What should happen if the path is /users/1
? Should the default value be set to view
or should the path not match? This is not clear and can lead to unexpected behavior.
Shadow path markers
In Frigate shadow paths are defined by adding a ^
after the path. Internally, the shadow path marker is used to attach
expressions that will be executed when the this marker is reached. For example:
"/users^"
- a any path that starts with/users
will invoke the attached expressions before the route is executed."/users^/{id:int}"
- when executing the route/users/1
the attached expressions will be executed before the route is executed, passing the modified context to the route."/users^/{id:int}^/profile"
- shadow paths can be chained together. when executing the route/users/1/profile
the shadow expressions will be executed twice, once for/users/1
and once for/users/1/profile
.
This mechanism is used to attach middleware to a route. and is described in the middleware section.
Note
The shadow path marker is not part of the path and will not be matched. It is used to attach expressions to the path.
Warning
Don't use the shadow path marker unless you know what you are doing. Frigate offers a better way to attach middleware to a route. See the middleware section.