4.控制器开发基础
控制器是 Phalcon 应用的核心组件,负责处理用户请求并协调模型与视图。在 MVC 架构中,控制器扮演着"交通警察"的角色,决定如何响应用户的各种操作。作为日常开发中接触最多的部分,掌握控制器的使用技巧对提高开发效率至关重要。
创建控制器
Phalcon 控制器是一个继承自 Phalcon\Mvc\Controller
的类,类名必须以 Controller
为后缀。这一约定看似简单,却能让框架自动识别并加载控制器,省去手动配置的麻烦。
<?php
use Phalcon\Mvc\Controller;
class InvoicesController extends Controller
{
// 控制器方法
}
上面这段代码定义了一个基本的控制器。类名 InvoicesController
遵循了命名规范,确保框架能够正确识别。实际项目中,我建议将控制器按功能模块组织,比如用户相关的操作放在 UsersController
,订单相关的放在 OrdersController
,这样可以让项目结构更清晰。
控制器文件的存放位置也有讲究。默认情况下,Phalcon 会在 app/controllers
目录下查找控制器文件。如果使用了模块化结构,则每个模块的控制器会放在相应模块的 controllers
目录中。这种约定优于配置的方式,能让团队成员快速定位到相关代码。
动作方法定义
控制器中的公共方法称为"动作",是处理具体请求的地方。与控制器类名类似,动作方法也有命名规范——必须以 Action
为后缀。
public function indexAction()
{
// 处理首页请求
}
public function listAction()
{
// 处理列表请求
}
为什么要加 Action
后缀?这是为了区分普通方法和可通过 URL 访问的动作。框架会自动忽略没有 Action
后缀的方法,避免意外暴露内部实现。
默认情况下,如果 URL 中没有指定动作,框架会调用 indexAction
。这就是为什么几乎每个控制器都会定义这个方法作为默认入口。
需要注意的是,动作方法应该保持简洁。实际开发中,我见过有些开发者把所有逻辑都堆在一个动作方法里,导致代码难以维护。好的做法是将复杂逻辑抽象到模型或服务类中,让控制器专注于请求处理和响应生成。
请求参数获取
Web 应用离不开处理用户输入,Phalcon 提供了多种获取请求参数的方式。最直接的是通过动作方法的参数:
public function showAction($id)
{
// 使用$id参数
$invoice = Invoices::findFirstById($id);
}
这种方式简洁明了,但需要注意参数类型。如果 URL 中传递的参数无法转换为预期类型,会导致错误。因此,我建议对参数进行类型转换和验证:
public function showAction($id)
{
$id = (int)$id;
if ($id <= 0) {
$this->response->redirect('invoices/list');
return false;
}
// 继续处理...
}
另一种方式是通过调度器获取参数:
public function showAction()
{
$id = $this->dispatcher->getParam('id');
// 或者获取所有参数
$params = $this->dispatcher->getParams();
}
这种方式更灵活,特别是当参数数量不固定时。我通常在处理复杂路由时使用这种方法。
对于 POST 数据,可以使用请求对象:
public function saveAction()
{
if ($this->request->isPost()) {
$title = $this->request->getPost('title');
$content = $this->request->getPost('content');
// 保存数据...
}
}
getPost
方法还支持过滤和默认值,这是个很实用的特性:
$page = $this->request->getPost('page', 'int', 1);
上面的代码会获取 POST 参数 page
,将其转换为整数,如果不存在则使用默认值 1。这种方式能有效避免未定义变量错误,让代码更健壮。
响应数据返回
处理完请求后,需要向用户返回响应。最常见的是渲染视图文件,但有时也需要直接返回数据,特别是在开发 API 时。
Phalcon 提供了 response
服务来处理响应:
public function apiAction()
{
$data = [
'status' => 'success',
'data' => [/* ... */]
];
$this->response->setStatusCode(200, 'OK');
$this->response->setJsonContent($data);
$this->response->send();
}
这段代码设置了 HTTP 状态码并返回 JSON 数据。在实际项目中,我通常会在基类控制器中封装一个 jsonResponse
方法,简化 API 开发:
protected function jsonResponse($data, $statusCode = 200)
{
$this->view->disable();
$this->response->setStatusCode($statusCode);
$this->response->setJsonContent($data);
return $this->response;
}
public function apiAction()
{
$data = [/* ... */];
return $this->jsonResponse($data);
}
除了 JSON,还可以返回其他格式的数据,比如 XML 或纯文本。Phalcon 的响应对象提供了灵活的接口来满足各种需求。
有时需要重定向用户到其他页面:
public function createAction()
{
// 处理表单提交...
$this->response->redirect('invoices/list');
return false;
}
注意重定向后要返回 false
,防止继续执行后续代码。
控制器事件
Phalcon 控制器内置了事件系统,允许在请求处理的不同阶段执行代码。这是一个强大的特性,可以用来实现横切关注点,如权限检查、日志记录等。
最常用的事件是 beforeExecuteRoute
和 afterExecuteRoute
:
public function beforeExecuteRoute($dispatcher)
{
// 在动作执行前调用
$actionName = $dispatcher->getActionName();
if ($actionName === 'delete' && !$this->isAdmin()) {
$this->flash->error('没有删除权限');
$dispatcher->forward([
'controller' => 'invoices',
'action' => 'list'
]);
return false;
}
}
public function afterExecuteRoute($dispatcher)
{
// 在动作执行后调用
$this->logger->log('Action executed: ' . $dispatcher->getActionName());
}
上面的例子展示了如何使用 beforeExecuteRoute
进行权限检查。如果用户没有管理员权限,就重定向到列表页。这种方式比在每个动作中重复检查权限要优雅得多。
除了这两个事件,Phalcon 还提供了其他事件,如 initialize
和 onConstruct
。需要注意它们的执行顺序:
onConstruct
- 控制器实例化时调用initialize
- 在beforeExecuteRoute
之后,动作执行前调用
onConstruct
适合做一些基础初始化工作,而 initialize
可以访问到调度器等服务,适合更复杂的初始化。
我建议将通用的前置检查放在 beforeExecuteRoute
中,这样可以确保所有动作都经过检查。对于需要在多个控制器中共享的逻辑,可以考虑使用事件管理器或行为(Behaviors)。
控制器初始化
Phalcon 提供了两种初始化控制器的方法:initialize
和 onConstruct
。它们有不同的用途和执行时机,理解这一点对正确使用控制器至关重要。
onConstruct
是构造函数之后立即调用的方法:
public function onConstruct()
{
// 这里可以访问控制器属性
$this->view->setTemplateBefore('main');
}
而 initialize
则是在依赖注入完成后调用:
public function initialize()
{
// 这里可以安全地使用依赖服务
$this->tag->setTitle('发票管理');
}
两者的主要区别在于,onConstruct
在依赖注入完成前执行,因此不能保证所有服务都可用。而 initialize
则是在所有依赖都准备好之后才调用,是更安全的初始化位置。
实际开发中,我通常使用 initialize
来设置视图变量、页面标题等,而 onConstruct
则很少用到,除非有特殊需求。
请求转发
有时需要在控制器内部将请求转发到另一个动作或控制器。这不同于重定向,转发是在服务器内部完成的,浏览器不会感知到 URL 的变化。
public function viewAction($id)
{
$invoice = Invoices::findFirstById($id);
if (!$invoice) {
$this->dispatcher->forward([
'controller' => 'errors',
'action' => 'notFound'
]);
return false;
}
// 显示发票...
}
上面的代码检查发票是否存在,如果不存在就转发到错误控制器的 notFound
动作。这种方式可以提供更好的用户体验,而不是直接显示一个生硬的 404 页面。
转发时可以传递参数:
$this->dispatcher->forward([
'controller' => 'invoices',
'action' => 'show',
'params' => [$invoiceId]
]);
需要注意的是,转发后当前动作应该返回 false
,防止继续执行后续代码。另外,过度使用转发可能会使代码流程变得复杂,难以跟踪,因此应谨慎使用。
会话管理
在控制器中访问会话数据非常简单,Phalcon 提供了 persistent
属性来处理会话数据:
public function loginAction()
{
if ($this->request->isPost()) {
$username = $this->request->getPost('username');
$password = $this->request->getPost('password');
$user = Users::findFirstByUsername($username);
if ($user && $this->security->checkHash($password, $user->password)) {
$this->persistent->set('userId', $user->id);
$this->persistent->set('username', $user->username);
$this->response->redirect('dashboard');
}
}
}
persistent
实际上是 Phalcon\Session\Bag
的实例,提供了简单的接口来访问会话数据。除了 set
方法,还可以直接赋值:
$this->persistent->userId = $user->id;
读取会话数据也很简单:
public function dashboardAction()
{
if (!$this->persistent->userId) {
$this->response->redirect('login');
return false;
}
$username = $this->persistent->username;
// 显示仪表板...
}
使用 persistent
的好处是它会自动处理会话的启动和关闭,开发者无需关心底层细节。对于需要跨请求保存的数据,这是一个非常方便的工具。
依赖注入
Phalcon 的控制器默认支持依赖注入,这意味着可以直接访问注册在 DI 容器中的服务:
public function indexAction()
{
// 访问数据库服务
$this->db->query('SELECT * FROM invoices');
// 访问配置服务
$maxItems = $this->config->application->maxItemsPerPage;
// 访问日志服务
$this->logger->info('Invoices index viewed');
}
这种方式非常便捷,但在大型项目中,过度依赖全局服务可能会使代码难以测试。因此,我建议对于复杂的业务逻辑,还是应该使用依赖注入的方式显式传入依赖,而不是直接在控制器中使用 $this->serviceName
。
Phalcon 也支持构造函数注入:
use Phalcon\Db\Adapter\Pdo\Mysql;
use Phalcon\Mvc\Controller;
class InvoicesController extends Controller
{
private $db;
public function __construct(Mysql $db)
{
$this->db = $db;
}
public function indexAction()
{
$this->db->query('SELECT * FROM invoices');
}
}
不过要注意,如果使用构造函数注入,需要确保父类构造函数也被正确调用。在大多数情况下,直接使用属性访问服务已经足够简单实用。
最佳实践
基于一些 Phalcon 项目的开发经验,我总结出一些控制器使用的最佳实践,希望能帮助你写出更优雅、更可维护的代码。
保持控制器精简
控制器应该只负责处理请求、协调模型和视图,不应该包含复杂的业务逻辑。如果发现一个动作方法变得很长,考虑将业务逻辑抽象到模型或服务类中。
// 不推荐
public function processOrderAction()
{
$orderId = $this->request->getPost('orderId');
$order = Orders::findFirstById($orderId);
if (!$order) {
// 处理错误...
}
// 大量订单处理逻辑...
// 发送邮件...
// 更新库存...
// ...
}
// 推荐
public function processOrderAction()
{
$orderId = $this->request->getPost('orderId');
$result = $this->orderService->process($orderId);
if ($result->isSuccess()) {
$this->flash->success('订单处理成功');
} else {
$this->flash->error($result->getMessage());
}
$this->response->redirect('orders/view/' . $orderId);
}
使用基类控制器
创建一个 BaseController
,将所有控制器共用的逻辑放在其中,如权限检查、日志记录等。其他控制器继承这个基类,实现代码复用。
class BaseController extends Controller
{
public function beforeExecuteRoute($dispatcher)
{
// 通用权限检查
if (!$this->auth->check()) {
$dispatcher->forward([
'controller' => 'auth',
'action' => 'login'
]);
return false;
}
}
}
class InvoicesController extends BaseController
{
// 具体控制器逻辑
}
合理使用事件
利用控制器事件分离关注点,将横切关注点如日志、缓存、安全检查等放在事件方法中,保持动作方法的简洁。
验证输入数据
始终验证和过滤用户输入,Phalcon 的请求对象提供了方便的方法来实现这一点。不要相信任何来自客户端的数据。
使用视图模型
对于复杂的视图数据,考虑使用视图模型(ViewModel)模式,将视图需要的数据组织在专门的类中,而不是在控制器中直接组装数据。
避免在控制器中直接操作数据库
控制器应该通过模型或服务来操作数据,而不是直接使用 $this->db
执行 SQL。这样可以更好地实现业务逻辑复用和单元测试。
总结
控制器是 Phalcon 应用的核心,连接用户请求和业务逻辑。本章介绍了控制器的创建、动作方法定义、参数获取、响应处理、事件系统等基础知识,以及一些实用的最佳实践。
掌握控制器的使用技巧,能够帮助我们构建更清晰、更可维护的 Phalcon 应用。记住,好的控制器应该保持精简,只负责协调和委派,将复杂的业务逻辑交给模型和服务处理。
下一章将介绍视图与模板引擎,学习如何将控制器处理后的数据以优雅的方式呈现给用户。