本文举例说明了创建可配置 PHP 应用程序的几种方法文中也探讨了应用程序中理想的配置点并在应用程序过分可配置和过分封闭之间寻求一个平衡点
如果计划让其他人或公司可以使用您的 PHP 应用程序需要确保该程序是可配置的至少要允许用户以一种安全的方式设置数据库登录及密码从而使其中的材料不会对外公开
本文展示了几种用于存储配置设置及编辑这些设置的技术另外文中也为哪些元素需要设为可配置以及如何避免陷入配置过度或者配置不足的困境提供了指导
使用 INI 文件进行配置
PHP 内建了对配置文件的支持这是通过 phpini 文件这样的初始化文件(INI)机制实现的在 phpini 文件中定义了数据库连接超时或会话如何存储等常量如果愿意的话可以在这个 phpini 文件中为应用程序定制配置为了说明我将下列代码行添加到 phpini 文件中
myapptempdir=foo
然后我编写了一个小 PHP 脚本来读取这个配置项如清单 所示
清单 iniphp
<?phpfunction get_template_directory(){ $v = get_cfg_var( myapptempdir ); return ( $v == null ) ? tempdir : $v;}echo( get_template_directory()\n );?>
当在命令行中运行这段代码时得到如下结果
% php iniphpfoo%
太棒了但为什么不能用标准的 INI 函数来获取 myapptempdir
配置项的值呢?我研究了一下发现在大多数情况下定制配置项不能使用这些方法来获取然而使用 get_cfg_var
函数却是可以访问的
为使这个方法更加简单将对变量的访问封装在第二个函数中该函数使用配置键名及一个缺省值作为参数如下所示
清单 iniphp
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 文件所以这样做只适合于只读配置项第二phpini 文件在服务器的所有应用程序上共享所以我认为特定于应用程序的配置项不应该写在该文件中
需要对 INI 文件了解什么呢?最重要的是如何重置 include
路径来添加配置项如下所示
清单 iniphp
<?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
语句中
PHP 中的配置
通常对于在 INI 文件中存储配置条目的一个替代办法是使用一个简单的 PHP 脚本来保持数据如下是一个样例
清单 configphp
<?php# Specify the location of the temporary directory#$TEMPLATE_DIRECTORY = tempdir;?>
使用该常量的代码如下所示
清单 phpphp
<?phprequire_once configphp;function get_template_directory(){ global $TEMPLATE_DIRECTORY; return $TEMPLATE_DIRECTORY;}echo( get_template_directory()\n );?>
该代码首先包含配置文件(configphp)接着就可以直接使用这些常量了
使用这项技术有很多优势首先如果某些人仅仅浏览 configphp 文件该页面是空白的所以可以将 configphp 放到相同的文件中并作为 Web 应用程序的根第二在任何编辑器中都可编辑并且在一些编辑器中甚至具备语法着色及语法检查功能
这项技术的缺点是这是一个像 INI 文件一样的只读技术将数据从此文件中提取出来是轻而易举的但在该 PHP 文件中调整数据却很困难在一些情况下甚至是不可能的
下面的替代方法显示了如何编写在本质上既可读又可写的配置系统
文本文件
前面的两个例子对于只读配置条目都是合适的但对于既读又写的配置参数来说又如何呢?首先看看清单 中的文本配置文件
清单 configtxt
# My applications configuration fileTitle=My AppTemplateDirectory=tempdir
这是同 INI 文件相同的文件格式但我自己编写了辅助工具为此我创建了自己的 Configuration
类如下所示
清单 textphp
<?phpclass Configuration{ private $configFile = configtxt; 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[] ] = $found[]; } } fclose( $fh ); }}$c = new Configuration();echo( $c>TemplateDirectory\n );?>
该代码首先创建了一个 Configuration
对象该构造函数接下来读取 configtxt 并用解析过的文件内容来设置局部变量 $items
该脚本随后寻找 TemplateDirectory
这并没有在对象中直接定义因此使用设置成 TemplateDirectory
的 $id
来调用神奇的 __get
方法__get
方法针对该键返回 $items
数组中的值
这个 __get
方法特定于 PHP V 环境所以此脚本必须在 PHP V 下运行实际上本文中所有的脚本都需要在 PHP V 下运行
当在命令行运行此脚本时能看到下列结果
% php textphp tempdir%
一切都在预料之中该对象读取 configtxt 文件然后为 TemplateDirectory
配置项获得正确的值
但对于设置一个配置值应该怎么做呢?在此类中建立一个新方法及一些新的测试代码就能够得到这个功能如下所示
清单 textphp
<?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 );?>
现在有了一个 __set
函数它是 __get
函数的 堂兄弟该函数并不为一个成员变量获取值当要设置一个成员变量时才调用这个函数底部的测试代码设置值并打印出新值
下面是在命令行中运行此代码时出现的结果
% php textphp tempdirfoobar%
太好了!但如何能将它存储到文件中从而将使这个改动固定下来呢?为此需要写文件并读取它用于写文件的新函数如下所示
清单 textphp
<?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[]=$this>items[$found[]]\n; } else { $nf = $l; } } fclose( $fh ); copy( $this>configFile $this>configFilebak ); $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
函数巧妙地操作 configtxt我并没有仅用更新过的配置项重写文件(这样会移除掉注释)而是读取了这个文件并灵活地重写了 $items
数组中的内容这样的话就保留了文件中的注释
在命令行运行该脚本并输出文本配置文件中的内容能够看到下列输出
清单 保存函数输出
% php textphp tempdirfoobar% cat configtxt# My applications configuration fileTitle=My AppTemplateDirectory=foobar%
原始的 configtxt 文件现在被新值更新了
XML 配置文件
尽管文本文件易于阅读及编辑但却不如 XML 文件流行另外XML 有众多适用的编辑器这些编辑器能够理解标记特殊符号转义等等所以配置文件的 XML 版本会是什么样的呢?清单 显示了 XML 格式的配置文件
清单 configxml
<?xml version=?><config> <Title>My App</Title> <TemplateDirectory>tempdir</TemplateDirectory></config>
清单 显示了使用 XML 来装载配置设置的 Configuration
类的更新版
清单 xmlphp
<?phpclass Configuration{ private $configFile = configxml; 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()>getElementsByTagName( * ); foreach( $nodes as $node ) $this>items[ $node>nodeName ] = $node>nodeValue; }}$c = new Configuration();echo( $c>TemplateDirectory\n );?>
看起来 XML 还有另一个好处代码比文本版的代码更为简洁容易为保存这个 XML需要另一个版本的 save
函数将结果保存为 XML 格式而不是文本格式
清单 xmlphp
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>configFilebak ); $doc>save( $this>configFile ); }
这段代码创建了一个新的 XML 文档对象模型(Document Object Model DOM)然后将 $items
数组中的所有数据都保存到这个模型中完成这些以后使用 save
方法将 XML 保存为一个文件
使用数据库
最后的替代方式是使用一个数据库保存配置元素的值那首先要用一个简单的模式来存储配置数据下面是一个简单的模式
清单 schemasql
DROP TABLE IF EXISTS settings;CREATE TABLE settings ( id MEDIUMINT NOT NULL AUTO_INCREMENT name TEXT value TEXT PRIMARY KEY ( id ));
这要求进行一些基于应用程序需求的调整例如如果想让配置元素按照每个用户进行存储就需要添加用户 ID 作为额外的一列
为了读取及写入数据我编写了如图 所示的更新过的 Configuration
类
清单 dbphp
<?phprequire_once( DBphp );$dsn = mysql://root:password@localhost/config;$db =& DB::Connect( $dsn array() );if (PEAR::isError($db)) { die($db>getMessage()); }class Configuration{ private $configFile = configxml; private $items = array(); function __construct() { $this>parse(); } function __get($id) { return $this>items[ $id ]; } function __set($id$v) { global $db; $this>items[ $id ] = $v; $sth = $db>prepare( DELETE FROM settings WHERE name=? ); $db>execute( $sth $id ); if (PEAR::isError($db)) { die($db>getMessage()); } $sth = $db>prepare( INSERT INTO settings ( id name value ) VALUES ( ? ? ) ); $db>execute( $sth 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()>getElementsByTagName( * ); foreach( $nodes as $node ) $this>items[ $node>nodeName ] = $node>nodeValue; $res = $db>query( SELECT namevalue FROM settings ); if (PEAR::isError($db)) { die($db>getMessage()); } while( $res>fetchInto( $row ) ) { $this>items[ $row[] ] = $row[]; } }}$c = new Configuration();echo( $c>TemplateDirectory\n );$c>TemplateDirectory = new foo;echo( $c>TemplateDirectory\n );?>
这实际上是一个混合的文本/数据库解决方案请仔细观察 parse
方法该类首先读取文本文件来获取初始值然后读取数据库进而将键更新为最新的值在设置一个值后键就从数据库中移除掉并添加一条具有更新过的值的新记录
观察 Configuration
类如何通过本文的多个版本来发挥作用是一件有趣的事该类能从文本文件XML 及数据库中读取数据并一直保持相同的接口我鼓励您在开发中也使用具有相同稳定性的接口对于对象的客户机来说这项工作具体是如何运行的是不明确的关键的是对象与客户机之间的契约
什么是配置及怎样配置
在配置过多的配置选项与配置不足间找一个适当的中间点是一件困难的事可以肯定的是任何数据库配置(例如数据库名称数据库用户用及密码)都应该是可配置的除此之外我还有一些基本的推荐配置项
在高级设置中每一个特性都应该有一个独立的启用/禁用选项根据其对应用程序的重要性来允许或禁用这些选项例如在一个 Web 论坛应用程序中延时特性在缺省状态下是启用的但电子邮件通知在缺省状态下却是禁用的因为这似乎需要定制
用户界面(UI)选项全应该设置到一个位置上界面的结构(例如菜单位置额外的菜单项链接到界面特定元素的 URL使用的 logo诸如此类)全应该设置到一个单一位置上我强烈地建议不要将字体颜色或样式条目指定为配置项这些都应该通过层叠样式表(Cascading Style SheetsCSS)来设置且配置系统应该指定使用哪个 CSS 文件CSS 是设置字体样式颜色等等的一种有效且灵活的方式有许多出色的 CSS 工具您的应用程序应该很好地利用 CSS而不是试图自行设置标准
在每一个特性中我推荐设置 到 个配置选项这些配置选项应该以一种意义明显的方式命名如果配置选项能够通过 UI 设置在文本文件XML 文件及数据库中的选项名称应该直接同界面元素的标题相关另外这些选项全应该有明确的缺省值
总的来说下面这些选项应该是可配置的电子邮件地址CSS 所使用的东西从文件中引用的系统资源的位置以及图形元素的文件名
对于图形元素您也许想要创建一个名为皮肤 的独立的配置文件类型该类型中包含了对配置文件的设置包括 CSS 文件的位置图形的位置及这些类型的东西然后让用户在多种皮肤文件中进行挑选这使得对应用程序外观和感觉的大规模更改变得简单这也同样为用户提供了一个机会使应用程序能够在不同的产品安装间更换皮肤本文并不涵盖这些皮肤文件但您在这里学到的基础知识将会使对皮肤文件的支持变得更加简单
结束语
可配置性对于任何 PHP 应用程序来说都是至关重要的一个部分一开始就应该成为设计的中心部分我希望本文能够对您实现配置架构提供一些帮助并对应该允许什么样的配置选项有所指导