[BE101] 留言板(上-基礎實作篇)


Posted by s103071049 on 2021-06-12

最陽春的留言板只有兩個功能:新增留言、觀看所有留言
php 文件

專案設計步驟

第一步、規劃產品路由與功能:產品路徑、產品功能

index.php 觀看留言
handle_add_post.php 新增留言
// form 送出後連到 handle_add_post.php 新增留言後回到 index.php

第二步、規劃資料結構以及建置資料庫

想一下,產品的資料結構是甚麼,然後把資料庫建置好

  1. ID
  2. 暱稱
  3. 內容
  4. 顯示留言時間(可以在 db 做這塊)

資料庫 => 新增資料表 => 建立 4 個欄位
id 設為 A_I
nickname 設為 128 個字元、var_char
content 設為 TEXT
create_at 設為 DATETIME、預設值 CURRENT_TIMESTAMP

可以用新增留言的方式,檢視我們的資料結構。

第三步、實作留言板前端頁面

先刻出一個資料寫死的資料頁面,之後需要做的只是將靜態變動態。會比直接做功能更容易。
實作留言板前端頁面

第四步、串接資料庫顯示留言

確認過多張小卡顯示無問題後,先小卡刪成一張。

引入 conn.php

/*
The require_once expression is identical to require except PHP will check if the file has already been included, and if so, not include (require) it again.
*/
<?php
  require_once('conn.php');
?>

下 SQL query

 $result = $conn->query("SELECT * FROM comments ORDER BY id DESC");
  if (!$result) {
    die('錯誤訊息' . $conn->error);
  }
  while($row = $result->fetch_assoc()) {
    print_r($row);
  }

php 程式碼可以安插在任何地方,可以將 html 與 php 混著寫。

如果是要輸出變數,也可以在問號後直接接等於,但這樣的簡寫某些地方不支援

<?php echo $row['nickname'];?>
<?=$row['nickname'];?>

第五步、加入新增留言功能

我們目前還沒有 handle.add.comment.php 這個檔案。

參照<[BE101] PHP 與 MySQL (語法)>

<?php
  require_once('conn.php');
  if (empty($_POST['nickname'])|| empty($_POST['content'])) {
    die('資料不齊全');
  }
  $nickname = $_POST['nickname'];
  $content = $_POST['content'];
  $sql = sprintf(
    "insert into comments(nickname, content) values('%s', '%s')",
    $nickname, $content
  );
  echo 'SQL: ' . $sql . "<br>";
  $result = $conn->query($sql);
  if (!$result) {
    die($conn->error);
  }
  echo "新增成功!";
  header('Location: index.php');
?>

調整換行變空白狀況

white-space: pre-line;

前端顯示文字,重要兩幫手:white-space, word-break

資料不齊全,要自己手動回上頁。現在,進行錯誤訊息處理。

在 comment 頁面上進行錯誤訊息顯示:

方式一、comment 頁面加上 get 參數,get 參數有東西,就顯示 err messanger

// handle_add_comment.php
  if (empty($_POST['nickname'])|| empty($_POST['content'])) {
    header('Location: index.php?errMsg=資料不齊全');
    die('資料不齊全');
  }
// 網址變成 http://localhost/board/index.php?errMsg=資料不齊全

在 index.php 檢視有無 errMsg 參數,如果有就顯示錯誤訊息

// index.php
<?php
  if (!empty($_GET['errMsg'])) {
     $msg = $_GET['errMsg'];
     echo '<h2>' . $msg . '</h2>';
  }
?>

缺點,query string 隨便改,網址就會出現麼樣的內容。

方法二、errCode

// handle_add_comment.php
if (empty($_POST['nickname'])|| empty($_POST['content'])) {
    header('Location: index.php?errCode=1');
    die('資料不齊全');
  }

$_GET拿到的東西都是字串
預設為 ERROR,如果是 1 表示資料不齊全

// index.php
<?php
if (!empty($_GET['errCode'])) {
    $code = $_GET['errCode'];
    $msg = 'error';
      if ($code === '1' ) {
        $msg = '資料不齊全';
      }
     echo '<h2>' . $msg . '</h2>';
}
?>

重新整理,錯誤還是會在,所以也不是很好處理錯誤的方式。

最佳處理方式、前端做判斷:表單按下前用 js 做判斷。

在 form submit時,判斷資料有無填,如果沒填,不要讓 form submit 並出現錯誤訊息。
如果要用後端判斷,可以用 flash messenger <留置後話>

第六步、規劃會員功能與路由

不希望機器人、路人亂留言,所以我們加入會員機制,可以登入、可以註冊。

register.php 註冊頁面
handle_register.php 處理註冊邏輯
login.php 登入表單
handle_login.php 處理登入邏輯
logout.php 登出

第七步、規劃會員資料結構以及建置 database

每個會員要有 id、暱稱、user_name、password、create_at

MyAdmin 結構 => 新增欄位 => 增加欄位

第八步、實作註冊功能

先放兩個按鈕,點擊分別會連到註冊頁面、登入頁面

<a href="register.php" class='board__btn'>註冊</a>
<a href="login.php" class='board__btn'>登入</a>
.board__btn {
    padding: 10px;
    border: 1px solid #c2dffb;
    border-radius: 5px;
    max-width: 60px;
    color: #583c63;
    font-size: 16px;
    font-weight: bold;
    background: white;
    text-align: center;
    transition: background 1s;
    margin-top: 10px;
    text-decoration: none;
    min-width: 30px;
    display: inline-block;
}

index.php 複製一份為 register.php,將該刪的刪一刪。

<?php
  require_once('conn.php');
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>留言板</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header class='warning'>
        注意!本站為練習用網站,因教學用途刻意忽略資安的實作,
        註冊時請勿使用任何真實的帳號或密碼。
    </header>
    <main class='board'>
        <a href="index.php" class='board__btn'>回留言板</a>
        <a href="login.php" class='board__btn'>登入</a>
        <h1 class='board__title'>Comments</h1>
        <?php
          if (!empty($_GET['errCode'])) {
              $code = $_GET['errCode'];
              $msg = 'error';
              if ($code === '1' ) {
                $msg = '資料不齊全';
              }
              echo '<h2 class="error">錯誤:' . $msg . '</h2>';
          }
        ?>
        <form class='board__comment' method="POST" action="handle_register.php">
            <div>
                <span>暱稱:</span>
                <input type="text" name='nickname'>
            </div>
            <div>
                <span>帳號:</span>
                <input type="text" name='username'>
            </div>
            <div>
                <span>密碼:</span>
                <input type="password" name='password'>
            </div>
            <textarea name="content" rows="10"></textarea>
            <input class="board__submit-btn" type="submit">
        </form>     
    </main>
</body>
</html>

建立 handle_rigister.php。

username, nickname, password 都不能是空的。如果是空的,就導回 register.php 並顯示資料不齊全。<錯誤處理同上>

// handle_register.php
  if (empty($_POST['nickname'])|| empty($_POST['username'])|| empty($_POST['password']) ) {
    header('Location: register.php?errCode=1');
    die('資料不齊全');
  }
// register.php
if (!empty($_GET['errCode'])) {
    $code = $_GET['errCode'];
    $msg = 'error';
    if ($code === '1' ) {
    $msg = '資料不齊全';
    }
echo '<h2 class="error">錯誤:' . $msg . '</h2>';
 }

當有 err 跳到 errcode2

// handle_register.php
  echo 'SQL: ' . $sql . "<br>";
  $result = $conn->query($sql);
  if (!$result) {
    header('Location: register.php?errCode=2');
    die($conn->error);
  }
  echo "新增成功!";
  header('Location: index.php')
// register.php
if (!empty($_GET['errCode'])) {
    $code = $_GET['errCode'];
    $msg = 'error';
    if ($code === '1' ) {
    $msg = '資料不齊全';
    } else if ($code === '2') {
    $msg = '帳號已被註冊';
    }
echo '<h2 class="error">錯誤:' . $msg . '</h2>';
}

但 err 可能不是重複帳號,而是其他問題。

解決方式一、比對字串

// handle_register.php
  if (!$result) {
    if (strpos($conn->error, 'Duplicate entry') !== false) {
      header('Location: register.php?errCode=2');
    }
    die($conn->error);
  }

解決方式二、用 mySQL 定義的 error code
$conn->errno => code:1062
數字用 die() 會被當作是一個錯誤的狀態碼然後不會被顯示出來,因此後面加上一個字串變成字串之後就會正常顯示出來了

  $result = $conn->query($sql);
  if (!$result) {
    $code = $conn->errno;
    if ($code === 1062) {
      header('Location: register.php?errCode=2');
    }
    die($conn->error);
  }

第九步、實作登入功能

將 register.php 複製一份,改成 login.php,打開來開始改裡面的東西。

// login.php
<body>
    <header class='warning'>
        注意!本站為練習用網站,因教學用途刻意忽略資安的實作,
        註冊時請勿使用任何真實的帳號或密碼。
    </header>
    <main class='board'>
        <a href="index.php" class='board__btn'>回留言板</a>
        <a href="register.php" class='board__btn'>註冊</a>
        <h1 class='board__title'>登入</h1>
        <?php
          if (!empty($_GET['errCode'])) {
              $code = $_GET['errCode'];
              $msg = 'error';
              if ($code === '1' ) {
                $msg = '資料不齊全';
              } else if ($code === '2') {
                $msg = '帳號已被註冊';
              }
              echo '<h2 class="error">錯誤:' . $msg . '</h2>';
          }
        ?>
        <form class='board__comment' method="POST" action="handle_login.php">
            <div>
                <span>帳號:</span>
                <input type="text" name='username'>
            </div>
            <div>
                <span>密碼:</span>
                <input type="password" name='password'>
            </div>
            <input class="board__submit-btn" type="submit">
        </form>     
    </main>
</body>

輸入帳密提交,會進入 handle_login.php,可以用相同的做法,複製 handle.register.php 的內容

怎麼判斷使用者有無登入成功 ? 如果資料庫有這筆帳密就代表他登入成功。
所以可以這樣寫 sql

$sql = sprintf(
    "select * from users where username='%s' and password='%s'",
    $username, $password
  );

不會有 duplicate entry 的狀況,所以可以將除錯關於這塊地段落刪掉。

用 print_r(); 看有登入沒登入的結果差別,

  $result = $conn->query($sql);
  if (!$result) {
    die($conn->error);
  }
  echo "登入成功!";
  print_r($result);

[num_rows] => 1 代表有在資料庫找到這筆資料
[num_rows] => 0 代表沒有在資料庫找到這筆資料

用 print_r($result->num_rows); 進行資料抓取與判斷

  $result = $conn->query($sql);
  if (!$result) {
    die($conn->error);
  }
  if ($result->num_rows) {
    echo "登入成功!";
  } else {
    header('Location: login.php?errCode=2');
  }

登入成功後會導回首頁,但導回首頁後已經是兩個不同的頁面,我要怎麼樣在這兩個頁面傳遞訊息,去知道我剛剛是有登入成功的 ? 因為 http 是沒有狀態的,沒有狀態是說:他每發一個 request 都會當作一個新的 request,他會不知道跟上個 request 之間的關係。也就是說,我登入成功再回到首頁,他會不知道我登入成功了。如何解決這個困擾?

第十步、該怎麼記住登入狀態?

Cookie 簡介與實作

以 server 的角度,他並不知道 request1, request2 是不是同一個人。但 server 可以從 ip 位置去查,可是浮動 ip 會變來變去,就算同 ip 也可能是在同一個區網內(虛擬 ip)。

所以,http 的無狀態性,會讓登錄機制無法實作。

我們可以利用 Cookie 解決上述問題。cookie 可以經由 server 設定。server 可以傳一個 response header。

前面我們使用 response header Location 指定要跳轉到哪邊,這邊我們使用 response header cookie,他可以叫瀏覽器設置 cookie。之後當我們再訪問同樣的頁面,瀏覽器會幫我們自動帶上符合條件的 cookie。甚麼是符合條件 ? 1.沒過期 2. domain 符合 3.path 符合。path 預設為當前資料夾底下的檔案,都可以共享 cookie。path 設為 / 表示我 domain 開頭的所有頁面,都可以共享到這個 cookie。

php 裡面有一個 funct 叫做 setcookie(key, value, 過期時間)

處理登入成功的狀態

  if ($result->num_rows) {
    //登入成功
    $expire = time() + 3600*24*30; // 30 天
    setcookie("username", $username, $expire);
    header('Location: index.php');
  } else {
    header('Location: login.php?errCode=2');
  }

在 index.php 將 cookie 印出來

<?php
  require_once('conn.php');
  $result = $conn->query("SELECT * FROM comments ORDER BY id DESC");
  if (!$result) {
    die('錯誤訊息' . $conn->error);
  }
  print_r($_COOKIE);
?>

用 $username 來判斷是否為登入狀態

  $username = NULL;
  if (!empty($_COOKIE['username'])) {
    $username = $_COOKIE['username'];
  }

如果沒有 $username 就顯示這兩個按鈕

<?php if (!$username) {?>
    <a href="register.php" class='board__btn'>註冊</a>
    <a href="login.php" class='board__btn'>登入</a>
<?php } ?>

如果有登入,就要有登出的按鈕

<?php if (!$username) {?>
    <a href="register.php" class='board__btn'>註冊</a>
    <a href="login.php" class='board__btn'>登入</a>
    <?php } else { ?>
    <a href="logout.php" class='board__btn'>登出</a><?php } ?>

實作 logout.php
我們是用 cookie 紀錄狀態,所以只要將 cookie 清空就好了,所以我們可以將時間設過期。規格中,時間過期就是過去,

<?php
  $expire = time() - 3600;
  setcookie("username", '', $expire);
  header('Location: index.php');
?>

現在實作,沒有登入就看不到底下區塊。

<?php if ($username) {?>
<form class='board__comment' method="POST" action="handle_add_comment.php">
    <div>
        <span>暱稱:</span>
        <input type="text" name='nickname'>
    </div>
    <textarea name="content" rows="10"></textarea>
    <input class="board__submit-btn" type="submit">
</form>
<?php } else { ?>
    <h3>請登入發布留言</h3>
<?php }?>

或者是用登入與否狀態改變提交鈕。

<?php if ($username) {?>
<input class="board__submit-btn" type="submit">
<?php } else { ?>
<h3>請登入發布留言</h3>
<?php }?>

發布留言,原本要有暱稱,但註冊時已有。所以

  1. 將 index.php 的暱稱整段拿掉
  2. 現在 nickname 從 cookie 裡的 username 抓使用者資訊。不把 nickname 加入 cookie 是因為 nickname 可能會改,改了他跟 cookie 裡面的值就會不同步了。
  //先去資料庫撈東西出來
  $username = $_COOKIE['username'];
  $content = $_POST['content'];
  $user_sql = sprintf("SELECT nickname FROM users WHERE username = '%s'", $username);
  $user_result = $conn -> query($user_sql);
  $row = $user_result -> fetch_assoc();
  $nickname = $row['nickname'];
  $sql = sprintf(
    "insert into comments(nickname, content) values('%s', '%s')",
    $nickname, $content
  );

小結、

  1. Cookie 會記錄 http 狀態
  2. 檢視 cookies:dev-tool => application => cookies









Related Posts

Single Number

Single Number

[Web] 使用 OpenSSL 建立開發測試用途的自簽憑證(Self-Signed Certificate)和設置Domain Name

[Web] 使用 OpenSSL 建立開發測試用途的自簽憑證(Self-Signed Certificate)和設置Domain Name

[讀書筆記 Flutter 實戰 004] Dart 語言簡介

[讀書筆記 Flutter 實戰 004] Dart 語言簡介


Comments