本文舉例說明了創建可配置PHP 應用程式的幾種方法。文中也探討了應用程式中理想的配置點,並在應用程式過度可配置和過度封閉之間尋求一個平衡點。
如果計劃讓其他人或公司可以使用您的PHP 應用程序,則需要確保該程式是可設定的。至少,要允許使用者以一種安全的方式設定資料庫登入及密碼,從而使其中的資料不會對外公開。
本文展示了幾種用於儲存配置設定及編輯這些設定的技術。另外,文中也為哪些元素需要設為可配置以及如何避免陷入配置過度或配置不足的困境提供了指導。
使用INI 檔案進行設定
PHP 內建了對設定檔的支援。這是透過php.ini 檔案這樣的初始化檔案(INI)機制實現的,在php.ini 檔案中定義了資料庫連接逾時或會話如何儲存等常數。如果願意的話,可以在這個php.ini 檔案中為應用程式定製配置。為了說明,我將下列程式碼行新增到php.ini 檔案中。
myapptempdir=foo
然後,我寫了一個小PHP 腳本來讀取這個配置項,如清單1 所示。
清單1. ini1.php
<?php
function get_template_directory()
{
$v = get_cfg_var( "myapptempdir" );
return ( $v == null ) ? "tempdir" : $v;
}
echo( get_template_directory()."n" );
?>
當在命令列中執行這段程式碼時,得到以下結果:
% php ini1.php
foo
%
太棒了。但為什麼不能用標準的INI 函數來取得myapptempdir 配置項目的值呢?我研究了一下,發現在大多數情況下,定製配置項不能使用這些方法來取得。然而,使用get_cfg_var 函數卻是可以存取的。
要使這個方法更加簡單,將對變數的存取封裝在第二個函數中,該函數使用配置鍵名及一個缺省值作為參數,如下所示。
清單2. ini2.php
function get_ini_value( $n, $dv )
{
$c = get_cfg_var( $n );
return ( $c == null ) ? $dv : $c;
}
function get_template_directory()
{
return get_ini_value( "myapptempdir", "tempdir" );
}
這是對如何存取INI 檔案的一個很好的概括,所以,如果要使用一個不同的機製或將這個INI 檔案儲存到其他位置,就不需要為更改大量的函數而大費周折。
我不推薦使用INI 檔案作為應用程式的配置,這有兩個理由。首先,雖然這樣做較容易讀取INI 文件,但幾乎不可能安全地寫INI 文件。所以這樣做只適合只讀配置項。第二,php.ini 檔案在伺服器的所有應用程式上共享,所以我認為特定於應用程式的設定項不應該寫在該檔案中。
需要對INI 文件了解什麼呢?最重要的是如何重置include 路徑來新增配置項,如下所示。
清單3. ini3.php
<?php
echo( ini_get("include_path")."n" );
ini_set("include_path",
ini_get("include_path").":./mylib" );
echo( ini_get("include_path")."n" );
?>
在本例中,我將我的本機mylib 目錄加入了include 路徑中,所以能夠從該目錄中require PHP 文件,而不需要將該路徑加入require 語句中。
PHP 中的配置
通常對於在INI 檔案中儲存配置條目的一個替代方法是使用一個簡單的PHP 腳本來保持資料。如下是一個範例。
清單4. config.php
<?php
# Specify the location of the temporary directory
#
$TEMPLATE_DIRECTORY = "tempdir";
?>
使用該常數的程式碼如下所示。
清單5. php.php
<?php
require_once 'config.php';
function get_template_directory()
{
global $TEMPLATE_DIRECTORY;
return $TEMPLATE_DIRECTORY;
}
echo( get_template_directory()."n" );
?>
程式碼先包含設定檔(config.php),接著就可以直接使用這些常數了。
使用這項技術有很多優勢。首先,如果某些人只是瀏覽config.php 文件,則該頁面是空白的。所以可以將config.php 放到相同的檔案中,並當作Web 應用程式的根。第二,在任何編輯器中都可編輯,並且在某些編輯器中甚至具備語法著色及語法檢查功能。
這項技術的缺點是,這是像INI 檔案一樣的唯讀技術。將資料從此文件中提取出來是輕而易舉的,但在該PHP 檔案中調整資料卻很困難,在某些情況下甚至是不可能的。
下面的替代方法顯示瞭如何編寫在本質上既可讀又可寫入的配置系統。
文字檔案
前面的兩個例子對於只讀配置條目都是合適的,但對於既讀又寫的配置參數來說又如何呢?首先,看看清單6 中的文字設定檔。
清單6. config.txt
# My application's configuration file
Title=My App
TemplateDirectory=tempdir
這是同INI 檔案相同的檔案格式,但我自己編寫了輔助工具。為此,我創建了自己的Configuration 類,如下所示。
清單7. text1.php
<?php
class Configuration
{
private $configFile = 'config.txt';
private $items = array();
function __construct() { $this->parse(); }
function __get($id) { return $this->items[ $id ]; }
function parse()
{
$fh = fopen( $this->configFile, 'r' );
while( $l = fgets( $fh ) )
{
if ( preg_match( '/^#/', $l ) == false )
{
preg_match( '/^(.*?)=(.*?)$/', $l, $found );
$this->items[ $found[1] ] = $found[2];
}
}
fclose( $fh );
}
}
$c = new Configuration();
echo( $c->TemplateDirectory."n" );
?>
程式碼首先建立了一個Configuration 物件。這個建構子接下來讀取config.txt 並用解析過的檔案內容來設定局部變數$items。
腳本隨後尋找TemplateDirectory,並沒有在物件中直接定義。因此,使用設定成'TemplateDirectory' 的$id 來呼叫神奇的__get 方法,__get 方法針對該鍵傳回$items 陣列中的值。
這個__get 方法是特定於PHP V5 環境,所以此腳本必須在PHP V5 下運作。實際上,本文中所有的腳本都需要在PHP V5 下運作。
當在命令列執行此腳本時,能看到下列結果:
% php text1.php
tempdir
%
一切都在預料之中,該物件讀取config.txt 文件,然後為TemplateDirectory 配置項目獲得正確的值。
但對於設定一個配置值,該怎麼做呢?在此類中建立一個新方法及一些新的測試程式碼,就能夠得到這個功能,如下所示。
清單8. text2.php
<?php
class Configuration
{
...
function __get($id) { return $this->items[ $id ]; }
function __set($id,$v) { $this->items[ $id ] = $v; }
function parse() { ... }
}
$c = new Configuration();
echo( $c->TemplateDirectory."n" );
$c->TemplateDirectory = 'foobar';
echo( $c->TemplateDirectory."n" );
?>
現在,有了一個__set 函數,它是__get 函數的「堂兄弟」。函數並不會為一個成員變數取得值,當要設定一個成員變數時,才會呼叫這個函數。底部的測試程式碼設定值並列印出新值。
以下是在命令列中執行此程式碼時出現的結果:
% php text2.php
tempdir
foobar
%
太好了!但如何能將它儲存到文件中,從而將使這個改動固定下來呢?為此,需要寫入檔案並讀取它。用於寫入檔案的新函數,如下所示。
清單9. text3.php
<?php
class Configuration
{
...
function save()
{
$nf = '';
$fh = fopen( $this->configFile, 'r' );
while( $l = fgets( $fh ) )
{
if ( preg_match( '/^#/', $l ) == false )
{
preg_match( '/^(.*?)=(.*?)$/', $l, $found );
$nf .= $found[1]."=".$this->items[$found[1]]."n";
}
else
{
$nf .= $l;
}
}
fclose( $fh );
copy( $this->configFile, $this->configFile.'.bak' );
$fh = fopen( $this->configFile, 'w' );
fwrite( $fh, $nf );
fclose( $fh );
}
}
$c = new Configuration();
echo( $c->TemplateDirectory."n" );
$c->TemplateDirectory = 'foobar';
echo( $c->TemplateDirectory."n" );
$c->save();
?>
新的save 函式巧妙地操作config.txt。我並沒有僅用更新過的配置項重寫文件(這樣會移除掉註釋),而是讀取了這個文件並靈活地重寫了$items 數組中的內容。這樣的話,就保留了文件中的註解。
在命令列運行該腳本並輸出文字設定檔中的內容,能夠看到下列輸出。
清單10. 保存函數輸出
% php text3.php
tempdir
foobar
% cat config.txt
# My application's configuration file
Title=My App
TemplateDirectory=foobar
%
原始的config.txt 檔案現在被新值更新了。
XML 設定檔
儘管文字檔易於閱讀及編輯,但卻不如XML 檔案流行。另外,XML 有許多適用的編輯器,這些編輯器能夠理解標記、特殊符號轉義等等。所以設定檔的XML 版本會是什麼樣的呢?清單11 顯示了XML 格式的設定檔。
清單11. config.xml
<?xml version="1.0"?>
<config>
<Title>My App</Title>
<TemplateDirectory>tempdir</TemplateDirectory>
</config>
清單12 顯示了使用XML 來裝載配置設定的Configuration 類別的更新版。
清單12. xml1.php
<?php
class Configuration
{
private $configFile = 'config.xml';
private $items = array();
function __construct() { $this->parse(); }
function __get($id) { return $this->items[ $id ]; }
function parse()
{
$doc = new DOMDocument();
$doc->load( $this->configFile );
$cn = $doc->getElementsByTagName( "config" );
$nodes = $cn->item(0)->getElementsByTagName( "*" );
foreach( $nodes as $node )
$this->items[ $node->nodeName ] = $node->nodeValue;
}
}
$c = new Configuration();
echo( $c->TemplateDirectory."n" );
?>
看起來XML 還有另一個好處:程式碼比文字版的程式碼更簡潔、更容易。要儲存這個XML,需要另一個版本的save 函數,將結果儲存為XML 格式,而不是文字格式。
清單13. xml2.php
...
function save()
{
$doc = new DOMDocument();
$doc->formatOutput = true;
$r = $doc->createElement( "config" );
$doc->appendChild( $r );
foreach( $this->items as $k => $v )
{
$kn = $doc->createElement( $k );
$kn->appendChild( $doc->createTextNode( $v ) );
$r->appendChild( $kn );
}
copy( $this->configFile, $this->configFile.'.bak' );
$doc->save( $this->configFile );
}
……
這段程式碼建立了一個新的XML 文件物件模型(Document Object Model ,DOM),然後將$items 陣列中的所有資料都儲存到這個模型中。完成這些以後,使用save 方法將XML 儲存為一個檔案。
使用資料庫
最後的替代方式是使用一個資料庫保存配置元素的值。那首先要用一個簡單的模式來儲存配置資料。下面是一個簡單的模式。
清單14. schema.sql
DROP TABLE IF EXISTS settings;
CREATE TABLE settings (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
name TEXT,
value TEXT,
PRIMARY KEY ( id )
);
這要求進行一些基於應用程式需求的調整。例如,如果想讓配置元素按照每個使用者進行存儲,就需要新增使用者ID 作為額外的一列。
為了讀取及寫入數據,我寫瞭如圖15 所示的更新過的Configuration 類別。
清單15. db1.php
<?php
require_once( 'DB.php' );
$dsn = 'mysql://root:password@localhost/config';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class Configuration
{
private $configFile = 'config.xml';
private $items = array();
function __construct() { $this->parse(); }
function __get($id) { return $this->items[ $id ]; }
function __set($id,$v)
{
global $db;
$this->items[ $id ] = $v;
$sth1 = $db->prepare( 'DELETE FROM settings WHERE name=?' );
$db->execute( $sth1, $id );
if (PEAR::isError($db)) { die($db->getMessage()); }
$sth2 = $db->prepare('INSERT INTO settings ( id, name, value ) VALUES ( 0, ?, ? )' );
$db->execute( $sth2, array( $id, $v ) );
if (PEAR::isError($db)) { die($db->getMessage()); }
}
function parse()
{
global $db;
$doc = new DOMDocument();
$doc->load( $this->configFile );
$cn = $doc->getElementsByTagName( "config" );
$nodes = $cn->item(0)->getElementsByTagName( "*" );
foreach( $nodes as $node )
$this->items[ $node->nodeName ] = $node->nodeValue;
$res = $db->query( 'SELECT name,value FROM settings' );
if (PEAR::isError($db)) { die($db->getMessage()); }
while( $res->fetchInto( $row ) ) {
$this->items[ $row[0] ] = $row[1];
}
}
}
$c = new Configuration();
echo( $c->TemplateDirectory."n" );
$c->TemplateDirectory = 'new foo';
echo( $c->TemplateDirectory."n" );
?>
這其實是一個混合的文字/資料庫解決方案。請仔細觀察parse 方法。類別首先讀取文字檔案來取得初始值,然後讀取資料庫,進而將鍵更新為最新的值。設定一個值後,鍵就從資料庫中移除掉,並新增一筆具有更新過的值的新記錄。
觀察Configuration 類別如何透過本文的多個版本來發揮作用是一件有趣的事,該類別能從文字檔案、XML 及資料庫中讀取數據,並且一直保持相同的介面。我鼓勵您在開發中也使用具有相同穩定性的介面。對於物件的客戶機來說,這項工作具體是如何運作的是不明確的。關鍵的是物件與客戶機之間的契約。
什麼是配置及怎樣配置
在配置過多的配置選項與配置不足間找一個適當的中間點是一件困難的事。可以肯定的是,任何資料庫配置(例如,資料庫名稱、資料庫使用者用及密碼)都應該是可設定的。除此之外,我還有一些基本的建議配置。
在進階設定中,每個特性都應該有一個獨立的啟用/停用選項。根據其對應用程式的重要性來允許或停用這些選項。例如,在一個Web 論壇應用程式中,延時特性在預設狀態下是啟用的。但電子郵件通知在缺省狀態下卻是禁用的,因為這似乎需要自訂。
使用者介面(UI)選項全應該設定到一個位置。介面的結構(例如,選單位置、額外的選單項目、連結到介面特定元素的URL、使用的logo,諸如此類)全應該設定到一個單一位置。我強烈建議不要將字體、顏色或樣式條目指定為配置項目。這些都應該透過層疊樣式表(Cascading Style Sheets,CSS)來設置,且配置系統應該指定使用哪個CSS 檔案。 CSS 是設定字體、樣式、顏色等等的有效且靈活的方式。有許多出色的CSS 工具,您的應用程式應該可以很好地利用CSS,而不是試圖自行設定標準。
在每一個特性中,我推薦設定3 到10 個配置選項。這些配置選項應該以一種意義明顯的方式命名。如果設定選項能夠透過UI 設置,在文字檔案、XML 檔案及資料庫中的選項名稱應該直接同介面元素的標題相關。另外,這些選項全應該有明確的預設值。
總的來說,以下這些選項應該是可設定的:電子郵件地址、CSS 所使用的東西、從文件中引用的系統資源的位置以及圖形元素的檔案名稱。
對於圖形元素,您也許想要創建一個名為皮膚的獨立的配置文件類型,該類型中包含了對配置文件的設置,包括CSS 文件的位置、圖形的位置及這些類型的東西。然後,讓使用者在多種皮膚文件中進行挑選。這使得對應用程式外觀和感覺的大規模更改變得簡單。這也為用戶提供了一個機會,使應用程式能夠在不同的產品安裝間更換皮膚。本文並不涵蓋這些皮膚文件,但您在這裡學到的基礎知識將會使對皮膚文件的支援變得更加簡單。
結束語
可配置性對於任何PHP 應用程式來說都是至關重要的一個部分,一開始就應該成為設計的中心部分。我希望本文能夠為您實現配置架構提供一些幫助,並對應該允許什麼樣的配置選項有所指導。