路由

注意: 想了解更多关于路由的内容吗?请查看"为什么选择框架?"页面以获取更深入的解释。

在 Flight 中,基本路由是通过将 URL 模式与回调函数或类和方法的数组匹配来完成的。

Flight::route('/', function(){
    echo '你好,世界!';
});

路由按照定义的顺序进行匹配。第一个匹配请求的路由将被调用。

回调/函数

回调可以是任何可调用的对象。因此,您可以使用常规函数:

function hello() {
    echo '你好,世界!';
}

Flight::route('/', 'hello');

您也可以使用类的静态方法:

class Greeting {
    public static function hello() {
        echo '你好,世界!';
    }
}

Flight::route('/', [ 'Greeting','hello' ]);

或者先创建一个对象,然后调用该方法:


// Greeting.php
class Greeting
{
    public function __construct() {
        $this->name = '约翰·多';
    }

    public function hello() {
        echo "你好,{$this->name}!";
    }
}

// index.php
$greeting = new Greeting();

Flight::route('/', [ $greeting, 'hello' ]);
// 您也可以在未先创建对象的情况下执行此操作
// 注意:没有参数将被注入到构造函数
Flight::route('/', [ 'Greeting', 'hello' ]);
// 此外,您还可以使用更短的语法
Flight::route('/', 'Greeting->hello');
// 或者
Flight::route('/', Greeting::class.'->hello');

通过 DIC(依赖注入容器)进行依赖注入

如果您想通过容器(PSR-11,PHP-DI,Dice 等)使用依赖注入, 唯一可用的路由类型是直接自己创建对象并使用容器创建对象,或者可以使用字符串来定义要调用的类和方法。您可以查看依赖注入页面以获取更多信息。

这是一个快速示例:


use flight\database\PdoWrapper;

// Greeting.php
class Greeting
{
    protected PdoWrapper $pdoWrapper;
    public function __construct(PdoWrapper $pdoWrapper) {
        $this->pdoWrapper = $pdoWrapper;
    }

    public function hello(int $id) {
        // 对 $this->pdoWrapper 做一些事情
        $name = $this->pdoWrapper->fetchField("SELECT name FROM users WHERE id = ?", [ $id ]);
        echo "你好,世界!我的名字是 {$name}!";
    }
}

// index.php

// 设置容器,带上您需要的参数
// 有关 PSR-11 的更多信息,请查看依赖注入页面
$dice = new \Dice\Dice();

// 别忘了要重新分配变量为 '$dice = '!!!!!
$dice = $dice->addRule('flight\database\PdoWrapper', [
    'shared' => true,
    'constructParams' => [ 
        'mysql:host=localhost;dbname=test', 
        'root',
        'password'
    ]
]);

// 注册容器处理程序
Flight::registerContainerHandler(function($class, $params) use ($dice) {
    return $dice->create($class, $params);
});

// 像往常一样路由
Flight::route('/hello/@id', [ 'Greeting', 'hello' ]);
// 或
Flight::route('/hello/@id', 'Greeting->hello');
// 或
Flight::route('/hello/@id', 'Greeting::hello');

Flight::start();

方法路由

默认情况下,路由模式会与所有请求方法进行匹配。您可以通过在 URL 之前添加标识符来响应特定方法。

Flight::route('GET /', function () {
  echo '我收到了一个 GET 请求。';
});

Flight::route('POST /', function () {
  echo '我收到了一个 POST 请求。';
});

// 您不能使用 Flight::get() 来创建路由,因为那是一个用于获取变量的方法,而不是创建路由。
// Flight::post('/', function() { /* 代码 */ });
// Flight::patch('/', function() { /* 代码 */ });
// Flight::put('/', function() { /* 代码 */ });
// Flight::delete('/', function() { /* 代码 */ });

您也可以通过使用 | 分隔符将多个方法映射到单个回调:

Flight::route('GET|POST /', function () {
  echo '我收到了一个 GET 或 POST 请求。';
});

此外,您可以获取具有一些助手方法的 Router 对象:


$router = Flight::router();

// 映射所有方法
$router->map('/', function() {
    echo '你好,世界!';
});

// GET 请求
$router->get('/users', function() {
    echo '用户';
});
// $router->post();
// $router->put();
// $router->delete();
// $router->patch();

正则表达式

您可以在路由中使用正则表达式:

Flight::route('/user/[0-9]+', function () {
  // 这将匹配 /user/1234
});

虽然此方法可用,但建议使用命名参数,或与正则表达式结合的命名参数,因为它们更具可读性且更易于维护。

命名参数

您可以在路由中指定命名参数,这些参数将传递给回调函数。这更是为了路由的可读性,而非其他原因。请参见下面关于重要注意事项的部分。

Flight::route('/@name/@id', function (string $name, string $id) {
  echo "你好,$name ($id)!";
});

您还可以通过使用 : 分隔符将正则表达式包含在命名参数中:

Flight::route('/@name/@id:[0-9]{3}', function (string $name, string $id) {
  // 这将匹配 /bob/123
  // 但不会匹配 /bob/12345
});

注意: 匹配正则组 () 和位置参数不受支持。:'(

重要注意事项

虽然在上面的示例中,似乎 @name 直接与变量 $name 绑定,但实际上并非如此。回调函数中参数的顺序决定了传递给它的内容。因此,如果您更改回调函数中参数的顺序,变量也会随之交换。以下是一个示例:

Flight::route('/@name/@id', function (string $id, string $name) {
  echo "你好,$name ($id)!";
});

如果您访问以下 URL:/bob/123,输出将是 你好,123 (bob)!。在设置路由和回调函数时,请小心。

可选参数

您可以通过将段括在括号中来指定可选的命名参数以进行匹配。

Flight::route(
  '/blog(/@year(/@month(/@day)))',
  function(?string $year, ?string $month, ?string $day) {
    // 这将匹配以下 URL:
    // /blog/2012/12/10
    // /blog/2012/12
    // /blog/2012
    // /blog
  }
);

任何未匹配的可选参数将作为 NULL 传递。

通配符

匹配仅针对单个 URL 段进行。如果您想匹配多个段,可以使用 * 通配符。

Flight::route('/blog/*', function () {
  // 这将匹配 /blog/2000/02/01
});

要将所有请求路由到单个回调,您可以这样做:

Flight::route('*', function () {
  // 做一些事情
});

传递

您可以通过从回调函数返回 true 将执行权传递给下一个匹配路由。

Flight::route('/user/@name', function (string $name) {
  // 检查某个条件
  if ($name !== "Bob") {
    // 继续到下一个路由
    return true;
  }
});

Flight::route('/user/*', function () {
  // 这将被调用
});

路由别名

您可以为路由分配别名,以便稍后在代码中动态生成 URL(例如作为模板)。

Flight::route('/users/@id', function($id) { echo '用户:'.$id; }, false, 'user_view');

// 稍后在代码的某个地方
Flight::getUrl('user_view', [ 'id' => 5 ]); // 将返回 '/users/5'

这在您的 URL 发生变化时特别有用。在上面的示例中,假设用户被移到 /admin/users/@id 。 有了别名后,您不必更改在任何地方引用别名的地方,因为别名将现在返回 /admin/users/5,就像上面的示例一样。

路由别名仍然可以在组中使用:

Flight::group('/users', function() {
    Flight::route('/@id', function($id) { echo '用户:'.$id; }, false, 'user_view');
});

// 稍后在代码的某个地方
Flight::getUrl('user_view', [ 'id' => 5 ]); // 将返回 '/users/5'

路由信息

如果您想检查匹配路由的信息,可以通过在路由方法中传入 true 作为第三个参数来请求将路由对象传递给回调。路由对象将始终是传递给回调函数的最后一个参数。

Flight::route('/', function(\flight\net\Route $route) {
  // 匹配的 HTTP 方法数组
  $route->methods;

  // 命名参数数组
  $route->params;

  // 匹配的正则表达式
  $route->regex;

  // 包含 URL 模式中使用的任何 '*' 的内容
  $route->splat;

  // 显示 URL 路径....如果您真的需要它
  $route->pattern;

  // 显示分配给此路由的中间件
  $route->middleware;

  // 显示分配给此路由的别名
  $route->alias;
}, true);

路由分组

有时候您可能希望将相关路由分组在一起(例如 /api/v1)。 您可以使用 group 方法来实现:

Flight::group('/api/v1', function () {
  Flight::route('/users', function () {
    // 匹配 /api/v1/users
  });

  Flight::route('/posts', function () {
    // 匹配 /api/v1/posts
  });
});

您甚至可以嵌套组:

Flight::group('/api', function () {
  Flight::group('/v1', function () {
    // Flight::get() 获取变量,不设置路由!请参见下面的对象上下文
    Flight::route('GET /users', function () {
      // 匹配 GET /api/v1/users
    });

    Flight::post('/posts', function () {
      // 匹配 POST /api/v1/posts
    });

    Flight::put('/posts/1', function () {
      // 匹配 PUT /api/v1/posts
    });
  });
  Flight::group('/v2', function () {

    // Flight::get() 获取变量,不设置路由!请参见下面的对象上下文
    Flight::route('GET /users', function () {
      // 匹配 GET /api/v2/users
    });
  });
});

使用对象上下文的分组

您仍然可以与 Engine 对象一起使用路由分组,如下所示:

$app = new \flight\Engine();
$app->group('/api/v1', function (Router $router) {

  // 使用 $router 变量
  $router->get('/users', function () {
    // 匹配 GET /api/v1/users
  });

  $router->post('/posts', function () {
    // 匹配 POST /api/v1/posts
  });
});

资源路由

您可以使用 resource 方法为资源创建一组路由。这将创建遵循 RESTful 规范的一组路由。

要创建资源,请执行以下操作:

Flight::resource('/users', UsersController::class);

在后台将发生以下内容:

[
      'index' => 'GET ',
      'create' => 'GET /create',
      'store' => 'POST ',
      'show' => 'GET /@id',
      'edit' => 'GET /@id/edit',
      'update' => 'PUT /@id',
      'destroy' => 'DELETE /@id'
]

而您的控制器将如下所示:

class UsersController
{
    public function index(): void
    {
    }

    public function show(string $id): void
    {
    }

    public function create(): void
    {
    }

    public function store(): void
    {
    }

    public function edit(string $id): void
    {
    }

    public function update(string $id): void
    {
    }

    public function destroy(string $id): void
    {
    }
}

注意:您可以通过运行 php runway routes 来查看新增的路由。

自定义资源路由

有一些选项可以配置资源路由。

别名基础

您可以配置 aliasBase。 默认情况下,别名是指定 URL 的最后部分。 例如 /users/ 将导致别名为 users。 创建这些路由时,别名为 users.indexusers.create 等。如果您想更改别名,请将 aliasBase 设置为您想要的值。

Flight::resource('/users', UsersController::class, [ 'aliasBase' => 'user' ]);

仅和排除

您还可以通过使用 onlyexcept 选项来指定要创建的路由。

Flight::resource('/users', UsersController::class, [ 'only' => [ 'index', 'show' ] ]);
Flight::resource('/users', UsersController::class, [ 'except' => [ 'create', 'store', 'edit', 'update', 'destroy' ] ]);

这些实际上是白名单和黑名单选项,以便您可以指定要创建的路由。

中间件

您还可以指定在 resource 方法创建的每个路由上运行的中间件。

Flight::resource('/users', UsersController::class, [ 'middleware' => [ MyAuthMiddleware::class ] ]);

流式传输

您现在可以使用 streamWithHeaders() 方法将响应流式传输到客户端。 这对于发送大文件、长时间运行的过程或生成大量响应很有用。 流式传输路由的处理方式与常规路由略有不同。

注意: 如果您将 flight.v2.output_buffering 设置为 false,则仅在此情况下可以使用流式响应。

使用手动标头流式传输

您可以通过在路由上使用 stream() 方法将响应流式传输到客户端。如果您这样做,必须在将任何内容输出到客户端之前手动设置所有方法。 这可以通过 header() php 函数或 Flight::response()->setRealHeader() 方法完成。

Flight::route('/@filename', function($filename) {

    // 显然,您需要清理路径等内容。
    $fileNameSafe = basename($filename);

    // 如果您在路由执行后有其他要设置的标头,
    // 您必须在任何内容被输出之前定义它们。
    // 它们必须全部是对 header() 函数的原始调用或
    // 调用 Flight::response()->setRealHeader()
    header('Content-Disposition: attachment; filename="'.$fileNameSafe.'"');
    // 或者
    Flight::response()->setRealHeader('Content-Disposition', 'attachment; filename="'.$fileNameSafe.'"');

    $fileData = file_get_contents('/some/path/to/files/'.$fileNameSafe);

    // 错误捕获等
    if(empty($fileData)) {
        Flight::halt(404, '文件未找到');
    }

    // 如果您喜欢,可以手动设置内容长度
    header('Content-Length: '.filesize($filename));

    // 将数据流式传输到客户端
    echo $fileData;

// 这是这里的魔法行
})->stream();

使用标头流式传输

您还可以使用 streamWithHeaders() 方法在开始流式传输之前设置标头。

Flight::route('/stream-users', function() {

    // 您可以在此处添加任何额外的标头
    // 您必须使用 header() 或 Flight::response()->setRealHeader()

    // 无论您如何提取数据,举个例子...
    $users_stmt = Flight::db()->query("SELECT id, first_name, last_name FROM users");

    echo '{';
    $user_count = count($users);
    while($user = $users_stmt->fetch(PDO::FETCH_ASSOC)) {
        echo json_encode($user);
        if(--$user_count > 0) {
            echo ',';
        }

        // 这是必需的,以将数据发送到客户端
        ob_flush();
    }
    echo '}';

// 在您开始流式传输之前设置标头。
})->streamWithHeaders([
    'Content-Type' => 'application/json',
    'Content-Disposition' => 'attachment; filename="users.json"',
    // 可选状态码,默认为 200
    'status' => 200
]);