2026 暑訓 / COURSE 02.05

Day 3 - CMS 教學 1

從 MVC 到 CRUD,動手打造你的第一個 CakePHP CMS。

INSTRUCTOR王朝威UPDATED2026.08.23

完成 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_mysql Extension

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 名稱為:

ArticlesTable

CakePHP 也會根據 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 articles

4. 建立 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/index

CakePHP 就會執行:

ArticlesController::index()

並根據 Convention 尋找:

templates/Articles/index.php

5. 建立文章列表 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-post

CakePHP 會將:

first-post

傳給:

$slug

Dynamic 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()
→ 先查詢現有 Entity

13. 建立編輯文章 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()
Validation

Controller

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/*.php

Template 檔案。

COURSE FILES

附件下載

FILE2026 CakePHP-Day3 CakePHP CMS 教學1.pdf下載 ↓FILECMS 教學1.zip下載 ↓
NEXT COURSE / 02.06Day 4 - CMS 教學 2 →