A very simple PHP router and other utilities.
Yeah, why another framework?
These are a few (hopefully good enough) reasons:
-
web service-oriented
zapcoreand in generalzap*packages are geared towards HTTP RESTful APIs with very little emphasis on traditional HTML document serving. If you are building the back end of a single page web application, you'll feel immediately at home. -
performance
zapcoreperformance is guaranteed to be much faster than any popular batteries-included monolithic frameworks, and is at least on par with other microframeworks. TODO: Benchmark result. -
idiosyncracy
This is just a fancy way of saying, "Because that's the way we like it."
Install it from Packagist:
$ composer -vvv require bfitech/zapcoreHere's a bare-minimum index.php file:
<?php
require __DIR__ . '/vendor/autoload.php';
use BFITech\ZapCore\Router;
(new Router())->route('/', function($args){
echo "Hello, World!";
});Run it with PHP builtin web server and see it from your default browser:
$ php -S 0.0.0.0:9999 &
$ x-www-browser http://localhost:9999Routing in zapcore is the responsibility of the method Router::route.
Here's a simple route with /hello path, a regular function as the
callback to handle the request data, applied to PUT request method.
function my_callback($args) {
$name = $args['put'];
file_put_contents('name.txt', $name);
die(sprintf("Hello, %s.", $name));
}
$core = new Router();
$core->route('/hello', 'my_callback', 'PUT');which will produce:
$ curl -XPUT -d Johnny localhost:9999/hello
Hello, Johnny.We can use multiple methods for the same path:
$core = new Router();
function my_callback($args) {
global $core;
if ($core->get_request_method() == 'PUT') {
$name = $args['put'];
} else {
if (!isset($args['post']['name']))
die("Who are you?");
$name = $args['post']['name'];
}
file_put_contents('name.txt', $name);
die(sprintf("Hello, %s.", $name));
}
$core->route('/hello', 'my_callback', ['PUT', 'POST']);Instead of letting globals floating around, we can use closure and inherited variable for the callback:
function my_callback($args, $core) {
if ($core->get_request_method() == 'PUT') {
$name = $args['put'];
} else {
if (!isset($args['post']['name']))
die("Who are you?");
$name = $args['post']['name'];
}
file_put_contents('name.txt', $name);
die(sprintf("Hello, %s.", $name));
}
$core = new Router();
$core->route('/hello', function($args) use($core) {
my_callback($args, $core);
}, ['PUT', 'POST']);Callback can be a method instead of function:
$core = new Router();
class MyName {
public function my_callback($args) {
global $core;
if ($core->get_request_method() == 'PUT') {
$name = $args['put'];
} else {
if (!isset($args['post']['name']))
die("Who are you?");
$name = $args['post']['name'];
}
file_put_contents('name.txt', $name);
die(sprintf("Hello, %s.", $name));
}
}
$myname = new MyName();
$core->route('/hello', [$myname, 'my_callback'],
['PUT', 'POST']);And finally, you can subclass Router:
class MyName extends Router {
public function my_callback($args) {
if ($this->get_request_method() == 'PUT') {
$name = $args['put'];
} else {
if (!isset($args['post']['name']))
die("Who are you?");
$name = $args['post']['name'];
}
file_put_contents('name.txt', $name);
die(sprintf("Hello, %s.", $name));
}
public function my_home($args) {
if (!file_exists('name.txt'))
die("Hello, stranger.");
$name = file_get_contents('name.txt');
die(sprintf("You're home, %s.", $name));
}
}
$core = new MyName();
$core->route('/hello', [$core, 'my_callback'], ['PUT', 'POST']);
$core->route('/', [$core, 'my_home']);When request URI and request method do not match any route, a
default 404 error page will be sent unless you configure shutdown
to false (see below).
$ curl -si http://localhost:9999/hello | head -n1
HTTP/1.1 404 Not FoundApart from static path of the form /path/to/some/where, there are
also two types of dynamic path built with enclosing pairs of symbols
'<>' and '{}' that will capture matching strings from request URI
and store them under $args['params']:
class MyPath extends Router {
public function my_short_param($args) {
printf("Showing profile for user '%s'.\n",
$args['params']['short']);
}
public function my_long_param($args) {
printf("Showing version 1 of file '%s'.\n",
$args['params']['long']);
}
public function my_compound_param($args) {
extract($args['params']);
printf("Showing revision %s of file '%s'.\n",
$short, $long);
}
}
$core = new MyPath();
// short parameter with '<>', no slash captured
$core->route('/user/<short>/profile', [$core, 'my_short_param']);
// long parameter with '{}', slashes captured
$core->route('/file/{long}/v1', [$core, 'my_long_param']);
// short and long parameters combined
$core->route('/rev/{long}/v/<short>', [$core, 'my_compound_param']);which will produce:
$ curl localhost:9999/user/Johnny/profile
Showing profile for user 'Johnny'.
$ curl localhost:9999/file/in/the/cupboard/v1
Showing version 1 of file 'in/the/cupboard'.
$ curl localhost:9999/rev/in/the/cupboard/v/3
Showing revision 3 of file 'in/the/cupboard'.All request headers are available under $args['header']. These
include custom headers:
class MyToken extends MyName {
public function my_token($args) {
if (!isset($args['header']['my_token']))
die("No token sent.");
die(sprintf("Your token is '%s'.",
$args['header']['my_token']));
}
}
$core = new MyToken();
$core->route('/token', [$core, 'my_token']);which will produce:
$ curl -H "My-Token: somerandomstring" localhost:9999/token
Your token is 'somerandomstring'.NOTE: Custom request header keys will always be received in lower case, with all '-' changed into '_'.
You can send all kinds of response headers easily with the static
method Header::header from the parent class:
class MyName extends Router {
public function my_response($args) {
if (!isset($args['get']['name']))
self::halt("Oh noe!");
self:header(sprintf("X-Name: %s",
$args['get']['name']));
}
}
$core = new MyName();
$core->route('/response', [$core, 'my_response']);which will produce:
$ curl -si 'localhost:9999/response?name=Johnny' | grep -i name
X-Name: JohnnyFor a more proper sequence of response headers, you can use
Header::start_header static method:
class MyName extends Router {
public function my_response($args) {
if (isset($args['get']['name']))
self::start_header(200);
else
self::start_header(404);
}
}
$core = new MyName();
$core->route('/response', [$core, 'my_response']);which will produce:
$ curl -si 'localhost:9999/response?name=Johnny' | head -n1
HTTP/1.1 200 OK
$ curl -si localhost:9999/response | head -n1
HTTP/1.1 404 Not FoundThere are wrappers specifically-tailored for error pages, redirect and static file serving:
class MyFile extends Router {
public function my_file($args) {
if (!isset($args['get']['name']))
// show a 403 immediately
return $this->abort(403);
$name = $args['get']['name'];
if ($name == 'John')
// redirect to another query string
return $this->redirect('?name=Johnny');
// a dummy file
if (!file_exists('Johnny.txt'))
file_put_contents('Johnny.txt', "Here's Johnny.\n");
// serve a static file, will call $this->abort(404)
// internally if the file is not found
$file_name = $name . '.txt';
$this->static_file($file_name);
}
}
$core = new MyFile();
$core->route('/file', [$core, 'my_file']);which will produce:
$ curl -siL localhost:9999/file | grep HTTP
HTTP/1.1 403 Forbidden
$ curl -siL 'localhost:9999/file?name=Jack' | grep HTTP
HTTP/1.1 404 Not Found
$ curl -siL 'localhost:9999/file?name=John' | grep HTTP
HTTP/1.1 301 Moved Permanently
HTTP/1.1 200 OK
$ curl -L 'localhost:9999/file?name=Johnny'
Here's Johnny.Router::config is a special method to finetune the router behavior,
e.g.:
$core = (new Router())
->config('shutdown', false)
->config('logger', new Logger());Available configuration items are:
-
homeandhostRouterattempts to infer your application root path from$_SERVER['SCRIPT_NAME']which is mostly accurate when you deploy your application via Apachemod_phpwithmod_rewriteenabled. This most likely fails when$_SERVER['SCRIPT_NAME']is no longer reliable, e.g. when you deploy your application under ApacheAliasor Nginxlocationdirectives; or when you make it world-visible after a reverse-proxying. This is wherehomeandhostmanual setup comes to the rescue.# your nginx configuration location @app { set $app_dir /var/www/myapp; fastcgi_pass unix:/var/run/php5-fpm.sock; fastcgi_index index.php; fastcgi_buffers 256 4k; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $app_dir/index.php; # an inaccurate setting of SCRIPT_NAME fastcgi_param SCRIPT_NAME index.php; } location /app { try_files $uri @app; }
# your index.php $core = (new Router()) ->config('home', '/app') ->config('host', 'https://example.org/app'); // No matter where you put your app in the filesystem, it should // only be world-visible via https://example.org/app.
-
shutdownzapcoreallows more than oneRouterinstances in a single file. However, each instance executes a series of methods on shutdown if there is no matched route to ensure the routing doesn't end up in a blank page. In a multiple router situation, setshutdownconfig to false except for the lastRouterinstance.$core1 = new Router(); $core1->config('shutdown', false); $core1->route('/page', ...); $core1->route('/post', ...); $core2 = new Router(); $core2->route('/post', ...); # this route will never be executed, # see above $core2->route('/usr', ...); $core2->route('/usr/profile', ...); $core2->route('/usr/login', ...); $core2->route('/usr/logout', ...); // $core2 is the one responsible to internally call abort(404) at // the end of script execution when there's no matching route found.
-
loggerAll
zap*packages use the same logging service provided byLoggerclass. By default, eachRouterinstance has its ownLoggerinstance, but you can share instance betweenRouters to avoid multiple log files.$logger = new Logger(Logger::DEBUG, '/tmp/myapp.log'); $core1 = (new Router()) ->config('logger', $logger); $core2 = (new Router()) ->config('logger', $logger); // Both $core1 and $core2 write to the same log file /tmp/myapp.log.
See CONTRIBUTING.md.
If you have Doxygen installed, detailed generated documentation is available with:
$ doxygen
$ x-www-browser docs/html/index.html