PHP PDO 物件

PDO 連線方式

PDO 是使用物件導向的方式進行使用,好處是 PDO 除了 MySQL 也支援其他資料庫類型,如果今天專案的資料庫變成其他品牌了,PDO 只要調整宣告的代碼即可異動範圍較小。

  1. 連線方式

    <?php
     $pdo = new PDO("mysql:host=127.0.0.1;dbname=class; charset=utf8", "root", "1qaz@wsx");
    ?>
    • 使用 PDO 必須先新宣告成一個變數(名稱自訂),需完整提供 SQL 資訊,包含 ("SQL 類型類型:host=位置;dbname=資料庫名稱;charset=編碼","帳號","密碼")
    • 之後每次要透過 PDO 進行 SQL 操作,都能用 $pdo 來進行執行 PDO 函數。
  2. 執行指令 query()
    PDO 因為是物件導向,要利用 -> 做一個導向執行。下列為透過 PHP 去執行新增資料表的動作。

    $sql = "
    CREATE TABLE `students` (
     `cID` tinyint(2) UNSIGNED ZEROFILL NOT NULL AUTO_INCREMENT,
     `cName` varchar(20) COLLATE utf8_unicode_ci NOT NULL,
     `cSex` enum('F','M') COLLATE utf8_unicode_ci NOT NULL   DEFAULT 'F',
     `cBirthday` date NOT NULL,
     `cEmail` varchar(100) COLLATE utf8_unicode_ci DEFAULT   NULL,
     `cPhone` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
     `cAddr` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
     `cHeight` tinyint(3) UNSIGNED DEFAULT NULL,
     `cWeight` tinyint(3) UNSIGNED DEFAULT NULL
    )   ENGINE=InnoDB DEFAULT CHARSET=utf8mb4  COLLATE=utf8mb4_unicode_ci;
    ";
    
    $result = $pdo->query($sql);
    • 先將 SQL 語法當作字串放到變數
    • 再來執行 PDO 物件並導向到裡面的 query() 函數。讓 PDO 進行 SQL 連接並且執行 query()
    • 每次 PDO 連線結束後會 return 資料給我們,我們可以用個變數(名稱自訂)存起來。
  3. 檢查錯誤訊息 errorInfo()

    PDO 異常發生問題,PDO 會自動儲存錯誤訊息。需要透過沒有如期的得到你要的結果,需要 errorInfo() 函數。下列為檢查錯誤的示範

    $result = $pdo->query($sql);
    if(!$result) print_r($pdo->errorInfo());

    如果回傳是空的(無結果),陣列印出 $pdo->errorInfo() 這個結果

singleton

  singleton 中文名稱為 單例模式,是一種建構類別的設計模式(一套編寫類別的規範、技巧,將它作成公版來給大家使用;設計模式有許多種,例如:工廠模式簡單工廠模式抽象工廠模式複雜模式單例模式高抽象模式..等)。其目的是為了在全域中獲取這個類別的物件時 ==總是能獲取到唯一的物件==,而不是每次創建實體時都創建出新的物件的一種類別結構。特別的在 DB 操作中,DB 連接這種物件就必須是通過 單例模式 來實現的。

  1. 說明
    類別的主要目的在建構物件,所以每次創建類別都會獲得一個實體,這是正常操作現象,但是為了達到在全局開發中,對一個類別,不管創建幾次都只會得到一個物件,所以有了這個類別的設計模式:單例模式

    • JS 中寫一個類別 01單例模式.html
    • PHP 中寫一個資料庫連線類別 01單例模式.php
  2. 練習: 設計一單例模式,類別名稱: richestMan,裡面有一單例方法: findRichestMan(),用來在全局中獲取唯一的單例對象 (richestMan)。

    解答: 單例模式-練習.php
    輸出:

    ==================創建單例類別 =================
    richestMan Object ( [pname] => 最富有的人 )
    richestMan Object ( [pname] => 最富有的人 )
  3. 結論
    為何需要單例模式:

    • 例一:購物車
      網站中在任一頁面開啟時,購物車應該都只有一個,依過去類別的概念來作,打開一個頁面實例一個對象一個新的購物車對象,這時就須用單例的結構。
    • 例二:地圖
      全局中只希望一張地圖,否則一經修改又須一張地圖,將耗時耗力。

PDODB

  1. 為何需使用 PDO

    mysqli_xxx() 一系列的方法,都是針對 mysql 資料庫的操作。

  2. 問題

    公司成立剛開始人數約莫只有 5人,客戶幾百人,使用 mysql 就足夠應付,但公司發展到一規模階段後,比如員工人數有 5000人,用戶從幾千人變成千萬人時,這時 mysql 就不夠足以處理業務上的需求,資料庫需變大,這時如把 mysql DB 更換成容量更大,體積更大,處理能力更強的資料庫,比如升級成 oracle, 但問題是原先的程式都是透過 mysql 處理,所以一旦要變更資料庫,資料庫操作的指令也要改變,如果有多個 php mysql 程式頁面要轉換(例如: 轉成 oracl),將造成一大維護成本問題產生,所以 php 提供了一訪問資料庫通用的方法。

    通過 PHP 操作 PDO, PDO 會將這些操作自動轉換成想要連接的資料庫操作的方法

  3. 描述

    • PDOPHP 資料物件 (PHP Data Object)。
    • PDO 可被視為是一個工具,而這個工具為 PHP 訪問資料庫定義了一個羽量級的一致介面。
    • 實現 PDO 介面的每個資料庫驅動可以公開具體資料庫的特性作為標準擴展功能。
  4. 語法
    ==$pdo=new PDO("DB名稱:host=主機名稱;dbname=DB名","DB帳號","DB密碼");==

  5. 注意

    • 利用 PDO 擴展自身並不能實現任何資料庫功能,必須使用一個具體資料庫的 PDO 驅動來訪問資料庫服務。
    • ==PDO 提供了一個【資料訪問】抽象層,這意味著不管使用哪種資料庫,都可以用相同的函數(方法)來查詢和獲取資料。==
    • PDO 不提供資料庫抽象層,它不會重寫 SQL,也不會模擬缺失的特性。
      如果需要的話,應該使用一個成熟的抽象層。
    • PHP 5.1 開始附帶了 PDO,在 PHP 5.0 中是作為一個PECL 擴展使用。
      PDO 需要 PHP 5 核心的新特性,因此不能在較早版本的 PHP 上運行。
  6. 總結
    PDO 就像是一把槍,而使用哪種資料庫就好比是選擇不同的子彈。
    不管子彈有怎樣的特性,擊發的方法總沒有偏差,都是開槍而已。

    補充:在連接 DB 的時候,並不是每一次的連接都能保證一定完成。因此我們必須設置一個"保險"來説明我們監測連接的情況,這個東西就是

    ==try…catch==機制。

  7. 範例 connDB.php

    header("content-type:text/html;charset=utf-8");
    
    try{
     $pdo  = new PDO("mysql:host=localhost;dbname=class","root","1qaz@wsx");
     $pdo->query("set names utf8");    
    }catch(PDOException $e){
      echo "錯誤";
      echo $e->getMessage();
    }

    在整個 try…catch 結構中,try 部分是可能會出現異常的代碼。
    而當代碼執行的過程當中一旦 try 部分的代碼真的發生了異常,那麼會立即將這個異常拋出,並執行 catch 部分的代碼。
    catch 部分的形參 $e 就是用來接收拋出的異常的。

    可以這樣認為:上述結構是獲取 PDO 時的一個固定結構!

    02pdo對象與mysql數據庫連接.php

singleton 獲取 PDO

  1. 目的: 透過單例模式來確保唯一的 $PDO 對象去操作資料庫。

  2. 說明:
    操作資料庫時,後台的 php 頁面 與 mysql DB 是兩個獨立的東西,資料庫如同一表格,而 php 如同一文件,若要操作資料庫需要借助像是 document 類似的對象,這裡就是之前建立的 $PDO 對象。
    所以要操作資料庫就得透過 $PDO 對象,但如果操作:

    • PHP 要插入資料到資料庫時時,先找到一個 $PDO 對象。
    • PHP 要刪除資料到資料庫時時,再先找到一個 $PDO 對象。
    • 假設此刻資料庫為空,若有兩個對象 $PDO 來操作:
      case1: 一個 $PDO+1, 另一個 $PDO-1, 不會有問題發生。
      case2: 但順序若相反,先做 -1, 但資料庫此刻並無數據(一開始為空),所以操作會失敗。

      意味著若使用兩個 $PDO 對象來操作,結果可能會不相同;因為沒辦法確認哪一個 $PDO 對象會先執行(程序是多執行緒性的)。
      所以解決的唯一辦法,就是指定同一個 $PDO 對象來做操作。

  3. 範例
    03單例模式獲取pdo對像.php

    每次使用 DB 要連上 DB 都要寫一次,太麻煩,所以將其寫成一獨立檔案,改成:

    singletonPDO.php

    然後再來引用,寫成:

    03_1單例模式獲取pdo對像.php

PDO 實現 CRUD 操作

 在上說明中說過,pdo 是一種【資料訪問】層的抽象。
 因此本質上來講在面對同一種 DB 進行操作的時候,pdo 的操作和php 本身直接操作沒有區別。

  1. INSERT、UPDATE、DELETE
    練習: 跟著做以下指令,每寫完一行執行看看並觀看 MySQL 變化,接著註解再執行下行避免干擾

    //新增
    //$sql = "INSERT INTO `students` (`cName`, `cSex`, `cBirthday`, `cEmail`, `cPhone`, `cAddr`, `cHeight`, `cWeight`) VALUES ('王子建', 'F', '1987-04-04', 'wan@gmail.com', '0988265123', '台北市三重市', 176, 65)";
    //$result = $pdo->query($sql);
    //修改
    //$sql = "UPDATE `students` SET `cWeight`=70 WHERE `cName`='王子建'";
    //$result = $pdo->query($sql);
    //刪除
    $sql = "DELETE FROM `students` WHERE `cId`=1";
    $result = $pdo->query(); 
    if(!$result) print_r($dblink->errorInfo($sql)); // 找錯誤問題的方法
  2. SELECT
    接下來,請先塞個大概四筆學生資料。 示範如何查詢,並將資料列印到網頁上。(你可以用 PHP 或是 phpmyadmin 完成。

    $sql = "INSERT INTO `students`(`cName`,`cSex`,`cBirthday`,`cEmail`,`cPhone`,`cAddr`,`cHeight`, `cWeight`) VALUES
      ('簡奉君','F','1987-04-04','elven@superstar.com','0922988876','台北市濟洲北路12號',160,49),
      ('黃靖輪','M','1987-07-01','jinglun@superstar.com','0918181111','台北市敦化南路93號5樓',175,72),
      ('潘四敬','M','1987-08-11','sugie@superstar.com','0914530768','台北市中央路201號7樓',162,65),
      ('賴勝恩','M','1984-06-20','shane@superstar.com','0946820035','台北市建國路177號6樓',178,72)";
    $result = $pdo->query(); 

    接下來有兩種做法分別是 fetchfetchAll,差別在於一個抓(回傳字串)跟全部抓(回傳二維陣列):

    • fetch: 通常你並不知道資料結果會有幾筆,所以你需要用 while 的方式去做。

      //select by fetch
      $result = $pdo->query("SELECT * FROM `students` WHERE 1");
      //不為空
      if (!$result) 
      print_r($pdo->errorInfo()); // 找錯誤問題的方法
      else{
      while ($row = $result->fetch()) {
        print_r($row); echo "<br>";
      }
      }   
    • fetchAll : 會一次全部回傳(用陣列包住回傳),所以這裡會到二維陣列去解讀。

      //select
      $result = $pdo->query("SELECT * FROM `students` WHERE 1");
      if (!$result) print_r($pdo->errorInfo()); // 找錯誤問題的方法
      
      // by fetchAll
      $row = $result->fetchAll();
      print_r($row);
      1. fetchAll 取得的會是二維陣列,跟 fetch 取得的不同。
      2. fetchAll 透過一次全吐出來,優點是快直接處理資料,缺點是你暫存會隨資料多而吃重。
      3. 要注意不管是 fetchfetchAll,被讀取出來後就會被清掉。
      4. fetchAll() 可以直接物件去直接串,一行寫完
      $result = $pdo->query("SELECT * FROM ch5_animal WHERE 1")->fetchAll();
      print_r($result);
  3. 範例
    04pdo實現mysql增刪改操作.php

    本例子,示範如何使用 POSTMAN 來測試 04pdo實現mysql增刪改操作.php

    新增、刪除、修改執行上沒問題,但查詢的結果是一個不可見的值,而且這不可見的值,不知如何處理,這裡需借用 PDO 預處理 prepare 和交易處理 transaction

SQL 注入攻擊

SQL 注入 (SQL injection),也稱 SQL 隱碼SQL 注碼,為駭客利用超全域變數的表單提交時,透過變數欄位進行 SQL 語言變化導致,使得在伺服器端進行不預期的 SQL 資料指令。

  1. 攻擊範例: 帳號登入

    1. 登入資料表如下:

      nano login.php

      CREATE DATABASE IF NOT EXISTS `login` DEFAULT CHARACTER SET utf8mb4  COLLATE utf8mb4_unicode_ci;
      USE `login`;
      CREATE TABLE `users` (
      `id` int(11) NOT NULL,
      `username` varchar(10) NOT NULL DEFAULT '',
      `password` varchar(6) NOT NULL DEFAULT '',
      `email` varchar(50) NOT NULL DEFAULT ''
      ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
      INSERT INTO `users` (`id`, `username`, `password`, `email`) VALUES
      (1, 'admin', '1234', 'admin@gmail.com'),
      (2, 'test',  'test', 'test@gmail.com');
      
      ALTER TABLE `users`
      ADD PRIMARY KEY (`id`);
    2. 伺服器之帳密檢查方式,如下:

      <?php
      :
      function select($table, $cond){
        global $pdo;
        return $pdo->query("SELECT * FROM {$table} WHERE {$cond}")->fetchAll();
      }
      
      $result=select("login","username= { $_POST['username'] and password= {$_POST['password']} ");
      if($result->rowCount()){ //有找到此帳號密碼
        $_SESSION['admin']=$_POST['username'];
        header("location: admin.php");
      }
      else{ 
        echo "<script>alert('帳號或密碼錯誤');</script>";
      }
      :
      ?>

      完整範例: login.php

    3. 此時輸入提交驗證
      帳號admin, 密碼1111' OR 1=1;/*

      結果變成了:

      select * from users where username='admin' and password='1111' OR 1=1;/'
    4. 進而獲得登入之許可

      另外或是當駭客或工程師在已知你的資料結構時,也能夾帶 SQL

      帳號: admin
      密碼: 1111' ; DROP TABLE users;/*

      結果變成了:

      select * from users where username='admin' and password='1111';DROP TABLE users;/*'
  2. 防範方式:

    1. 替換符號
      最大的問題是透過 '" 開頭所影響,透過 preg_replace() 將變數內(含陣列)任何出現的單雙引號都強迫拿掉。

      //下面會把所有字元 ' 與 " 過濾掉
      $_POST = preg_replace("/['"]+/", '', $_POST);
      $result=select("users","username='".$_POST['username']."' and password='".$_POST['password']."'");
      
      //過濾為: select * from users where username='admin' and password='1111;DROP TABLE users;/*'

      相對來說每次跑 SQL 之前都需要替換檢查過。

      完整範例: login_sol1.php

    2. PDOprepare
      透過 prepare 的方式,對 SQL 進行準備告知我有這行指令要執行。
      execute() 等於執行,與 query() 不同的是 execute 都會伴隨 prepare(預告)來使用。

      1. 將原本的 sql 指令分解為那些不先寫好,使用 ? 來代替。
      2. ? 這些真正的內容,用一維陣列即可,順序根據 ? 出現順序為準。
        由於是透過自訂函式代替 SELECT,所以步驟 2 也是要提交 FUNCTION

        //for SQL Injection
        $data=array($_POST['username'], $_POST['password']);
        $result = selectV2("users", "username=? and password=?", $data);
        
        if($result){//有找到此帳號密碼
        $_SESSION['admin']=$_POST['username'];
        plo("admin.php");
        }
        else echo "<script>alert('帳號或密碼錯誤');".jlo("login.php")."</script>";
      3. 關於自訂函式,prepare 預告後還要用個變數將物件存著。
      4. ? 將你的 ? 轉換陣列丟給 execute() 做替換執行。
      5. 最後做 fetchAll 作業(無法簡化為 1 行寫完)並回傳結果

        //selet SQL v2 for SQL injection
        function selectV2($table, $where, $data){
        global $pdo;
        $sql = "select * from " . $table . " where " . $where;
        $stmt=$pdo->prepare($sql);
        $stmt->execute($data);
        return $stmt->fetchAll();
        }
      6. 透過 prepare 由於 sql 代碼會並固定住,駭客就無法將原語意誘騙為不同的內容。

      完整範例: login_sol2.php

    3. 結論
      嚴格上 PDO 的方式會比較好一些,SQL 不會受到任何暴力的 SQL 注入類型所干擾。而 resplace 只是將符號替換,有可能部分資料輸入就剛好需此符號。

XSS(Cross-site scripting) 跨網站指令碼-攻擊

是一種網站應用程式的安全漏洞攻擊,是代碼注入的一種。它允許惡意使用者將程式碼注入到網頁上,其他使用者在觀看網頁時就會受到影響。這類攻擊通常包含了 HTML 以及使用者端手稿語言。

  1. 範例: 跨網站指令碼-攻擊

    1. 使用表單方式產生資料 xss.php 請在標題輸入框分別輸入底下內容試試(XSS 攻擊),若是無法順利寫入,那就是有問題:

      <?php
      // 告訴瀏覽器這頁面是 UTF-8 編碼
      header("content-type:text/html; charset=utf-8");
      date_default_timezone_set("Asia/Taipei");
      
      if (isset($_POST["action"]) && $_POST["action"] == "add") {
        // require_once "connDB.php";
        $user = "root";
        $password = "1qaz@wsx";
        $host = "localhost:3306";
        $db = "blog";
        $port = "3306";
      
        $conn = mysqli_connect($host, $user, $password);
        if ($conn) {
            //選擇資料庫
            mysqli_select_db($conn, $db);
            //設置資料庫編碼方式
            mysqli_query($conn, 'set names utf8');
            mysqli_set_charset($conn, "utf8");
        } else {
            echo "資料庫連線失敗";
        }
      
        $sql_query = "INSERT INTO `articles`(`title`,`content`) VALUES(";
        $sql_query .= "'" . $_POST["title"] . "',";
        $sql_query .= "'" . $_POST["content"] . "')";
        // echo $sql_query;
        // exit;
        mysqli_query($conn, $sql_query);
        mysqli_close($conn); //關閉資料庫連接
        // header("location: xss.html");
      }
      ?>
      
      <!DOCTYPE html>
      <html lang="en">
      
      <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      </head>
      
      <body>
      <center>
        <h2>XSS 示範</h2>
        <form action="xss_create.php" method="post">
          <p>
            <label for="title"> 文章標題 </label>
            <input type="text" name="title" placeholder="請輸入文章標題" value="" />
          </p>
      
          <p>
            <label for="content"> 文章內容 </label>
            <textarea name="content" placeholder="請輸入文章內容" rows="10"></textarea>
          </p>
          <p>
            <input type="hidden" name="action" value="add">
            <button type="submit">送出</button>
          </p>
        </form>
      </center>
      </body>
      </html>
      <script>alert('這就是XSS攻擊')</script>
      <img src="https://source.unsplash.com/rndom/320x200">

      20230104073454

    2. 改用 xss_pdo.phpPDOprepare()方式寫資料到資料庫

      <?php
      // 告訴瀏覽器這頁面是 UTF-8 編碼
      header("content-type:text/html; charset=utf-8");
      date_default_timezone_set("Asia/Taipei");
      
      $dbhost = 'localhost'; //一般是 localhost 或 127.0.0.1
      $dbuser = 'root'; //一般是 root
      $dbpasswd = '1qaz@wsx';
      $dbname = 'blog';
      $dbcharacter = 'utf8mb4'; //一般是 utf8
      
      if (isset($_POST["action"]) && $_POST["action"] == "add") {
        try {
            $db = new PDO("mysql:host={$dbhost};dbname={$dbname};charset={$dbcharacter}      ", $dbuser, $dbpasswd);
            $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); //禁用prepared       statements的模擬效果
            $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //讓資料庫顯示錯      誤原因
            // echo "連線成功";
        } catch (PDOException $e) {
            die("無法連上資料庫:" . $e->getMessage());
        }
      
        //$title = $_POST["title"];
        //過濾顯示變數
        //$title = filter_var($news['title'], FILTER_SANITIZE_SPECIAL_CHARS);
        //$content = $_POST["content"];
        //過濾顯示變數
        //$content = filter_var($news['content'], FILTER_SANITIZE_SPECIAL_CHARS);
      
        // 寫入資料庫
        $sql = "INSERT INTO `articles`
      (`title`, `content`)
      VALUES(?, ?)";
        $sth = $db->prepare($sql);
        $values = [
            $_POST['title'],
            $_POST['content'],
        ];
        $sth->execute($values);
      }
      ?>
      <!DOCTYPE html>
      <html lang="en">
      
      <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      </head>
      
      <body>
      <center>
        <h2>XSS 示範</h2>
        <form action="xss_pdo.php" method="post">
          <p>
            <label for="title"> 文章標題 </label>
            <input type="text" name="title" placeholder="請輸入文章標題" value="" />
          </p>
      
          <p>
            <label for="content"> 文章內容 </label>
            <textarea name="content" placeholder="請輸入文章內容" rows="10"></textarea>
          </p>
          <p>
            <input type="hidden" name="action" value="add">
            <button type="submit">送出</button>
          </p>
        </form>
      </center>
      </body>
      
      </html>

      20230104074108

      20230104074752

    3. 在去以 xss_read.phpxss_read_pdo.php 讀資料庫內容
  2. 過濾顯示變數

    1. 讀出的資料,要考量是否允許使用 HTML
    2. 若是允許使用 HTML,那可能要用專業的工具來做,如:http://htmlpurifier.org/
    3. 若是不允許使用 HTML,可以直接使用內建的 htmlspecialchars($string) 來過濾
    4. 但內建的 htmlspecialchars($string) 預設只轉化雙引號,不對單引號做轉義,所以,這樣用 htmlspecialchars($string,ENT_QUOTES) 更好:

      $title = htmlspecialchars($news['title'], ENT_QUOTES);

      另一個和 htmlspecialchars 很像的 htmlentities 函數並不適用中文,因為會連同中文字一起轉義。

    5. PHPfilter_var 過濾器亦有同樣效果(還更彈性一些):

      $title = filter_var($news['title'], FILTER_SANITIZE_SPECIAL_CHARS);
  3. PHPfilter_var 過濾器

    1. 可利用 PHP 內建的 filter_var() 函數來過濾變數。
    2. 幾種常用過濾方法,完整過濾器可由此查看:

      名稱 功用
      FILTER_CALLBACK option可以讓開發者用自訂的function處理
      FILTER_SANITIZE_STRING 去除標籤或特殊字元(html標籤會直接被消除)
      FILTER_SANITIZE_ENCODED 與urlencode()相同,過濾特殊字串
      FILTER_SANITIZE_MAGIC_QUOTES 過濾針對SQL injection做過濾(例如單、雙引號)
      FILTER_SANITIZE_SPECIAL_CHARS 針對HTML做encoding,例如<會轉成<
      FILTER_SANITIZE_EMAIL 過濾e-mail,刪除e-mail格式不該出現的字元(除了$-_. +!*'{} ^~[]`#%/?@&=和數字),例如a(b)@gmail.com會被過濾成ab@gmail.com
      FILTER_SANITIZE_URL 過濾URL,刪除URL格式不該出現的字元
      FILTER_SANITIZE_NUMBER_INT 刪除所有字元,只留下數字與+-符號
      FILTER_SANITIZE_NUMBER_FLOAT 刪除所有字元,只留下數字和+-.,eE
      FILTER_VALIDATE_INT 判斷數字是否有在範圍內
      FILTER_VALIDATE_BOOLEAN 判斷布林值,1、true、on、yes都會判斷成true,反之為 false,若是這些以外的值會回傳NULL
      FILTER_VALIDATE_FLOAT 判斷是否為浮點數
      FILTER_VALIDATE_REGEXP 利用regexp做驗證
      FILTER_VALIDATE_URL URL驗證
      FILTER_VALIDATE_EMAIL e-mail驗證
      FILTER_VALIDATE_IP IP驗證
  4. 過濾數字

    1. 一般過濾數字用 (int) $xxxintval($xxx) 即可,會強制把輸入的內容變成數字。
    2. 亦可用 filter_var($num, FILTER_SANITIZE_NUMBER_INT) 來過濾,但和 intval 不太一樣,例如:

      $num = "123其他文字456";
      echo filter_var($num, FILTER_SANITIZE_NUMBER_INT) . "<br>";
      echo (int) $num . "<br>";
      echo intval($num) . "<br>";
    3. (int) $xxxintval($xxx) 會得到 123,但用 filter_var() 會得到 123456
  5. 將欲呈現變數過濾

    例如:

    while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
       echo (int) $news['id'] . "<br>";
       echo filter_var($news['title'], FILTER_SANITIZE_SPECIAL_CHARS) . "<br>";
       echo filter_var($news['content'], FILTER_SANITIZE_SPECIAL_CHARS) . "<br>";
    }
  6. 改寫 xss_read.phpxss_read_pdo.php 加入過濾 xss_read_filter_var.php

5. PDO 異常處理 Exception

  Exception 指的都是在執行 db 操作的時候發生異常,例如 SQL 語句異常或語法錯誤。而如果是 DB
連接發生的錯誤則不會走本異常處理,而是直接由 pdo 輸出連接失敗。

異常處理 Exception 是指在 try…catch 時發生異常時的處理手段,通常異常處理都是直接拋出提醒即可。而設置提醒的手段有三種設置方式:

  • 預設模式
    主要依賴於系統提供的 errorCodeerrorInfo 屬性實現
  • 警報模式:
    pdo 設置 setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_WARNING);
  • 中斷模式:
    pdo 設置 setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

需要說明的是異常處理並不是三種模式必須有一種顯式的去表現出來,哪怕一種都不主動寫出也不會認為是違法。只不過主動實現異常處理能夠在異常發生的時候給予我們更好的提示,因此推薦如果允許的情況下盡可能的添加異常處理模組代碼。

  • 範例: 05pdo異常捕獲.php

    注意: 即使 SQL 語句執行成功,但沒更新資料(例:無須改變),仍會回傳 false。

6. PDO 預處理 prepare

   預處理語句 preparepdo 提供的一種 db 操作方式。其語言邏輯與正常的 pdo 訪問相同。但區別在於 prepare 語句允許用戶在【設置sql語句】與【執行sql語句】之間部分進行參數的注入與提取操作,而不是像正常的pdo訪問一樣直接將參數寫死。

  • 正常 pdo 直接訪問程序
    【設置 sql 語句】→【執行 sql 語句】

    $sql = "SQL 語句";
    $pdo->exec($sql);
  • 預處理訪問程序
    【設置 sql 語句】→ 【執行 sql 語句】【處理sql中參數】 → 【執行sql語句】

    $sql = "SQL 語句";
    $pdo->prepare();

    prepare 是另一種像是 $PDO 提供資料庫操作 exec 的另一種方法,只是部分功能,得到的是一個 PDOStatement 物件,這個物件是可定義與執行參數化的 SQL 命令 。
    預先處理得到一種結果,再用這結果執行另一新方法,這新方法可處理 sql 中所需參數,然後最後再執行 $pdo->exec($sql)

  • 區別
    在於再處理參數時,預處理可以將當初用 $pdo->exec(); 處理上,看不到,拿不到,獲取不到的內容,通過處理參數後可取得。

    處理分三部分

    1. 關鍵核心: prepare()處理 sql 語句execute() 處理 sql 中參數 兩種方法,
    2. bindValue()
    3. bindColumn()

6.1 prepare()方法和execute()方法

  1. 描述:

    • prepare() 方法為預處理 sql 語句的方法,能夠讓 pdo 預先處理【半成品的】sql 語句。
      並生成一個 PDOStatementObject 類型的結果。

    • execute() 方法是提供給 PDOStatementObject 類型物件去執行的【成品】sql 語句的方法並生成一個 PDOStatementObject 類型的結果。
  2. 說明:

    • 交由 pdoprepare 預處理的【半成品】sql 語句,使用 ? 問號作為預留位置,表示待傳參的參數

    • prepare 預處理必須只能處理【半成品】sql 語句,如果是完整則需要使用 exec 方法執行

    • execute() 方法允許一個陣列作為參數,將參數帶入到預處理的 sql 語句中,並且會將結果存放到 PDOStatementObject 對象中。

    • PDOStatementObject 物件在預處理的不同階段有著不同的含義!!不可混淆,必須根據上下文判斷。
  3. 語法:

    require_once "singletonPDO.php";
    
    $sql = "INSERT INTO students (`cName`,`cSex`,`cBirthday`,`cEmail`,`cPhone`,`cAddr`,`cHeight`,`cWeight`)values(?,?,?,?,?,?,?,?)";
    $stmt = $pdo->prepare($sql);
    echo $stmt->execute(array('allen','m','1990-01-02','test@gmail.com','098876543','台北市','180', '80'));

    輸出: 1。 如果新增成功

    上面寫法等同:

    require_once "singletonPDO.php";
    
    $pdo->prepare("INSERT INTO students (`cName`,`cSex`,`cBirthday`,`cEmail`,`cPhone`,`cAddr`,`cHeight`,`cWeight`) values(?,?,?,?,?,?,?,?)")->execute(array('allen', 'm', '1990-01-02', 'test@gmail.com', '098876543', '台北市', '180', '80'));

6.2 bindColumn()方法

  • 描述:
    bindColumn() 方法允許將執行結果的一列資料綁定到一個指定物件上,本方法需要在 execute() 方法執行結束後在執行。

  • 語法:$pdoStatementObject->bindColumn(index,指定變數);

  • 說明:

    • 第一個參數表示給 sql 語句中第幾個參數傳值。第一個就寫 1,以此類推。
    • 第二個參數表示給 sql 語句中的對應參數傳的具體的值。
    • bindColumn() 一次綁定一個參數,如需綁定多個,則執行多次即可。
  • 範例

    06pdo預處理語句.php

  • 練習: 修改原 06pdo預處理語句.phpSQL 插入操作,變成查詢 06pdo預處理語句_查詢.php:

    06pdo預處理語句_查詢.php

3. bindValue()方法

  1. 前言:
    之前的方式,SQL 查詢中,使用 prepare()SQL 語句預處理,得到的半成品,再利用 execute() 傳遞所需參數,同時轉換得到成品,但讀不到內容,將結果中的內容透過 bindColumn() 來綁定資料在指定變數上。

    若不想使用 execute() 來傳遞參數,可以改用 bindValue() 來給半成品的 SQL 語句進行傳值。

  2. 描述:
    bindValue() 方法是提供給 pdo 預處理之後得到的 PdoStatementObject 物件使用的方法,用來給【半成品】的 sql 語句進行傳值。

  3. 語法:$pdoStatementObject->bindValue(index,value);

  4. 說明:

    • 第一個參數表示給 sql 語句中第幾個參數傳值。第一個就寫 1,以此類推。
    • 第二個參數表示給 sql 語句中的對應參數傳的具體的值。
    • bindValue() 一次綁定一個參數,如果有多個則需要調用多次。
  5. 例子:

    $sql = "INSERT INTO students values(?,?,?,?,?,?,?,?,?)";
    $pdoStatementObject = $pdo->prepare($sql);
    $halfPro->bindValue(1, 12);
    $halfPro->bindValue(2, 'allen');
    $halfPro->bindValue(3, 'm');
    $halfPro->bindValue(4, '2019-08-01');
    $halfPro->bindValue(5, 'test@gmail.com');
    $halfPro->bindValue(6, '098876543');
    $halfPro->bindValue(7, '台北市');
    $halfPro->bindValue(8, '170');
    $halfPro->bindValue(9, '68');
    echo $pdoStatementObject->execute();
  6. 完整案例: 07pdo補充bindValue方法.php

    這種方式好處,是傳遞的值可以寫成一變數,提供了一種更靈活的方式,來編輯 sql 語句。

7. PDO 交易處理 transaction

  • 交易:多個事件組成的結構。
  • 事件:事件實際上就是預處理語句執行的execute語句

    即多個 execute 子句

  • 注意:

    1. 整個交易操作必須放到 try...catch 中,這是因為我們並不能保證執行的事件一定成功。
      而對於整個交易而言,任何一個事件的失敗都會導致 catch 的觸發。
      catch 觸發就意味著必須將之前做出的所有的操作都必須還原回滾操作: $pdo->rollBack()

    2. 動作陳述式(CRUW)必須在交易開啟之後執行,在交易提交之前停止.
      開啟交易:$pdo->beginTransaction();
      關閉交易:$pdo->commit();

    3. 中文處理方案(避免亂碼):
      讀取:$pdo->query("set names utf8");
      插入:$pdo->exec('set names utf8');

    4. 銀行轉帳:

      • 提錢 -> 轉多少 -> 輸入密碼 -> 成功 -> 轉帳成功
      • 提錢 -> 轉多少 -> 輸入密碼 -> 錯誤 -> 轉帳失敗

      只有成功,才允許轉錢出去。

  • 交易目的:保證只有在交易成功才會有影響發生,否則不會有任何異動

    require_once "singletonPDO.php";
    $pdo = singletonPDO::getpdo();
    $pdo->query('set names utf8');
    $pdo->exec('set names utf8');
    try{
     $pdo->beginTransaction();
     $sql = "update students set cHeight=?, cWeight=? where cID=?";
     $pdoso = $pdo->prepare($sql);
    
     //成功
     $pdoso->execute(array('170', '60', 1));
     //失敗,參數不符
     $pdoso->execute(array('180', '60'));
    
     echo $pdo->commit();
    }catch (PDOException $e){
     $pdo -> rollBack();
     echo "失敗";
    }
  • 範例: 08pdo交易處理transaction方法.php

    執行第二條 execute 語句(第二個事件),沒給 cID, 不足數據,所以會觸發 Exception,本次交易發生異常。

    此錯誤為操作錯誤,屬於第一類異常,執行到這裏有問題,就該中斷停止繼續往下執行,異常需設中斷模式,在單例模式產生 $PDO 就要加入 $pdo ->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);,參考 singletonPDO.php

8. 學生資料管理系統 crud ptocessed by pdo

最後修改日期: 2023 年 11 月 22 日

作者

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。