构建可配置PHP应用程序的正确方式

  本文举例说明了创建可配置 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 应用程序来说都是至关重要的一个部分,一开始就应该成为设计的中心部分。我希望本文能够对您实现配置架构提供一些帮助,并对应该允许什么样的配置选项有所指导。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Grow your business fast with

Suku