3. 路由配置与 URL 管理

3.路由配置与 URL 管理

基本路由规则

在 Phalcon 应用中,路由系统扮演着交通指挥官的角色,它决定了每个请求应该由哪个控制器和动作来处理。Phalcon 的路由组件不仅功能强大,而且使用起来相当直观。让我们从最基础的路由定义开始说起。

路由基础

Phalcon 的路由系统默认工作在 MVC 模式下,这意味着它会将请求映射到控制器和动作上。最简单的路由定义如下所示:

<?php

use Phalcon\Mvc\Router;

$router = new Router();

$router->add(
    '/admin/invoices/list',
    [
        'controller' => 'invoices',
        'action'     => 'list',
    ]
);

$router->handle($_SERVER["REQUEST_URI"]);

这段代码创建了一个基本路由,当用户访问 /admin/invoices/list 时,系统会调用 InvoicesControllerlistAction 方法。需要注意的是,路由本身并不执行控制器和动作,它只是收集这些信息并传递给调度器(Dispatcher)去执行。

路由占位符

手动为每个 URL 定义路由显然不是高效的做法。Phalcon 提供了占位符功能,可以让一条路由匹配多种 URL 模式。常用的占位符有:

占位符 正则表达式 匹配内容
/:module /([a-zA-Z0-9\_\-]+) 模块名,仅包含字母、数字、下划线和连字符
/:controller /([a-zA-Z0-9\_\-]+) 控制器名,格式限制同上
/:action /([a-zA-Z0-9_-]+) 动作名,格式限制同上
/:params (/.*)* 可选参数列表,只能用在路由末尾
/:int /([0-9]+) 整数参数

使用占位符可以大大简化路由定义。例如,下面这条路由可以匹配所有控制器和动作:

<?php

$router->add(
    '/admin/:controller/:action/:params',
    [
        'controller' => 1,
        'action'     => 2,
        'params'     => 3,
    ]
);

这里的数字 1、2、3 对应着占位符在 URL 中的位置。当用户访问 /admin/customers/view/12345/1 时,Phalcon 会将其解析为:

  • 控制器:customers
  • 动作:view
  • 参数:12345 和 1

HTTP 方法限制

在 RESTful API 开发中,我们经常需要根据 HTTP 方法来区分不同的操作。Phalcon 路由系统支持为路由指定 HTTP 方法约束:

<?php

// 仅匹配GET请求
$router->addGet(
    '/invoices/edit/{id}',
    'Invoices::edit'
);

// 仅匹配POST请求
$router->addPost(
    '/invoices/save',
    'Invoices::save'
);

// 匹配POST和PUT请求
$router->add(
    '/invoices/update',
    'Invoices::update'
)->via(['POST', 'PUT']);

这种方式可以让我们的路由设计更加符合 RESTful 架构风格,同一个 URL 可以根据不同的 HTTP 方法执行不同的操作。

路由参数与转换器

在实际开发中,我们经常需要从 URL 中提取参数,并可能需要对这些参数进行处理后再传递给控制器。Phalcon 提供了灵活的参数处理机制来满足这些需求。

命名参数

Phalcon 允许我们为路由参数命名,这样在控制器中可以更直观地获取这些参数:

<?php

$router->add(
    '/admin/{year:[0-9]{4}}/{month:[0-9]{2}}/{day:[0-9]{2}}/{invoiceNo:[0-9]+}',
    [
        'controller' => 'invoices',
        'action'     => 'view',
    ]
);

在控制器中,我们可以通过调度器获取这些命名参数:

<?php

use Phalcon\Mvc\Controller;
use Phalcon\Mvc\Dispatcher;

/**
 * @property Dispatcher $dispatcher
 */
class InvoicesController extends Controller
{
    public function viewAction()
    {
        $year = $this->dispatcher->getParam('year');
        $month = $this->dispatcher->getParam('month');
        $day = $this->dispatcher->getParam('day');
        $invoiceNo = $this->dispatcher->getParam('invoiceNo');

        // 使用这些参数查询数据...
    }
}

这种方式不仅让代码更易读,也使得参数的获取更加灵活。

参数转换器

有时候,我们需要对 URL 中的参数进行一些转换后再使用。Phalcon 的路由系统提供了参数转换器功能,可以在路由匹配阶段就对参数进行处理:

<?php

$route = $router->add(
    '/products/{slug:[a-z\-]+}',
    [
        'controller' => 'products',
        'action'     => 'show',
    ]
);

$route->convert(
    'slug',
    function ($slug) {
        // 将URL中的连字符替换掉,得到产品ID
        return str_replace('-', '', $slug);
    }
);

在这个例子中,当用户访问 /products/new-ipod-nano 时,路由系统会自动将 slug 参数转换为 newipodnano,然后传递给控制器。

参数转换器还可以用于更复杂的场景,比如直接从数据库中加载模型:

<?php

$route = $router->add(
    '/products/{id}',
    [
        'controller' => 'products',
        'action'     => 'show',
    ]
);

$route->convert(
    'id',
    function ($id) {
        // 直接加载产品模型
        return Product::findFirstById($id);
    }
);

这样,在控制器的动作方法中,我们直接就能得到一个 Product 模型实例,而不需要再手动查询数据库。

命名路由与反向生成

随着应用规模的增长,路由定义会越来越多,直接在模板中硬编码 URL 会变得难以维护。Phalcon 提供了命名路由和 URL 反向生成功能,可以有效解决这个问题。

命名路由

我们可以为每条路由指定一个唯一的名称,以便后续引用:

<?php

$route = $router->add(
    '/admin/{year:[0-9]{4}}/{month:[0-9]{2}}/{day:[0-9]{2}}/{id:[0-9]+}',
    [
        'controller' => 'invoices',
        'action'     => 'view',
    ]
);

// 为路由命名
$route->setName('invoices-view');

URL 反向生成

有了命名路由后,我们就可以使用 Phalcon 的 Url 组件来生成 URL,而不是硬编码:

<?php

use Phalcon\Mvc\Url;

$url = new Url();

// 生成URL
$invoiceUrl = $url->get([
    'for'   => 'invoices-view',
    'year'  => '2023',
    'month' => '10',
    'day'   => '15',
    'id'    => '12345',
]);

// 输出: /admin/2023/10/15/12345
echo $invoiceUrl;

这种方式的好处是,当我们需要修改 URL 结构时,只需要更新路由定义,而不需要在整个应用中查找和替换所有硬编码的 URL。

在 Volt 模板中,我们可以使用 url 函数更方便地生成 URL:

{{ url(['for': 'invoices-view', 'year': '2023', 'month': '10', 'day': '15', 'id': '12345']) }}

路由组与模块化路由

对于大型应用,我们可能需要将路由按照模块或功能进行分组管理。Phalcon 的路由组功能可以帮助我们更好地组织路由定义。

创建路由组

使用 Phalcon\Mvc\Router\Group 类可以创建路由组:

<?php

use Phalcon\Mvc\Router;
use Phalcon\Mvc\Router\Group;

$router = new Router();

// 创建一个路由组
$invoices = new RouterGroup([
    'module'     => 'admin',
    'controller' => 'invoices',
]);

// 设置路由组前缀
$invoices->setPrefix('/invoices');

// 添加路由到组
$invoices->add('/list', ['action' => 'list']);
$invoices->add('/edit/{id}', ['action' => 'edit']);
$invoices->add('/view/{id}', ['action' => 'view']);
$invoices->add('/delete/{id}', ['action' => 'delete']);

// 将路由组挂载到路由器
$router->mount($invoices);

这样定义的路由会自动带上 /invoices 前缀。例如,/list 实际上会匹配 /invoices/list 这个 URL。

模块化路由

在多模块应用中,我们可以为每个模块创建独立的路由组类,以实现路由的模块化管理:

<?php

use Phalcon\Mvc\Router\Group;

class InvoicesRoutes extends Group
{
    public function initialize()
    {
        // 设置默认模块和命名空间
        $this->setPaths([
            'module'    => 'invoices',
            'namespace' => 'Invoices\Controllers',
        ]);

        // 设置路由前缀
        $this->setPrefix('/invoices');

        // 添加路由
        $this->add('/', ['action' => 'index']);
        $this->add('/list', ['action' => 'list']);
        $this->add('/view/{id}', ['action' => 'view']);
        // ...更多路由
    }
}

然后在应用初始化时挂载这些路由组:

<?php

$router->mount(new InvoicesRoutes());
$router->mount(new ProductsRoutes());
$router->mount(new UsersRoutes());

这种方式可以让每个模块的路由定义更加清晰,也方便多人协作开发。

路由组的域名约束

Phalcon 还支持为路由组设置域名约束,使得某些路由只能在特定域名下访问:

<?php

$adminRoutes = new RouterGroup([
    'module' => 'admin',
]);

// 设置只能通过admin.example.com访问
$adminRoutes->setHostName('admin.example.com');
$adminRoutes->setPrefix('/');

// 添加管理员路由...

$router->mount($adminRoutes);

这在需要将管理后台和前台用户系统分离到不同域名的场景下非常有用。

404 页面配置

当用户访问一个不存在的路由时,我们需要友好地提示用户。Phalcon 提供了 notFound 方法来处理这种情况:

<?php

// 禁用默认路由
$router = new Router(false);

// 定义应用路由...

// 配置404页面
$router->notFound([
    'controller' => 'index',
    'action'     => 'show404',
]);

然后在 IndexController 中实现 show404Action 方法:

<?php

class IndexController extends Controller
{
    public function show404Action()
    {
        // 设置HTTP状态码为404
        $this->response->setStatusCode(404, 'Not Found');

        // 渲染404页面模板
        $this->view->pick('errors/404');
    }
}

处理额外的斜杠

有时候用户可能会在 URL 末尾添加额外的斜杠,比如访问 /invoices/list/ 而不是 /invoices/list。为了避免这种情况导致 404 错误,我们可以启用自动移除额外斜杠的功能:

<?php

$router = new Router();

// 自动移除URL末尾的额外斜杠
$router->removeExtraSlashes(true);

这样,/invoices/list//invoices/list 会被视为同一个路由。

路由优先级

Phalcon 路由系统按照路由定义的顺序来匹配请求,后定义的路由优先级更高。因此,我们应该将更具体的路由定义放在前面,而将更通用的路由定义放在后面:

<?php

// 具体路由应该先定义
$router->add('/invoices/special', 'Invoices::special');

// 通用路由后定义
$router->add('/invoices/:action', 'Invoices::{1}');

如果顺序颠倒,那么 /invoices/special 会被第二条路由匹配为 Invoices::special,而不是第一条路由。虽然结果相同,但这会增加不必要的匹配过程。

高级路由技巧

路由回调

Phalcon 允许我们为路由添加回调函数,在路由匹配前进行一些自定义检查:

<?php

$route = $router->add(
    '/admin/login',
    [
        'module'     => 'admin',
        'controller' => 'session',
        'action'     => 'login',
    ]
);

// 添加匹配前检查
$route->beforeMatch(function ($uri, $route) {
    $request = $this->getShared('request');

    // 只允许AJAX请求
    return $request->isAjax();
});

这个例子中,只有 AJAX 请求才能访问 /admin/login 路由。我们还可以使用这种方式实现更复杂的路由控制逻辑,比如基于用户角色的路由访问控制。

路由事件

Phalcon 路由系统提供了多个事件,我们可以通过事件管理器来监听这些事件,实现更灵活的路由控制:

<?php

use Phalcon\Events\Event;
use Phalcon\Events\Manager;

$eventsManager = new Manager();

$eventsManager->attach('router', function (Event $event, $router) {
    if ($event->getType() === 'beforeCheckRoutes') {
        // 在检查所有路由前执行
        $uri = $router->getRewriteUri();
        // 记录访问日志等操作...
    }

    if ($event->getType() === 'matchedRoute') {
        // 当路由匹配成功后执行
        $route = $router->getMatchedRoute();
        // 路由统计等操作...
    }
});

// 将事件管理器附加到路由器
$router->setEventsManager($eventsManager);

路由系统支持的事件包括:

  • afterCheckRoutes: 检查所有路由后触发
  • beforeCheckRoute: 检查每个路由前触发
  • beforeCheckRoutes: 检查所有路由前触发
  • beforeMount: 挂载路由组前触发
  • matchedRoute: 路由匹配成功后触发

测试路由

为了确保路由定义的正确性,我们可以编写简单的测试代码来验证路由是否按预期工作:

<?php

use Phalcon\Mvc\Router;

$testRoutes = [
    '/',
    '/invoices',
    '/invoices/list',
    '/invoices/view/123',
    '/products/edit/456',
];

$router = new Router();
// 添加应用路由...

foreach ($testRoutes as $testRoute) {
    $router->handle($testRoute);

    echo 'Testing: ', $testRoute, PHP_EOL;

    if ($router->wasMatched()) {
        echo 'Controller: ', $router->getControllerName(), PHP_EOL;
        echo 'Action: ', $router->getActionName(), PHP_EOL;
    } else {
        echo 'Route not matched', PHP_EOL;
    }

    echo PHP_EOL;
}

这种测试可以帮助我们在开发早期发现路由定义中的问题,特别是在重构路由时非常有用。

总结

路由系统是 Phalcon 应用的重要组成部分,它负责将用户请求映射到相应的控制器和动作。本章详细介绍了 Phalcon 路由系统的各种功能,包括基本路由规则、参数处理、命名路由、路由组、404 页面配置以及一些高级技巧。

合理的路由设计可以让应用的 URL 结构更加清晰和友好,也能提高代码的可维护性。在实际开发中,我们应该根据应用的规模和需求,选择合适的路由策略,既要考虑功能需求,也要兼顾性能和可维护性。

下一章我们将学习控制器的开发,看看在路由匹配之后,Phalcon 是如何处理请求并生成响应的。