控制器

控制器是应用程序的核心,它们决定了如何处理 HTTP 请求。

什么是控制器?

控制器就是一个处理 HTTP 请求的类文件。URI 路由 将 URI 与控制器关联起来。它返回视图字符串或 Response 对象。

创建的每个控制器都应该继承 BaseController 类。该类提供了几个可供所有控制器使用的功能。

构造函数

CodeIgniter 的控制器有一个特殊的构造函数 initController()。它会在 PHP 的构造函数 __construct() 执行之后被框架调用。

如果要重写 initController(),别忘了在方法中添加 parent::initController($request, $response, $logger);

<?php

namespace App\Controllers;

use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;

class Product extends BaseController
{
    public function initController(
        RequestInterface $request,
        ResponseInterface $response,
        LoggerInterface $logger,
    ) {
        parent::initController($request, $response, $logger);

        // Add your code here.
    }

    // ...
}

重要

不能在构造函数中使用 return。所以 return redirect()->to('route'); 不会起作用。

initController() 方法设置以下三个属性。

包含的属性

CodeIgniter 的控制器提供了这些属性。

Request 对象

应用程序的主 Request 实例 始终作为类属性 $this->request 可用。

Response 对象

应用程序的主 Response 实例 始终作为类属性 $this->response 可用。

Logger 对象

Logger 类的实例作为类属性 $this->logger 可用。

辅助函数

可以将辅助函数文件数组定义为类属性。每当加载控制器时,这些辅助函数文件会自动加载到内存中,以便可以在控制器内的任何地方使用它们的方法:

<?php

namespace App\Controllers;

class MyController extends BaseController
{
    protected $helpers = ['url', 'form'];
}

forceHTTPS

所有控制器中都提供了一个方便的方法,用于强制通过 HTTPS 访问方法:

<?php

if (! $this->request->isSecure()) {
    $this->forceHTTPS();
}

默认情况下,在支持 HTTP 严格传输安全(HSTS)标头的现代浏览器中,此调用应强制浏览器将非 HTTPS 调用转换为 HTTPS 调用,持续一年。可以通过将持续时间(以秒为单位)作为第一个参数传递来修改这一点:

<?php

if (! $this->request->isSecure()) {
    $this->forceHTTPS(31536000); // one year
}

备注

有许多 基于时间的常量 始终可供使用,包括 YEARMONTH 等。

验证数据

$this->validateData()

在 4.2.0 版本加入.

为了简化数据检查,控制器还提供了便捷方法 validateData()

该方法接受 (1) 要验证的数据数组,(2) 规则数组,(3) 如果项目无效时显示的自定义错误消息的可选数组,(4) 要使用的可选数据库组。

验证库文档 详细说明了规则和消息数组格式以及可用的规则:

<?php

namespace App\Controllers;

class StoreController extends BaseController
{
    public function product(int $id)
    {
        $data = [
            'id'   => $id,
            'name' => $this->request->getPost('name'),
        ];

        $rule = [
            'id'   => 'integer',
            'name' => 'required|max_length[255]',
        ];

        if (! $this->validateData($data, $rule)) {
            return view('store/product', [
                'errors' => $this->validator->getErrors(),
            ]);
        }

        // ...
    }
}

$this->validate()

重要

此方法仅为向后兼容而存在。不要在新项目中使用它。即使已经在使用,我们也建议使用 validateData() 方法代替。

控制器还提供了便捷方法 validate()

警告

应使用 validateData() 而不是 validate() 来仅验证 POST 数据。validate() 使用 $request->getVar() 返回 $_GET$_POST$_COOKIE 数据(按该顺序,取决于 php.ini request-order)。较新的值会覆盖较旧的值。如果 Cookie 具有相同的名称,POST 值可能会被覆盖。

该方法在第一个参数中接受规则数组,在可选的第二个参数中接受自定义错误消息数组(如果项目无效时显示)。

在内部,这使用控制器的 $this->request 实例来获取要验证的数据。

验证库文档 详细说明了规则和消息数组格式以及可用的规则:

<?php

namespace App\Controllers;

class UserController extends BaseController
{
    public function updateUser(int $userID)
    {
        if (! $this->validate([
            'email' => "required|is_unique[users.email,id,{$userID}]",
            'name'  => 'required|alpha_numeric_spaces',
        ])) {
            // The validation failed.
            return view('users/update', [
                'errors' => $this->validator->getErrors(),
            ]);
        }

        // The validation was successful.

        // Get the validated data.
        $validData = $this->validator->getValidated();

        // ...
    }
}

警告

当使用 validate() 方法时,应该使用 getValidated() 方法来获取验证后的数据。因为 validate() 方法在内部使用 Validation::withRequest() 方法,它验证来自 $request->getJSON()$request->getRawInput()$request->getVar() 的数据,攻击者可能会更改验证的数据。

备注

$this->validator->getValidated() 方法从 v4.4.0 开始可用。

如果发现将规则保留在配置文件中更简单,可以将 $rules 数组替换为 app/Config/Validation.php 中定义的组名:

<?php

namespace App\Controllers;

class UserController extends BaseController
{
    public function updateUser(int $userID)
    {
        if (! $this->validate('userRules')) {
            // The validation failed.
            return view('users/update', [
                'errors' => $this->validator->getErrors(),
            ]);
        }

        // The validation was successful.

        // Get the validated data.
        $validData = $this->validator->getValidated();

        // ...
    }
}

备注

验证也可以在模型中自动处理,但有时在控制器中更容易。具体位置由你决定。

保护方法

在某些情况下,你可能希望某些方法对公共访问隐藏。要实现这一点,只需将方法声明为 privateprotected。这样可以防止通过 URL 请求访问它。

例如,如果为 Helloworld 控制器定义这样的方法:

<?php

namespace App\Controllers;

class Helloworld extends BaseController
{
    protected function utility()
    {
        // some code
    }
}

并为该方法定义路由(helloworld/utitilty)。那么尝试使用以下 URL 访问它将不起作用:

example.com/index.php/helloworld/utility

自动路由也不起作用。

自动路由(改进版)

在 4.2.0 版本加入.

自动路由(改进版)是一个新的、更安全的自动路由系统。

详见 自动路由(改进版)

自动路由(传统版)

重要

此功能仅为向后兼容而存在。不要在新项目中使用它。即使已经在使用,我们也建议使用 自动路由(改进版) 代替。

本节描述了自动路由(传统版)的功能,这是来自 CodeIgniter 3 的路由系统。它自动路由 HTTP 请求,并在没有路由定义的情况下执行相应的控制器方法。自动路由默认是禁用的。

警告

为了防止错误配置和错误编码,我们建议不要使用自动路由(传统版)。很容易创建易受攻击的应用程序,其中控制器过滤器或 CSRF 保护被绕过。

重要

自动路由(传统版)使用 任何 HTTP 方法将 HTTP 请求路由到控制器方法。

重要

从 v4.5.0 开始,如果自动路由(传统版)找不到控制器,它将在控制器过滤器执行之前抛出 PageNotFoundException 异常。

考虑以下 URI:

example.com/index.php/helloworld/

在上面的例子中,CodeIgniter 将尝试查找名为 Helloworld.php 的控制器并加载它。

备注

当控制器的短名称与 URI 的第一分段匹配时,它将被加载。

让我们试试:Hello World!(传统版)

让我们创建一个简单的控制器,这样你就可以看到它的实际效果。使用文本编辑器,创建一个名为 Helloworld.php 的文件,并将以下代码放入其中。你会注意到 Helloworld 控制器继承了 BaseController。如果不需要 BaseController 的功能,也可以继承 CodeIgniter\Controller

BaseController 为加载组件和执行所有控制器都需要的功能提供了一个方便的地方。可以在任何新控制器中继承此类。

出于安全原因,请确保将任何新的实用方法声明为 protectedprivate

<?php

namespace App\Controllers;

class Helloworld extends BaseController
{
    public function index()
    {
        return 'Hello World!';
    }
}

然后将文件保存到 app/Controllers 目录。

重要

文件必须命名为 Helloworld.php,首字母 H 大写。使用自动路由时,控制器类名必须以大写字母开头,且只有第一个字符可以大写。

现在使用类似以下的 URL 访问你的站点:

example.com/index.php/helloworld

如果操作正确,你应该看到:

Hello World!

这是有效的:

<?php

namespace App\Controllers;

class Helloworld extends BaseController
{
    // ...
}

这是 无效的

<?php

namespace App\Controllers;

class helloworld extends BaseController
{
    // ...
}

这是 无效的

<?php

namespace App\Controllers;

class HelloWorld extends BaseController
{
    // ...
}

此外,始终确保控制器继承父控制器类,以便它可以继承其所有方法。

备注

当未找到已定义的路由匹配项时,系统将通过将每个分段与 app/Controllers 中的目录/文件进行匹配来尝试将 URI 与控制器匹配。这就是为什么目录/文件必须以大写字母开头,其余必须小写。

如果想要另一种命名约定,需要使用 定义路由 手动定义它。以下是基于 PSR-4 自动加载器的示例:

<?php

/*
 * Folder and file structure:
 * \<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>
 */

$routes->get('helloworld', '\App\Controllers\HelloWorld::index');

方法(传统版)

在上面的例子中,方法名是 index()。如果 URI 的 第二分段 为空,则默认情况下总是加载 index() 方法。显示”Hello World”消息的另一种方法是:

example.com/index.php/helloworld/index/

URI 的第二分段决定了调用控制器中的哪个方法。

让我们试试。向控制器添加一个新方法:

<?php

namespace App\Controllers;

class Helloworld extends BaseController
{
    public function index()
    {
        return 'Hello World!';
    }

    public function comment()
    {
        return 'I am not flat!';
    }
}

现在加载以下 URL 以查看 comment 方法:

example.com/index.php/helloworld/comment/

你应该看到新消息。

向方法传递 URI 分段(传统版)

如果 URI 包含两个以上的分段,它们将作为参数传递给方法。

例如,假设有一个这样的 URI:

example.com/index.php/products/shoes/sandals/123

方法将接收 URI 分段 3 和 4('sandals''123'):

<?php

namespace App\Controllers;

class Products extends BaseController
{
    public function shoes($sandals, $id)
    {
        return $sandals . $id;
    }
}

默认控制器(传统版)

默认控制器是一个特殊的控制器,当 URI 以目录名结尾或 URI 不存在时使用,就像仅请求站点根 URL 的情况一样。

定义默认控制器(传统版)

让我们用 Helloworld 控制器试试。

要指定默认控制器,打开 app/Config/Routing.php 文件并设置此属性:

public string $defaultController = 'Helloworld';

其中 Helloworld 是要使用的控制器类的名称。

并在 app/Config/Routes.php 中注释掉该行:

$routes->get('/', 'Home::index');

如果现在浏览站点而不指定任何 URI 分段,将看到”Hello World”消息。

备注

$routes->get('/', 'Home::index'); 这行代码是一个优化,你会希望在“真实世界”的应用程序中使用它。但出于演示目的,我们不想使用该功能。$routes->get()URI 路由 中有说明。

有关更多信息,请参阅 配置选项(传统版) 文档。

将控制器组织到子目录(传统版)

在构建大型应用程序时,你可能希望将控制器按层级结构放入子目录中来管理。CodeIgniter 支持这种做法。

只需在主 app/Controllers 下创建子目录,并将控制器类放入其中。

重要

目录名必须以大写字母开头,且只有第一个字符可以大写。

使用此功能时,URI 的第一分段必须指定目录。例如,假设有一个位于此处的控制器:

app/Controllers/Products/Shoes.php

要调用上述控制器,URI 将如下所示:

example.com/index.php/products/shoes/show/123

备注

不能在 app/Controllerspublic/ 中使用相同名称的目录。这是因为如果存在目录,Web 服务器将搜索它,并且不会路由到 CodeIgniter。

每个子目录都可以包含一个默认控制器。如果 URL 访问的路径 刚好 是这个子目录(即没有指定具体的控制器名),那么这个默认控制器就会被调用。实现方法很简单:你只需在该目录中放置一个控制器,确保它的名称与你在 app/Config/Routing.php 文件中指定的默认控制器名称一致即可。

CodeIgniter 还支持使用 定义路由 映射 URI。

重新映射方法调用

备注

自动路由(改进版) 不支持此功能,这是有意为之。

如上所述,URI 的第二分段通常确定调用控制器中的哪个方法。CodeIgniter 支持通过使用 _remap() 方法来重写此行为:

<?php

namespace App\Controllers;

class Products extends BaseController
{
    public function _remap()
    {
        // Some code here...
    }
}

重要

如果控制器包含名为 _remap() 的方法,无论 URI 包含什么,它都将 始终 被调用。它覆盖了 URI 确定调用哪个方法的正常行为,允许定义自己的方法路由规则。

重写的方法调用(通常是 URI 的第二分段)将作为参数传递给 _remap() 方法:

<?php

namespace App\Controllers;

class Products extends BaseController
{
    public function _remap($method)
    {
        if ($method === 'some_method') {
            return $this->{$method}();
        }

        return $this->default_method();
    }
}

方法名之后的任何额外分段都会传递到 _remap()。这些参数可以传递给方法以模拟 CodeIgniter 的默认行为。

示例:

<?php

namespace App\Controllers;

class Products extends BaseController
{
    public function _remap($method, ...$params)
    {
        $method = 'process_' . $method;

        if (method_exists($this, $method)) {
            return $this->{$method}(...$params);
        }

        throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
    }
}

扩展控制器

如果要扩展控制器,请参阅 扩展控制器

就是这样!

简而言之,这就是关于控制器需要了解的全部内容。