控制器

class Cake\Controller\Controller

控制器是 MVC 中的 ‘C’。在路由应用并找到正确的控制器后,您的控制器操作被调用。您的控制器应该处理解释请求数据,确保调用了正确的模型,以及渲染正确的响应或视图。控制器可以被视为模型和视图之间的中间层。您希望控制器很薄,模型很胖。这将有助于您重用代码,并使您的代码更容易测试。

通常,控制器用于管理围绕单个模型的逻辑。例如,如果您正在为在线烘焙店构建网站,您可能有一个 RecipesController 来管理您的食谱,以及一个 IngredientsController 来管理您的食材。但是,控制器也可以与多个模型一起使用。在 CakePHP 中,控制器以其处理的主要模型命名。

您的应用程序的控制器扩展了 AppController 类,该类又扩展了核心 Controller 类。 AppController 类可以在 src/Controller/AppController.php 中定义,它应该包含在您应用程序的所有控制器之间共享的方法。

控制器提供了许多处理请求的方法。这些方法被称为操作。默认情况下,控制器中的每个公共方法都是一个操作,可以通过 URL 访问。操作负责解释请求并创建响应。通常,响应以渲染的视图的形式出现,但也有其他方法可以创建响应。

应用程序控制器

如引言中所述, AppController 类是您应用程序所有控制器的父类。 AppController 本身扩展了 CakePHP 中包含的 Cake\Controller\Controller 类。 AppControllersrc/Controller/AppController.php 中定义,如下所示

namespace App\Controller;

use Cake\Controller\Controller;

class AppController extends Controller
{
}

在您的 AppController 中创建的控制器属性和方法将可用于扩展它的所有控制器。组件(您将在后面学习)最适合用于在许多(但并非全部)控制器中使用的代码。

您可以使用您的 AppController 加载将在您应用程序的每个控制器中使用的组件。CakePHP 提供了一个 initialize() 方法,该方法在控制器构造函数结束时被调用,用于这种用途

namespace App\Controller;

use Cake\Controller\Controller;

class AppController extends Controller
{
    public function initialize(): void
    {
        // Always enable the FormProtection component.
        $this->loadComponent('FormProtection');
    }
}

请求流程

当向 CakePHP 应用程序发出请求时,CakePHP 的 Cake\Routing\RouterCake\Routing\Dispatcher 类使用 连接路由 查找并创建正确的控制器实例。请求数据封装在请求对象中。CakePHP 将所有重要的请求信息放入 $this->request 属性中。有关 CakePHP 请求对象的更多信息,请参阅有关 请求 的部分。

控制器操作

控制器操作负责将请求参数转换为对发出请求的浏览器/用户的响应。CakePHP 使用约定来自动化此过程,并删除您否则需要编写的某些样板代码。

根据约定,CakePHP 使用操作名称的词形变化渲染视图。回到我们的在线烘焙店示例,我们的 RecipesController 可能包含 view()share()search() 操作。控制器将在 src/Controller/RecipesController.php 中找到,并包含以下内容

// src/Controller/RecipesController.php

class RecipesController extends AppController
{
    public function view($id)
    {
        // Action logic goes here.
    }

    public function share($customerId, $recipeId)
    {
        // Action logic goes here.
    }

    public function search($query)
    {
        // Action logic goes here.
    }
}

这些操作的模板文件将是 templates/Recipes/view.phptemplates/Recipes/share.phptemplates/Recipes/search.php。传统的视图文件名是操作名称的小写下划线版本。

控制器操作通常使用 Controller::set() 创建一个上下文, View 使用该上下文来渲染视图层。由于 CakePHP 使用的约定,您无需手动创建和渲染视图。相反,一旦控制器操作完成,CakePHP 将处理渲染和交付视图。

如果您出于某种原因希望跳过默认行为,您可以从操作中返回一个 Cake\Http\Response 对象,其中包含已完全创建的响应。

为了让您在自己的应用程序中有效地使用控制器,我们将介绍 CakePHP 控制器提供的一些核心属性和方法。

与视图交互

控制器通过多种方式与视图交互。首先,它们可以使用 Controller::set() 将数据传递给视图。您还可以决定使用哪个视图类以及从控制器渲染哪个视图文件。

设置视图变量

Cake\Controller\Controller::set(string $var, mixed $value)

Controller::set() 方法是将数据从您的控制器发送到您的视图的主要方式。一旦您使用了 Controller::set(),您就可以在您的视图中访问该变量

// First you pass data from the controller:

$this->set('color', 'pink');

// Then, in the view, you can utilize the data:
?>

You have selected <?= h($color) ?> icing for the cake.

Controller::set() 方法也以关联数组作为其第一个参数。这通常是将一组信息分配给视图的快速方法

$data = [
    'color' => 'pink',
    'type' => 'sugar',
    'base_price' => 23.95,
];

// Make $color, $type, and $base_price
// available to the view:

$this->set($data);

请记住,视图变量在您的视图渲染的所有部分中共享。它们将在视图的所有部分中可用:模板、布局和前两者中的所有元素。

设置视图选项

如果您想自定义视图类、布局/模板路径、助手或渲染视图时使用的主题,可以使用 viewBuilder() 方法获取一个构建器。此构建器可用于在创建视图之前定义视图的属性。

$this->viewBuilder()
    ->addHelper('MyCustom')
    ->setTheme('Modern')
    ->setClassName('Modern.Admin');

上面展示了如何加载自定义助手、设置主题并使用自定义视图类。

渲染视图

Cake\Controller\Controller::render(string $view, string $layout)

Controller::render() 方法会在每个请求的控制器操作结束时自动调用。此方法执行所有视图逻辑(使用您通过 Controller::set() 方法提交的数据),将视图放置在 View::$layout 中,并将其返回给最终用户。

render 使用的默认视图文件由约定决定。如果请求 RecipesController 的 search() 操作,将渲染 **templates/Recipes/search.php** 中的视图文件。

namespace App\Controller;

class RecipesController extends AppController
{
// ...
    public function search()
    {
        // Render the view in templates/Recipes/search.php
        return $this->render();
    }
// ...
}

虽然 CakePHP 会在每个操作的逻辑之后自动调用它(除非您已经调用了 $this->disableAutoRender()),但您可以使用它通过在 Controller::render() 方法的第一个参数中指定一个视图文件名来指定一个替代视图文件。

如果 $view 以 ‘/’ 开头,则认为它是一个相对于 **templates** 文件夹的视图或元素文件。这允许直接渲染元素,这对 AJAX 调用非常有用。

// Render the element in templates/element/ajaxreturn.php
$this->render('/element/ajaxreturn');

Controller::render() 的第二个参数 $layout 允许您指定渲染视图时使用的布局。

渲染特定模板

在您的控制器中,您可能想要渲染一个与常规视图不同的视图。您可以通过直接调用 Controller::render() 来实现。一旦您调用了 Controller::render(),CakePHP 将不会尝试重新渲染视图。

namespace App\Controller;

class PostsController extends AppController
{
    public function my_action()
    {
        $this->render('custom_file');
    }
}

这将渲染 **templates/Posts/custom_file.php** 而不是 **templates/Posts/my_action.php**。

您还可以使用以下语法渲染插件中的视图:$this->render('PluginName.PluginController/custom_file')。例如

namespace App\Controller;

class PostsController extends AppController
{
    public function myAction()
    {
        $this->render('Users.UserDetails/custom_file');
    }
}

这将渲染 **plugins/Users/templates/UserDetails/custom_file.php**

内容类型协商

Cake\Controller\Controller::addViewClasses()

控制器可以定义它们支持的视图类列表。在控制器操作完成后,CakePHP 将使用视图列表使用 路由文件扩展名Accept 标头执行内容类型协商。这使得您的应用程序能够重新使用相同的控制器操作来渲染 HTML 视图或渲染 JSON 或 XML 响应。要定义控制器支持的视图类列表,可以使用 addViewClasses() 方法。

namespace App\Controller;

use Cake\View\JsonView;
use Cake\View\XmlView;

class PostsController extends AppController
{
    public function initialize(): void
    {
        parent::initialize();

        $this->addViewClasses([JsonView::class, XmlView::class]);
    }
}

应用程序的 View 类会在无法根据请求的 Accept 标头或路由扩展名选择其他视图时自动用作后备。如果您的应用程序只支持特定操作的内容类型,您也可以在您的操作中调用 addClasses()

public function export(): void
{
    // Use a custom CSV view for data exports.
    $this->addViewClasses([CsvView::class]);

    // Rest of the action code
}

如果在您的控制器操作中,您需要根据内容类型以不同的方式处理请求或加载数据,您可以使用 检查请求条件

// In a controller action

// Load additional data when preparing JSON responses
if ($this->request->is('json')) {
    $query->contain('Authors');
}

如果您的应用程序需要更复杂的逻辑来决定使用哪些视图类,那么您可以覆盖 Controller::viewClasses() 方法并返回一个所需的视图类数组。

注意

视图类必须实现静态 contentType() 钩子方法才能参与内容类型协商。

内容类型协商后备

如果无法将任何视图与请求的内容类型首选项匹配,CakePHP 将使用基本 View 类。如果您想要求内容类型协商,您可以使用 NegotiationRequiredView,它会设置 406 状态代码。

public function initialize(): void
{
    parent::initialize();

    // Require Accept header negotiation or return a 406 response.
    $this->addViewClasses([JsonView::class, NegotiationRequiredView::class]);
}

您可以使用 TYPE_MATCH_ALL 内容类型值来构建您自己的后备视图逻辑。

namespace App\View;

use Cake\View\View;

class CustomFallbackView extends View
{
    public static function contentType(): string
    {
        return static::TYPE_MATCH_ALL;
    }

}

重要的是要记住,匹配所有视图仅在尝试内容类型协商 *之后* 应用。

使用 AjaxView

在使用超媒体或 AJAX 客户端的应用程序中,您通常需要在没有包装布局的情况下渲染视图内容。您可以使用捆绑在应用程序骨架中的 AjaxView

// In a controller action, or in beforeRender.
if ($this->request->is('ajax')) {
    $this->viewBuilder()->setClassName('Ajax');
}

AjaxView 将响应为 text/html 并使用 ajax 布局。通常,此布局是最小的,或者包含特定于客户端的标记。这使用 4.x 中的 AjaxView 自动替换使用 RequestHandlerComponent

重定向到其他页面

Cake\Controller\Controller::redirect(string|array $url, integer $status)

redirect() 方法添加一个 Location 标头并设置响应的状态代码,然后返回它。您应该返回由 redirect() 创建的响应,以使 CakePHP 发送重定向,而不是完成控制器操作并渲染视图。

您可以使用 路由数组 值进行重定向

return $this->redirect([
    'controller' => 'Orders',
    'action' => 'confirm',
    $order->id,
    '?' => [
        'product' => 'pizza',
        'quantity' => 5
    ],
    '#' => 'top'
]);

或者使用相对或绝对 URL

return $this->redirect('/orders/confirm');

return $this->redirect('http://www.example.com');

或者重定向到引用页

return $this->redirect($this->referer());

通过使用第二个参数,您可以为重定向定义一个状态代码。

// Do a 301 (moved permanently)
return $this->redirect('/order/confirm', 301);

// Do a 303 (see other)
return $this->redirect('/order/confirm', 303);

有关如何在生命周期处理程序中进行重定向,请参阅 在组件事件中使用重定向 部分。

加载其他表/模型

Cake\Controller\Controller::fetchTable(string $alias, array $config = [])

当您需要使用不是控制器默认表的 ORM 表时,fetchTable() 方法非常方便。

// In a controller method.
$recentArticles = $this->fetchTable('Articles')->find('all',
        limit: 5,
        order: 'Articles.created DESC'
    )
    ->all();
Cake\Controller\Controller::fetchModel(string|null $modelClass = null, string|null $modelType = null)

fetchModel() 方法用于加载非 ORM 模型或不是控制器默认表的 ORM 表。

// ModelAwareTrait need to be explicity added to your controler first for fetchModel() to work.
use ModelAwareTrait;

// Get an ElasticSearch model
$articles = $this->fetchModel('Articles', 'Elastic');

// Get a webservices model
$github = $this->fetchModel('GitHub', 'Webservice');

// If you skip the 2nd argument it will by default try to load a ORM table.
$authors = $this->fetchModel('Authors');

在版本 4.5.0 中添加。

对模型进行分页

Cake\Controller\Controller::paginate()

此方法用于对您的模型获取的结果进行分页。您可以指定页面大小、模型查找条件等。有关如何使用 paginate() 的更多详细信息,请参阅 分页 部分。

$paginate 属性为您提供了一种自定义 paginate() 行为的方式。

class ArticlesController extends AppController
{
    protected array $paginate = [
        'Articles' => [
            'conditions' => ['published' => 1],
        ],
    ];
}

配置要加载的组件

Cake\Controller\Controller::loadComponent($name, $config = [])

在您的控制器的 initialize() 方法中,您可以定义要加载的任何组件,以及它们的任何配置数据。

public function initialize(): void
{
    parent::initialize();
    $this->loadComponent('Flash');
    $this->loadComponent('Comments', Configure::read('Comments'));
}

请求生命周期回调

CakePHP 控制器触发了几个事件/回调,您可以使用它们在请求生命周期周围插入逻辑。

事件列表

  • Controller.initialize

  • Controller.startup

  • Controller.beforeRedirect

  • Controller.beforeRender

  • Controller.shutdown

控制器回调方法

默认情况下,如果您的控制器实现了以下回调方法,它们将连接到相关的事件。

Cake\Controller\Controller::beforeFilter(EventInterface $event)

Controller.initialize 事件期间调用,该事件在控制器的每个操作之前发生。它是一个检查活动会话或检查用户权限的好地方。

注意

beforeFilter() 方法将为缺失的操作调用。

beforeFilter 方法返回响应不会阻止相同事件的其他侦听器被调用。您必须显式地 停止事件

Cake\Controller\Controller::beforeRender(EventInterface $event)

Controller.beforeRender 事件发生后,在控制器动作逻辑执行后,但视图渲染之前调用。此回调不常使用,但如果您在给定动作结束之前手动调用 Cake\Controller\Controller::render(),则可能需要它。

Cake\Controller\Controller::afterFilter(EventInterface $event)

Controller.shutdown 事件发生后调用,该事件在每个控制器动作执行后以及渲染完成后触发。这是最后一个运行的控制器方法。

除了控制器生命周期回调,组件 还提供一组类似的回调。

为了获得最佳效果,请记住在子控制器回调中调用 AppController 的回调。

//use Cake\Event\EventInterface;
public function beforeFilter(EventInterface $event)
{
    parent::beforeFilter($event);
}

控制器中间件

Cake\Controller\Controller::middleware($middleware, array $options = [])

中间件 可以全局定义,在路由范围内定义,也可以在控制器中定义。要为特定控制器定义中间件,请使用控制器 initialize() 方法中的 middleware() 方法。

public function initialize(): void
{
    parent::initialize();

    $this->middleware(function ($request, $handler) {
        // Do middleware logic.

        // Make sure you return a response or call handle()
        return $handler->handle($request);
    });
}

控制器定义的中间件将在 beforeFilter() 和动作方法调用之前被调用。

关于控制器的更多信息