2026 暑訓 / COURSE 02.05
Day 3 - CMS 教學 1
從 MVC 到 CRUD,動手打造你的第一個 CakePHP CMS。
完成 CakePHP 的基本概念與開發環境建置後,接下來正式開始實作一套簡單的 CMS(Content Management System,內容管理系統)。
本章會從文章管理功能開始,逐步建立:
- Articles Model
- Article Entity
- ArticlesController
- 文章列表與詳細頁
- 新增文章
- 編輯文章
- Slug
- Validation
- 刪除文章
藉由完整實作文章 CRUD,實際理解 CakePHP 的 MVC、ORM、FormHelper 與 Validation 如何互相配合。
1. 修改 PHP 時區
在開始 CMS 開發之前,先設定 PHP 的預設時區。
在 Dockerfile 中加入:
RUN echo "date.timezone=Asia/Taipei" > /usr/local/etc/php/conf.d/timezone.ini這樣 PHP 產生日期與時間時,就會使用:
Asia/Taipei也就是台灣時區。
之後文章的 created、modified 等時間欄位,就能配合台灣時間顯示。
2. CMS 與文章管理
這次要建立的是一個簡單的內容管理系統。
我們會使用:
CakePHP 5.x
MySQL
PHP來建立基本文章管理功能。
在開始之前,需要具備:
- 基本 PHP 知識
- 基本 SQL 概念
- MySQL Database
- PHP 的
pdo_mysqlExtension
CakePHP ORM 會替我們處理大部分 SQL Query,因此實際開發時不需要在 Controller 中手寫大量 SQL。
3. 建立 Articles Model
Model 是 CakePHP Application 的核心之一。
它主要負責:
- 讀取資料
- 修改資料
- 驗證資料
- 建立 Association
- 套用 Business Rule
CakePHP Model 主要由:
Table
Entity兩種物件組成。
可以簡單理解成:
ArticlesTable
→ articles 資料表與資料集合
Article
→ articles 資料表中的單筆資料建立 ArticlesTable
建立:
src/Model/Table/ArticlesTable.php內容如下:
<?php
// src/Model/Table/ArticlesTable.php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Table;
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('Timestamp');
}
}這裡加入:
$this->addBehavior('Timestamp');之後 CakePHP 在新增或修改資料時,就可以自動處理:
created
modified欄位。
由於 Class 名稱為:
ArticlesTableCakePHP 也會根據 Convention 自動判斷它對應:
articles資料表,並假設:
id是 Primary Key。
建立 Article Entity
接著建立:
src/Model/Entity/Article.php<?php
// src/Model/Entity/Article.php
declare(strict_types=1);
namespace App\Model\Entity;
use Cake\ORM\Entity;
class Article extends Entity
{
protected array $_accessible = [
'user_id' => true,
'title' => true,
'slug' => true,
'body' => true,
'published' => true,
'created' => true,
'modified' => true,
'user' => true,
'tags' => true,
];
}Entity 代表 Database 中的一筆 Record。
例如:
articles Table中的其中一篇文章,就是一個:
Article Entity而:
$_accessible用來控制哪些欄位允許透過 Mass Assignment(批次賦值) 修改。
Model 也可以透過 Bake 快速建立:
bin/cake bake model articles4. 建立 ArticlesController
建立:
src/Controller/ArticlesController.php<?php
// src/Controller/ArticlesController.php
namespace App\Controller;
class ArticlesController extends AppController
{
}Controller 主要負責:
接收 HTTP Request
↓
呼叫 Model
↓
準備資料
↓
將資料交給 View接下來就從文章列表開始。
建立 index() Action
在 ArticlesController 中加入:
public function index()
{
$articles = $this->paginate($this->Articles);
$this->set(compact('articles'));
}其中:
$this->Articles代表 Articles Model。
$this->paginate($this->Articles)取得分頁後的 Article Collection。
最後:
$this->set(compact('articles'));把 $articles 傳遞給 View。
當使用者存取:
/articles或:
/articles/indexCakePHP 就會執行:
ArticlesController::index()並根據 Convention 尋找:
templates/Articles/index.php5. 建立文章列表 Template
建立:
templates/Articles/index.php<!-- templates/Articles/index.php -->
<h1>文章</h1>
<table>
<tr>
<th>標題</th>
<th>建立時間</th>
</tr>
<?php foreach ($articles as $article): ?>
<tr>
<td>
<?= $this->Html->link(
$article->title,
['action' => 'view', $article->slug]
) ?>
</td>
<td>
<?= $article->created
->i18nFormat('yyyy年MM月dd日 EEEE HH:mm:ss') ?>
</td>
</tr>
<?php endforeach; ?>
</table>這裡使用:
foreach ($articles as $article)逐筆顯示文章。
而:
$this->Html->link()是 CakePHP 的 HtmlHelper,可以協助產生 HTML Link。
使用 i18nFormat() 顯示日期
教材中將日期顯示以 i18nFormat()進行格式化:
$article->created
->i18nFormat('yyyy年MM月dd日 EEEE HH:mm')例如可能顯示:
2026年08月19日 星期三 13:30與單純使用固定英文日期格式相比,這種方式更適合中文網站,也可以配合前面設定好的:
Asia/Taipei時區。
6. 建立 view() Action
點擊文章標題後,需要建立文章詳細頁。
在 ArticlesController 中加入:
public function view(?string $slug = null)
{
$article = $this->Articles
->findBySlug($slug)
->firstOrFail();
$this->set(compact('article'));
}這裡的:
?string $slug = null表示 $slug 可以是:
string或:
null
例如網址:
/articles/view/first-postCakePHP 會將:
first-post傳給:
$slugDynamic Finder
這裡使用:
findBySlug($slug)它是 CakePHP 的 Dynamic Finder。
相當於告訴 CakePHP:
尋找 slug 等於
$slug的 Article。
接著:
firstOrFail()會取得第一筆結果。
如果找不到資料,就會拋出:
RecordNotFoundException由 CakePHP ErrorHandler 處理。
7. 建立文章視圖 Template
建立:
templates/Articles/view.php<!-- templates/Articles/view.php -->
<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p>
<small>
創建於:
<?= $article->created
->i18nFormat('yyyy年MM月dd日 EEEE HH:mm') ?>
</small>
</p>
<p>
<?= $this->Html->link(
'編輯',
['action' => 'edit', $article->slug]
) ?>
</p>其中:
h(...)會對文字進行 HTML Encoding,避免直接把使用者輸入內容當成 HTML 執行。
8. 建立 add() Action
接下來加入:
add()Action。
public function add()
{
$article = $this->Articles->newEmptyEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity(
$article,
$this->request->getData()
);
// 暫時寫死 user_id
$article->user_id = 1;
if ($this->Articles->save($article)) {
$this->Flash->success(
__('您的文章已經儲存。')
);
return $this->redirect([
'action' => 'index'
]);
}
$this->Flash->error(
__('無法新增您的文章。')
);
}
$this->set('article', $article);
}add() 的執行流程
可以整理成:
newEmptyEntity()
↓
確認是否為 POST
↓
getData()
↓
patchEntity()
↓
save()
↓
成功 → Flash + Redirect
失敗 → 顯示錯誤其中:
$this->request->getData()用來取得 Form 提交的 POST Data。
而:
patchEntity()會把 Request Data 套用到 Article Entity。
9. 建立新增文章 Template
建立:
templates/Articles/add.php<!-- templates/Articles/add.php -->
<h1>新增文章</h1>
<?php
echo $this->Form->create($article);
// 暫時寫死使用者
echo $this->Form->control(
'user_id',
['type' => 'hidden','value' => 1]
);
echo $this->Form->control(
'title',
['label' => '標題']
);
echo $this->Form->control(
'body',
['label' => '內文','rows' => '3']
);
echo $this->Form->button(
__('儲存文章')
);
echo $this->Form->end();
?>這裡使用 CakePHP 的:
FormHelper產生 HTML Form。
例如:
$this->Form->create($article)建立 Form。
$this->Form->control('title')建立 Input。
$this->Form->end()則關閉 Form。
10. 加入「新增文章」連結
回到:
templates/Articles/index.php在 <table> 前加入:
<?= $this->Html->link(
'新增文章',
['action' => 'add']
) ?>這樣使用者就可以直接從 Article List 進入新增畫面。
11. 自動產生 Slug
目前 slug 欄位為 NOT NULL。
但是新增文章時並沒有讓使用者自己填寫 Slug,因此必須在儲存之前自動產生。
在:
src/Model/Table/ArticlesTable.php加入:
use Cake\Event\EventInterface;
use Cake\Utility\Text;接著新增:
public function beforeSave(
EventInterface $event,
$entity,
$options
) {
if ($entity->isNew() && !$entity->slug) {
$sluggedTitle =
Text::slug($entity->title);
$entity->slug =
substr($sluggedTitle, 0, 191);
}
}例如:
My First Article可能轉成:
My-First-Article作為 URL 中使用的識別值。
目前這個簡單版本還沒有處理重複 Slug。
12. 建立 edit() Action
接下來新增:
edit()public function edit(?string $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(
__('您的文章已經更新。')
);
return $this->redirect([
'action' => 'index'
]);
}
$this->Flash->error(
__('無法更新您的文章。')
);
}
$this->set('article', $article);
}與 add() 最大不同在於:
add()
→ 建立新的 Entity
edit()
→ 先查詢現有 Entity13. 建立編輯文章 Template
建立:
templates/Articles/edit.php<!-- templates/Articles/edit.php -->
<h1>編輯文章</h1>
<?php
echo $this->Form->create($article);
echo $this->Form->control(
'user_id',
['type' => 'hidden']
);
echo $this->Form->control(
'title',
['label' => '標題']
);
echo $this->Form->control(
'body',
['label' => '內文','rows' => '3']
);
echo $this->Form->button(
__('儲存文章')
);
echo $this->Form->end();
?>因為 Form 綁定的是已有資料的:
$article所以 FormHelper 可以自動顯示原本的 Article Data。
14. 加入「編輯操作」連結
回到:
templates/Articles/index.php加入操作欄位:
<!-- templates/Articles/index.php 加入編輯操作 -->
<h1>文章</h1>
<?= $this->Html->link(
'新增文章',
['action' => 'add']
) ?>
<table>
<tr>
<th>標題</th>
<th>建立時間</th>
<th>操作</th>
</tr>
<!-- 在這裡我們迭代 $articles 查詢物件,並列印出文章資訊 -->
<?php foreach ($articles as $article): ?>
<tr>
<td>
<?= $this->Html->link(
$article->title,
['action' => 'view', $article->slug]
) ?>
</td>
<td>
<?= $article->created
->i18nFormat('yyyy年MM月dd日 EEEE HH:mm:ss') ?>
</td>
<td>
<?= $this->Html->link(
'編輯',
['action' => 'edit', $article->slug]
) ?>
</td>
</tr>
<?php endforeach; ?>
</table>這樣每篇文章都可以進入對應的 Edit Page。
15. 加入 Validation
接下來替 Article 加入輸入驗證。
在:
ArticlesTable.php加入:
use Cake\Validation\Validator;接著建立:
public function validationDefault(
Validator $validator
): Validator {
$validator
->notEmptyString(
'title',
'請輸入標題'
)
->minLength(
'title',
2,
'標題至少需要 2 個字元'
)
->maxLength(
'title',
255,
'標題最多 255 個字元'
)
->notEmptyString(
'body',
'請輸入內容'
);
return $validator;
}Validation 規則
Title 的規則為:
不可為空
至少 2 個字元
最多 255 個字元Body:
不可為空教材中也替每一條 Rule 加上中文錯誤訊息。
例如:
請輸入標題
標題至少需要 2 個字元
標題最多 255 個字元
請輸入內容因為 Form 使用:
$this->Form->control()建立,所以 Validation 失敗時,錯誤訊息可以直接顯示在對應欄位附近。
16. 建立 delete() Action
最後建立 Delete Action:
public function delete(?string $slug)
{
$this->request->allowMethod([
'post',
'delete'
]);
$article = $this->Articles
->findBySlug($slug)
->firstOrFail();
if ($this->Articles->delete($article)) {
$this->Flash->success(
__(
'文章 {0} 已被刪除。',
$article->title
)
);
return $this->redirect([
'action' => 'index'
]);
}
}為什麼刪除不能使用 GET?
這裡:
$this->request->allowMethod([
'post',
'delete'
]);限制只有:
POST
DELETE可以進行刪除。
原因是:
GET應該用來讀取資料,而不是修改資料。
如果直接使用:
GET /articles/delete/...刪除文章,Web Crawler 或其他自動化工具也可能意外觸發 URL。
因此刪除操作應限制使用適當的 HTTP Method。
17. 加入「刪除操作」連結
回到:
templates/Articles/index.php加入刪除操作:
<!-- templates/Articles/index.php 加入編輯操作 -->
<h1>文章</h1>
<?= $this->Html->link(
'新增文章',
['action' => 'add']
) ?>
<table>
<tr>
<th>標題</th>
<th>建立時間</th>
<th>操作</th>
</tr>
<!-- 在這裡我們迭代 $articles 查詢物件,並列印出文章資訊 -->
<?php foreach ($articles as $article): ?>
<tr>
<td>
<?= $this->Html->link(
$article->title,
['action' => 'view', $article->slug]
) ?>
</td>
<td>
<?= $article->created
->i18nFormat('yyyy年MM月dd日 EEEE HH:mm:ss') ?>
</td>
<td>
<?= $this->Html->link(
'編輯',
['action' => 'edit', $article->slug]
) ?>
<?= $this->Form->deleteLink(
'刪除',
['action' => 'delete', $article->slug],
['confirm' => '您確定要刪除這篇文章嗎?']
) ?>
</td>
</tr>
<?php endforeach; ?>
</table>postLink() 表面上看起來像一個 Link,但會以 POST Request 觸發 Delete Action。
另外:
'confirm'會在執行前跳出確認視窗,降低誤刪資料的可能性。
18. 完成 Articles CRUD
到這裡已經完成基本:
Create
Read
Update
Delete也就是:
CRUD對應:
Create
→ add()
Read
→ index()
→ view()
Update
→ edit()
Delete
→ delete()19. Day 3 重點整理
今天我們實際將 MVC 串在一起。
Model
ArticlesTable
Article Entity
Timestamp
beforeSave()
ValidationController
index()
view()
add()
edit()
delete()View / Template
index.php
view.php
add.php
edit.php同時使用到:
ORM
Dynamic Finder
HtmlHelper
FormHelper
FlashComponent
Slug
Validation
i18nFormat()完整流程可以理解成:
Browser
↓
ArticlesController
↓
ArticlesTable / Article
↓
Database
↓
Controller
↓
Template
↓
Browser下一步
基本 Articles CRUD 完成之後,下一階段就會加入:
Users
Tags
Bake
Association讓目前只有文章管理功能的 CMS,逐漸發展成具有:
多使用者
文章分類
標籤管理功能的完整內容管理系統。
另外,CakePHP 也可以使用:
bin/cake bake controller articles快速產生基本的 ArticlesController。
不過仍要注意,單獨執行這個指令並不會產生:
templates/Articles/*.phpTemplate 檔案。
COURSE FILES