用不同檔案 建置可配置PHP應用程式的正確方式

日期:2006-11-16  作者:喜騰小二  來源:PHPChina


    如果計劃讓其他人或公司可以使用您的 PHP 應用程式,需要確保該程式是可配置的。至少,要允許使用者以一種安全的方式設定資料庫登入及密碼,從而使其中的材料不會對外公開。

    本文展示了幾種用於存儲配置設定及編輯這些設定的技術。另外,文中也為哪些元素需要設為可配置以及如何避免陷入配置過度或者配置不足的困境提供了指導。

    使用 INI 檔案進行配置

    PHP 內建了對配置檔案的支援。這是透過 php.ini 檔案這樣的初始化檔案(INI)機制實現的,在 php.ini 檔案中定義了資料庫連線逾時或工作階段如何存儲等常量。如果願意的話,可以在這個 php.ini 檔案中為應用程式自訂配置。為了幫助,我將下列程式碼行新增到 php.ini 檔案中。

myapptempdir=foo
 


然後,我編寫了一個小 PHP 指令檔來讀取這個配置項,如清單 1 所示。


    清單 1. ini1.php

function get_template_directory()
{
  $v = get_cfg_var( "myapptempdir" );
  return ( $v == null ) ? "tempdir" : $v;
}

echo( get_template_directory()."
" );
?>
 

當在指令行中執行這段程式碼時,得到如下結果:

% 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

echo( ini_get("include_path")."
" );
ini_set("include_path",
        ini_get("include_path").":./mylib" );
echo( ini_get("include_path")."
" );
?>
 

    在本例中,我將我的本機 mylib 目錄新增到了 include 路徑中,所以能夠從該目錄中 require PHP 檔案,而不需要將該路徑新增到 require 陳述式中。

    PHP 中的配置

    通常對於在 INI 檔案中存儲配置條目的一個替代辦法是使用一個簡單的 PHP 指令檔來保持資料。如下是一個樣例。

    清單 4. config.php

# Specify the location of the temporary directory
#
$TEMPLATE_DIRECTORY = "tempdir";
?>
 

使用該常量的程式碼如下所示。

    清單 5. php.php

require_once 'config.php';

function get_template_directory()
{
  global $TEMPLATE_DIRECTORY;
  return $TEMPLATE_DIRECTORY;
}

echo( get_template_directory()."
" );
?>
 

    該程式碼首先包含配置檔案(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

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."
" );
?>
 

    該程式碼首先建立了一個 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

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."
" );
$c->TemplateDirectory = 'foobar';
echo( $c->TemplateDirectory."
" );
?>
 

    現在,有了一個 __set 函式,它是 __get 函式的 “堂兄弟”。該函式並不為一個成員變數獲取值,當要設定一個成員變數時,才調用這個函式。底部的測試程式碼設定值並列印出新值。

    下麵是在指令行中執行此程式碼時出現的結果:

% php text2.php
tempdir
foobar
%
 

    太好了!但如何能將它存儲到檔案中,從而將使這個改動固定下來呢?為此,需要寫檔案並讀取它。用於寫檔案的新函式,如下所示。

    清單 9. text3.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]]."
";
      }
      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."
" );
$c->TemplateDirectory = 'foobar';
echo( $c->TemplateDirectory."
" );
$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



  My App
  tempdir

 

    清單 12 顯示了使用 XML 來裝載配置設定的 Configuration 類的更新版。


    清單 12. xml1.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."
" );
?>

    看起來 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

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."
" );
$c->TemplateDirectory = 'new foo';
echo( $c->TemplateDirectory."
" );
?>

    這實際上是一個混合的文字/資料庫解決方案。請仔細觀察 parse 方法。該類首先讀取文字檔案來獲取初始值,然後讀取資料庫,進而將鍵更新為最新的值。在設定一個值後,鍵就從資料庫中移除掉,並新增一條俱有更新過的值的新記錄。

    觀察 Configuration 類如何透過本文的多個版本來發揮作用是一件有趣的事,該類能從文字檔案、XML 及資料庫中讀取資料,並一直保持相同的介麵。我鼓勵您在開發中也使用俱有相同穩定性的介麵。對於物件的客戶機來說,這項工作俱體是如何執行的是不明確的。關鍵的是物件與客戶機之間的契約。

    什麼是配置及怎樣配置

    在配置過多的配置選項與配置不足間找一個適當的中間點是一件困難的事。可以肯定的是,任何資料庫配置(例如,資料庫名稱、資料庫使用者用及密碼)都應該是可配置的。除此之外,我還有一些基本的推薦配置項。

    在進階設定中,每一個特性都應該有一個獨立的啓用/禁用選項。根據其對應用程式的重要性來允許或禁用這些選項。例如,在一個 Web 論壇應用程式中,延時特性在預設狀態下是啓用的。但電子郵件通知在預設狀態下卻是禁用的,因為這似乎需要自訂。

    使用者介麵(UI)選項全應該設定到一個位置上。介麵的結構(例如,功能表位置、額外的功能表項、連結到介麵特定元素的 URL、使用的 logo,諸如此類)全應該設定到一個單一位置上。我強烈地建議不要將字型、色彩或樣式條目指定為配置項。這些都應該透過層疊樣式表(Cascading Style Sheets,CSS)來設定,且配置係統應該指定使用哪個 CSS 檔案。CSS 是設定字型、樣式、色彩等等的一種有效且靈活的方式。有許多出色的 CSS 工俱,您的應用程式應該很好地利用 CSS,而不是試圖自行設定標準。

    在每一個特性中,我推薦設定 3 到 10 個配置選項。這些配置選項應該以一種意義明顯的方式命名。如果配置選項能夠透過 UI 設定,在文字檔案、XML 檔案及資料庫中的選項名稱應該直接同介麵元素的標題相關。另外,這些選項全應該有明確的預設值。

    總的來說,下麵這些選項應該是可配置的:電子郵件地址、CSS 所使用的東西、從檔案中引用的係統資源的位置以及圖形元素的檔案名。

    對於圖形元素,您也許想要建立一個名為皮膚 的獨立的配置檔案類型,該類型中包含了對配置檔案的設定,包括 CSS 檔案的位置、圖形的位置及這些類型的東西。然後,讓使用者在多種皮膚檔案中進行挑選。這使得對應用程式外觀和感覺的大規模變更變得簡單。這也同樣為使用者提供了一個機會,使應用程式能夠在不同的產品安裝間更換皮膚。本文並不涵蓋這些皮膚檔案,但您在這裡學到的基礎知識將會使對皮膚檔案的支援變得更加簡單。

    結束語

    可配置性對於任何 PHP 應用程式來說都是至關重要的一個部分,一開始就應該成為設計的中心部分。我希望本文能夠對您實現配置架構提供一些說明,並對應該允許什麼樣的配置選項有所指導。

<<<返回技術中心

技術文章

站內新聞

我要啦免费统计