bfitech/zapcore

一个非常简单的路由器和其他实用工具。

2.3.1 2020-10-02 20:29 UTC

This package is auto-updated.

Last update: 2024-09-16 04:19:00 UTC


README

一个非常简单的PHP路由器和其他实用工具。

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

0. 原因

but why?

是的,为什么还要另一个框架?

以下是一些(希望足够好)的原因

  1. 面向Web服务

    zapcore 和一般的 zap* 包针对HTTP RESTful API,对传统HTML文档服务重视较少。如果你正在构建单页Web应用的后端,你会感到立即自在。

  2. 性能

    zapcore 的性能保证比任何流行的单体框架都要快得多,并且至少与其它微框架相当。 待办事项:基准测试结果。

  3. 个性

    这只是说,"因为我们喜欢这样"的一种花哨说法。

1. 安装

从Packagist安装

$ composer -vvv require bfitech/zapcore

2. Hello, World

这是一个最简单的 index.php 文件

<?php

require __DIR__ . '/vendor/autoload.php';
use BFITech\ZapCore\Router;
(new Router())->route('/', function($args){
	echo "Hello, World!";
});

用PHP内置的Web服务器运行它,然后用你默认的浏览器查看

$ php -S 0.0.0.0:9999 &
$ x-www-browser https://:9999

3. 使用方法

3.0 路由

zapcore 中,路由是 Router::route 方法的责任。这里有一个简单的路由,使用 /hello 路径,一个作为回调来处理请求数据的常规函数,应用于 PUT 请求方法。

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');

这将产生

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

我们可以对同一个路径使用多种方法

$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']);

我们可以使用闭包和继承的变量来代替全局变量作为回调

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']);

回调可以是一个方法而不是函数

$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']);

最后,你可以继承 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']);

当请求URI和请求方法与任何路由不匹配时,除非你将 shutdown 配置为 false(见下文),否则将发送默认的404错误页面。

$ curl -si https://:9999/hello | head -n1
HTTP/1.1 404 Not Found

3.1 动态路径

除了静态路径 /path/to/some/where 的形式外,还有两种类型的动态路径,它们由成对的符号 '<>''{}' 构成,将捕获匹配的字符串从请求URI中,并存储在 $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']);

这将产生

$ 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'.

3.2 请求头

所有请求头都在 $args['header'] 下可用。这些包括自定义头

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']);

这将产生

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

注意:自定义请求头键始终以小写接收,将所有的 '-' 更改为 '_'。

3.3 响应头

你可以使用父类的静态方法 Header::header 轻松发送各种响应头

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']);

这将产生

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

为了响应头的更合适顺序,你可以使用 Header::start_header 静态方法

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']);

这将产生

$ 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

3.4 特殊响应

有专门针对错误页面、重定向和静态文件服务的包装器

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']);

这将产生

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

3.5 高级

Router::config 是一个特殊方法,用于微调路由器的行为,例如。

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

可用的配置项有

  • homehost

    Router 尝试从 $_SERVER['SCRIPT_NAME'] 推断你的应用程序根路径,这在通过Apache mod_php 部署应用程序且启用了 mod_rewrite 时通常很准确。当 $_SERVER['SCRIPT_NAME'] 不再可靠时,这很可能会失败,例如,当你在Apache Alias 或 Nginx location 指令下部署应用程序;或者当你在反向代理之后使其对全球可见时。这就是 homehost 手动设置派上用场的地方。

    # 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 允许在单个文件中存在多个 Router 实例。然而,如果没有匹配的路由,每个实例在关闭时都会执行一系列方法,以确保路由不会导致空白页面。在多个路由的情况下,除了最后一个 Router 实例外,将 shutdown 配置设置为 false。

    $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.
  • 日志记录器

    所有 zap* 包都使用 Logger 类提供的相同日志服务。默认情况下,每个 Router 实例都有自己的 Logger 实例,但您可以在 Router 之间共享实例以避免多个日志文件。

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

4. 贡献

请参阅 CONTRIBUTING.md。

5. 文档

如果您已安装 Doxygen,详细生成的文档可通过以下方式获得:

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