展開文件目錄

資料庫

你的PHP程式碼常常需要使用資料庫來保存資訊,這時,你有一些選擇來連接並與你的資料庫互動,在PHP 5.1.0之前的推薦方案是使用一些原生的驅動程式,例如mysqli, pgsql, mssql等等。

如果你在應用程式中只使用 一個 資料庫,原生的驅動程式(native driver)是一個很好的選擇。但是,假如你使用了MySQL和一點點的MSSQL,或是你需要連接到Oracle資料庫,那麼你就不能只使用同一個驅動程式,你將需要針對每個不同的資料庫學習API — 而這些行為非常的愚蠢。

MySQL 擴充套件

PHP的mysql擴充套件已經不再主動的開發,官方也在PHP 5.5.0時不贊成使用,意思是mysql擴充套件將在接下來的幾個釋出版本移除。如果你正在使用類似mysql_connect()mysql_query()這些mysql_*開頭的函式,這些函式將在未來的幾個版本不能使用了。這代表你將需要重寫程式,所以最好的選擇就是開始使用 mysqli 或是 PDO

如果你正要開始寫程式,那麼絕對不要使用 mysql 擴充套件: 請轉而使用MySQLi extension或是使用PDO

PDO 擴充套件

PDO 是從 — PHP 5.1.0 — 可以開始使用的一個和資料庫連結的抽象程式庫,它提供了一個共同的介面來和不同的資料庫溝通。例如,你可以使用同一套程式碼和MySQL或是SQLite溝通。

// PDO + MySQL
$pdo = new PDO('mysql:host=example.com;dbname=database', 'user', 'password');
$statement = $pdo->query("SELECT some\_field FROM some\_table");
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo htmlentities($row['some_field']);

// PDO + SQLite
$pdo = new PDO('sqlite:/path/db/foo.sqlite');
$statement = $pdo->query("SELECT some\_field FROM some\_table");
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo htmlentities($row['some_field']);

PDO不會轉譯你的SQL請求,或是模擬失去的特性。它只是簡單的使用同一個API去連向各個不同類型的資料庫。最重要的是, PDO 允許你簡單的直接將外部的輸入插進你的SQL請求,而不需要擔心SQL注入攻擊(SQL injection attacks)。讓我們假設一個PHP腳本接收一個數字ID當作請求參數。這個ID是用來從資料庫取得使用者的資料。這是錯誤的使用方式。

<?php
$pdo = new PDO('sqlite:/path/db/users.db');
$pdo->query("SELECT name FROM users WHERE id = " . $_GET['id']); // <-- 不!

這是一個很糟糕的程式碼,你正在插入一個未處理過的請求參數到資料庫請求中。這會導致你在一瞬間被駭入。用一個實際的案例來解釋這個情況:資料庫注入攻擊。想像一下有一個駭客使用一個網址類似這樣http://domain.com/?id=1%3BDELETE+FROM+users。這將會造成 $_GET['id'] 變數的值變成 1;DELETE FROM users ,這會刪除所有你的使用者!因此你應該要使用PDO bound parameters對這個ID輸入消毒。

<?php
$pdo = new PDO('sqlite:/path/db/users.db');
$stmt = $pdo->prepare('SELECT name FROM users WHERE id = :id');
$stmt->bindParam(':id', $_GET['id'], PDO::PARAM_INT); // <-- PDO自動消毒這個變數
$stmt->execute();

這個是正確的程式碼,它在PDO敘述上使用了bound parameter。這些動作在被放入資料庫請求之前逃脫了外部的ID輸入以避免潛藏的資料庫注入攻擊。

你應該要注意到資料庫連線沒有自行關閉而耗盡資源的問題,這個問題不是沒有前例的。當使用PDO的時候,你可以藉由摧毀物件來自行結束連線,以確保所有關聯到這個物件的東西都被刪除。例如,將它的值設為NULL。如果你沒有明確的將它關閉,那麼PHP會在腳本結束時自動的關閉連線,除非你使用的是持續的連線。

與資料庫溝通

當開發者們開始學習PHP,他們常常將資料庫溝通的動作與他們的表達邏輯混合,而使得程式碼長得像這樣:

<ul>
<?php
foreach ($db->query('SELECT * FROM table') as $row) {
    echo "<li>".$row['field1']." - ".$row['field1']."</li>";
}
</ul>

這是一個很糟的例子,主要是因為它很難除蟲,很難測試,很難閱讀,而且沒有限制的話,它將會輸出過多資料。

而有非常多的方法可以達到同樣的目的,一切都看你喜歡哪種,物件導向 或是 函數編程 - 但是一定要有基本的分離。

考慮以下最基本的步驟:

<?php
function getAllFoos($db) {
    return $db->query('SELECT * FROM table');
}

foreach (getAllFoos($db) as $row) {
    echo "<li>".$row['field1']." - ".$row['field1']."</li>"; // 不好!!
}

這是一個好的開始,將這兩個程式碼放在不同的兩個檔案,你將會得到一些乾淨的分離。

創造一個類別來放置這個方法,你將會得到一個 "Model",再創造一個簡單的 .php 檔案來放入表達的邏輯,你將會得到一個 "View",這非常接近 MVC - 一個對多數的 frameworks 非常普遍的物件導向架構。

foo.php

<?php

$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');

// 使你的model可以被使用
include 'models/FooModel.php';

// 創造一個實例
$fooList = new FooModel($db);

// 展示你的view
include 'views/foo-list.php';

models/FooModel.php

<?php
class Foo()
{
    protected $db;

    public function __construct(PDO $db)
    {
        $this->db = $db;
    }

    public function getAllFoos() {
        return $this->db->query('SELECT * FROM table');
    }
}

views/foo-list.php

<? foreach ($fooList as $row): ?>
    <?= $row['field1'] ?> - <?= $row['field1'] ?>
<? endforeach ?>

這是對許多現代的frameworks非常基本的架構,你不需要每次都做這些,但是將表達邏輯和資料庫溝通的部分混合,在想要做單元測試的情況下,這是一個非常真實的問題

PHPBridge 有個叫 Creating a Data Class 的資源來說明非常相似的主題,而且對剛開始接觸這些概念的程式開發者而言,這是一個不錯的開始。

抽象層

許多的framework提供他們自己的抽象層,有些會建立在PDO的上層,這些抽象層常常會藉由將你的資料庫請求包裝起來,以模擬一些非你正在使用的資料庫所擁有的特色,提供你一個真正的資料庫抽象,而不是僅僅是PDO所提供的連線抽象。這些抽象層當然地會造成一些小小的效能負擔,但是你如果你正在建立一個應用程式需要與MySQL, PostgreSQL和SQLite的時候,這些代價來交換程式碼的乾淨是值得的。

有些抽象層是建立在 PSR-0PSR-4 的命名空間標準,所以可以被安裝在任何你喜歡的應用程式裡。