4. 控制器开发基础

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 控制器内置了事件系统,允许在请求处理的不同阶段执行代码。这是一个强大的特性,可以用来实现横切关注点,如权限检查、日志记录等。

最常用的事件是 beforeExecuteRouteafterExecuteRoute

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 还提供了其他事件,如 initializeonConstruct。需要注意它们的执行顺序:

  1. onConstruct - 控制器实例化时调用
  2. initialize - 在 beforeExecuteRoute 之后,动作执行前调用

onConstruct 适合做一些基础初始化工作,而 initialize 可以访问到调度器等服务,适合更复杂的初始化。

我建议将通用的前置检查放在 beforeExecuteRoute 中,这样可以确保所有动作都经过检查。对于需要在多个控制器中共享的逻辑,可以考虑使用事件管理器或行为(Behaviors)。

控制器初始化

Phalcon 提供了两种初始化控制器的方法:initializeonConstruct。它们有不同的用途和执行时机,理解这一点对正确使用控制器至关重要。

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 应用。记住,好的控制器应该保持精简,只负责协调和委派,将复杂的业务逻辑交给模型和服务处理。

下一章将介绍视图与模板引擎,学习如何将控制器处理后的数据以优雅的方式呈现给用户。