###### tags: `Back-End` `php` `資料庫`
# [week 9] 利用 PHP 實作留言板 - 初階實作篇 & Cookie 與 Session 的差別
> 本篇為 [[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)
## 前置作業
在開始實作留言板之前,需先進行前置作業:
- 規劃產品路由與功能
- 規劃資料結構:建立資料庫
### Step1. 規劃產品路由與功能
![](https://i.imgur.com/HKuwCYA.png)
#### 頁面
- 留言板首頁 `index.php`
- 註冊頁面 `register.php`
- 登入頁面 `login.php`
#### 功能
- 新增留言 `handle_add_post.php` → comments 資料庫
- 註冊 `handle_register.php` → users 資料庫
- 登入 `handle_login.php`
- 登出 `logout.php`
### Step2. 規劃資料結構:建置資料庫
#### 建立 comments 資料庫
- id
- nickname
- content
- creat_at
![](https://i.imgur.com/4WWjEaA.png)
#### 建立 users 資料庫
- id
- nickname
- username
- password
- creat_at
![](https://i.imgur.com/w8cOsfs.png)
---
## 實作留言板
可分為前後端。通常會先切出前端頁面,再加入功能,從資料庫取出並資料串聯到頁面。
### Step3. 實作前端頁面
依照設計稿切出所需頁面,可參考[留言板 DEMO](http://mentor-program.co/mtr04group2/Heidi/week9/index.php)。
### Step4. 實作目標功能
接著就進入重頭戲,也就是替靜態網頁加上各種功能。
### PHP 常用函式
- 用 `->` 符號:取用物件中的變數
- 例如:`$conn->error`
- `require_once();`:取用資料夾中其他 library
```php=
require_once("conn.php"); // 連線到資料庫
require_once("utils.php"); // 導入常用函式
```
#### echo、print_r 與 var_dump 的區別
- `echo`:印出變數、字串等
- 例如: `echo "hello World"`
- 若使用 echo 輸出引用變數時(如陣列),只會輸出陣列名
- `print_r()`;:印出物件、陣列
- 例如:`print_r($row);`
- `var_dump`:印出變數型態,作用是輸出變數的詳細資訊
#### 讀取資料
- `fetch_array()`:讀取資料同時,以數字與欄位名稱各存一次在陣列中
- `fetch_assoc()`:讀取的資料 Key 值設定為欄位名稱的陣列
- `fetch_row()`:讀取的資料 Key 值設定為依序的數字
```htmlmixed=
<?php
// 把 $result 資料的 Key 值設定為欄位名稱的陣列
while($row = $result->fetch_assoc()) {
?>
<div class="card">
<div class="card__avatar"></div>
<div class="card__body">
<div class="card__info">
<span class="card__author"><?php echo $row['nickname']; ?></span>
<span class="card__time"><?php echo $row['created_at']; ?></span>
</div>
<p class="card__content"><?php echo $row['content']; ?></p>
</div>
</div>
<div class="board__hr"></div>
<?php } ?>
```
若以 `print_r($row);` 印出上述程式碼,可知 `$row` 為陣列:
![](https://i.imgur.com/EnxNFAl.png)
#### 檢查是否存在
- `isset()`:檢查是否有此變數
- `empty()`:檢查是否有值
```php=
$username = NULL;
// 如果 session 中沒有存 username,就讀取 session
if(!empty($_SESSION['username'])) {
$username = $_SESSION['username'];
}
```
- `query()`:判斷資料庫查詢是否成功
- 順利執行回傳 true
- 查詢的帳密有誤、查詢的指定資料庫、資料表欄位有誤等,均回傳 false
- `exit()` 和 `die()`:兩者幾乎相同,均為輸出消息後退出程式
```php=
// 以 id 進行 DESC(遞減)排序:"後新增的留言"會排在前面
$result = $conn->query("SELECT * FROM comments ORDER BY id DESC");
// 檢查是否查詢成功
if (!$result) {
die('Error:' . $conn->error);
}
```
- `sprintf()`:裡面可放入替代字元
例如使用 `sprintf()` 做 SELECT:
```php=
// handle_login.php
$sql = sprintf(
"SELECT * FROM users WHERE username='%s' AND password='%s'",
$username,
$password
);
// 把執行結果存在 $result 這個變數中
$result = $conn->query($sql);
// 確認是否有拿到結果
if (!$result) {
die($conn->error);
}
```
用 `sprintf()` 做 INSERT INTO:
```php=
// handle_add_comment.php
$sql = sprintf(
"INSERT INTO comments(nickname, content) VALUES('%s', '%s')",
$nickname,
$content
);
// handle_register.php
$sql = sprintf(
"INSERT INTO users(nickname, username, password) VALUES('%s', '%s', '%s')",
$nickname,
$username,
$password
);
```
### `conn.php` 連線資料庫
> 【注意】由於 `conn.php` 放有帳號密碼等重要資料,因此在 commit 前需加入 `git.ignore`,不進行版本控制。
程式碼如下:
```php=
<?php
$server_name = 'localhost';
$username = 'heidi';
$password = '1234';
$db_name = 'heidiDB';
// `mysqli` 的四個參數分別為:伺服器名稱、帳號、密碼、資料庫名稱
$conn = new mysqli($server_name, $username, $password, $db_name);
// 確認是否出現連線錯誤
if (!empty($conn->connect_error)) {
die('資料庫連線錯誤:' . $conn->connect_error);
}
// 設定編碼,避免出現亂碼
$conn->query('SET NAMES UTF8');
// 設定成臺灣時區
$conn->query('SET time_zone = "+8:00"');
?>
```
### `index.php` 顯示所有留言
```php=
<?php
session_start();
// 連線到資料庫
require_once("conn.php");
require_once("utils.php");
$username = NULL;
if(!empty($_SESSION['username'])) {
$username = $_SESSION['username'];
}
// 以 id 進行 desc(遞減)排序,也就是"後新增的留言"會排在前面
$result = $conn->query("SELECT * FROM comments ORDER BY id DESC");
// 檢查是否有資料
if (!$result) {
die('Error:' . $conn->error);
}
?>
```
---
## 儲存狀態的方式:Cookie & Session
### 使用 Cookie 記住 HTTP 狀態
首先要了解 Cookie 是什麼:
- 是一種小型純文字檔案,
- 網站伺服器會將其儲存在 client 端,以記錄使用者的相關資訊。
- 例如:會員登入狀態、瀏覽紀錄、購物車等。
由於 HTTP 是一個無狀態協議,會把每一次收到的請求都視為獨立的行為。但伺服器能透過 response header 的 `Set-Cookie` 屬性,將使用者狀態記錄在 Cookie。
瀏覽器會在每次發送請求時,自動在 request header 帶上 Cookie 資料;伺服器即可藉由檢視 Cookie 內容,得知瀏覽器使用者的狀態。
![](https://i.imgur.com/WsBvdz5.png)
但這麼做有個缺點,儲存在 client 端的 Cookie 是能夠被竄改的,因此不適合放機密或重要的資訊。這時有兩種解法:
#### 1. 將 Cookie 內容加密
也就是 Cookie-based session,把狀態加密後存在 Cookie。但如果加密方式以及密鑰被破解,往後仍有安全疑慮。
#### 2. 透過 Session ID 辨識身分
全名是 Session Identifier。如此 Server 只需在 Cookie 儲存一組亂數產生的 Session ID,其餘狀態資訊則存在 Server 端。
因此,Session 其實就是一種讓 Request 變成 stateful 的機制。
![](https://i.imgur.com/Nbk8TCj.png)
### 如何使用 Session
#### 儲存 Session
當 `$_SEESION` 儲存成功,會進行下列三件事:
1. 產生 sesseion id (token)
2. 把 username 寫入檔案
3. `set-cookie`: `session-id`
```php=
// 使用 Session 時,均需在開頭加上 session_start()
session_start();
$username = htmlspecialchars($_POST['username']);
// 把資料存在 Session 對應的 key 裡面
$_SEESION['username'] = $username;
```
#### 讀取 Session
當 `$_SESSION` 讀取資料時,會進行下列三件事:
1. 從 cookie 裡讀取 PHPSESSID (token)
2. 從檔案裡面讀取 session id 的內容
3. 把內容放到 `$_SESSION`
```php=
session_start();
// 若 Session 內有存過 username,則 $username 為剛才存的 $_SESSION['username']
if(isset($_SESSION['username'])) {
$username = $_SESSION['username'];
}
```
#### 清除 Session
```php=
session_start();
// 直接清除所有 session
session_destroy();
```
參考資料:
1. [27. [WEB] Cookie & Session 是什麼?](https://ithelp.ithome.com.tw/articles/10227602)
2. [白話 Session 與 Cookie:從經營雜貨店開始](https://medium.com/@hulitw/session-and-cookie-15e47ed838bc)