控制器

控制器是你应用程序的核心,因为它们确定了如何处理 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 的控制器提供了这些属性。

请求对象

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

响应对象

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

日志对象

日志 类的一个实例作为类属性 $this->logger 可用。

辅助函数

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

<?php

namespace App\Controllers;

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

forceHTTPS

所有控制器中都可以使用强制通过 HTTPS 访问方法的便利方法:

<?php

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

默认情况下,在支持 HTTP 严格传输安全头的现代浏览器中,此调用应强制浏览器将非 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()

警告

与其使用 validate(),不如使用 validateData() 仅验证 POST 数据。 validate() 使用 $request->getVar(),它返回 $_GET$_POST$_COOKIE 数据,按照该顺序(取决于 php.ini 的 request-order)。新的值会覆写旧的值。如果它们的名字相同,POST 值可能被 Cookie 覆写。

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

在内部,这使用控制器的 $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() 的数据,而攻击者可能会更改要验证的数据。

备注

自 v4.4.0 版本开始,可以使用 $this->validator->getValidated() 方法。

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

<?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 版本加入.

自 v4.2.0 起,引入了新的更安全的自动路由。

备注

如果你熟悉自动路由,它在 CodeIgniter 3 到 4.1.x 中默认启用,你可以在 ChangeLog v4.2.0 中看到差异。

本节描述了新自动路由的功能。 它会自动路由 HTTP 请求,并执行相应的控制器方法, 而无需路由定义。

自 v4.2.0 起,自动路由默认被禁用。要使用它,请参阅 启用自动路由

考虑这个 URI:

example.com/index.php/helloworld/

在上面的例子中,启用自动路由后,CodeIgniter 会尝试查找名为 App\Controllers\Helloworld 的控制器并加载它。

备注

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

让我们试一试: Hello World!

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

BaseController 为加载组件和执行所有控制器需要的函数提供了方便的位置。你可以在任何新控制器中扩展此类。

<?php

namespace App\Controllers;

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

然后将该文件保存到你的 app/Controllers 目录中。

重要

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

重要

通过自动路由(改进版)执行的控制器方法需要 HTTP 动词(getpostput 等)前缀,如 getIndex()postCreate()

现在使用类似以下的 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');

方法

方法可见性

当你定义通过 HTTP 请求可执行的方法时,该方法必须声明为 public

警告

为了安全起见,请确保将任何新实用程序方法声明为 protectedprivate

默认方法

在上面的示例中,方法名称是 getIndex()。 方法(HTTP 动词 + Index())称为 默认方法,如果 URI 的 第二段 为空,则加载它。

普通方法

URI 的第二段通常确定控制器中的哪个方法被调用。

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

<?php

namespace App\Controllers;

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

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

现在加载以下 URL 以查看 getComment() 方法:

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 getShoes($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'); 这一行。因为定义的路由优先于自动路由,并且出于安全考虑,自动路由(改进版)拒绝定义路由中的控制器访问。

有关更多信息,请参阅 配置选项 文档。

默认方法回退

在 4.4.0 版本加入.

如果与方法名的 URI 段对应的控制器方法不存在,并且如果定义了默认方法,则剩余的 URI 段将传递给默认方法进行执行。

<?php

namespace App\Controllers;

class Product extends BaseController
{
    public function getIndex($id = null, $action = '')
    {
        // ...
    }
}

加载以下 URL:

example.com/index.php/product/15/edit

该方法将传递 URI 段 2 和 3('15''edit'):

重要

如果 URI 中的参数多于方法参数,自动路由(改进版)不会执行该方法,并返回 404 Not Found。

回退到默认控制器

如果与控制器名的 URI 段对应的控制器不存在,并且如果默认控制器(默认为 Home)存在于目录中,则剩余的 URI 段将传递给默认控制器的默认方法。

例如,当你在 app/Controllers/News 目录中有以下默认控制器 Home 时:

<?php

namespace App\Controllers\News;

use App\Controllers\BaseController;

class Home extends BaseController
{
    public function getIndex($id = null)
    {
        // ...
    }
}

加载以下 URL:

example.com/index.php/news/101

将找到 News\Home 控制器和默认的 getIndex() 方法。因此,默认方法将传递 URI 段 2('101'):

备注

如果存在 App\Controllers\News 控制器,则它具有优先权。URI 段按顺序搜索,找到的第一个控制器将被使用。

备注

如果 URI 中的参数多于方法参数,自动路由(改进版)不会执行该方法,并返回 404 Not Found。

将控制器组织到子目录中

如果你正在构建一个大型应用程序,你可能希望以分层的方式组织或结构化控制器到子目录中。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。

自动路由(传统)

重要

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

本节描述自动路由(传统)的功能,这是 CodeIgniter 3 的路由系统。 它会自动路由 HTTP 请求,并执行相应的控制器方法, 而无需路由定义。自动路由默认被禁用。

警告

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

重要

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

考虑这个 URI:

example.com/index.php/helloworld/

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

备注

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

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

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

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()index() 方法如果 URI 的 第二段 为空,总是被默认加载。另一种显示“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();
    }
}

扩展控制器

如果你想扩展控制器,请查看 扩展控制器

就这样了!

这基本上就是关于控制器需要了解的所有内容。