miladrahimi/phprouter

一个功能强大、轻量级且非常快速的 PHP 项目 HTTP URL 路由器。

5.1.4 2022-09-06 09:45 UTC

README

Latest Stable Version Total Downloads Build codecov Scrutinizer Code Quality License

PhpRouter

PhpRouter 是一个功能全面且非常快速的 HTTP URL 路由器,适用于 PHP 项目。

一些提供的功能

  • 路由参数
  • 预定义的路由参数模式
  • 中间件
  • 闭包和类控制器/中间件
  • 路由分组(按前缀、中间件和域名)
  • 路由命名(以及通过名称生成路由)
  • PSR-7 请求和响应
  • 视图(简单的 PHP/HTML 视图)
  • 多个(子)域名(使用正则表达式模式)
  • 自定义 HTTP 方法
  • 与 IoC 容器集成(PhpContainer
  • Request、Route、Url 等的自动注入方法和构造函数

当前版本需要 PHP v7.4 或更高版本,包括 v8.*

内容

版本

  • v5.x.x(当前,受支持)
  • v4.x.x
  • v3.x.x
  • v2.x.x
  • v1.x.x

文档

安装

安装 Composer 并在项目根目录中运行以下命令

composer require miladrahimi/phprouter "5.*"

配置

首先,您需要配置您的 Web 服务器以使用单个 PHP 文件(如 index.php 文件)处理所有 HTTP 请求。以下为 NGINX 和 Apache HTTP 服务器配置示例。

  • NGINX 配置示例

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
  • Apache .htaccess 配置示例

    <IfModule mod_rewrite.c>
        <IfModule mod_negotiation.c>
            Options -MultiViews
        </IfModule>
    
        RewriteEngine On
    
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule ^(.*)/$ /$1 [L,R=301]
    
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteRule ^ index.php [L]
    </IfModule>
    

入门

与 PhpRouter 一起工作非常简单!只需看一下以下示例。

  • JSON API 示例

    use MiladRahimi\PhpRouter\Router;
    use Laminas\Diactoros\Response\JsonResponse;
    
    $router = Router::create();
    
    $router->get('/', function () {
        return new JsonResponse(['message' => 'ok']);
    });
    
    $router->dispatch();
  • 视图示例

    use MiladRahimi\PhpRouter\Router;
    use MiladRahimi\PhpRouter\View\View
    
    $router = Router::create();
    $router->setupView('/../views');
    
    $router->get('/', function (View $view) {
        return $view->make('profile', ['user' => 'Jack']);
    });
    
    $router->dispatch();

HTTP 方法

以下示例说明如何为不同的 HTTP 方法声明不同的路由。

use MiladRahimi\PhpRouter\Router;

$router = Router::create();

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

$router->dispatch();

您可以使用 define() 方法为其他 HTTP 方法,如下例所示

use MiladRahimi\PhpRouter\Router;

$router = Router::create();

$router->define('GET', '/', function () { /* ... */ });
$router->define('OPTIONS', '/', function () { /* ... */ });
$router->define('CUSTOM', '/', function () { /* ... */ });

$router->dispatch();

如果您不关心 HTTP 动词,可以使用 any() 方法。

use MiladRahimi\PhpRouter\Router;

$router = Router::create();

$router->any('/', function () {
    return 'This is Home! No matter what the HTTP method is!';
});

$router->dispatch();

控制器

闭包控制器

use MiladRahimi\PhpRouter\Router;

$router = Router::create();

$router->get('/', function () {
    return 'This is a closure controller!';
});

$router->dispatch();

类方法控制器

use MiladRahimi\PhpRouter\Router;

class UsersController
{
    function index()
    {
        return 'Class: UsersController & Method: index';
    }

    function handle()
    {
        return 'Class UsersController.';
    }
}

$router = Router::create();

// Controller: Class=UsersController Method=index()
$router->get('/method', [UsersController::class, 'index']);

// Controller: Class=UsersController Method=handle()
$router->get('/class', UsersController::class);

$router->dispatch();

路由参数

一个 URL 可能有一个或多个变量部分,例如购物网站上产品 ID。我们称之为路由参数。您可以通过控制器方法参数捕获它们,如下例所示。

use MiladRahimi\PhpRouter\Router;

$router = Router::create();

// Required parameter
$router->get('/post/{id}', function ($id) {
    return "The content of post $id";
});

// Optional parameter
$router->get('/welcome/{name?}', function ($name = null) {
    return 'Welcome ' . ($name ?: 'Dear User');
});

// Optional parameter, Optional / (Slash)!
$router->get('/profile/?{user?}', function ($user = null) {
    return ($user ?: 'Your') . ' profile';
});

// Optional parameter with default value
$router->get('/roles/{role?}', function ($role = 'guest') {
    return "Your role is $role";
});

// Multiple parameters
$router->get('/post/{pid}/comment/{cid}', function ($pid, $cid) {
    return "The comment $cid of the post $pid";
});

$router->dispatch();

路由参数模式

默认情况下,路由参数可以具有任何值,但您可以定义正则表达式模式来限制它们。

use MiladRahimi\PhpRouter\Router;

$router = Router::create();

// "id" must be numeric
$router->pattern('id', '[0-9]+');

$router->get('/post/{id}', function (int $id) { /* ... */ });

$router->dispatch();

请求和响应

PhpRouter 使用 laminas-diactoros(以前称为 zend-diactoros)包(v2)向您的控制器和中间件提供 PSR-7 请求和响应对象。

请求

您可以在控制器中捕获请求对象,如下例所示

use MiladRahimi\PhpRouter\Router;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\Response\JsonResponse;

$router = Router::create();

$router->get('/', function (ServerRequest $request) {
    $method  = $request->getMethod();
    $uriPath = $request->getUri()->getPath();
    $headers = $request->getHeaders();
    $queryParameters = $request->getQueryParams();
    $bodyContents    = $request->getBody()->getContents();
    // ...
});

$router->dispatch();

响应

以下示例说明了内置的响应。

use Laminas\Diactoros\Response\RedirectResponse;
use MiladRahimi\PhpRouter\Router;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\Response\TextResponse;

$router = Router::create();

$router->get('/html/1', function () {
    return '<html>This is an HTML response</html>';
});
$router->get('/html/2', function () {
    return new HtmlResponse('<html>This is also an HTML response</html>', 200);
});
$router->get('/json', function () {
    return new JsonResponse(['error' => 'Unauthorized!'], 401);
});
$router->get('/text', function () {
    return new TextResponse('This is a plain text...');
});
$router->get('/empty', function () {
    return new EmptyResponse(204);
});
$router->get('/redirect', function () {
    return new RedirectResponse('https://miladrahimi.com');
});

$router->dispatch();

视图

您可能需要创建一个使用视图的经典样式服务器端渲染(SSR)网站。PhpRouter提供了一个简单功能来处理PHP/HTML视图。请看以下示例。

use MiladRahimi\PhpRouter\Router;
use MiladRahimi\PhpRouter\View\View

$router = Router::create();

// Setup view feature and set the directory of view files
$router->setupView(__DIR__ . '/../views');

$router->get('/profile', function (View $view) {
    // It looks for a view with path: __DIR__/../views/profile.phtml
    return $view->make('profile', ['user' => 'Jack']);
});

$router->get('/blog/post', function (View $view) {
    // It looks for a view with path: __DIR__/../views/blog/post.phtml
    return $view->make('blog.post', ['post' => $post]);
});

$router->dispatch();

还有一些要点

  • 视图文件必须具有".phtml"扩展名(例如profile.phtml)。
  • 您可以使用点.来分隔目录(例如blog.post对应于blog/post.phtml)。

视图文件是纯PHP或与HTML混合。您应该在视图文件中使用模板风格的PHP语言。这是一个示例视图文件

<h1><?php echo $title ?></h1>
<ul>
    <?php foreach ($posts as $post): ?>
        <li><?php echo $post['content'] ?></li>
    <?php endforeach ?>
</ul>

路由分组

您可以将路由分组。组可以具有一些公共属性,如中间件、域名或前缀。以下示例展示了如何分组路由

use MiladRahimi\PhpRouter\Router;

$router = Router::create();

// A group with uri prefix
$router->group(['prefix' => '/admin'], function (Router $router) {
    // URI: /admin/setting
    $router->get('/setting', function () {
        return 'Setting Panel';
    });
});

// All of group attributes together!
$attributes = [
    'prefix' => '/admin',
    'domain' => 'shop.example.com',
    'middleware' => [AuthMiddleware::class],
];

$router->group($attributes, function (Router $router) {
    // URL: http://shop.example.com/admin/users
    // Domain: shop.example.com
    // Middleware: AuthMiddleware
    $router->get('/users', [UsersController::class, 'index']);
});

$router->dispatch();

组属性将在本文档的后续部分进行解释。

您还可以使用Attributes枚举。

中间件

PhpRouter支持中间件。您可以使用它进行不同的目的,如身份验证、授权、节流等。中间件在控制器前后运行,可以检查和操作请求和响应。

在此,您可以看到考虑了一些中间件的请求生命周期

[Request]  ↦ Router ↦ Middleware 1 ↦ ... ↦ Middleware N ↦ Controller
                                                              ↧
[Response] ↤ Router ↤ Middleware 1 ↤ ... ↤ Middleware N ↤ [Response]

要声明中间件,您可以使用闭包和类,就像控制器一样。要使用中间件,您必须分组路由并在组属性中提及中间件。注意!组中的中间件属性接受一个中间件数组,而不是单个中间件。

use MiladRahimi\PhpRouter\Router;
use Psr\Http\Message\ServerRequestInterface;
use Laminas\Diactoros\Response\JsonResponse;

class AuthMiddleware
{
    public function handle(ServerRequestInterface $request, Closure $next)
    {
        if ($request->getHeader('Authorization')) {            
            // Call the next middleware/controller
            return $next($request);
        }

        return new JsonResponse(['error' => 'Unauthorized!'], 401);
    }
}

$router = Router::create();

// The middleware attribute takes an array of middleware, not a single one!
$router->group(['middleware' => [AuthMiddleware::class]], function(Router $router) {
    $router->get('/admin', function () {
        return 'Admin API';
    });
});

$router->dispatch();

如您所见,中间件捕获了请求和$next闭包。闭包调用下一个中间件或控制器(如果没有中间件),中间件必须返回一个响应。中间件可以中断生命周期并返回一个响应,或者它可以调用$next闭包以继续生命周期。

域名和子域名

您的应用程序可能在不同的域名或子域名上提供不同的服务。在这种情况下,您可以指定路由的域名或子域名。请看以下示例

use MiladRahimi\PhpRouter\Router;

$router = Router::create();

// Domain
$router->group(['domain' => 'shop.com'], function(Router $router) {
    $router->get('/', function () {
        return 'This is shop.com';
    });
});

// Subdomain
$router->group(['domain' => 'admin.shop.com'], function(Router $router) {
    $router->get('/', function () {
        return 'This is admin.shop.com';
    });
});

// Subdomain with regex pattern
$router->group(['domain' => '(.*).example.com'], function(Router $router) {
    $router->get('/', function () {
        return 'This is a subdomain';
    });
});

$router->dispatch();

路由名称

您可以为您的路由分配名称并在代码中使用它们,而不是使用硬编码的URL。请看以下示例

use MiladRahimi\PhpRouter\Router;
use Laminas\Diactoros\Response\JsonResponse;
use MiladRahimi\PhpRouter\Url;

$router = Router::create();

$router->get('/about', [AboutController::class, 'show'], 'about');
$router->get('/post/{id}', [PostController::class, 'show'], 'post');
$router->get('/links', function (Url $url) {
    return new JsonResponse([
        'about' => $url->make('about'),             /* Result: /about  */
        'post1' => $url->make('post', ['id' => 1]), /* Result: /post/1 */
        'post2' => $url->make('post', ['id' => 2])  /* Result: /post/2 */
    ]);
});

$router->dispatch();

当前路由

您可能在控制器或中间件中需要获取有关当前路由的信息。此示例展示了如何获取这些信息。

use MiladRahimi\PhpRouter\Router;
use Laminas\Diactoros\Response\JsonResponse;
use MiladRahimi\PhpRouter\Routing\Route;

$router = Router::create();

$router->get('/{id}', function (Route $route) {
    return new JsonResponse([
        'uri'    => $route->getUri(),            /* Result: "/1" */
        'name'   => $route->getName(),           /* Result: "sample" */
        'path'   => $route->getPath(),           /* Result: "/{id}" */
        'method' => $route->getMethod(),         /* Result: "GET" */
        'domain' => $route->getDomain(),         /* Result: null */
        'parameters' => $route->getParameters(), /* Result: {"id": "1"} */
        'middleware' => $route->getMiddleware(), /* Result: []  */
        'controller' => $route->getController(), /* Result: {}  */
    ]);
}, 'sample');

$router->dispatch();

IoC容器

PhpRouter使用PhpContainer为包本身和应用程序的依赖项提供IoC容器。

PhpRouter如何使用容器?

PhpRouter绑定路由参数、HTTP请求、路由(当前路由)、URL(URL生成器)、容器本身。控制器方法或构造函数可以解析这些依赖项并捕获它们。

您的应用程序如何使用容器?

只需查看以下示例。

use MiladRahimi\PhpContainer\Container;
use MiladRahimi\PhpRouter\Router;

$router = Router::create();

$router->getContainer()->singleton(Database::class, MySQL::class);
$router->getContainer()->singleton(Config::class, JsonConfig::class);

// Resolve directly
$router->get('/', function (Database $database, Config $config) {
    // Use MySQL and JsonConfig...
});

// Resolve container
$router->get('/', function (Container $container) {
    $database = $container->get(Database::class);
    $config = $container->get(Config::class);
});

$router->dispatch();

有关此强大IoC容器的更多信息,请参阅PhpContainer

错误处理

应用程序通过Router::dispatch()方法运行。您应该将其放在try块中并捕获异常。它抛出应用程序和PhpRouter异常。

use MiladRahimi\PhpRouter\Router;
use MiladRahimi\PhpRouter\Exceptions\RouteNotFoundException;
use Laminas\Diactoros\Response\HtmlResponse;

$router = Router::create();

$router->get('/', function () {
    return 'Home.';
});

try {
    $router->dispatch();
} catch (RouteNotFoundException $e) {
    // It's 404!
    $router->getPublisher()->publish(new HtmlResponse('Not found.', 404));
} catch (Throwable $e) {
    // Log and report...
    $router->getPublisher()->publish(new HtmlResponse('Internal error.', 500));
}

PhpRouter抛出以下异常

  • RouteNotFoundException如果PhpRouter找不到任何与用户请求匹配的路由。
  • InvalidCallableException如果PhpRouter无法调用控制器或中间件。

RouteNotFoundException应视为404 Not found错误。

许可

PhpRouter最初由Milad Rahimi创建,并按照MIT许可证发布。