项目作者: bfitech

项目描述 :
A simple PHP router and other utils.
高级语言: PHP
项目地址: git://github.com/bfitech/zapcore.git
创建时间: 2017-03-18T04:30:31Z
项目社区:https://github.com/bfitech/zapcore

开源协议:MIT License

下载


zapcore

A very simple PHP router and other utilities.

Latest Stable Version
Latest Unstable Version
Build Status
Code Coverage
GitHub license


0. Reason

but why?

Yeah, why another framework?

These are a few (hopefully good enough) reasons:

  1. web service-oriented

    zapcore and in general zap* 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.

  2. 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.

  3. idiosyncracy

    This is just a fancy way of saying, “Because that’s the way we like
    it.”

1. Installation

Install it from Packagist:

  1. $ composer -vvv require bfitech/zapcore

2. Hello, World

Here’s a bare-minimum index.php file:

  1. <?php
  2. require __DIR__ . '/vendor/autoload.php';
  3. use BFITech\ZapCore\Router;
  4. (new Router())->route('/', function($args){
  5. echo "Hello, World!";
  6. });

Run it with PHP builtin web server and see it from your default browser:

  1. $ php -S 0.0.0.0:9999 &
  2. $ x-www-browser http://localhost:9999

3. Usage

3.0 Routing

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.

  1. function my_callback($args) {
  2. $name = $args['put'];
  3. file_put_contents('name.txt', $name);
  4. die(sprintf("Hello, %s.", $name));
  5. }
  6. $core = new Router();
  7. $core->route('/hello', 'my_callback', 'PUT');

which will produce:

  1. $ curl -XPUT -d Johnny localhost:9999/hello
  2. Hello, Johnny.

We can use multiple methods for the same path:

  1. $core = new Router();
  2. function my_callback($args) {
  3. global $core;
  4. if ($core->get_request_method() == 'PUT') {
  5. $name = $args['put'];
  6. } else {
  7. if (!isset($args['post']['name']))
  8. die("Who are you?");
  9. $name = $args['post']['name'];
  10. }
  11. file_put_contents('name.txt', $name);
  12. die(sprintf("Hello, %s.", $name));
  13. }
  14. $core->route('/hello', 'my_callback', ['PUT', 'POST']);

Instead of letting globals floating around, we can use closure and
inherited variable for the callback:

  1. function my_callback($args, $core) {
  2. if ($core->get_request_method() == 'PUT') {
  3. $name = $args['put'];
  4. } else {
  5. if (!isset($args['post']['name']))
  6. die("Who are you?");
  7. $name = $args['post']['name'];
  8. }
  9. file_put_contents('name.txt', $name);
  10. die(sprintf("Hello, %s.", $name));
  11. }
  12. $core = new Router();
  13. $core->route('/hello', function($args) use($core) {
  14. my_callback($args, $core);
  15. }, ['PUT', 'POST']);

Callback can be a method instead of function:

  1. $core = new Router();
  2. class MyName {
  3. public function my_callback($args) {
  4. global $core;
  5. if ($core->get_request_method() == 'PUT') {
  6. $name = $args['put'];
  7. } else {
  8. if (!isset($args['post']['name']))
  9. die("Who are you?");
  10. $name = $args['post']['name'];
  11. }
  12. file_put_contents('name.txt', $name);
  13. die(sprintf("Hello, %s.", $name));
  14. }
  15. }
  16. $myname = new MyName();
  17. $core->route('/hello', [$myname, 'my_callback'],
  18. ['PUT', 'POST']);

And finally, you can subclass Router:

  1. class MyName extends Router {
  2. public function my_callback($args) {
  3. if ($this->get_request_method() == 'PUT') {
  4. $name = $args['put'];
  5. } else {
  6. if (!isset($args['post']['name']))
  7. die("Who are you?");
  8. $name = $args['post']['name'];
  9. }
  10. file_put_contents('name.txt', $name);
  11. die(sprintf("Hello, %s.", $name));
  12. }
  13. public function my_home($args) {
  14. if (!file_exists('name.txt'))
  15. die("Hello, stranger.");
  16. $name = file_get_contents('name.txt');
  17. die(sprintf("You're home, %s.", $name));
  18. }
  19. }
  20. $core = new MyName();
  21. $core->route('/hello', [$core, 'my_callback'], ['PUT', 'POST']);
  22. $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).

  1. $ curl -si http://localhost:9999/hello | head -n1
  2. HTTP/1.1 404 Not Found

3.1 Dynamic Path

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']:

  1. class MyPath extends Router {
  2. public function my_short_param($args) {
  3. printf("Showing profile for user '%s'.\n",
  4. $args['params']['short']);
  5. }
  6. public function my_long_param($args) {
  7. printf("Showing version 1 of file '%s'.\n",
  8. $args['params']['long']);
  9. }
  10. public function my_compound_param($args) {
  11. extract($args['params']);
  12. printf("Showing revision %s of file '%s'.\n",
  13. $short, $long);
  14. }
  15. }
  16. $core = new MyPath();
  17. // short parameter with '<>', no slash captured
  18. $core->route('/user/<short>/profile', [$core, 'my_short_param']);
  19. // long parameter with '{}', slashes captured
  20. $core->route('/file/{long}/v1', [$core, 'my_long_param']);
  21. // short and long parameters combined
  22. $core->route('/rev/{long}/v/<short>', [$core, 'my_compound_param']);

which will produce:

  1. $ curl localhost:9999/user/Johnny/profile
  2. Showing profile for user 'Johnny'.
  3. $ curl localhost:9999/file/in/the/cupboard/v1
  4. Showing version 1 of file 'in/the/cupboard'.
  5. $ curl localhost:9999/rev/in/the/cupboard/v/3
  6. Showing revision 3 of file 'in/the/cupboard'.

3.2 Request Headers

All request headers are available under $args['header']. These
include custom headers:

  1. class MyToken extends MyName {
  2. public function my_token($args) {
  3. if (!isset($args['header']['my_token']))
  4. die("No token sent.");
  5. die(sprintf("Your token is '%s'.",
  6. $args['header']['my_token']));
  7. }
  8. }
  9. $core = new MyToken();
  10. $core->route('/token', [$core, 'my_token']);

which will produce:

  1. $ curl -H "My-Token: somerandomstring" localhost:9999/token
  2. Your token is 'somerandomstring'.

NOTE: Custom request header keys will always be received in
lower case, with all ‘-‘ changed into ‘_‘.

3.3 Response Headers

You can send all kinds of response headers easily with the static
method Header::header from the parent class:

  1. class MyName extends Router {
  2. public function my_response($args) {
  3. if (!isset($args['get']['name']))
  4. self::halt("Oh noe!");
  5. self:header(sprintf("X-Name: %s",
  6. $args['get']['name']));
  7. }
  8. }
  9. $core = new MyName();
  10. $core->route('/response', [$core, 'my_response']);

which will produce:

  1. $ curl -si 'localhost:9999/response?name=Johnny' | grep -i name
  2. X-Name: Johnny

For a more proper sequence of response headers, you can use
Header::start_header static method:

  1. class MyName extends Router {
  2. public function my_response($args) {
  3. if (isset($args['get']['name']))
  4. self::start_header(200);
  5. else
  6. self::start_header(404);
  7. }
  8. }
  9. $core = new MyName();
  10. $core->route('/response', [$core, 'my_response']);

which will produce:

  1. $ curl -si 'localhost:9999/response?name=Johnny' | head -n1
  2. HTTP/1.1 200 OK
  3. $ curl -si localhost:9999/response | head -n1
  4. HTTP/1.1 404 Not Found

3.4 Special Responses

There are wrappers specifically-tailored for error pages, redirect and
static file serving:

  1. class MyFile extends Router {
  2. public function my_file($args) {
  3. if (!isset($args['get']['name']))
  4. // show a 403 immediately
  5. return $this->abort(403);
  6. $name = $args['get']['name'];
  7. if ($name == 'John')
  8. // redirect to another query string
  9. return $this->redirect('?name=Johnny');
  10. // a dummy file
  11. if (!file_exists('Johnny.txt'))
  12. file_put_contents('Johnny.txt', "Here's Johnny.\n");
  13. // serve a static file, will call $this->abort(404)
  14. // internally if the file is not found
  15. $file_name = $name . '.txt';
  16. $this->static_file($file_name);
  17. }
  18. }
  19. $core = new MyFile();
  20. $core->route('/file', [$core, 'my_file']);

which will produce:

  1. $ curl -siL localhost:9999/file | grep HTTP
  2. HTTP/1.1 403 Forbidden
  3. $ curl -siL 'localhost:9999/file?name=Jack' | grep HTTP
  4. HTTP/1.1 404 Not Found
  5. $ curl -siL 'localhost:9999/file?name=John' | grep HTTP
  6. HTTP/1.1 301 Moved Permanently
  7. HTTP/1.1 200 OK
  8. $ curl -L 'localhost:9999/file?name=Johnny'
  9. Here's Johnny.

3.5 Advanced

Router::config is a special method to finetune the router behavior,
e.g.:

  1. $core = (new Router())
  2. ->config('shutdown', false)
  3. ->config('logger', new Logger());

Available configuration items are:

  • home and host

    Router attempts to infer your application root path from
    $_SERVER['SCRIPT_NAME'] which is mostly accurate when you
    deploy your application via Apache mod_php with mod_rewrite
    enabled. This most likely fails when $_SERVER['SCRIPT_NAME'] is
    no longer reliable, e.g. when you deploy your application
    under Apache Alias or Nginx location directives; or when you
    make it world-visible after a reverse-proxying. This is where
    home and host manual setup comes to the rescue.

    1. # your nginx configuration
    2. location @app {
    3. set $app_dir /var/www/myapp;
    4. fastcgi_pass unix:/var/run/php5-fpm.sock;
    5. fastcgi_index index.php;
    6. fastcgi_buffers 256 4k;
    7. include fastcgi_params;
    8. fastcgi_param SCRIPT_FILENAME $app_dir/index.php;
    9. # an inaccurate setting of SCRIPT_NAME
    10. fastcgi_param SCRIPT_NAME index.php;
    11. }
    12. location /app {
    13. try_files $uri @app;
    14. }
    1. # your index.php
    2. $core = (new Router())
    3. ->config('home', '/app')
    4. ->config('host', 'https://example.org/app');
    5. // No matter where you put your app in the filesystem, it should
    6. // only be world-visible via https://example.org/app.
  • shutdown

    zapcore allows more than one Router 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, set shutdown config
    to false except for the last Router instance.

    1. $core1 = new Router();
    2. $core1->config('shutdown', false);
    3. $core1->route('/page', ...);
    4. $core1->route('/post', ...);
    5. $core2 = new Router();
    6. $core2->route('/post', ...); # this route will never be executed,
    7. # see above
    8. $core2->route('/usr', ...);
    9. $core2->route('/usr/profile', ...);
    10. $core2->route('/usr/login', ...);
    11. $core2->route('/usr/logout', ...);
    12. // $core2 is the one responsible to internally call abort(404) at
    13. // the end of script execution when there's no matching route found.
  • logger

    All zap* packages use the same logging service provided by
    Logger class. By default, each Router instance has its own
    Logger instance, but you can share instance between Routers to
    avoid multiple log files.

    1. $logger = new Logger(Logger::DEBUG, '/tmp/myapp.log');
    2. $core1 = (new Router())
    3. ->config('logger', $logger);
    4. $core2 = (new Router())
    5. ->config('logger', $logger);
    6. // Both $core1 and $core2 write to the same log file /tmp/myapp.log.

4. Contributing

See CONTRIBUTING.md.

5. Documentation

If you have Doxygen installed,
detailed generated documentation is available with:

  1. $ doxygen
  2. $ x-www-browser docs/html/index.html