本文舉例說明了建立可配置 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 )
);
這要求進行一些基於應用程式需求的調整。例如,如果想讓配置元素按照每個使用者進行儲存,就需要添加使用者識別碼 作為額外的一列。
為了讀取及寫入資料,我編寫了如圖 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 應用程式來說都是至關重要的一個部分,一開始就應該成為設計的中心部分。我希望本文能夠對您實現配置架構提供一些協助,並對應該允許什麼樣的配置選項有所指導。