bramus / router
轻量级且简单的面向对象PHP路由器
Requires
- php: >=5.3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ~2.14
- phpunit/php-code-coverage: ~2.0
- phpunit/phpunit: ~4.8
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 bramus/router ~1.6
演示
在 demo
子文件夹中包含了一个演示。使用您喜欢的Web服务器或通过在shell中执行 php -S localhost:8080
使用PHP 5.4+ 内置的服务器进行服务。包含用于Apache的 .htaccess
。
此外,还包括了一个多语言路由器的演示。该演示位于 demo-multilang
子文件夹中,可以以与正常演示相同的方式运行。
用法
创建一个 \Bramus\Router\Router
实例,将其上的某些路由定义到其中,并运行它。
// Require composer autoloader require __DIR__ . '/vendor/autoload.php'; // Create Router instance $router = new \Bramus\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']
)匹配时,附加的 路由处理函数 将被执行。路由处理函数必须是一个 callable。只有第一个匹配的路由会被处理。当找不到匹配的路由时,将执行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速查表可能会有所帮助。
在动态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; });
可选路由子模式
可以通过在子模式后添加一个?
来使路由子模式成为可选的。想想形如/blog(/year)(/month)(/day)(/slug)
的博客URL。
$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
支持Before Route Middlewares
,它们在路由处理之前执行。
与路由处理函数类似,你将处理函数钩接到一个或多个HTTP请求方法和一个特定的路由模式组合上。
$router->before('GET|POST', '/admin/.*', function() { if (!isset($_SESSION['user'])) { header('location: /auth/login'); exit(); } });
与路由处理函数不同,当找到多个路由匹配时,将执行多个before路由中间件。
Before Router Middlewares
Before路由中间件是特定于路由的。使用通用路由模式(即所有URL),它们可以成为Before Router Middlewares(在其他项目中有时称为before app middlewares),无论请求的URL是什么,它们都会始终执行。
$router->before('GET', '/.*', function() { // ... this will always be executed });
After Router Middleware / Run Callback
运行一个(1)中间件函数,在路由处理之后命名After Router Middleware
(在其他项目中有时称为after app middlewares)。只需通过$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(…)
。
禁用子文件夹支持
如果您不想让bramus/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
关键字将依赖项传递到处理函数中,将其他库与bramus/router
集成。
$tpl = new \Acme\Template\Template(); $router->get('/', function() use ($tpl) { $tpl->load('home.tpl'); $tpl->setdata(array( 'name' => 'Bramus!' )); }); $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请求发送的信息相同。此方法可用于在不传输实体主体的情况下获取由请求暗示的实体元信息。此方法通常用于测试超文本链接的有效性、可访问性和最近修改。
为此,bramus/router
将内部重新路由HEAD
请求到等效的GET
请求,并自动抑制所有输出。
单元测试与代码覆盖率
bramus/router
附带使用PHPUnit的单元测试。
-
如果PHPUnit已全局安装,请运行
phpunit
以运行测试。 -
如果PHPUnit未全局安装,请通过运行
composer install --dev
在本地安装它。通过调用vendor/bin/phpunit
运行测试本身。包含的
composer.json
还将安装php-code-coverage
,这允许您生成代码覆盖率报告
。运行phpunit --coverage-html ./tests-report
(需要XDebug),报告将放置在tests-report
子文件夹中。
致谢
bramus/router
受到Klein、Ham和JREAM/route的启发。虽然Klein提供了许多功能,但它不是面向对象的。虽然Ham是面向对象的,但它不擅长关注点分离,因为它还在路由类中提供模板。虽然JREAM/route是一个很好的起点,但它所做的限制(例如,只有GET路由)。
许可证
bramus/router
在MIT公共许可证下发布。有关详细信息,请参阅所附的LICENSE
。