在创建完模型之后,我们需要一个控制器来处理我们的文章。在 CakePHP 中,控制器处理 HTTP 请求并执行模型方法中包含的业务逻辑,以准备响应。我们将把这个新的控制器放在名为 ArticlesController.php 的文件中,该文件位于 src/Controller 目录中。以下是基本控制器的示例
<?php
// src/Controller/ArticlesController.php
namespace App\Controller;
class ArticlesController extends AppController
{
}
现在,让我们向我们的控制器添加一个操作。操作是控制器方法,它们与路由相连。例如,当用户请求 www.example.com/articles/index(它与 www.example.com/articles 相同)时,CakePHP 将调用您的 ArticlesController
的 index
方法。此方法应该查询模型层,并通过在视图中渲染一个模板来准备响应。该操作的代码如下所示
<?php
// src/Controller/ArticlesController.php
namespace App\Controller;
class ArticlesController extends AppController
{
public function index()
{
$articles = $this->paginate($this->Articles);
$this->set(compact('articles'));
}
}
通过在我们的 ArticlesController
中定义函数 index()
,用户现在可以通过请求 www.example.com/articles/index 来访问那里的逻辑。类似地,如果我们要定义一个名为 foobar()
的函数,用户将能够在 www.example.com/articles/foobar 访问它。您可能会倾向于以允许您获取特定 URL 的方式命名您的控制器和操作。请抵制这种诱惑。相反,请遵循 CakePHP 约定 来创建可读且有意义的操作名称。然后,您可以使用 路由 将您想要的 URL 连接到您创建的操作。
我们的控制器操作非常简单。它使用通过命名约定自动加载的 Articles 模型从数据库中获取一组分页的文章。然后,它使用 set()
将文章传递到模板(我们将在稍后创建)。CakePHP 将在我们的控制器操作完成后自动渲染模板。
现在我们的控制器从模型中提取数据并准备视图上下文,让我们为我们的索引操作创建一个视图模板。
CakePHP 视图模板是注入到应用程序布局中的带有人性化的 PHP 代码。虽然我们将在此处创建 HTML,但视图也可以生成 JSON、CSV 甚至像 PDF 这样的二进制文件。
布局是包裹在视图周围的人性化代码。布局文件包含常见的网站元素,如页眉、页脚和导航元素。您的应用程序可以有多个布局,您可以在它们之间切换,但现在,让我们只使用默认布局。
CakePHP 的模板文件存储在 templates 中,该文件夹以它们对应的控制器的名称命名。因此,在这种情况下,我们必须创建一个名为“Articles”的文件夹。将以下代码添加到您的应用程序中
<!-- File: templates/Articles/index.php -->
<h1>Articles</h1>
<table>
<tr>
<th>Title</th>
<th>Created</th>
</tr>
<!-- Here is where we iterate through our $articles query object, printing out article info -->
<?php foreach ($articles as $article): ?>
<tr>
<td>
<?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
</td>
<td>
<?= $article->created->format(DATE_RFC850) ?>
</td>
</tr>
<?php endforeach; ?>
</table>
在上一节中,我们使用 set()
将“articles”变量分配给视图。传递到视图中的变量在视图模板中可用作局部变量,我们在上面的代码中使用了这些变量。
您可能已经注意到使用了名为 $this->Html
的对象。这是 CakePHP HtmlHelper 的实例。CakePHP 附带一组视图助手,可以简化创建链接、表单和分页按钮等任务。您可以在其章节中了解更多关于 助手 的信息,但这里需要说明的是,link()
方法将使用给定的链接文本(第一个参数)和 URL(第二个参数)生成一个 HTML 链接。
在 CakePHP 中指定 URL 时,建议使用数组或 命名路由。这些语法允许您利用 CakePHP 提供的反向路由功能。
此时,您应该能够将浏览器指向 https://127.0.0.1:8765/articles/index。您应该会看到您的列表视图,以标题和文章列表的格式正确显示。
如果您点击我们文章列表页面中的一个“查看”链接,您会看到一个错误页面,说明该操作尚未实现。现在让我们来解决这个问题
// Add to existing src/Controller/ArticlesController.php file
public function view($slug = null)
{
$article = $this->Articles->findBySlug($slug)->firstOrFail();
$this->set(compact('article'));
}
虽然这是一个简单的操作,但我们使用了一些强大的 CakePHP 功能。我们首先使用 findBySlug()
开始我们的操作,这是一个 动态查找器。此方法允许我们创建一个基本查询,该查询按给定的 slug 查找文章。然后,我们使用 firstOrFail()
来获取第一条记录,或者抛出一个 \Cake\Datasource\Exception\RecordNotFoundException
。
我们的操作接受一个 $slug
参数,但该参数从哪里来呢?如果用户请求 /articles/view/first-post
,那么值“first-post”将由 CakePHP 的路由和调度层作为 $slug
传递。如果我们使用保存了新操作的浏览器重新加载,我们将看到另一个 CakePHP 错误页面,告诉我们缺少视图模板;让我们来解决这个问题。
让我们为我们的新“查看”操作创建一个视图,并将其放置在 templates/Articles/view.php 中
<!-- File: templates/Articles/view.php -->
<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p><small>Created: <?= $article->created->format(DATE_RFC850) ?></small></p>
<p><?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?></p>
您可以通过尝试 /articles/index
上的链接或手动请求文章(访问 /articles/view/first-post
等 URL)来验证这一点是否有效。
创建了基本阅读视图后,我们需要让用户能够创建新文章。首先在 ArticlesController
中创建一个 add()
操作。我们的控制器现在应该如下所示
<?php
// src/Controller/ArticlesController.php
namespace App\Controller;
use App\Controller\AppController;
class ArticlesController extends AppController
{
public function index()
{
$articles = $this->paginate($this->Articles);
$this->set(compact('articles'));
}
public function view($slug)
{
$article = $this->Articles->findBySlug($slug)->firstOrFail();
$this->set(compact('article'));
}
public function add()
{
$article = $this->Articles->newEmptyEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
// Hardcoding the user_id is temporary, and will be removed later
// when we build authentication out.
$article->user_id = 1;
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to add your article.'));
}
$this->set('article', $article);
}
}
注意
您需要在将使用 Flash 组件的任何控制器中包含它。通常,将其包含在您的 AppController
中是有意义的,在本教程中,它已存在。
以下是 add()
操作执行的操作
如果请求的 HTTP 方法是 POST,请尝试使用 Articles 模型保存数据。
如果由于某种原因它没有保存,只需渲染视图即可。这使我们有机会向用户显示验证错误或其他警告。
每个 CakePHP 请求都包含一个请求对象,可以使用 $this->request
访问它。请求对象包含有关刚刚接收的请求的信息。我们使用 Cake\Http\ServerRequest::is()
方法来检查请求是否是 HTTP POST 请求。
我们的 POST 数据在 $this->request->getData()
中可用。如果您想查看它是什么样子,可以使用 pr()
或 debug()
函数来打印它。要保存我们的数据,我们首先将 POST 数据“编组”到一个 Article 实体中。然后,使用我们之前创建的 ArticlesTable 持久化实体。
保存新文章后,我们使用 FlashComponent 的 success()
方法将消息设置到会话中。 success
方法是使用 PHP 的 魔术方法特性 提供的。闪存消息将在重定向到下一页后显示。在我们的布局中,我们有 <?= $this->Flash->render() ?>
,它显示闪存消息并清除相应的会话变量。最后,保存完成后,我们使用 Cake\Controller\Controller::redirect
将用户送回文章列表。参数 ['action' => 'index']
对应于 URL /articles
,即 ArticlesController
的索引操作。您可以参考 API 上的 Cake\Routing\Router::url()
函数,查看您可以为各种 CakePHP 函数指定 URL 的格式。
这是我们的添加视图模板
<!-- File: templates/Articles/add.php -->
<h1>Add Article</h1>
<?php
echo $this->Form->create($article);
// Hard code the user for now.
echo $this->Form->control('user_id', ['type' => 'hidden', 'value' => 1]);
echo $this->Form->control('title');
echo $this->Form->control('body', ['rows' => '3']);
echo $this->Form->button(__('Save Article'));
echo $this->Form->end();
?>
我们使用 FormHelper 生成 HTML 表单的开始标签。这是 $this->Form->create()
生成的 HTML 代码
<form method="post" action="/articles/add">
因为我们在没有 URL 选项的情况下调用了 create()
,所以 FormHelper
假设我们希望表单提交回当前操作。
$this->Form->control()
方法用于创建相同名称的表单元素。第一个参数告诉 CakePHP 它们对应哪个字段,第二个参数允许您指定各种选项 - 在这种情况下,是 textarea 的行数。这里使用了一些内省和约定。 control()
将根据指定的模型字段输出不同的表单元素,并使用词形变化来生成标签文本。您可以使用选项自定义标签、输入或表单控件的任何其他方面。 $this->Form->end()
调用关闭表单。
现在,让我们回去更新我们的 templates/Articles/index.php 视图,以包含一个新的“添加文章”链接。在 <table>
之前,添加以下行
<?= $this->Html->link('Add Article', ['action' => 'add']) ?>
如果我们现在保存文章,保存将失败,因为我们没有创建 slug 属性,并且该列为 NOT NULL
。Slug 值通常是文章标题的 URL 安全版本。我们可以使用 ORM 的 beforeSave() 回调 来填充我们的 slug
<?php
// in src/Model/Table/ArticlesTable.php
namespace App\Model\Table;
use Cake\ORM\Table;
// the Text class
use Cake\Utility\Text;
// the EventInterface class
use Cake\Event\EventInterface;
// Add the following method.
public function beforeSave(EventInterface $event, $entity, $options)
{
if ($entity->isNew() && !$entity->slug) {
$sluggedTitle = Text::slug($entity->title);
// trim slug to maximum length defined in schema
$entity->slug = substr($sluggedTitle, 0, 191);
}
}
这段代码很简单,没有考虑重复的 slug。但我们稍后会解决这个问题。
我们的应用程序现在可以保存文章,但我们无法编辑它们。让我们现在纠正这一点。将以下操作添加到您的 ArticlesController
中
// in src/Controller/ArticlesController.php
// Add the following method.
public function edit($slug)
{
$article = $this->Articles
->findBySlug($slug)
->firstOrFail();
if ($this->request->is(['post', 'put'])) {
$this->Articles->patchEntity($article, $this->request->getData());
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been updated.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to update your article.'));
}
$this->set('article', $article);
}
此操作首先确保用户尝试访问现有记录。如果他们没有传递 $slug
参数,或者文章不存在,则会抛出 RecordNotFoundException
,CakePHP 错误处理程序将呈现相应的错误页面。
接下来,操作检查请求是否为 POST 或 PUT 请求。如果是,那么我们使用 POST/PUT 数据通过使用 patchEntity()
方法更新我们的文章实体。最后,我们调用 save()
,设置适当的闪存消息,然后重定向或显示验证错误。
编辑模板应如下所示
<!-- File: templates/Articles/edit.php -->
<h1>Edit Article</h1>
<?php
echo $this->Form->create($article);
echo $this->Form->control('user_id', ['type' => 'hidden']);
echo $this->Form->control('title');
echo $this->Form->control('body', ['rows' => '3']);
echo $this->Form->button(__('Save Article'));
echo $this->Form->end();
?>
此模板输出编辑表单(填充了值),以及任何必要的验证错误消息。
您现在可以使用链接更新您的索引视图,以编辑特定文章
<!-- File: templates/Articles/index.php (edit links added) -->
<h1>Articles</h1>
<p><?= $this->Html->link("Add Article", ['action' => 'add']) ?></p>
<table>
<tr>
<th>Title</th>
<th>Created</th>
<th>Action</th>
</tr>
<!-- Here's where we iterate through our $articles query object, printing out article info -->
<?php foreach ($articles as $article): ?>
<tr>
<td>
<?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
</td>
<td>
<?= $article->created->format(DATE_RFC850) ?>
</td>
<td>
<?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
</td>
</tr>
<?php endforeach; ?>
</table>
到目前为止,我们的文章没有进行任何输入验证。让我们使用 验证器 来解决这个问题
// src/Model/Table/ArticlesTable.php
// add this use statement right below the namespace declaration to import
// the Validator class
use Cake\Validation\Validator;
// Add the following method.
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('title')
->minLength('title', 10)
->maxLength('title', 255)
->notEmptyString('body')
->minLength('body', 10);
return $validator;
}
validationDefault()
方法告诉 CakePHP 在调用 save()
方法时如何验证您的数据。在这里,我们指定标题和正文字段不能为空,并且具有某些长度限制。
CakePHP 的验证引擎功能强大且灵活。它为电子邮件地址、IP 地址等任务提供了一套常用的规则,以及添加您自己的验证规则的灵活性。有关该设置的更多信息,请查看 验证 文档。
现在您的验证规则已到位,使用应用程序尝试添加标题或正文为空的文章,看看它是如何工作的。由于我们使用的是 Cake\View\Helper\FormHelper::control()
FormHelper 的方法来创建我们的表单元素,因此我们的验证错误消息将自动显示。
接下来,让我们创建一个用户删除文章的方法。从 ArticlesController
中的 delete()
操作开始
// src/Controller/ArticlesController.php
// Add the following method.
public function delete($slug)
{
$this->request->allowMethod(['post', 'delete']);
$article = $this->Articles->findBySlug($slug)->firstOrFail();
if ($this->Articles->delete($article)) {
$this->Flash->success(__('The {0} article has been deleted.', $article->title));
return $this->redirect(['action' => 'index']);
}
}
此逻辑删除由 $slug
指定的文章,并使用 $this->Flash->success()
在将用户重定向到 /articles
后向用户显示确认消息。如果用户尝试使用 GET 请求删除文章,则 allowMethod()
将抛出异常。未捕获的异常将被 CakePHP 的异常处理程序捕获,并显示一个漂亮的错误页面。有许多内置的 异常 可用于指示您的应用程序可能需要生成的各种 HTTP 错误。
警告
允许使用 GET 请求删除内容非常危险,因为网络爬虫可能会意外删除您所有的内容。这就是我们在控制器中使用 allowMethod()
的原因。
因为我们只执行逻辑并将用户重定向到另一个操作,所以此操作没有模板。您可能希望更新您的索引模板,其中包含允许用户删除文章的链接
<!-- File: templates/Articles/index.php (delete links added) -->
<h1>Articles</h1>
<p><?= $this->Html->link("Add Article", ['action' => 'add']) ?></p>
<table>
<tr>
<th>Title</th>
<th>Created</th>
<th>Action</th>
</tr>
<!-- Here's where we iterate through our $articles query object, printing out article info -->
<?php foreach ($articles as $article): ?>
<tr>
<td>
<?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
</td>
<td>
<?= $article->created->format(DATE_RFC850) ?>
</td>
<td>
<?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
<?= $this->Form->postLink(
'Delete',
['action' => 'delete', $article->slug],
['confirm' => 'Are you sure?'])
?>
</td>
</tr>
<?php endforeach; ?>
</table>
使用 postLink()
将创建一个使用 JavaScript 执行 POST 请求删除我们文章的链接。
注意
此视图代码还使用 FormHelper
在用户尝试删除文章之前,使用 JavaScript 确认对话框提示用户。
小贴士
ArticlesController
也可以使用 bake
构建
/bin/cake bake controller articles
但是,这不会构建 templates/Articles/*.php 文件。
使用基本的文章管理设置,我们将创建 我们 Tags 和 Users 表的基本操作。