不同文件構(gòu)建PHP程序的正確方式
以下為引用的內(nèi)容:
如果計(jì)劃讓其他人或公司可以使用您的 PHP 應(yīng)用程序,需要確保該程序是可配置的。至少,要允許用戶以一種安全的方式設(shè)置數(shù)據(jù)庫(kù)登錄及密碼,從而使其中的材料不會(huì)對(duì)外公開。
本文展示了幾種用于存儲(chǔ)配置設(shè)置及編輯這些設(shè)置的技術(shù)。另外,文中也為哪些元素需要設(shè)為可配置以及如何避免陷入配置過(guò)度或者配置不足的困境提供了指導(dǎo)。
使用 INI 文件進(jìn)行配置
PHP 內(nèi)建了對(duì)配置文件的支持。這是通過(guò) php.ini 文件這樣的初始化文件(INI)機(jī)制實(shí)現(xiàn)的,在 php.ini 文件中定義了數(shù)據(jù)庫(kù)連接超時(shí)或會(huì)話如何存儲(chǔ)等常量。如果愿意的話,可以在這個(gè) php.ini 文件中為應(yīng)用程序定制配置。為了說(shuō)明,我將下列代碼行添加到 php.ini 文件中。
myapptempdir=foo
然后,我編寫了一個(gè)小 PHP 腳本來(lái)讀取這個(gè)配置項(xiàng),如清單 1 所示。
清單 1. ini1.php
<?phpfunction get_template_directory(){$v = get_cfg_var( “myapptempdir” );return ( $v == null ) ? “tempdir” : $v;}
echo( get_template_directory().”n” );?>當(dāng)在命令行中運(yùn)行這段代碼時(shí),得到如下結(jié)果:
% php ini1.phpfoo%
太棒了。但為什么不能用標(biāo)準(zhǔn)的 INI 函數(shù)來(lái)獲取 myapptempdir 配置項(xiàng)的值呢?我研究了一下,發(fā)現(xiàn)在大多數(shù)情況下,定制配置項(xiàng)不能使用這些方法來(lái)獲取。然而,使用 get_cfg_var 函數(shù)卻是可以訪問的。
為使這個(gè)方法更加簡(jiǎn)單,將對(duì)變量的訪問封裝在第二個(gè)函數(shù)中,該函數(shù)使用配置鍵名及一個(gè)缺省值作為參數(shù),如下所示。
清單 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” );}
這是對(duì)如何訪問 INI 文件的一個(gè)很好的概括,所以,如果要使用一個(gè)不同的機(jī)制或?qū)⑦@個(gè) INI 文件存儲(chǔ)到其他位置,就不需要為更改大量的函數(shù)而大費(fèi)周折。
我不推薦使用 INI 文件作為應(yīng)用程序的配置,這有兩個(gè)理由。首先,雖然這樣做較容易讀取 INI 文件,但卻幾乎不可能安全地寫 INI 文件。所以這樣做只適合于只讀配置項(xiàng)。第二,php.ini 文件在服務(wù)器的所有應(yīng)用程序上共享,所以我認(rèn)為特定于應(yīng)用程序的配置項(xiàng)不應(yīng)該寫在該文件中。需要對(duì) INI 文件了解什么呢?最重要的是如何重置 include 路徑來(lái)添加配置項(xiàng),如下所示。
清單 3. ini3.php
<?phpecho( 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 語(yǔ)句中。
PHP 中的配置
通常對(duì)于在 INI 文件中存儲(chǔ)配置條目的一個(gè)替代辦法是使用一個(gè)簡(jiǎn)單的 PHP 腳本來(lái)保持?jǐn)?shù)據(jù)。如下是一個(gè)樣例。
清單 4. config.php
<?php# Specify the location of the temporary directory#$TEMPLATE_DIRECTORY = “tempdir”;?>使用該常量的代碼如下所示。
清單 5. php.php
<?phprequire_once ‘config.php’;
function get_template_directory(){global $TEMPLATE_DIRECTORY;return $TEMPLATE_DIRECTORY;}
echo( get_template_directory().”n” );?>該代碼首先包含配置文件(config.php),接著就可以直接使用這些常量了。
使用這項(xiàng)技術(shù)有很多優(yōu)勢(shì)。首先,如果某些人僅僅瀏覽 config.php 文件,該頁(yè)面是空白的。所以可以將 config.php 放到相同的文件中,并作為 Web 應(yīng)用程序的根。第二,在任何編輯器中都可編輯,并且在一些編輯器中甚至具備語(yǔ)法著色及語(yǔ)法檢查功能。這項(xiàng)技術(shù)的缺點(diǎn)是,這是一個(gè)像 INI 文件一樣的只讀技術(shù)。將數(shù)據(jù)從此文件中提取出來(lái)是輕而易舉的,但在該 PHP 文件中調(diào)整數(shù)據(jù)卻很困難,在一些情況下甚至是不可能的。
下面的替代方法顯示了如何編寫在本質(zhì)上既可讀又可寫的配置系統(tǒng)。
文本文件
前面的兩個(gè)例子對(duì)于只讀配置條目都是合適的,但對(duì)于既讀又寫的配置參數(shù)來(lái)說(shuō)又如何呢?首先,看看清單 6 中的文本配置文件。
清單 6. config.txt
# My application’s configuration fileTitle=My AppTemplateDirectory=tempdir這是同 INI 文件相同的文件格式,但我自己編寫了輔助工具。為此,我創(chuàng)建了自己的 Configuration 類,如下所示。
清單 7. text1.php
<?phpclass 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” );?>該代碼首先創(chuàng)建了一個(gè) Configuration 對(duì)象。該構(gòu)造函數(shù)接下來(lái)讀取 config.txt 并用解析過(guò)的文件內(nèi)容來(lái)設(shè)置局部變量 $items。
該腳本隨后尋找 TemplateDirectory,這并沒有在對(duì)象中直接定義。因此,使用設(shè)置成 ‘TemplateDirectory’ 的 $id 來(lái)調(diào)用神奇的 __get 方法,__get 方法針對(duì)該鍵返回 $items 數(shù)組中的值。
這個(gè) __get 方法特定于 PHP V5 環(huán)境,所以此腳本必須在 PHP V5 下運(yùn)行。實(shí)際上,本文中所有的腳本都需要在 PHP V5 下運(yùn)行。
當(dāng)在命令行運(yùn)行此腳本時(shí),能看到下列結(jié)果:http://www.mypchelp.cn/php.asp% php text1.phptempdir%一切都在預(yù)料之中,該對(duì)象讀取 config.txt 文件,然后為 TemplateDirectory 配置項(xiàng)獲得正確的值。
但對(duì)于設(shè)置一個(gè)配置值,應(yīng)該怎么做呢?在此類中建立一個(gè)新方法及一些新的測(cè)試代碼,就能夠得到這個(gè)功能,如下所示。
清單 8. text2.php
<?phpclass 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” );?>現(xiàn)在,有了一個(gè) __set 函數(shù),它是 __get 函數(shù)的 “堂兄弟”。該函數(shù)并不為一個(gè)成員變量獲取值,當(dāng)要設(shè)置一個(gè)成員變量時(shí),才調(diào)用這個(gè)函數(shù)。底部的測(cè)試代碼設(shè)置值并打印出新值。
下面是在命令行中運(yùn)行此代碼時(shí)出現(xiàn)的結(jié)果:
% php text2.phptempdirfoobar%太好了!但如何能將它存儲(chǔ)到文件中,從而將使這個(gè)改動(dòng)固定下來(lái)呢?為此,需要寫文件并讀取它。用于寫文件的新函數(shù),如下所示。
清單 9. text3.php
<?phpclass 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 函數(shù)巧妙地操作 config.txt。我并沒有僅用更新過(guò)的配置項(xiàng)重寫文件(這樣會(huì)移除掉注釋),而是讀取了這個(gè)文件并靈活地重寫了 $items 數(shù)組中的內(nèi)容。這樣的話,就保留了文件中的注釋。
在命令行運(yùn)行該腳本并輸出文本配置文件中的內(nèi)容,能夠看到下列輸出。
清單 10. 保存函數(shù)輸出% php text3.phptempdirfoobar% cat config.txt# My application’s configuration fileTitle=My AppTemplateDirectory=foobar%
原始的 config.txt 文件現(xiàn)在被新值更新了。
XML 配置文件
盡管文本文件易于閱讀及編輯,但卻不如 XML 文件流行。另外,XML 有眾多適用的編輯器,這些編輯器能夠理解標(biāo)記、特殊符號(hào)轉(zhuǎn)義等等。所以配置文件的 XML 版本會(huì)是什么樣的呢?清單 11 顯示了 XML 格式的配置文件。
清單 11. config.xml
<?xml version=”1.0″?><config><Title>My App</Title><TemplateDirectory>tempdir</TemplateDirectory></config>清單 12 顯示了使用 XML 來(lái)裝載配置設(shè)置的 Configuration 類的更新版。清單 12. xml1.php
<?phpclass 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” );?>
看起來(lái) XML 還有另一個(gè)好處:代碼比文本版的代碼更為簡(jiǎn)潔、容易。為保存這個(gè) XML,需要另一個(gè)版本的 save 函數(shù),將結(jié)果保存為 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 );}…
這段代碼創(chuàng)建了一個(gè)新的 XML 文檔對(duì)象模型(Document Object Model ,DOM),然后將 $items 數(shù)組中的所有數(shù)據(jù)都保存到這個(gè)模型中。完成這些以后,使用 save 方法將 XML 保存為一個(gè)文件。
使用數(shù)據(jù)庫(kù)
最后的替代方式是使用一個(gè)數(shù)據(jù)庫(kù)保存配置元素的值。那首先要用一個(gè)簡(jiǎn)單的模式來(lái)存儲(chǔ)配置數(shù)據(jù)。下面是一個(gè)簡(jiǎn)單的模式。
清單 14. schema.sql
DROP TABLE IF EXISTS settings;CREATE TABLE settings (id MEDIUMINT NOT NULL AUTO_INCREMENT,name TEXT,value TEXT,PRIMARY KEY ( id ));這要求進(jìn)行一些基于應(yīng)用程序需求的調(diào)整。例如,如果想讓配置元素按照每個(gè)用戶進(jìn)行存儲(chǔ),就需要添加用戶 ID 作為額外的一列。
為了讀取及寫入數(shù)據(jù),我編寫了如圖 15 所示的更新過(guò)的 Configuration 類。
清單 15. db1.php
<?phprequire_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” );?>
這實(shí)際上是一個(gè)混合的文本/數(shù)據(jù)庫(kù)解決方案。請(qǐng)仔細(xì)觀察 parse 方法。該類首先讀取文本文件來(lái)獲取初始值,然后讀取數(shù)據(jù)庫(kù),進(jìn)而將鍵更新為最新的值。在設(shè)置一個(gè)值后,鍵就從數(shù)據(jù)庫(kù)中移除掉,并添加一條具有更新過(guò)的值的新記錄。
觀察 Configuration 類如何通過(guò)本文的多個(gè)版本來(lái)發(fā)揮作用是一件有趣的事,該類能從文本文件、XML 及數(shù)據(jù)庫(kù)中讀取數(shù)據(jù),并一直保持相同的接口。我鼓勵(lì)您在開發(fā)中也使用具有相同穩(wěn)定性的接口。對(duì)于對(duì)象的客戶機(jī)來(lái)說(shuō),這項(xiàng)工作具體是如何運(yùn)行的是不明確的。關(guān)鍵的是對(duì)象與客戶機(jī)之間的契約。
什么是配置及怎樣配置
在配置過(guò)多的配置選項(xiàng)與配置不足間找一個(gè)適當(dāng)?shù)闹虚g點(diǎn)是一件困難的事??梢钥隙ǖ氖牵魏螖?shù)據(jù)庫(kù)配置(例如,數(shù)據(jù)庫(kù)名稱、數(shù)據(jù)庫(kù)用戶用及密碼)都應(yīng)該是可配置的。除此之外,我還有一些基本的推薦配置項(xiàng)。
在高級(jí)設(shè)置中,每一個(gè)特性都應(yīng)該有一個(gè)獨(dú)立的啟用/禁用選項(xiàng)。根據(jù)其對(duì)應(yīng)用程序的重要性來(lái)允許或禁用這些選項(xiàng)。例如,在一個(gè) Web 論壇應(yīng)用程序中,延時(shí)特性在缺省狀態(tài)下是啟用的。但電子郵件通知在缺省狀態(tài)下卻是禁用的,因?yàn)檫@似乎需要定制。
用戶界面(UI)選項(xiàng)全應(yīng)該設(shè)置到一個(gè)位置上。界面的結(jié)構(gòu)(例如,菜單位置、額外的菜單項(xiàng)、鏈接到界面特定元素的 URL、使用的 logo,諸如此類)全應(yīng)該設(shè)置到一個(gè)單一位置上。我強(qiáng)烈地建議不要將字體、顏色或樣式條目指定為配置項(xiàng)。這些都應(yīng)該通過(guò)層疊樣式表(Cascading Style Sheets,CSS)來(lái)設(shè)置,且配置系統(tǒng)應(yīng)該指定使用哪個(gè) CSS 文件。CSS 是設(shè)置字體、樣式、顏色等等的一種有效且靈活的方式。有許多出色的 CSS 工具,您的應(yīng)用程序應(yīng)該很好地利用 CSS,而不是試圖自行設(shè)置標(biāo)準(zhǔn)。
在每一個(gè)特性中,我推薦設(shè)置 3 到 10 個(gè)配置選項(xiàng)。這些配置選項(xiàng)應(yīng)該以一種意義明顯的方式命名。如果配置選項(xiàng)能夠通過(guò) UI 設(shè)置,在文本文件、XML 文件及數(shù)據(jù)庫(kù)中的選項(xiàng)名稱應(yīng)該直接同界面元素的標(biāo)題相關(guān)。另外,這些選項(xiàng)全應(yīng)該有明確的缺省值。
總的來(lái)說(shuō),下面這些選項(xiàng)應(yīng)該是可配置的:電子郵件地址、CSS 所使用的東西、從文件中引用的系統(tǒng)資源的位置以及圖形元素的文件名。
對(duì)于圖形元素,您也許想要?jiǎng)?chuàng)建一個(gè)名為皮膚 的獨(dú)立的配置文件類型,該類型中包含了對(duì)配置文件的設(shè)置,包括 CSS 文件的位置、圖形的位置及這些類型的東西。然后,讓用戶在多種皮膚文件中進(jìn)行挑選。這使得對(duì)應(yīng)用程序外觀和感覺的大規(guī)模更改變得簡(jiǎn)單。這也同樣為用戶提供了一個(gè)機(jī)會(huì),使應(yīng)用程序能夠在不同的產(chǎn)品安裝間更換皮膚。本文并不涵蓋這些皮膚文件,但您在這里學(xué)到的基礎(chǔ)知識(shí)將會(huì)使對(duì)皮膚文件的支持變得更加簡(jiǎn)單。
結(jié)束語(yǔ)
可配置性對(duì)于任何 PHP 應(yīng)用程序來(lái)說(shuō)都是至關(guān)重要的一個(gè)部分,一開始就應(yīng)該成為設(shè)計(jì)的中心部分。我希望本文能夠?qū)δ鷮?shí)現(xiàn)配置架構(gòu)提供一些幫助,并對(duì)應(yīng)該允許什么樣的配置選項(xiàng)有所指導(dǎo)。
