# [BE101] 用 PHP 與 MySQL 學習後端基礎 ###### tags:`Backend` [TOC] > [SQL語法集](https://www.1keydata.com/tw/sql/sql.html) > 帳密不要放到網路上 ## SQL !== 資料庫 資料庫 > MySQL 被收購之後,社群就自己做了一個類似的 MariaDB ==SQL, php > 程式語言== >資料庫其實是一種特殊格式的檔案,這種檔案必需要透過特殊介面 (資料庫軟體) 來進行讀寫。由於這個特殊介面已經針對資料的查詢、寫入做過最佳化設計, 因此很適合多人同時寫入與查詢的工作。 >針對資料庫的語法有所謂的 SQL 標準語法,任何根據這種資料檢索語法發展出來的資料庫,就稱為 SQL 資料庫。 比較知名的自由軟體資料庫系統有 MySQL 及 PostgreSQL ,其中 MySQL 的使用率又比較高一些。 >BY *vbird* die($connection->error); $err_code = $connection->errno; // 發生其他錯誤的處理 if($err_code) { echo '伺服器連線錯誤'; echo $err_code; die($connection->error); } header('Location: update_comment.php?errCode=0' . $_POST['ID']); ## PHP 基礎 ==request > apache(server) > php > html > apache > response== > server 也是一個程式 > php 的輸出入一般會先給 server 做**預處理**,所以不會直接輸出 .php 的原始程式碼 * 網址跟實際檔案的對稱關係可以自己設定 ## 資料庫基礎 server > 程式,專門處理 request / response 資料庫 > 程式,專門處理資料 * 資料庫有兩種: relational database,關聯性資料庫>相同類型的資料放在一起,例如 MySQL, PostgreSQL,靠著彼此間有相關的部份把 TABLE 連在一起 NoSQL(not only SQL),例如 MongoDB * phpmyadmin : 寫在 php 上的圖形化資料庫管理程式==(GUI)== * schema : 資料庫的結構 * ==比較 primary key, foreign key== The main purpose of primary key is to identify the uniqueness of a row, where as unique key is to prevent the duplicates https://stackoverflow.com/questions/54527999/why-we-need-a-primary-key/54528079 https://stackoverflow.com/questions/655446/what-exactly-is-a-foreign-key * index,方便查詢特定欄位做定位用,要花時間,一個索引可以對應多個欄位 * [distinct](https://www.1keydata.com/tw/sql/sqldistinct.html) * 記得操作網頁前要把 browser 給 disable cache,免得一直讀取快取而不讀取原始檔案 ### 基礎語法 跟資料庫連線的語法有兩種,`$link = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db');` 和 `$mysqli = new mysqli('localhost', 'my_user', 'my_password', 'my_db')`,後者比較新而且錯誤處理比較容易,[參考](https://stackoverflow.com/questions/15707696/new-mysqli-vs-mysqli-connect),[BE101] 裡面有提到可以使用 `->` 來調用函數,查詢之後發現 `mysqli` 是個 Class(所以查詢[官方文件](https://www.php.net/manual/en/class.mysqli.php)的時候才會看到 `::` 這種奇妙的符號?),底下有很多函式可以調用(類似 prototype 的概念...?) ```php= $sql = "SELECT * FROM table WHERE 1"; $sql = "INSERT INTO table (id, role) VALUES (value1, value2)"; $sql = "UPDATE table SET id = value1, role = value2 WHERE 1"; $sql = "DELETE FROM table WHERE 1"; $result = $connection->query($sql); // 考慮到資安因素,建議使用 stmt $row = $result->fecth_assoc(); ``` `WHERE = ` 設定額外篩選條件 (SELECT, UPDATE 會用到) 使用 `is_deleted = 1` ˊ這個屬性,來代表資料是被刪除的,而不是直接從資料庫刪掉(沒救) ### 再次複習 PHP 執行流程 動態網頁會在 送出 response 檔案給 SERVER 之前,先執行 PHP ,把檔案裏面 php 的部分先跑過一次,再給 server,是否要先執行某個檔案這件事是可以被 server 設定的 > 這次使用的 XAMPP 會預執行 .php 檔 ![](https://i.imgur.com/9obHtvc.png) ### 實作 > 筆記在實作的 xampp/htdocs/Test 資料夾裡面 * php 是用來操作 DB 的程式語言,一個 DB 可以對稱很多個 php * 通常會把 php 連線到 DB 的 php程式碼獨立出來寫,然後再用 `require_once('conn.php');` 這段程式碼來引入 ### Cookie 相同 domain 的 cookie 可以互相讀取 cookie 由瀏覽器來做設定和傳送,存放在客戶端 ```php= setcookie ( string $name , string $value = "" , array $options = [] ); // 另一種讓 COOKIE 過期的方法 setcookie('name', 'content', 1); ``` ## 修正問題 永遠不要相信 client 的資料(server以外) * 要做 token 驗證 ### PHP 內建 session 機制 > COOKIE 和 session 都相當於識別證的概念 跟 cookie 不一樣,sessionID 存放在 SERVER,存放內容如下 ![](https://i.imgur.com/cNPkIzh.png) ```php= // 開啟內建的 session $session_start(); // 1.產生 PHPSESSID(token) // 2.把 $username 寫入檔案 // 3.最後設定 cookie "PHPSESSID: ###" $_SESSION['username'] = $username; // 1.從 cookie 裡面讀取 PHPSESSID(token) // 2.再從檔案裡面讀取 PHPSESSID(token) 的內容 // 3.最後放到 $username $username = $_SESSION['username']; // 清除 PHPSESSID, PHPSESSID 還在瀏覽器但是內容已經清空了 $seccion_destroy(); ``` PHPSESSID 是存在 xampp/tmp 裡面,我用不同帳號去登入和登出做測試,後來發現同個瀏覽器的 PHPSESSID 是不變的,但檔案裏面寫入的內容會根據登入的帳號不同而改變 ![](https://i.imgur.com/xudyDeI.png) ### hash 存放密碼 hash 雜湊函數,例如: MD5, SHA265 它是一個單向的函數,輸入一樣,輸出的結果保證一樣,不同輸入可能會有相同輸出(碰撞),因此沒辦法從輸出反推出輸入 所以如果使用者忘記密碼了,是沒辦法從 server 給出原本的密碼,只能重設密碼XD(除非對方是存明碼) 加鹽 salting,自動幫使用者的輸入加上一段亂數,降低被破解的可能 > [補充](https://medium.com/starbugs/what-are-encoding-encrypt-and-hashing-4b03d40e7b0c) ### XSS cross-site-scripting,使用==htmlspecialchars==來解決 但資料庫存的通常還是使用者輸入的原始資料,在顯示的時候才做處理 ### SQL injection 加入`'#` 讓#後面的語法被註解掉,方法五花八門...,使用==prepared statement==來解決 ```php= $sql = 'insert into `message-board`(`nickname`, `content`) values (?, ?)'; $stmt = $connection->prepare($sql); $stmt->bind_param('ss', $nickname, $content); $result = $stmt->execute(); // 正確新增的話會回傳 1 if(!$result) { die($connection->error); } $result = $stmt->get_reults(); // 之後在做 fetch_assco() ``` ### 加上分頁功能 limit 和 offset limit: 限制取出的資料量 offset: 設定 limit 的偏移量 ![](https://i.imgur.com/dA1u3LB.png) ### 加上權限檢查 ## 留言板 - API 篇 之前W9和11的作業的作法是: 把 DB 資料拿出來 > 把資料跟 HTML 結合 > 回傳 HTML 現在的 API的作法則是: 把 DB 資料拿出來 > 把資料變成某種格式 > 回傳特定格式的資料 > HTML 再做動態新增 ### API 實作 ![](https://i.imgur.com/w00LTtt.png) ![](https://i.imgur.com/TzTz93K.png) ![](https://i.imgur.com/tn7YDmA.png) ## 實作小細節 * row 有相同名稱,會以後面出現的為主:![](https://i.imgur.com/jdTEYz4.png) * 把相同的HTML 模板獨立出來:`require_once('./nav.php');` * 後台、`handle_*`系列也要做 SESSION 或其他驗證,避免使用者套過修改網址、query string、直接對表單送出資料(不透過瀏覽器)而進入`require('check_permission.php');` * 只輸出資料的一部份:`substr(); // limit string` * redirect 可以透過 php 或 DOM 來達成: ```javascript= goBack(-2) function goBack(page) { window.history.go(page) } ``` * css 的文章部分記得要加上 `white-space: pre-line;`,才會正常斷行R ### JSON String ? decode ? encode ? parse ? stringfy ? 為了處理正確地在 JS 與 PHP 之間傳送和讀取 JSON,我卡關好久之後(整整兩天),在血與淚的研究和發問之後,終於比較懂這三者之間的差異了,寫個筆記紀錄一下,因為感覺未來很可能再踩進同一個坑QQ 先建立個觀念: 所謂的 JSON 就只是一個 string type 的資料,它**不是**物件、陣列,所以我通常都稱它為 JSON String,以避免自己又搞混。 既然 JSON String 是個 string,那個 JS、PHP 要怎麼讀懂它所要表達的內容呢?這時候就要靠 `json_decode()` (PHP) 或是 `JSON.parse()` (JS)啦, 再延伸一題,如果我們想要自己產生一個 JSON String 要怎麼做呢?一般會用陣列或物件型式的變數來蒐集資料,然後再用`json_encode()` (PHP) 或是 `JSON.stringfy()` (JS) 來轉換成 JSON String。 總結:文章標題提到的的概念差不多是這樣,PHP 的 json_decode() 和 JavaScript 的 JSON.parse() 差不多,目的是得到陣列或物件,以方便操作他們。PHP 的 json_encode() 和 JavaScript 的 JSON.stringfy() 差不多,目的是得到 JSON String #### JSON: 從 PHP 傳到 JS 傳送端(PHP) 這裡就要考慮要如何產生 JSON String 了, 作法之一是自己把需要的資料取出來,建立 array 來存放資料,這個作法所產生的資料型式實際上是一個**陣列**,需要使用 `json_encode()` 來轉換成 JSON String 作法之二是直接在後端的資料庫建立一個 JSON 型式的欄位來做存取,例如下圖,從該欄位取出的資料實際上就是 JSON String 了,所以不用特地再做`json_encode()` 來轉換 ![](https://i.imgur.com/Suij0PP.png) 接收端(JS) 要記得做 `JSON.parse()` 來轉換成物件或陣列 #### JSON: 從 JS 傳到 PHP 傳送端(JS) 一樣是要自己先產生 JSON String,一般會使用陣列或物件來蒐集資料,蒐集完之後記得要做`JSON.stringfy()` 來轉換成 JSON String 再做傳送,我在這邊滿順利的,就不介紹了XD 傳送的方式以 POST 為例,會遇到 CORS 和 preflight 的問題,參考[這篇文章](https://shubo.io/what-is-cors/#preflight-request-%E9%A0%90%E6%AA%A2%E8%AB%8B%E6%B1%82)設置對應的 header 之後就可以解決 接收端(PHP) 這邊就神奇了,因為我是直接傳送 JSON String,例如下面程式碼,PHP 裡面寫成這樣才可以正確拿到資料(`$obj = json_decode(file_get_contents('php://input'), true);`),有查到[W3SCHOOL](https://www.w3schools.com/js/js_json_php.asp) 提到的做法,但是在我這邊無效,原因還不知道XD > 21/07/16更新: 原因是 jQuery 的 request 忘記加上 content-type ```javascript= const url = 'http://localhost/dashboard/Todo/api_handle_add_todo.php' const request = new XMLHttpRequest request.open('POST', url, true) request.setRequestHeader("Content-Type", "application/json;charset=UTF-8") request.addEventListener('load', (e) => { if (!(200 <= request.status < 400)) { alert('123Oops, something is going wrong:', request.status) } let json = '' try { json = JSON.parse(request.response) } catch (err) { alert('Oops, something is going wrong:' + err) } if(!json.success) { alert(json.message) return } alert(json.message) }) request.send(data) // data 已經用 JSON.stringfy() 做轉換 ``` ## 程式導師實驗計畫:Lesson 8-2 之資料庫 NoSQL, Not only SQL * 沒有 schema * 用 key-value 存(JSON) * 不支援 JOIN * 通常用來存結構不固定的資料,ex log ## Transaction 交易 資料庫做一次 query 不能完成的操作,例如轉帳。為了保證資料庫操作的正確,需要符合 ==ACID== 這四個特性: 原子性 atomicity: 確保操作全部成功或全部失敗 一致性 consistency: 操作前和操作後,錢的總數要相同 隔離性 isolation: 不能同時改一個值 持久性 durability,動作完成之後,資料還在 例如↓,在`commit()`之後才會執行 query,所以也可以在一次`commit()`裡面塞很多 query,然後依次執行,效率會比較好。 ```php= $conn->autocommit(FALSE); $conn->begin_transaction(); $conn->query("UPDATE FROM money SET amount = 20"); $conn->query("UPDATE FROM money SET sum = 10"); $conn->commit(); ``` (MYISAM 不支援、innoDB 支援) ## Lock 鎖 為了避免多筆 request 對資料庫操作可能產生的錯誤,需要==在每一次的 trancaction 裡面加上 LOCK==,==確保第一次動作完成才進行下一次動作==。 例如↓,第 3 行把 table 'products' 的 id = 1 row 鎖起來,等到`UPDATE`完之後才可以做下一次的操作。`` ```php= $conn->autocommit(FALSE); $conn->begin_transaction(); $conn->query("UPDATE amount FROM products WHERE id = 1 for update"); $conn->query("UPDATE FROM products SET sum = 10"); $conn->commit(); ``` 做交易的期間DB 會被鎖定,有很多種[鎖定模式](http://caryhsu.blogspot.com/2011/09/sql-server.html) ## View ==虛擬的表格== 把不同表格的部分資料取出來,產生一個"檢視表" * 跟一般的資料表不同, view 是==唯讀的== * 限制使用者可以看到的資料 ```sql= CREATE VIEW my_viwe AS SELECT * FROM my_table ``` [補充](https://www.fooish.com/sql/view.html) ## stored procedure [mariaDB Doc](https://mariadb.com/kb/en/stored-procedure-overview/) [補充](https://ryanchen34057.github.io/2019/09/25/storedProcedure/) 一般的 function ```sql= SELECT SUM(prive) FROM my_table WHERE 1 ``` stored procedure,==自己寫的 function,會存在 db 預存程序那邊== ```sql= DELIMITER // CREATE PROCEDURE(id INT) BEGIN SELECT id FROM my_table WHERE id = id; END // DELIMITER ; ``` ```sql= CALL getorder(1) ``` ## Triggers 事情發生前/後要做的事情 [maria DB](https://mariadb.com/kb/en/trigger-overview/) ==類似 JS 的監聽器==,ex 自動化的 log 紀錄? ```sql= DELIMITER // CREATE TRIGGER before_product_update BEFORE UPDATE ON my_table FOR EACH ROW BEGIN INERT INTO my_table(id, name, price, action) VALUE(OLD.id, OLD.name, OLD.price, 'UPDATE'); <!-- OLD, 更動之前的值 NOW更動之後的值 --> <!-- my_table 會存放 OLD(update前)的值 --> END // DELIMITER ; ``` 做更新?當下的價格 ## CS75 (Summer 2012) Lecture 9 Scalability Harvard Web Development David Malan