控制器是 MVC 中的 ‘C’。在路由应用并找到正确的控制器后,您的控制器操作被调用。您的控制器应该处理解释请求数据,确保调用了正确的模型,以及渲染正确的响应或视图。控制器可以被视为模型和视图之间的中间层。您希望控制器很薄,模型很胖。这将有助于您重用代码,并使您的代码更容易测试。
通常,控制器用于管理围绕单个模型的逻辑。例如,如果您正在为在线烘焙店构建网站,您可能有一个 RecipesController 来管理您的食谱,以及一个 IngredientsController 来管理您的食材。但是,控制器也可以与多个模型一起使用。在 CakePHP 中,控制器以其处理的主要模型命名。
您的应用程序的控制器扩展了 AppController
类,该类又扩展了核心 Controller
类。 AppController
类可以在 src/Controller/AppController.php 中定义,它应该包含在您应用程序的所有控制器之间共享的方法。
控制器提供了许多处理请求的方法。这些方法被称为操作。默认情况下,控制器中的每个公共方法都是一个操作,可以通过 URL 访问。操作负责解释请求并创建响应。通常,响应以渲染的视图的形式出现,但也有其他方法可以创建响应。
如引言中所述, AppController
类是您应用程序所有控制器的父类。 AppController
本身扩展了 CakePHP 中包含的 Cake\Controller\Controller
类。 AppController
在 src/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\Router
和 Cake\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.php, templates/Recipes/share.php 和 templates/Recipes/search.php。传统的视图文件名是操作名称的小写下划线版本。
控制器操作通常使用 Controller::set()
创建一个上下文, View
使用该上下文来渲染视图层。由于 CakePHP 使用的约定,您无需手动创建和渲染视图。相反,一旦控制器操作完成,CakePHP 将处理渲染和交付视图。
如果您出于某种原因希望跳过默认行为,您可以从操作中返回一个 Cake\Http\Response
对象,其中包含已完全创建的响应。
为了让您在自己的应用程序中有效地使用控制器,我们将介绍 CakePHP 控制器提供的一些核心属性和方法。
控制器通过多种方式与视图交互。首先,它们可以使用 Controller::set()
将数据传递给视图。您还可以决定使用哪个视图类以及从控制器渲染哪个视图文件。
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');
上面展示了如何加载自定义助手、设置主题并使用自定义视图类。
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**
控制器可以定义它们支持的视图类列表。在控制器操作完成后,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;
}
}
重要的是要记住,匹配所有视图仅在尝试内容类型协商 *之后* 应用。
在使用超媒体或 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
。
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);
有关如何在生命周期处理程序中进行重定向,请参阅 在组件事件中使用重定向 部分。
当您需要使用不是控制器默认表的 ORM 表时,fetchTable()
方法非常方便。
// In a controller method.
$recentArticles = $this->fetchTable('Articles')->find('all',
limit: 5,
order: 'Articles.created DESC'
)
->all();
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 中添加。
此方法用于对您的模型获取的结果进行分页。您可以指定页面大小、模型查找条件等。有关如何使用 paginate()
的更多详细信息,请参阅 分页 部分。
$paginate
属性为您提供了一种自定义 paginate()
行为的方式。
class ArticlesController extends AppController
{
protected array $paginate = [
'Articles' => [
'conditions' => ['published' => 1],
],
];
}
在您的控制器的 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
默认情况下,如果您的控制器实现了以下回调方法,它们将连接到相关的事件。
在 Controller.initialize
事件期间调用,该事件在控制器的每个操作之前发生。它是一个检查活动会话或检查用户权限的好地方。
注意
beforeFilter() 方法将为缺失的操作调用。
从 beforeFilter
方法返回响应不会阻止相同事件的其他侦听器被调用。您必须显式地 停止事件。
在 Controller.beforeRender
事件发生后,在控制器动作逻辑执行后,但视图渲染之前调用。此回调不常使用,但如果您在给定动作结束之前手动调用 Cake\Controller\Controller::render()
,则可能需要它。
在 Controller.shutdown
事件发生后调用,该事件在每个控制器动作执行后以及渲染完成后触发。这是最后一个运行的控制器方法。
除了控制器生命周期回调,组件 还提供一组类似的回调。
为了获得最佳效果,请记住在子控制器回调中调用 AppController
的回调。
//use Cake\Event\EventInterface;
public function beforeFilter(EventInterface $event)
{
parent::beforeFilter($event);
}
中间件 可以全局定义,在路由范围内定义,也可以在控制器中定义。要为特定控制器定义中间件,请使用控制器 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()
和动作方法调用之前被调用。