命令字符串/路由器

此包已被弃用且不再维护。未建议替代包。

v3.4.3 2023-02-02 13:45 UTC

This package is auto-updated.

Last update: 2023-06-23 18:38:02 UTC


README

如果您仍然需要React/Http路由器,请查看 Tnapf/Router

CommandString/Router

一个底层使用ReactPHP/HTTP构建的路由器包

目录

安装

composer require commandstring/router

入门

首先您需要创建一个ReactPHP SocketServer

$socket = new \React\Socket\SocketServer("127.0.0.1:8000");

然后创建一个路由器实例

$router = new \Router\Http\Router($socket, true);

第二个参数是是否启用开发模式,您可以在此处了解开发模式 这里

创建一些路由然后开始监听请求

$router->listen();

路由

您可以通过使用match方法添加路由

use Router\Http\Methods;

$router->match([Methods::GET], "/", function() { /* ... */ });

您可以通过将它们添加到数组中监听更多方法

$router->match([Methods::GET, Methods::POST], "/", function() { /* ... */ });

路由简写

为单请求方法提供了简写

$router->get('pattern', function() { /* ... */ });
$router->post('pattern', function() { /* ... */ });
$router->put('pattern', function() { /* ... */ });
$router->delete('pattern', function() { /* ... */ });
$router->options('pattern', function() { /* ... */ });
$router->patch('pattern', function() { /* ... */ });
$router->head('pattern', function() { /* ... */ });

您可以使用此简写来访问使用任何方法的路由

$router->all('pattern', function() { /* ... */ });

路由模式

路由模式可以是静态的或动态的

  • 静态路由模式 不包含动态部分,必须与当前URL的 path 部分完全匹配。
  • 动态路由模式 包含可以随请求而变化的动态部分。这些变化的部分被命名为 子模式,可以使用Perl兼容的正则表达式 (PCRE) 或使用 占位符 定义。

静态路由模式

静态路由模式是一个表示URI的普通字符串。它将直接与当前URL的 path 部分进行比较。

示例

  • /about
  • /contact

使用示例

$router->get('/about', function($req, $res) {
    $res->getBody()->write("Hello World");
    return $res;
});

基于PCRE的动态路由模式

此类型的路由模式包含可以随请求而变化的动态部分。这些变化的部分被命名为 子模式,并使用正则表达式定义。

示例

  • /movies/(\d+)
  • /profile/(\w+)

动态路由模式中常用的基于PCRE的子模式包括

  • \d+ = 一个或多个数字 (0-9)
  • \w+ = 一个或多个单词字符 (a-z 0-9 _)
  • [a-z0-9_-]+ = 一个或多个单词字符 (a-z 0-9 _) 和连字符 (-)
  • .* = 任何字符(包括 /),零个或多个
  • [^/]+ = 除了 / 之外的任意字符,至少一个

注意:您可以参考 PHP PCRE 技巧表,这可能很有帮助。

在动态 PCRE 路由模式中定义的 子模式 被转换为传递给路由处理函数的参数。前提是这些子模式需要定义为 括号子模式,这意味着它们应该被括号包围

// Bad
$router->get('/hello/\w+', function($req, $res, $name) {
    $res->getBody()->write('Hello '.htmlentities($name));
    return $res;
});

// Good
$router->get('/hello/(\w+)', function($req, $res, $name) {
    $res->getBody()->write('Hello '.htmlentities($name));
    return $res;
});

注意:路由模式开头的一级 / 不是必须的,但建议使用。

当定义了多个子模式时,结果 路由处理参数 将按照定义的顺序传递给路由处理函数

$router->get('/movies/(\d+)/photos/(\d+)', function($req, $res, $movieId, $photoId) {
    $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId);
    return $res;
});

基于动态占位符的路由模式

此类路由模式与 动态 PCRE 路由模式 相同,但有一个区别:它们不使用正则表达式进行模式匹配,而是使用更简单的 占位符。占位符是包围在花括号中的字符串,例如 {name}。您不需要在占位符周围添加括号。

示例

  • /movies/{id}
  • /profile/{username}

占位符比 PCRE 更容易使用,但提供的控制较少,因为它们在内部被转换为匹配任何字符的正则表达式(.*)。

$router->get('/movies/{movieId}/photos/{photoId}', function($req, $res, $movieId, $photoId) {
    $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId);
    return $res;
});

注意:占位符的名称不需要与传递给路由处理函数的参数名称匹配

$router->get('/movies/{foo}/photos/{bar}', function($req, $res, $movieId, $photoId) {
    $res->getBody()->write('Movie #'.$movieId.', photo #'.$photoId);
    return $res;
});

可选路由子模式

可以通过在子模式后添加 ? 来使路由子模式可选。以博客 URL 为例,形式为 /blog(/year)(/month)(/day)(/slug)

$router->get(
	'/blog(/\d+)?(/\d+)?(/\d+)?(/[a-z0-9_-]+)?',
	function($req, $res, $year = null, $month = null, $day = null, $slug = null) {
		if (!$year) { 
			$res->getBody()->write("Blog Overview");
			return $res;
		}
		
		if (!$month) {
			$res->getBody()->write("Blog year overview");
			return $res;
		}
		
		if (!$day) {
			$res->getBody()->write("Blog month overview");
			return $res;
		}
		
		if (!$slug) {
			$res->getBody()->write("Blog day overview");
			return $res;
		}
		
		$res->getBody()->write('Blogpost ' . htmlentities($slug) . ' detail');
		return $res;
	}
);

上面的代码片段响应于以下 URL:/blog/blog/year/blog/year/month/blog/year/month/day/blog/year/month/day/slug

注意:使用可选参数时,很重要的一点是将子模式的一级 / 放在子模式内部。不要忘记为可选参数设置默认值。

不幸的是,上面的代码片段还响应于类似 /blog/foo 的 URL,并声明需要显示概览——这是不正确的。可以通过扩展括号子模式以包含其他可选子模式来使可选子模式连续:模式应该类似于 /blog(/year(/month(/day(/slug)))) 而不是之前的 /blog(/year)(/month)(/day)(/slug)

$router->get('/blog(/\d+(/\d+(/\d+(/[a-z0-9_-]+)?)?)?)?', function($req, $res, $year = null, $month = null, $day = null, $slug = null) {
    // ...
});

注意:强烈建议始终定义连续的可选参数。

使用 量词 来强制在 URL 中使用正确数量的数字

$router->get('/blog(/\d{4}(/\d{2}(/\d{2zz}(/[a-z0-9_-]+)?)?)?)?', function($req, $res, $year = null, $month = null, $day = null, $slug = null) {
    // ...
});

控制器

定义路由时,您可以传递匿名函数或包含一个类及其要调用的静态方法的数组。此外,您的控制器必须返回 PSR7 响应接口的实现

匿名函数控制器

$router->get("/home", function ($req, $res) {
    $res->getBody()->write("Welcome home!");
    return $res;
});

类控制器

我有一个带有 静态 方法的类,您的处理程序必须是静态方法!

class Home {
	public static function handler($req, $res) {
		$res->getBody()->write("Welcome home!");
		return $res;
	}
}

然后,我用一个包含字符串的数组替换匿名函数,第一个元素是类字符串,第二个键是静态方法名称。

$router->get("/home", [Home::class, "handler"]);

404 处理器

定义一个 404 处理器是 必需的,并且与创建路由类似。您还可以为不同的模式创建不同的 404 页面。

要设置一个 404 处理器,您可以调用 map404 方法,并插入一个用于第一个参数的模式,然后是第二个参数的控制台。

$router->map404("/(.*)", function ($req, $res) {
	$res->getBody()->write("{$req->getRequestTarget()} is not a valid route");
	return $res;
});

500 处理器

定义一个 500 处理器是 推荐的,并且与映射 404 处理器完全相同。

$router->map500("/(.*)", function ($req, $res) {
	$res->getBody()->write("An error has happened internally :(");
	return $res;
});

请注意,在开发模式下,您的 500 错误处理器将被覆盖

中间件

中间件是一种连接 MVC 应用程序中的模型和视图的软件,它促进了这两个组件之间的通信和数据流,同时也提供了一层抽象,解耦模型和视图,使它们能够在不知道其他组件操作细节的情况下进行交互。

一个很好的例子是具有“之前”中间件,以确保用户在访问受限制页面之前是管理员。您可以在每个管理页面的路由控制器中这样做,但这将是多余的。或者对于“之后”中间件,您可能有一个返回 JSON 响应的 REST API。您可以有“之后”中间件来确保 JSON 响应没有损坏。

之前中间件

您可以像路由一样定义之前中间件,提供方法、模式和控制器。请注意,使用 beforeMiddleware 时,您应该创建一个实现 ResponseInterface 的对象,并将其传递给主路由。

$router->beforeMiddleware("/admin?(.*)", function (ServerRequest $req, Closure $next) {
	if (!isAdmin()) {
		return new RedirectResponse("/", 403);
	}

	return $next(new Response);
});

在主路由中进行“之后”中间件

如果路由器检测到“之后”中间件,则第三个参数将是一个闭包,其功能与“之前”中间件类似。

$router->all("/admin/roster/json", function (ServerRequest $req, Response $res, Closure $next) {
	/*
		some code that gets the admin roster and converts to json
	*/

	$res->getBody()->write($jsonString);
	$res->withHeader("content-type", "application/json");

	return $next($res);
});

之后中间件

$router->afterMiddleware("/admin?(.*)", function (ServerRequest $req, ResponseInterface $res) {
	/*
		some code that makes sure the body is a valid json string
	*/

	if (!jsonValid((string)$res->getBody())) {
		return new EmptyResponse(500);
	}

	return $res;
});

关于中间件的特别说明,您可以通过在 next 闭包的第二个参数中提供它,从“之前”中间件传递变量到主路由,或从主路由传递到“之后”中间件。

模板引擎集成

您可以使用 CommandString/Env 将模板引擎对象存储在单例中。然后您可以轻松地获取它,而无需尝试将其传递到控制器。

use CommandString\Env\Env;

$env = new Env;
$env->twig = new Environment(new \Twig\Loader\FilesystemLoader("/path/to/views"));

// ...

$router->get("/home", function ($req, $res) {
	return new HtmlResponse($env->get("twig")->render("home.html"));\\\
});

响应请求

所有控制器 必须 返回 \Psr\Http\Message\ResponseInterface 的实现。您可以使用传递到控制器的预制响应对象 实例化自己的。我建议您查看 HttpSoft/Response 以了解预制响应类型。这也包含在路由中,因为它用于开发模式。

$response = new HttpSoft\Response\HtmlResponse('<p>HTML</p>');
$response = new HttpSoft\Response\JsonResponse(['key' => 'value']);
$response = new HttpSoft\Response\JsonResponse("{key: 'value'}");
$response = new HttpSoft\Response\TextResponse('Text');
$response = new HttpSoft\Response\XmlResponse('<xmltag>XML</xmltag>');
$response = new HttpSoft\Response\RedirectResponse('https/example.com');
$response = new HttpSoft\Response\EmptyResponse();

运行和高级用法

如果您想要对 HTTP 服务器有更多的控制,您可以使用 getHttpServer 方法创建并返回 HttpServer 对象。

$httpServer = $router->getHttpServer();

除了可以检索 HTTP 服务器之外,您还可以使用 getSocketServer 方法检索套接字服务器。

$socketServer = $router->getSocketServer();

开发模式

目前开发模式只做一件事。当您的路由抛出异常时,它会将异常以及堆栈跟踪作为响应返回,而不是将其输出到控制台。

Nodemon

我建议在开发时使用Nodemon,因为它会在每次文件更改时重新启动您的服务器。要安装Nodemon,您需要Node.js和npm。

npm install -g nodemon

然后在项目目录的根目录下创建一个名为nodemon.json的新文件,并将以下内容放入其中

{
    "verbose": false,
    "ignore": [
        ".git",
        ".idea"
    ],
    "execMap": {
        "php": "php"
    },
    "restartable": "r",
    "ext": "php,html,json"
}

之后,不要使用php index.php来启动您的服务器,而是使用nodemon index.php更改一个文件。您会看到服务器因为文件更改而重新启动。现在,您在更改文件时不需要反复重新启动服务器!如果需要,您还可以在控制台输入r手动重新启动!