###### tags: `Back-End` `php` `資料庫`
# [week 11] 利用 PHP 實作留言板 - 新增功能篇
> 本篇為 [[BE101] 用 PHP 與 MySQL 學習後端基礎](https://lidemy.com/p/be101-php-mysql) 這門課程的學習筆記。如有錯誤歡迎指正。
相關筆記:
- [[week 9] 後端基礎 - PHP 語法、資料庫 MySQL](https://github.com/heidiliu2020/This-is-Codediary/blob/master/week9_%E5%BE%8C%E7%AB%AF%E5%9F%BA%E7%A4%8E_PHP%E3%80%81MySQL.md)
- [[week 9] 利用 PHP 實作留言板 - 初階實作篇](https://github.com/heidiliu2020/This-is-Codediary/blob/master/week9_%E5%88%A9%E7%94%A8PHP%E5%AF%A6%E4%BD%9C%E7%95%99%E8%A8%80%E6%9D%BF-%E5%88%9D%E9%9A%8E%E5%AF%A6%E4%BD%9C%E7%AF%87.md)
- [[week 11] 資訊安全 - 雜湊與加密 & 常見攻擊:SQL Injection、XSS](https://github.com/heidiliu2020/This-is-Codediary/blob/week11/week11_%E8%B3%87%E8%A8%8A%E5%AE%89%E5%85%A8_%E9%9B%9C%E6%B9%8A%E8%88%87%E5%8A%A0%E5%AF%86%E3%80%81%E5%B8%B8%E8%A6%8B%E6%94%BB%E6%93%8A.md)
---
## 規劃產品路由與功能
在開始實作新功能前,同樣先進行前置作業:
### 新增功能
- 編輯暱稱功能 `handle_update_user.php`
- 編輯留言功能 `handle_update_comment.php`
- 刪除留言功能 `handle_delete_comment.php`
- 分頁功能 `offset` & `limit`
### 新增頁面
- 編輯留言 `update_comment.php`
---
## 編輯暱稱
作法有兩種:
1. 在首頁建立「編輯暱稱」按鈕,點擊可連至新頁面,可進行編輯功能
2. 若不想多寫一個頁面,也可直接在首頁放表單
在登入狀態下,才能看到下列程式碼:
```php=
<?php if ($username) { ?>
<div class="board__user-block">
<h4>你好!<?php echo escape($username); ?></h4>
<form class="board__new-comment-form" method="POST" action="handle_update_user.php">
<div class="board__nickname">
<span>編輯暱稱:</span>
<input type="text" name="nickname">
<input class="board__submit-btn" type="submit">
</div>
</form>
</div>
<?php } ?>
```
---
## 編輯留言
在實作編輯留言之前,我們可發現當初建立的 comments 資料庫欄位是有問題的,因為一旦 user 修改 nickname,就會影響其他 table。
### 資料庫正規化
因此我們必須進行「資料庫正規化」,也就是以 username 來代替 nickname,如此即可和 users 資料庫產生關聯性。
修改後欄位如下:
- id
- username(產生關聯)
- content
- creat_at
### SQL JOIN 語法
那麼該如何利用 username 欄位,把 comments 和 users 資料庫關聯起來呢?就是使用 SQL JOIN 語法。
SQL JOIN(連接)是利用不同資料表之間,各欄位的「關連性」來結合多資料表之檢索。
透過結合多個資料表,組成一抽象的暫時性資料表以供資料查詢,原各資料表中之紀錄及結構,皆不會因此連接查詢而改變。
語法可參考 [Visual join](https://joins.spathon.com/):
![](https://i.imgur.com/i67eLkk.png)
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- OUTER JOIN
其中最常用到的是 LEFT JOIN 語法:
- LEFT JOIN 左側資料表 (table_name1)的所有記錄都會加入到查詢結果中
- 即使右側資料表(table_name2)中的連接欄位沒有符合的值也一樣
```sql=
SELECT table_column1, table_column2...
FROM table_name1
LEFT JOIN table_name2
ON table_name1.column_name=table_name2.column_name;
```
以先前資料庫的 username 欄位為例,實作留言與會員的 JOIN:
```sql
SELECT *
FROM comments -- 將左側資料表 comments 的
LEFT JOIN users -- 所有記錄加入查詢結果
ON comments.username = users.username -- 與右側資料表 users 進行關聯
ORDER BY comments.id DESC -- 並以倒敘排列
```
而在實際操作中,為了避免資料被覆蓋,我們可在 SQL 語法中幫 table 補上別名,並直接指定要取出的資料:
- `comments AS C`
- `users AS U`
```sql
SELECT
C.id as id, C.content AS content,
C.created_at AS created_at, U.nickname AS nickname, U.username AS username
FROM comments AS C
LEFT JOIN users AS U ON C.username = U.username
ORDER BY C.id DESC
```
參考資料:
- [SQL JOIN 合併資料表- SQL 語法教學Tutorial](https://www.fooish.com/sql/join.html)
---
## 刪除留言
### hard delete 硬刪除
使用 SQL 刪除語法,將留言從資料庫中刪除。
```sql
$sql = "DELETE FROM comments WHERE id=?";
```
### soft delete 軟刪除
為了避免誤刪重要資料,我們可利用 soft delete(軟刪除),也可稱為「邏輯刪除」或「標記刪除」。
- 和硬刪除不同之處:並不會真正從資料庫中刪除該筆記錄
- 作法:設定一個欄位,例如 `is_deleted` 來標記刪除狀態,預設值為 `NULL`
- 優點:保證資料的完整性,避免誤刪重要資料,隨時能夠進行復原
- 缺點:儲存的資料會不斷佔據空間。因此若確定某些資料不再需要時,即可使用硬刪除。例如:驗證碼
在 comments 資料庫新增 is_deleted 欄位,再把上面的 SQL 指令改為下列語法:
```sql
-- handle_delete_comment.php
$sql = "UPDATE comments SET is_deleted=1 WHERE id=?";
```
再回到 `index.php` 修改 SQL 語法,也就是加上 `"WHERE C.is_deleted IS NULL ".`,只抓取 `is_deleted` 為 `NULL` 的資料:
```php=
$stmt = $conn->prepare(
"SELECT ".
"C.id as id, C.content AS content, ".
"C.created_at AS created_at, U.nickname AS nickname, U.username AS username ".
"FROM comments AS C ".
"LEFT JOIN users AS U ON C.username = U.username ".
"WHERE C.is_deleted IS NULL ".
"ORDER BY C.id DESC"
```
參考資料:
1. [軟刪除和刪除](https://www.itread01.com/content/1546740906.html)
---
## 分頁功能 offset & limit
### LIMIT 語法
- 限制傳回的資料筆數
- 通常會伴隨著 `ORDER BY` 使用
- LIMIT: `$item_per_page`
```sql
-- 用 ORDER BY 以 id 欄位做排序,回傳最新的 5 筆資料
SELECT * FROM comments ORDER BY id DESC LIMIT 5
```
### OFFSET 語法
- 跳過的資料筆數
- OFFSET: `(page-1)*item_per_page`
```sql
-- 跳過 5 筆資料,回傳接下來的 5 筆資料
SELECT * FROM comments ORDER BY id DESC LIMIT 5 OFFSET 5
```
### 應用在實作留言版
```php=
$page = 1;
// 確認網址是否有帶到 $page 參數
if (!empty($_GET['page'])) {
// intval() 將字串轉成數字
$page = intval($_GET['page']);
}
$items_per_page = 5; // LIMIT 限制回傳資料筆數
$offset = ($page -1) * $items_per_page; // OFFSET 跳過的資料筆數
$stmt = $conn->prepare(
// 在 table 補上別名,避免資料覆蓋,並直接指定要取出的資料
"SELECT ".
"C.id as id, C.content AS content, ".
"C.created_at AS created_at, U.nickname AS nickname, U.username AS username ".
"FROM comments AS C ".
"LEFT JOIN users AS U ON C.username = U.username ".
"WHERE C.is_deleted IS NULL ".
"ORDER BY C.id DESC ".
"LIMIT ? OFFSET ? "
);
$stmt->bind_param("ii", $items_per_page, $offset);
$result = $stmt->execute();
if (!$result) {
die('Error:' . $conn->error);
}
$result = $stmt->get_result();
```
把抓到的資料,在前端頁面顯示之程式碼如下:
```htmlmixed=
<div class="page-info">
<span>總共 <?php echo $count ?> 筆 - 頁數:</span>
<span><?php echo $page ?> / <?php echo $total_page ?></span>
</div>
<div class="paginator">
<?php if ($page != 1) { ?>
<a href="index.php?page=1">首頁</a>
<a href="index.php?page=<?php echo $page - 1 ?>">上一頁</a>
<?php } ?>
<?php if ($page != $total_page) { ?>
<a href="index.php?page=<?php echo $page +1 ?>">下一頁</a>
<a href="index.php?page=<?php echo $total_page ?>">最末頁</a>
<?php } ?>
</div>
```
### 修正問題:權限管理
到這邊留言版的功能就完成得差不多了,但其實還有個重大安全問題。就是任何人都能藉由更改「網址列」,來編輯或刪除其他人的留言。
解決方法,就是透過「設立權限管理」,來驗證使用者是否和該留言為同一人:
```php=
$id = $_GET['id'];
$username = $_SESSION['username'];
// 判斷留言 id 和從 SESSION 抓取的 username 是否相同
$sql = "UPDATE comments SET is_deleted=1 WHERE id=? AND username=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param('is', $id, $username);
$result = $stmt->execute();
if (!$result) {
die($conn->error);
}
```