soanix / router
轻量级且简单的面向对象的PHP路由器
Requires
- php: >=8.0
- ext-json: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ~2.14
- phpunit/php-code-coverage: ~2.2.3
- phpunit/phpunit: ~4.8.36
README
一个轻量级且简单的面向对象的PHP路由器。由Bramus Van Damme (https://www.bram.us)和贡献者构建
特性
- 支持
GET、POST、PUT、DELETE、OPTIONS、PATCH和HEAD请求方法 - 支持如
get()、post()、put()等路由简写 - 静态路由模式
- 动态路由模式:基于PCRE的动态路由模式或基于占位符的动态路由模式
- 可选路由子模式
- 支持
X-HTTP-Method-Override头部 - 子路由/路由挂载
- 允许
Class@Method调用 - 自定义404处理
- 路由之前中间件
- 路由之前中间件/应用之前中间件
- 路由之后中间件/应用之后中间件(完成回调)
- 在子文件夹中也能正常工作
先决条件/要求
- PHP 5.3或更高版本
- URL重写
安装
可以使用Composer进行安装
composer require soanix/router ~1.6
演示
在demo子文件夹中包含了一个演示。使用您喜欢的Web服务器提供服务,或者使用PHP 5.4+内置的服务器,通过在shell中执行php -S localhost:8080来提供服务。包含了一个用于Apache的.htaccess文件。
此外,还包括了一个多语言路由器的演示。这个演示可以在demo-multilang子文件夹中找到,并且可以使用与正常演示相同的方式运行。
使用方法
创建一个\Bramus\Router\Router实例,向其定义一些路由,然后运行它。
// Require composer autoloader require __DIR__ . '/vendor/autoload.php'; use \Soanix\Router\Router; // Define routes // ... // Run it! Router::run();
路由
使用Router::match(method(s), pattern, function)钩子路由(一个或多个HTTP方法的组合和一个模式)
Router::match('GET|POST', 'pattern', function() { … });
bramus/router支持GET、POST、PUT、PATCH、DELETE、HEAD (见说明)和OPTIONS HTTP请求方法。传入单个请求方法,或多个用|分隔的请求方法。
当路由与当前URL(例如$_SERVER['REQUEST_URI'])匹配时,将执行附加的路由处理函数。路由处理函数必须是一个可调用。只有第一个匹配的路由将被处理。如果没有找到匹配的路由,将执行404处理程序。
路由简写
提供了单请求方法的简写
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::all('pattern', function() { … });
注意:必须在调用Router::run();之前钩子路由。
注意:没有为match()提供简写,因为bramus/router会内部将此类请求重定向到等效的GET请求,以符合RFC2616 (见说明)。
路由模式
路由模式可以是静态的或动态的
- 静态路由模式不包含动态部分,必须与当前URL的
path部分完全匹配。 - 动态路由模式包含可以随请求而变化的动态部分。这些变化的部分被称为子模式,可以使用Perl兼容正则表达式(PCRE)或使用占位符来定义。
静态路由模式
静态路由模式是一个表示URI的普通字符串。它将直接与当前URL的path部分进行比较。
示例
/about/contact
使用示例
// This route handling function will only be executed when visiting http(s)://www.example.org/about Router::get('/about', function() { echo 'About Page Contents'; });
基于PCRE的动态路由模式
此类路由模式包含可以随请求而变化的动态部分。这些变化的部分被称为子模式,并使用正则表达式进行定义。
示例
/movies/(\d+)/profile/(\w+)
在动态路由模式中,常用的基于PCRE的子模式有:
\d+= 一个或多个数字(0-9)\w+= 一个或多个字母数字字符(a-z 0-9 _)[a-z0-9_-]+= 一个或多个字母数字字符(a-z 0-9 _)和连字符(-).*= 任何字符(包括/),零个或多个[^/]+= 任何字符但/,一个或多个
注意:PHP PCRE速查表(https://courses.cs.washington.edu/courses/cse154/15sp/cheat-sheets/php-regex-cheat-sheet.pdf)可能很有用。
在动态PCRE路由模式中定义的子模式被转换为参数,传递给路由处理函数。前提是这些子模式需要定义为括号子模式,这意味着它们应该被括号包围。
// Bad Router::get('/hello/\w+', function($name) { echo 'Hello ' . htmlentities($name); }); // Good Router::get('/hello/(\w+)', function($name) { echo 'Hello ' . htmlentities($name); });
注意:路由模式开头处的/不是必须的,但建议使用。
当定义了多个子模式时,结果的路由处理参数将按照定义的顺序传递给路由处理函数。
Router::get('/movies/(\d+)/photos/(\d+)', function($movieId, $photoId) { echo 'Movie #' . $movieId . ', photo #' . $photoId; });
基于占位符的动态路由模式
此类路由模式与基于PCRE的动态路由模式相同,但有一个区别:它们不使用正则表达式进行模式匹配,而是使用更简单的占位符。占位符是包围在花括号中的字符串,例如{name}。您不需要在占位符周围添加括号。
示例
/movies/{id}/profile/{username}
占位符比PCRE更容易使用,但提供的控制较少,因为它们在内部被转换为匹配任何字符的正则表达式(.*)。
Router::get('/movies/{movieId}/photos/{photoId}', function($movieId, $photoId) { echo 'Movie #' . $movieId . ', photo #' . $photoId; });
注意:占位符的名称不需要与传递给路由处理函数的参数名称匹配。
Router::get('/movies/{foo}/photos/{bar}', function($movieId, $photoId) { echo 'Movie #' . $movieId . ', photo #' . $photoId; });
可选路由子模式
可以通过在子模式后添加?使其成为可选的,例如博客URL的形式为/blog(/year)(/month)(/day)(/slug)。
Router::get( '/blog(/\d+)?(/\d+)?(/\d+)?(/[a-z0-9_-]+)?', function($year = null, $month = null, $day = null, $slug = null) { if (!$year) { echo 'Blog overview'; return; } if (!$month) { echo 'Blog year overview'; return; } if (!$day) { echo 'Blog month overview'; return; } if (!$slug) { echo 'Blog day overview'; return; } echo 'Blogpost ' . htmlentities($slug) . ' detail'; } );
上面的代码片段响应了以下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($year = null, $month = null, $day = null, $slug = null) { // ... });
注意:强烈建议始终定义连续的可选参数。
使用量词来要求URL中正确数量的数字。
Router::get('/blog(/\d{4}(/\d{2}(/\d{2}(/[a-z0-9_-]+)?)?)?)?', function($year = null, $month = null, $day = null, $slug = null) { // ... });
子路由/路由挂载
使用 Router::mount($baseroute, $fn) 将一组路由挂载到子路由模式上。子路由模式将添加到该作用域中定义的所有后续路由之前。例如,将回调 $fn 挂载到 /movies 上将在所有后续路由之前添加 /movies。
Router::mount('/movies', function() use ($router) { // will result in '/movies/' Router::get('/', function() { echo 'movies overview'; }); // will result in '/movies/id' Router::get('/(\d+)', function($id) { echo 'movie id ' . htmlentities($id); }); });
子路由嵌套是可能的,只需在已包含在先前 Router::mount() 中的可调用对象中定义第二个 Router::mount()。
Class@Method 调用
我们可以这样路由到类动作
Router::get('/(\d+)', '\App\Controllers\User@showProfile');
当一个请求匹配指定的路由 URI 时,User 类的 showProfile 方法将被执行。定义的路由参数将被传递到类方法。
该方法可以是静态的(推荐)或非静态的(不推荐)。如果方法是非静态的,将创建该类的一个新实例。
如果大多数或所有的处理类都在同一个命名空间中,您可以通过 setNamespace() 在您的路由实例上设置默认命名空间。
Router::setNamespace('\App\Controllers'); Router::get('/users/(\d+)', 'User@showProfile'); Router::get('/cars/(\d+)', 'Car@showProfile');
自定义 404
默认的 404 处理程序设置一个 404 状态码并退出。您可以通过使用 Router::set404(callable); 来覆盖此默认的 404 处理程序。
Router::set404(function() { header('HTTP/1.1 404 Not Found'); // ... do something special here });
您还可以定义多个自定义路由,例如,您想要定义一个 /api 路由,您可以打印一个自定义的 404 页面。
Router::set404('/api(/.*)?', function() { header('HTTP/1.1 404 Not Found'); header('Content-Type: application/json'); $jsonArray = array(); $jsonArray['status'] = "404"; $jsonArray['status_text'] = "route not defined"; echo json_encode($jsonArray); });
也支持 Class@Method 可调用对象
Router::set404('\App\Controllers\Error@notFound');
当没有路由模式匹配当前 URL 时,将执行 404 处理程序。
💡 您也可以通过调用 Router::trigger404() 手动触发 404 处理程序
Router::get('/([a-z0-9-]+)', function($id) use ($router) { if (!Posts::exists($id)) { Router::trigger404(); return; } // … });
路由之前中间件
bramus/router 支持 路由之前的中间件,这些中间件在处理路由之前执行。
与路由处理函数一样,您将处理函数连接到一组 HTTP 请求方法和一个特定的路由模式。
Router::middleware('GET|POST', '/admin/.*', function() { if (!isset($_SESSION['user'])) { header('location: /auth/login'); exit(); } });
与路由处理函数不同,当找到多个路由匹配项时,将执行多个路由之前的中间件。
路由之前的中间件
路由之前的中间件是路由特定的。使用通用的路由模式(即 所有 URL),它们可以成为 路由之前的中间件(在其他项目中有时被称为路由之前的中间件),无论请求的 URL 是什么,它们都会始终执行。
Router::middleware('GET', '/.*', function() { // ... this will always be executed });
路由之后中间件/运行回调
运行一个(1)中间件函数,在路由处理完成后命名 路由之后中间件(在其他项目中有时称为应用之后中间件)。只需通过 Router::run() 函数传递即可。运行回调是路由无关的。
Router::run(function() { … });
注意:如果路由处理函数已执行 exit(),则不会运行运行回调。
重写请求方法
使用 X-HTTP-Method-Override 来重写 HTTP 请求方法。仅在原始请求方法是 POST 时有效。允许的 X-HTTP-Method-Override 值是 PUT、DELETE 或 PATCH。
子文件夹支持
默认情况下,bramus/router 将在任何(子)文件夹中运行……您不需要对代码进行任何调整。您可以自由移动您的 入口脚本 index.php,并且路由器将自动根据当前文件夹的路径相对于它来调整自己,通过将所有路由挂载到该 basePath。
例如,如果您有一个托管域名 www.example.org 的服务器,使用 public_html/ 作为其文档根目录,并具有这个小 入口脚本 index.php:
Router::get('/', function() { echo 'Index'; }); Router::get('/hello', function() { echo 'Hello!'; });
-
如果您将此文件(以及其相关的
.htaccess文件等)放置在文档根级别(例如public_html/index.php),bramus/router将将所有路由挂载到域名根(例如/),因此响应https://www.example.org/和https://www.example.org/hello。 -
如果您要将此文件(及其相关的
.htaccess文件等)移动到子文件夹中(例如public_html/demo/index.php),则bramus/router将所有路由挂载到当前路径(例如/demo),因此将响应对https://www.example.org/demo和https://www.example.org/demo/hello的请求。在这种情况下,无需使用Router::mount(…)。
禁用子文件夹支持
如果您不希望soanix/router自动适应其放置的文件夹,可以通过调用setBasePath()手动覆盖basePath。这在(不常见)情况下是必要的,即您的入口脚本和您的入口URL不是紧密耦合的(例如,当入口脚本放置在一个不需要成为它响应的URL部分的子文件夹中时)。
// Override auto base path detection Router::setBasePath('/'); Router::get('/', function() { echo 'Index'; }); Router::get('/hello', function() { echo 'Hello!'; }); Router::run();
如果您要将此文件放置到子文件夹中(例如public_html/some/sub/folder/index.php),它仍然会将路由挂载到域名根目录(例如/),因此将响应对https://www.example.org/和https://www.example.org/hello的请求(假设您的.htaccess文件——放置在文档根级别——重新编写了对它的请求)。
与其他库的集成
通过使用use关键字将依赖项传递到处理函数中,将其他库与soanix/router集成。
$tpl = new \Acme\Template\Template(); Router::get('/', function() use ($tpl) { $tpl->load('home.tpl'); $tpl->setdata(array( 'name' => 'Soanix!' )); }); Router::run(function() use ($tpl) { $tpl->display(); });
在这个结构中,仍然可以在After Router Middleware内部操作输出
关于PUT操作的工作方法
PHP中不存在$_PUT。必须伪造它
Router::put('/movies/(\d+)', function($id) { // Fake $_PUT $_PUT = array(); parse_str(file_get_contents('php://input'), $_PUT); // ... });
关于发送HEAD请求的注意事项
在发送HEAD请求时,所有输出都将被缓冲,以防止任何内容渗透到响应体中,如RFC2616(超文本传输协议——HTTP/1.1)中定义。
HEAD方法与GET方法相同,但服务器必须在响应中返回消息体。对HEAD请求的响应中包含在HTTP头中的元信息应与对GET请求的响应中发送的元信息相同。此方法可以用于获取由请求隐含的实体的元信息,而不传输实体体。此方法通常用于测试超文本链接的有效性、可访问性和最近修改。
为了实现这一点,soanix/router将内部重新路由HEAD请求到等效的GET请求,并自动抑制所有输出。
单元测试 & 代码覆盖率
soanix/router附带使用PHPUnit的单元测试。
-
如果全局安装了PHPUnit,请运行
phpunit以运行测试。 -
如果没有全局安装PHPUnit,请通过composer在本地安装它,运行
composer install --dev。通过调用vendor/bin/phpunit运行测试。包含的
composer.json还会安装php-code-coverage,允许您生成一个代码覆盖率报告。运行phpunit --coverage-html ./tests-report(需要XDebug),报告将放置在tests-report子文件夹中。
致谢
soanix/router是从bramus/router派生出来的。
bramus/router灵感来源于Klein、Ham和JREAM/route 。虽然Klein提供了许多功能,但它不是面向对象的。虽然Ham是面向对象的,但它不擅长关注点分离,因为它还在路由类中提供了模板。虽然JREAM/route是一个好的起点,但它所做的限制(例如,只有GET路由)。
许可证
bramus/router和soanix/router在MIT公共许可证下发布。有关详细信息,请参阅所附的LICENSE。