PHP Router is a lightweight library to help with managing HTTP requests, responses and routing in order to build robust applications quickly. This library uses patterns most commonly found in popular Node.js libraries like Express and Hapi.
In order to glean the most benefit from implementing this pattern of routing, you may wish to make some adjustments to your Apache or Nginx configuration.
Install with Composer:
$ composer require guahanweb/php-router
Once you have installed and configured your project to use Composer's autoload, you can begin routing.
<?php
// Include the namespace
use GuahanWeb\Http;
// Get a router instance
$router = Http\Router::instance();
// Register a route
$router->get('/', function ($req, $res) {
$res->send('Hello, world!');
});
// Tell the router to start
$router->process();
Routing is supported in the same basic pattern as Express.js. Provided an HTTP verb, route pattern and handler, the handler will be executed whenever the pattern and verb combination match the registered route.
A route may be registered one of two ways. The first is by explicit verb methods:
$router->get($route, $handler);
$router->post($route, $handler);
$router->put($route, $handler);
$router->delete($route, $handler);
A second option is to register one or more verbs (or all with a *
) in
a single registration call:
// multiple verbs
$router->route(['POST', 'PUT'], $route, $handler);
// wildcard for all supported verbs
$router->route('*', $route, $handler);
If you are wishing to capture parameters from your route URI, you may use brackets to name your parameters. These parameters will be applied as properties on the request object:
// capture a username
$router->get('/profile/{username}', function ($req, $res) {
var_dump($req->username);
});
You may also choose to greedily match blocks of the path for manual parsing.
For instance, if we wanted to distinguish between a json
extension to a
URI agnostic of the inner match, we can do something like this:
// match a path with json extension
// URI: /api/v1/my_method/json
$router->get('/api/{path*}/json', function ($req, $res) {
$res->send($req->path); // v1/my_method
});
This type of matching may be useful for doing a passthru to request static files not managed by Apache or Nginx configs:
// route all image requests to a handler
$router->get('/img/{image*}', function ($req, $res) {
ImageManager::serve($req->image);
});
Each route handler will be passed two parameters: a request and a response
object. These objects are able to be used to get detailed information and
calculate the appropriate response. In most cases, a route handler should
end with a call to $res->send()
.
When a router is initialized, a new Http\Request
object is created to be
passed into the handler. This object contains a lot of pre-parsed attributes
to help manage your response.
The HTTP method (or verb) will be assigned to the request object and can be accessed by property:
echo $req->method;
Valid supported types are GET
, POST
, PUT
and DELETE
.
All request headers are accessible on the request object as well. Rather than
using the native PHP getallheaders()
method, we are using a custom polyfill
to allow retrieval of the headers in both Apache and Nginx environments.
Some request headers will be used by the router to set default values on the
response object. To access the headers, you may reference the headers
property
of the request object:
$router->post('/api/user', function ($req, $res) {
if ($req->headers['Content-Type'] == 'application/json') {
// request has JSON payload
}
});
The request object will pre-parse the query string and assign it to the query
property. If you are doing logic based on expected query parameters, use this
property.
$router->post('/calendar', function ($req, $res) {
if (!isset($req->query['month'])) {
$res->send('Please select a month!');
} else {
$res->send(Calendar::render($req->query['month']));
}
});
The fully matched URI is assigned to the uri
property of the request object.
$router->get('/profile/{username}', function ($req, $res) {
$res->send($req->uri);
});
In this example, if you then navigate to http://yourdomain.com/profile/guahanweb,
you would get a response of /profile/guahanweb
.
Parsing the request body is somethig we have tried to optimize a little. Rather
than always parsing the payload, we will statically parse it only when it has
been requested the first time. In other words, the body will not be parsed until
the first time the body
attribute is referenced. If you reference the body
property again, the payload will not be parsed a second time.
Additionally, there are a few assumptions that are made about the body based on the combination of HTTP verb and Content-Type header.
According to the HTTP/1.1 spec, GET
and DELETE
request bodies, if present,
should not have any meaning. Therefore, the request object will short circuit
any body parsing with a null
value on these verbs.
The body for a PUT
request will be read from STDIN
. If the Content-Type of
the request is application/json
, the body will also be parsed for JSON content,
and the resulting object will be returned.
The POST
verb acts just like PUT
with one additional caveat. In addition to
a JSON payload, the POST
verb can also take a Content-Type beginning with
mulipart/form-data
. In this case, since PHP has already processed the payload
into the $_POST
variable, the body will simply return that variable.
In both PUT
and POST
cases, if neither JSON nor form content are specified,
the raw string of the body will be returned.
The response object allows for granular control over the HTTP response sent back to the browser.
Allows you to set or override default headers before the response is sent. The
default content type is dependent upon the data being sent. If a string is passed,
the content type defaults to text/html
, but if an array or object is passed,
the content will be JSON encoded and sent with application/json
type instead.
Shortcut to set multiple headers at once. This method accepts an array of headers where the key is the header name and the value is the header content.
This method will send the final built response back to the user. The optional
$code
parameter will allow you to specify status codes other than the default
200 Success
response.
$router->route('*', '/admin', function ($req, $res) {
$res->send('What do you think you are doing?', 500);
});
Here is an example of an endpoint that lets you examine properties of all requests from all verbs.
// examine request from any verbs
$router->route('*', '/info', function ($req, $res) {
$res->send(array(
'method' => $req->method,
'uri' => $req->uri,
'query' => $req->query,
'headers' => $req->headers,
'body' => $req->body
));
});
In order to make the most use out of manual routing, it is recommended that you configure your server to funnel all requests you are wanting to handle into a single PHP file.
If you are planning to use the PUT
and DELETE
verbs with your routing, you will
need to be sure your Nginx has been built --with-webdav
installed. Then, in your
configuration, you will need to enable the additional methods:
dav_methods PUT DELETE;
I also recommend providing a base location from which Nginx can serve all your static content so that you only have to process PHP for protected content or things requiring additional logic.
location ~* ^/(css|js|img|font)/ {
root /var/www/<my-project>/assets;
}
One other thing I like to do is only direct traffic that doesn't explicitly match another file on disk. So, a full configuration may look like this:
server {
charset UTF-8;
listen 80;
server_name <project-domain>;
access_log /var/www/logs/<project-domain>/access.log;
location ~* ^/(css|js|img|font)/ {
root /var/www/<project-domain>/assets;
}
location / {
root /var/www/<project-domain>;
index index.php index.html index.htm;
include /usr/local/etc/nginx/conf.d/php-fpm;
dav_methods PUT DELETE;
if (!-e $request_filename) {
rewrite ^(.+)$ /index.php last;
}
error_page 404 /index.php;
}
}
There are many different ways to solve this challenge, and I have simply looked into the model that best fits my personal preference. If you have ideas that may improve this project or simply want to let me know that it's helped you, please feel free to contact me.