# 控制器

控制器是应用程序的核心，它们决定了如何处理 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
    }

备注

有许多 基于时间的常量 始终可供使用，包括 `YEAR`、`MONTH` 等。

## 验证数据

### $this->validateData()

Added in version 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();

            // ...
        }
    }

备注

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

## 保护方法

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

例如，如果为 `Helloworld` 控制器定义这样的方法：

    <?php

    namespace App\Controllers;

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

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

    example.com/index.php/helloworld/utility

自动路由也不起作用。

## 自动路由（改进版）

Added in version 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 为加载组件和执行所有控制器都需要的功能提供了一个方便的地方。可以在任何新控制器中继承此类。

出于安全原因，请确保将任何新的实用方法声明为 `protected` 或 `private`：

    <?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/Controllers 和 public/ 中使用相同名称的目录。这是因为如果存在目录，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();
        }
    }

## 扩展控制器

如果要扩展控制器，请参阅 扩展控制器。

## 就是这样！

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