A very simple PHP router and other utilities.
Yeah, why another framework?
These are a few (hopefully good enough) reasons:
-
web service-oriented
zapcore
and 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
zapcore
performance 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/zapcore
Here'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:9999
Routing 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 Found
Apart 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: Johnny
For 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 Found
There 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:
-
home
andhost
Router
attempts to infer your application root path from$_SERVER['SCRIPT_NAME']
which is mostly accurate when you deploy your application via Apachemod_php
withmod_rewrite
enabled. This most likely fails when$_SERVER['SCRIPT_NAME']
is no longer reliable, e.g. when you deploy your application under ApacheAlias
or Nginxlocation
directives; or when you make it world-visible after a reverse-proxying. This is wherehome
andhost
manual 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.
-
shutdown
zapcore
allows more than oneRouter
instances 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, setshutdown
config to false except for the lastRouter
instance.$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.
-
logger
All
zap*
packages use the same logging service provided byLogger
class. By default, eachRouter
instance has its ownLogger
instance, but you can share instance betweenRouter
s 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