PEAR-安装指南-全-
PEAR 安装指南(全)
原文:
zh.annas-archive.org/md5/394e0cc061c0274ab8d9077f10d345d5译者:飞龙
前言
很可能在您使用 PHP 的某个时刻看到过缩写词 PEAR,无论是偶然间还是安装和使用来自 pear.php.net 的包时。如果您已经调查过,您可能已经听说过 PEAR 提供的流行软件,例如 DB 数据库抽象包或 HTML_QuickForm 包。您可能没有意识到的是,PEAR 不仅仅是一个您可以使用的包集合。PEAR 还包含了 PHP 最灵活的安装程序,即 PEAR 安装器。
使用 PEAR 安装器,您不仅可以安装来自 pear.php.net 的包,还可以从其他 PEAR 通道安装包,使用您自己的 PEAR 通道分发您自己的软件项目,甚至使用 PEAR 安装器维护复杂的 Web 项目。惊讶吗?好吧,继续阅读,因为这本书揭示了 PEAR 安装器的秘密以及它将如何用 PHP 编程语言彻底改变您的日常开发!
本书涵盖的内容
第一章 介绍了您对 PEAR 安装器的了解。我们首先回顾了传统的解压并运行方法来分发 PHP 软件,并将其与 PEAR 安装器基于包的分发 PHP 软件方法进行比较。您将看到 PEAR 通道的创新之处,一瞥 PEAR 安装器如何从包中安装文件,以及它是如何知道安装位置的。最后,您将了解获取 PEAR 安装器的多种方法,甚至如何在没有提供 shell 访问的 Web 主机上远程安装 PEAR。
第二章 是所有 PHP 开发者必读的,因为它解释了 package.xml 的基本工作原理,这是 PEAR 安装器的核心。package.xml 用于控制 PEAR 安装器可以做的几乎所有事情。您将了解版本控制对于控制安装包质量的重要性,依赖关系的重要性,以及 PEAR 安装器如何管理库和应用程序之间的重要链接。您还将了解 package.xml 如何组织包元数据,如包名称、作者、发行说明和变更日志,以及如何组织关键安装数据,如文件、依赖关系和版本控制。
第三章 为想要利用 package.xml 版本 2.0 中引入的完整应用程序支持功能的开发者提供了更深入的探讨。
第四章 从查看 PEAR 安装器的细节转向使用 PEAR 安装器开发和维护一个复杂且快速发展的网站。
第五章 涵盖了 PEAR 通道。通道旨在使从任何位置安装包变得容易,但在过程中很难损害您的系统,遵循一个基本的安全原则:始终设计事情,使其最简单的方法是最安全的。
通道打开了pear.php.net对 PEAR 安装程序的垄断,使其面向整个互联网。通过您的通道分发的自定义包甚至可以出售并提供给特定用户,同时与公开可用的开源包和平共处。
第六章 教您如何嵌入 PEAR 安装程序以创建插件管理器。该章节创建了一个假博客应用程序,它提供了无缝查询设计用于分发模板的远程 PEAR 通道服务器的功能。使用 PEAR 安装程序的内部类,我们的博客 Web 应用程序智能地安装和升级模板,具有从 PEAR 安装程序期望的所有复杂性。
惯例
在这本书中,您将找到许多不同信息类型的文本样式。以下是一些这些样式的示例及其含义的解释。
代码有三种样式。文本中的代码单词如下所示:“接下来,您需要使用config-create命令为远程机器创建一个配置文件。”
代码块将如下设置:
<file name="blah.php" role="php">
<tasks:replace from="@DATABASE-URL@" to="database_url"
type="pear-config" />
</file>
当我们希望您注意代码块中的特定部分时,相关的行或项目将被加粗:
if (is_object($infoplugin)) {
$bag = new serendipity_property_bag;
$infoplugin->introspect($bag);
if ($bag->get('version') == $data['version']) {
$installable = false;
} elseif (version_compare($bag->get('version'),
$data['version'], '<')) {
$data['upgradable'] = true;
$data['upgrade_version'] = $data['version'];
$data['version'] = $bag->get('version');
任何命令行输入和输出都应如下编写:
$ pear -c pear.ini remote-install -o DB_DataObject
新术语和重要词汇以粗体字形式引入。您在屏幕上看到的单词,例如在菜单或对话框中,在我们的文本中如下所示:“点击下一步按钮将您带到下一屏幕”。
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小技巧和技巧看起来是这样的。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法,您喜欢或可能不喜欢的地方。读者反馈对我们开发您真正从中受益的标题非常重要。
要发送一般反馈,只需将电子邮件发送到<feedback@packtpub.com>,确保在邮件主题中提及书名。
如果您需要一本书并且希望我们出版,请通过www.packtpub.com上的建议标题表单或发送电子邮件至<suggest@packtpub.com>给我们。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。
下载本书的示例代码
访问 www.packtpub.com/support,并从标题列表中选择这本书来下载本书的任何示例代码或额外资源。然后,将显示可下载的文件。
可下载的文件包含如何使用它们的说明。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫败感,并有助于改进本书的后续版本。如果你发现任何勘误,请通过访问 www.packtpub.com/support,选择你的书籍,点击提交勘误链接,并输入你的勘误详情来报告。一旦你的勘误得到验证,你的提交将被接受,勘误将被添加到现有勘误列表中。现有的勘误可以通过从 www.packtpub.com/support 选择你的标题来查看。
问答
如果你在这本书的某个方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们会尽力解决。
第一章。获取 PEAR:它是什么以及如何获取它?
很可能,您在 PHP 的使用过程中某个时候见过 PEAR 的缩写,无论是偶然还是安装和使用来自pear.php.net的包时。如果您调查过,您可能已经听说过由 PEAR 提供的流行软件,如 DB 数据库抽象包或 HTML_QuickForm 包。您可能没有意识到,PEAR 不仅仅是可以使用的包的集合。PEAR 还包含 PHP 最通用的安装程序,即 PEAR 安装程序。使用 PEAR 安装程序,您不仅可以从pear.php.net安装包,还可以从其他 PEAR 频道安装包,使用您自己的 PEAR 频道分发自己的软件项目,甚至使用 PEAR 安装程序维护复杂的内部网络 Web 项目。惊讶吗?请继续阅读,这本书将揭示 PEAR 安装程序的私密秘密以及它将如何通过 PHP 编程语言彻底改变您的日常开发。
PEAR 的主要目的是支持代码重用。PEAR代表PHP 扩展和应用仓库。PEAR 提供了一个高级安装程序和一个代码仓库,位于pear.php.net。与您可能熟悉的竞争性 PHP 代码仓库,如www.phpclasses.org或通用开发网站www.sourceforge.net不同,所有 PEAR 代码都组织成离散的可重用组件,称为包。一个包由一组文件和一个名为package.xml的描述文件组成,该文件包含有关包内容的元数据,例如包版本、任何特殊依赖项以及包描述和作者等文本信息。
虽然大多数包包含 PHP 代码,但包的内容没有特殊限制。一些包,如pear.php.net/HTML_AJAX,除了 PHP 文件外还提供 JavaScript 文件。在第四章中提到的示例包仅包含 MP3 音乐文件。实际上,您可以将其保存为文件的任何内容都可以在 PEAR 包中分发。
将包从惰性文件组合转换为动态软件包的软件称为PEAR 安装程序,它本身也是一个 PEAR 包,位于pear.php.net/PEAR。换句话说,PEAR 安装程序可以用来升级自己。它确实是一个非常强大的应用程序。
传统上,PHP 软件的发布采用被动安装方法,遵循以下典型步骤:
-
下载包含应用程序所有文件的
.zip或.tar.gz文件 -
将文件解压缩到您网站文档根目录下的一个文件夹中
-
阅读 README 和安装文件
-
执行各种安装后任务,创建文件,检查需求
-
测试它
-
通常,需要在系统级别进行更改(向
php.ini添加扩展,更改php.ini设置,升级 PHP 本身)
由于没有更好的名字,我们将这个 PHP 软件分发系统称为“解压并运行”系统。尽管实际上它对小型、单开发者、低流量网站非常有效,但它包含了一个不立即明显的隐藏成本。关于解压并运行软件安装系统的一个单一事实限制了其最终的有用性:
小贴士
升级解压并运行安装非常困难
在当今快节奏的开发世界中,互联网的一个弱点是安全性。相当常见的是,在软件中发现严重的安全漏洞,需要立即升级以修复。当使用完整的解压并运行软件应用程序时,升级涉及很大的风险。首先,一旦升级完成,如果软件损坏,撤销升级需要从备份中恢复或重新安装旧软件。使用 PEAR 安装程序撤销到早期包版本是一条命令,并且非常直接。
注意
如果代码正在工作,为什么升级是必要的呢?
就在撰写本章的前一个月,我们的托管提供商的网站遭到了入侵。由于一系列不幸的事件,我完全被锁在我们的服务器上,时间过长,我们因延迟收到重要邮件而丢失了业务。
这种妥协的原因是——一个无辜地安装了过时的 CVS 查看程序副本。该程序包含了一个任意的 PHP 执行漏洞,系统管理员没有升级到最新版本,因为升级查看软件非常困难。
如果这个相同的软件以 PEAR 包的形式分发,升级将只需一条命令,如下所示:
$ pear upgrade PackageName
那时丢失的业务永远不会成为问题。在当今世界,升级软件实际上对任何网站(无论大小)的长期成功至关重要。
使用 PEAR 安装程序而不是简单的解压并运行解决方案的优势,在项目复杂性增加时最为明显。让我们看看以下的一些优势:
-
文件冲突是不可能的
-
由不兼容的 PHP 版本/PHP 扩展/PHP 代码引起的问题都由高级依赖关系解析处理
-
由于 PEAR 版本 1.4.0 中引入的革命性新特性 PEAR 通道功能,在不同站点之间分发应用程序开发变得简单(第五章 Chapter 5 专门探讨了 PEAR 通道)
-
所有安装配置都可以以标准化和一致的方式处理所有包——一旦你学会了如何处理一个 PEAR 包;其他所有包都以相同的方式处理。
-
代码的版本控制允许清晰的故障排除,以及回滚破坏代码的更改的能力。
在使用 PEAR 安装程序之前,了解使用 PEAR 安装程序而不是解压并运行的缺点是很重要的:
-
PEAR 安装程序本身必须在开发机器上安装,最好在服务器上安装(尽管由于 1.3.3.1 节中讨论的 PEAR_RemoteInstaller 包,这不再是必需的,使用 PEAR_RemoteInstaller 可以在没有 shell 访问的服务器上进行同步)。
-
如果你在分发自己的包,你需要完全理解
package.xml描述文件,并且可能需要了解 PEAR 通道以便自己设置一个。 -
依赖于相对文件位置的传统方法并不总是可行。这是由于 PEAR 配置的灵活性所致。而不是依赖于
dirname(__FILE__)这样的 PEAR 特定方式,必须使用文件替换任务(在第二章中讨论),例如。 -
在
php.ini之外可能还需要在pear.conf/pear.ini中进行额外的配置(大部分配置是在安装 PEAR 安装程序时处理的)。
使用 PEAR 的传统最大障碍是安装 PEAR 安装程序本身所需的努力,这也是最近改进安装程序安装过程的重点。PHP 5.1.0 或更新的版本带来的创新可能性很大,正如 PHP_Archive PEAR 包(pear.php.net/PHP_Archive)及其兄弟 phar PECL 扩展(pecl.php.net/phar)所证明的那样。这些包使得将应用程序分发给单个文件成为可能,这大大增强了安装程序的功能。
PHP 的民主创新:PEAR 通道
PEAR 安装程序最重要的创新是 PEAR 通道,这是其他任何包分发机制所不支持的东西。PEAR 通道是一种简单的方法,可以轻松地从多个来源通过互联网安装包。
通过使用 PEAR 通道,应用程序可以可靠地依赖于来自多个无关来源的代码。一些更突出的通道包括:
-
pear.php.net:PEAR 本身是一个通道 -
pecl.php.net:PECL 是 PHP 扩展的 PEAR,就像 PEAR 是常规 PHP 包一样 -
gnope.org:PHP-GTK2 通道 -
pear.chiaraquartet.net:php.net域之外的第一个 PEAR 通道 -
components.ez.no:eZ 组件 PEAR 通道 -
pearified.com:PEAR 打包的 Smarty、phpMyAdmin 等的来源
同样值得关注的还有通道聚合器,如:
-
www.pearadise.com:托马斯·施利特的 PEAR 通道聚合器 -
www.pearified.com:pearified 通道的通道聚合部分 -
www.upear.com:另一个聚合器
每个 PEAR 频道都分发适合广泛需求的代码。
什么是 PEAR?是一个代码仓库还是一个安装程序?
PEAR 容易让人混淆,因为这个名字既可以指代 PEAR 安装程序,也可以指代位于http://pear.php.net的 PEAR 仓库。这种混淆自然源于pear.php.net首页上写着“PEAR 是一个用于可重用 PHP 组件的框架和分发系统”,但 PEAR 安装程序的包名却是“PEAR”。实际上,PEAR 既是代码仓库也是安装程序;由于 PEAR 的大部分优势在于能够通过互联网从远程服务器上的包更新本地安装,这两个功能通过不可分割的联系紧密相连。
PEAR 包仓库和 PEAR 频道
位于pear.php.net的 PEAR 包仓库是官方的 PEAR 频道,也是最早的 PEAR 频道,比 PEAR 频道的概念早五年。许多流行的包都是通过pear.php.net分发的,例如HTML_QuickForm, DB_DataObject, MDB2和PhpDocumentor。大多数时候,当人们抽象地提到“PEAR”时,他们谈论的通常是pear.php.net上分发的某个包。
pear.php.net上的包仓库是由 Stig Bakken 和其他一些人于 1999 年建立的,旨在帮助填补 PHP 的空白。PEAR 的第一个可用版本与大约 PHP 版本 4.0.6 同时发布。当时,PEAR 被设计为一个框架,提供了一些基础类和一个用于管理它们的安装程序。PEAR 包提供了两个基础类,PEAR和System。位于PEAR.php文件中的 PEAR 类被设计用来提供基本的错误处理和一些 PHP 内部trigger_error()错误处理之外的其他附加功能。
几个基本包被设计来处理常见任务,例如 DB 用于数据库无关的代码,HTML_Form用于HTML 表单,Log用于基本日志记录,这个列表(长话短说)一直扩展到今天,在撰写本章时,pear.php.net上有 374 个包可用,还有更多通过待定提案正在路上。
自从 PEAR 开始时充满希望以来,它也经历了一些困难。PHP 已经发展,并引入了几个超越 PEAR 提供功能的功能。例如,PHP 5.0.0 中引入的Exception类提供了与PEAR_Error 类类似的功能,PDO 扩展提供了与 DB 包类似的功能,等等。此外,随着 PEAR 的成长,其作为仓库的初始设计中的一些问题浮出水面,导致了小到大的危机。
在这些成长的痛苦中,产生了许多好的东西,包括用于提出新软件包的有效的 PEPr 提案系统,以及向稳定性和创新的双重推动。推动创新的大部分力量来自外部压力,这是由于来自 SolarPHP、eZ components、Phing、Propel、Agavi 和 Zend Framework 等项目的新想法的出现。然而,在 PEAR 中推动创新的显著力量之一就是 PEAR 安装程序本身,特别是新版本 1.4.0 及更高版本中的新特性。
在 PEAR 安装程序的新版本中,一个关键的新特性是 PEAR 通道。PEAR 通道简单来说就是一个提供类似 pear.php.net 这样的软件包的服务器,同时也提供了特殊的网络服务,使得 PEAR 安装程序能够与服务器通信;通道的详细内容在第四章中有介绍。
PEAR 安装程序
大多数讨论 PEAR 的书籍都花费了大量时间在介绍如何使用 PEAR 存储库中的流行软件包(pear.php.net)上,例如 DB(pear.php.net/DB)或 HTML_QuickForm(pear.php.net/HTML_QuickForm)。本书则采取了不同的路线,专注于 PEAR 安装程序本身安装机制周围的强大功能。当你在本书中看到对 PEAR 的引用时,你应该知道这实际上是对 PEAR 安装程序(pear.php.net/PEAR)的引用,而不是对整个存储库的引用。
PEAR 安装程序由四个面向任务的抽象部分组成:
-
package.xml解析器和依赖处理程序 -
文件安装处理器、配置处理器和软件包注册器
-
用户前端和命令处理器
-
远程服务器同步和下载引擎
幸运的是,作为一个最终用户,你实际上并不需要了解或关心这些内容,除了你的应用程序如何与安装程序通信以及人们如何实际使用安装程序来安装你的代码。
首先,你需要了解 PEAR 实际上是如何安装它的包的,因为这与旧的解压缩并运行哲学略有不同。PEAR 将文件分类到不同的类型,并以不同的方式安装每种类型的文件。例如,php 类型的文件将被安装到用户的配置变量php_dir的子目录中。所以如果用户将php_dir设置为/usr/local/lib/php/pear,那么打包在Foo/Bar/Test.php目录中的文件将被安装到/usr/local/lib/php/pear/Foo/Bar/Test.php。数据类型的文件将被安装到用户的配置变量data_dir的子目录中,但与 PHP 文件不同,数据文件将根据包名安装到它们自己的私有子目录中。如果用户将data_dir设置为/usr/local/lib/php/data,并且包名为“TestPackage”,那么打包在Foo/Bar/Test.dat目录中的文件将不会安装到/usr/local/lib/php/data/Foo/Bar/Test.dat,而是安装到/usr/local/lib/php/data/TestPackage/Foo/Bar/Test.dat!关于 PEAR 安装的其他细节,请参阅第二章。
接下来,你需要了解一些关于 PEAR 如何知道一个包中包含哪些文件,它需要什么才能工作(即“这个包仅在 PHP 4.3.0 及更高版本中工作,需要任何版本的 DB_DataObject 包,以及仅 1.2.3 版本的 Log 包,建议安装 1.4.6 版本”),或者它的依赖关系。这些信息也在第二章中有介绍,我们学习了package.xml文件格式。
在阅读了第二章和第三章之后,你应该对 PEAR 有足够的了解,以便管理自己包的本地安装,或者向官方pear.php.net仓库提出新的包建议。如果你希望将你的包分发给他人使用,无论是免费、为私人客户还是销售,你需要了解 PEAR 通道是如何工作的。设置通道服务器是一个相对简单的任务。在撰写本章时,有几种设置通道的选项,但几种潜在的竞争选项即将完成。这是一个参与 PHP 开发令人兴奋的时期!关于如何设置自己的通道,请参阅第五章。
当然,并不是每个人都公开分发他们的代码。大多数开发者都在忙于自己或团队设计自己的网站。当与版本控制系统结合使用时,PEAR 安装程序是管理完整网站的一个非常有效的工具。第四章 详细介绍了这个主题。
简而言之,PEAR 安装程序是管理高质量软件库、高质量应用程序或高质量网站的最有效的工具之一。
安装 PEAR 安装程序
获取 PEAR 有三种方法。第一种方法自 PHP 4.2.0 以来就可用,只需安装 PHP 并配置 --with-pear(在 Unix 上)或从 PHP 目录中运行 go-pear 即可。
小贴士
我正在运行 Windows,我的 PHP 中没有 PEAR,它在哪?
如果你正在运行 PHP 版本 5.1 或更早版本,为了获取捆绑了 PEAR 的 PHP 版本,你需要下载 .zip 文件而不是 Windows 安装程序(.msi)。PHP 5.2.0 及更高版本显著改进了 Windows 安装程序(.msi)的发行版,并且推荐使用它来获取 PEAR。安装程序(至少到目前为止)实际上并不太有用,因为它除非出现关键错误否则永远不会更新。因此,获取 PHP 的真正有用元素需要使用 .zip 文件。不必担心,PHP 在 Windows 上基本上是一个解压即用的语言。
如果我不那么外交,我可能会说些像“甚至不要考虑在 Windows 上运行 PHP 作为生产服务器”这样的话。然而,我认为重要的是要说:“在我看来,在 Windows 上运行 PHP 作为生产服务器既不值得努力也不值得花费。”尽管可以做到,但唯一应该考虑的理由是如果你的老板会在你安装 Unix 时解雇你。或者你为维护 Windows 操作系统的公司工作。在这种情况下,你会得到原谅。
第二种方法是从 pear.php.net/go-pear 获取 go-pear 脚本,并将其保存为 go-pear.php,然后运行此文件。最后,还有一些非官方的 PEAR 获取来源,最著名的是 Gnope 安装程序,它一步到位地设置了 PHP 5.1、PEAR 和 PHP-GTK2。
PHP 捆绑的 PEAR
如上所述,如果你正在运行 Windows,安装 PEAR 相对简单。运行 go-pear 命令将运行安装脚本。此外,自 PHP 版本 5.2.0 以来,有一个基于 Windows 安装程序(.msi 文件扩展名)的优秀安装机制,它在其发行版中捆绑了 PEAR,并推荐使用。对于更早的 PHP 版本,为了获取捆绑的 PEAR,你必须下载 .zip 文件发行版,而不是基于 .msi 的 Windows 安装程序机制。
如果你正在运行 Unix,你需要使用至少以下最小配置行来设置 PEAR:
$ ./configure --enable-cli --enable-tokenizer --with-pcre-regex -- enable-xml
为了充分利用 PEAR,启用zlib扩展以访问压缩的 PEAR 包是个好主意:
$ ./configure --enable-cli --enable-tokenizer --with-pcre-regex -- enable-xml --with-zlib
在安装时,请确保您对 PEAR 将要安装的目录有完整的写访问权限(通常是/usr/local/lib,但可以通过--prefix选项进行配置;/lib附加到前缀)。当您安装 PEAR 时,它也会安装所需的包Console_Getopt和Archive_Tar。
$ make install-pear
Installing PEAR Environment: /usr/local/lib/php/
[PEAR] Archive_Tar - installed : 1.3.1
[PEAR] Console_Getopt installed : 1.2
pear/PEAR can optionally use package "pear/XML_RPC" (version >= 1.4.0)
[PEAR] PEAR - installed : 1.4.9
Wrote PEAR system config file at: /usr/local/etc/pear.conf
You may want to add: /usr/local/lib/php to your php.ini include_path
完成 PEAR 安装后,如安装说明的最后一行所建议的,您将想要确保 PEAR 位于php.ini的include_path设置中。确定这一点最简单的方法是首先找到php.ini:
$ php i |grep php[.]ini
Configuration File (php.ini) Path => /usr/local/lib
这个例子显示php.ini文件位于/usr/local/lib/php.ini。使用您喜欢的编辑器打开此文件,然后找到说"include_path="的行,或者添加一个类似这样的新行:
include_path=.:/usr/local/lib/php
当然,用您特定安装的推荐路径(输出最后一行)替换/usr/local/lib/php。
小贴士
这个所谓的“include_path”是什么意思?
哎呀——是时候再次查阅 PHP 手册了,具体来说,www.php.net/include。总结一下,include语句及其类似项include_once, require和require_once都用于动态包含外部 PHP 代码。如果您传入一个完整路径,例如/path/to/my.php::
include '/path/to/PEAR.php';
然后,自然地/path/to/my.php将替换掉包含语句并执行。然而,如果您传入一个相对路径,例如:
include 'PEAR.php';
这将搜索include_path并尝试找到PEAR.php文件。如果include_path是.:/usr/local/lib/php,那么 PHP 将首先尝试在当前目录(.)中找到PEAR.php,如果没有找到,它将在/usr/local/lib/php/PEAR.php中搜索。这允许在磁盘上自定义库位置,这正是 PEAR 试图实现的目标。
如果您正在运行 Windows,go-pear批处理文件将提示您选择安装 PEAR 的位置,默认为版本 4 的C:\php4,版本 5.0 的C:\php5,或版本 5.1 或更高版本的当前目录。除非您有充分的理由在其他地方安装 PEAR,否则接受默认位置是个好主意,因为它可以节省一些潜在的问题。如果您从版本 5.1.0 或更高版本安装 PEAR,安装程序将询问您是安装本地还是系统安装。
Are you installing a system-wide PEAR or a local copy?
(system|local) [system] :
直接按回车安装系统范围内的 PEAR(一般来说,在安装 PEAR 时,如果您不知道它要求什么,总是选择默认值)。接下来,它将询问您安装 PEAR 副本的位置:
Below is a suggested file layout for your new PEAR installation. To
change individual locations, type the number in front of the
directory. Type 'all' to change all of them or simply press Enter to
accept these locations.
1\. Installation base ($prefix) : C:\php51
2\. Binaries directory : C:\php51
3\. PHP code directory ($php_dir) : C:\php51\pear
4\. Documentation directory : C:\php51\pear\docs
5\. Data directory : C:\php51\pear\data
6\. Tests directory : C:\php51\pear\tests
7\. Name of configuration file : C:\php5\pear.ini
8\. Path to CLI php.exe : C:\php51\.
1-8, 'all' or Enter to continue:
通过输入项目前面的数字并按Enter键来更改您希望更改的任何值。如果您想更改安装基本目录,请输入1并按Enter。如果您想修改所有值,请输入all并按Enter。一旦您满意,请按Enter键而不输入任何内容以继续。此时,它将安装包(应该看起来与 Unix 中 PEAR 的安装类似),最后询问是否要修改php.ini。如果您选择这样做,它将自动更新php.ini中的include_path设置,然后您就可以正常使用了。
5.1.0 及更早版本 PHP 的安装
如果您有 5.1.0 及更早版本的 PHP,并希望安装 PEAR,您不应使用捆绑的 PEAR 版本,而应通过go-pear脚本安装 PEAR。要检索此脚本,请使用任何网页浏览器浏览到pear.php.net/go-pear,并将其保存到磁盘上为go-pear.php,或者在 Unix 上,您有wget命令:
$ wget -O go-pear.php http://pear.php.net/go-pear
一旦您可以访问go-pear.php,只需运行它:
$ php go-pear.php
小贴士
为什么只针对 PHP 5.1.0 及更早版本?
就像有人曾经问过我,“为什么只使用go-pear脚本来安装 5.1.0 及更早版本的 PHP 很重要?当 PHP 6.0.0 发布带有新 PEAR 安装程序的版本时,这不会随时间改变吗?”
这种情况的原因与go-pear的工作方式有关。go-pear脚本是一个出色的设计,它可以直接从 CVS 中提取基于其发布标签的关键文件,然后根据其最新稳定版本下载包并安装它们。虽然很出色,但这是一种危险的方法,如果 CVS 中出现问题,则保证你的安装会中断。
PHP 版本 5.1.0 及更高版本捆绑的 PEAR 安装程序实际上是自包含的,根本不接触互联网,并且经过测试和保证与该版本的 PHP 兼容。一旦安装了 PEAR,获取最新版本既简单又比使用go-pear更安全:
pear upgrade PEAR
输出将与捆绑 PHP 中的go-pear非常相似,但将开始于:
$ php go-pear.php
Welcome to go-pear!
Go-pear will install the 'pear' command and all the files needed by
it. This command is your tool for PEAR installation and maintenance.
Use 'php go-pear.php local' to install a local copy of PEAR.
Go-pear also lets you download and install the PEAR packages bundled
with PHP: DB, Net_Socket, Net_SMTP, Mail, XML_Parser, PHPUnit.
If you wish to abort, press Control-C now, or press Enter to continue:
由于go-pear需要互联网访问,下一个问题将是代理。如果您在防火墙后面,请确保您知道是否需要使用代理:
HTTP proxy (http://user:password@proxy.myhost.com:port), or Enter for none::
安装过程的下一步应该看起来相当熟悉:
Below is a suggested file layout for your new PEAR installation. To change individual locations, type the number in front of the directory. Type 'all' to change all of them or simply press Enter to accept these locations.
1\. Installation prefix : C:\php51
2\. Binaries directory : $prefix
3\. PHP code directory ($php_dir) : $prefix\pear
4\. Documentation base directory : $php_dir\docs
5\. Data base directory : $php_dir\data
6\. Tests base directory : $php_dir\tests
7\. Temporary files directory :
8\. php.exe path : C:\php51\php.exe
1-8, 'all' or Enter to continue:
选择位置后,go-pear将下载所需的代码,并安装 PEAR 包。它还会询问是否要安装一些额外的包,最后是否要修改php.ini。
如果您喜欢冒险,您也可以简单地尝试在网页浏览器中打开go-pear.php。这将安装 PEAR 的 Web 前端,这非常好,但不如命令行安装程序(CLI)活跃。
其他非官方来源
在我们继续之前,重要的是要注意本节中的所有内容都是非官方的,因此不会得到pear.php.net那边的人的支持。你遇到的所有问题都是你下载它的网站的责任!好,不再多说了。
在 www.gnope.org 的 Christian Weiske 一直在努力使 PHP-GTK2 在 PHP 5.1 中工作,并使使用 PEAR 安装 PHP-GTK2 应用程序变得容易。因此,他与我们紧密合作,创建了一个 PEAR 安装程序的 GTK2 前端,现在可以通过 PEAR 在 pear.php.net/PEAR_Frontend_Gtk2 获取。这是 PEAR 的官方 PHP-GTK2 前端。前端要求 PEAR 已经安装,才能使用它。
然而,Christian 已经将事情提升到了一个新的层次,他编写了一个 Windows 安装程序,提供了最新的稳定版 PHP 5.1、PEAR 和 PHP-GTK2,为你设置了一切。如果你是这个过程的新手,这是一个开始使用 PEAR 的好方法。当前版本是 1.0,但这是一个 alpha 版本,所以可能不会完全正常工作。
首先,你应该注意的是,如果你已经设置了 PHP,Windows 中的 PATH 环境变量可能会与现有的 PHP 安装产生一些冲突。简而言之,除非你知道自己在做什么,否则不要尝试同时安装一个常规的 PHP 复制和 gnope PHP。
安装 gnope 非常简单,只需下载安装程序并运行即可。提示与任何其他 Windows 安装一样。
使用 PEAR_RemoteInstaller 同步到无 Shell 访问的服务器
一旦你在开发机器上成功安装了 PEAR,你如何在实时服务器上正确安装 PEAR?对于许多开发者来说,这只是在实时服务器上重复在开发服务器上执行的同一步骤。然而,许多开发者不幸地使用了不提供 shell 但提供 PHP 的共享主机提供商。在过去,这基本上消除了成功安装 PEAR 包的任何可能性。
截至 PEAR 1.4.0,这个问题已经不再存在,多亏了 PEAR_RemoteInstaller 包(pear.php.net/PEAR_RemoteInstaller)。这个包提供了同步定制 PEAR 服务器和远程服务器的特殊命令。与其他解决方案不同,实现此功能所需的唯一工具是开发机器上的 PHP 5 和对 FTP 服务器的访问。
要开始,请将 PEAR_RemoteInstaller 安装到你的开发机器上:
$ pear install PEAR_RemoteInstaller
小贴士
发布限制
注意,在本书编写时,PEAR_RemoteInstaller 的最新版本是 0.3.0,稳定性为 alpha。安装时,你需要追加 alpha 或 -0.3.0,例如:
$ pear install PEAR_RemoteInstaller-alpha
或者
$ pear install PEAR_RemoteInstaller-0.3.0
接下来,你需要使用 config-create 命令为远程机器创建一个配置文件。然而,在这之前,知道你的主机的完整路径是很重要的。确定这个路径最简单的方法是在你的网络主机上运行这个简单的脚本:
<?php
echo __FILE__;
?>
将其保存为 me.php,并上传到你的网络主机。运行时,这将输出类似的内容:
/home/myuser/htdocs/me.php
一旦你有了这些信息,你就可以开始了
$ pear config-create /home/myuser pear.ini
这将显示类似的内容:
CONFIGURATION (CHANNEL PEAR.PHP.NET):
=====================================
Auto-discover new Channels auto_discover <not set>
Default Channel default_channel pear.php.net
HTTP Proxy Server Address http_proxy <not set>
PEAR server [DEPRECATED] master_server <not set>
Default Channel Mirror preferred_mirror <not set>
Remote Configuration File remote_config <not set>
PEAR executables directory bin_dir /home/myuser/pear
PEAR documentation directory doc_dir /home/myuser/pear/docs
PHP extension directory ext_dir /home/myuser/pear/ext
PEAR directory php_dir /home/myuser/pear/php
PEAR Installer cache directory cache_dir /home/myuser/pear/cache
PEAR data directory data_dir /home/myuser/pear/data
PHP CLI/CGI binary php_bin C:\php5\php.exe
PEAR test directory test_dir /home/myuser/pear/tests
Cache TimeToLive cache_ttl <not set>
Preferred Package State preferred_state <not set>
Unix file mask umask <not set>
Debug Log Level verbose <not set>
PEAR password (for password <not set> maintainers)
Signature Handling Program sig_bin <not set>
Signature Key Directory sig_keydir <not set>
Signature Key Id sig_keyid <not set>
Package Signature Type sig_type <not set>
PEAR username (for username <not set> maintainers)
User Configuration File Filename C:\test\pear.ini
System Configuration File Filename #no#system#config#
Successfully created default configuration file "C:\test\pear.ini"
一旦你有了 pear.ini 文件,将其上传到你的远程网络主机,并保存在 /home/myuser 目录(你的 FTP 登录的默认目录)。接下来,我们需要创建 PEAR 的本地工作副本:
$ mkdir remote
$ cd remote
如果你正在运行 Windows:
$ pear config-create -w C:\remote\ pear.ini
否则(假设你在你的开发机器上以用户 foo 运行):
$ pear config-create /home/foo pear.ini
接下来,确定你使用的 FTP 连接类型。你需要知道访问 FTP 的用户名和密码以及主机名。在我们的例子中,我们将使用 myuser 和 password 作为用户/密码组合,以及 yourwebsite.example.com 作为主机。
如果你使用常规 FTP,你可能需要这样做:
$ pear -c pear.ini config-set remote_config ftp://myuser:password@yourwebsite.example.com/
如果你使用 FTPS,你可能需要这样做:
$ pear -c pear.ini config-set remote_config ftps://myuser:password@yourwebsite.example.com/
如果你使用 SFTP,你可能需要这样做:
$ pear -c pear.ini config-set remote_config ssh2.sftp://myuser:password@yourwebsite.example.com/
为了使用 SFTP,你需要从 pecl.php.net ( pecl.php.net/ssh2) 安装 SSH2 扩展,这是一个由 Sara Golemon 编写的优秀扩展。在 Unix 上,这很简单;SSH2 可以通过以下方式安装:
$ pecl install ssh2
在 Windows 上,php_ssh2.dll 应该与你的 PHP 5 发行版一起分发。如果没有,你可以从 pecl4win.php.net 获取一份副本。
一旦我们达到这个阶段,下一步是尝试安装一个包。让我们安装 Savant3,这是一个优秀的 PHP 5+ 模板引擎,它利用 PHP 本身的优雅性作为模板语言。
$ pear -c pear.ini channel-discover savant.pearified.com
$ pear -c pear.ini remote-install savant/Savant3
如果一切正常,你将看到:
远程安装成功:savant.pearified.com/Savant3-3.0.0
到目前为止,启动你的 FTP 浏览器,你会看到文件已上传到 /home/myuser/pear/php/Savant3.php 和 /home/myuser/pear/php/Savant3 目录。
小贴士
关于 alpha/beta 的错误
如果安装失败并显示有关首选状态的提示,请尝试添加 -alpha ,例如:
$ pear -c pear.ini remote-install savant/Savant3-alpha
这将有效,因为它指示安装程序暂时忽略你的 preferred_state 配置变量,并下载最新的 alpha 或更稳定的版本。换句话说,如果首选状态为稳定,如果 Savant3 版本 3.2.0 稳定版可用,它将被安装。然而,如果版本 3.2.1 alpha 可用,一个更新的版本,它将不会安装,因为其稳定性太低。但是,通过添加 -alpha,安装程序被指示获取最新的版本 3.2.1 并安装该版本。
你可能会想,这与手动提取文件并手动上传有什么不同?当管理像 DB_DataObject 或 LiveUser 这样的更复杂的包时,答案就显而易见了。这两个包都有必须安装才能工作的依赖项。虽然可以手动安装并上传,但很容易忘记一个依赖项。更糟糕的是,整个过程在升级时必须重复。
使用 PEAR、Validate 和 DB 依赖项安装 DB_DataObject 非常简单:
$ pear -c pear.ini remote-install o DB_DataObject
同样适用于升级:
$ pear -c pear.ini remote-upgrade o DB_DataObject
卸载也同样简单,并且响应于 PEAR 安装程序的依赖项验证的全功能:
$ pear -c pear.ini remote-uninstall DB_DataObject
小贴士
rsync 方法
如果你正在为客户开发一个复杂的网站,客户有足够的预算,那么最好在具有相同软件和硬件配置的相同机器上开发。在本地设置整个网站,并使用rsync与远程网站同步。即使使用源控制和可安装的 PEAR 包,这也仍然工作得很好,因为开发应该与离散的包版本同步。
摘要
在本章中,我们学习了 PEAR 作为pear.php.net上的代码仓库和作为 PEAR 安装程序的双重性格。我们研究了 PHP 软件的主要分发方法——解压缩并运行,并将其与 PEAR 安装程序的基于包的分发方法进行了比较。在快速了解了令人兴奋的新创新——PEAR 通道之后,我们窥探了 PEAR 安装程序从包中安装文件的基本原理以及它知道在哪里安装它们的方法。最后,我们学习了获取 PEAR 安装程序的各种方法,甚至如何在没有提供 shell 访问权限的 Web 主机上远程安装 PEAR。
第二章。使用 PEAR 安装程序精通 PHP 软件管理
在本章中,我们学习如何将 PHP 软件转换为可分发的 PEAR 软件包。2005 年 9 月,PEAR 安装程序的 1.4.0 版本发布。这是一个里程碑,标志着 PEAR 从 pear.php.net 分发的狭小市场工具转变为一个完整的应用程序安装工具。第一次,可以分发大规模的应用程序,甚至完整的基于网络的数据库密集型应用程序也可以一步安装和配置。
PEAR 安装程序现在可以用来安装传统的基于网络的程序,如 phpMyAdmin 和非 PEAR 库,例如流行的 Smarty 模板引擎(这两个都可以通过 pearified.com PEAR 频道)安装)。PEAR 安装程序的两个主要设计目标是:
-
使应用程序开发能够在多个开发团队之间进行分发(即停止重复造轮子)
-
防止冲突的软件包相互覆盖
所有这些魔法都是通过 package.xml 文件格式实现的。package.xml 是 PEAR 安装程序的核心和灵魂,为了充分利用 PEAR 的功能,你需要了解其结构。package.xml 包含要安装的文件列表,供 PEAR 安装程序区分不同软件包和版本的信息,以及对人类有用的信息,例如软件包的描述和变更日志。实际上,这个文件就是 PEAR 安装程序正确安装软件所需的一切。PEAR 安装程序还使用 package.xml 中的信息,通过 pear package 命令创建可安装的存档,格式为 .tar 或压缩的 .tar(.tgz)。
PEAR 安装程序不仅限于安装本地文件,实际上它是设计用来通过互联网与 PEAR 频道服务器 进行通信的。什么是频道服务器?频道服务器为每个软件包提供可下载的版本,并通过 XML-RPC、REST 或 SOAP 提供关于这些软件包和版本的元信息的网络服务接口。频道在 第五章 中有深入讨论。现在,随着 PEAR 1.4.0+ 和 Chiara_PEAR_Server(pear.chiaraquartet.net/index.php?package=Chiara_PEAR_Server)等软件包的可用,设置自己的 PEAR 频道服务器并分发库和应用程序变得非常简单,这些都可以通过 pear 命令来实现。
小贴士
命令行界面(CLI)与 Web/Gtk2 安装程序
一些阅读此文档的人可能已经安装了 Web 前端(来自 pear.php.net 的 PEAR_Frontend_Web 包)或 Gtk2 前端(来自 pear.php.net 的 PEAR_Frontend_Gtk2 包)。如果是这样,那么你可能已经注意到,从其他渠道安装包甚至更简单,因为最终用户只需选择要安装的渠道,所有包都会列出最新版本和可用的升级。
在接下来的几章中,我们将使用命令行(CLI)前端来处理 PEAR 安装程序,因为与 Web 前端相比,CLI 前端提供了更高级的复杂性,Web 前端设计得更多是为了 PEAR 包的最终用户,而不是 PEAR 包的开发者。在撰写本章时,Gtk2 前端比 Web 安装程序更复杂,如果你运行的是 PHP 5.1.0 或更高版本,那么它值得使用。
例如,如果你的服务器是 pear.example.com,并且你发布了一个名为 Foo 的包,那么你的用户只需要输入以下内容来安装你的包:
$ pear channel-discover pear.example.com
$ pear install pear.example.com/Foo
然而,PEAR 安装程序最动态和最重要的功能是它处理对其他包、PHP 版本、PHP 扩展和系统架构的依赖的复杂方式。通过 package.xml 中的非常简单的语法,可以轻松且安全地管理极其复杂的依赖场景。
在我们开始实际创建自己的包的工作之前,了解 PEAR 安装程序设计背后的核心概念以及你需要如何调整你的软件设计以充分利用其优势是非常重要的。
分发库和应用程序
最重要的是要理解 PEAR 安装程序实际上是如何安装文件的。大多数 PHP 开发者将他们的应用程序作为解压即用的存档进行分发。因此,我们倾向于假设文件将在最终用户机器上的确切相对位置与它们在我们开发机器上的位置相同。然而,PEAR 的设计要灵活得多。
例如,共享 Web 主机通常会安装一个全局的 PEAR 副本,但用户也可以安装本地副本,并使用 php.ini 中的 include_path 来选择当本地包可用时使用本地包,当不可用时使用全局包。为了使这种灵活性成为可能,PEAR 按类型或文件 角色 对组和安装文件。
每个文件角色都有一个相应的配置条目,它定义了所有该文件角色的文件将被安装的位置。例如,PHP 文件角色有一个名为 php_dir 的配置变量,它定义了所有 PHP 文件将被安装到的目录,数据文件角色有一个名为 data_dir 的配置变量,依此类推。
这与传统“解压即用”的哲学有着根本性的区别,因为它允许用户根据自己的需求在他们的机器上以任何方式配置安装位置。这也意味着,在 PEAR 包中使用如 dirname(__FILE__) 这样的巧妙构造来定位已安装文件是危险的编码方式。幸运的是,还有其他更灵活、更安全的巧妙方法可以解决这个问题。
小贴士
PEAR 配置
要检索所有配置变量及其值,请使用 config-show 命令。与文件角色对应的配置变量通常在其名称中包含 _dir,例如 doc_dir 和 php_dir。
这也稍微改变了开发过程:不再是直接在开发目录中运行代码进行测试,而是首先通过其 package.xml 文件安装代码,并以 PEAR 的解压即用方式测试。因此,最常用的 pear 命令是:
$ pear up -f package.xml
或者
$ pear upgrade --force package.xml
此命令允许从(例如)PEAR 版本 1.5.0a1 升级到 PEAR 版本 1.5.0a1,当修复了错误或添加了新功能时。实际上,此命令通过允许在更改后快速替换版本内的现有文件,从而促进了快速开发。换句话说,如果我们每次在开发机器上对微小更改进行更改时都必须更改版本号,这将是非常繁琐的。--force 选项绕过了这个问题。
虽然表面上使用 PEAR 管理安装可能看起来更复杂,但快速调查 有用的 解压即用包表明,许多解压即用包实际上需要大量的手动配置,这些配置可以很容易地被 PEAR 安装程序的自动化功能所取代。简而言之,一旦习惯了它,你就会 wonder 如何在没有使用 PEAR 安装程序来管理你的包的情况下开发 PHP。
从安装程序的角度来看,库和应用程序之间的区别
缩写PEAR代表PHP扩展和应用存储****库,但在 PEAR 版本 1.4.0 之前,对应用程序的支持是有限的。大多数 PEAR 包是设计成可以集成到其他外部应用程序中的库。PEAR 安装程序的初始设计非常有效地支持了这种模式,但并没有提供应用程序安装和配置所需的所有定制化功能。引入 PEAR 版本 1.4.0 的新功能的主要动机之一是更好地支持应用程序的安装。
尽管库和完整应用程序在功能上存在明显的差异,但从安装程序的角度来看,库和应用程序不需要被非常不同地处理。两者都使用package.xml进行分发,都以相同的方式存储在注册表中,并应用相同的版本控制和依赖性规则。实际上,这是 PEAR 安装程序最大的优势之一,因为它既遵循KISS(Keep It Simple, Stupid)原则,又让应用程序设计取决于package.xml的使用方式。因此,为了充分利用其功能,对package.xml的设计有深入理解是必要的。
新特性旨在简化应用程序开发,包括:
-
可定制的文件角色
-
可定制的文件任务
-
更高级的依赖性可能性和预下载依赖性解析
-
将多个软件包打包成一个单一的 tar 包的能力
-
基于单个发布版本而非抽象包的静态依赖性
在阅读完这个列表后,如果你的头脑发晕或者你看到类似问号的斑点,不要担心——所有这些特性将在接下来的几节中简单而详尽地探讨。
在我们深入探讨新特性之前,让我们更仔细地看看一些基本原理,这些原理为最佳使用新特性提供了基础。
使用版本控制和依赖性帮助跟踪和消除错误
版本控制和依赖性是每个企业级分发系统必须非常支持的两大特性。有了简单的依赖性和高级版本控制功能,PEAR 安装程序使得依赖外部包比以往任何时候都更加安全和容易。
版本控制
PEAR 安装程序最基本的基础是版本控制的概念。版本控制应该对我们所有人都很熟悉,以“软件包版本 X.Y.Z”的形式,例如“PHP 版本 5.1.4”。基本思想是,软件的旧版本号较低。换句话说,PHP 版本 4.0.6 比 PHP 版本 4.1.1beta1 旧,而 PHP 版本 4.1.1beta1 又比 PHP 版本 4.1.1 旧。
版本控制如何帮助跟踪和消除错误?想象以下场景:
你正在处理一个 Wiki,允许用户随时从你的 FTP 站点获取源代码并自行使用。其中一位用户发现了一个错误并报告说:“它正在做一件奇怪的事情,试图删除我所有的文件。”用户无法记得他或她何时下载了源代码,因为他或她不得不从备份中恢复,并且文件修改时间已被重置。在这种情况下,唯一确定问题是否存在以及它是否仍然存在于当前源代码中的方法是从用户项目获取并逐行与当前源代码进行比较。在最坏的情况下,这可能是繁琐的,在最坏的情况下是完全不可能的。
对于一个流动的、不断变化的软件项目,在特定时间发布版本并分配版本号,使得最终用户报告错误变得更加简单。最终用户可以简单地说明“您的软件包的 1.2.3 版本做了这样奇怪的事情,试图删除我所有的文件”,然后您作为开发者可以要求用户尝试最新版本或当前的开发副本,这使得错误修复变得更加简单。
此外,还可能维护同一代码的两个分支,一个是稳定版本,另一个是不稳定版本,具有创新的新功能。有许多微妙的方法可以使用版本管理来提供有关软件发布的更多信息。例如,Linux 内核版本管理系统在 en.wikipedia.org/wiki/Linux_kernel#Version_Numbering 中有详细描述。在这种情况下,小数点之一用于表示内核的稳定性,因此可以在 1.3.0 之后发布版本 1.2.9。
PEAR 安装程序在版本管理上采取了一种更明确的方法。它不是在版本号内部提供稳定性信息,而是在 package.xml 中的单独字段 <stability> 中使用来指定代码的稳定性。这并不妨碍使用 Linux 风格的版本管理或任何其他版本管理方案,只要基本前提(1.3.0 总是比 1.2.9 新)仍然成立。
小贴士
版本时间与绝对时间
在现实世界中,PEAR 的简单版本管理规则实际上可能会有些令人困惑。PEAR 安装程序并不特别关心版本在现实世界中的发布时间,而是关注其稳定性和版本号。
这意味着,例如,2005 年 8 月 18 日发布的 PEAR 1.3.6(稳定版)实际上比 2005 年 2 月 26 日发布的 PEAR 1.4.0a1(alpha 版)更老,因为 1.3.6 小于 1.4.0。绝对时间与版本时间无关。
最终用户可以通过一个名为 preferred_state 的配置变量来告诉 PEAR 安装程序,为了安装,必须保证软件包的稳定性。在我们的假设例子中,如果这个变量设置为 stable 或 beta,那么将安装 PEAR 1.3.6 而不是 1.4.0a1;否则,对于比 beta 版本稳定性低的值(alpha、devel 和 snapshot),将安装 1.4.0a1,尽管 1.3.6 版本是在几个月后发布的,因为在版本时间上 1.4.0 总是比 1.3.6 更新。
PEAR 打包和严格的版本验证
PEAR 软件包仓库位于 pear.php.net,使用严格的版本管理协议。所有软件包版本都是 X.Y.Z 格式,例如 "1.3.0"。第一个数字(X)用于描述应用程序接口(API)版本,第二个数字(Y)用于描述功能集版本,第三个数字(Z)用于描述错误修复修订级别。以下是一些版本号及其含义的示例:
| 样例 PEAR 版本号及其含义 | |
|---|---|
| 版本号 | 含义 |
| --- | --- |
| 1.0.0 | 稳定的 API 版本 1.0,包的初始稳定发布 |
| 0.4.5 | 不稳定的(开发中)API,第四个功能集设计,该功能集的第五次错误修复发布 |
| 2.4.0 | 稳定的 API 版本 2.0,第四个功能集设计,该新功能集的初始发布 |
| 1.3.4 | 稳定的 API 版本 1.0,自初始稳定发布以来的第三个新功能集,该功能集的第四次错误修复发布 |
在安装时,此信息是无关紧要的:PEAR 安装程序将安装它所提供的任何内容,包括类似于 24.25094.39.430 或 23.4.0-r1 等版本编号方案。然而,通过以下方式打包包时使用的验证方式:
$ pear package
与下载 PEAR 包时使用的验证方式相比要严格得多。为了帮助试图满足 PEAR 包仓库严格编码标准的开发者,PEAR 包中位于PEAR/Validate.php的验证例程集合会检查 PEAR 编码标准的多个重要方面,并对任何不符合标准的内容发出警告。例如,版本 1.0.0 必须是稳定的(而不是devel, alpha或beta稳定性),因此以下来自package.xml的片段将导致警告:
<version>
<release>1.0.0</release>
<api>1.0.0</api>
</version>
<stability>
<release>beta</release>
<api>beta</api>
</stability>
具体来说,它看起来可能像这样:
$ pear package
Warning: Channel validator warning: field "version" - version 1.0.0 probably should not be alpha or beta
尽管这对希望遵守 PEAR 严格编码标准的人来说非常有帮助,但在某些情况下,这种警告并不有用,反而令人烦恼或分心。例如,一个私有项目可能希望使用 Linux 版本控制方案。幸运的是,有一种方法可以控制在 PEAR 安装程序的打包、下载或安装阶段处理自定义验证例程的方式。通过扩展位于PEAR/Validate.php中的PEAR_Validate类,可以使用面向对象继承创建一个特殊的验证器。要激活它,验证器必须与一个频道相关联。尽管这个过程将在第五章中详细讨论,但这里有一个简单的示例。
pecl.php.net上的 PHP 扩展开发者有一个更宽松的版本验证系统,接受的版本控制差异很大,从两位数的 1.0 到 maxdb 包(pecl.php.net/maxdb)使用的 7.5.00.26,再到 MySQL 的 MaxDB 数据库使用的镜像版本控制。因此,pecl.php.net是pear.php.net的一个独立频道,在其频道定义文件channel.xml(也在第五章中详细讨论)中定义了一个频道验证器:
<validatepackage version="1.0">PEAR_Validator_PECL</validatepackage>
这个特定的频道验证器与 PEAR 安装程序一起分发,在文件PEAR/Validator/PECL.php中。这个文件是定制频道验证器的完美示例,也是最简单的,所以在这里,展示其全部的辉煌:
<?php
/**
* Channel Validator for the pecl.php.net channel
*
* PHP 4 and PHP 5
*
* @category pear
* @package PEAR
* @author Greg Beaver <cellog@php.net>
* @copyright 1997-2005 The PHP Group
* @license http://www.php.net/license/3_0.txt PHP License 3.0
* @version CVS: $Id: PECL.php,v 1.3 2005/08/21 03:31:48 cellog
Exp $
* @link http://pear.php.net/package/PEAR
* @since File available since Release 1.4.0a5
*/
/**
* This is the parent class for all validators
*/
require_once 'PEAR/Validate.php';
/**
* Channel Validator for the pecl.php.net channel
* @category pear
* @package PEAR
* @author Greg Beaver <cellog@php.net>
* @copyright 1997-2005 The PHP Group
* @license http://www.php.net/license/3_0.txt PHP License 3.0
* @version Release: @package_version@
* @link http://pear.php.net/package/PEAR
* @since Class available since Release 1.4.0a5
*/
class PEAR_Validator_PECL extends PEAR_Validate
{
function validateVersion()
{
if ($this->_state == PEAR_VALIDATE_PACKAGING) {
$version = $this->_packagexml->getVersion();
$versioncomponents = explode('.', $version);
$last = array_pop($versioncomponents);
if (substr($last, 1, 2) == 'rc') {
$this->_addFailure('version', 'Release Candidate versions must have ' .
'upper-case RC, not lower-case rc');
return false;
}
}
return true;
}
function validatePackageName()
{
$ret = parent::validatePackageName();
if ($this->_packagexml->getPackageType() == 'extsrc') {
if (strtolower($this->_packagexml->getPackage()) !=
strtolower($this->_packagexml->getProvidesExtension())) {
$this->_addWarning('providesextension', 'package name "' .
$this->_packagexml->getPackage() . '" is different from extension name "' .
$this->_packagexml->getProvidesExtension());
}
}
return $ret;
}
}
?>
这个类只是简单地覆盖了其父类的严格版本验证,然后添加了一个 PECL 特定的检查,以查看一个扩展包是否声称分发一个与包名不同名称的扩展。此外,它还会检查版本中是否包含(小写)rc作为候选发布版本,因为 PHP 的version_compare()函数(www.php.net/version_compare)对此处理方式与包含(大写)RC的版本非常不同。
小贴士
为什么 pear.php.net 和 pecl.php.net 是独立的频道?
在早期的 PEAR 版本中,pear命令用于安装 PEAR 包和PHP 扩展社区库(PECL)包。由于多个原因,这一变化发生在 1.4.0 版本中。首先,pear.php.net的开发者正在开发用 PHP 编写的包。而pecl.php.net的开发者正在开发用 C 编写的包,这些包被编译成共享的.dll或.so文件,作为 PHP 本身的内部组件(扩展)。与 PECL 风格的包相比,PEAR 风格的包在安装和维护方面存在固有的差异。
这些差异之一是正确版本的重要性。PECL 和 PHP 扩展不能与冲突的扩展共存;一次只能运行一个。PEAR 包没有这种限制,因为多个不同版本的 PEAR 包可以共存并通过include_path进行交替加载,因此版本控制变得尤为重要。
此外,由于文件锁定,加载到内存中的扩展无法卸载,这使得在不使用php.ini的情况下升级 PHP 扩展成为不可能。
有许多其他小原因加在一起导致了需要分割,例如对包的功能的混淆(这是一个 PHP 扩展还是用 PHP 编写的脚本?),因此现在我们既有pear也有新的pecl命令来管理 PECL 包。
企业级依赖管理
依赖关系是软件设计中的自然演变。如果您的论坛包尝试通过使用模板引擎将业务逻辑与显示分离,那么将精力集中在开发论坛功能上要比设计一个新的模板引擎好得多。不仅现有模板引擎的作者在模板引擎上花费了更多的时间和精力,而且他们的模板引擎已经被成千上万的开发者(就像您一样)使用。所有常见问题和大多数不寻常问题都已被遇到并解决。如果在您发现新的问题时,您可以向维护者报告,甚至可以充分利用开源的力量自行修复问题并将解决方案反馈给维护者。
另一方面,这需要信任模板引擎的维护者。通过使用模板引擎,您隐含地信任维护者能够有效地管理它,修复发现的全部错误,并防止在未来的版本中引入新的错误。您还信任维护者会继续维护该包,并回应您遇到的问题。
PEAR 开发者已经存在很长时间,知道在最坏的情况下,这种信任可能是天真的或愚蠢的,因为软件仍然由有能力出错的人类维护。幸运的是,由于这种知识,package.xml 提供了对依赖关系“信任”的完全控制。
小贴士
package.xml 中的依赖关系
关于 package.xml 中依赖关系的工作方式,请参阅 外部依赖关系 部分
最简单的依赖关系会告诉安装程序必须使用该包,但我们不需要检查版本问题:只要安装即可。在 package.xml 2.0 中,这种依赖关系看起来像这样:
<package>
<name>Dependency</name>
<channel>pear.php.net</channel>
</package>
一个稍微更严格的依赖关系可能会告诉安装程序只安装比最小版本 1.2.0 新的版本:
<package>
<name>Dependency</name>
<channel>pear.php.net</channel>
<min>1.2.0</min>
</package>
进一步来说,可能存在一些混乱的发布。我们可以告诉安装程序忽略特定的发布版本 1.2.0 和 1.4.2,但其他所有比 1.2.0 新的版本都可以安装:
<package>
<name>Dependency</name>
<channel>pear.php.net</channel>
<min>1.2.0</min>
<exclude>1.2.0</exclude>
<exclude>1.4.2</exclude>
</package>
最后,我们还可以告诉安装程序,我们强烈推荐版本 1.4.5,并且除非我们说可以升级,否则不要升级:
<package>
<name>Dependency</name>
<channel>pear.php.net</channel>
<min>1.2.0</min>
<recommended>1.4.5</recommended>
<exclude>1.2.0</exclude>
<exclude>1.4.2</exclude>
</package>
在最后一种情况下,我们得到了对依赖关系的大量控制。最终用户在依赖关系的维护者与您合作并认证其与另一个标签 <compatible> 的兼容性,或者您发布了一个推荐新版本的包之前,不会意外地通过升级到依赖关系的新版本而破坏我们的包。
package.xml 中依赖项提供的不同信任级别意味着您可以安全地依赖其他软件包,但这只是 PEAR 依赖项功能的开始。依赖项解决的另一个常见问题是与最终用户的计算机、操作系统、PHP 版本或 php.ini 中启用的扩展的基本不兼容性。PEAR 为这些情况中的每一个都提供了依赖项标签。此外,可选功能或插件可以通过可选依赖项或依赖组实现。可能性的列表令人印象深刻,而 package.xml 中的语法简单且易于学习。
最终用户的分发和升级
从最终用户的角度来看,在使用解压即用的软件包时,面临的最复杂任务之一就是升级。在闭源世界中,软件包的新版本会破坏旧版本中曾经正常工作的某些功能,以此来强迫用户升级,这有时需要大量的工作。
在开源世界中,许多开发者通过引入令人兴奋的新功能继续遵循这一模式,这意味着您将无法再使用旧版本。实际的升级过程通常意味着用新文件覆盖当前版本,可能还需要新的配置。此外,它还带来了完全破坏实时网站的可怕前景,这促使需要某种备份系统。
通过使用 PEAR 安装程序,所有这些恐惧和危险都已成为过去。使用以下方法升级到新版本非常简单:
$ pear upgrade Package
将软件包降级到旧版本同样简单:
$ pear upgrade --force Package-1.2.3
这使得维护一个实时网站既简单又安全——这是一个罕见而美好的组合。请注意,此示例假设软件包的旧版本为 1.2.3。
此外,升级或安装依赖项同样简单。对于使用 package.xml 2.0 的软件包,总是下载并安装所需的依赖项,而对于使用 package.xml 1.0 的软件包,可以使用 --onlyreqdeps(仅必需依赖项)选项自动下载和安装依赖项,如下所示:
$ pear upgrade --onlyreqdeps Package
可以使用 --alldeps(所有依赖项)选项自动安装可选依赖项:
$ pear upgrade --alldeps Package
此外,通过依赖组创建功能组,或者将几个相关软件包组合成一个单一依赖项,可以轻松实现,这意味着用户可以像安装 PEAR 的网络安装程序功能一样安装功能组:
$ pear install PEAR#webinstaller
当配置一个软件包时,通常需要执行大量工作来配置文件位置、设置数据库等。PEAR 提供了一些简单的方法来自动化配置,同时也提供了通过安装后脚本标准化任何复杂设置的方法。
如果您像我一样,想到内置在 PEAR 安装程序中的所有可能性会让您的心跳加速,充满期待。即使您不是那么极客,我也相信您会发现 PEAR 安装程序的力量会让您的编程生活变得更轻松。
package.xml结构概述
package.xml包含了 PEAR 安装程序安装和配置 PEAR 包所需的所有信息。通过利用 XML 和 XSchema 等标准来记录包结构(pear.php.net/dtd/package1.xsd和pear.php.net/dtd/package2.xsd分别提供了package.xml 1.0和package.xml 2.0的完整定义),PEAR 打开了未来编程的可能性,否则这些可能性将不可用。例如,使用 XSchema 允许通过 XML 命名空间扩展package.xml,以提供自定义功能。
讨论到package.xml时,理解package.xml 1.0和package.xml 2.0之间的共性和差异是很重要的。package.xml 2.0是package.xml 1.0的超集。换句话说,可以将每个可能的package.xml版本 1.0 表示为package.xml 2.0,但有一大批package.xml 2.0不能简化为唯一的package.xml 1.0。
了解package.xml 1.0的结构最好的方法是查看 PEAR 仓库的 CVS(cvs.php.net)。每个子目录都包含一个package.xml文件。如果该package.xml文件以<package version="1.0"...开头,那么它就是package.xml 1.0。许多包已经利用了package.xml 2.0,使用名为package2.xml的文件。同时,调查 PECL 仓库cvs.php.net/pecl/也很有用,看看 PHP 扩展开发者是如何使用package.xml的。
要探索等效的package.xml 2.0,有一个方便的 PEAR 命令可以将package.xml 1.0转换为package.xml 2.0。该命令的调用方式如下:
$ pear convert
这将解析当前目录中名为package.xml的文件,并以新格式输出名为package2.xml的文件。
此外,关于package.xml的完整最新文档始终可在 PEAR 手册的pear.php.net/manual/中找到。
为了这本书的目的,我们将主要讨论package.xml 2.0,并且只提及 1.0 版本,以供熟悉旧格式的人了解重要的概念性变化。
这里是一个package.xml文件的示例。这个示例是从 PEAR 包本身取出的,展示了将在后续章节中探讨的package.xml的一些功能。您可以随意浏览这个示例,稍后再回来参考。现在,只需吸收基本结构,具体细节将在您阅读文本的后续部分时变得有意义。
<?xml version="1.0" encoding="UTF-8"?>
<package version="2.0"
xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>PEAR</name>
<channel>pear.php.net</channel>
<summary>PEAR Base System</summary>
<description>The PEAR package contains:
* the PEAR installer, for creating, distributing
and installing packages
* the beta-quality PEAR_Exception PHP5 error handling mechanism
* the beta-quality PEAR_ErrorStack advanced error handling class
* the PEAR_Error error handling mechanism
* the OS_Guess class for retrieving info about the OS
where PHP is running on
* the System class for quick handling of common operations
with files and directories
* the PEAR base class
</description>
<lead>
<name>Greg Beaver</name>
<user>cellog</user>
<email>cellog@php.net</email>
<active>yes</active>
</lead>
<lead>
<name>Stig Bakken</name>
<user>ssb</user>
<email>stig@php.net</email>
<active>yes</active>
</lead>
<lead>
<name>Tomas V.V.Cox</name>
<user>cox</user>
<email>cox@idecnet.com</email>
<active>yes</active>
</lead>
<lead>
<name>Pierre-Alain Joye</name>
<user>pajoye</user>
<email>pajoye@pearfr.org</email>
<active>yes</active>
</lead>
<helper>
<name>Martin Jansen</name>
<user>mj</user>
<email>mj@php.net</email>
<active>no</active>
</helper>
<date>2005-10-09</date>
<version>
<release>1.4.2</release>
<api>1.4.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.php.net/license">PHP License</license>
<notes>
This is a major milestone release for PEAR. In addition to several killer features, every single element of PEAR has a regression test, and so stability is much higher than any previous PEAR release, even with the beta label.
New features in a nutshell:
* full support for channels
* pre-download dependency validation
* new package.xml 2.0 format allows tremendous flexibility * while maintaining BC support for optional dependency groups * and limited support for sub-packaging robust dependency support
* full dependency validation on uninstall
* remote install for hosts with only ftp access - * no more problems with restricted host installation
* full support for mirroring
* support for bundling several packages into a single tarball
* support for static dependencies on a uri-based package
* support for custom file roles and installation tasks
</notes>
<contents>
<dir name="/">
<dir name="OS">
<file name="Guess.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
</dir> <!-- /OS -->
<dir name="PEAR">
<dir name="ChannelFile">
<file name="Parser.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
</dir> <!-- /PEAR/ChannelFile -->
<dir name="Command">
<file name="Auth-init.php" role="php"/>
<file name="Auth.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
<file name="Build-init.php" role="php"/>
<file name="Build.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
[snip...]
</dir> <!-- /PEAR -->
<dir name="scripts" baseinstalldir="/">
<file name="pear.bat" role="script">
<tasks:replace from="@bin_dir@" to="bin_dir" type="pear-config"
/>
<tasks:replace from="@php_bin@" to="php_bin" type="pear-config"
/>
<tasks:replace from="@include_path@" to="php_dir" type="pear-
config" />
<tasks:windowseol/>
</file>
<file name="peardev.bat" role="script">
<tasks:replace from="@bin_dir@" to="bin_dir" type="pear-config"
/>
<tasks:replace from="@php_bin@" to="php_bin" type="pear-config"
/>
<tasks:replace from="@include_path@" to="php_dir" type="pear-config" />
<tasks:windowseol/>
</file>
<file name="pecl.bat" role="script">
<tasks:replace from="@bin_dir@" to="bin_dir" type="pear-config"
/>
<tasks:replace from="@php_bin@" to="php_bin" type="pear-config"
/>
<tasks:replace from="@include_path@" to="php_dir" type="pear-config" />
<tasks:windowseol/>
</file>
<file name="pear.sh" role="script">
<tasks:replace from="@php_bin@" to="php_bin" type="pear-config"
/>
<tasks:replace from="@php_dir@" to="php_dir" type="pear-config"
/>
<tasks:replace from="@pear_version@" to="version" type="package-info" />
<tasks:replace from="@include_path@" to="php_dir" type="pear-config" />
<tasks:unixeol/>
</file>
[snip...]
</dir> <!-- /scripts -->
<file name="package.dtd" role="data" />
<file name="PEAR.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
<file name="System.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
<file name="template.spec" role="data" />
</dir> <!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>4.2</min>
</php>
<pearinstaller>
<min>1.4.0a12</min>
</pearinstaller>
<package>
<name>Archive_Tar</name>
<channel>pear.php.net</channel>
<min>1.1</min>
<recommended>1.3.1</recommended>
<exclude>1.3.0</exclude>
</package>
<package>
<name>Console_Getopt</name>
<channel>pear.php.net</channel>
<min>1.2</min>
<recommended>1.2</recommended>
</package>
<package>
<name>XML_RPC</name>
<channel>pear.php.net</channel>
<min>1.4.0</min>
<recommended>1.4.1</recommended>
</package>
<package>
<name>PEAR_Frontend_Web</name>
<channel>pear.php.net</channel>
<max>0.5.0</max>
<exclude>0.5.0</exclude>
<conflicts/>
</package>
<package>
<name>PEAR_Frontend_Gtk</name>
<channel>pear.php.net</channel>
<max>0.4.0</max>
<exclude>0.4.0</exclude>
<conflicts/>
</package>
<extension>
<name>xml</name>
</extension>
<extension>
<name>pcre</name>
</extension>
</required>
<group name="remoteinstall" hint="adds the ability to install
packages to a remote ftp server">
<subpackage>
<name>PEAR_RemoteInstaller</name>
<channel>pear.php.net</channel>
<min>0.1.0</min>
<recommended>0.1.0</recommended>
</subpackage>
</group>
<group name="webinstaller" hint="PEAR's web-based installer">
<package>
<name>PEAR_Frontend_Web</name>
<channel>pear.php.net</channel>
<min>0.5.0</min>
</package>
</group>
<group name="gtkinstaller" hint="PEAR's PHP-GTK-based installer">
<package>
<name>PEAR_Frontend_Gtk</name>
<channel>pear.php.net</channel>
<min>0.4.0</min>
</package>
</group>
</dependencies>
<phprelease>
<installconditions>
<os>
<name>windows</name>
</os>
</installconditions>
<filelist>
<install as="pear.bat" name="scripts/pear.bat" />
<install as="peardev.bat" name="scripts/peardev.bat" />
<install as="pecl.bat" name="scripts/pecl.bat" />
<install as="pearcmd.php" name="scripts/pearcmd.php" />
<install as="peclcmd.php" name="scripts/peclcmd.php" />
<ignore name="scripts/peardev.sh" />
<ignore name="scripts/pear.sh" />
<ignore name="scripts/pecl.sh" />
</filelist>
</phprelease>
<phprelease>
<filelist>
<install as="pear" name="scripts/pear.sh" />
<install as="peardev" name="scripts/peardev.sh" />
<install as="pecl" name="scripts/pecl.sh" />
<install as="pearcmd.php" name="scripts/pearcmd.php" />
<install as="peclcmd.php" name="scripts/peclcmd.php" />
<ignore name="scripts/pear.bat" />
<ignore name="scripts/peardev.bat" />
<ignore name="scripts/pecl.bat" />
</filelist>
</phprelease>
<changelog>
<release>
<version>
<release>1.3.6</release>
<api>1.3.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<date>2005-08-18</date>
<license>PHP License</license>
<notes>
* Bump XML_RPC dependency to 1.4.0
* return by reference from PEAR::raiseError()
</notes>
</release>
<release>
<version>
<release>1.4.0a1</release>
<api>1.4.0</api>
</version>
<stability>
<release>alpha</release>
<api>alpha</api>
</stability>
<date>2005-02-26</date>
<license uri="http://www.php.net/license/3_0.txt">PHP
License</license>
<notes>
This is a major milestone release for PEAR. In addition to several
killer features,
every single element of PEAR has a regression test, and so
stability is much higher
than any previous PEAR release, even with the alpha label.
New features in a nutshell:
* full support for channels
* pre-download dependency validation
* new package.xml 2.0 format allows tremendous flexibility while
maintaining BC
* support for optional dependency groups and limited support for
sub-packaging
* robust dependency support
* full dependency validation on uninstall
* support for binary PECL packages
* remote install for hosts with only ftp access - * no more problems
with
restricted host installation
* full support for mirroring
* support for bundling several packages into a single tarball
* support for static dependencies on a url-based package
Specific changes from 1.3.5:
* Implement request #1789: SSL support for xml-rpc and download
* Everything above here that you just read
</notes>
</release>
[snip...]
</changelog>
</package>
package.xml 1.0 和 2.0 之间的共享标签
如果你已经对package.xml 1.0有基本的了解,只想查看package.xml 2.0中的变化,最好浏览接下来的几节内容。每个部分的差异总是在开头展示,随后深入探讨变化背后的原因。
在深入探讨package.xml 2.0的高级新功能之前,了解从package.xml 1.0继承下来的标签和属性非常重要。这两种格式都有共同的标签。大多数标签没有变化。少数标签增加了信息或稍微更改了名称,而其他标签则被完全重新设计。让我们开始吧。
包元数据
两个版本的package.xml都提供了类似的包元数据。任何包都必须包含的一些基本信息包括:
-
包名称/频道
-
维护者(作者)
-
包描述
-
包摘要(单行描述)
这些信息在版本之间通常保持不变。例如,包名称和频道是恒定的。包描述和摘要很少改变。维护者可能会更频繁地改变,这取决于社区,因此尽管这被归类为包元数据,但将其视为基于发布的信息可能是合理的。
包名称/频道
这两个字段位于package.xml的开头,是 PEAR 安装程序区分包的核心。它们就像数据库表中的主键:包/频道 = 唯一的包。请注意,<channel>和<uri>标签仅在package.xml 2.0中存在。
<name>Packagename</name>
<channel>channel.example.com</channel> -or- <uri>http://www.example.com/Packagename-1.2.3</uri>
小贴士
频道和 package.xml 1.0
如你之前所学的,频道的概念是在package.xml 2.0中引入的。当它不是其规范的一部分时,package.xml 1.0是如何定义频道的呢?
PEAR 以一种简单的方式处理这个问题:使用package.xml 1.0打包的所有包都被安装,就好像在包名称声明之后立即插入了一个<channel>pear.php.net</channel>标签。请注意,pecl.php.net上的使用package.xml 1.0的包在升级到使用package.xml 2.0时允许迁移到pecl.php.net频道,但所有其他包必须从新的频道重新开始。
包名称使用<name>标签声明,并且必须以字母开头,否则只能包含字母、数字和下划线字符,除非频道特定的验证器允许包名称采用其他格式。频道特定的验证器在第五章中有详细说明。如果你只是创建一个包,你不需要了解任何关于频道特定验证器的内容。如果你的包满足包的要求,当你运行:
$ pear package
你的包将会无错误地创建。否则,打包将会失败,并且会显示一个或多个错误消息,描述失败的原因。
<channel>标签不能与<uri>标签共存。使用<uri>标签的包实际上是伪频道__uri的一部分,这个频道没有与之关联的服务器或协议。__uri频道实际上是一个真正的魔法频道,它只用来作为命名空间,防止基于 URI 的包与其他频道的包冲突。
例如,考虑一个package.xml文件开始的包:
<name>Packagename</name>
<uri>http://pear.php.net/Packagename-1.2.3</uri>
即使包的版本号为1.2.3,这个包与package.xml以以下代码开始的包也不相同。
<name>Packagename</name>
<channel>pear.php.net</channel>
基于 URI 的包是严格基于发布的。<uri>标签必须是一个绝对的真实 URI,可以用来访问该包。然而,URI 不应该包含.tgz或.tar文件扩展名,但两者都应该存在。在我们的例子中,pear.php.net/Packagename-1.2.3.tgz和pear.php.net/Packagename-1.2.3.tar都应该存在,并且除了.tgz使用 zlib 压缩外,两者应该是相同的。
维护者(作者)
package.xml中维护者的列表应该起到与传统AUTHORS文件相同的作用。然而,这个作者列表不仅仅是有用的信息。《user》标签被频道服务器用来将包维护者与包的发布匹配起来,允许非频道管理员上传他们维护的软件包的发布版本。例如,如果你维护的是通过pear.php.net发布的包,那么<user>标签必须包含你的 PEAR 用户名。对于非频道发布,这些标签的内容仅作为信息,让包的最终用户知道如何联系你。
package.xml 1.0中的<role>标签只能包含lead、developer、contributor或helper的值。这证明使用 XSchema 进行验证是不可能的,因此为了简化事情,package.xml 2.0已经将<role>标签的内容提取出来,并创建了新的标签<lead>、<developer>、<contributor>和<helper>。此外,<maintainers>和<maintainer>标签已被移除以简化解析。
对于package.xml 1.0:
<maintainers>
<maintainer>
<user>pearserverhandle</user>
<role>lead</role>
<name>Dorkus McForkus</name>
<email>dorkus@example.com</email>
</maintainer>
<maintainer>
<user>pearserverhelper</user>
<role>helper</role>
<name>Horgie Borgie</name>
<email>borgie@example.com</email>
</maintainer>
</maintainers>
对于package.xml 2.0:
<lead>
<name>Dorkus McForkus</name>
<user>pearserverhandle</user>
<email>dorkus@example.com</email>
<active>yes</active>
</lead>
<helper>
<name>Horgie Borgie</name>
<user>pearserverhelper</user>
<email>borgie@example.com</email>
<active>no</active>
</helper>
包描述和摘要
下面的示例摘要/描述对显示了如何在package.xml 1.0和package.xml 2.0中使用<summary>和<description>标签。这些标签仅用于信息,不被 PEAR 安装程序的安装部分使用。如果包通过频道提供,list-all等命令将显示摘要。当用户输入info或remote-info等命令以显示特定包或发布的详细信息时,将显示描述。
确保这些标签清晰、简洁且易于理解。我经常看到像GBRL这样的摘要,用于名为File_GBRL的包——如果这些缩写不是众所周知,请定义它们!
<summary>Provides an interface to the BORG collective</summary>
<description>
The Net_BORG package uses PHP to interface with the neural
Implants found in the face of all BORG. Both an object-oriented
and a functional interface is available. Both will be used.
Resistance is futile.
</description>
基本发布元数据
package.xml的其余部分通常是特定于发布的资料,但有少数例外。特定于发布的资料比包名称等事物更容易变化。具体来说,package.xml中记录的特定于发布的资料区域包括:
-
包版本
-
包稳定性
-
外部依赖
-
发布说明
-
发布许可
-
更新日志
-
文件列表,或包的内容
在package.xml 1.0中,这些数据被包含在冗余的<release>标签中。在版本 2.0 中,已删除此标签。
包版本
包版本非常重要,因为这是安装程序用来确定不同发布版本相对年龄的主要机制。版本是连续的,这意味着版本 1.3.6 比版本 1.4.0b1 旧,即使 1.3.6 是在 1.4.0b1 之后一个月发布的。
<version>1.2.3</version>
对于package.xml 2.0:
<version>
<release>1.2.3</release>
<api>1.0.0</api>
</version>
发布的 API 版本仅用于信息目的。然而,这可以在以下replace任务中使用:
<file name="Foo.php" role="php">
<tasks:replace from="apiversion" to="@API-VER@" type="package-info"/>
</file>
这减少了在文件内部维护版本时的冗余。事实上,通过 API 方法提供 API 版本是一个非常好的主意。在我的包中,我通常提供以下代码:
/**
* Get the current API version
* @return string
*/
function APIVersion()
{
Return '@API-VER@';
}
在打包后,包含上述代码的Foo.php文件将看起来像这样:
/**
* Get the current API version
* @return string
*/
function APIVersion()
{
Return '1.0.0';
}
换句话说,所有@API-VER@标记的实例都将被<api>版本标签的内容所替换。替换文件任务用于执行这个魔法。
包稳定性
在package.xml 1.0中,使用<state>标签来描述代码的稳定性。在package.xml 2.0中,使用<stability>内的<release>标签来描述代码的稳定性。
<state>beta</state>
对于package.xml 2.0:
<stability>
<release>beta</release>
<api>stable</api>
</stability>
PEAR 安装程序结合使用发布稳定性和最终用户的preferred_state配置变量来确定发布是否足够稳定以安装。如果用户希望安装Foo包,并且有这些发布可用:
| Version | Stability |
|---|---|
| 1.0.1 | stable |
| 1.1.0a1 | alpha |
| 1.0.0 | stable |
| 0.9.0 | beta |
| 0.8.0 | alpha |
安装程序将根据用户的preferred_state设置选择要安装的版本。
| preferred_state value | 要安装的 Foo 版本 |
|---|---|
| stable | 1.0.1 |
| beta | 1.0.1 |
| alpha | 1.1.0a1 |
注意,对于 preferred_state 为 beta 的情况,当可以选择较新版本 1.0.1(稳定)和较旧版本 0.9.0(beta)时,安装程序将选择最新、最稳定的版本——版本 1.0.1。对于 preferred_state 为 alpha 的情况,安装程序将选择较新但不太稳定的版本 1.1.0a1(alpha),即使版本 1.0.1 是在版本 1.1.0a1 之后发布的。
释放稳定性合法稳定性的列表按稳定性递减的顺序是:
-
stable:代码应在所有情况下都能正常工作。 -
beta:代码应在所有情况下都能正常工作,并且是功能完整的,但需要实际世界的测试。 -
alpha:代码处于变化状态,功能可能会随时更改,稳定性不确定。 -
devel:代码尚未功能完善,可能在大多数情况下无法工作。 -
snapshot:这是正常发布之间的实时开发期间从源代码中获取的开发代码的当前副本。
API 稳定性服务于与 API 版本相同的信息目的。它不会被安装程序使用,但可以用来跟踪 API 变化的速率。标记为稳定的 API 实际上除了添加新功能外,不应发生变化——用户需要能够依赖稳定的 API,以便包依赖项能够正常工作。这是企业级依赖项的关键特性。
API 合法稳定性的列表略有不同,因为不允许 API 快照。API 稳定性应被视为:
-
stable:API 已设置,不会破坏向后兼容性。 -
beta:API 可能已设置,并且只有在测试中遇到设计中的严重错误时才会更改,以修复严重错误。 -
alpha:API 是流动的,可能会更改,破坏现有功能,以及添加新功能。 -
devel:API 极不稳定,可能随时发生重大变化。
外部依赖
PEAR 安装程序识别两种类型的依赖项:必需 和 可选 依赖项。此外,还有两类依赖项,一类是限制安装的依赖项,另一类是描述主要包使用的其他资源(包/扩展)的依赖项。限制性依赖项由 <php>, <pearinstaller>, <arch>, <extension> 和 <os> 依赖项定义。基于资源的依赖项由 <package>, <subpackage> 和 <extension> 依赖项定义。
虽然在 package.xml 1.0 DTD 中定义了多种依赖类型,但只有三种被实现过:
-
pkg:对包的依赖 -
ext:对扩展的依赖 -
php:对 PHP 版本的依赖
package.xml 1.0 中依赖项的结构相当简单:
<deps>
<dep type="php" rel="ge" version="4.2.0"/>
<dep type="pkg" rel="has">PackageName</dep>
<dep type="pkg" rel="ge" version="1.0">PackageName2</dep>
<dep type="pkg" rel="ge" version="1.0"
optional="yes">PackageName3</dep>
<dep type="ext" rel="not">ExtensionName</dep>
</deps>
rel 属性的合法值是:
-
has:依赖项必须存在 -
not:依赖项必须不存在 -
gt:与version属性结合使用时,依赖项的版本必须大于所需版本。 -
ge:与version属性结合使用时,依赖项的版本必须大于或等于所需版本 -
eq:与version属性结合使用时,依赖项必须具有 version == 所需版本。 -
lt:与version属性结合使用时,依赖项必须具有 version < 所需版本。 -
le:与version属性结合使用时,依赖项必须具有 version <= 所需版本。 -
ne:与version属性结合使用时,依赖项必须具有 version != 所需版本。(仅限 PEAR 版本 1.3.6。)
小贴士
版本比较是如何进行的?
PHP 函数 version_compare() 用于确定基于版本的依赖项是否有效。version_compare() 的文档在 www.php.net/version_compare。
只有在积累了大量经验之后,这种方法的严重设计缺陷才显现出来:
-
使用 xmllint 等工具进行 XML 验证无法揭示无效的依赖项。考虑以下依赖项
<dep type="php" rel="has" version="4.3.0"/>。这个依赖项是无效的,因为rel="has"忽略了版本属性,因此依赖项验证将不会执行——根据定义,每个 PEAR 安装都安装了 PHP。 -
依赖项升级的信任级别无法控制。 PEAR 包依赖于 Console_Getopt 包。在某个时刻,Console_Getopt 的维护者 修复 了一个突然导致 PEAR 升级后停止工作的错误。经过一番混乱之后才找到了解决方案,但这一事件凸显了 PEAR 安装程序在 1.4.0 版本之前的致命缺陷:对包的依赖本身是不安全的。无法限制对依赖项的信任。
rel="eq"属性没有达到预期的效果,因为这会阻止出于任何原因的安全升级,实际上冻结了开发。此外,依赖项验证中的缺陷意味着即使是升级到较新版本的包也是被禁止的。如果当前版本有一个
<dep type="pkg" rel="eq" version="1.0">Deppackage</dep>的依赖项标签,PEAR 安装程序将查看新版本的依赖项,即<dep type="pkg" rel="eq" version="1.1">Deppackage</dep>,然后查看磁盘上是否已安装 Deppackage 版本 1.0,并失败升级。即使主包和 Deppackage 都已通过,Deppackage 的升级也会失败,因为主包已安装的版本需要 Deppackage 的 1.0 版本。如果当前版本有一个
<dep type="pkg" rel="eq" version="1.0">Deppackage</dep>的依赖项标签,PEAR 安装程序将查看新版本的依赖项,即<dep type="pkg" rel="eq" version="1.1">Deppackage</dep>,然后查看磁盘上是否已安装 Deppackage 版本 1.0,并失败升级。即使主包和 Deppackage 都已通过,Deppackage 的升级也会失败,因为主包已安装的版本需要 Deppackage 的 1.0 版本。 -
基于 PECL 扩展包的依赖项无法工作。一个
<dep type="ext" rel="has">peclextension</dep>标签只会检查内存中的扩展。大多数 PECL 扩展可以直接构建到 PHP 中(非共享),作为共享模块与 PHP 一起分发,或者下载并安装。如果 PECL 扩展被构建到 PHP 中,作为共享模块分发,或通过 PECL 安装,则type="ext"依赖项将正常工作。不幸的是,为了使用 PEAR 安装程序升级扩展,必须禁用php.ini,否则文件锁定将防止在当前 PHP 进程使用时覆盖扩展。较新的扩展,如 PDO,有依赖 PDO 扩展存在的驱动程序。如果禁用php.ini,将无法检测扩展以验证依赖项!
简化 package.xml 的 XML 验证
为了使外部工具能够验证依赖项,需要重新设计所需的结构。通常,在package.xml 2.0中,优先使用标签而不是属性,尤其是优先使用属性值。package.xml 1.0中的依赖项示例可以用以下方式表示在package.xml 2.0中:
<dependencies>
<required>
<php>
<min>4.2.0</min>
</php>
<pearinstaller>
<min>1.4.1</min>
</pearinstaller>
<package>
<name>Packagename</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>PackageName2</name>
<channel>pear.php.net</channel>
<min>1.0</min>
</package>
<package>
<name>ExtensionName</name>
<channel>pecl.php.net</channel>
<providesextension>ExtensionName</providesextension>
<conflicts/>
</package>
</required>
<optional>
<package>
<name>PackageName3</name>
<channel>pear.php.net</channel>
<min>1.0</min>
<exclude>1.0</exclude>
</package>
</optional>
</dependencies>
注意,属性optional="yes"和隐含的optional="no"都已从<dep>标签中提取出来,放入<required>和<optional>标签中。此外,type属性已提取到<package>、<php>和之前未定义的<pearinstaller>标签中。最后,rel和version属性已被一组新的标签完全取代。
在package.xml 1.0中,为了定义单个依赖项的版本集,需要多个<dep>标签:
<dep type="pkg" rel="ge" version="1.2.3">PackageName</dep>
<dep type="pkg" rel="ne" version="1.3.0">PackageName</dep>
<dep type="pkg" rel="lt" version="2.0.0">PackageName</dep>
这可以与package.xml 2.0的等效版本进行比较:
<package>
<name>PackageName</name>
<channel>pear.php.net</channel>
<min>1.2.3</min>
<max>2.0.0</max>
<exclude>1.3.0</exclude>
<exclude>2.0.0</exclude>
</package>
实际上,这个变化促进了在复杂的package.xml文件中更简单地调试依赖项问题。上述依赖项可以简单地翻译为“来自通道pear.php.net的 PackageName 包,最小版本 1.2.3,最大版本 2.0.0,排除版本 1.3.0 和 2.0.0。”不仅使用像xmllint这样的工具检测错误更容易,而且对依赖项的复杂版本控制的理解也更容易。
rel/version对已被<min>/<max>/<exclude>标签的组合所淘汰。这三个标签可以被视为rel的ge、le和ne的新实现。
-
<min>1.2.0</min>=<dep rel="ge" version="1.2.0"> -
<max>1.2.0</max>=<dep rel="le" version="1.2.0"> -
<exclude>1.2.0</exclude>=<dep rel="ne" version="1.2.0">
使用这三个标签,我们可以在单个依赖项中有效地简单地定义任何版本集,无需考虑数学比较运算符。
管理依赖项的信任度
PEAR 安装程序非常灵活,使得升级到包的新版本变得如此简单;直到 PEAR 版本 1.4.0,它的最大优势也是它的最大弱点。轻松升级包的能力,以及使用upgrade-all和download-all等命令执行批量自动升级的能力,是安装程序的关键卖点之一。然而,这种便利性建立在对新版本质量的隐含信任之上。通过依赖具有简单<dep type="pkg" rel="ge" version="X.Y.Z">的包,这给了依赖包的开发者一张空白支票。如果依赖包的开发者引入了与先前版本的不兼容,无论是由于疏忽还是对您的代码缺乏同情,您的最终用户就会遇到麻烦。因此,直接在代码中捆绑依赖项已经成为分发应用程序的首选方法。然而,这却否定了 PEAR 安装程序的主要好处——在发现严重错误,如功能故障,或更糟糕的是,可能导致网站遭受外部攻击的微妙安全漏洞时,能够快速轻松地升级。这也将维护捆绑依赖项的负担直接放在了应用程序维护者身上,降低了分布式开发的效率及其所有相关好处。
package.xml 2.0通过引入新的依赖概念:推荐版本,简单地优雅地解决了这个困境。这个依赖可以从 PEAR 的package.xml中获取:
<package>
<name>Console_Getopt</name>
<channel>pear.php.net</channel>
<min>1.2</min>
</package>
这指示安装程序可以随意升级Console_Getopt到任何版本的Console_Getopt,版本1.2或更高。通过更改依赖项:
<package>
<name>Console_Getopt</name>
<channel>pear.php.net</channel>
<min>1.2</min>
<recommended>1.2</recommended>
</package>
这一切都改变了。现在,安装程序不会在发布时自动升级到 1.3.0 版本,除非传递了--force或--loose选项,或者Console_Getopt的发布中存在<compatible>标签。
<compatible>的语法很简单:
<compatible>
<name>ParentPackage</name>
<channel>pear.example.com</channel>
<min>1.0.0</min>
<max>1.3.0</max>
<exclude>1.1.2</exclude> [optional]
</compatible>
在依赖项中,<min>/<max>/<exclude>用于定义一组包保证与之兼容的版本。与依赖项不同,<max>标签是必需的,以限制版本集。此外,版本集必须限制到实际存在的、经过测试的发布版本。
从理论上讲,懒惰的开发者仍然有可能实现一个与旧rel="ge"-based依赖管理技术一样危险的新<compatible>标签,但由于开发者惰性原则,这不太可能,而且对于依赖该依赖的应用的开发者来说,也容易捕捉和纠正。
小贴士
开发者惰性原则
开发者,就像电子一样,会选择阻力最小的路径来实现他们的目标。让编写糟糕的代码变得困难,让编写好的代码变得容易,开发者就会编写好的代码。
可靠地依赖 PECL 包
由于两个创新,可靠地依赖 PECL 包变得可能:
-
将
pear脚本拆分为两个脚本:pear和pecl -
<providesextension>标签的引入
pear 和 pecl 命令之间的主要区别可以概括为:“使用 pear 来管理像 pear.php.net 上的那样用 PHP 编写的包,并使用 pecl 来管理扩展 PHP 的包,如 pecl.php.net 上的那样。”从技术上讲,pecl 命令禁用了 php.ini,并默认使用 pecl.php.net 通道,但除此之外,它与 pear 命令相同。禁用 php.ini 使得升级 php.ini 内部的扩展成为可能,而无需卸载它们。
<providesextension> 标签在依赖项中的使用如下:
<package>
<name>PDO</name>
<channel>pecl.php.net</channel>
<min>1.0</min>
<providesextension>PDO</providesextension>
</package>
这个简单的添加指示安装程序将包对 PDO 的依赖视为这两个依赖的组合:
<extension>
<name>PDO</name>
<min>1.0</min>
</extension>
<package>
<name>PDO</name>
<channel>pecl.php.net</channel>
<min>1.0</min>
</package>
安装程序将首先检查内存中是否存在 PDO 扩展,版本为 1.0 或更高版本,如果不存在,它将检查包注册表以查看是否安装了 pecl.php.net/PDO 版本 1.0 或更高版本。这允许,例如,使用基于 CLI 的 PEAR 安装程序安装基于 Apache 的 PEAR 安装的生产扩展,而无需要求基于 CLI 的安装程序将每个扩展加载到其 php.ini 中。此外,它允许像 PDO_mysql 这样的扩展依赖于 PDO,而无需在内存中加载 PDO 扩展,极大地简化了最终用户的使用体验。
发布说明
在 package.xml 1.0 和 package.xml 2.0 中,发布说明由 <notes> 标签定义。此标签的格式为任何文本。
<notes>
Minor bugfix release
These bugs are fixed:
* Bug #1234: stupid politicians elected to office
* Bug #2345: I guess we voted for them didn't we
</notes>
发布许可
<license> 标签的文本不会进行验证——可以输入任何内容。
<license>BSD license</license>
在 package.xml 2.0 中,有可选的 uri 和 filesource 属性,用于将许可证链接到在线版本,以及链接到包本身内的特定许可证文件。
<license uri="http://www.opensource.org/licenses/bsd-license.php">BSD
license</license>
变更日志
package.xml 1.0 中的变更日志格式与 <release> 标签的格式相匹配,唯一的区别是 <filelist> 不允许。变更日志纯粹是供人类使用的纯信息,安装程序不会对其进行任何处理。package.xml 2.0 中的变更日志格式与 package.xml 1.0 非常相似,以下是一个示例:
<release>
<version>
<release>1.3.6</release>
<api>1.3.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<date>2005-08-18</date>
<license uri="http://www.php.net/license">PHP License</license>
<notes>
* Bump XML_RPC dependency to 1.4.0
* return by reference from PEAR::raiseError()
</notes>
</release>
与 package.xml 的发布部分一样,<version>、<stability>、<date>、<license> 和 <notes> 标签都存在。这与 package.xml 1.0 相同。唯一的区别是 <version>、<stability> 和 <license> 标签的格式,它们与 package.xml 2.0 中所做的更改相匹配。
文件列表,或包的内容
PEAR 的主要目的,以及由此产生的 package.xml,是分发包含编程代码的文件。这最终由包中定义的文件列表控制,由 <filelist> 或 <contents> 定义。
对于 package.xml 1.0:
<filelist>
<dir name="OS">
<file role="php" name="Guess.php">
<replace from="@package_version@" to="version" type="package-
info" />
</file>
</dir>
<dir name="PEAR">
<dir name="ChannelFile" baseinstalldir="Foo">
<file name="Parser.php" role="php">
<replace from="@package_version@" to="version" type="package-
info" />
</file>
</dir>
<file role="data" name="Foo.dat"/>
</dir>
<file role="doc" name="linux-Howto.doc" install-as="README"
platform="!windows"/>
<file role="doc" name="windows-Howto.doc" install-as="README"
platform="windows"/>
</filelist>
对于 package.xml 2.0:
<contents>
<dir name="/">
<dir name="OS">
<file name="Guess.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info"/>
</file>
</dir>
<dir name="PEAR">
<dir name="ChannelFile" baseinstalldir="Foo">
<file name="Parser.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info"/>
</file>
</dir>
<file role="data" name="Foo.dat"/>
</dir>
<file role="doc" name="linux-Howto.doc"/>
<file role="doc" name="windows-Howto.doc"/>
</dir>
</contents>
这个标签是 package.xml 文件的核心。一旦我们可靠的包通过了依赖性测试,这就是 package.xml 在安装过程中实际使用的部分。
package.xml 内的文件列表用于定义发布中的目录结构。它必须精确反映文件在开发者计算机上的相对位置。换句话说,如果 package.xml 在 /home/frank/mypackage/ 目录中,并且 package.xml 中的一个文件位于 /home/frank /mypackage/foo/test.php,它必须列出为:
<file role="php" name="foo/test.php"/>
或者,作为替代:
<dir name="foo">
<file role="php" name="test.php"/>
</dir>
任何一种选择都会得到相同的结果。在安装时,PEAR 会自动将像第二个例子那样的所有递归目录树转换成像第一个例子那样的单个扁平分支。
package.xml 中的新标签
package.xml 2.0 引入了新的标签 <phprelease>, <extsrcrelease> 和 <extbinrelease>,以区分 PEAR 安装器处理的包的不同类型。package.xml 2.1 引入了 <zendextsrcrelease> 和 <zendextbinrelease>,以便区分常规 PHP 扩展和像 xdebug ( pecl.php.net/xdebug) 这样的 Zend 扩展。
你可能已经注意到,package.xml 1.0 中的主要标签被命名为 <filelist>,而 package.xml 2.0 中的主要标签被命名为 <contents>。这种变化是由于一个简单的功能请求而产生的。当 PEAR 1.3.3 流行时,定制安装的需求增长;越来越多的属性和信息被塞进了 <file> 标签。platform 属性告诉安装器一个文件应该只安装在一个特定的平台上,例如 UNIX 或 Windows。这在 PEAR 包中用于在 UNIX 上使用 shell 脚本安装 pear 命令,在 Windows 上使用 .bat 批处理文件。提交的功能请求是实施一个额外的 platform="!windows" 漏洞,告诉安装器除了 Windows 之外的所有平台上安装文件。这引入了一系列问题。随着包的复杂性增加,支持更多的系统,可能需要指定一个文件可以安装的有限系统列表,或者在不同系统上安装文件的不同名称。实施这一点将需要在 install-as 和 platform 属性之间进行复杂的映射,这是 package.xml 1.0 的设计者没有预料到的,并且会在单个文件标签中引入一个糟糕的混乱。想象一下遇到这个噩梦并试图调试它:
<file name="Foo.scr" role="script" install-
as="windows=Foo.bat;Darwim=Fooscr;unix=Foo;"
platform="windows;!Solaris;unix"/>
你是否看到了安装为属性中埋藏的错别字?
有一次,在处理由这些复杂特性引起的难题时,我突然意识到像platform属性这样的属性实际上是实现特定文件依赖关系的笨拙方式。突然,答案变得清晰。比扩展属性的意义更好的是,将信息抽象成单独的发布标签。每个发布标签都会有一个文件列表和安装条件,这将定义在最终用户的计算机上应该使用哪一个。例如,而不是:
<file role="doc" name="linux-Howto.doc" install-as="README"
platform="!windows"/>
<file role="doc" name="windows-Howto.doc" install-as="README"
platform="windows"/>
我们将会有:
<file role="doc" name="linux-Howto.doc"/>
<file role="doc" name="windows-Howto.doc"/>
...
<phprelease>
<installconditions>
<os>windows</os>
</installconditions>
<filelist>
<install name="windows-Howto.doc" as="README"/>
<ignore name="linux-Howto.doc"/>
</filelist>
</phprelease>
<phprelease>
<filelist>
<install name="linux-Howto.doc" as="README"/>
<ignore name="windows-Howto.doc"/>
</filelist>
</phprelease>
然而,最明显的益处来自于那个糟糕的“Darwin”示例。这会翻译成:
<file name="Foo.scr" role="script" install-
as="windows=Foo.bat;Darwim=Fooscr;unix=Foo;"
platform="windows;!Solaris;unix"/>
到:
<file name="Foo.scr" role="script"/>
...
<phprelease>
<installconditions>
<os>windows</os>
</installconditions>
<filelist>
<install name="Foo.scr" as="Foo.bat"/>
</filelist>
</phprelease>
<phprelease>
<installconditions>
<os>Darwim</os>
</installconditions>
<filelist>
<install name="Foo.scr" as="Fooscr"/>
</filelist>
</phprelease>
<phprelease>
<installconditions>
<arch>Solaris</arch>
</installconditions>
<filelist>
<ignore name="Foo.scr"/>
</filelist>
</phprelease>
<phprelease>
<installconditions>
<os>unix</os>
</installconditions>
<filelist>
<install name="Foo.scr" as="Foo"/>
</filelist>
</phprelease>
在这里,简单的验证的潜力是明显的:<os>安装条件限制为几个已知的可能性,如windows、unix、linux、Darwin等。操作系统“Darwim”将简单地拒绝验证。此外,如何处理Foo.scr的复杂性是根据操作系统分组,而不是放入几个具有易出错的非 XML 语法的属性中。
文件/目录属性:name、role 和 baseinstalldir
<file>和<dir>标签都有许多可用的选项。这两个标签都需要一个name属性,用于定义元素在磁盘上的名称。与操作系统不同,package.xml不允许空目录。所有<dir>标签都必须包含至少一个<file>标签。如前所述,在package.xml中描述文件位置有两种方式,要么使用完整的相对路径,由 UNIX 路径分隔符/:分隔。
<file role="php" name="foo/test.php"/>
或者,也可以这样:
<dir name="foo">
<file role="php" name="test.php"/>
</dir>
所有文件都必须有一个角色属性。此属性告诉安装程序如何处理文件。允许的文件角色默认列表如下:
| 默认文件角色 | |
|---|---|
| 角色 | 描述 |
| --- | --- |
| php | PHP 脚本文件,例如"PEAR.php" |
| 数据 | 脚本使用的数据文件(只读) |
| doc | 文档文件 |
| test | 测试脚本,单元测试文件 |
| script | 可执行脚本文件(例如 pear.bat, pear.sh) |
| ext | PHP 扩展二进制文件(例如 php_mysql.dll) |
| src | PHP 扩展源文件(例如 mysql.c) |
每个文件角色都与其关联一个配置值。例如,在我的 Windows XP 系统上,当我列出配置值时,我看到如下内容:
C:\>pear config-show
CONFIGURATION (CHANNEL PEAR.PHP.NET):
=====================================
Auto-discover new Channels auto_discover <not set>
Default Channel default_channel pear.php.net
HTTP Proxy Server Address http_proxy <not set>
PEAR server [DEPRECATED] master_server pear.php.net
Default Channel Mirror preferred_mirror pear.php.net
Remote Configuration File remote_config <not set>
PEAR executables directory bin_dir C:\php4
PEAR documentation directory doc_dir C:\php4\PEAR\docs
PHP extension directory ext_dir C:\php4\extensions
PEAR directory php_dir C:\php4\PEAR
PEAR Installer cache directory cache_dir C:\DOCUME~1\GREGBE~1\LOCALS~1\
Temp\pear\cache
PEAR data directory data_dir C:\php4\PEAR\data
PHP CLI/CGI binary php_bin C:\php-4.3.8\cli\php.exe
PEAR test directory test_dir C:\php4\PEAR\tests
Cache TimeToLive cache_ttl 3600
Preferred Package State preferred_state stable
Unix file mask umask 0
Debug Log Level verbose 1
PEAR password (for password <not set> maintainers)
Signature Handling Program sig_bin c:\gnupg\gpg.exe
Signature Key Directory sig_keydir C:\php4\pearkeys
Signature Key Id sig_keyid <not set> Package
Signature Type sig_type gpg
PEAR username (for username <not set> maintainers)
User Configuration File Filename C:\php4\pear.ini
System Configuration File Filename C:\php4\pearsys.ini
在某些情况下,特别是对于像HTML_QuickForm_Controller(pear.php.net/package/HTML_QuickForm_Controller)这样的子包,所有文件都应该安装到子目录中(在我们的例子中是 HTML/QuickForm/Controller)。为了在我们的文件中反映这个安装路径,我们需要将所有文件的前缀设置为完整路径,如下所示:
<file role="php" name="HTML/QuickForm/Controller.php"/>
<file role="php" name="HTML/QuickForm/Controller/Action.php"/>
或者,也可以这样使用<dir>标签:
<dir name="HTML">
<dir name="QuickForm">
<dir name="Controller">
<file role="php" name="Action.php"/>
</dir>
<file role="php" name="Controller.php"/>
</dir>
</dir>
此外,我们的实际开发路径也需要反映这一点。这并不符合“让懒惰的开发者一切变得简单”的开发模式,因此 PEAR 提供了baseinstalldir属性来简化事情。现在,我们只需要:
<file role="php" baseinstalldir="HTML/QuickForm" name=" Controller.php"/>
<file role="php" baseinstalldir="HTML/QuickForm" name=" Controller/Action.php"/>
或者,更常见的:
<dir name="/" baseinstalldir="HTML/QuickForm">
<dir name="Controller">
<file role="php" name="Action.php"/>
</dir>
<file role="php" name="Controller.php"/>
</dir>
小贴士
开发机上的路径位置
注意,作为一个开发者,为了使这生效,磁盘上的实际路径必须与<dir>/<file>标签匹配。在我们上面的例子中,文件应该位于:
Controller/Action.php Controller.php package.xml
而不是:
HTML/QuickForm/Controller/Action.php HTML/QuickForm/Controller.php package.xml
否则,PEAR 安装程序将无法找到这些文件。
一般而言,如果一个文件在package.xml中被列出:
<file role="php" name="Path/To/Foo.php"/>
并且php_dir是C:\php4\PEAR,文件将被安装到C:\php4\PEAR\Path\To\Foo.php。这并不总是有优势,尤其是对于像脚本这样的东西。例如,PEAR 将所有脚本放在scripts/子目录中以方便组织。然而,这意味着如果bin_dir是C:\php4,
<file role="script" name="scripts/pear.bat"/>
将被安装到C:\php4\scripts\pear.bat,这不一定在路径中。要在 PEAR 1.4.x 中更改基本安装目录,您需要在发布标签内使用<install>标签,此外,还可以对文件内容执行文本转换。
摘要
在本章中,我们通过package.xml的结构,了解了 PEAR 安装程序内部工作原理的基础。首先,我们探讨了 PEAR 安装程序的基本设计理念,以及 PEAR 包与传统的解压缩并运行方法的不同。我们学习了 PEAR 的配置选项,以及 PEAR 处理库与应用程序的灵活方式。
接下来,我们探讨了版本控制对控制安装包质量的重要性,以及依赖关系的重要性,以及 PEAR 安装程序如何管理库和应用程序之间的重要链接。然后,我们探讨了使用 PEAR 安装程序升级的便利性,与传统的解压缩并运行应用程序的升级方式相比。
之后,我们一头扎进了package.xml的结构,学习如何组织包元数据,如包名、作者、发布说明和变更日志。这伴随着对关键安装数据(如文件、依赖关系和版本控制)如何组织的考察。
下一章将探讨高级主题,特别是package.xml 2.0如何引入更好的应用程序支持,以及如何在您的包中利用这些新特性。
第三章:利用 PEAR 安装程序的全应用支持
在上一章中,我们学习了关于package.xml内部的大量知识。在这一章中,我们将强度提高一个档次,探索使我们能够轻松分发 PHP 应用程序并管理其安装和安装后定制的令人兴奋的新功能。
如果你曾经想要使在多个平台、PHP 版本和用户设置上定制 PHP 应用程序变得容易,那么这一章就是为你准备的。
package.xml 版本 2.0:你的新性感伙伴
本节标题已经说明了所有内容。package.xml 2.0比package.xml 1.0有重大改进。在 PEAR 安装程序中实现的一些重要新功能,如自定义文件角色/任务、企业级依赖关系和通道,在package.xml 2.0中的新标签中得到了反映。此外,结构设计得易于使用其他工具进行验证。
PEAR 通道:PHP 安装的革命
package.xml 2.0最小的增加是<channel>标签。但不要被骗了,通道是 PEAR 安装程序中实现的最重要的新功能。通道对于 PEAR 来说,就像依赖关系对于团队开发一样。通过将 PEAR 安装程序的便利性扩展到pear.php.net以外的网站,为 PHP 用户提供了许多免费选择。第一次,可以设计一个依赖于pear.php.net、pear.example.com和任何数量网站的程序,并且所有这些都可以通过单个命令自动下载、安装和轻松升级到最终用户的计算机上。尽管第五章讨论了通道的细节和channel.xml通道定义文件,但在设计你的包时,了解通道的工作原理是很好的。
通道有效地解决了两个问题:
-
在多个开发团队之间分配应用程序开发
-
防止冲突的包相互覆盖
一个用户的 PEAR 安装程序可以了解一个通道,在这个例子中,通过channelserver.example.com了解通道:
$ pear channel-discover channelserver.example.com
一旦用户的 PEAR 安装程序有了这方面的知识,就可以简单地安装来自channelserver.example.com的包,例如一个假设的名为Packagename的包,使用以下命令:
$ pear install channelserver.example.com/Packagename
用户还可以安装依赖于channelserver.example.com/Packagename的包。在 PEAR 通道出现之前,这是不可能的。
当用户简单地输入以下命令时:
$ pear install Package
就像在 PEAR 版本 1.3.6 及更早版本中一样,安装程序使用default_channel配置变量,通常为pear.php.net或pecl.php.net(对于pecl命令),然后就像用户输入了以下内容一样操作:
$ pear install pear.php.net/Package
事实上,现在每个现有的 PEAR 包 Foo 都变成了pear.php.net/Foo,实际上充当了一个命名空间,将其与channelserver.example.com/Foo区分开来。这是频道用来防止冲突包相互覆盖的机制。由于pear.php.net/Foo与channelserver.example.com/Foo不是同一个包,因此不可能从pear.php.net/Foo升级到channelserver.example.com/Foo。因此,现在是时候介绍一个重要的概念了:
小贴士
尽管频道名称是服务器名称,但它们也充当了一个分类命名方案,用于区分来自不同来源的包。
要理解这一点,我们需要研究一些频道背后的历史。在描述频道的原始草案提案中,频道名称(用于安装和依赖项)和用于访问频道的服务器是不同的。例如,pear.php.net频道最初被命名为 PEAR 频道,这样用户就会输入:
$ pear install PEAR/Foo
经过几个 PEAR 的开发版本发布后,很明显,这有几个原因是不好的,其中一个原因就是如果你不知道频道的位置,它就根本无法找到。因此,在 PEAR 的第一个 alpha 版本 1.4.0a1 中,频道的名称与服务器名称相同。
小贴士
我们是否总是必须输入 pear.php.net/Package?
不,事实上,使使用服务器名称作为频道名称合理化的创新是频道别名的想法。从命令行,我们可以输入以下内容,PEAR 就会安装pear.php.net/Package。
$ pear install pear/Package
此外,如果我们愿意,我们可以通过使用channel-alias命令将其更改为我们想要的任何名称,如下所示:
$ pear channel-alias pear.php.net p $ pear install p/Package
然而,在package.xml的<channel>和其他标签中,必须始终使用完整的频道服务器名称,不允许使用别名。
使用服务器名称作为频道名称的转换还有一个期望的结果。最初,可以透明地更改与频道关联的服务器。这在很多层面上都是个坏主意!首先,这意味着一个恶意程序员可以通过简单地更改 PEAR 频道使用的服务器来绕过 PEAR 频道的冲突保护。其次,通过提供与pear.php.net上可用的同名包相同名称的包,并在其中隐藏恶意代码,甚至可能欺骗用户在不了解的情况下使用恶意代码。
使用服务器名称作为频道名称,这不再可能,除非对原始频道服务器进行老式的黑客攻击,而这种攻击很快就会被发现。
简而言之,通道的强大之处不仅在于其对最终用户的易用性和对开发者的灵活性,还在于其设计中考虑到的广泛安全性。正如最近在主要 PHP 包(如 XML_RPC 和 phpBB)中出现的安全漏洞所证明的那样,我们不能过于小心。在 PEAR 中,安全性是至关重要的,开发者们已经竭尽全力确保 PEAR 无懈可击。
应用支持
到目前为止,我们已经了解到 PEAR 安装程序最初是为了支持库而设计的,然后在 PEAR 版本 1.4.0 中添加了应用支持。让我们通过检查四个令人兴奋的新功能来更详细地了解这意味着什么:自定义文件角色、自定义文件任务、安装后脚本以及将多个包捆绑成一个单一存档的能力。
在本节中,我们将通过创建一个新的自定义文件角色chiaramdb2schema、一个自定义文件任务chiara-managedb、一个用于填充所需数据的安装后脚本和一个示例应用程序来探索这些功能的细节。然后我们将把角色和任务捆绑在一个单一存档中分发。
小贴士
在我们开始之前,你需要检查一个重要的点:
你在解决什么问题?你应该使用自定义文件角色、自定义文件任务、安装后脚本还是其他什么?
自定义文件角色旨在将相关的文件类分组在一起。例如,如果你希望以不同于基于 Web 的 JavaScript 文件的方式安装所有基于 Web 的图像文件,自定义文件角色是完成这一点的最佳方式。
自定义文件任务旨在在安装之前或打包之前操作单个文件的内容。如果你需要将通用模板转换为特定机器的文件(例如,将通用数据库创建 SQL 文件转换为 MySQL 特定或 Oracle 特定 SQL 文件),自定义任务是很好的选择。
安装后脚本旨在允许用户在包准备就绪使用之前执行任何其他高级配置。
我们的示例文件角色和任务是为单用户情况设计的。在共享主机上,这必须通过安装后脚本来完成,因此我们将提供一个脚本,以便系统管理员可以维护多个数据库安装的包。
小贴士
自定义文件角色/任务的命名约定
对于所有扩展 PEAR 安装程序的功能,使用自定义前缀是一个非常不错的选择。在我们的示例中,如果我们把角色命名为sql而不是chiara_sql,任务命名为updatedb而不是chiara_updatedb,那么存在与官方自定义角色或任务冲突的风险,这些角色或任务是从pear.php.net分发的。特别是,如果任何角色或任务被认为足够有用,足以成为 PEAR 安装程序默认部分,那么使用你自定义角色/任务的用户将无法升级他们的 PEAR 安装,除非他们卸载该角色及其依赖的所有包。
自定义文件角色的介绍
文件角色用于将相关文件分组在一起以处理安装。标准文件角色有 php, data, doc, test, script, src 和 ext。安装程序会以不同的方式处理每个角色。在名为My_Package的包中指定此类标签的文件将被安装到My/Package/foo.php。
<file name="foo.php" role="php" baseinstalldir="My/Package"/>
然而,具有data角色的相同标签会提示安装程序采取非常不同的行动。这个文件不会安装到My/Package/foo.php,而是安装到My_Package/foo.php。
<file name="foo.php" role="data" baseinstalldir="My/Package"/>
baseinstalldir属性被data, doc和test角色忽略,这些角色将根据package.xml中定义的相对路径安装到<package name>/path/to/file。
此外,每个角色都通过不同的config变量确定文件安装的位置。role: configuration变量映射如下:
-
php: php_dir -
data: data_dir -
doc: doc_dir -
test: test_dir -
script: bin_dir -
ext: ext_dir -
src: <none>(不安装)
通常来说,配置变量与带有_dir后缀的文件角色相同,但role="script"除外,它应该附加bin_dir。此外,注意带有role="src"的文件实际上并没有安装。相反,这些文件会被提取出来,然后编译成扩展二进制文件,之后被丢弃。每个角色都有一组特性,使其与其他角色区分开来:
-
有些适用于 PHP 包,而有些适用于扩展包
-
有些被安装,而有些则没有
-
可安装的角色有一个配置变量,用于确定它们应该安装的位置
-
有些遵守
baseinstalldir属性,而有些则不遵守 -
有些角色安装到
<packagename>/path,而有些则不是 -
有些代表 PHP 脚本
-
有些代表可执行文件(如脚本)
-
有些代表 PHP 扩展二进制文件
这些特性就是定义自定义文件角色所需的所有内容。实际上,现有的文件角色就是使用这些特性和特殊对象定义的。例如,定义 PHP 角色的代码如下:
<?php
/**
* PEAR_Installer_Role_Php
*
* PHP versions 4 and 5
*
* LICENSE: This source file is subject to version 3.0 of the PHP
* license
* that is available through the world-wide-web at the following URI:
* http://www.php.net/license/3_0.txt. If you did not receive a copy
* of the PHP License and are unable to obtain it through the web,
* please send a note to license@php.net so we can mail you a copy
* immediately.
*
* @category pear
* @package PEAR
* @author Greg Beaver <cellog@php.net>
* @copyright 1997-2005 The PHP Group
* @license http://www.php.net/license/3_0.txt PHP License 3.0
* @version CVS: $Id: Php.php,v 1.5 2005/07/28 16:51:53 cellog Exp
$
* @link http://pear.php.net/package/PEAR
* @since File available since Release 1.4.0a1
*/
/**
* @category pear
* @package PEAR
* @author Greg Beaver <cellog@php.net>
* @copyright 1997-2005 The PHP Group
* @license http://www.php.net/license/3_0.txt PHP License 3.0
* @version Release: @package_version@
* @link http://pear.php.net/package/PEAR
* @since Class available since Release 1.4.0a1
*/
class PEAR_Installer_Role_Php extends PEAR_Installer_Role_Common {}
?>
对于大多数角色来说,这可能是唯一需要定义的代码!然而,除了这段 PHP 代码之外,还应安装一个 XML 文件,用于记录角色的属性。PHP 角色的 XML 文件如下:
<role version="1.0">
<releasetypes>php</releasetypes>
<releasetypes>extsrc</releasetypes>
<releasetypes>extbin</releasetypes>
<installable>1</installable>
<locationconfig>php_dir</locationconfig>
<honorsbaseinstall>1</honorsbaseinstall>
<unusualbaseinstall />
<phpfile>1</phpfile>
<executable />
<phpextension />
<config_vars />
</role>
各种标签如下:
-
<releasetypes>:这个标签的作用类似于数组,其内容定义了哪些发布类型可以包含此角色。可能的发布类型列表为php, extsrc, extbin或bundle。 -
<installable>:这个布尔值确定角色是否安装到磁盘上。 -
<locationconfig>:对于可安装的角色,这个字符串值确定用于安装文件的配置变量。 -
<honorsbaseinstall>:这个布尔值(表示为 1 或空标签)确定是否在计算最终安装位置时使用baseinstalldir。 -
<unusualbaseinstall>:这个布尔值(表示为 1 或一个空标签)决定了包名是否被添加到安装路径之前。 -
<phpfile>:这个布尔值(表示为 1 或一个空标签)决定了文件是否被视为 PHP 文件(在打包时分析有效的 PHP/类名/函数名)。 -
<executable>:这个布尔值(表示为 1 或一个空标签)决定了在基于 UNIX 的系统上是否将文件与可执行属性一起安装。 -
<phpextension>:这个布尔值(表示为 1 或一个空标签)决定了在覆盖现有扩展二进制文件失败(由于文件锁定)时,安装程序是否会显示一个有用的错误消息。
创建 PEAR_Installer_Role_Chiaramdb2schema 自定义角色
首先,重要的是要理解这个角色在package.xml中的使用方式。为了实现自定义角色,package.xml验证应该能够告诉用户在哪里下载和安装它,因为依赖验证仅在package.xml文件从基本结构角度验证之后才会发生。
<package>
<name>Role_Chiaramdb2schema</name>
<channel>pear.chiaraquartet.net</channel>
</package>
因此,除了包依赖package.xml外,还应包含<usesrole>标签,描述所使用的自定义文件角色名称以及包含此角色的包的远程位置。在我们的例子中,如下所示:
<usesrole>
<role>chiaramdb2schema</role>
<package>Role_Chiaramdb2schema</package>
<channel>pear.chiaraquartet.net</channel>
</usesrole>
此标签将提示安装程序首先检查pear.chiaraquartet.net/Role_Chiaramdb2schemaql包是否已安装。如果没有,安装程序将发出警告:
This package contains role "chiaramdb2schema" and requires package "pear.chiaraquartet.net/Role_Chiaramdb2schema" to be used
小贴士
为什么在依赖关系之外还要使用
一旦开始安装,PEAR 安装程序就无法成功配置角色或任务。它们必须在尝试安装使用它们的包之前安装和配置。因此,自定义角色或任务的安装必须在与使用它们的包分开的过程中进行。
在<file>标签内使用自定义角色与任何常规角色没有区别。
<file name="dbcontents.xml" role="chiaramdb2schema"/>
所有自定义文件角色都实现在一个 PHP 文件中,该文件安装到PEAR/Installer/Role/目录。例如,data角色位于PEAR/Installer/Role/Data.php。与自定义任务不同,自定义文件角色不能在子目录中,因此前缀应该不带下划线,以匹配 PEAR 命名约定。此外,每个自定义角色都必须扩展PEAR_Installer_Role_Common类,该类位于PEAR/Installer/Role/Common.php。
我们的定制文件角色使用data_dir配置变量来确定安装位置,因此在安装方面,它表现得就像data角色一样。然而,它通过Chiaramdb2schema.xml文件中的此 XML 执行了神奇的事情:
<config_vars>
<chiaramdb2schema_driver>
<type>string</type>
<default />
<doc>MDB2 database driver used to connect to the database</doc>
<prompt>Database driver type. This must be a valid MDB2 driver.
Example drivers are mysql, mysqli, pgsql, sqlite, and so on</prompt>
<group>Database</group>
</chiaramdb2schema_driver>
<chiaramdb2schema_dsn>
<type>string</type>
<default />
<doc>PEAR::MDB2 dsn string[s] for database connection, separated
by ;.
This must be of format:
[user@]host/dbname[;[Package[#schemafile]::]dsn2...]
One default database connection must be specified, and package-
specific databases
may be specified. The driver type and password should be excluded.
Passwords
are set with the chiaramdb2schema_password config variable
</doc>
<prompt>Database connection DSN[s] (no driver/password)</prompt>
<group>Database</group>
</chiaramdb2schema_dsn>
<chiaramdb2schema_password>
<type>string</type>
<default />
<doc>PEAR::MDB2 dsn password[s] for database connection.
This must be of format: password[:password...]
Each DSN in chiaramdb2schema_dsn must match with a password in this
list, or
none will be used. To use no password, simply put another :: like
::::
</doc>
<prompt>Database connection password[s]</prompt>
<group>Database</group>
</chiaramdb2schema_password>
</config_vars>
通过这种方式定义<config_vars>标签,将向 PEAR 配置添加三个全新的配置变量。它们以与其他配置变量相同的方式操作,并提供将使我们的chiaramdb2schema角色变得特殊的信息。
我们的角色利用了基于 MDB2 的架构文件是数据文件的特殊子类这一事实,通过直接扩展PEAR_Installer_Role_Data类。以下是我们的示例角色的完整源代码:
<?php
/**
* Custom file role for MDB2_Schema-based database setup files
*
* This file contains the PEAR_Installer_Role_Chiaramdb2schema file * role
*
* PHP versions 4 and 5
*
* @package Role_Chiaramdb2schema
* @author Greg Beaver <cellog@php.net>
* @copyright 2005 Gregory Beaver
* @license http://www.opensource.org/licenses/bsd-license.php BSD
* License
* @version Release: 0.2.0
* @link
http://pear.chiaraquartet.net/index.php?package=Role_Chiaramdb2schema
*/
/**
* Contains the PEAR_Installer_Role_Data class
*/
require_once 'PEAR/Installer/Role/Data.php';
/**
* chiaramdb2schema Custom file role for MDB2_Schema-based database
* setup files
*
* This file role provides the <var>chiaramdb2schema_driver</var>,
* <var>chiaramdb2schema_dsn</var>, and
<var>chiaramdb2schema_password</var>
* configuration variables for use by the chiara-managedb custom task
* to set up and initialize database files
*
* PHP versions 4 and 5
*
* @package Role_Chiaramdb2schema
* @author Greg Beaver <cellog@php.net>
* @copyright 2005 Gregory Beaver
* @license http://www.opensource.org/licenses/bsd-license.php BSD
* License
* @version Release: 0.2.0
* @link http://pear.chiaraquartet.net/index.php?package=Role_Chiaramdb2schema
*/
class PEAR_Installer_Role_Chiaramdb2schema extends
PEAR_Installer_Role_Data
{
}
?>
伴随此角色的Chiaramdb2schema.xml文件:
<role version="1.0">
<releasetypes>php</releasetypes>
<releasetypes>extsrc</releasetypes>
<releasetypes>extbin</releasetypes>
<installable>1</installable>
<locationconfig>data_dir</locationconfig>
<honorsbaseinstall />
<unusualbaseinstall />
<phpfile />
<executable />
<phpextension />
<config_vars>
<chiaramdb2schema_driver>
<type>string</type>
<default />
<doc>MDB2 database driver used to connect to the database</doc>
<prompt>Database driver type. This must be a valid MDB2 driver.
Example drivers are mysql, mysqli, pgsql, sqlite, and so on</prompt>
<group>Database</group>
</chiaramdb2schema_driver>
<chiaramdb2schema_dsn>
<type>string</type>
<default />
<doc>PEAR::MDB2 dsn string[s] for database connection, separated
by ;.
This must be of format:
[user@]host/dbname[;[Package[#schemafile]::]dsn2...]
One default database connection must be specified, and package-
specific databases
may be specified. The driver type and password should be excluded.
Passwords
are set with the chiaramdb2schema_password config variable
</doc>
<prompt>Database connection DSN[s] (no driver/password)</prompt>
<group>Database</group>
</chiaramdb2schema_dsn>
<chiaramdb2schema_password>
<type>string</type>
<default />
<doc>PEAR::MDB2 dsn password[s] for database connection.
This must be of format: password[:password...]
Each DSN in chiaramdb2schema_dsn must match with a password in this
list, or
none will be used. To use no password, simply put another :: like
::::
</doc>
<prompt>Database connection password[s]</prompt>
<group>Database</group>
</chiaramdb2schema_password>
</config_vars>
</role>
就这些了!现在我们已经看到了如何实现一个简单的角色,让我们来探讨自定义文件角色设计内置的可能性的范围。
可能的自定义文件角色全范围
大多数自定义文件角色只需要指定如前几节所述的配置变量和属性。然而,有时这还不够,需要一些不寻常的设置。基类PEAR_Installer_Role_Common提供的受保护的setup()方法专门用于允许文件角色执行任何所需的不寻常设置功能。方法签名如下:
/**
* Do any unusual setup here
* @param PEAR_Installer
* @param PEAR_PackageFile_v2
* @param array file attributes
* @param string file name
*/
function setup(&$installer, $pkg, $atts, $file)
参数相当直接:
-
PEAR_Installer $installer:这允许通过PEAR_Installer类的公共 API 完成任何专门的安装任务。 -
PEAR_PackageFile_v2 $pkg:这允许从package.xml检索对自定义角色可能有用的任何信息。请注意,PEAR_PackageFile_v2类的公共 API 是只读的。 -
array $atts:这是从package.xml解析出的文件属性,格式类似于以下内容:array( 'name' => 'Full/Path/To/File.php', 'role' => 'customrolename', 'baseinstalldir' => 'Whatever', ); -
string $file:这是文件名。
注意,setup()方法在计算任何安装位置之前为每个角色调用。此外,当前PEAR_Config配置对象可通过$this->config成员访问。
还需要探索的是自定义文件角色的配置变量定义方式。
<config_vars>标签定义配置变量。每个配置变量都使用其名称的标签进行声明。如果您想创建一个名为foo的简单配置变量,您将使用以下 XML:
<config_vars>
<foo>
<type>string</type>
<default />
<doc>Foo configuration</doc>
<prompt>Foo protocol login</prompt>
<group>Auth</group>
</foo>
</config_vars>
合法的配置类型是string, directory, file, set和password。如果您希望将可能的输入限制到指定的值,您还需要使用<valid_set>标签定义有效值的集合:
<config_vars>
<foo>
<type>set</type>
<default />
<doc>Foo configuration</doc>
<valid_set>bar</valid_set>
<valid_set>baz</valid_set>
<valid_set>gronk</valid_set>
<prompt>Foo protocol type</prompt>
<group>Auth</group>
</foo>
</config_vars>
在PEAR/Config.php文件中查看现有配置变量组的示例。此变量仅用于信息目的,可以是您想要的任何内容。
另一方面,<default>标签有大量的可能性。可以访问三种类型的值来设置配置变量的默认值:
-
现有配置变量的默认值
-
PHP 常量
-
任何文本
为了检索php_dir配置变量的默认值,您将使用此标签:
<default><php_dir/></default>
只有内置配置变量可以访问其默认值。要访问像PHP_OS这样的 PHP 常量,请使用此标签:
<default><constant>PHP_OS</constant></default>
注意,在PEAR/Common.php或PEAR/Config.php中定义的任何常量也将可用作默认值。最后,可以直接使用纯文本,如下所示:
<default><text>hello world</text></default>
为了组合这些任务中的几个,只需按所需顺序使用它们即可:
<default><php_dir/><constant>DIRECTORY_SEPARATOR</constant> <text>foo</text></default>
如果你希望使用多个常量或多个文本,可以在标签名末尾附加一个数字,如下所示:
<default><text1>.</text1><constant>PATH_SEPARATOR</constant> <text2>mychannel</text2></default>
自定义文件任务简介
PEAR 随带三个自定义文件任务和一个脚本任务(下一节将讨论安装后脚本)。任务如下:
-
<tasks:replace/>:在已安装或打包的文件上执行基本的str_replace操作。可能的替换值来自package.xml的信息,来自 PEAR 的配置信息,如php_dir的值,或 PHP 常量如PHP_OS。 -
<tasks:windowseol/>:这将所有行结束转换为 Windows 的"\r\n"行结束符。 -
<tasks:unixeol/>:这将所有行结束转换为 UNIX 的"\n"行结束符。
在本节中,我们将检查这些任务如何在 PEAR 代码内部定义,以及如何创建你自己的自定义文件任务。
文件任务通常用于在安装前操作文件的內容。然而,这仅受限于你的想象力。在我们的例子中,我们将使用一个任务来创建和更新数据库结构,在升级时使用我们之前创建的chiaramdb2schema文件角色。这个任务是一个非常高级的任务,执行复杂的处理,因此展示了这种系统的多功能性。
自定义任务的 XML 内容的唯一约束是任务的命名空间(通常为tasks)必须作为每个标签的前缀。验证由每个任务的 PHP 代码控制。自定义文件任务必须扩展PEAR_Task_Common,并且必须位于 PEAR 的PEAR/Task/子目录中。与自定义文件角色不同,自定义文件任务可以直接通过使用下划线支持子目录。在我们的示例文件任务chiara-managedb中,类名为PEAR_Task_Chiara_Managedb,这可以在文件PEAR/Task/Chiara/Managedb.php中找到。
有三种自定义文件任务:单个、多个和脚本。单个任务在其操作上对单个文件执行,并在文件安装前执行。多个任务在包含任务的每个文件上操作,并在安装完成后执行。脚本任务在安装后使用run-scripts命令执行,将在下一节详细说明安装后脚本。此外,任务在文件标签中出现的顺序也很重要。以下可能的但逻辑上不合理的任务顺序会导致foo.php中的@blah@出现被替换为data_dir配置变量的内容。
<file name="foo.php" role="php">
<tasks:replace from="@blah@" to="data_dir" type="pear-config"/>
<tasks:replace from="@blah@" to="version" type="package-info"/>
</file>
然而,相反的顺序会导致foo.php中的@blah@出现,并用package.xml中的<version>标签的内容替换。
<file name="foo.php" role="php">
<tasks:replace from="@blah@" to="version" type="package-info"/>
<tasks:replace from="@blah@" to="data_dir" type="pear-config"/>
</file>
此外,在打包过程中可以执行单个任务。换句话说,有些任务不需要依赖于客户端机器的状态来执行。一个例子是replace任务。package-info替换仅依赖于package.xml文件的内容,该内容在pear package时已知。任务执行的时间被称为任务的安装阶段。目前识别的安装阶段有安装和打包。自定义任务可以使用$phase属性来控制其安装阶段。定义了三个常量:
-
PEAR_TASK_INSTALL:安装阶段 -
PEAR_TASK_PACKAGE:打包阶段 -
PEAR_TASK_PACKAGEANDINSTALL:安装和打包阶段
因此,例如,windowseol任务的阶段声明如下:
var $phase = PEAR_TASK_PACKAGEANDINSTALL;
实际的安装阶段由PEAR_Task_Common的构造函数设置,可以通过$installphase属性访问。唯一合法的值是PEAR_TASK_INSTALL和PEAR_TASK_PACKAGE。此成员用于确定哪些替换应该发生。例如,如果$this->installphase是PEAR_TASK_PACKAGE,则不会执行pear-config和php-const替换。
也许最好的自定义文件任务的介绍是使用在 PEAR 包本身中分发的某些简单任务。最简单的任务是<tasks:windowseol/>和<tasks:unixeol/>任务。这些将处理其文件的内容并将行结束转换为 Windows 格式或 UNIX 格式。以下是windowseol任务的完整源代码:
<?php
/**
* <tasks:windowseol>
*
* PHP versions 4 and 5
*
* LICENSE: This source file is subject to version 3.0 of the PHP
* license that is available through the world-wide-web at the * following URI:
* http://www.php.net/license/3_0.txt. If you did not receive a copy * of the PHP License and are unable to obtain it through the web,
* please send a note to license@php.net so we can mail you a copy
immediately.
*
* @category pear
* @package PEAR
* @author Greg Beaver <cellog@php.net>
* @copyright 1997-2005 The PHP Group
* @license http://www.php.net/license/3_0.txt PHP License 3.0
* @version CVS: $Id: Windowseol.php,v 1.6 2005/10/02 06:29:39
cellog Exp $
* @link http://pear.php.net/package/PEAR
* @since File available since Release 1.4.0a1
*/
/**
* Base class
*/
require_once 'PEAR/Task/Common.php';
/**
* Implements the windows line endings file task.
* @category pear
* @package PEAR
* @author Greg Beaver <cellog@php.net>
* @copyright 1997-2005 The PHP Group
* @license http://www.php.net/license/3_0.txt PHP License 3.0
* @version Release: @package_version@
* @link http://pear.php.net/package/PEAR
* @since Class available since Release 1.4.0a1
*/
class PEAR_Task_Windowseol extends PEAR_Task_Common
{
var $type = 'simple';
var $phase = PEAR_TASK_PACKAGE;
var $_replacements;
/**
* Validate the raw xml at parsing-time.
* @param PEAR_PackageFile_v2
* @param array raw, parsed xml
* @param PEAR_Config
* @static
*/
function validateXml($pkg, $xml, &$config, $fileXml)
{
if ($xml != '') {
return array(PEAR_TASK_ERROR_INVALID, 'no attributes allowed');
}
return true;
}
/**
* Initialize a task instance with the parameters
* @param array raw, parsed xml
* @param unused
*/
function init($xml, $attribs)
{
}
/**
* Replace all line endings with windows line endings
*
* See validateXml() source for the complete list of allowed
fields
* @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
* @param string file contents
* @param string the eventual final file location (informational
only)
* @return string|false|PEAR_Error false to skip this file,
PEAR_Error to fail
* (use $this->throwError), otherwise return the new contents
*/
function startSession($pkg, $contents, $dest)
{
$this->logger->log(3, "replacing all line endings with \\r\\n in $dest");
return preg_replace("/\r\n|\n\r|\r|\n/", "\r\n", $contents);
}
}
?>
如您所见,主要操作是在startSession()方法中执行的。对于大多数任务来说,这已经足够了。接下来,让我们创建我们自己的自定义文件任务!
创建 PEAR_Task_Chiara_Managedb 自定义任务
创建我们的任务的第一步是确定任务期望的目的。在我们的情况下,我们可以用一个需要解决的问题来总结期望的目的。
小贴士
问题:安装和更新包使用的数据库是一个繁琐的过程,应该自动化。
更具体地说,我们需要一个能够执行以下任务的解决方案:
-
在包的新安装上从头创建数据库
-
在升级包时更新现有的数据库结构以反映包新版本中的任何更改
-
能够操作大量不同的数据库,并轻松管理在未来日期迁移到不同的数据库
-
根据用户的控制,为不同的包操作不同的数据库
为了满足这些约束,我们将利用可从pear.php.net/MDB2_Schema获取的 MDB2_Schema 包。此包提供了一些相对于我们从头开始设计的任何自定义解决方案的独特优势:
-
MDB2 支持广泛的数据库驱动程序。
-
用于描述数据库结构的 XML 模式格式是数据库无关的,允许使用 MDB2 支持的任何数据库的用户使用使用此任务的包。
-
有一个广泛的用户基础和几个活跃的维护者,他们帮助确保该包按预期运行。
-
MDB2_Schema::updateDatabase()方法能够通过比较两个模式文件来执行数据库的复杂更新。
此外,我们将依赖于一个不太理想的解决方案来满足每个包需要不同数据库的需求:我们将使用chiaramdb2schema角色提供的配置变量的所需格式。
为了确定任务是否需要为包提供一个唯一的数据库,我们将在 XML 中添加一个名为unique的可选属性。因此,在package.xml中,我们的任务有三个合法的可能性:
<tasks:chiara-managedb/>
<tasks:chiara-managedb unique="0"/>
<tasks:chiara-managedb unique="1"/>
小贴士
需要 1.5.0a1 或更高版本的 PEAR 才能运行此任务
不幸的是,在 1.5.0a1 之前的 PEAR 版本中的一个严重错误阻止了此任务的正确使用,因此如果您想尝试它,请确保您已安装了最新的 PEAR 版本。
此外,由于我们需要chiaramdb2schema角色以确保我们的配置变量已安装并准备好使用,因此我们将要求任务包含一个名为chiaramdb2schema的角色的文件,如下所示:
<file name="blah.xml" role="chiaramdb2schema">
<tasks:chiara-managedb/>
</file>
这是我们的任务的 XML 验证方法:
/**
* Validate the raw xml at parsing-time.
* @param PEAR_PackageFile_v2
* @param array raw, parsed xml
* @param PEAR_Config
* @static
*/
function validateXml($pkg, $xml, &$config, $fileXml)
{
if ($fileXml['role'] !='chiaramdb2schema') {
return array(PEAR_TASK_ERROR_INVALID,
'chiara_managedb task can only be ' .
'used with files whose role is chiaramdb2schema.
File is role "' .
$fileXml['role'] . '"');
}
if (isset($xml['attribs'])) {
if (!isset($xml['attribs']['unique'])) {
return array(PEAR_TASK_ERROR_MISSING_ATTRIB, 'unique');
}
if (!in_array($xml['attribs']['unique'], array('0', '1'))) {
return array (PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE, 'unique',
$xml['attribs']['unique'], array('0', '1'));
}
}
return true;
}
在运行任务时,我们将使用unique属性的值来控制用于连接数据库的数据库 DSN(数据源名称)。因此,以下是我们的初始化方法:
/**
* Initialize a task instance with the parameters
* @param array raw, parsed xml
* @param unused
*/
function init($xml, $attribs)
{
if (isset($xml['attribs']['unique']) &&
$xml['attribs']['unique']) {
$this->_unique = true;
} else {
$this->_unique = false;
}
}
到目前为止,这很简单,不是吗?下一步是确定要使用哪个数据库以及如何连接。为此,我们将结合使用chiaramdb2schema_driver配置变量、chiaramdb2schema_dsn变量和chiara_mdb2schema_password变量。
首先,我们将定义一个方法来从这些配置变量中构建数据源名称(DSN)。在分析源代码之前,让我们看看它的全貌:
/**
* parse the chiaramdb2schema_dsn config variable and the
* password variable to determine an actual DSN that should be * used for this task.
* @return string|PEAR_Error
* @access private
*/
function _parseDSN($pkg)
{
// get channel-specific configuration for this variable
$driver = $this->config->get('chiaramdb2schema_driver', null, $pkg->getChannel());
if (!$driver) {
return PEAR::raiseError('Error: no driver set. use
"config-set ' . 'chiaramdb2schema_driver <drivertype>" before installing');
}
$allDSN = $this->config->get('chiaramdb2schema_dsn', null, $pkg->getChannel());
if (!$allDSN) {
return $this->throwError('Error: no dsn set. use
"config-set ' . 'chiaramdb2schema_dsn <dsn>" before installing');
}
$allPasswords = $this->config->get('chiaramdb2schema_password', null, $pkg->getChannel());
$allDSN = explode(';', $allDSN);
$badDSN = array();
$allPasswords = explode(':', $allPasswords);
for ($i = 0; $i < count($allDSN); $i++) {
if ($i && strpos($allDSN[$i], '::')) {
$allDSN[$i] = explode('::', $allDSN[$i]);
$password = (isset($allPasswords[$i]) &&
$allPasswords[$i]) ? $allPasswords[$i] : '';
if (!strpos($allDSN[$i][1], '@')) {
$password = '';
} elseif ($password) {
// insert password into DSN
$a = explode('@', $allDSN[$i][1]);
$allDSN[$i][1] = $a[0] . ':' . $password . '@';
unset($a[0]);
$allDSN[$i][1] .= implode('@', $a);
}
} elseif (!$i && !strpos($allDSN[0], '::')) {
$password = (isset($allPasswords[0]) &&
$allPasswords[0]) ? $allPasswords[0] : '';
if (!strpos($allDSN[0], '@'))
{$password = '';
} elseif ($password) {
// insert password into DSN
$a = explode('@', $allDSN[0]);
$allDSN[0] = $a[0] . ':' . $password . '@';
unset($a[0]);
$allDSN[0] .= implode('@', $a);
}
} else {
// invalid DSN
$badDSN[$i] = $allDSN[$i];
$allDSN[$i] = false;
}
}
if ($this->_unique) {
$lookfor = array($pkg->getPackage(), $pkg->getPackage() . '#' . $this->_file);
foreach ($allDSN as $i => $dsn) {
if (!$i) {
continue;
}
if (strcasecmp($dsn[0], $lookfor[0]) === 0) {
return $driver . '://' . $dsn[1];
}
if (strcasecmp($dsn[0], $lookfor[1]) === 0) {
return $driver . '://' . $dsn[1];
}
}
return $this->throwError('No valid DSNs for package "' .
$pkg->getPackage() . '" were found in config variable
chiaramdb2schema_dsn');
} else {
if (!$allDSN[0]) {
return $this->throwError('invalid default DSN "' .
$badDSN[0] . '" in config variable chiaramdb2schema_dsn');
}
return $driver . '://' . $allDSN[0];
}
}
首先,使用构造函数中设置的$config成员检索配置变量:
// get channel-specific configuration for this variable
$driver = $this->config->get('chiaramdb2schema_driver', null, $pkg->getChannel());
if (!$driver) {
return PEAR::raiseError('Error: no driver set. use
"config-set ' . 'chiaramdb2schema_driver <drivertype>" before installing');
}
$allDSN = $this->config->get('chiaramdb2schema_dsn', null, $pkg->getChannel());
if (!$allDSN) {
return $this->throwError('Error: no dsn set. use
"config-set ' . 'chiaramdb2schema_dsn <dsn>" before installing');
}
$allPasswords = $this->config->get('chiaramdb2schema_password', null, $pkg->getChannel());
为了在用户端简化事情,我们还将尝试检索包的通道配置数据,然后默认使用pear.php.net通道配置。
接下来,我们将根据其分隔符";"拆分DSN变量,并根据其分隔符":"拆分Passwords变量。通过遍历DSN变量,我们可以为每个 DSN 插入适当的密码。例如,对于 DSN "user:pass@localhost/databasename",DSN 将存储为"user@localhost/databasename",因此我们需要在"@"之前插入":pass"。此外,第一个 DSN 是默认 DSN,如果找到非包特定的 DSN,则使用该 DSN(通过$allDSN[0]找到),所以这是一个特殊情况。
$allDSN = explode(';', $allDSN);
$badDSN = array();
$allPasswords = explode(':', $allPasswords);
for ($i = 0; $i < count($allDSN); $i++) {
if ($i && strpos($allDSN[$i], '::')) {
$allDSN[$i] = explode('::', $allDSN[$i]);
$password = (isset($allPasswords[$i]) &&
$allPasswords[$i]) ?
$allPasswords[$i] : '';
if (!strpos($allDSN[$i][1], '@')) {
$password = '';
} elseif ($password) {
// insert password into DSN
$a = explode('@', $allDSN[$i][1]);
$allDSN[$i][1] = $a[0] . ':' . $password . '@';
unset($a[0]);
$allDSN[$i][1] .= implode('@', $a);
}
} elseif (!$i && !strpos($allDSN[0], '::')) {
$password = (isset($allPasswords[0]) &&
$allPasswords[0]) ?
$allPasswords[0] : '';
if (!strpos($allDSN[0], '@')) {
$password = '';
} elseif ($password) {
// insert password into DSN
$a = explode('@', $allDSN[0]);
$allDSN[0] = $a[0] . ':' . $password . '@';
unset($a[0]);
$allDSN[0] .= implode('@', $a);
}
} else {
// invalid DSN
$badDSN[$i] = $allDSN[$i];
$allDSN[$i] = false;
}
}
最后,我们将确定包是否需要使用$this->_unique指定的特定数据库连接字符串,正如你回忆的那样,这是在init()方法中设置的。包特定的 DSN 以包名称为前缀,例如"Packagename::user:password@localhost/databasename",或者在一个包内的特定文件中,例如"Packagename#file::user:password@localhost/databasename",因此我们将搜索解析的 DSN,直到找到或失败。
最后,在确定要使用哪个 DSN 之后,我们需要在前面加上应该连接到的数据库类型。例如,这可以是 MySQL、MySQLi、OCI、Firebird、pgSQL 等等。
if ($this->_unique) {
$lookfor = array($pkg->getPackage(), $pkg->getPackage() . '#' . $this->_file);
foreach ($allDSN as $i => $dsn) {
if (!$i) {
continue;
}
if (strcasecmp($dsn[0], $lookfor[0]) === 0) {
return $driver . ':/' . $dsn[1];
}
if (strcasecmp($dsn[0], $lookfor[1]) === 0) {
return $driver . ':/' . $dsn[1];
}
}
return $this->throwError('No valid DSNs for package "' .
$pkg->getPackage() .
'" were found in config variable
chiaramdb2schema_dsn');
} else {
if (!$allDSN[0]) {
return $this->throwError('invalid default DSN "' .
$badDSN[0] . '" in config variable
chiaramdb2schema_dsn');
}
return $driver . ':/' . $allDSN[0];
}
如果你发现你的眼睛开始发花,不要害怕。重要的是要意识到,在体验结束时,该方法将返回一个包含详细错误信息的PEAR_Error,或者一个像"mysqli://user:pass@localhost/databasename"这样的字符串。
我们自定义任务的最后一部分是startSession()方法,它实际上执行任务,因为这是一个类型为单的任务。
/**
* Update the database.
*
* First, determine which DSN to use from the
* chiaramdb2schema_dsn config variable
* with {@link _parseDSN()}, then determine whether the database
* already exists based
* on the contents of a previous installation, and finally use
* {@link MDB2_Schema::updateDatabase()} * to update the database itself
*
* PEAR_Error is returned on any problem.
* See validateXml() source for the complete list of allowed fields
* @param PEAR_PackageFile_v2
* @param string file contents
* @param string the eventual final file location * (informational only)
* @return string|false|PEAR_Error false to skip this file,
* PEAR_Error to fail
* (use $this->throwError), otherwise return the new contents
*/
function startSession($pkg, $contents, $dest)
{
$this->_file = basename($dest);
$dsn = $this->_parseDSN($pkg);
if (PEAR::isError($dsn)) {
return $dsn;
}
require_once 'MDB2/Schema.php';
require_once 'System.php';
$tmp = System::mktemp(array('foo.xml'));
if (PEAR::isError($tmp)) {
return $tmp;
}
$fp = fopen($tmp, 'wb');
fwrite($fp, $contents);
fclose($fp);
$schema = &MDB2_Schema::factory($dsn);
$reg = &$this->config->getRegistry();
if ($installed && file_exists($dest)) {
// update existing database
$res = $schema->updateDatabase($tmp, $dest);
if (PEAR::isError($res)) {
return PEAR::raiseError($res->getMessage() . $res->getUserInfo());
}
} else {
// create new database
$res = $schema->updateDatabase($tmp);
if (PEAR::isError($res)) {
return PEAR::raiseError($res->getMessage() . $res->getUserInfo());
}
}
// unmodified
return $contents;
}
MDB2_Schema::updateDatabase()需要两个模式文件才能升级数据库。在升级数据库时,我们将使用最终的安装目标$dest来确定我们是否正在替换现有的模式文件。如果是这样,则将其传递给updateDatabase()。否则,我们只需调用updateDatabase()来创建新的数据库结构。
注意,在此阶段,文件内容尚未写入磁盘,因为任务是在安装之前对文件进行操作的。因此,我们将使用包含在 PEAR 包中的System类创建的临时位置写入模式文件。
任务的大部分工作由MDB2_Schema类执行。在完成任务后,用户的数据库将在安装和升级时自动配置。
可能的所有自定义文件任务的全范围
可用于自定义任务的方法有:
-
true|array validXml($pkg, $xml, &$config, $fileXml):验证任务XML -
void init($xml, $fileAttributes, $lastVersion):初始化任务 -
true|PEAR_Error startSession($pkg, $contents, $dest):开始(通常完成)任务处理 -
true|PEAR_Error run($tasks):仅对类型为"multiple"的任务,处理所有任务并执行所需操作
validXml($pkg, $xml, &$config, $fileXml)
这种方法在package.xml验证期间用于所有三种类型的任务,以验证特定任务的 XML。$pkg是一个表示包含任务的package.xml的PEAR_PackageFile_v2对象。这是只读的,应该仅用于检索信息。$xml是文件任务的解析内容,$config是一个表示当前配置的PEAR_Config对象,而$fileXml是package.xml文件标签的解析内容。
这里是一些示例任务 XML 和$xml变量内容的简单映射:
| XML | 解析内容 |
|---|---|
<tasks:something/> |
'' |
<tasks:something att="blah"/> |
array('attribs' => array('att' => 'blah')) |
<tasks:something>blah</tasks:something> |
'blah' |
<tasks:something att="blah">blah2</tasks:something> |
array('attribs' => array('att' => 'blah'), '_content' => 'blah2') |
<tasks:something> <tasks:subtag>hi</tasks:subtag> </tasks:something> |
array('tasks:subtag' => 'hi') |
<tasks:something> <tasks:subtag>hi</tasks:subtag> <tasks:subtag att="blah">again</tasks:subtag> </tasks:something> |
array('tasks:subtag' => array(0 => 'hi', 1 => array('attribs' => array('att' => 'blah'), '_content' => 'again')))) |
$fileXml 参数将包含一个此格式的数组,其中包含在 <file> 标签中定义的所有属性。
array('attribs' => array('name' => 'Filename', 'role' =>
'filerole',...));
错误应返回为数组。第一个索引必须是以下错误代码之一:
-
PEAR_TASK_ERROR_NOATTRIBS:数组应返回为:array(PEAR_TASK_ERROR_NOATTRIBS); -
PEAR_TASK_ERROR_MISSING_ATTRIB:数组应返回为:array(PEAR_TASK_ERROR_MISSING_ATTRIB, 'attributename'); -
PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE:数组应返回为:array(PEAR_TASK_ERROR_WRONG_ATTRIB_VALUE, 'attributename', 'actualvalue', ['expectedvalue'|array('expectedvalue1', 'expectedvalue2',...)]); -
PEAR_TASK_ERROR_INVALID:数组应返回为:array(PEAR_TASK_ERROR_INVALID, 'unusual error message');
init($xml, $fileAttributes, $lastVersion)
init() 方法被调用以初始化所有非脚本任务,并且可用于任何目的。三个参数是:
-
mixed $xml:表示package.xml中任务的 XML 的数组。这与传递给validXml()的$xml参数的格式相同。 -
array $fileAttributes:表示文件属性的数组。这与传递给validXml()的$fileXml参数的格式相同。 -
string|NULL $lastVersion:如果正在升级包,则为包的最后一个安装版本,如果这是第一次安装此包,则为NULL。这可以用于依赖于先前安装配置的任务。
init() 的任何返回值都将被丢弃。
startSession($pkg, $contents, $dest)
startSession() 方法被调用以执行任务,并在 init() 方法之后调用。需要注意的是,此方法预期返回文件的精确内容,因为它应该被安装到磁盘上。不应在磁盘上修改文件。如果在运行任务时发生任何错误,应返回一个包含描述问题的清晰错误消息的 PEAR_Error 对象,并包含包含任务的文件信息。
如果任务确定此文件不应安装,返回 FALSE 将提示安装程序跳过此文件的安装。请注意,只有字面量 FALSE 会导致跳过安装;空字符串、数字 0 和任何其他可以用作假条件的字面量都不会影响安装。
在任务成功执行后,必须返回完整的文件内容。返回值用于将文件内容写入磁盘。例如,windowseol 任务在将所有新行转换为 \r\n 后返回 $contents 的值。
传递给 startSession() 的参数是:
-
PEAR_PackageFile_v2 $pkg:代表包含此任务的完整package.xml的包文件对象。 -
string $contents:文件的完整内容,可以在任务成功完成后对其进行操作并返回。 -
string $dest:文件最终安装位置的完整路径。这仅用于信息用途。
run($tasks)
此方法仅对类型为多重的任务调用。$tasks 参数是 package.xml 中每个多重任务的数组。例如,如果 package.xml 包含类型为多重的 <tasks:foo/> 和 <tasks:bar/> 任务,run() 方法将为所有 foo 任务调用,并且 $tasks 参数将包含每个 foo 任务的数组。然后,相同的程序将重复用于 bar 任务。
run() 方法在安装成功完成后被调用,因此可以操作包的已安装内容。
在出错时,run() 方法应返回一个包含有关任务失败原因的详细信息的 PEAR_Error 对象。所有其他返回值都被忽略。
适用于终极定制的安装后脚本
第三个也是最后一个任务是安装后脚本。这些是最强大和可定制的任务,可以字面意义上用于执行安装所需的任何定制。PEAR 安装程序通过在 package.xml 文件中定义一系列问题来实施安装后脚本,并通过传递用户给出的答案到一个特殊的 PHP 文件中来执行。以下是一组简单的问题和相应的安装后脚本:
首先,package.xml 中的 XML:
<file name="rolesetup.php" role="php">
<tasks:postinstallscript>
<tasks:paramgroup>
<tasks:id>setup</tasks:id>
<tasks:param>
<tasks:name>channel</tasks:name>
<tasks:prompt>Choose a channel to modify configuration
values from</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>pear.php.net</tasks:default>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>driver</tasks:id>
<tasks:instructions>
In order to set up the database, please choose a database
driver.
This should be a MDB2-compatible driver name, such as mysql, mysqli,
Pgsql, oci8, etc.
</tasks:instructions>
<tasks:param>
<tasks:name>driver</tasks:name>
<tasks:prompt>Database driver?</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>choosedsn</tasks:id>
<tasks:param>
<tasks:name>dsnchoice</tasks:name>
<tasks:prompt>%sChoose a DSN to modify, or to add a new
dsn, type
"new". To remove a DSN prepend with
"!"</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>new</tasks:default>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>deletedsn</tasks:id>
<tasks:param>
<tasks:name>confirm</tasks:name>
<tasks:prompt>Really delete "%s" DSN? (yes to
delete)</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>no</tasks:default>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>modifydsn</tasks:id>
<tasks:name>choosedsn::dsnchoice</tasks:name>
<tasks:conditiontype>!=</tasks:conditiontype>
<tasks:value>new</tasks:value>
<tasks:param>
<tasks:name>user</tasks:name>
<tasks:prompt>User name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>password</tasks:name>
<tasks:prompt>Database password</tasks:prompt>
<tasks:type>password</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>host</tasks:name>
<tasks:prompt>Database host</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>localhost</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>database</tasks:name>
<tasks:prompt>Database name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>newpackagedsn</tasks:id>
<tasks:param>
<tasks:name>package</tasks:name>
<tasks:prompt>Package name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>host</tasks:name>
<tasks:prompt>Database host</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>localhost</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>user</tasks:name>
<tasks:prompt>User name</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>root</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>password</tasks:name>
<tasks:prompt>Database password</tasks:prompt>
<tasks:type>password</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>database</tasks:name>
<tasks:prompt>Database name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>newdefaultdsn</tasks:id>
<tasks:param>
<tasks:name>host</tasks:name>
<tasks:prompt>Database host</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>localhost</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>user</tasks:name>
<tasks:prompt>User name</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>root</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>password</tasks:name>
<tasks:prompt>Database password</tasks:prompt>
<tasks:type>password</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>database</tasks:name>
<tasks:prompt>database name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
</tasks:postinstallscript>
</file>
然后,安装后脚本(rolesetup.php 的内容):
<?php
/**
* Post-installation script for the Chiara_Managedb task.
*
* This script takes user input on DSNs and sets up DSNs, allowing
* the addition of one custom DSN per iteration.
* @version @package_version@
*/
class rolesetup_postinstall
{
/**
* object representing package.xml
* @var PEAR_PackageFile_v2
* @access private
*/
var $_pkg;
/**
* Frontend object
* @var PEAR_Frontend
* @access private
*/
var $_ui;
/**
* @var PEAR_Config
* @access private
*/
var $_config;
/**
* The actual DSN value as will be saved to the configuration file
* @var string
*/
var $dsnvalue;
/**
* The actual password value as will be saved to the * configuration file
* @var string
*/
var $passwordvalue;
/**
* The channel to modify configuration values from
*
* @var string
*/
var $channel;
/**
* The task object used for dsn serialization/unserialization
* @var PEAR_Task_Chiara_Managedb
*/
var $managedb;
/**
* An "unserialized" array of DSNs parsed from the chiaramdb2schema
* configuration variables.
* @var array
*/
var $dsns;
/**
* The index of the DSN in $this->dsns we will be modifying
* @var string
*/
var $choice;
/**
* Initialize the post-installation script
*
* @param PEAR_Config $config
* @param PEAR_PackageFile_v2 $pkg
* @param string|null $lastversion Last installed version. * Not used in this script
* @return boolean success of initialization
*/
function init(&$config, &$pkg, $lastversion)
{
require_once 'PEAR/Task/Chiara/Managedb.php';
$this->_config = &$config;
$this->_ui = &PEAR_Frontend::singleton();
$this->managedb = new PEAR_Task_Chiara_Managedb($config,
$this->_ui, PEAR_TASK_INSTALL);
$this->_pkg = &$pkg;
if (!in_array('chiaramdb2schema_dsn', $this->_config->getKeys())) {
// fail: role was not installed?
return false;
}
$this->channel = $this->_config->get('default_channel');
$this->dsns = PEAR::isError( $e = $this->managedb->unserializeDSN($pkg)) ? array() : $e;
return true;
}
/**
* Set up the prompts properly for the script
*
* @param array $prompts
* @param string $section
* @return array
*/
function postProcessPrompts($prompts, $section)
{
switch ($section) {
case 'driver' :
if ($this->driver) {
$prompts[0]['default'] = $this->driver;
}
break;
case 'deletedsn' :
$count = 1;
foreach ($this->dsns as $i => $dsn) {
$text = ($i ? "(Package $i) " : '') . $dsn;
if ($count == $this->choice) {
break;
}
$count++;
}
$prompts[0]['prompt'] = sprintf($prompts[0]['prompt'], $text);
break;
case 'choosedsn' :
$text = '';
$count = 1;
foreach ($this->dsns as $i => $dsn) {
$text .= "[$count] " . ($i ? "(Package $i) " : '') . $dsn . "\n";
$count++;
}
$prompts[0]['prompt'] =
sprintf($prompts[0]['prompt'], $text);
break;
case 'modifydsn' :
$count = 1;
$found = false;
foreach ($this->dsns as $i => $dsn) {
if ($count == $this->choice) {
$found = true;
break;
}
$count++;
}
if ($found) {
$dsn = MDB2::parseDSN($this->dsns[$i]);
// user
$prompts[0]['default'] = $dsn['username'];
// password
if (isset($dsn['password'])) {
$prompts[1]['default'] = $dsn['password'];
}
// host
$prompts[2]['default'] = $dsn['hostspec'];
if (isset($dsn['port'])) {
$prompts[2]['default'] .= ':' . $dsn['port'];
}
// database
$prompts[3]['default'] = $dsn['database'];
}
break;
}
return $prompts;
}
/**
* Run the script itself
*
* @param array $answers
* @param string $phase
*/
function run($answers, $phase)
{
switch ($phase) {
case 'setup' :
return $this->_doSetup($answers);
break;
case 'driver' :
require_once 'MDB2.php';
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
if (PEAR::isError($err =
MDB2::loadFile('Driver' . DIRECTORY_SEPARATOR .
$answers['driver']))) {
PEAR::popErrorHandling();
$this->_ui->outputData( 'ERROR: Unknown MDB2 driver "' .
$answers['driver'] . '": ' .
$err->getUserInfo() . '. Be sure you have
installed ' . 'MDB2_Driver_' .
$answers['driver']);
return false;
}
PEAR::popErrorHandling();
$ret = $this->_config->set('chiaramdb2schema_driver',
$answers['driver'],
'user', $this->channel);
return $ret && $this->_config->writeConfigFile();
break;
case 'choosedsn' :
if ($answers['dsnchoice'] && $answers['dsnchoice']{0} == '!') {
// delete a DSN
$answers['dsnchoice'] =
substr($answers['dsnchoice'], 1);
} else {
$this->_ui->skipParamgroup('deletedsn');
}
if ($answers['dsnchoice'] > count($this->dsns)) {
$this->_ui->outputData('ERROR: No suchdsn "' .
$answers['dsnchoice'] . '"');
return false;
}
$this->choice = $answers['dsnchoice'];
break;
case 'deletedsn' :
$this->_ui->skipParamgroup('modifydsn');
$this->_ui->skipParamgroup('newpackagedsn');
$this->_ui->skipParamgroup('newdefaultdsn');
if ($answers['confirm'] == 'yes') {
$count = 1;
foreach ($this->dsns as $i => $dsn) {
if ($count == $this->choice) {
unset($this->dsns[$i]);
break;
}
$count++;
}
$this->_ui->outputData('DSN deleted');
$this->managedb->serializeDSN($this->dsns, $this->channel);
return true;
} else {
$this->_ui->outputData('No changes performed');
}
break;
case 'modifydsn' :
$count = 1;
$found = false;
foreach ($this->dsns as $i => $dsn) {
if ($count == $this->choice) {
$found = true;
break;
}
$count++;
}
if (!$found) {
$this->_ui->outputData('ERROR: DSN "' . $this->choice . '" not found!');
return false;
}
$dsn = $answers['user'] . ':' . $answers['password'] . '@' .
$answers['host'] . '/' . $answers['database'];
$this->dsns[$i] = $dsn;
$this->managedb->serializeDSN($this->dsns, $this->channel);
$this->_ui->skipParamgroup('newpackagedsn');
$this->_ui->skipParamgroup('newdefaultdsn');
break;
case 'newpackagedsn' :
$dsn = $answers['user'] . ':' . $answers['password'] . '@' .
$answers['host'] . '/' . $answers['database'];
$this->dsns[$answers['package']] = $dsn;
$this->managedb->serializeDSN($this->dsns, $this->channel);
$this->_ui->skipParamgroup('newdefaultdsn');
break;
case 'newdefaultdsn' :
$dsn = $answers['user'] . ':' . $answers['password'] . '@' .
$answers['host'] . '/' . $answers['database'];
$this->dsns[0] = $dsn;
$this->managedb->serializeDSN($this->dsns, $this->channel);
break;
case '_undoOnError' :
// answers contains paramgroups that succeeded in
// reverse order foreach ($answers as $group) {
}
break;
}
return true;
}
/**
* Run the setup paramgroup
*
* @param array $answers
* @return boolean
* @access private
*/
function _doSetup($answers)
{
$reg = &$this->_config->getRegistry();
if (!$reg->channelExists($answers['channel'])) {
$this->_ui->outputData('ERROR: channel "' .
$answers['channel'] . '" is not registered, use the channel-discover command');
return false;
}
$this->channel = $answers['channel'];
$this->driver = $this->_config->get('chiaramdb2schema_driver', null, $this->channel);
$this->dsnvalue = $this->_config->get('chiaramdb2schema_dsn',
null, $this->channel);
$this->passwordvalue = $this->_config->get('chiaramdb2schema_dsn', null,
$this->channel);
if (!$this->dsnvalue) {
// magically skip the "choosedsn", "deleteDSN" and
// "modifydsn" <paramgroup>s,
// and only create a new, default DSN
$this->_ui->skipParamgroup('choosedsn');
$this->_ui->skipParamgroup('deletedsn');
$this->_ui->skipParamgroup('modifydsn');
$this->_ui->skipParamgroup('newpackagedsn');
}
return true;
}
}
?>
安装后脚本与 PEAR 提供的不同前端紧密交互。脚本有许多可用可能性。除了使用用户提供的数据外,安装后脚本可以基于用户的先前答案交互式地修改提示,并且可以动态地跳过整个 <tasks:paramgroup> 部分。这些功能允许对实际脚本的显著定制。
安装后脚本的组成部分
每个安装后脚本必须定义两个方法,init() 和 run()。init() 方法应定义得类似于这样:
/**
* Initialize the post-installation script
*
* @param PEAR_Config $config
* @param PEAR_PackageFile_v2 $pkg
* @param string|null $lastversion Last installed version. * Not used in this script
* @return boolean success of initialization
*/
function init(&$config, &$pkg, $lastversion)
{
require_once 'PEAR/Task/Chiara/Managedb.php';
$this->_config = &$config;
$this->_ui = &PEAR_Frontend::singleton();
$this->managedb = new PEAR_Task_Chiara_Managedb($config, $this->_ui,
PEAR_TASK_INSTALL);
$this->_pkg = &$pkg;
if (!in_array('chiaramdb2schema_dsn', $this->_config->getKeys())) {
// fail: role was not installed?
return false;
}
$this->channel = $this->_config->get('default_channel');
$this->dsns = PEAR::isError($e = $this->managedb->unserializeDSN($pkg)) ? array() : $e;
return true;
}
注意使用$this->_ui = &PEAR_Frontend::singleton(): 这行代码打开了一个巨大的可能性。除了公开可用的整个公共 API 以显示文本外,还包括:
-
Void outputData(string $text):向用户显示信息 -
string bold(string $text):接受文本并返回该文本的粗体转换版本,然后可以将其传递给outputData()
这使得skipParamGroup(string $id)方法可用。$id参数应该是尚未执行的 paramgroup 的 ID(来自<tasks:paramgroup>标签的<tasks:id>标签的内容)。
通过创建一个名为postProcessPrompts()的方法来修改提示或参数的默认值,如下所示:
/**
* Set up the prompts properly for the script
*
* @param array $prompts
* @param string $section
* @return array
*/
function postProcessPrompts($prompts, $section)
{
switch ($section) {
case 'driver' :
if ($this->driver) {
$prompts[0]['default'] = $this->driver;
}
break;
case 'deletedsn' :
$count = 1;
foreach ($this->dsns as $i => $dsn) {
$text = ($i ? "(Package $i) " : '') . $dsn;
if ($count == $this->choice) {
break;
}
$count++;
}
$prompts[0]['prompt'] =
sprintf($prompts[0]['prompt'], $text);
break;
case 'choosedsn' :
$text = '';
$count = 1;
foreach ($this->dsns as $i => $dsn) {
$text .= "[$count] " . ($i ? "(Package $i) " :
'') . $dsn . "\n";
$count++;
}
$prompts[0]['prompt'] =
sprintf($prompts[0]['prompt'], $text);
break;
case 'modifydsn' :
$count = 1;
$found = false;
foreach ($this->dsns as $i => $dsn) {
if ($count == $this->choice) {
$found = true;
break;
}
$count++;
}
if ($found) {
$dsn = MDB2::parseDSN($this->dsns[$i]);
// user
$prompts[0]['default'] = $dsn['username'];
// password
if (isset($dsn['password'])) {
$prompts[1]['default'] = $dsn['password'];
}
// host
$prompts[2]['default'] = $dsn['hostspec'];
if (isset($dsn['port'])) {
$prompts[2]['default'] .= ':' . $dsn['port'];
}
// database
$prompts[3]['default'] = $dsn['database'];
}
break;
}
return $prompts;
}
$prompts参数将是<tasks:paramgroup>标签的解析内容。
<tasks:paramgroup>
<tasks:id>databaseSetup</tasks:id>
<tasks:param>
<tasks:name>database</tasks:name>
<tasks:prompt>%s database name</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>pear</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>user</tasks:name>
<tasks:prompt>%s database username</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>%s_pear</tasks:default>
</tasks:param>
</tasks:paramgroup>
对于这个paramgroup,$prompts变量如下所示:
array(
'id' => 'databaseSetup';
'param' =>
array(
array(
'name' => 'database',
'prompt' => '%s database name',
'type' => 'string',
'default' => 'pear',
),
array(
'name' => 'user',
'prompt' => '%s database username',
'type' => 'string',
'default' => '%s_pear',
),
),
);
postProcessPrompts()方法应该返回经过修改的$prompts数组,仅修改提示和默认字段。如果修改了其他内容,将导致安装后脚本直接失败。
例如,在确定用户正在使用 pgSQL 驱动程序后,postProcessPrompts()的返回值可能是:
array(
'id' => 'databaseSetup';
'param' =>
array(
array(
'name' => 'database',
'prompt' => 'Postgresql database name',
'type' => 'string',
'default' => 'pear',
),
array(
'name' => 'user',
'prompt' => 'Postgresql database username',
'type' => 'string',
'default' => 'pgsql_pear',
),
),
);
此外,还可以替换整个提示。这可能是处理国际化的简单方法。例如:
array(
'id' => 'databaseSetup';
'param' =>
array(
array(
'name' => 'database',
'prompt' => 'Nom de la base de données Postgresql',
'type' => 'string',
'default' => 'pear',
),
array(
'name' => 'user',
'prompt' => 'Nom d'utilisateur de la base de données Postgresql',
'type' => 'string',
'default' => 'pgsql_pear',
),
),
);
run()方法应接受两种类型的参数。在正常操作中,第一个参数将是一个包含用户答案的数组,第二个参数是 paramgroup 的 ID。对于这个<tasks:paramgroup>,示例值可能如下:
array(
'database' => 'huggiepear',
'user' => 'killinator',
);
如你所想,ID 将是'databaseSetup'。
除了这些旨在成功的功能外,有时在出错时需要中止安装后脚本。在这些情况下,run()方法也带有两个参数,但第二个是'_undoOnError',第一个是按逆序排列的已完成的 paramgroup ID 数组,以方便迭代回滚安装后脚本所做的更改。
小贴士
_undoOnError 是错误头而不是另一个 paramgroup ID 吗?
Paramgroup ID 不能以下划线开头,它只能包含字母数字字符。因此,_undoOnError是错误头,而不是另一个 paramgroup ID。
将多个包打包成一个单独的存档
通常,将包及其依赖项打包成一个可安装的存档是一个期望的功能。有两种方法可以实现这一点。最简单的方法是使用一个类似于下面的package.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<package version="2.0"
xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>PEAR_all</name>
<channel>pear.php.net</channel>
<summary>PEAR Base System</summary>
<description>
The PEAR package and its dependencies
</description>
<lead>
<name>Greg Beaver</name>
<user>cellog</user>
<email>cellog@php.net</email>
<active>yes</active>
</lead>
<date>2005-09-25</date>
<version>
<release>1.4.2</release>
<api>1.0.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.php.net/license">PHP License</license>
<notes>
This contains PEAR version 1.4.2 and its dependencies
</notes>
<contents>
<bundledpackage>PEAR-1.4.2.tgz</bundledpackage>
<bundledpackage>Archive_Tar-1.3.1.tgz</bundledpackage>
<bundledpackage>Console_Getopt-1.2.tgz</bundledpackage>
<bundledpackage>XML_RPC-1.4.3.tgz</bundledpackage>
</contents>
<dependencies>
<required>
<php>
<min>4.2</min>
</php>
<pearinstaller>
<min>1.4.0a12</min>
</pearinstaller>
</required>
</dependencies>
<bundle/>
</package>
这个简单的package.xml可以打包成PEAR_all-1.4.2.tgz,并作为单个存档分发,用户可以使用它从非互联网位置升级所有包:
$ pear upgrade PEAR_all-1.4.2.tgz
分发依赖项的另一种方法是旧版 bundle-all-dependencies 方法和 PEAR 分发依赖项方法的巧妙混合。
向后兼容性:使用 package.xml 1.0 和 2.0
PEAR 版本 1.4.0 及更高版本最重要的新特性之一是,随着 package.xml 2.0 的出现,能够使包适用于较旧的 PEAR 版本。使用以下命令调用的包命令,将 package.xml 作为输入,并输出一个 GZIP 压缩的 tar 文件(.tgz)。
$ pear package
如果包名为 Foo,版本为 1.0.0,.tgz 文件将被命名为 Foo-1.0.0.tgz。从版本 1.4.0 开始,如果有第二个名为 package2.xml 的 package.xml,包命令将尝试将其包含在存档中。当 PEAR 下载包进行安装时,它首先寻找一个 package2.xml 文件,该文件始终以 2.0 格式,然后回退到 package.xml。这样,就支持了较旧的 PEAR 版本,因为它们总是首先寻找 package.xml。
为了使此功能正常工作,PEAR 对 package.xml 文件的内容进行非常严格的比较。package.xml 版本 1.0 和 package.xml 版本 2.0 必须满足以下约束列表才能被认为是等效的,否则验证将失败:
-
相同的包名称
-
相同的包摘要
-
相同的包描述
-
相同的包版本(发布版本)
-
相同的包稳定性(发布稳定性/状态)
-
相同的许可证
-
相同的发布说明
-
相同的维护者
-
package.xml 1.0中的所有文件都必须存在于package.xml 2.0 <contents>
注意,由于 package.xml 2.0 允许使用 <ignore> 标签在安装期间忽略文件的存在,因此 package.xml 2.0 可以用来提供既可由 PEAR 安装,又可解压后直接使用的存档。
为什么支持过时的 package.xml 1.0?
这在 PHP 界是一个常见的争议。为什么支持向后兼容性?这些是过时且存在错误的 PEAR 版本,对吧?是的,它们是过时且存在错误的版本,任何使用它们的人都在寻求麻烦,但人们可能找不到一个令人信服的理由去升级他们的 PEAR 安装器,仅仅是为了使用你的包,因为对他们来说“已经足够好”。作为包分发者,你的目标应该是使升级过程尽可能无痛。只有在你实际上在 PHP 代码中使用 PEAR 的新功能,或者你的包是一个没有安装用户基础的全新包时,你才应该停止支持 package.xml 版本 1.0。
PEAR 的发展速度非常快,但新安装器的采用不会一夜之间发生。像 Linux 发行版这样的大型软件项目需要时间来评估新功能,并在采用新版本之前确保一切正常工作。作为 PEAR 开发者,我们必须尊重这一需求。
一旦安装的用户不再使用过时且存在错误的 PEAR 版本,升级安装器依赖项应尽快进行,以使用户的利益为重。话虽如此,PEAR 用户应尽快升级以避免在较旧的 PEAR 安装器版本中发现的安全漏洞。
小贴士
PEAR 1.4.3 及更早版本的安全问题
在撰写本章的前几个月,PEAR 中发现了两个主要的安全漏洞。基本上,如果你正在使用 PEAR 1.4.3 或更早版本,你需要尽快升级。
详细信息可在:pear.php.net/advisory-20051104.txt 和 pear.php.net/advisory-20060108.txt 查找。
案例研究:PEAR 包
PEAR 是一个始终需要支持package.xml 1.0的包的完美例子。我们总会有一批用户从早期版本升级到最新版本,而 PEAR 1.3.6 及更早版本根本不了解package.xml 2.0。如果我们不能使 PEAR 升级成为可能,那么让代码可用就没有太多意义。
然而,与此同时,新的依赖特性以及package.xml 2.0的任务对于 PEAR 包来说非常重要,因此需要同时使用package.xml 1.0和package.xml 2.0。例如,pear命令本身在 UNIX 上是一个 shell 脚本(带有 UNIX \n 行结束符),在 Windows 上是一个批处理文件(带有 Windows \r\n 行结束符)。在package.xml 2.0之前,必须将这些脚本作为二进制文件添加到 CVS 中,以确保行结束符不会被打包者的系统行结束符替换。现在,通过使用<tasks:windowseol/>和<tasks:unixeol/>任务,这不再是必要的,因为正确的行结束符在打包时已经设置好了。此外,由于 PEAR 1.4.0 及更早版本与 PEAR_Frontend_Web 的早期版本以及较老的 PEAR_Frontend_Gtk(已被 PEAR_Frontend_Gtk2 取代)之间的不兼容性,有必要检查这些版本的存在,如果版本正确,则静默成功。package.xml 2.0通过在包依赖中使用<conflicts/>标签提供了这一功能。
PEAR_PackageFileManager
虽然在有限的情况下可以使用convert和pickle命令来管理package.xml的 1.0 和 2.0 版本,但这些命令也可能很危险。通过使用 PEAR 包 PEAR_PackageFileManager,从单一位置维护两个版本的package.xml是一个更安全的方法。这个包提供了一个简单的接口,可以从中导入现有的package.xml文件并更新为当前信息,或者从头开始创建一个新的package.xml文件。此外,它非常简单地将现有的package.xml 2.0(无论其多么复杂)转换为等效的package.xml 1.0,并绝对控制每个package.xml的内容。
获取 PEAR_PackageFileManager
可以通过使用 PEAR 安装程序轻松获取 PEAR_PackageFileManager。在撰写本书时,版本 1.6.0b1 可用。要安装它,您必须通过以下方式设置preferred_state配置变量为beta:
$ pear config-set preferred_state beta
或者,您可以运行:
$ pear install PEAR_PackageFileManager-beta
当然,始终确定最新版本在 pear.php.net/package/PEAR_PackageFileManager 并安装该版本是一个好习惯。
PEAR_PackageFileManager 脚本及其生成的 package.xml 文件
对于我们的示例 PEAR_PackageFileManager 脚本,我们将为 Chiara_Managedb 任务生成一个 package.xml 文件。在编写 package.xml 脚本之前,让我们确保我们理解了我们希望 package.xml 包含的组件。在我们的例子中,我们将有三个文件需要打包,包含任务实际代码的 Managedb.php 文件,包含可读/可写 PEAR_Task_Chiara_Managedb_rw 类的 rw.php 文件,用于通过 PEAR_PackageFile_v2_rw API 将任务添加到 package.xml,以及 rolesetup.php,它是初始化 chiaramdb2schema 配置变量的安装后脚本。
在深入研究源代码之前,有几个重要的细节需要注意。首先,这个脚本是从头开始生成 package.xml 文件的。当使用 importOptions() 方法时,大多数脚本不需要这种详细程度。此外,需要注意的是,PEAR_PackageFileManager2 类扩展了 PEAR 自身提供的 PEAR_PackageFile_v2_rw 类。这允许使用如
使用 setPackage() 等方法来调整 package.xml 的内容。让我们看看如何生成一个包含安装后脚本的复杂 package.xml 2.0。
<?php
/**
* package.xml generation script for Task_Chiara_Managedb package
* @author Gregory Beaver <cellog@php.net>
*/
require_once 'PEAR/PackageFileManager2.php';
PEAR::setErrorHandling(PEAR_ERROR_DIE);
$pfm = &PEAR_PackageFileManager2::importOptions('package.xml',
array(
// set a subdirectory everything is installed into
'baseinstalldir' => 'PEAR/Task/Chiara',
// location of files to package
'packagedirectory' => dirname(__FILE__),
// what method is used to glob files? cvs, svn, perforce
// and file are options
'filelistgenerator' => 'file',
// don't distribute this script
'ignore' => array('package.php', 'package2.xml', 'package.xml'),
// put the post-installation script in a
// different location from the task itself
'installexceptions' =>
array(
'rolesetup.php' => 'Chiara/Task/Managedb',
),
// make the output human-friendly
'simpleoutput' => true,
));
$pfm->setPackage('PEAR_Task_Chiara_Managedb');
$pfm->setChannel('pear.chiaraquartet.net');
$pfm->setLicense('BSD license', 'http://www.opensource.org/licenses/bsd-license.php');
$pfm->setSummary('Provides the <tasks:chiara-managedb/> file task for
managing ' . 'databases on installation');
$pfm->setDescription('Task_Chiara_Managedb provides the code to
implement the <tasks:chiara-managedb/> task, as well as a post- installation script to manage the configuration variables it needs.
This task works in conjunction with the chiaramdb2schema file role
(package PEAR_Installer_Role_Chiaramdb2schema) to create databases
used by a package on installation, and to upgrade the database structure automatically on upgrade. To do this, it uses MDB2_Schema\'s
updateDatabase() functionality.
The post-install script must be run with "pear run-scripts"
to initialize configuration variables');
// initial release version should be 0.1.0
$pfm->addMaintainer('lead', 'cellog', 'Greg Beaver',
'cellog@php.net', 'yes');
$pfm->setAPIVersion('0.1.0');
$pfm->setReleaseVersion('0.1.0');
// our API is reasonably stable, but may need tweaking
$pfm->setAPIStability('beta');
// the code is very new, and may change dramatically
$pfm->setReleaseStability('alpha');
// release notes
$pfm->setNotes('initial release');
// this is a PHP script, not a PECL extension source/binary or a
// bundle package
$pfm->setPackageType('php');
$pfm->addRelease();
// set up special file properties
$pfm->addGlobalReplacement('package-info', '@package_version@',
'version');
$script = &$pfm->initPostinstallScript('rolesetup.php');
// add paramgroups to the post-install script
$script->addParamGroup(
'setup',
$script->getParam('channel', 'Choose a channel to modify
configuration values from',
'string', 'pear.php.net'));
$script->addParamGroup(
'driver',
$script->getParam('driver', 'Database driver?'),
'In order to set up the database, please choose a database
driver. This should be a MDB2-compatible driver name, such as mysql, mysqli, Pgsql, oci8, etc.');
$script->addParamGroup(
'choosedsn',
$script->getParam('dsnchoice', '%sChoose a DSN to modify, or to add a' . ' new dsn, type "new". To remove a DSN prepend with "!"'));
$script->addParamGroup(
'deletedsn',
$script->getParam('confirm', 'Really delete "%s" DSN? (yes to delete)', 'string', 'no'));
$script->addConditionTypeGroup(
'modifydsn',
'choosedsn', 'dsnchoice', 'new', '!=',
array(
$script->getParam('user', 'User name', 'string', 'root'),
$script->getParam('password', 'Database password',
'password'),
$script->getParam('host', 'Database host', 'string',
'localhost'),
$script->getParam('database', 'Database name'),
));
$script->addParamGroup(
'newpackagedsn',
array(
$script->getParam('package', 'Package name'),
$script->getParam('user', 'User name', 'string', 'root'),
$script->getParam('password', 'Database password',
'password'),
$script->getParam('host', 'Database host', 'string',
'localhost'),
$script->getParam('database', 'Database name'),
));
$script->addParamGroup(
'newdefaultdsn',
array(
$script->getParam('user', 'User name', 'string', 'root'),
$script->getParam('password', 'Database password',
'password'),
$script->getParam('host', 'Database host', 'string',
'localhost'),
$script->getParam('database', 'Database name'),
));
$pfm->addPostinstallTask($script, 'rolesetup.php');
// start over with dependencies
$pfm->clearDeps();
$pfm->setPhpDep('4.2.0');
// we use post-install script features fixed in PEAR 1.4.3
$pfm->setPearinstallerDep('1.4.3');
$pfm->addPackageDepWithChannel('required', 'PEAR', 'pear.php.net',
'1.4.3');
$pfm->addPackageDepWithChannel('required', 'MDB2_Schema',
'pear.php.net', '0.3.0');
$pfm->addPackageDepWithChannel('required',
'PEAR_Installer_Role_Chiaramdb2schmea',
'pear.chiaraquartet.net', '0.1.0');
// create the <contents> tag
$pfm->generateContents();
// create package.xml 1.0 to gracefully tell PEAR 1.3.x users they have
// to upgrade to use this package
$pfm1 = $pfm->exportCompatiblePackageFile1(array(
// set a subdirectory everything is installed into
'baseinstalldir' => 'PEAR/Task/Chiara',
// location of files to package
'packagedirectory' => dirname(__FILE__),
// what method is used to glob files? cvs, svn, perforce
// and file are options
'filelistgenerator' => 'file',
// don't distribute this script
'ignore' => array('package.php', 'package.xml', 'package2.xml', 'rolesetup.php'),
// put the post-installation script in a
// different location from the task itself
// make the output human-friendly
'simpleoutput' => true,
));
// display the package.xml by default to allow "debugging" by eye,
// and then create it if explicitly asked to
if (isset($_GET['make']) || (isset($_SERVER['argv']) &&
@$_SERVER['argv'][1] == 'make')) {
$pfm1->writePackageFile();
$pfm->writePackageFile();
} else {
$pfm1->debugPackageFile();
$pfm->debugPackageFile();
}
?>
它生成的 package.xml 如下:
<?xml version="1.0" encoding="UTF-8"?>
<package packagerversion="1.4.3" version="2.0"
xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>PEAR_Task_Chiara_Managedb</name>
<channel>pear.chiaraquartet.net</channel>
<summary>Provides the <tasks:chiara-managedb/> file task for
managing databases on installation</summary>
<description>Task_Chiara_Managedb provides the code to implement the
<tasks:chiara-managedb/> task, as well as a post-installation
script to manage the configuration variables it needs.
This task works in conjunction with the chiaramdb2schema file role
(package PEAR_Installer_Role_Chiaramdb2schema) to create databases
used by a package on installation, and to upgrade the database structure automatically on upgrade. To do this, it uses MDB2_Schema's
updateDatabase() functionality.
The post-install script must be run with "pear run-scripts"
to initialize configuration variables</description>
<lead>
<name>Greg Beaver</name>
<user>cellog</user>
<email>cellog@php.net</email>
<active>yes</active>
</lead>
<date>2005-10-18</date>
<time>23:55:29</time>
<version>
<release>0.1.0</release>
<api>0.1.0</api>
</version>
<stability>
<release>alpha</release>
<api>beta</api>
</stability>
<license uri="http://www.opensource.org/licenses/bsd-
license.php">BSD license</license>
<notes>initial release</notes>
<contents>
<dir baseinstalldir="PEAR/Task/Chiara" name="/">
<dir name="Managedb">
<file name="rw.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
</dir> <!-- //Managedb -->
<file name="Managedb.php" role="php">
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
<file name="rolesetup.php" role="php">
<tasks:postinstallscript>
<tasks:paramgroup>
<tasks:id>setup</tasks:id>
<tasks:param>
<tasks:name>channel</tasks:name>
<tasks:prompt>Choose a channel to modify configuration values
from</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>pear.php.net</tasks:default>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>driver</tasks:id>
<tasks:instructions>In order to set up the database, please choose a database driver.
This should be a MDB2-compatible driver name, such as mysql, mysqli, Pgsql, oci8, etc. </tasks:instructions>
<tasks:param>
<tasks:name>driver</tasks:name>
<tasks:prompt>Database driver?</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>choosedsn</tasks:id>
<tasks:param>
<tasks:name>dsnchoice</tasks:name>
<tasks:prompt>%sChoose a DSN to modify, or to add a new dsn, type "new". To remove a DSN prepend with "!" </tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>deletedsn</tasks:id>
<tasks:param>
<tasks:name>confirm</tasks:name>
<tasks:prompt>Really delete "%s" DSN? (yes to delete)</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>no</tasks:default>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>modifydsn</tasks:id>
<tasks:name>choosedsn::dsnchoice</tasks:name>
<tasks:conditiontype>!=</tasks:conditiontype>
<tasks:value>new</tasks:value>
<tasks:param>
<tasks:name>user</tasks:name>
<tasks:prompt>User name</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>root</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>password</tasks:name>
<tasks:prompt>Database password</tasks:prompt>
<tasks:type>password</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>host</tasks:name>
<tasks:prompt>Database host</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>localhost</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>database</tasks:name>
<tasks:prompt>Database name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>newpackagedsn</tasks:id>
<tasks:param>
<tasks:name>package</tasks:name>
<tasks:prompt>Package name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>user</tasks:name>
<tasks:prompt>User name</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>root</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>password</tasks:name>
<tasks:prompt>Database password</tasks:prompt>
<tasks:type>password</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>host</tasks:name>
<tasks:prompt>Database host</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>localhost</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>database</tasks:name>
<tasks:prompt>Database name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
<tasks:paramgroup>
<tasks:id>newdefaultdsn</tasks:id>
<tasks:param>
<tasks:name>user</tasks:name>
<tasks:prompt>User name</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>root</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>password</tasks:name>
<tasks:prompt>Database password</tasks:prompt>
<tasks:type>password</tasks:type>
</tasks:param>
<tasks:param>
<tasks:name>host</tasks:name>
<tasks:prompt>Database host</tasks:prompt>
<tasks:type>string</tasks:type>
<tasks:default>localhost</tasks:default>
</tasks:param>
<tasks:param>
<tasks:name>database</tasks:name>
<tasks:prompt>Database name</tasks:prompt>
<tasks:type>string</tasks:type>
</tasks:param>
</tasks:paramgroup>
</tasks:postinstallscript>
<tasks:replace from="@package_version@" to="version"
type="package-info" />
</file>
</dir> <!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>4.2.0</min>
</php>
<pearinstaller>
<min>1.4.3</min>
</pearinstaller>
<package>
<name>PEAR</name>
<channel>pear.php.net</channel>
<min>1.4.3</min>
</package>
<package>
<name>MDB2_Schema</name>
<channel>pear.php.net</channel>
<min>0.3.0</min>
</package>
<package>
<name>PEAR_Installer_Role_Chiaramdb2schema</name>
<channel>pear.chiaraquartet.net</channel>
<min>0.1.0</min>
</package>
</required>
</dependencies>
<phprelease />
<changelog>
<release>
<version>
<release>0.1.0</release>
<api>0.1.0</api>
</version>
<stability>
<release>alpha</release>
<api>beta</api>
</stability>
<date>2005-10-18</date>
<license>BSD license</license>
<notes>initial release</notes>
</release>
</changelog>
</package>
PEAR_PackageFileManager 如何让艰难的生活变得容易
聪明的读者可能已经注意到,package.xml 生成脚本相当广泛且长。好消息是,在许多情况下,这将是多余的。事实上,package.xml 的初始生成通常不是 PEAR_PackageFileManager 完成的最重要的功能。更重要的要数是发布数据的维护。维护 package.xml 文件,在许多情况下 package.xml 和 package2.xml 是一个严重的问题。尽管 PEAR 安装程序通过在包文件之间进行仔细的等价性比较来使其变得容易一些,但这个过程并不完美。
PEAR_PackageFileManager 使用相同的数据,并使用显式逻辑来生成 package.xml 的元数据,从而保证创建的 package.xml 文件是等效的。此外,数据的集中化意味着您只需在更新发布说明时修改脚本。此外,由于 PEAR 内置的 package.xml 验证用于验证生成的 package.xml 文件——与打包和安装时使用的相同验证,因此不可能生成无效的 package.xml。
为 package.xml 查找文件
PEAR_PackageFileManager 执行的最重要功能之一是创建文件列表。我们的简单包中只有几个文件,但对于像 PhpDocumentor(pear.php.net/PhpDocumentor)这样的大型、复杂包来说,管理 package.xml 就变成了一项越来越困难的任务。PhpDocumentor 不仅包含了几百个文件,而且由于使用了 Smarty 模板,它们在版本之间也往往会发生巨大的变化。
通过关闭 simpleoutput 选项,可以轻松检测到已修改的文件,并从版本到版本进行监控,而无需依赖于外部工具。
小贴士
为什么会出现 PEAR_PackageFileManager?
最初,PEAR_PackageFileManager 是一个用于生成 PhpDocumentor 的 package.xml 的单一脚本。随着时间的推移,随着对该脚本的请求越来越多,它得到了改进,最终变得明确,它应该成为一个独立的项目。
最初,PEAR_PackageFileManager 简单地使用 shell 通配符的 'ignore' 选项来排除文件,遍历当前文件列表中的所有文件。例如,我们可以使用这个通配符 "*test*" 来忽略所有名称中包含 "test" 的文件。此外,通过在模式后附加一个 "/",可以忽略整个目录及其所有内容,包括子目录,例如 "CVS/"。
上述示例突出了这种方法的其中一个问题:在一个基于 CVS 的包中,可能存在不属于项目的文件在包内部,并且不会随着 cvs export 命令一起导出。因此,PEAR_PackageFileManager 有几个文件遍历驱动程序,或文件列表生成器。选择使用哪个文件列表生成器驱动程序由 setOptions()/importOptions() 方法族中的 'filelistgenerator' 选项控制。最简单的是 file 生成器。
其他驱动程序有 'cvs', 'svn', 和 'perforce'。这些驱动程序与 'file' 驱动程序相同,只是它们不是简单地遍历目录及其所有子目录中的每个文件,而是将文件列表限制为远程版本控制源代码库的本地签出中的文件。并发版本控制系统(CVS)、Subversion 和 Perforce 都是版本控制源代码库系统。如果你不知道它们是什么,调查 Subversion 和 CVS 会是一个好主意,因为它们都是免费的开源解决方案。Subversion 比 CVS 功能更全面,而且更新,而 CVS 是一个经过考验的可靠系统。
管理变更日志
PEAR_PackageFileManager 默认会自动为当前发布版本生成一个变更日志,从旧到新。有一些选项可以控制这一点。首先,如果'changelogoldtonew'选项设置为 false,将重新排序变更日志,使较新的条目更靠近文件顶部。此外,如果变更日志要使用不同于发布说明的笔记集,请使用'changelognotes'选项来控制这一点。
同步 package.xml 版本 1.0 和 package.xml 版本 2.0
在某些情况下,可能需要生成等效的package.xml版本 1.0。例如,我们可能希望允许 PEAR 1.3.x 用户在出现“requires PEAR 1.4.3 or newer”错误消息时优雅地失败。使用 PEAR_PackageFileManager 来做这件事非常简单。将importOptions行从:
$pfm = &PEAR_PackageFileManager2::importOptions('package.xml',
更改为:
$pfm = &PEAR_PackageFileManager2::importOptions('package2.xml',
然后,将脚本的最后几行更改为:
// create the <contents> tag
$pfm->generateContents();
// create package.xml 1.0 to gracefully tell PEAR 1.3.x users they
// have to upgrade to use this package
$pfm1 = $pfm->exportCompatiblePackageFile1(array(
// set a subdirectory everything is installed into
'baseinstalldir' => 'PEAR/Task/Chiara',
// location of files to package
'packagedirectory' => dirname(__FILE__),
// what method is used to glob files? cvs, svn, perforce
// and file are options
'filelistgenerator' => 'file',
// don't distribute this script
'ignore' => array('package.php', 'package.xml',
'package2.xml', 'rolesetup.php'),
// put the post-installation script in a
// different location from the task itself
// make the output human-friendly
'simpleoutput' => true,
));
// display the package.xml by default to allow "debugging" by eye,
// and then create it if explicitly asked to
if (isset($_GET['make']) || (isset($_SERVER['argv']) &&
@$_SERVER['argv'][1] == 'make')) {
$pfm1->writePackageFile();
$pfm->writePackageFile();
} else {
$pfm1->debugPackageFile();
$pfm->debugPackageFile();
}
?>
然后,脚本将输出package.xml和package2.xml。
使用 PEAR Installer 安装包
处理过程的最后一步是创建一个包。一旦生成了一个package.xml文件,就可以使用它来创建包含包内容的文件。为此,应使用package命令:
$ pear package
此命令应在包含package.xml文件的目录中执行。这将创建一个.tgz文件,例如Package-version.tgz,其中Package是包名,version是发布版本。如果你的包名为Foo且版本为1.2.3,则包命令将创建一个名为Foo-1.2.3.tgz的文件。此文件可以安装为:
$ pear install Foo-1.2.3.tgz
或者也可以上传到频道服务器进行公开发布(见第五章)。
包命令还可以使用--uncompress或-Z选项创建一个未压缩的.tar文件:
$ pear package Z
在某些情况下,你可能已经重命名了包文件。在这种情况下,有必要明确指定用于打包的package.xml,如下所示:
$ pear package package-PEAR.xml package2.xml
这是创建用于发布的 PEAR 包的实际命令行。请注意,传入哪个package.xml(版本 1.0 或版本 2.0)无关紧要,以下命令行序列是相同的。
$ pear package package2.xml package-PEAR.xml
然而,如果两个package.xml版本相同,打包将失败。此外,两个package.xml文件之间会进行严格的比较。如果<description>标签、<summary>标签或<notes>标签的文本之间有任何细微的差异,验证将失败。实际上,package.xml 1.0中的每个文件都必须包含在package.xml 2.0中。维护者的数量和他们的角色必须完全相同。
然而,允许存在一些差异。例如,package.xml 1.0 的依赖关系无需与 package.xml 2.0 的依赖关系相匹配,这是因为 package.xml 2.0 简单地代表了一个比 package.xml 1.0 更广泛的可能依赖集。此外,package.xml 2.0 中引入的 <ignore> 标签使得能够分发被 PEAR 安装程序忽略的文件。通过这种方式,一个即插即用的应用程序也可以通过分发用于即插即用运行的文件,并要求 PEAR 安装程序忽略它们,而轻松地使用 PEAR 进行安装。这些文件将不会出现在 package.xml 1.0 中,因为 PEAR 1.3.x 没有这个功能。
摘要
在这个阶段,我们已经深入探讨了 PEAR 安装程序和 package.xml 的内部工作原理——可以肯定地说,你现在已经成为了一个 package.xml 专家。
第四章. 使用 PEAR 安装程序进行巧妙的网站协调
在最后两章中,我们学习了如何使用 PEAR 安装程序的功能来管理公共分发用的库和应用程序。在本章中,我们将学习如何使用 PEAR 安装程序使管理复杂且快速发展的网站内容变得容易。实际上,PEAR 安装程序可以用来提供额外的保险,确保网站按预期运行,甚至使诊断问题更容易。
在本章中,我们将首先从高层次了解我们问题的细节,并了解 PEAR 安装程序如何帮助我们解决问题。然后我们将查看解决方案的第一步,设置源代码控制系统,最后我们将以一个实际网站为例,完成多段网站复杂性的管理。
问题概述
最重要的任务之一是保持复杂且动态网站的结构在信息表示演变时保持一致和最新。在许多情况下,重新组织可能会导致不再使用的文件充斥着目录布局。更糟糕的是,在删除未使用的文件时,可能会不小心删除一个重要的文件而未意识到这一点。此外,协调多开发者模块化网站显然是一个挑战:如何防止更新和添加时的冲突?
与如 CVS(并发版本系统)之类的版本控制系统结合使用时,PEAR 安装程序提供了一个独特且经过实战检验的解决方案,以有效地管理所有这些问题。版本控制系统提供了一种冗余和灵活性的组合,这是简单的文件系统无法比拟的。能够在不担心破坏主代码库的情况下检查出一个个人沙盒进行开发的能力是任何严肃代码开发的一个基本部分。
传统上,分支(CVS 和 Subversion)和标签(仅限 CVS)用于记录“发布点”,即程序已准备好使用。例如,可以通过以下命令直接从 cvs.php.net 获取 PEAR 版本 1.4.5:
$ cvs -d :pserver:anoncvs@cvs.php.net:/respository login
Password:<enter your email address>
$ cvs -d :pserver:anoncvs@cvs.php.net:/respository co r RELEASE_1_4_5 pear-core
此序列检查 PEAR 安装程序的源代码,该源代码位于 /repository/pear-core 中的 cvs.php.net,然后检索在 PEAR 版本 1.4.5 发布时设置的标签 RELEASE_1_4_5,这是通过方便的 cvstag 命令(本章后面将详细介绍)完成的:
$ pear cvstag package2.xml package-PEAR.xml tests
上述示例是最复杂的可能命令。在大多数情况下,开发者可以使用以下命令简单地标记一个包:
$ pear cvstag package.xml
当然,PEAR 安装程序和版本控制系统只与它们背后的协调计划一样有效,因此制定一个结合这两个工具开发网站的良好策略是等式中的一个基本部分,并在本章的最后一段中进行了介绍。
使用路线图或开发时间表,描述新功能将何时添加以及旧功能将何时移除,这是一个很好的第一步。定义开发者应该如何协调和同步他们的努力,这也是另一个方面。使用设计工具和策略,如 UML 和极限编程(测试驱动开发及其相关技术)也可能有用,但最终,网站架构师设计目标中的思维清晰度通常更为重要,并且将导致最佳解决方案,无论选择哪种工具来实现。
小贴士
我们将解决的主要问题是如何协调一个复杂的网站,具体来说,如何从开发机器安全且系统地更新实时网站。
理解问题
要理解问题的解决方案,从高层次理解问题是有意义的。在我们的案例中,理解围绕协调主要网站开发的主要问题非常重要。一个好的网站将吸引用户尽可能频繁地访问,并发展成一个社区。这只有在有令人兴奋的内容和令人愉悦的视觉和逻辑布局的情况下才会发生。通常,内容可以更新而不需要更改任何代码(想想博客或内容管理系统),但更改网站的视觉布局和逻辑结构需要更广泛的网站内部更改。
即使你在第一次尝试时就设计出了完美的网站(恭喜!)并且有简单的方法来调整网站的内容甚至逻辑结构,这也可能导致从小型网站过渡到每分钟有成千上万的访问量的巨大成功网站的最大挑战:可扩展性。通常在这个阶段,可能需要进行全面的重设计来满足意外的需求。成功不可避免地会导致需要聘请新的开发者,他们可能不熟悉网站结构或设计目标。不断重构以改进事物的威胁也可能给甚至最善意的网络团队带来意外的混乱。
所有这些不确定性都将导致代码损坏的可能性更大,目录结构混乱,以及其他问题。
我们将探讨四个典型的问题领域,并更好地了解 PEAR 安装程序将如何帮助我们:
-
管理代码损坏和回滚到先前版本
-
管理缺失或多余的文件
-
与一组开发者协调开发
-
备份代码:冗余作为必要的预防措施
管理代码损坏和回滚到先前版本
有时,尽管经过精心设计和更加仔细的测试,但最近的一次更新中仍然可能破坏了网站的关键部分。更糟糕的是,安全漏洞可能导致恶意黑客破坏精心构建的网站结构。
在过去,这可能意味着希望从备份中恢复能够解决问题。在某些情况下,一个漏洞可能长时间未被注意到,需要从早期的备份中恢复。这可能导致在确定要传输和删除的正确文件时遇到极大的困难。
PEAR 安装程序以极其高效的方式管理这些问题。无需花费狂热的时间手动检查每个目录,只需两个命令就足以完全删除并恢复最新的网站结构:
$ rm -rf /path/to/htdocs
$ pear upgrade --force WebSite-1.2.3.tgz
与“一切是否都正常?哦,我需要恢复那个文件并删除这个文件”的慌乱相比,这种复杂性之间的差异是惊人的。
小贴士
PEAR 安装程序或 rsync?
经验丰富的网络开发者也应该了解 rsync 命令,它促进了相隔甚远的机器上目录的远程同步。在许多情况下,这是一种确保本地和远程存储库同步的非常有效的方法。然而,如果您在协调几个开发者之间,或者开发网站的某些部分而其他部分稳定时,它可能比便利性造成更多困难。在这种情况下,您将更多地受益于 PEAR 安装程序提供的版本控制和简单回滚的优势。
管理缺失或多余的文件
理论上,如果你在上传之前在一个开发机器上完全测试了网站,那么缺失或额外文件的问题永远不会发生,但有时在 rsync 传输中发生错误,删除了不应该删除的文件。在最佳情况下,这将导致网站立即损坏,并且可以轻松追踪并修复。然而,在最坏的情况下,损坏可能很微妙,实际上可能直到你的网站的一个最终用户执行了一个罕见但关键的任务,或者黑客发现了一个未使用的文件中的安全漏洞时才变得明显。这可能导致吓跑用户,商业网站的利润损失,甚至如果您的网站被黑客用来犯罪,并且可以证明您的疏忽,还可能导致法律问题。
当使用 PEAR 安装程序管理网站时,这些问题有何不同?PEAR 安装程序有两个特性使其与传统解决方案区别开来:
-
版本控制
-
文件事务
通过版本控制的概念,可以通过检查 package.xml 的内容或运行类似 list-files 的命令来确定哪些文件存在:
$ pear list-files mychannel/PackageName
使用这个系统可以轻松检测到缺失或多余的文件,无需进行缓慢的递归检查实际目录。此外,能够快速回滚到早期版本,即使暂时如此,然后恢复更新的修复版本,这也非常简单,如下所示示例命令序列:
$ pear upgrade --force mychannel/PackageName-1.0.2
<after fixing things>
$ pear upgrade mychannel/PackageName
PEAR 安装程序利用关系数据库中的一个概念,并实现了基于事务的文件安装和删除。这意味着只有在所有操作都成功完成后,目录结构才会发生变化。此外,尽可能进行原子文件操作。文件的生命周期相当简单,包括以下步骤:
-
文件
path/to/foo.php被安装为/path/to/pear/path/to/.tmpfoo.php -
如果存在,文件
/path/to/pear/path/to/foo.php将被重命名为/path/to/pear/path/to/foo.php.bak。 -
文件
/path/to/pear/path/to/.tmpfoo.php被重命名为/path/to/pear/path/to/foo.php。
安装完成后,所有创建的.bak文件都会被删除。
然而,如果在任何步骤中出现问题,已安装的文件将被删除,.bak文件将被重命名为原始文件名。这样,就可以非常安全地管理文件升级。还会执行额外的检查,以确保安装程序能够写入安装目录,并且文件确实按照预期安装。所有这些额外的工作有助于保证安装的成功。
与开发者团队协调开发
PEAR 安装程序以两种方式帮助协调一组网络开发者:
-
离散打包
-
文件冲突解决
离散打包简单来说,就是每个开发者或子团队的文件集可以占用自己的包,并且可以独立于其他包进行安装/升级。此外,它们都可以通过使用依赖关系从中央包进行管理。
文件冲突解决解决了意外覆盖其他团队文件的可能性,并使得安全共享目录空间成为可能。PEAR 安装程序不允许不同包之间冲突任何其他包的文件。这个简单的事实将为你的团队文件命名约定增加一个额外的错误检查层。
即使指定了--force选项,也会使用文件冲突解决机制。只有危险的--ignore-errors选项可以覆盖文件冲突检查。
代码备份:作为必要预防措施的冗余
使用 PEAR 安装程序管理网站提供了一种可能不那么明显的利益,即通过复制代码。通常人们只从保留整个站点的副本的角度考虑从网站备份代码的需求。这很重要,但可能还不够。如果只有少数文件损坏,使用 PEAR 安装程序快速恢复网站损坏部分的能力仍然相当简单;只需运行:
$ pear upgrade --force mychannel/MyPackage
此外,将网站代码作为打包存档存储在频道服务器上,在源控制和传统完整备份方法提供的冗余之上提供了额外的冗余级别。
既然我们已经知道了问题区域以及 PEAR 安装程序如何帮助我们处理这些问题,让我们详细探讨解决方案。
解决方案,第一部分:至关重要的源代码控制
在使用 PEAR 安装程序之前,设置一个源代码控制系统非常重要。有许多优秀的商业软件程序可以用于执行源代码控制,包括Perforce和Visual SourceSafe,但我们将关注经过验证的、免费的开放源代码版本控制系统:CVS和Subversion。
CVS(并发版本控制系统)是最早的源代码控制产品之一,基于更早的RCS(版本控制系统)源代码控制程序。CVS 通过使用客户端-服务器模型来实现其源代码控制。服务器包含最终代码,组织成目录和文件。然而,在服务器上,每个文件实际上都包含该文件的完整版本历史。在客户端,用户检出本地沙盒——服务器代码的副本,然后可以独立于其他开发者进行开发。当代码准备好提交到服务器时,用户向服务器发送一个特殊命令。那时,服务器会检查是否有其他用户更改了仓库,如果有,将防止可能发生的冲突。用户提交之间的冲突解决完全支持,以及合并兼容的更改。
虽然 CVS 做得非常好,但也有一些限制促使 Subversion 开发团队开始开发新的模型。与 CVS 一样,Subversion 提供了相同的协作工具。区别在于 Subversion 存储信息的方式。使用伯克利数据库文件来记录更改和版本(或在最新版本中,基于文件的 FSFS 数据库),Subversion 能够跟踪文件组和目录以及单个文件的变化,这是 CVS 难以做到的。此外,Subversion 在客户端沙盒中存储服务器代码的完整副本,这使得在执行检查更改和制作补丁等操作时,能够非常有效地使用带宽。
提供冗余和版本历史
源代码控制系统的主要好处是冗余和版本历史的结合。源代码控制系统是围绕人类易犯错误的原则设计的:我们有时会犯错误,能够尽可能容易地从这些错误中恢复过来非常重要。通过将开发者的沙盒与服务器仓库分离提供的冗余,可以快速从开发者的机器上的错误中恢复过来。版本历史的存在,以及 CVS/Subversion 从特定时间点、特定标签或分支检查代码的复杂能力,意味着也可以简单地回滚错误的提交或更改到服务器仓库。
安装 CVS 或 Subversion
在大多数情况下,设置版本控制系统是容易的。本文不是安装的最佳资源,但涵盖了基础知识。对于扩展支持,最好是直接咨询 CVS 或 Subversion 的支持资源。
在接下来的几节中,你将学习如何在 CVS 或 Subversion 中初始化仓库,以及如何在仓库内创建新项目,以及如何创建用于开发的本地沙盒。设置版本控制系统有一些先决条件。重要的是你能够访问你打算设置版本控制系统的主机上的 shell。如果你没有文件系统访问权限,修复仓库中的任何问题将会极其困难。
如果你没有远程主机的 shell 访问权限,并且没有资源切换到提供 shell 访问的互联网服务提供商,事情还没有完全失去希望。
在基于 Windows 的系统上,使用免费软件 TortoiseCVS 或 TortoiseSVN 程序设置本地仓库非常容易,而在像 Mac OS X 或 Linux 这样的基于 Unix 的系统上,你可以直接编译并使用 CVS 和 Subversion 工具来初始化仓库。这种方法唯一的缺点是,你失去了一些远程仓库的故障安全优势。
在任何情况下,无论是远程仓库还是本地仓库,你都需要定期备份系统,以避免在硬件故障或其他墨菲定律的令人不快的事实发生时的麻烦。
小贴士
以下是墨菲定律:
“如果有可能出错,它就会出错。”
总是计划可能出现的问题——硬件故障、数据损坏和安全漏洞只是困扰我们工作的诸多问题中的开始。
并发版本控制系统
并发版本控制系统(CVS)托管在www.nongnu.org/cvs/,是版本控制的最古老形式。CVS 非常稳定,已经稳定了多年。因此,它是版本控制的老将,经受了时间的考验。CVS 基于一个非常简单的基于文件的版本控制。在仓库中,每个文件都包含内容和包含修订之间差异的元数据。仓库不应被用于直接访问或工作。相反,为了开发,需要检出目录的完整副本。除非你签入或提交代码,否则不会将任何更改保存到主仓库中。
CVS 旨在协调多个开发者的工作,因此具有拒绝提交的能力,如果两个不同开发者的工作可能存在冲突。让我们看看一个例子。
PHP 仓库由cvs.php.net托管了几个模块,它们像文件系统一样组织。要检出模块,你必须首先登录到 CVS 服务器。有账户的用户将使用他们的账户名,但 PHP 还提供了匿名只读 CVS 访问。要登录到 CVS 服务器,输入以下命令:
$ cvs -d :pserver:cvsread@cvs.php.net:/repository login
到目前为止,cvs 命令将提示输入密码。对于匿名 CVS 访问,请输入您的电子邮件地址。一旦您登录,请检出模块。例如,要查看 PEAR_PackageFileManager 包的源代码,您将输入:
$ cvs -d :pserver:cvsread@cvs.php.net:/repository checkout pear/ PEAR_PackageFileManager
此命令创建 pear 目录和子目录 PEAR_PackageFileManager,并将项目中的所有文件和目录填充到其中。此外,它还创建了名为 CVS 的特殊目录,其中包含有关仓库本地副本状态的详细信息。每个目录必须包含名为 Entries, Root 和 Repository 的文件。根据仓库的状态,还可能有其他文件。这些文件在极端情况下不应手动修改,但了解控制您的 CVS 检出的是什么非常重要。
关于使用 CVS 的更多信息和技术支持,最好从阅读 man cvs 的 Unix 手册页 开始,并阅读由 O'Reilly 出版的 CVS 书籍,该书籍在 cvsbook.red-bean.com/ 上以 GNU 通用公共许可证在线分发。
在 Windows 上,使用像 TortoiseCVS 这样的工具可能最简单,它可以从 www.tortoisecvs.org 获取。这个免费工具为 Windows 资源管理器添加了一个扩展,允许通过鼠标右键单击文件或目录来直接操作 CVS 检出和仓库。它非常直观且功能强大。
设置 CVS 仓库
设置 CVS 仓库的第一步是确定您打算将仓库放在哪里。CVS 有几种远程连接到仓库的方法。在大多数情况下,最好要求通过安全外壳(SSH)通过 ext 方法访问,如下所示:
$ cvs -d :ext:cellog@cvs.phpdoc.org:/opt/cvsroot
进一步解析此命令行,-d 选项告诉 CVS 在哪里定位 CVS 的根目录,或 CVSROOT。在这种情况下,它告诉 CVS 通过 pserver 协议连接到远程 CVSROOT cvs.phpdoc.org,使用 cellog 用户名。此外,它通知远程 CVS 守护进程 CVSROOT 位于 /opt/cvsroot。在 cvs.php.net 上,CVSROOT 位于 /repository。
如果您在共享主机上,假设您的远程用户名为 youruser,最好将 cvsroot 放在 /home/youruser/cvs 或类似的位置。否则,您可能无法访问初始化 CVS 仓库的目录。显然,写入访问非常重要;否则,从开发沙盒中提交代码是没有办法的。
一旦您决定将 CVS 仓库放在哪里,下一步就是初始化它。这是直截了当的:
$ cvs -d /home/youruser/cvs init
这将创建 /home/youruser/cvs/CVSROOT 目录。
下一步是创建您将用于网站的模块。在导入网站之前,首先创建网站的模块。
$ mkdir /home/youruser/website
$ cd /home/youruser/website
$ echo "hi" >> README
$ cvs -d /home/youruser/cvs imoort website tcvs-vendor tcvs-release
这将创建一个名为website的模块,可以检出。为了确保成功,检出website模块的一个副本:
$ cd
$ cvs -d /home/youruser/cvs checkout website
如果一切顺利,这将导致在/home/youruser/website目录和/home/youruser/website/README文件中创建目录。为了测试 CVS 是否可以从远程访问,可以通过类似以下方式检出website模块的副本:
$ cvs -d :ext:youruser@example.com:/home/youruser/cvs checkout website
下一步是将网站内容添加到 CVS 仓库中。只需将所需目录层次结构的所有网站文件复制到本地website模块的检出中即可。下一步是最复杂的。
大多数网站都包含文本文件(如我们的 PHP 脚本)和二进制文件,如图像或声音剪辑。CVS 将二进制文件和文本文件处理方式不同。文本文件会被处理,并且像$Id$或$Revision$这样的特殊 CVS 内文件标签会被根据文件在仓库中的状态替换为特殊值。像"\(Id\)"这样的标签必须由开发者手动添加到文件中,CVS 不会自动创建它们。二进制文件被视为一个单一实体,其内容不会被修改。
当向 CVS 模块添加文件时,它们必须首先通过cvs add命令添加,然后通过cvs commit命令提交。文本文件和目录只需这样添加:
$ cvs add file
使用-kb开关添加二进制文件:
$ cvs add -kb file
可以使用通配符添加文件,但务必非常小心,确保不要将图像文件作为文本文件添加,或将文本文件作为二进制文件添加!在最坏的情况下,可以在提交之前使用以下命令删除文件:
$ cvs remove file
注意,在删除之前必须先删除已提交到仓库的文件:
$ rm file
$ cvs remove file
如果你使用的是 Windows,TortoiseCVS 可以使添加文件变得容易得多,因为它会递归地这样做,并隐藏实现细节。
Subversion
Subversion 是在几年前开发的,用以解决 CVS 的一些不足。具体来说,Subversion 使用数据库存储仓库信息,因此支持按提交而不是按文件分组更改。Subversion 较新,因此没有像 CVS 那样经过长时间的实战检验,但两者都已经用于生产多年。
小贴士
svnbook.red-bean.com 包含了由 O'Reilly 出版的同一本书的几种不同格式。Subversion 书籍包含了设置、配置和管理 Subversion 仓库所需的一切。
Subversion 在几个重要方面与 CVS 不同:
-
远程仓库的完整副本存储在本地,简化了差异比较,并使得离线操作成为可能。
-
标签存储为分支,与 CVS 不同。在 CVS 中,标签是只读的,很难意外修改标签。修改分支相当简单,由于在 Subversion 中标签是分支,这使得实现只读标签更加困难。
-
大文件更容易管理。因为沙盒包含仓库模块当前状态的完整副本,这意味着提交大文本文件只需发送 diff。最终,这可以在服务器上节省带宽和处理器周期,这非常重要。(我曾经通过在 CVS 仓库中提交一个 133MB 数据库转储的微小更改而锁定整个实时服务器,需要重启。这很糟糕。)
-
关键字(
$Id$、$Revision$等)默认不替换;所有 Subversion 中的文件都被视为二进制文件。要为文件设置关键字替换,您需要设置一个属性,例如:$ svn propset svn:keywords "Id" blah.php
更多信息请参阅svnbook.red-bean.com/nightly/en/svn.advanced.props.html#svn.advanced.props.special.keywords
设置 Subversion 仓库
与 CVS 一样,设置仓库相对简单。只需运行:
$ svnadmin create /path/to/subversion
这将在当前目录下创建一个 Subversion 仓库作为子目录。要将您的网站代码导入到仓库中,首先设置标准的 Subversion 目录:
$ mkdir ~/tmp
$ cd ~/tmp
$ mkdir website
$ cd website
$ mkdir trunk
$ mkdir tags
$ mkdir branches
接下来,将您当前网站的完整内容复制到website/trunk目录中。最后执行:
$ cd ~/tmp
$ svn import . file:///path/to/subversion -m "initial import"
成功导入后,通过检出模块进行测试:
$ svn checkout file:///path/to/subversion/website/trunk website
小贴士
警告:检出 website/trunk,而不是 website。
如果您检出整个模块,您将获得所有分支和标签。这最终会消耗所有可用的磁盘空间。
远程访问website模块需要svnserve守护进程正在运行,或者mod_svn作为 Apache web 服务器模块正在运行。如果您没有设置,请参考svnbook.red-bean.com中的 Subversion 书籍以获取设置此环境的详细信息。
如果您正在运行svnserve,可以通过以下方式检出:
$ svn checkout svn://yourwebsite.example.com/path/to/subversion/website/trunk website
或者,如果您支持高度推荐的 SSH 隧道:
$ svn checkout
svn+ssh://yourwebsite.example.com/path/to/subversion/website/ trunk website
相反,如果您有mod_svn运行,检出操作就简单多了:
$ svn checkout http://yourwebsite.example.com/subversion/website/ trunk website
或者,如果您有一个安全的服务器:
$ svn checkout https://yourwebsite.example.com/subversion/website/ trunk website
这些命令将创建一个名为website的目录,其中包含您网站的代码。
在 Subversion 仓库中添加和删除文件非常直接,类似于 CVS。只需使用此格式从website目录中添加文件或目录:
$ svn add file.php
使用此格式来删除文件或目录:
$ svn delete file.php
与 CVS 不同,使用move和copy命令可以移动文件或复制它们,同时保留它们的修订历史:
$ svn copy file.php newfile.php
$ svn move oldfile.php anotherfile.php
如果您希望替换像$Id$或$Revision$这样的关键字,您需要手动告诉 Subversion 执行此替换:
$ svn propset svn:keywords "Id Revision" file.php
这应该足以让您开始使用您选择的仓库。
智能源代码控制
好的,现在你已经设置并配置了版本控制仓库。太好了!接下来是什么?智能地使用版本控制系统是一个非常重要的步骤。应该遵循基本原理以确保这一点发生。尽管许多是常识,但保持警惕并坚持它们并不容易。
-
定期备份你的仓库,并将它们存储在独立于托管仓库的机器上的媒体上。如果你记得其他任何事情,请记住这一点!
-
只将可工作的代码提交到仓库——在提交前进行测试,以避免像语法错误这样的明显错误。
-
使用标签标记将要部署到实时服务器的可工作代码的点发布。
-
使用分支来同时支持创新和稳定的代码库。
-
如果你有多名开发者,定义一些基本的编码标准(如 PEAR 的编码标准),以便修订之间的差异不包含对空白和其他噪声的虚假更改。
-
如果你有多名开发者,设置一个专门用于提交到仓库的邮件列表。有许多优秀的程序可用于在提交后脚本中使用,将差异发送到邮件列表。确保每个开发者都订阅了该邮件列表。
维护分支以支持复杂版本控制
分支允许同时开发同一软件的多个版本。例如,当软件达到稳定性,并将添加主要新功能时,从软件中分出一个副本,以便在开发继续的同时,在稳定版本中修复小错误。最佳实践是将稳定版本分支出来,并在 HEAD 上继续开发。
要在分支中使用 CVS 开发稳定的版本 1.2.X:
$ cvs tag -b VERSION_1_2
$ cvs update -b DEVEL_1_2
update 命令是一个不直观的要求,但非常重要;如果不更新你的源代码,你将不会编辑分支代码,任何更改最终都会出现在 HEAD 上。
有两个独立的目录是个好主意。例如,当开发 PEAR 版本 1.5.0 时,我使用了两个目录,它们创建的方式如下:
$ cvs -d :pserver:cellog@cvs.php.net:/repository co -r PEAR_1_4 d pear1.4 pear-core
$ cvs -d :pserver:cellog@cvs.php.net:/repository co pear-core
因为我在 checkout 命令(缩写为 co)中指定了 -d pear1.4 选项,所以文件将被检出到 pear1.4 目录。-r PEAR_1_4 选项检索 PEAR_1_4 分支以修复 PEAR 版本 1.4.X 中的错误。在第二种情况下,默认 HEAD 分支的文件被检出到 pear-core 目录。
使用 Subversion 执行相同任务的命令类似于:
$ svn checkout http://yourwebsite.example.com/subversion/website/ trunk website
使用标签标记点发布
标签之所以重要,原因很多,其中最重要的是在数据灾难性丢失的情况下能够重建旧版本。使用 CVS 创建标签有两种方法。推荐的方法非常简单:
$ pear cvstag package.xml
此命令解析 package.xml,并为每个找到的文件添加 RELEASE_X_Y_Z 标签,其中 X_Y_Z 是版本号。版本 1.2.3 将被标记为 RELEASE_1_2_3。
从模块检出中手动标记可以通过以下方式完成:
$ cvs tag -r RELEASE_1_2_3
Subversion 不区分标签和分支,因此创建标签和分支之间的唯一区别在于你使用svn复制命令将其复制到何处。默认情况下,标签应该使用svn复制命令复制到 trunk/tags 分支:
$ svn copy trunk tags/RELEASE_1_2_3
$ svn commit -m "tag release version 1.2.3"
解决方案,第二部分:使用 PEAR 安装程序更新网站
到目前为止,你应该熟悉 PEAR 安装程序和源代码控制系统的基本用法。现在我们将把这种知识提升到下一个层次,并了解如何利用这两个工具的优势来管理具有相互依赖关系的多段网站复杂性。首先,重要的是要从离散包和依赖关系的角度考虑网站代码。为此,使用图表通常很有帮助。对于复杂的系统,使用统一建模语言(UML)来描述系统是非常有意义的,因为这是描述的通用标准。
让我们考察一个现实世界的例子:Chiara 弦乐四重奏网站,www.chiaraquartet.net。截至 2006 年初,这是一个由单个开发者设计的中型网站,但原则可以很好地扩展到多开发者的情况。该网站由多个子站点以及主站点组成。
小贴士
在出版前几天,Chiara 弦乐四重奏的网站现在由一个独立开发者管理,不再由作者直接维护,如前所述。作为一个使用package.xml管理的网站的例子,在出版时,pear.php.net正在迁移到这种方法。查看cvs.php.net中的 pearweb 模块,以及package.php脚本和相应的package.xml以及安装后脚本,这些可以用来设置 MySQL 数据库并配置http.conf文件,以开发pear.php.net代码的副本。
公共站点包括:
-
music.chiaraquartet.net(MP3s/音频样本)
私人后端站点包括:
-
addressbook.chiaraquartet.net(联系数据库的数据录入) -
database.chiaraquartet.net(管理通用后端数据)
这些站点各自拥有独立的代码库,但它们在图像和模板等元素上存在相互链接的依赖关系,这些元素用于统一不同站点的外观。此外,随着四重奏事业的成长,网站的需求发生了巨大变化,能够添加新的子站点和删除过时的站点非常重要。
还需要注意的是,需要非常大的文件,例如高分辨率的新闻图片和 MP3 音频剪辑,这些文件不会像 PHP 代码那样频繁更改。
因此,网站可以被分组为几个简单的包:
-
网站
-
网站数据库后端
-
网站联系数据输入后端
-
网站图片
-
网站 MP3 文件
-
网站图片
-
网站 PDF 文件
主要网站包本身可以在稍后日期拆分为单独的包,例如主要网站和网站博客包,而不会受到任何惩罚,这可以通过我们将在本章稍后考察的一些方法实现。
一旦我们确定了网站逻辑分区为包,所需做的就是创建一个私有通道,为每个包生成适当的 package.xml 文件,并安装网站。每个组件都可以独立升级——和降级——这使得维护和跟踪更改远不如一个神奇的仪式。
备份数据库的一个美妙技巧不仅是要保存完整的转储,还要将这些转储提交到 Subversion 仓库。这样,你存储的是更小的版本,并且有在远程机器上为开发和测试目的检出数据库转储的能力。
相比 CVS,Subversion 更受欢迎,因为它处理极大文件的方式更为优雅。CVS 在计算两个 150MB 数据库转储之间的差异时,很容易使整个机器崩溃。我是通过艰难的方式学到这一点的。Subversion 在这方面远胜一筹,因为它使用本地副本来计算差异,因此只有差异会被来回发送,从而指数级地减少网络流量。
小贴士
重要的一点是,如果数据库中存在任何私有数据,应限制访问为 svn+ssh,以减少意外将敏感数据提供给错误人员的可能性。
无需多言,如果存在任何至关重要的敏感数据,例如信用卡号码或身份盗贼渴望获取的数据,绝对不允许对数据进行任何远程访问;相反,应使用像 rdiff-backup(www.nongnu.org/rdiff-backup/)这样的工具。
当移植现有网站时,最好将其添加到 CVS/Subversion 中,保持现有网站的源布局。
开发远程网站的一个困难是需要代码理解它将在不同的 IP 地址上运行,可能还有不同的主机名。有三种处理这个问题的方法:
-
将实时主机名添加到
/etc/hosts文件中,作为本地主机的别名,这样所有请求都会发送到本地主机。 -
使用主机中立的代码,例如依赖于 Apache 网络服务器使用的
$_SERVER['HTTP_HOST']变量,或$_SERVER['PHP_SELF']。 -
使用 PEAR 的替换设施和自定义文件角色来定义每台机器的主机信息。
将实时主机名添加到 /etc/hosts 文件中(在 Microsoft Windows 系统上通常是 C:\WINDOWS\system32\drivers\etc\hosts),将使得从开发机器实际上无法访问实时网络服务器——或 FTP 服务器——成为不可能,因此这根本不是解决方案。
使用主机中立的代码看起来是个好主意,但正如最近的安全担忧所显示的,跨站脚本(XSS)攻击利用了通过使用这些工具创建的漏洞。虽然避免安全问题是容易的,但它确实增加了相当大的复杂性,使得引入另一个错误或安全问题的可能性高于舒适水平。
第三个选项涉及创建自定义 PEAR 安装器文件角色,定义特殊的配置变量,然后与 PEAR 的替换任务耦合,以自动为每台机器定制文件。
具体来说,我们的示例网站 www.chiaraquartet.net,需要在开发机上设置虚拟主机 www.chiaraquartet.net, database.chiaraquartet.net, music.chiaraquartet.net 和 calendar.chiaraquartet.net。真麻烦!相反,我创建了两个自定义包,定义了五个配置变量:
-
root_url:这定义了网站的基础 URL。 -
music_url: 这定义了网站音乐音频部分的基础 URL。 -
calendar_url:这定义了音乐会和活动日程的基础 URL。 -
addressbook_url:这定义了后端联系列表的基础 URL。 -
database_url:这定义了后端数据库的基础 URL。
在 开发 服务器上,我使用以下方式设置这些配置变量的所需值:
$ pear config-set root_url http://localhost
$ pear config-set music_url http://localhost/music
$ pear config-set calendar_url http://localhost/calendar
$ pear config-set addressbook_url http://localhost/addressbook
$ pear config-set database_url http://localhost/database
在 实时 服务器上,我使用以下方式设置配置变量的所需值:
$ pear config-set root_url http://www.chiaraquartet.net
$ pear config-set music_url http://music.chiaraquartet.net
$ pear config-set calendar_url http://calendar.chiaraquartet.net
$ pear config-set addressbook_url http://addressbook.chiaraquartet.net
$ pear config-set database_url http://database.chiaraquartet.net
然后,为了直接在源代码中设置这些信息,我使用如下方式:
$ajaxHelper->serverUrl = '@DATABASE-URL@/rest/rest_server.php';
如果在 package.xml 中指定了这个标签,@DATABASE-URL@ 的值将被替换为 database_url 配置变量的值:
<file name="blah.php" role="php">
<tasks:replace from="@DATABASE-URL@" to="database_url"
type="pear-config" />
</file>
在这项工作完成后,当安装在本地 开发 机器上时,代码将是:
$ajaxHelper->serverUrl =
'http://localhost/database/rest/rest_server.php';
并且在 实时 服务器上,代码将是:
$ajaxHelper->serverUrl = 'http://database.chiaraquartet.net/rest/ rest_server.php';
最好的是,URL 在两台机器上都能保证正确无误,无需额外工作。这个基本原理也可以应用于开发服务器和实时服务器之间任何重要的差异。
标准库包和网站之间的另一个具体区别是,网站应该安装到公开可访问的目录中,而标准库包文件应该安装到不可访问的位置。为此,我们有默认配置变量,如 php_dir, data_dir 和 test_dir。对于网页文件没有默认角色。幸运的是,pearified.com 通道上确实存在一个自定义文件角色包。要获取此包,请按照以下步骤操作:
$ pear channel-discover pearified.com
$ pear install pearified/Role_Web
$ pear run-scripts pearified/Role_Web
然后,要在 package.xml 中使用它,只需简单地写下:
<file name="foo.html" role="web" />
此外,您应该指定对 pearified.com/Role_Web 的必需依赖关系,以及如 第二章 中所述的 <usesrole> 标签。
在这些细节确定之后,是时候生成 PEAR 安装程序所需的 package.xml 文件,以便管理网站的安装。
从源代码检出生成 package.xml
要生成 package.xml,有几种选项可用。对于复杂的网站,最古老和最简单的方法是使用 PEAR_PackageFileManager 包创建一个 package.xml 生成脚本。该脚本应生成所需的每个 package.xml 文件,使其更新变得简单。此外,它应正确忽略无关的文件和子包。
我们真实网站的生成脚本 www.chiaraquartet.net 是使用这种方法维护的:
<?php
require_once 'PEAR/PackageFileManager2.php';
PEAR::setErrorHAndling(PEAR_ERROR_DIE);
下一个部分简单地设置每个子包的 package.xml 的发布说明和版本,位于文件顶部的集中位置,这使得编辑文件更容易。
$imageversion = '0.1.0';
$imagenotes = <<<EOT
initial release
EOT;
$mp3version = '0.1.0';
$mp3notes = <<<EOT
initial release
EOT;
$photoversion = '0.1.0';
$photonotes = <<<EOT
initial release
EOT;
$pressversion = '0.1.0';
$pressnotes = <<<EOT
initial release
EOT;
$dataversion = '0.10.3';
$datanotes = <<<EOT
fix saving multiple program items
EOT;
$version = '0.10.0';
$apiversion = '0.1.0';
$notes = <<<EOT
split off database from main package
EOT;
接下来,我们通过从每个子包现有的 package.xml 中导入来创建每个 package.xml 文件。为了简洁起见,我们将省略一些子包。以下是一个典型的例子(网站图片):
$package_images =
PEAR_PackageFileManager2::importOptions(dirname(__FILE__) .
DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR .
'package.xml',
$options = array(
'ignore' => array('package.xml'),
'filelistgenerator' => 'cvs', // other option is 'file'
'changelogoldtonew' => false,
'baseinstalldir' => 'images',
'packagedirectory' => dirname(__FILE__) . DIRECTORY_SEPARATOR .
'images',
'simpleoutput' => true,
'roles' => array('*' => 'web'),
));
$package_images->setPackageType('php');
$package_images->setReleaseVersion($imageversion);
$package_images->setAPIVersion($imageversion);
$package_images->setReleaseStability('alpha');
$package_images->setAPIStability('alpha');
$package_images->setNotes($imagenotes);
$package_images->clearDeps();
$package_images->resetUsesRole();
$package_images->addUsesRole('web_dir', 'Role_Web', 'pearified.com');
$package_images->addPackageDepWithChannel('required', 'Role_Web',
'pearified.com');
$package_images->setPhpDep('5.1.0');
$package_images->setPearinstallerDep('1.4.3');
$package_images->generateContents();
$package_images->addRelease();
// snip
$package_data =
PEAR_PackageFileManager2::importOptions(dirname(__FILE__) .
DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR .
'package.xml',
$options = array(
'ignore' => array('package.xml'),
'filelistgenerator' => 'cvs', // other option is 'file'
'changelogoldtonew' => false,
'baseinstalldir' => 'database',
'packagedirectory' => dirname(__FILE__) . DIRECTORY_SEPARATOR .
'database',
'simpleoutput' => true,
'roles' => array('*' => 'web'),
));
$package_data->setPackageType('php');
$package_data->setReleaseVersion($dataversion);
$package_data->setAPIVersion($dataversion);
$package_data->setReleaseStability('alpha');
$package_data->setAPIStability('alpha');
$package_data->setNotes($datanotes);
$package_data->clearDeps();
$package_data->resetUsesRole();
$package_data->addPackageDepWithChannel('required', 'HTML_AJAX',
'pear.php.net');
$package_data->addPackageDepWithChannel('required',
'HTML_Javascript', 'pear.php.net');
$package_data->addPackageDepWithChannel('required', 'XML_RPC2',
'pear.php.net');
$package_data->addUsesRole('web_dir', 'Role_Web', 'pearified.com');
$package_data->addUsesRole('root_url', 'Role_Chiara',
'pear.chiaraquartet.net/private');
$package_data->addUsesRole('music_url', 'Role_Chiara',
'pear.chiaraquartet.net/private');
$package_data->addUsesRole('calendar_url', 'Role_Chiara',
'pear.chiaraquartet.net/private');
$package_data->addUsesRole('database_url', 'Role_Chiara2',
'pear.chiaraquartet.net/private');
$package_data->addPackageDepWithChannel('required', 'Role_Web',
'pearified.com');
$package_data->setPhpDep('5.1.0');
$package_data->setPearinstallerDep('1.4.3');
在这里,我们将向所有文件添加替换任务,展示我们针对开发与生产机器所需的定制:
$package_data->addGlobalReplacement('pear-config', '@ROOT-URL@',
'root_url');
$package_data->addGlobalReplacement('pear-config', '@MUSIC-URL@',
'music_url');
$package_data->addGlobalReplacement('pear-config', '@CALENDAR-URL@',
'calendar_url');
$package_data->addGlobalReplacement('pear-config', '@DATABASE-URL@',
'database_url');
$package_data->addGlobalReplacement('pear-config', '@WEB-DIR@',
'web_dir');
$package_data->generateContents();
$package_data->addRelease();
$package_website =
PEAR_PackageFileManager2::importOptions(dirname(__FILE__) .
DIRECTORY_SEPARATOR . 'package.xml',
这下一行至关重要;我们需要忽略所有子包的内容,否则它们将被重复,并与父包冲突!
$options = array(
'ignore' => array('package.php', 'package.xml', '*.bak',
'chiaraqu_Chiara', '*.tgz', 'README', 'Chiara_Role/',
'images/', 'photos/', 'music/mp3/', 'press/', 'database/'),
'filelistgenerator' => 'cvs', // other option is 'file'
'changelogoldtonew' => false,
'baseinstalldir' => '/',
'packagedirectory' => dirname(__FILE__),
'simpleoutput' => true,
'roles' => array('*' => 'web'),
));
$package_website->setPackageType('php');
$package_website->setReleaseVersion($version);
$package_website->setAPIVersion($apiversion);
$package_website->setReleaseStability('alpha');
$package_website->setAPIStability('alpha');
$package_website->setNotes($notes);
$package_website->clearDeps();
$package_website->resetUsesRole();
$package_website->addUsesRole('web_dir', 'Role_Web',
'pearified.com');
$package_website->addUsesRole('root_url', 'Role_Chiara',
'pear.chiaraquartet.net/private');
$package_website->addUsesRole('music_url', 'Role_Chiara',
'pear.chiaraquartet.net/private');
$package_website->addUsesRole('calendar_url', 'Role_Chiara',
'pear.chiaraquartet.net/private');
$package_website->addUsesRole('database_url', 'Role_Chiara2',
'pear.chiaraquartet.net/private');
$package_website->addPackageDepWithChannel('required', 'PEAR',
'pear.php.net', '1.4.3');
$package_website->addPackageDepWithChannel('required', 'Role_Web',
'pearified.com');
$package_website->addPackageDepWithChannel('required', 'Role_Chiara',
'pear.chiaraquartet.net/private');
$package_website->addPackageDepWithChannel('required',
'Role_Chiara2', 'pear.chiaraquartet.net/private');
在这里,我们为每个子包添加依赖项:
$package_website->addPackageDepWithChannel('required',
'website_photos', 'pear.chiaraquartet.net/private');
$package_website->addPackageDepWithChannel('required',
'website_mp3s', 'pear.chiaraquartet.net/private');
$package_website->addPackageDepWithChannel('required',
'website_press', 'pear.chiaraquartet.net/private');
$package_website->addPackageDepWithChannel('required',
'website_images', 'pear.chiaraquartet.net/private');
$package_website->setPhpDep('5.1.0');
$package_website->setPearinstallerDep('1.4.3');
$package_website->addGlobalReplacement('pear-config', '@ROOT-URL@',
'root_url');
$package_website->addGlobalReplacement('pear-config', '@MUSIC-URL@',
'music_url');
$package_website->addGlobalReplacement('pear-config',
'@CALENDAR-URL@', 'calendar_url');
$package_website->addGlobalReplacement('pear-config',
'@DATABASE-URL@', 'database_url');
$package_website->addGlobalReplacement('pear-config', '@WEB-DIR@',
'web_dir');
$package_website->generateContents();
$package_website->addRelease();
最后,我们创建每个 package.xml 文件或显示它们以进行错误检查:
if (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] == 'commit') {
$package_press->writePackageFile();
$package_images->writePackageFile();
$package_mp3s->writePackageFile();
$package_photos->writePackageFile();
$package_website->writePackageFile();
$package_data->writePackageFile();
} else {
$package_press->debugPackageFile();
$package_mp3s->debugPackageFile();
$package_images->debugPackageFile();
$package_photos->debugPackageFile();
$package_website->debugPackageFile();
$package_data->debugPackageFile();
}
?>
每个子包都有自己的 PEAR_PackageFileManager2 对象,并从现有的 package.xml 中导入选项,仅修改必要的部分。为了创建 package.xml,我从 PEAR 包(在这种情况下,是 PEAR 的 package2.xml)中复制了一个现有的文件,并修改了 summary、description、license 部分以及维护者列表,以适应网站包。
要使用此脚本,我将其保存为 package.php,现在使用 PHP 5.1.0 或更高版本运行它,如下所示:
$ php package.php
这允许我查看 package.xml 文件并检查错误。要保存对 package.xml 的更改,我运行:
$ php package.php commit
哇!package.xml 已在 website/、website/database、website/calendar、website/press 和 website/music 中创建。
在这个阶段,我们准备开始发布代码并将其部署到测试服务器。
打包:协调标签和分支的发布版本
达到这个阶段意味着我们准备开始将我们的网站打包成 PEAR 包以进行安装。在这个时候,过程开始与我们在上一章中学到的打包过程合并。最终,这就是为什么使用 PEAR 安装程序打包网站是一个好主意。安装、升级,甚至回滚网站的过程与安装、升级和回滚任何 PEAR 包没有区别。这个过程使得管理变得极其简单。
当从管理网站的老方法转换为 PEAR 方法时,必须采取几个重要的步骤:
-
在本地开发服务器上测试发布版本。
-
如果可能,在部署前立即备份 Live 服务器。
-
在远程服务器上部署 PEAR 包。
一旦网站成功部署,升级远程服务器上的相应包就是一个简单的过程。
当发布一个版本时,在源代码控制系统中使用标签标记该版本非常重要。如果你选择使用 CVS,标记过程很简单:
$ pear cvstag package.xml
这将自动扫描 package.xml 以查找发布中包含的文件,并使用发布版本创建一个名为 RELEASE_X_Y_Z 的标签,并将其应用于所有文件。如果发布版本是 0.10.0,则标签将是 RELEASE_0_10_0;如果发布版本是 1.2.3,则标签将是 RELEASE_1_2_3,依此类推。
使用 Subversion 标记并不是完全自动的,但可以通过以下方式简单完成:
$ svn copy trunk tags/RELEASE_X_Y_Z
$ svn commit -m "tag release X.Y.Z"
按照这些步骤,版本 X.Y.Z 将被标记。
在上传前测试发布版本
当你准备部署网站时,创建一个测试包并在本地安装它以确保一切正常非常重要。打包的输出将类似于以下内容:
$ pear package
Analyzing addressbook/data.php
Analyzing addressbook/form.php
Analyzing addressbook/index.php
Analyzing addressbook/list.php
Analyzing addressbook/login.php
Analyzing addressbook/nameparser.php
Analyzing addressbook/usps.php
Analyzing calendar/chiaraSchedule.php
Analyzing calendar/HTMLController.php
Analyzing calendar/HTMLView.php
Analyzing calendar/index.php
Analyzing calendar/schedulelogin.php
Analyzing css/default.css
Analyzing css/history.css
Analyzing css/jonah.css
Analyzing css/music.css
Analyzing css/newindex.css
Analyzing css/news.css
Analyzing css/schedule.css
Analyzing domLib/Changelog
Analyzing domLib/domLib.js
Analyzing domLib/LICENSE
Analyzing domTT/alphaAPI.js
Analyzing domTT/domLib.js
Analyzing domTT/domTT.js
Analyzing domTT/domTT_drag.js
Analyzing music/index.php
Analyzing rest_/list/composers.php
Analyzing rest_/list/concerts.php
Analyzing rest_/list/halls.php
Analyzing rest_/list/pieces.php
Analyzing templates/bio.tpl
Analyzing templates/genericheader.tpl
Analyzing templates/header.tpl
Analyzing templates/history.tpl
Analyzing templates/index.tpl
Analyzing templates/jonahheader.tpl
Analyzing templates/jonahindex.tpl
Analyzing templates/musicbody.tpl
Analyzing templates/news.tpl
Analyzing templates/schedulebody.tpl
Analyzing bio.php
Analyzing editconcerts.html
Analyzing history.php
Analyzing index.php
Analyzing news.php
Warning: in pieces.php: class "getPieces" not prefixed with package name "website"
Warning: in halls.php: class "getHalls" not prefixed with package name "website"
Warning: in concerts.php: class "getConcerts" not prefixed with package name "website"
Warning: in composers.php: class "getComposers" not prefixed with package name "website"
Warning: in index.php: function "err" not prefixed with package name "website"
Warning: in index.php: class "schedulebody" not prefixed with package name "website"
Warning: in news.php: class "test" not prefixed with package name "website"
Warning: in HTMLView.php: class "HTMLView" not prefixed with package name "website"
Warning: in HTMLController.php: class "HTMLController" not prefixed with package name "website"
Warning: in chiaraSchedule.php: class "chiaraSchedule" not prefixed with package name "website"
Warning: in usps.php: class "Services_USPS" not prefixed with package name "website"
Warning: in nameparser.php: class "Spouses" not prefixed with package name "website"
Warning: in nameparser.php: class "CareOf" not prefixed with package name "website"
Warning: in nameparser.php: class "Name" not prefixed with package name "website"
Warning: in nameparser.php: class "NameParser" not prefixed with package name "website"
Warning: in schedulelogin.php: class "Login" not prefixed with package name "website"
Warning: in index.php: class "Page3Display" not prefixed with package name "website"
Warning: in index.php: class "Page1Display" not prefixed with package name "website"
Warning: in index.php: class "MyDisplay" not prefixed with package name "website"
Warning: in index.php: class "Recycle" not prefixed with package name "website"
Warning: in index.php: class "Cancel" not prefixed with package name "website"
Warning: in index.php: class "FinalPage" not prefixed with package name "website"
Warning: in index.php: class "VerifyPage" not prefixed with package name "website"
Warning: in index.php: class "FirstPage" not prefixed with package name "website"
Warning: in form.php: class "HTML_QuickForm_Action_Next" not prefixed with package name "website"
Warning: in data.php: class "ContactAddress" not prefixed with package name "website"
Warning: in data.php: class "Address" not prefixed with package name "website"
Warning: in data.php: class "Data" not prefixed with package name "website"
Warning: Channel validator warning: field "date" - Release Date "2006-04-08" is not today
Package website-0.10.0.tgz done
Tag the released code with `pear cvstag package.xml'
(or set the CVS tag RELEASE_0_10_0 by hand)
$ pear upgrade website-0.10.0.tgz
upgrade ok: channel://pear.chiaraquartet.net/private/website-0.10.0
小贴士
关于所有那些 "Warning: in index.php..." 的事情怎么办?
这条警告是针对那些正在为官方 PEAR 仓库 pear.php.net 开发的开发者,旨在帮助捕捉到类命名错误的情况。我们可以通过创建一个自定义通道验证器来轻松消除这些警告,正如在第五章(Chapter 5. 发布到世界:PEAR 通道)中讨论的那样,但这并不是必需的,因为我们知道这些警告是多余的(编写软件或阅读这本书的一个优点!)
接下来,浏览每个页面并点击链接(或者如果你有一个测试套件,运行它)以确保其正常工作。一旦你确信它已经准备好并且可以正常工作,那么就是时候升级 Live 服务器了。
升级 Live 服务器
虽然实际上可以在不关闭服务器的情况下升级 Live 服务器,但这通常不是一个好主意。实际上,最好创建一个包含 index.html 和 404.html 文件的测试目录;如下所示:
<html>
<head>
<title>Upgrading site check back later</title>
</head>
<body>
We are currently upgrading our server, please check back later.
</body>
</html>
然后,保存一个类似以下的 .htaccess 文件(假设你正在使用 Apache):
RewriteEngine On
RewriteRule .+ test/index.html
这假设你的网络主机在 Apache 中启用了 mod_rewrite(并非所有主机都这样做)。更好的是,如果你可以访问 httpd.conf,只需将 DocumentRoot 的目录更改为测试目录,并添加 ErrorDocument 引用。
小贴士
由于 PEAR 安装器使用原子文件事务,因此几乎不可能出现半完成的安装。临时网站的目的在于避免用户在出现任何重大问题时看到网站。你可以通过添加一个 RewriteCond 规则来测试网站,该规则指定你的计算机 IP 将忽略该规则,这样你就可以看到完整的网站并检测需要修复的任何问题。
一旦你正确地将网站隐藏在 RewriteRule 之后,就是时候真正升级网站了。首先,使用我们在本章开头学到的cvs checkout或svn checkout从源代码控制中检出网站的副本。接下来,我们需要创建包并安装它。在我们能够这样做之前,我们需要安装我们创建的所有必要的自定义文件角色:
$ pear channel-discover pearified.com
$ pear install pearified/Role_Web
$ pear channel-discover pear.chiaraquartet.net/private
$ pear install priv/Role_Chiara
$ pear install priv/Role_Chiara2
接下来,我们需要初始化自定义文件角色:
$ pear run-scripts pearified/Role_Web
$ pear config-set root_url http://www.chiaraquartet.net/
$ pear config-set calendar_url http://calendar.chiaraquartet.net/
$ pear config-set database_url http://database.chiaraquartet.net/
$ pear config-set music_url http://music.chiaraquartet.net/
$ pear config-set addressbook_url http://addressbook.chiaraquartet.net/
一旦完成这些,我们就可以开始安装了:
$ cvs checkout...etc.
$ cd website
$ pear package
$ pear install website-0.10.0.tgz
就这样!现在我们已经完成了初始安装,当需要时升级到下一个版本变得非常简单。
使用 pear upgrade 命令
对于本节,我们将使用 CVS 作为我们的源代码控制示例,但如果你使用的是 Subversion 仓库,请替换我们学到的关于 Subversion 的内容。
当你需要修复一个错误或添加一个新功能时,只需修改package.php package.xml生成文件中的发布说明:
$version = '0.11.0';
$apiversion = '0.1.0';
$notes = <<<EOT
Add a doohickey to the main page
EOT;
然后,创建package.xml文件:
$ php package.php commit
最后,提交你的工作:
$ cvs commit -m "add a doohickey to the main page, and prepare for 0.11.0 release"
再次,通过以下方式在本地服务器上进行测试:
$ pear upgrade package.xml
当你在远程服务器上确定它工作正常后,只需运行这些命令:
$ cd website
$ cvs upd -P -d
$ pear package
$ pear cvstag package.xml
$ pear upgrade website-0.11.0.tgz
到这一阶段,你已经成功升级到版本 0.11.0。
如果你发现版本 0.11.0 中存在版本 0.10.0 中不存在的关键错误,会发生什么?幸运的是,修复问题的顺序简单而优雅:
$ cd website
$ pear upgrade -f website-0.10.0.tgz
只需两行输入即可。这是假设你保留了 0.10.0 版本的 tarball。即使你没有,这个过程仍然非常简单:
$ cd website
$ cvs upd -r RELEASE_0_10_0 -P -d
$ pear upgrade -f package.xml
$ cvs upd -r HEAD -P -d
从技术上讲,返回 CVS 的HEAD的最后一条命令对于恢复网站来说并不是真正必要的,但它会在你忘记已经检出RELEASE_0_10_0标签时节省未来的麻烦。在 Subversion 中,这个过程同样简单:
$ cd website
$ svn switch http://yourwebsite.example.com/subversion/website/tags/RELEASE_0_10_0
$ pear upgrade -f package.xml
$ svn switch http://yourwebsite.example.com/subversion/website/trunk
现在是时候回答那个价值 1000 万美元的问题了:如果你在远程服务器上没有 shell 访问权限,你该如何执行这些任务?(提示:阅读第一章中关于PEAR_RemoteInstaller的部分,你将找到答案)。
使用梨来解决问题的关键之美
对于网页开发者来说,最糟糕的时刻是在网站上发现严重缺陷。通常很难快速回滚到旧版本。PEAR 安装器使这个过程变得简单。如果测试确定问题是在版本 1.2.3 中引入的,而在版本 1.2.2 中不存在,那么回滚到版本 1.2.2 就像这样:
$ pear upgrade --force website-1.2.2.tgz
或者如果你已经设置了一个频道(我们假设你已经将其别名为private):
$ pear upgrade --force private/website-1.2.2
简称:
$ pear up f private/website-1.2.2
此外,如果你传递了-o命令行选项,任何所需的依赖项也将被降级。
摘要
在本章中,我们看到了如何使用 PEAR 安装程序来管理一个复杂且快速发展的网站。我们看到了在网站开发中涉及的一些问题,例如代码冲突、文件缺失或多余,以及 PEAR 安装程序如何帮助我们解决这些问题。
我们还看到了如何设置版本控制系统,无论是 CVS 还是 Subversion。最后,我们看到了如何使用 PEAR 安装程序和源代码控制系统来更新一个网站。
在下一章中,你将学习如何在互联网上公开分发你的库和应用程序的方法。
第五章。向世界发布:PEAR 渠道
PEAR 版本 1.4.0 及更高版本的一个主要特性是能够公开分发自己的应用程序,以便使用 PEAR 安装程序进行安装。尽管在 PEAR 1.3.6 及更早版本中这是可行的,但难度很大,因此很少尝试。PEAR 版本 1.4.0+ 通过使用一种新的分发介质称为 渠道 来简化包的分发。每个 PEAR 渠道提供一组独特的包,可以使用 PEAR 安装程序轻松安装。例如,要从 pear.chiaraquartet.net 渠道安装一个包,只需输入:
$ pear channel-discover pear.chiaraquartet.net
$ pear install chiara/Chiara_PEAR_Server
在过去,这根本不可能实现。安装来自 pear.chiaraquartet.net: 的包需要一组非直观的按键操作。
$ pear config-set master_server pear.chiaraquartet.net
$ pear install Chiara_PEAR_Server
$ pear config-set master_server pear.php.net
复杂性被跨渠道依赖所加剧。pear.chiaraquartet.net/Chiara_PEAR_Server 包依赖于 pear.php.net/HTML_QuickForm,因此实际上需要按照以下顺序:
$ pear install HTML_QuickForm
$ pear config-set master_server pear.chiaraquartet.net
$ pear install Chiara_PEAR_Server
$ pear config-set master_server pear.php.net
在升级时,需要重复相同的流程,这不仅带来了痛苦的回忆需求(“我上次是从哪里得到这个 Chiara_PEAR_Server 的?”),还增加了出错的机会。如果 pear.chiaraquartet.net 恰好提供了一个名为 LogXML 的包,而 pear.php.net 也引入了一个包,那么如果你不小心输入了:
$ pear upgrade LogXML
如果没有必要的:
$ pear config-set master_server pear.chiaraquartet.net
$ pear upgrade LogXML
$ pear config-set master_server pear.php.net
你可能会无意中升级到错误的包!渠道消除了所有这些麻烦,并且以严格的安全措施来实现。
那么,如何设置你自己的渠道?本章将探讨安装 Chiara_PEAR_Server 所需的步骤,以及用于记录渠道独特特征的渠道定义文件的架构。
此外,我们还将学习如何按用户分发定制的 PEAR 应用程序,即使是付费使用应用程序。我们将发现用于为用户浏览网页提供公共入口的 Crtx_PEAR_Channel_Frontend 包,最后,我们将讨论安全问题。
分发基于 package.xml 的包
分发包有两种方式(任选其一):
-
渠道服务器
-
静态压缩包
你将在本节中了解这两种方法。
在 PEAR 1.4.0 版本发布之前,用户需要输入:
$ pear remote-list
PEAR 安装程序将使用 XML-RPC 发送请求,请求 package.listAll 方法到 pear.php.net/xmlrpc.php。同时,在 pear.php.net,将查询所有包、发布和依赖关系的数据库以获取数据(或访问服务器端缓存),然后将其动态编码成 XML-RPC 响应,在用户端解码,并转换成一个包含所有包及其发布的 PHP 数组。然后,这些信息将被格式化为一个漂亮的包名称列表,并显示在屏幕上。
小贴士
XML-RPC 代表 XML 远程过程调用,是一种协议,允许程序像在本地机器上实现一样在远程服务器上调用函数。
SOAP(直到最近它被称为 简单对象访问协议,但现在只是 "SOAP",因为开发者意识到它并不简单,而且这个名字也很容易引起混淆)是同一想法的更复杂实现。
从最终用户的角度来看,使用 PEAR 安装程序安装远程包有两种方式。第一种方式是安装一个抽象包,如下所示:
$ pear install PEAR
$ pear install PEAR-stable
$ pear install PEAR-1.4.3
$ pear install channel://pear/PEAR
$ pear install channel://pear.php.net/PEAR-1.4.3
这些示例中的每一个都将用户传入的信息转换为从频道服务器(在这种情况下为 pear.php.net)检索文件的实际、现有 URL,然后下载该包进行安装。实际上,在撰写本章时,这些示例本质上都转换为以下内容:
$ pear install http://pear.php.net/get/PEAR-1.4.3.tgz
安装包的第二种方式是直接指定安装的 URL,如上所述。
这两种方法对最终用户来说看起来是相同的。然而,在幕后,它们有显著的不同。通过通过像 PEAR 或 PEAR-stable 这样的抽象包下载和安装包时,可以在下载单个文件之前验证所有依赖关系,从而在安装过程中最慢的部分——下载——中节省大量时间。当通过静态 URL(pear.php.net/get/PEAR-1.4.3.tgz)安装时,在执行任何依赖关系验证之前,必须下载整个包,这可能会导致带宽浪费。
为了将抽象包请求转换为实际的物理 URL,需要从远程频道服务器检索少量信息。这些信息用于在下载完整包之前验证依赖关系,并根据用户的需求确定要下载的包的正确版本。
例如,以下调用首先检索按版本号和稳定性组织的所有 PEAR 版本列表:
$ pear upgrade PEAR-stable
假设服务器返回如下列表:
| 版本 | 稳定性 |
|---|---|
| 1.5.0a1 | alpha |
| 1.4.3 | stable |
| 1.4.2 | stable |
| 1.4.1 | stable |
| 1.4.0 | stable |
| 1.4.0RC1 | beta |
| 1.4.0a14 | alpha |
PEAR 安装程序将检查版本 1.5.0a1,这是可用的最新版本,并确定它不够稳定以进行安装。接下来,它将检查版本 1.4.3,并且(假设已安装的版本是 1.4.2 或更早版本)确定这是应该下载的版本。接下来,它将查询服务器并检索版本 1.4.3 的依赖关系列表,这类似于以下内容:
| 依赖类型 | 依赖名称(如果有) | 依赖版本要求 |
|---|---|---|
| PHP | 4.2.0 或更高版本 | |
| PEAR 安装程序 | 1.3.3 1.3.6, 1.4.0a12 或更高版本 | |
| 包 | Archive_Tar | 1.3.1 或更高版本(推荐 1.3.1) |
| 包 | Console_Getopt | 1.2 或更高版本(推荐使用 1.2) |
| 包 | XML_RPC | 1.4.3 或更高版本(推荐使用 1.4.3) |
| 冲突包 | PEAR_Frontend_Web | 0.4 或更早版本 |
| 冲突包 | PEAR_Frontend_Gtk | 0.3 或更早版本 |
| 远程安装程序组 | PEAR_RemoteInstaller | 0.1.0 或更高版本 |
| 网页安装程序组 | PEAR_Frontend_Web | 0.5.0 或更高版本 |
| Gtk 安装程序组 | PEAR_Frontend_Gtk | 0.4.0 或更高版本 |
在下载 PEAR 1.4.3 以安装它之前,PEAR 安装程序将使用这些信息来确定该包是否与现有已安装的包兼容,以及 PHP 和 PEAR 安装程序的运行版本。只有当所有检查都通过时,PEAR 安装程序才会继续下载/安装。
此外,由于 PEAR 1.4.3 的 package.xml 版本为 2.0,当从 PEAR 1.4.0 或更高版本升级时,所需的依赖项列表也将自动下载并安装。
您可能会问自己,我如何分发我的应用程序和库,以利用 PEAR 安装程序内置的强大功能和优雅性?答案出乎意料地简单,正如自 PEAR 版本 1.4.0 发布以来频道服务器数量的小幅激增所证明的那样,例如 eZ components(http://www.ez.no),以及流行的 pearified 频道(www.pearified.com)。Chiara_PEAR_Server 包是一个完全功能的 PEAR 频道服务器,可以从 pear.chiaraquartet.net 频道服务器安装。
小贴士
最初,Chiara_PEAR_Server 的名字是 PEAR_Server。意图是在代码足够稳定时,向 pear.php.net 仓库提出一个名为 "PEAR_Server" 或 "PEAR_Channel_Server" 的包。然而,在此之前,存在潜在的命名冲突(通常 PEAR 保留用于源自 pear.php.net 的包),因此只要它从 pear.chiaraquartet.net 分发,包将被命名为 Chiara_PEAR_Server。
一旦 Chiara_PEAR_Server 运行起来(前提条件包括一个有效的 PEAR 安装、MySQL 服务器以及 PHP 5.0.0 或更高版本,并带有 mysql 或 mysqli 扩展),您也可以考虑安装 Davey Shafik 的公共前端 Crtx_Channel_PEAR_Server_Frontend,该前端可通过 crtx.org 频道获取。这一点将在本章后面的 配置服务器;为最终用户提供前端 部分中讨论。
通过频道服务器分发包
当通过频道服务器分发包时,PEAR 安装程序需要一些信息来确定要安装哪些包。最重要的是如何与频道服务器通信。服务器是否期望接收 XML-RPC 请求,或者支持 REST?实现了哪些 XML-RPC 函数,提供了哪些 REST 信息?是否有可用的镜像?是否有任何自定义包验证要求?
所有这些问题都通过简单的channel.xml结构得到解答。在安装 Chiara_PEAR_Server 之前,了解该包的基础非常重要,因为它将使快速启动和运行成为可能。
channel.xml文件
一个通道为了存在,首先需要的是一个channel.xml文件。在 XSchema 格式中,channel.xml的官方定义可以在pear.php.net/dtd/channel-1.0.xsd找到。channel.xml文件必须命名为channel.xml,并且必须位于通道的根目录中;否则,PEAR 安装程序的自动发现机制将无法工作。例如,pear.php.net的通道定义文件位于pear.php.net/channel.xml,而pear.chiaraquartet.net的通道定义文件位于pear.chiaraquartet.net/channel.xml。
此文件允许 PEAR 安装程序快速有效地确定通道服务器提供的功能,而不会浪费任何带宽。一个channel.xml文件必须定义通道名称(其服务器主机名和路径)、通道目的的简要说明,以及用于检索安装目的的包信息的元数据。此外,channel.xml文件允许显式定义通道镜像,首次使镜像通道存储库成为可能。
这里是一个包含每个可能标签的示例channel.xml文件:
<?xml version="1.0" encoding="ISO-8859-1"?>
<channel version="1.0"
xsi:schemaLocation="http://pear.php.net/channel-1.0
http://pear.php.net/dtd/channel-1.0.xsd">
<name>pear.example.com</name>
<summary>Example channel</summary>
<suggestedalias>example</suggestedalias>
<validatepackage version="1.0">
Example_Validate_Package</validatepackage>
<servers>
<primary ssl="yes" port="81">
<xmlrpc path="myxmlrpc.php">
<function version="1.0">logintest</function>
<function version="1.0">package.listLatestReleases</function>
<function version="1.0">package.listAll</function>
<function version="1.0">package.info</function>
<function version="1.0">package.getDownloadURL</function>
<function version="1.1">package.getDownloadURL</function>
<function version="1.0">package.getDepDownloadURL</function>
<function version="1.1">package.getDepDownloadURL</function>
<function version="1.0">package.search</function>
<function version="1.0">channel.listAll</function>
</xmlrpc>
<soap path="soap.pl">
<function version="1.0">customSoapFunction</function>
</soap>
<rest>
<baseurl type="REST1.0">http://pear.example.com/rest/</baseurl>
<baseurl type="REST1.1">http://pear.example.com/rest/</baseurl>
</rest>
</primary>
<mirror host="poor.example.com" port="80" ssl="no">
<xmlrpc>
<function version="1.0">logintest</function>
<function version="1.0">package.listLatestReleases</function>
<function version="1.0">package.listAll</function>
<function version="1.0">package.info</function>
<function version="1.0">package.getDownloadURL</function>
<function version="1.1">package.getDownloadURL</function>
<function version="1.0">package.getDepDownloadURL</function>
<function version="1.1">package.getDepDownloadURL</function>
<function version="1.0">package.search</function>
<function version="1.0">channel.listAll</function>
</xmlrpc>
<soap path="soap.php">
<function version="1.0">customSoapFunction</function>
</soap>
<rest>
<baseurl type="REST1.0">http://poor.example.com/rest/</baseurl>
<baseurl type="REST1.1">http://poor.example.com/rest/</baseurl>
</rest>
</mirror>
</servers>
</channel>
快速浏览channel.xml文件可以揭示大量信息,这些信息以非常简单的格式呈现。通过此文件,我们告诉 PEAR 安装程序是否使用安全连接,如何访问包元数据(使用 XML-RPC、SOAP 或 REST),以及用户如何访问/使用通道(建议别名、验证包)。
小贴士
PEAR 安装程序支持 SOAP 吗?
很抱歉打破你的幻想,但 SOAP 对于 PEAR 安装程序所需的相对简单的远程通信并不是必需的;所以,SOAP 没有被实现。然而,如果将来有需要,或者通道希望宣传自定义的 SOAP 方法,channel.xml规范支持 SOAP。
然而,这仅应用于通知客户存在一个WSDL(Web 服务描述语言)文件,因为这种格式比channel.xml丰富得多。
一个通道可以位于主机名的根目录(pear.example.com)或子目录(pear.example.com/subdirectory)。请注意,pear.example.com与pear.example.com/subdirectory是不同的通道。用户将按照以下方式从pear.example.com/subdirectory通道安装包:
$ pear install pear.example.com/subdirectory/Packagename
其他包将依赖于来自pear.example.com/subdirectory通道的包,其中包含一个类似于下面的package.xml标签:
<dependencies>
<required>
...
<package>
<name>Packagename</name>
<channel>pear.example.com/subdirectory</channel>
</package>
</required>
</dependencies>
channel.xml标签摘要
通道的<summary>应该是对通道的单行描述,例如:“PHP 扩展和应用仓库”。
通道的<suggestedalias>是用户可以在命令行中使用的简称。
例如,pear.php.net通道的建议别名是pear,pecl.php.net通道的建议别名是pecl,而pear.chiaraquartet.net通道的建议别名是chiara。这些别名可以用来快速安装包,例如:
$ pear install pear/DB
$ pear install chiara/Chiara_PEAR_Server
别名是建议的别名,因为最终用户有通过channel-alias命令重新定义别名的选项:
$ pear channel-alias pear.chiaraquartet.net c
这将允许快速安装包:
$ pear install c/Chiara_PEAR_Server
小贴士
你不能在package.xml文件的依赖关系部分使用通道建议的别名。你必须使用通道的全名。
通道的验证包(由<validatepackage>标签控制)由安装程序用于执行针对通道的特定定制验证。默认验证(在 PEAR 包的PEAR/Validate.php文件中找到)在版本和包命名方面非常严格,并试图实现针对pear.php.net-based包的特定编码标准。这些规则比pecl.php.net通道实施的规则更严格,因此pecl.php.net包使用 PEAR 包的PEAR/Validate/PECL.php文件中找到的定制通道验证器进行验证。
大多数外部于pear.php.net的通道都希望复制pecl.php.net通道的channel.xml文件,并使用PEAR_Validate_PECL验证包。
自定义通道验证器必须提供一个与路径匹配的类(PEAR/Validate/PECL.php提供了PEAR_Validate_PECL类),并且包名必须与类名相同。此外,该类必须扩展PEAR_Validate,并实现使用validate*()方法(如validateVersion()、validatePackage()、validateSummary()等)的验证。此外,该类必须是来自通道本身的包分发的,除非该类已经加载到内存中。
小贴士
默认验证类PEAR_Validate和 PECL 验证类PEAR_Validate_PECL将始终可用,供通道作为自定义验证包使用。
要使用PEAR_Validate_PECL类,只需将此行添加到channel.xml中:
<validatepackage version="1.0"> PEAR_Validate_PECL</validatepackage>
channel.xml通道定义文件最重要的部分是<servers>标签。这是 PEAR 安装程序确定如何连接到通道(通过 REST 或 XML-RPC)以及是否有可用镜像的地方。
主要通道服务器(必须与通道名称相同)支持的协议在<primary>标签中定义。镜像通过<mirror>标签(在本节末尾描述)逻辑上定义。<primary>标签有几个可选属性:
-
ssl — 合法值是 yes 和 no。默认情况下,
ssl设置为 no。如果设置为 yes,则将通过安全套接字联系通道服务器。 -
端口 — 合法值是任何正整数。默认情况下,
端口设置为 80,这是联系远程 Web 服务器的默认 HTTP 端口。所有来自通道的数据都通过 HTTP 传输,因此这是一个自然的选择。小贴士
虽然 REST 非常新,但它相对于 XML-RPC 有几个显著的优势。首先,REST 内容(如 PEAR 通道标准中实现的那样)都是静态文件。这意味着可以使用轻量级服务器如 thttpd 来为高流量站点提供服务内容。此外,像
www.pearified.com和www.pearadise.com这样的通道聚合器可以爬取您的通道并提供包的可搜索索引。由于遵循相同的设计原则,基于 REST 的通道镜像非常简单,可以使用一个简单的网络爬虫脚本来完成。
此外,从 PEAR 1.4.3 版本开始,XML-RPC 支持在 PEAR 安装程序中是可选的,因此并非所有用户都会在客户端支持 XML-RPC。
PEAR 安装程序只识别少数几种协议。对于 XML-RPC,识别的函数有:
-
logintest(1.0): 这个函数简单地返回 true -
package.listLatestReleases(1.0): 这个函数返回一个数组,按包名索引,包含其最新发布版本的文件大小、版本、状态和依赖项(如果有)。 -
package.listAll(1.0): 这个函数返回一个包含其发布版本极端详细信息的包数组。 -
package.info(1.0): 这个函数返回关于单个包的详细信息的数组。 -
package.getDownloadURL(1.0): 这个函数返回一个包含关于发布版本简单信息和精确下载 URL 的数组。 -
package.getDownloadURL(1.1): 与版本 1.0 类似,这个函数返回一个包含关于发布版本简单信息和精确 URL 的数组。此外,这个函数接受作为参数的当前安装的包版本以缩小搜索范围。 -
package.getDepDownloadURL(1.0): 与package.getDownloadURL类似,这个函数返回关于发布版本的信息和下载该发布版本的精确 URL。然而,作为输入,它接受从package.xml解析出的依赖项。 -
package.getDepDownloadURL (1.1): 与package.getDepDownloadURL类似,这个函数返回关于一个发布版本和下载该发布版本的精确 URL 的信息。它还接受作为参数的当前已安装的依赖版本。 -
package.search(1.0): 与package.listAll类似,这个函数返回包含详细信息的包列表。然而,这个函数根据输入参数限制信息搜索。 -
channel.listAll(1.0): 这个函数返回当前通道所知的通道的简单列表。
提供此详细信息仅用于信息目的,因为所有通道的 XML-RPC 支持都已弃用。
相反,您的通道应该支持基于 REST 的静态文件,这些文件传达了通过通道可用的类别、维护者、软件包和发布信息。在撰写本章时,PEAR 安装程序支持两种协议。第一种统称为 REST1.0,它由安装程序做出的路径相关假设和几个 XSchema 文件定义。
在 channel.xml 文件中,REST 使用 <baseurl> 标签声明,类似于以下内容:
<baseurl type="REST1.0">http://pear.php.net/rest/</baseurl>
实际上,安装程序只需要这些信息来完全实现 REST。从这个信息中,安装程序能够构建任何必要的查询以确定远程信息。敏锐的读者可能已经注意到了他们与数据库工作中的一个熟悉词汇——查询——查询正是 PEAR 安装程序所做的事情;直接访问数据而不是通过像 SOAP 或 XML-RPC 这样的 API 包装器。与 XML-RPC 和基于 RPC 的 SOAP 所使用的程序性协议不同,REST 基于提供超链接数据或资源的原理,每个资源都有一个独特的 URL。
PEAR 是一个不寻常的 REST 接口,因为它严格是只读的,但这更是使用 REST 的好理由。不仅安装程序可以抓取它想要的任何数据而不依赖于本质上有限的 API,我们还可以利用 HTTP 协议的一些更强大的功能,并在客户端实现 HTTP 缓存,节省大量带宽和时间,否则这些带宽和时间将用于下载冗余信息。
这也为客户端和服务器提供了固有的安全优势。客户端只是与静态 XML 文件工作,服务器无需从客户端接受任何输入。简而言之,REST 简直是从所有方面来看都是最佳选择。
当访问 REST1.0 时,PEAR 安装程序期望的路径结构如下:
c/ [Categories]
CategoryName1/
info.xml [information on the "CategoryName1" category]
packages.xml [list of packages in the CategoryName1 category]
CategoryName2/
info.xml [information on the "CategoryName2" category]
packages.xml [list of packages in the CategoryName2 category]
m/ [Maintainers]
joe/
info.xml [information about maintainer "joe"]
frank/
info.xml [information about maintainer "frank"]
amy/
info.xml [information about maintainer "amy"]
p/ [Packages]
packages.xml [A list of all packages in this channel]
PackageName1/
info.xml [information on "PackageName1" package]
maintainers.xml [list of maintainers of this package]
PackageName2/
info.xml [information on "PackageName2" package]
maintainers.xml [list of maintainers of this package]
PackageName3/
info.xml [information on "PackageName3" package]
maintainers.xml [list of maintainers of this package]
r/ [Releases]
PackageName1/ [Releases of package PackageName1]
allreleases.xml [A brief list of all releases available]
1.0.0.xml [summary information about version 1.0.0]
package.1.0.0.xml [the complete package.xml of this release]
deps.1.0.0.txt [PHP-serialized dependencies of version 1.0.0]
0.9.0.xml [summary information about version 0.9.0]
deps.0.9.0.txt [PHP-serialized dependencies of version 0.9.0]
package.0.9.0.xml [the complete package.xml of this release]
...
...
latest.txt [the latest version number, in text format]
stable.txt [the latest stable version number, in text format]
beta.txt [the latest beta version number, in text format]
PackageName2/ [Releases of package PackageName2]
allreleases.xml [A brief list of all releases available]
1.1.0.xml [summary information about version 1.1.0]
deps.1.1.0.txt[PHP-serialized dependencies of version 1.1.0]
package.1.1.0.xml [the complete package.xml of this release]
1.0.4.xml [summary information about version 1.0.4]
deps.1.0.4.txt[PHP-serialized dependencies of version 1.0.4]
package.1.0.4.xml [the complete package.xml of this release]
...
...
latest.txt [the latest version number, in text format]
stable.txt [the latest stable version number, in text format]
beta.txt [the latest beta version number, in text format]
alpha.txt [the latest alpha version number, in text format]
devel.txt [the latest devel version number, in text format]
注意,PackageName3 没有发布版本,因此没有 REST 条目。
REST1.1 将这些文件添加到结构中:
c/
categories.xml [list of all categories]
CategoryName1/
packagesinfo.xml [consolidated package/release info for the entire
category]
CategoryName2/
packagesinfo.xml [consolidated package/release info for the entire
category]
m/
allmaintainers.xml [list of all maintainers]
p/
r/
REST1.1 的主要目的是在不要求允许老式的目录爬行的情况下,允许对通道进行爬取,消除了所有 Web 服务器固有的潜在安全漏洞。
通道服务器镜像由 <mirror> 标签定义。此标签与 <primary> 标签相同,但它需要一个额外的属性,即 host。host 属性定义了用于联系镜像的 URL。
获取 Chiara_PEAR_Server
Chiara_PEAR_Server 软件包很容易获取。首先,您需要满足一些先决条件。Chiara_PEAR_Server 软件包需要:
-
PHP 5.0.0 或更高版本;建议使用 PHP 5.1.0 或更高版本
-
MySQL 数据库服务器
-
mysql 或 mysqli PHP 扩展
-
一个运行中的 Web 服务器,例如 Apache
-
PEAR 版本 1.4.3 或更高
要获取 PEAR 版本 1.4.3 或更高版本,如果您使用的是 PHP 版本 5.1.0,在 UNIX 系统上您需要做的只是:
$ cd php-5.1.0
$ ./buildconf
$ ./configure
$ make cli
$ make install-pear
这将自动安装和配置 PEAR。请注意,configure 命令接受大量选项,您可以通过 ./configure --help 来了解它们。
注意,在 Web 服务器上安装 PHP 更为复杂,并且是安装 Chiara_PEAR_Server 软件包所必需的。如果您使用 Apache,只需将 --with-apache 或 --with-apache2 指令传递给 configure 即可,然后您就可以开始使用了。
在 Windows 上,对于版本 5.2.0 之前的所有 PHP 版本,您需要下载 PHP 的 .zip 版本,而不是 .msi 版本。然后切换到您解压缩 PHP 的目录,并输入:
go-pear
回答提示并选择安装位置。在两种情况下,安装后,请务必升级:
pear upgrade PEAR
这将确保您拥有 PEAR 的最新稳定版本。
mysql 或 mysqli PHP 扩展的安装文档位于 www.php.net/mysql。
一切准备就绪后,您可以通过以下简单步骤获取 Chiara_PEAR_Server 软件包:
$ pear channel-discover pear.chiaraquartet.net
$ pear up Chiara_PEAR_Server-alpha
就这些了!请注意,Chiara_PEAR_Server 目前需要 pear.php.net 软件包 DB_DataObject 和 HTML_QuickForm,所以在尝试安装 Chiara_PEAR_Server 之前,请确保您有一个正常工作的互联网连接或者已经安装了这些软件包。
配置服务器需要运行一个安装后脚本,我们将在下一节中介绍。
配置服务器;为最终用户提供前端
在我们运行安装后脚本之前,了解它正常运行所需的内容是很重要的。
首先,我们需要创建一个 MySQL 用户,该用户将由安装后脚本用于初始化数据库和创建表。因此,此用户需要具有 create 和 alter 权限(使用 MySQL 内部的 GRANT 命令来完成此操作)。由于这将作为公共 Web 脚本使用的数据库用户,出于安全考虑,一旦数据库被正确初始化,最好移除 create/alter 权限。Chiara_PEAR_Server 管理后端在日常工作操作中所需的唯一权限是 insert/delete/update。请注意,在升级 Chiara_PEAR_Server 时,应暂时重新授予 create/alter 权限,以防对数据库进行修改或添加。
运行 Chiara_PEAR_Server 安装后脚本在 PEAR 中是一个简单的任务,您只需输入:
$ pear run-scripts chiara/Chiara_PEAR_Server
PEAR 安装程序将引导您完成几个问题。完成这些问题后,如果没有错误,通道服务器将准备就绪,可以运行。

第一组问题将要求提供数据库连接信息,以及主通道管理员(您)的Handle和通道名称。您的Handle与package.xml文件中的 handle 或用户名相同,并且应该是一个单个小写单词。例如,您可以通过浏览pear.php.net/accounts.php上的维护者列表来查看 handle 的选择示例。您的通道名称必须与服务器相同。例如,如果您在本地主机上设置测试服务器,您的通道名称必须是localhost。

接下来的一组问题将涉及您通道的基本信息。此时,您应该已经想到了一个服务器名称。一般来说,如果通道名称与其提供的内容有直接关联,用户更容易记住。例如,如果您的通道提供财务软件,那么像software.companyname.com这样的通道名称可能很好,别名可以是companysoftware。
在此之后,将要求提供有关主通道管理员的信息。最后,将询问您的 Web 服务器的文档根信息。
这一节非常重要,因为它将用于创建用于维护包和上传发布的行政前端,以及用于支持 PEAR 安装程序所需 REST 协议的 REST 文件。通常,括号中提供的默认值应该是可接受的。然而,了解不同提示的目的是有帮助的。
-
PEAR 配置文件位置: 这指定了用于检索通道信息的文件位置。Chiara_PEAR_Server 通道如果不能检索其自身通道的信息,将根本无法正常工作,因此这个值必须正确。
-
本地主机的 Web 服务器文档根路径: 这应该是您本地文件系统上 Web 服务器基目录的完整路径。如果您的 Web 服务器在用户请求
servername/index.php时读取/var/lib/web/htdocs/servername/index.php,那么您的文档根是/var/lib/web/htdocs/servername。 -
前端.php HTML 管理前端文件的名称: 这是您的管理前端文件的文件名。选择一个独特的文件名将有助于防止令人烦恼或恶意的人尝试未经授权访问通道管理界面。
-
保存发布上传的临时路径: 这应该是一个可由 Web 服务器写入的位置,您最初将在此处保存上传的发布版本。
-
客户端应连接到的端口(443 是 SSI,80 是常规 HTTP): 按照说明操作。如果您有典型的配置,80 是
http://的正常端口,443 是https://的典型端口。 -
协议客户端应使用的连接方式(http 或 https): 再次,选择两个选项之一。
添加软件包和发布软件包
完成安装脚本后,导航到管理前端文件,其名称在前端.php HTML 管理前端文件名称部分中指定。例如,如果您的前端文件名为foo.php,并且您的频道是 localhost,请导航到http://localhost/foo.php。您应该会看到如下内容:

要以管理员身份登录,请输入在安装脚本中为管理员指定的句柄和密码。登录后,在上传发布版本之前,您首先需要在服务器上创建软件包,然后添加维护者。
当您登录时,您将看到与此类似的屏幕:

配置频道的链接位于屏幕左侧。为了开始,您可能还想要为要发布的软件创建一些类别,例如“数据库”或“XML 处理”。有关类别的示例,请参阅pear.php.net/packages.php。
在您创建了类别后,您将想要添加维护者并创建软件包。创建软件包很简单。首先,点击创建软件包,您将看到如下屏幕:

填写必要的字段(由红色星号标记),然后点击保存更改。下一步非常重要:在您上传发布版本之前,您需要创建维护者并将他们添加为软件包的维护者。为此,请点击屏幕左侧您刚创建的软件包旁边的(维护者)链接:

选择维护者、角色以及维护者是否活跃后,点击添加维护者,维护者将出现在软件包维护者列表中。
小贴士
谁可以上传发布版本?
频道管理员可以为频道上的任何软件包上传发布版本,以及列在频道中作为主要维护者的软件包维护者。请注意,在package.xml文件中列为主要维护者但在频道管理前端未列出的人员将无法上传发布版本,出于安全原因。
安装公共频道前端
在配置并运行 Chiara_PEAR_Server 之后,你可能希望从 channel pear.crtx.org安装 Davey Shafik 的Crtx_PEAR_Channel_Frontend包。Crtx_PEAR_Channel_Frontend 包提供了一个可浏览的网站,允许开发者查看你的频道提供的包以及如何获取它们。此外,它还支持链接到错误跟踪器、在线修订控制浏览器和其他功能。Crtx_PEAR_Channel_Frontend 对于 Chiara_PEAR_Server 来说,就像pear.php.net对于在pear.php.net/rest提供的安装数据一样。
获取 Crtx_PEAR_Channel_Frontend 可以通过以下步骤完成:
$ pear channel-discover pear.crtx.org
$ pear upgrade crtx/Crtx_PEAR_Channel_Frontend
一旦安装了包,就需要进行一些小的配置(这可能在未来的版本中通过安装后的脚本自动化)。首先,你需要找到pear_frontend.css文件,它被安装到data_dir/Crtx_PEAR_Channel_Frontend/data/pear_frontend.css。
data_dir是数据路径(在 UNIX 上通常是/usr/local/lib/php/data,在 Windows 上是C:\php5\PEAR\data或C:\php4\PEAR\data),由 PEAR 的data_dir配置变量定义。一旦找到文件,将其复制到你的频道文档根目录。
在复制pear_frontend.css文件后,你需要创建公共前端 PHP 文件。以下是一个示例前端:
<?php
/**
* An example of Crtx_PEAR_Channel_Frontend Usage
*
* @copyright Copyright © David Shafik and Synaptic Media 2004.
*All rights reserved.
* @author Davey Shafik <davey@synapticmedia.net>
* @link http://www.synapticmedia.net Synaptic Media
* @version $Id: $
* @package
* @category Crtx
*/
/**
* Crtx_PEAR_Channel_Frontend Class
*/
require_once 'Crtx/PEAR/Channel/Frontend.php';
$frontend = new Crtx_PEAR_Channel_Frontend('localhost',
array('database' => 'mysqli://user:pass@localhost/pearserver',
'index' => 'index.php', 'admin' => 'admin_myfront.php'));
?>
<html>
<head>
<title>localhost Channel Server</title>
<link rel="stylesheet" type="text/css"
href="pear_frontend.css" />
<?php
$frontend->showLinks();
?>
</head>
<body>
<div id="top">
<h1><a href="index.php">localhost Channel Server</a></h1>
</div>
<div id="menu">
<?php
$frontend->showMenu();
?>
<div id="releases">
<?php
$frontend->showLatestReleases();
?>
</div>
</div>
<div id="content">
<?php
if (!$frontend->run()) {
$frontend->welcome();
}
?>
</div>
</body>
</html>
如果将此文件保存为index.php在localhost网络服务器的文档根目录中,将提供类似于以下的有吸引力的屏幕:

此包内置了几个很好的功能,包括 RSS 源和向维护者发送电子邮件的能力。此外,外观和感觉的定制非常简单,通过基本修改pear_frontend.css文件和index.php前端来实现。只需做很少的工作,就可以创建一个非常吸引人的前端。可能性的多样性示例包括pear.crtx.org和pear.php-tools.net。
通过频道分发付费使用的 PHP 应用程序
关于频道的一些更常见的问题是:“我的业务能否通过频道分发付费使用的 PHP 应用程序并限制访问?”答案是响亮的肯定。
PEAR 安装程序通过使用pear login命令实现 HTTP 身份验证。为了分发你的非免费应用程序,最好依靠像 Apache 这样的 Web 服务器的优势。例如,通过将“获取”文件和httpd.conf中的ForceType指令如下使用:
<Location /get>
ForceType application/x-httpd-php
</Location>
你可以使用一个名为get的文件,其中包含 PHP 代码来处理用户提供的登录/密码,并将他们引导到为他们的登录定制的受限制的包。实际上,普通用户可以被引导到软件的试用版本,而无需更改他们的安装过程。
注意,PEAR 安装器仅支持 HTTP Basic 认证,并且为了实现真正的安全连接,应使用 SSL(HTTPS),否则任何人都可以获取受限制的密码。以下是一个用于 get 文件的示例脚本:
<?php
/**
* Example restricted access file
*
*
* This example requires Apache, PHP 4.3+, the mysqli extension, and
* this code to be added to httpd.conf/.htaccess:
* <pre>
* <Location "/get">
* ForceType application/x-httpd-php
* </Location>
* </pre>
*
* In addition, it assumes that a mysql database is set up with users
* who have purchased the packages, and that database connection
* info is set in php.ini
* @package download
*/
// shut up or we risk getting corrupted tgz files
error_reporting(0);
function error($message)
{
header('HTTP/1.0 404 Not Found');
echo $message;
exit;
}
/**
* Downloader class, handles authentication and actual downloading
* @package download
*/
class Download
{
var $user = false;
var $passwd = false;
/**
* A list of purchased versions that the current user may
*download
*
* @var array
*/
var $purchased = array();
/**
* MySQL database connection
*
* @var resource mysqli resource
*/
var $db;
/**
* Full path to offline location of package releases
*
* @var string
*/
var $path = '/path/to/releases/';
/**
* Hash of package names to demo versions
*
* This probably should be constructed from a database,
* but for our simple example it will be hard-coded
*
* @var array
*/
var $demo_versions = array(
'Foo' => array('1.0demo'),
'Bar' => array('1.1demo', '2.0demo'),
);
/**
* Hash of package names to full versions
*
* This probably should be constructed from a database,
* but for our simple example it will be hard-coded
*
* @var array
*/
var $full_versions = array(
'Foo' => array('1.0'),
'Bar' => array('1.1', '2.0'),
);
/**
* Connect to the database, authenticate the user,
* and grab the list
* of purchased packages for this user
*/
function Download()
{
// assume we have specified connection details in php.ini
$this->db = mysqli_connect();
if ($this->db) {
// on database connect failure,
// we can still download demos,
// so fail silently
if (isset($_SERVER['PHP_AUTH_USER']) &&
isset($_SERVER['PHP_AUTH_PASSWD'])) {
$this->user = $_SERVER['PHP_AUTH_USER'];
$this->passwd = $_SERVER['PHP_AUTH_PASSWD'];
// construct a list of purchased packages
// for this user/pass combination
if ($res = mysqli_query($this->db, '
SELECT purchased_package FROM regusers
WHERE user = "' .
mysqli_real_escape_string($this->db,
$this->user) . '", AND pass = "' .
mysqli_real_escape_string($this->db,
$this->passwd) . '"')) {
while ($row = mysqli_fetch_row($res)) {
$this->purchased[$row[0]] = true;
}
}
}
}
}
/**
* Feed the file to the user, or display an error
*
* @param string $path
*/
function downloadPackage($path)
{
// note that we assume the case is correct
// (the PEAR Installer always gets
// this correct, only manual downloads will fail)
if (!preg_match('/^([a-zA-Z0-9_]+)-(.+)\.(tar|tgz)$/',
$path, $matches)) {
error('invalid package/version: "' . $path . '"');
}
list(, $package, $version, $ext) = $matches;
// sanity check #1: does the release exist on the disk?
if (!file_exists($this->path . $package . '-' . $version .
'.' . $ext)) {
error('unknown package/release: "' . $path . '"');
}
// sanity check #2: do we know anything about this version?
if (!isset($this->demo_versions[$package]) &&
!isset($this->full_versions[$package])) {
error('unknown package: "' . $package . '"');
}
// check to see if it is a demo version, and return right
// away if so
// if you have more purchaser downloads than demos,
// put this after
// purchased check for slight speed increase
if (isset($this->demo_versions[$package]) &&
in_array($version, $this->demo_versions[$package],
true)) {
$this->_doDownload($package, $version, $ext);
}
if (isset($this->full_versions[$package]) &&
in_array($version, $this->full_versions[$package],
true)) {
if (isset($this->purchased[$package])) {
$this->_doDownload($package, $version, $ext);
}
// if we get here, the user has not purchased this
// version
error('version "' . $version . '" is restricted and
must be purchased. ' .
'Use "pear login" to set purchase key first');
}
// fall-through: this line of code should be unreachable
error('internal error, please report attempt to download "
' . $path . '" failed');
}
/**
* Do the actual downloading.
*
* @param string $package
* @param string $version
* @param string $ext this is either "tar" or "tgz"
* @access private
*/
function _doDownload($package, $version, $ext)
{
// construct local path to the downloadable object
$path = $this->path . $package . '-' . $version . '.' . $ext;
header('Last-modified: ' .
gmdate('D, d M Y H:i:s \G\M\T', filemtime($path)));
header('Content-type: application/octet-stream');
header('Content-disposition: attachment; filename="' .
$path . '"');
header('Content-length: ' . filesize($path));
readfile($path);
exit;
}
}
if (!isset($_SERVER['PATH_INFO']) || $_SERVER['PATH_INFO'] == '/') {
error('no package selected');
}
$info = explode('/', $_SERVER['PATH_INFO']);
switch (count($info)) {
case 2:
$dl = new Download;
$dl->downloadPackage($info[1]);
break;
default:
error('no package selected');
}
?>
上述示例展示了即使是复杂的版本验证也可以非常容易地完成。然而,这种方法并不非常高效——每个下载都会通过 PHP 解释器,这比获取静态文件慢得多。另一个选项,仅适用于 PEAR 版本 1.4.9 或更高版本,是发送重定向头(402),并允许 Apache 处理实际的文件下载。然而,与使用 readfile() 相比,节省的量非常小,可能不值得要求用户升级。
另一个选项是简单地通过 .htaccess 文件中的 HTTP Basic 认证限制对文件的访问,对于单个 tarballs 来说,这可能是可扩展的。
然后,指导用户在首次设置登录名/密码时采取以下步骤:
$ pear -d "default_channel=your.channel.com" login
Logging into your.channel.com
Username: myuser
Password: mypassword
只有当用户从 your.channel.com 请求包时,才会发送用户名/密码,利用了按频道配置的优势。
强烈建议为您的频道使用 SSL,这样用户名/密码对就不会以明文形式发送。
那就是所有必要的了!
通过静态 tarballs 为单个客户端安装分发包
除了通过频道分发包之外,还可以分发单个发布版本并将其发布到网络上。
小贴士
安装带有依赖项的静态发布版本需要 PEAR 1.4.10
PEAR 安装器中的一个错误阻止了静态 tarballs 的安装;使用版本 1.4.10 或更高版本以获得此问题的修复。
这基本上涉及通过 pear package 打包一个发布版本,然后将其上传到网站,然后可以下载或直接通过以下方式安装:
$ pear install http://www.example.com/Package-1.0.0.tgz
这不是什么新鲜事:最早的 PEAR 安装器支持这种语法。新的功能是能够在其他包发布中依赖这些 静态 tarballs。
谁需要这个功能?
在某些情况下,设置频道服务器并非必需。一般来说,设置频道服务器并以这种方式分发包会更好。然而,在现实世界中,一个常见的场景是一位为多个客户提供服务的 PHP 顾问,同时也为他们维护网站。尽管每个网站都是独特的,但拥有一套每个特定网站都可以使用的实用包非常有帮助。只为这些包提供频道只会引入不必要的复杂性。
通过将客户的网站安装为 PEAR 包,作为软件顾问的您就可以轻松地维护网站并以比频道允许的更严格的方式管理其内容。
package.xml 和依赖项的差异
为了防止通道和静态 tar 包之间的名称冲突,静态 tar 包的package.xml文件不能使用[<channel>](http://<channel>)标签,而必须使用<uri>标签。此外,<uri>标签必须包含 tar 包在互联网上的实际位置。如果静态 tar 包位于www.example.com/tarballs/Package-1.0.0.tgz,则package.xml文件应该以类似以下内容开始:
<?xml version="1.0" encoding="UTF-8"?>
<package packagerversion="1.4.3" version="2.0"
xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>Package</name>
<uri>http://www.example.com/tarballs/Package-1.0.0</uri>
非常重要的是要注意,uri已经去除了文件扩展名.tgz。这是因为当提供静态 tar 包时,预期您也会为没有启用 zlib 扩展的用户提供未压缩的.tar文件。
要依赖这个静态 tar 包,应使用如下依赖标签:
<package>
<name>Package</name>
<uri>http://www.example.com/tarballs/Package-1.0.0</uri>
</package>
当使用静态 tar 包作为依赖项时,版本号没有意义,因此不允许使用任何正常的版本号标签(<min>, <max>, <recommended>)。然而,升级静态 tar 包是可能的。
每个静态 tar 包软件包都有一个隐含的<channel>__uri</channel>标签——所有静态 tar 包都是作为伪通道__uri安装/升级/卸载的。这个通道被当作其他通道一样对待,只是它不能通过channel-update命令修改,也不能通过channel-delete命令删除,并且它不包含服务器,因此永远不会尝试联系远程通道服务器。运行pear channel-info __uri的结果是:
CHANNEL __URI INFORMATION:
==========================
Name and Server __uri
Summary Pseudo-channel for static packages
Validation Package Name PEAR_Validate
Validation Package default
Version
SERVER CAPABILITIES
===================
TYPE VERSION/REST TYPE FUNCTION NAME/REST BASE
No supported protocols
除了展示__uri伪通道的品质外,它还告诉我们静态 tar 包软件包的验证与pear.php.net通道软件包的验证一样严格(使用PEAR_Validate)。如果您需要验证的灵活性,则有必要使用通道而不是将软件作为静态 tar 包分发。
然而,这也意味着在通过以下方式安装我们的静态软件包之后:
$ pear install http://www.example.com/tarballs/Package-1.0.0.tgz
如果有新版本发布,可以通过以下方式升级此软件包:
$ pear upgrade http://www.example.com/tarballs/Package-1.0.1.tgz
此外,还可以通过简单的以下方式卸载软件包:
$ pear uninstall __uri/Package
如果您想查看已安装的所有静态 tar 包软件包的列表,只需运行带有-c 选项的list命令:
$ pear list -c __uri
小心静态 tar 包的简单性!如果您的最终用户有可能从任何其他来源安装静态 tar 包,您必须通过通道分发您的软件包。否则,用户可能会遇到两个不同软件包之间的名称冲突,如下两个假设的安装命令所示:
$ pear install http://www.example.com/tarballs/Foo-1.0.0.tgz
$ pear upgrade http://www.notexample.com/Foo-1.2.3.tgz
在这种情况下,从www.example.com/tarballs分发的名为Foo的软件包与从www.notexample.com分发的名为Foo的软件包不是同一个代码库,但 PEAR 安装程序将它们都当作您输入了以下内容:
$ pear install __uri/Foo
$ pear upgrade __uri/Foo
在这种情况下,微妙到严重的破坏可能性立即存在,并且难以调试。不要冒险;如果有可能发生这种情况,请使用通道。
发布等于上传
如前所述,使用静态 tarball 而不是通道的最大优点是,发布一个新的软件包只需上传由以下命令创建的.tgz和.tar文件:
$ pear package
$ pear package -Z
没有什么比这更容易了!
远程安装固有的安全问题
如 phpBB、MySpace.com 和 XML_RPC 中漏洞被互联网蠕虫利用的出现所证明的那样,安全漏洞可不是什么笑料,你必须意识到安装你自己没有编写的软件所涉及到的潜在风险。
幸运的是,PEAR 安装器模型提供了升级以获取所需的安全修复的便利性,并提供了内在的安全性,以确保你不会仅仅因为使用 PEAR 安装程序而成为恶意黑客的受害者。
虽然 PEAR 开发者已经采取了所有步骤来确保你的代码的安全性而不限制其有用性,但了解一些安全性的基础知识仍然非常重要,因为 PEAR 无法保护你自己,如果你选择使用或编写不安全的代码。最近有几篇关于 PHP 安全和互联网安全的一般性的优秀参考资料。如果你不熟悉诸如输出转义、输入过滤或 XSS、任意代码执行、隐蔽性安全等概念,Ilia Alshanetsky 的php|architect's Guide to PHP Security和 Chris Shiflett 的Essential PHP Security指南都是很好的起点。
小贴士
许多开发者错误地遵循旧的金科玉律:“己所不欲,勿施于人。”当你开发具有互联网连接组件的代码时,这种方法是致命的。如果除了你之外的其他人可以访问你编写的 PHP 应用程序,那么你需要假设他们只有最恶劣的意图。
当你设计一个功能时,要自问:“我如何利用这个功能来修改它运行的环境?我能用它执行意外的操作吗?”
如果答案是“是”或甚至“可能”,那么这个功能本质上是不安全的,必须限制它,直到答案是“否”或“只有在极端情况下,其他安全措施才能使其失效”。
PEAR 安装程序和 Chiara_PEAR_Server 如何提供安全性?
PEAR 安装程序已经采取了许多重要步骤来提供安全性。然而,从根本上说,PEAR 安装程序最终是为了安装任意 PHP 代码——这是它的存在理由,因此,使用 PEAR 的安全性第一条必须是:
小贴士
永远不要在没有先查看在开发服务器上提供的代码之前,在一个正在运行的、生产性的网站上安装一个软件包。
PEAR 提供的额外安全性
虽然 PEAR 安装程序所采取的大多数基本操作都有合理的预期安全性,但有一些应该避免。
这意味着,例如,在生产服务器上应该不惜一切代价避免使用 upgrade-all 命令。这个命令将所有现有包升级到最新版本,这本质上消除了你控制升级的能力。这个命令最好在开发服务器上使用,在升级生产服务器上的单个包之前测试包的新版本。
此外,如果你正在安装由 php.net(在撰写本章时为 pear.php.net 和 pecl.php.net)以外的渠道分发的包,首先下载该包,并在包上运行两个命令来了解更多信息:
$ pear info Packagename-1.0.0.tgz
$ pear list-files Packagename-1.0.0.tgz
第一个会告诉你包的依赖项。如果你不认识这些依赖项,那么你将需要对他们执行相同的步骤。
list-files 命令列出了存档中的所有文件。寻找安装到 PEAR/ 子目录中的文件。除非包提供自定义文件角色、自定义文件任务或自定义命令,否则包通常没有理由将文件安装到这个位置,因为这个目录是由 PEAR 安装器使用的。安装到这个目录中的任何文件都可能试图恶意影响安装器的工作方式。
这条规则的明显例外是名称中包含 PEAR 的包,例如 PEAR_PackageFileManager。如果一个包将文件安装到似乎与声明的目的没有太多关系的位置,你应该立即感到怀疑。联系包维护者,询问为什么包需要在那里安装文件。
如果你没有得到满意的答复,立即在 <pear-dev@lists.php.net> 上通知 PEAR 开发者的邮件列表。任何试图提供恶意软件包的渠道都将被 PEAR 安装器列入黑名单。
更重要的是,如果你从这本书中只记住一件事,那就让它成为这件事:
小贴士
永远不要在没有查看安装后脚本的 PHP 源代码的情况下运行安装后脚本。
安装后脚本按定义是任意 PHP 代码。任何可以在 PHP 中完成的事情都可以通过安装后脚本完成。这包括像安装间谍软件、擦除你的硬盘驱动器以及其他你可能在运行 pear run-scripts 命令时不想做的事情。为了不查看脚本而冒整个系统风险是永远不值得的。
最终,由于恶意软件包被追踪到源头的极端容易性,以及需要明确用户安装包的要求,通过渠道分发的恶意软件包被分发的可能性极低。除非你同时警惕地从未知来源安装包,以便在造成任何重大损害之前将其捕获,否则这两个因素将不足以阻止那些作恶者。
此外,您必须升级 PEAR 安装器到版本 1.4.3 或更高版本,并在安装器的新版本发布时持续升级。新版本中一定会解决错误、安全问题和小型修复。
在设计 PEAR 安装器和 Chiara_PEAR_Server 时应用的具体安全原则
在设计和实现最新版本的 PEAR 安装器时,主要关注的是防止对用户环境的意外修改。通过使安装器能够利用 pear.php.net/pecl.php.net 之外的来源获取软件包,并添加如安装后脚本、自定义文件角色和自定义文件任务等功能,存在一定的风险。每个新功能都在对合法活动的开放性和对危险活动的限制之间取得平衡。
例如,频道是通过其服务器名称定义的。这意味着您不能自动且秘密地更改由 pear.php.net 分发的软件包的来源。此外,当用户运行:
$ pear channel-update mychannel.example.com
PEAR 安装器试图检索 mychannel.example.com/channel.xml。一个聪明而邪恶的频道管理员实际上可以提供一个没有定义 mychannel.example.com 的 channel.xml 文件。这种恶作剧会被安装器立即检测到,并禁止。
添加频道镜像也引入了风险元素。通过在 channel.xml 中定义这些镜像,无法使安装器相信另一个频道是某个频道的合法镜像。
当从频道下载软件包时,会执行严格的 package.xml 验证。如果软件包是从 foo.example.com 频道下载的,而其 package.xml 声称来自 pear.php.net,PEAR 安装器将拒绝安装或升级该软件包,因为这将是明显的安全漏洞。
此外,如果请求的软件包名称与 package.xml 中的软件包名称不同,PEAR 安装器将拒绝安装或升级该软件包。否则,就有可能分发一个声称是 foo.example.com/Foo 实际上分发 pear.php.net/PEAR 的软件包。相同的机制防止了对恶意软件包的依赖。从频道/软件包下载的软件包必须是那个频道/软件包,仅此而已。对于静态 tarball 也是如此。一个依赖于使用静态 tarball 软件包依赖的软件包不能分发基于频道的软件包。
在 PEAR 安装器中实现的最可能危险的功能是安装后脚本和自定义文件任务。这两个功能在调用时都会自动执行任意代码。PEAR 通过使意外执行恶意代码变得极其困难来提供一层安全防护。用户必须:
-
明确安装恶意文件任务
-
明确安装使用恶意文件任务的软件包
对于安装后脚本,最终用户必须:
-
明确安装包含恶意后安装脚本的包
-
明确输入
pear run-scripts maliciouschannel/maliciouspackage以运行恶意包
这些额外步骤使得 PEAR 安装程序在无意中损害系统变得非常困难,同时也为执行不寻常情况提供了极高的可见性。
重要的是要注意,直到 PEAR 版本 1.4.3,PEAR 安装程序有两个安全漏洞。两者都需要用户安装一个公开分发的恶意包。这两个漏洞都是由命令模式的不当实现引起的。
命令模式是一种基于在特殊子目录中加载文件来提供扩展性的方法。PEAR 安装程序自 1.0 版本以来一直在使用这种模式来加载当你输入时显示的实际命令:
$ pear help
实现这种模式的文件位于 PEAR/Command/*.php(包括 PEAR/Command/Auth.php、PEAR/Command/Install.php 等),在 PEAR 版本 1.4.2 及更早版本中,这些文件在用户使用 pear 命令时会被加载。
这最终为恶意包提供了一个在不受控制的情况下执行任意 PHP 代码的途径。PEAR 1.4.3 及更高版本通过实现描述命令的 XML 格式来修复这个问题。除非用户明确请求除 help 之外的命令,否则不会加载任何实际的 PHP 代码。
命令模式也用于自定义文件角色,首次在 PEAR 1.4.x 版本中引入。每次执行 pear 命令时,都会加载所有 PEAR/Installer/Role/*.php 文件,以构建自定义配置变量的列表。用于修复命令中任意代码执行漏洞的相同解决方案也被用来修复这个漏洞。
摘要
本章向我们展示了频道的设计目的是为了使从任何位置安装包变得容易,但在过程中损害系统的难度很大,遵循一个基本的安全原则:总是让做事最容易的方式成为最安全的方式。
频道打破了 pear.php.net 对 PEAR 安装程序的垄断,使其面向整个互联网。通过你的频道分发的自定义构建的包甚至可以出售并提供给特定用户,同时与公开可用的开源包和平共处。
第六章。嵌入 PEAR 安装程序:设计自定义插件系统
到目前为止,使用pear和pecl命令以及现有的 PEAR 软件所能做的几乎所有事情都已经揭晓。在前一章中,我们学习了如何设置自定义 PEAR 频道,完成了掌握 PEAR 安装程序使用的任务。现在,既然我们可以征服 PEAR 的宇宙,让我们进一步看看,如何使用 PEAR 安装程序解决基于网络的框架的一些最常见问题。
在本章中,我们将为假博客应用程序MyBlog设计一个自定义插件系统。对于 MyBlog,我们将设计一个插件系统来管理模板,并使用 PEAR 安装程序来管理查询远程服务器模板的细节,处理版本之间的依赖关系,以及执行实际的安装过程。此外,它将使用远程服务器上 REST 的扩展来存储模板的缩略图,以帮助选择模板。当然,由于这是一个假博客,缩略图是猫的图片,但对于真实博客来说,它们将是模板的截图。
对于那些不想逐个字符输入的用户,MyBlog 的代码可以从 Packt 网站直接下载,或者通过直接使用 PEAR 安装程序(一种有趣的方式)进行安装,具体方法如下:
pear channel-discover pear.chiaraquartet.net
pear install MyBlog
pear run-scripts chiara/MyBlog
虽然有点丑陋(我从未声称自己是设计师),以下是我们的假 MyBlog 应用程序的管理页面,展示了从远程服务器拉取的图片和分页:

这是点击第二个模板进行安装后的安装过程截图:

为什么嵌入 PEAR?
在许多情况下,PHP 应用程序是像内容管理系统(CMS)或其他可扩展的自定义框架这样的网络应用程序。不可避免地,在某个时候,最终用户会想:“如果只需点击就能自动下载和安装我想要的功能,那不是很好吗?”
在此阶段,要求用户使用 PEAR 安装程序安装额外功能可能会更简单,但如果我们不希望我们的应用程序用户必须学习如何使用 PEAR 怎么办?下载您的 CMS 或框架(让我们称它为 XYZ 框架)的用户已经有很多关于框架设计、如何定制和使用它来创建内容要学习。他们最不想学习的是一些外部工具,只是为了安装一个特殊的投票插件或一个新的模板。
我们真正需要的是一个简单的 XYZ 框架管理部分的页面,允许用户浏览插件,并点击一个进行安装,无需担心细节。更好的是,允许高级用户自定义用于下载的远程服务器,无论是安装实验性插件,等等。
换句话说,我们需要将 PEAR 安装器直接嵌入到 XYZ 框架中,并使用它来处理查询远程服务器、检索插件以及执行实际的安装/升级过程。
简化安装的用户选择
嵌入 PEAR 安装器使得可以专注于应用程序的重要部分:易用性和为用户提供的特性。您最不希望的事情就是用户因为您在重造内部结构上浪费了太多时间,而没有足够的时间在视觉和逻辑布局上。
依赖 PEAR 安装器将为优化插件页面的外观和流程提供更多时间,并允许创建简单的“点击此处安装”链接,这些链接将正确下载适合当前 PHP 版本、框架版本和请求的稳定性级别的正确插件。
消除错误的可能性
总是避免重造轮子的冲动,这可以减少应用程序的复杂性,使得引入错误的几率大大降低,同时节省时间和未来的努力。可以信赖 PEAR 的健壮测试套件和庞大的用户群体,以确保 PEAR 安装器的稳定性,让您可以专注于自己的代码的健壮性。
其他插件系统
在深入之前,让我们看看其他打包插件的可能性。目前,主要 PHP 应用程序采用了三种模型:
-
直接在源代码中打包插件
-
使用子包的 PEAR 依赖
-
定制的远程插件服务器
直接在源代码中打包插件
在 PHP 世界中,最常用的解决方案是将插件直接打包在源代码中。这种方法,如前几章所探讨的,结果是可以创建更简单的代码,但维护起来却困难得多,尤其是升级,往往导致代码过时。正如最近在流行程序中发现的网络安全漏洞所证明的,保持代码更新和没有错误不仅对微小的烦恼很重要;它可能是一个安全和不安全应用程序之间的区别。
虽然这是最常见的选择,但将插件直接打包在源代码中并不是一个非常灵活的想法,所以我们不会深入探讨这个想法。
子包 - PEAR 依赖
使用 PEAR 安装程序的依赖关系管理功能是捆绑插件的其他方法,这种方法在 pear.php.net 上开始获得动力。这种方法最初是由我在维护 phpDocumentor 的工作中提出的。我注意到,在实现 phpDocumentor 的新模板或新转换器时,这通常意味着即使只是对模板或转换器进行了小的更改,也需要整个 phpDocumentor 包的点发布。虽然频繁发布并不一定是一件坏事,但提供良好的理由让用户升级是很重要的。在这种情况下,这意味着在包含小更改的发布和推迟发布之间做出选择。
此外,当引入了新的实验性转换器,如 PDF 转换器时,转换器的稳定性只能记录为不太稳定。用户经常对为什么一个不稳定的转换器会与稳定的 phpDocumentor 一起发布感到困惑。
所有这些问题都指向了需要一种更好的方式来处理应用程序的子部分。答案以子包的形式出现。子包是离散的 PEAR 包,它们定义了大型应用程序与较小应用程序部分之间的父子关系。
小贴士
子包是应用程序的一个自包含部分,没有父应用程序就无法工作;例如,MDB2_mysqli 子包属于 MDB2 包。如果安装了 MDB2_mysqli 包,MDB2 可以处理 MySQL 4.1 及以上版本的 mysqli 驱动程序连接。MDB2_mysqli 不能独立工作,需要 MDB2 才能发挥作用。因此,MDB2_mysqli 是 MDB2 的子包。
案例研究:MDB2
第一个利用这一想法的包是 MDB2 包 (pear.php.net/MDB2),由 Lukas Smith 和 Lorenzo Alberton 设计,旨在取代 Smith、Alberton 设计的 MDB 包,并受到 Manuel Lemos 创建的 Metabase 的启发。
MDB2 是一个数据库抽象层,其核心功能包含在基本的 MDB2 包中。每个特定的数据库都通过一个驱动包来访问。例如,mysqli 驱动是通过 MDB2_Driver_mysqli 包来访问的。与像 DB 这样的流行数据库抽象包不同,MDB2 已经将这些驱动程序分离出来,以便每个驱动程序可以单独维护。
每个驱动程序封装了一个数据库的功能,因此,子包是:
-
MDB2_Driver_mssql (
pear.php.net/MDB2_Driver_mssql) -
MDB2_Driver_sqlite (
pear.php.net/MDB2_Driver_sqlite) -
MDB2_Driver_querysim (
pear.php.net/MDB2_Driver_querysim) -
MDB2_Driver_pgsql (
pear.php.net/MDB2_Driver_pgsql) -
MDB2_Driver_oci8 (
pear.php.net/MDB2_Driver_oci8) -
MDB2_Driver_mysqli (
pear.php.net/MDB2_Driver_mysqli) -
MDB2_Driver_mysql (
pear.php.net/MDB2_Driver_mysql) -
MDB2_Driver_ibase (
pear.php.net/MDB2_Driver_ibase) -
MDB2_Driver_fbsql (
pear.php.net/MDB2_Driver_fbsql)
每个驱动程序都有自己的版本、稳定性和更重要的是,对所需数据库驱动程序的依赖。
DB 包之前使用的旧模型使得无法指定对数据库扩展的依赖。换句话说,要为每个驱动程序要求所需的扩展,就意味着对 mssql、sqlite、pgsql、oci8、mysqli、mysql、ibase 和 fbsql PHP 扩展的依赖。这不仅会强制在php.ini中加载不必要的数据库扩展,还可能导致数据库扩展之间的潜在冲突。
此外,如果引入了新的驱动程序,数据库包(稳定版)的稳定性会自动过滤到扩展。为了解决这个问题,DB 在文档中使用一个文本文件来描述每个驱动程序的稳定性,并使用表格来呈现。这些信息在安装时不会显示。如果为 MDB2 引入了新的驱动程序,即使 MDB2 是稳定的,它也可能具有devel或alpha的稳定性。还有益的是,可以独立于父 MDB2 包发布驱动程序的新版本。每次对 DB 驱动程序进行更改时,都必须发布包含所有其他驱动程序的整个 DB 包。
MDB2 方法有一些缺点。首先,为了安装 MDB2,需要两个步骤:
$ pear install MDB2
$ pear install MDB2_Driver_pgsql
这需要知道如何使用 pear 命令行以及驱动程序的名字。一个常见的错误(我自己也犯过)是输入:
$ pear install MDB2
$ pear install MDB2_pgsql
当然,这会导致一个非常不友好的错误信息:
$ pear install MDB2_pgsql
No releases available for package "pear.php.net/MDB2_pgsql"
Cannot initialize 'MDB2_pgsql', invalid or missing package file
Package "Mdb2_pgsql" is not valid
Install failed
此外,卸载也需要相同的两个步骤,或者在命令行上传递两个包:
$ pear uninstall MDB2 MDB2_Driver_pgsql
然而,当使用package.xml版本 2.0(截至本章编写时,MDB2 仍然使用原始的 1.0 版本实现)时,实现子包模型有更好的方法。如果 MDB2 为每个驱动程序定义安装组,这将使用户能够安装 MDB2 和适当的数据库。例如,考虑在package.xml中的这种方法:
<dependencies>
...
...
<group name="mssql" hint="Microsoft SQL Server driver">
<package>
<name>MDB2_Driver_mssql</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="sqlite" hint="SQLite driver">
<package>
<name>MDB2_Driver_sqlite</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="querysim" hint="Query Simulator driver">
<package>
<name>MDB2_Driver_querysim</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="pgsql" hint="Postgresql driver">
<package>
<name>MDB2_Driver_pgsql</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="oci8" hint="Oracle 8 driver">
<package>
<name>MDB2_Driver_oci8</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="mysqli" hint="MySQL 4.1+ driver">
<package>
<name>MDB2_Driver_mysqli</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="mysql" hint="MySQL 4.0- driver">
<package>
<name>MDB2_Driver_mysql</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="ibase" hint="Interbase driver">
<package>
<name>MDB2_Driver_ibase</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="fbsql" hint="Firebird driver">
<package>
<name>MDB2_Driver_fbsql</name>
<channel>pear.php.net</channel>
</package>
</group>
<group name="all" hint="all drivers [for uninstall]">
<package>
<name>MDB2_Driver_fbsql</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>MDB2_Driver_ibase</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>MDB2_Driver_mysql</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>MDB2_Driver_mysqli</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>MDB2_Driver_oci8</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>MDB2_Driver_pgsql</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>MDB2_Driver_querysim</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>MDB2_Driver_sqlite</name>
<channel>pear.php.net</channel>
</package>
<package>
<name>MDB2_Driver_mssql</name>
<channel>pear.php.net</channel>
</package>
</group>
</dependencies>
这个package.xml将为用户提供更多的灵活性。在安装时,用户会看到:
$ pear install MDB2
Install ok: channel://pear.php.net/MDB2-2.2.0
MDB2: Optional feature mssql available (Microsoft SQL Server driver)
MDB2: Optional feature sqlite available (SQLite driver)
MDB2: Optional feature querysim available (Query Simulator driver)
MDB2: Optional feature pgsql available (Postgresql driver)
MDB2: Optional feature oci8 available (Oracle 8 driver)
MDB2: Optional feature mysqli available (MySQL 4.1+ driver)
MDB2: Optional feature mysql available (MySQL 4.0- driver)
MDB2: Optional feature ibase available (Interbase driver)
MDB2: Optional feature fbsql available (Firebird driver)
MDB2: Optional feature all available (all drivers [for uninstall])
为了安装具有 mysqli 支持的 MDB2,用户只需输入:
$ pear install MDB2#mysqli
然后,将下载并安装 MDB2_Driver_mysqli 包。更好的是,如果在某个时间点需要卸载 MDB2,可以使用单个命令安装 MDB2 及其所有驱动程序:
$ pear uninstall MDB2#all
这种方法的缺点是,在安装或升级 MDB2 时输出的信息相当冗长,可能会使得难以注意到一个意外的错误或警告。此外,如果发布了新的驱动程序,必须将其添加到 package.xml 中,以便使便捷的功能可用。另一方面,这也可以是一个区分推荐和实验性驱动程序的好方法。
简而言之,这种方法非常适合探索。
定制插件系统:远程服务器
MDB2 使用的分包方法的缺点是,MDB2 的最终用户必须对 PEAR 安装程序有实际的理解,才能正确安装驱动程序。这对那些期望在一个地方管理并使用应用程序的图形化用户来说可能是一个重大的障碍。例如,博客作者期望能够专注于创作和与博客相关的任务。很少有博客作者愿意花时间去研究像 PEAR 安装程序这样的微妙而强大的安装系统的复杂性。
相反,他们希望能够自定义博客的外观和感觉,随意添加或删除博客的功能组件,并且这一切都可以从用于博客的相同视觉界面中完成。
如果你的应用程序适合于类似的一站式解决方案(并且大多数基于网络的程序都符合这种模式),你将希望考虑一种远程管理插件和/或模板的方法。为了做到这一点,你的应用程序需要具备三个抽象组件:
-
应用程序插件管理器
-
远程插件服务器
-
插件下载/安装器
如果你注意到了前四章的内容,你可能会注意到这种相似性与 PEAR 安装程序的目标非常相似。将一个 PEAR Channel Server(参见 第五章)。Serendipity 博客是一个易于安装、高度可配置的基于 PHP 的博客程序,它非常稳定且功能丰富。此外,它比大多数基于 PHP 的博客软件更早地完全支持 PHP 5 和最新的数据库扩展,并且最近刚刚达到了 1.0 版本的里程碑。Serendipity 是在 BSD 许可证下授权的。
案例研究:Serendipity 博客的 Spartacus 插件管理器
Serendipity 博客通过使用一个名为Spartacus的专业插件来管理插件。Spartacus 设计成与任何其他 Serendipity 插件以相同的方式工作,但具有查询受信任服务器中 Serendipity 博客的插件和模板列表的能力,并允许用户轻松下载和/或升级插件。

Serendipity 插件的架构由位于特殊位置的单一 PHP 文件组成。每个插件都位于一个单独的目录中,该目录可能包含多个附加文件。因此,Spartacus 插件由serendipity_event_spartacus.php文件和用于插件中使用的文本提示的不同语言翻译组成。
在serendipity_event_spartacus.php文件中,有一个包含多个不同方法的单个类。这些方法可以大致分为几个简单的类别:
-
如
microtime_float()之类的实用方法,它解决了 PHP 版本 5.0.0 之前 PHP 内部microtime()函数的不足。 -
通用插件方法,适用于每个插件,用于插件名称/作者、Serendipity 事件钩入、配置变量等的检查。
-
处理关于插件和模板的远程元数据的 XML 操作方法。
-
安装和卸载插件及模板的文件管理方法。
-
基于 PEAR 的HTTP_Request包的远程 HTTP 下载方法。
-
存储下载的插件信息的数据缓存方法。
-
构建插件列表的方法。
-
构建模板列表和下载预览缩略图的方法。
即使拥有所有这些功能,整个插件文件(包含注释)也有 805 行。
Spartacus 背后的基本设计是从受信任的服务器获取描述性 XML 文件(基本上是一个 REST 服务),从文件中解析有关插件或模板的信息,将其格式化为 Serendipity 显示插件所需的形式,然后从 Serendipity 的事件钩子处理用户请求,下载/安装插件和模板。
为了下载插件,Serendipity 使用了一个简单但不够灵活的系统,该系统基于静态 URL。构建插件元数据的 URL 的方式如下:
switch($type) {
// Sanitize to not fetch other URLs
default:
case 'event':
$url_type = 'event';
$i18n = true;
break;
case 'sidebar':
$url_type = 'sidebar';
$i18n = true;
break;
case 'template':
$url_type = 'template';
$i18n = false;
break;
}
if (!$i18n) {
$lang = '';
} elseif
(isset($serendipity['languages'][$serendipity['lang']])) {
$lang = '_' . $serendipity['lang'];
} else {
$lang = '_en';
}
$mirrors = $this->getMirrors('xml', true);
$mirror = $mirrors[$this->get_config('mirror_xml', 0)];
$url = $mirror . '/package_' . $url_type . $lang . '.xml';
然后,$url用于下载实际的元数据。
显示元数据需要检查插件是否已经安装:
if (in_array($data['class_name'], $plugins)) {
$infoplugin =&
serendipity_plugin_api::load_plugin($data['class_name']);
if (is_object($infoplugin)) {
$bag = new serendipity_property_bag;
$infoplugin->introspect($bag);
if ($bag->get('version') == $data['version']) {
$installable = false;
} elseif (version_compare($bag->get('version'),
$data['version'], '<')) {
$data['upgradable'] = true;
$data['upgrade_version'] = $data['version'];
$data['version'] = $bag->get('version');
$upgradeLink =
'&serendipity[spartacus_upgrade]=true';
}
}
}
注意突出显示的条目,这些条目显示了插件是否可以升级的基本测试。这用于决定插件是否可点击(可安装/升级)。
一旦用户决定下载插件,Spartacus 将遍历插件元数据中的文件列表,并使用静态 URL 和 ViewCVS 技巧(如下例所示)逐个下载它们。以下是代码:
foreach($files AS $file) {
$url = $mirror . '/' . $sfloc . '/' .
$file . '?rev=1.9999';
$target = $pdir . $file;
@mkdir($pdir . $plugin_to_install);
$this->fetchfile($url, $target);
if (!isset($baseDir)) {
$baseDirs = explode('/', $file);
$baseDir = $baseDirs[0];
}
}
这个 ViewCVS 技巧(检索修订版 1.9999)是一种非常简单的方法来检索要安装的正确版本的文件,但它并不提供太多的灵活性,并且需要在服务器上严格控制修订版本——一个错误就会使整个插件甚至我们个人 Serendipity 博客的本地安装崩溃。
因此,Serendipity 的开发者已在页面顶部放置了一个待办事项笔记,其中包含一些关于 Spartacus 下一个实现版本以解决这些问题的想法:
/************
TODO:
- Perform Serendipity version checks to only install plugins
available for version
- Allow fetching files from mirrors / different locations -
don't use ViewCVS hack (revision 1.999 dumbness)
***********/
这两个都是难以解决的问题。要解决第一个,我们需要能够进行一些相对复杂的依赖验证,并且有能力遍历插件的可用版本,直到找到一个可以与当前 Serendipity 版本一起工作的版本。第二个也需要一些复杂的服务器/客户端通信,并且可能会在 Spartacus 的开发过程中给 Serendipity 增加显著的冗余。
幸运的是,有一个解决方案。PEAR 安装器专门设计来处理比 Serendipity 的 Spartacus 插件试图解决的问题更为复杂的情况,并且可以以惊人的最小努力嵌入到应用程序中。
案例研究:Seagull 框架的嵌入式 PEAR 安装器
Seagull 框架([www.seagullproject.org/](http://www.seagullproject.org/))是一个基于网络的示例,它采取了极简的方法来嵌入
PEAR 安装器。与 Serendipity 不同,Seagull 更像是一个通用框架,旨在使构建其他事物变得容易。Seagull 提供了粘合剂以及许多方便的 buzzword-friendly 理念,以供您在开发中享受,例如在 PHP 中实现的软件模式(前端控制器、观察者、服务定位器、任务运行器、向导等),以及如 HtmlRenderer、UrlParser 和 Emailer.php 之类的实用类。快速浏览http://trac.seagullproject.org/browser/trunk/lib/SGL可以看到可用的全部功能范围。
与 phpDocumentor 一样,Seagull 也提供了一站式解压即用的 zip 文件,或者可以通过 PEAR 安装器进行安装。同样,与 phpDocumentor 一样,通过 PEAR 安装器安装或升级时,所需的配置较少。
此外,Seagull 的大部分工作基于从pear.php.net可获得的现有 PEAR 包的基础。Seagull 使用了一系列的 PEAR 包。以下是一些依赖性的样本:
-
归档 Tar
-
缓存 Lite
-
配置
-
日期
-
数据库
-
数据库数据对象
-
数据库嵌套集合
-
文件
-
HTML 公共部分
-
HTML 树菜单
-
HTML 快速表单
-
HTML 模板 Flexy
-
HTTP 头部
-
HTTP 下载
-
日志
-
邮件 MIME
-
网络套接字
-
网络用户代理检测
-
分页器
-
文本密码
-
翻译 2
-
验证
-
XML 解析器
-
XML 实用工具
哇!
此外,Seagull 利用来自 pearified.com 频道的 Role_Web 包,使得安装和升级变得一站式服务。
管理所有这些 PEAR 依赖项可能真的会让人头疼,尤其是当用户安装 Seagull 期望只使用 Seagull 时。当他们发现需要使用 PEAR 安装器来管理 Seagull 的依赖项时,这会使得问题更加复杂。
因此,Seagull 的创造者 Demian Turner 开发了第一个实验性的将 PEAR 安装器嵌入到 Seagull 中的实例。这种嵌入仅适用于 Seagull 的最新版本,并且被认为是 alpha 级代码质量,但非常值得研究其代码所采用的原理。以下是实际的 Seagull PEAR 管理器的截图:

在 modules/default/classes/PearMgr.php 文件中,Seagull 的 PEAR 管理器提供了一个定制的基于 Web 的前端,用于列出任何 PEAR 频道的包、安装和升级。
小贴士
PearMgr.php 的位置
Seagull 利用 package.xml 版本 2.0 的子包功能,将其庞大的代码库分为一个基础包和三个子包。PearMgr.php 文件位于 pear.phpkitchen.com/Seagull_Default 包中,该包在安装 Seagull 时默认需要和自动安装。从 SourceForge 安装源代码的用户将获得一个包含所有文件的单一压缩文件。
Seagull 使用了来自 pear.php.net 的 PEAR_Frontend_Web 包的定制版本(更改了一行,模板不同),以及 PearMgr.php 来管理 PEAR 的安装/升级。Seagull 还向 PEAR 安装器添加了七个命令:sgl-clear-cache, sgl-download, sgl-list-all, sgl-list-upgrades, sgl-remote-info 和 sgl-search。这些命令再次以 PEAR 等价物的几乎完全相同的方式实现,除了它们移除了所有对 outputData() 方法的调用,这一点我们将在稍后的文本中讨论。
PearMgr.php 中的代码大约有 200 行。因此,在 Serendipity 的 Spartacus 长度的四分之一内,Seagull 实现了一个功能齐全的远程插件安装器,也可以用来升级 Seagull 本身!
让我们看看用于确定哪些频道可以使用的代码:
$this->aChannels = array(
'pear.phpkitchen.com' => 'Seagull',
'pear.php.net' => 'PEAR',
'pearified.com' => 'Pearified',
);
与斯巴达克斯一样,服务器是硬编码的。然而,与斯巴达克斯不同,如果这些频道中的任何一个在未来任何时候添加一个镜像,PEAR 将会负责更新这些信息,并且镜像将自动对用户可用。
实际的包下载/安装/升级管理全部使用 PEAR 的抽象命令接口:
switch ($input->command) {
Seagull FrameworkPEAR's abstract command interfacecase 'sgl-list-all':
if ($serialized = $cache->get($cacheId, 'pear')) {
$data = unserialize($serialized);
SGL::logMessage('pear data from cache',
PEAR_LOG_DEBUG);
} else {
$cmd = PEAR_Command::factory($input->command,
$config);
$data = $cmd->run($input->command, $opts, $params);
$serialized = serialize($data);
$cache->save($serialized, $cacheId, 'pear');
SGL::logMessage('pear data from REST call',
PEAR_LOG_DEBUG);
}
break;
case 'sgl-install':
case 'sgl-uninstall':
case 'sgl-upgrade':
$params = array($input->pkg);
ob_start();
$cmd = PEAR_Command::factory($input->command, $config);
$ok = $cmd->run($input->command, $opts, $params);
$pearOutput = ob_get_contents();
ob_end_clean();
if ($ok) {
$this->_redirectToDefault($input, $output);
} else {
print '<pre>';print_r($ok);
}
break;
}
在示例中突出显示了 PEAR 特定的代码。列出包的整个复杂性仅用两行代码就解决了。安装、卸载和升级也是如此。
对于捕获 PEAR 命令的信息输出,存在一种相当奇怪的输出缓冲使用方式。这似乎是必要的,因为如果允许选择,PEAR 会输出信息到屏幕,但实际上可以在不使用黑客手段的情况下捕获这种输出。为了捍卫 Seagull 开发者,我无法想象比他们试图做的更前沿的代码了,而且他们没有这本书的帮助也做到了!
捕获 PEAR 安装过程输出的关键是注册一个前端对象。下面的代码展示了正确完成此类任务的一个示例:
require_once 'PEAR/Frontend.php';
class CaptureStuff extends PEAR_Frontend
{
public $data = array();
function log($msg)
{
$this->data[] = array('log' => $msg);
}
function outputData($data, $command = '_default')
{
$this->data[] = array('outputData' => array($data,
$command));
}
function userConfirm()
{
// needed to satisfy interface contract PHP4-style
}
}
$capture = new CaptureStuff;
PEAR_Frontend::setFrontendObject($capture);
// $config is a PEAR_Config object
$cmd = PEAR_Command::factory($command, $config);
$cmd->run($command, array(), $params);
到目前为止,$capture->data数组包含了所有通常由 PEAR 安装器按顺序显示的信息,并且可以被忽略或以适当的方式显示。
现在我们已经看到了嵌入 PEAR 安装器的示例,现在是时候展示我们的特性了:设计一个定制的基于 PEAR 通道的插件系统,充分利用 PEAR 安装器的可定制性。
设计基于自定义 PEAR 通道的插件系统
对于这个系统,我们将设计一个模板安装器,用于虚构的博客程序"MyBlog"。在我们查看代码之前,理解问题很重要。以下是 MyBlog 模板系统的要求:
-
模板需要与它们关联缩略图,用户可以使用这些缩略图来直观地预览模板。
-
模板必须列出而不需要滚动(分页结果)。
-
必须能够将新模板标记为实验性。
-
模板必须与它们打算使用的 MyBlog 版本相匹配。
-
任何不符合博客用户需求的模板都不应该在模板列表中显示,例如过于实验性或与当前版本的 MyBlog 不兼容的模板。
-
模板必须只能从一组离散的受信任服务器远程安装。
-
模板还应可以从本地下载的模板中安装。
-
插件管理器应在 PHP 5.0.0 及更高版本中工作。当然,PEAR 与 PHP 4.2.0 及更高版本兼容,但让我们假设 MyBlog 旨在利用 PHP 5 版本中的一些新特性。将此代码移植到与 PHP 4 兼容是一个简单的任务,它将涉及替换一些关键字,并使用 PEAR_Error 而不是异常;这是一个留给读者的练习。
在满足所有这些要求的情况下,我们将需要利用channel.xml指定自定义网络服务的能力,在这种情况下是一个额外的 REST 协议用于模板缩略图。此外,我们还需要实现一个自定义的远程模板列表,能够过滤掉与当前版本的 MyBlog 不兼容的模板,并且可以根据稳定性进行过滤。其余的要求可以通过现有的 PEAR 功能轻松处理。
在本节中,我们将实现 PEAR 特定的代码。因此,我们不会从 MyBlog 程序中制作出下一个酷炫的 CMS,也不会实现服务器端模板缩略图上传器,因为这是一个常见的任务,使用 PHP 实现起来非常简单。
为了实现所需的任务,我们将创建一个负责从远程服务器下载和处理 REST 的类,一个负责组织和分页模板的类,以及一个负责下载和安装模板的类。在这种情况下,一旦模板安装完成,就没有必要直接卸载,一旦理解了基本原理,实现这一点就相当简单。我们的模板切换器只需在必要时安装或升级。
重新使用现有功能
在这个例子中,我们将充分利用 PEAR_REST 类、PEAR_Downloader 和 PEAR_Installer 类,以及 PEAR_Config/PEAR_Registry 内部类。重用组件是这种方法的主要优势,当然也是 PEAR 存储库的主要目的。这一点的重要性不容忽视,对于减少初始开发时间和调试的痛苦都至关重要。
在重用代码时,重要的是要验证围绕代码的用户社区的稳定性和健壮性。当然,PEAR 安装程序有一个广泛的测试套件,以及一个庞大的用户基础和来自全球的众多经验丰富且投入的开发者核心,他们致力于维护和改进这个包。
让我们看看我们将使用的 PEAR 包中的类。
PEAR 安装程序基础设施:REST 和 PEAR 安装程序类
PEAR 安装程序主要由几个类组成。对于我们模板插件系统需要理解的是以下这些类:
-
PEAR_Config
-
PEAR_Dependency2
-
PEAR_REST
-
PEAR_REST_11
-
PEAR_REST_10
-
PEAR_Downloader
-
PEAR_Downloader_Package
-
PEAR_Installer
这些多功能类的复杂性可能相当令人畏惧;因此,让我们退一步,看看我们实际上需要从每个类中得到什么。
PEAR_Config
PEAR_Config 类用于管理 PEAR 安装程序的基本配置问题。因此,它被设计成可以轻松检索相关的内部包注册表、REST 对象以及我们实际上不需要关心的各种其他功能。
对于我们的目的,我们实际上只需要了解 PEAR_Config 对象在 PEAR 安装程序中使用时的三个要点:
-
PEAR_Config对象通常由安装程序作为单例使用。 -
get()方法用于检索配置值。 -
set()方法用于在内存中设置配置值,除非使用writeConfigFile()保存值,否则不会影响磁盘上的配置文件。
当 PEAR 安装程序初始化时,会调用PEAR_Config::singleton()方法,并使用默认值。如果这些值没有设置到正确的位置,那么我们的模板插件系统将保存模板到全局 PEAR 位置。在某些情况下,这很好,因为应用程序设计为在系统上下文中工作。
如果你的应用程序使用的是应用程序内部的插件本地安装,那么在执行安装任务之前,你需要设置所有配置值。Seagull 框架是一个需要设置配置值及其设置方法的优秀示例,这种方法是最佳实践。以下是具体方法:
$conf = &PEAR_Config::singleton();
$conf->set('default_channel', 'pear.php.net');
$conf->set('doc_dir', SGL_TMP_DIR);
$conf->set('php_dir', SGL_LIB_PEAR_DIR);
$conf->set('web_dir', SGL_WEB_ROOT);
$conf->set('cache_dir', SGL_TMP_DIR);
$conf->set('data_dir', SGL_TMP_DIR);
$conf->set('test_dir', SGL_TMP_DIR);
$conf->set('preferred_state', 'devel');
至少,你需要将所有_dir配置值(bin_dir, doc_dir, ext_dir, php_dir, cache_dir, data_dir, download_dir, temp_dir,以及从 PEAR 1.4.10 开始的test_dir)设置为有效值。preferred_state变量应该设置为stable,除非用户期望能够安装实验性模板,那么应该使用beta, alpha或devel之一。
我们还将使用低级注册表来确定模板包是否已经安装,类似于 Serendipity 的 Spartacus 插件所使用的方法:
$reg = $this->_config->getRegistry();
// default channel is set to the template channel
$existing = $reg->packageInfo($template, 'version',
$this->_config->get('default_channel'));
if (version_compare($existing, $version) === 0) {
// installed already
$this->log('Template set as active template');
return true;
}
在我们的示例中,我们检索当前安装的模板版本,并作为一个理智的检查,确保我们不是试图安装当前已安装的版本。
应该注意的是,可能需要允许用户强制重新安装。如果是这种情况,代码应该修改为,如果版本相同,将force选项传递给安装。关于安装选项的内容将在后面的PEAR_Installer部分进行讨论。
PEAR_Dependency2
PEAR_Dependency2类是一个简单的实用工具类,由 PEAR 安装程序用于验证package.xml依赖项与系统上的所有变量。这是一个低级类,实际上期望其输入是直接从package.xml中的未序列化 XML 依赖项。
换句话说,以下是你试图验证的依赖项:
<required>
<package>
<name>Blah</name>
<channel>foo.example.com</channel>
<min>1.2.3</min>
</package>
PEAR_Dependency2期望代表依赖的变量包含以下数组:
array(
'name' => 'Blah',
'channel' => 'foo.example.com',
'min' => '1.2.3');
对于我们的应用程序,我们对PEAR_Dependency2类的唯一用途是验证对父 MyBlog 应用程序的依赖。我们需要确保模板实际上与当前的 MyBlog 版本兼容。因此,我们唯一需要验证的依赖是 MyBlog 包上的所需包依赖。
这大大简化了 PEAR_Dependency2 的使用,我们只需要了解如何用它来验证包依赖。用于此任务的方法被称为 validatePackageDependency(),正如人们所期望的那样。该方法签名期望依赖数组如我们上面的示例,一个表示依赖是否必需或可选的布尔值,以及一个包含所有将在此次迭代中尝试安装的包的列表的数组。
第三个参数与我们的任务无关,因为一次只会安装一个模板,所以我们总是为这个参数传递一个空数组。
在 PHP 代码中,我们使用的签名将类似于:
$e = $d2->validatePackageDependency($pdep, true, array());
与 PEAR_Dependency2 的所有验证方法一样,validatePackageDependency() 返回 true、一个数组或一个 PEAR_Error 对象。在成功时返回 true,如果存在不使依赖无效的警告,则返回包含错误信息的数组,如果依赖验证失败,则返回一个 PEAR_Error 对象。
由于 PEAR 的错误处理允许注册回调,因此在调用依赖验证时禁用任何回调非常重要,因此我们的总代码如下:
PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
$e = $d2->validatePackageDependency($pdep, true, array());
PEAR::staticPopErrorHandling();
if (PEAR::isError($e)) {
// skip any template releases that cannot work
// with the current version of MyBlog
continue 2;
}
上述代码告诉 PEAR 简单地返回 PEAR_Error 对象,暂时忽略用户指定的任何特殊处理,尝试验证依赖,如果验证失败,则跳过此模板的发布。
我们如何从依赖信息中提取 $pdep 变量?答案是半直观的。正如上面的示例依赖被提取到一个与 XML 标签名匹配的数组中,其父标签也是如此。上述依赖的完整数组如下:
array(
'dependencies' => array(
'required' => array(
'package' => array(
'name' => 'Blah',
'channel' => 'foo.example.com',
'min' => '1.2.3'
)
)
)
);
注意,如果我们有多个必需的包依赖项,数组将看起来像:
array(
'dependencies' => array(
'required' => array(
'package' => array(
0 => array(
'name' => 'Blah',
'channel' => 'foo.example.com',
'min' => '1.2.3'),
1 => array(
'name' => 'Dep2',
'channel' => 'foo.example.com',
'min' => '1.2.3'),
)
)
)
);
在这种情况下,包标签实际上是一个数组的数组。我发现处理这个事实的最简单方法是将包元素始终转换为它自己的数组,如果它不包含数字索引。
这里是访问模板所需包依赖项的完整代码:
if (isset($dep['required']) && isset($dep['required']['package'])) {
if (!isset($dep['required']['package'][0])) {
$dep['required']['package'] =
array($dep['required']['package']);
}
foreach ($dep['required']['package'] as $pdep) {
if (!isset($pdep['channel'])) {
// skip uri-based dependencies
continue;
}
if ($pdep['name'] == 'MyBlog' &&
$pdep['channel'] == 'pear.chiaraquartet.net') {
PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
$e = $d2->validatePackageDependency($pdep, true,
array());
PEAR::staticPopErrorHandling();
if (PEAR::isError($e)) {
// skip any template releases that cannot work
// with the current version of MyBlog
continue 2;
}
}
}
}
注意,我们还可以通过检查依赖中是否存在 <channel> 标签来轻松跳过基于静态 URI 的依赖,并相应地跳过它们。
PEAR_REST 和 PEAR_REST_10/PEAR_REST_11
PEAR_REST类是一个用于下载、解析和缓存远程 REST 文件的通用工具类。它是 PEAR 曾经用于较老 XMLRPC 基于通道的 XML-RPC 类的 REST 等价物。我们将使用这个低级类来检索模板的 PNG 缩略图图像,因此需要了解retrieveCacheFirst()方法。此方法首先检查本地缓存,如果存在,则永远不会尝试查询远程服务器。如果文件从未下载过,它将查询服务器以检索它。作为其参数,它只需提供一个要下载的文件的完整 URL,并返回其内容作为字符串。
retrieveData()方法采取了不同的方法。如果缓存足够新(默认为 3600 秒),则不查询远程服务器而直接使用。在此之后,使用 HTTP 1.1 查询服务器,并使用 HTTP 缓存来确定是否需要下载。这可以显著减少带宽,因为只有在文件有变化时才会下载文件。
应当注意,XML 文件(由 Content-Type HTTP 头标识)将被retrieveData()和retrieveCacheFirst()自动解析成数组。
PEAR_REST_10和PEAR_REST_11实现了 REST1.0 和 REST1.1 PEAR REST 标准,如第五章所述。因此,这些方法实现了从原始 REST 数据中检索有用信息的方式,例如特定软件包的下载信息,或所有当前发布软件包的列表。为了我们的目的,我们需要实现 REST1.1 的listAll()方法的修改版,一个能够过滤掉不兼容模板和实验性模板的方法。为了实现这一点,我们将从PEAR_REST_11中剪切并粘贴代码,并使用它通过调整检查每个软件包的循环来实现我们的listTemplates()方法。
小贴士
从PEAR_REST_11剪切并粘贴可能看起来是不干净的方法,因为剪切和粘贴通常被纯粹主义者所不齿,但在此情况下,它展示了 REST 设计的一个重要原则。如果 PEAR 仍然使用 XML-RPC,实际上将无法实现我们想要的定制模板列表,因为一些必要的信息已经在服务器端被删除。
REST 模型使数据可用,并期望客户端对数据进行任何过滤。这实际上既更高效又更灵活,可以减少编程时间,并完全消除围绕系统设计时曾经必要的黑客手段。
是的,我们在剪切和粘贴,但代码正在利用一个定义良好、简单的远程 REST 资源系统,因此我们可以确信它将随着标准的演变而继续工作。
在我们的简单实现中,我们需要的基于 REST1.0 的PEAR_REST_10类的唯一方法是getDownloadURL()方法,以便实现我们的具有限制条件的定制模板下载。getDownloadURL()方法的 API 签名有些复杂,因此查看PEAR_REST_10类的实际代码和注释是有帮助的:
/**
* Retrieve information about a remote package to be downloaded
* from a REST server
*
* @param string $base The uri to prepend to all REST calls
* @param array $packageinfo an array of format:
* <pre>
* array(
* 'package' => 'packagename',
* 'channel' => 'channelname',
* ['state' => 'alpha' (or valid state),]
* -or-
* ['version' => '1.whatever']
* </pre>
* @param string $prefstate Current preferred_state config
* variable value
* @param bool $installed the installed version of this package
* to compare against
* @return array|false|PEAR_Error see {@link _returnDownloadURL()}
*/
function getDownloadURL($base, $packageinfo, $prefstate, $installed)
getDownloadURL()要么返回一个PEAR_Error,要么返回一个数组。如果包可以成功下载,数组将包含一个关联索引url。如果没有发布满足指定的条件,则url索引将不存在。因此,我们可以非常简单地使用getDownloadURL()方法:
$info = $this->getDownloadURL($this->_restBase,
array('channel' => $this->_channel,
'package' => $templateName,
'version' => $version),
$this->_config->get('preferred_state', null, $this->_channel),
$installed
);
if (PEAR::isError($info)) {
throw new MyBlog_Template_Exception($info->getMessage());
}
if (!isset($info['url'])) {
throw new MyBlog_Template_Exception('Template "' .
$templateName . '" cannot be installed');
}
if (!extension_loaded("zlib")) {
$ext = '.tar';
} else {
$ext = '.tgz';
}
return $info['url'] . $ext;
注意,url不包含文件扩展名,因为这由 zlib 扩展的存在来决定。如果我们有 zlib,那么我们可以下载压缩版本,节省时间和带宽。否则,将下载并安装未压缩的.tar文件。
这基本上是成功利用我们的远程服务器查询所需的所有代码!剩下的唯一任务是实际下载和安装。
PEAR_Downloader 和 PEAR_Downloader_Package
从 PEAR 安装程序的角度看,下载不仅仅是一个通过 HTTP 获取 URL 并本地保存其内容的简单任务。PEAR 安装程序智能地根据依赖关系和本地系统下载包的正确版本,并在某些情况下自动下载包依赖项。
PEAR Downloader机制理解三种不同的可安装包类型:
-
抽象包名,例如
pear install PEAR或pear install PEAR-beta -
绝对 URL,例如
pear installpear.example.com/Blah-1.2.3.tgz -
本地文件,例如
pear install /path/to/Blah-1.2.3.tgz
每个实例的处理方式都大不相同。本地包以简单的方式处理,正如人们所期望的那样。首先下载绝对 URL,然后以与本地包相同的方式处理。抽象包的处理方式则完全不同。安装程序在确保所有依赖项都已满足之前不会进行任何下载。这确保了如果复杂应用程序的依赖项未满足,就不会浪费时间和带宽下载大型包文件。
然而,在三种可下载包的每一种中,它们依赖项和包信息处理的方式是相同的。正因为如此,为了避免不必要的代码重复,三种可下载包被抽象为PEAR_Downloader_Package类。PEAR_Downloader和PEAR_Installer中的许多 API 函数期望或返回一个PEAR_Downloader_Package对象。
然而,首先要研究和调查的方法是 PEAR_Downloader 的 download() 方法。此方法期望输入一个简单的字符串数组,每个字符串代表三种可下载软件包形式之一。以下是一个演示所有三种形式的示例:
$downloader->download(array('PEAR-beta',
'http://pear.example.com/Blah-1.2.3.tgz',
'/path/to/Blah-1.2.3.tgz'));
可以将选项传递给 download() 方法,但必须在创建 PEAR_Downloader 对象时指定:
$ui = PEAR_Frontend::singleton();
$config = PEAR_Config::singleton();
$downloader = new PEAR_Downloader($ui, $config,
array('force' => true));
可用于 PEAR_Downloader 的选项包括 force, downloadonly, soft, offline, packagingroot, nodeps, pretend, ignore-errors, nocompress, alldeps, onlyreqdeps 和 installroot。换句话说,当执行 pear help install 时显示的所有选项都是可用的,加上内部选项 downloadonly,该选项由 pear download 命令使用。
在设计嵌入式 PEAR 安装程序时,可能感兴趣的主要选项如下:
-
force:这个选项强制安装,即使依赖项无效或软件包已安装。force选项的最佳用法是修复损坏的安装。 -
offline:这个选项阻止了尝试联系远程服务器,在安装设置中可能很有用。 -
nocompress:这个选项指示安装程序下载未压缩的.tar文件而不是.tgz文件,当 zlib 扩展不可用时很有用。 -
nodeps:这个选项阻止下载器尝试验证依赖项,或下载必需的软件包依赖项。 -
pretend:这个选项指示下载器仅获取所有将要下载的软件包的列表,并返回该列表,但不进行任何实际下载。这对于向用户显示需要安装或升级的软件包很有用。 -
alldeps/onlyreqdeps:这些选项指示下载器自动下载软件包依赖项。onlyreqdeps选项指示安装程序仅下载必需的依赖项,而alldeps则下载所有依赖项。onlyreqdeps在package.xml版本 2.0 中已过时,但对于仍在pear.php.net使用package.xml 1.0的许多 PEAR 软件包来说仍然很有用。
download() 方法可以返回一个 PEAR_Error 对象,或者一个 array()。现在事情变得有点棘手。只有在出现严重错误的情况下,例如无法访问本地软件包注册表或某些其他异常情况时,才会返回 PEAR_Error 对象。在其他情况下返回一个数组。
换句话说,如果由于每个传入的软件包都未通过依赖项验证而导致下载失败,将返回一个空数组而不是 PEAR_Error 对象。另一种思考方式是,PEAR_Downloader 足够智能,可以跳过失败的下载并继续进行成功的下载,而不是因为单个软件包依赖于 Gronk_Wzilnk 软件包且不可用而停止整个下载过程。
为了处理多个错误的情况,PEAR_Downloader 提供了一个 getErrorMsgs() 方法,在下载后应该始终进行检查。此方法实现了一个简单的包含多个错误消息的数组,这些错误消息是在高级 PEAR_ErrorStack 类之前出现的(并且顺便提一下,有助于激发其创建)。因此,用于下载的代码看起来应该像这样:
$ui = PEAR_Frontend::singleton();
$config = PEAR_Config::singleton();
$dl = new PEAR_Downloader($this, array('upgrade' => true),
$this->_config);
// download the actual URL to the template
$downloaded = $dl->download(array($info));
if (PEAR::isError($downloaded)) {
throw new MyBlog_Template_Exception($downloaded->getMessage());
}
$errors = $dl->getErrorMsgs();
if (count($errors)) {
$err = array();
foreach ($errors as $error) {
$err[] = $error;
}
if (!count($downloaded)) {
throw new MyBlog_Template_Exception('template "' .
$template . '" installation failed:<br />' .
implode('<br />', $err));
}
}
注意,在这个例子中,$info 变量应该包含要下载的模板的名称。
PEAR_Installer
最后,我们来到了 PEAR_Installer 类。这个类非常庞大,包含处理文件事务、基本安装、卸载的代码,总体上非常巨大。PEAR_Installer 也是 PEAR 包中最古老的类之一,尽管它在 PEAR 1.4.0 及更高版本中经历了一次严重的减肥手术,但未来版本还将进行进一步的精简。
提示
文件事务是 PEAR 安装程序的数据库事务等价物。在安装软件包时,PEAR 安装程序要么完全安装软件包,要么完全回滚安装。在升级时,PEAR 安装程序会备份前一个版本。如果升级过程中出现任何问题,将完全恢复前一个版本。这确保了如果出现错误,软件包不可能处于半安装的悬而未决状态。
在我们安装模板的探索中,需要了解的方法并不令人意外地被命名为 install()。由于向后兼容性,install() 方法在输入方面相当灵活。可以传递我们本应传递给 PEAR_Downloader->download() 的数组,但这会导致安装程序进行大量的猜测,以执行你应该告诉它执行的操作,并且已被弃用。更好的做法是传递 PEAR_Downloader->downloader() 返回的数组,并使用一些辅助方法准备安装:
// $templatePackage is the PEAR_Downloader_Package object
// we received from PEAR_Downloader->download()
// $template is the name of the template
$ui = PEAR_Frontend::singleton();
$installer = new PEAR_Installer($ui);
$packages = array($templatePackage);
// always upgrade
$installer->setOptions(array('upgrade' => true));
$installer->sortPackagesForInstall($packages);
PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
$err = $installer->setDownloadedPackages($packages);
if (PEAR::isError($err)) {
PEAR::staticPopErrorHandling();
throw new MyBlog_Template_Exception($err->getMessage());
}
// always upgrade
$info = $installer->install($templatePackage,
array('upgrade' => true));
PEAR::staticPopErrorHandling();
if (PEAR::isError($info)) {
throw new MyBlog_Template_Exception($info->getMessage());
}
if (is_array($info)) {
$this->log('Installation successful');
return true;
} else {
throw new MyBlog_Template_Exception('install of "' . $template .
'" failed');
}
上文中特别与安装相关的代码已被突出显示。再次强调,由于 PEAR_Installer 类是 PEAR 安装程序包中的一个较老的遗物,我们使用 PEAR_Downloader 可以执行的一些操作必须手动完成。例如,应该使用 setOptions() 方法设置选项。此外,应该使用 sortPackagesForInstall() 方法对软件包进行排序,以确保在安装之前先安装依赖项。由于软件包是通过 install() 方法逐个安装的,且不知道其他正在安装的软件包,因此这个系统也有助于确保软件包很少处于安装失败时的危险损坏状态。
在 install() 方法之前,应该将已经下载的软件包注册为已下载,这样预安装验证就只执行一次。这必须使用 setDownloadedPackages() 方法完成。
现在我们已经对 PEAR 安装器的内部有了实际的理解,让我们看看服务器端。
通过自定义信息扩展 REST
为了在服务器上实现缩略图,我们首先需要为服务器实现定制的 REST。让我们看看一个典型的 channel.xml:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<channel version="1.0"
xsi:schemaLocation="http://pear.php.net/dtd/channel-1.0
http://pear.php.net/dtd/channel-1.0.xsd">
<name>pear.chiaraquartet.net/template</name>
<summary>Template example for book</summary>
<suggestedalias>te</suggestedalias>
<servers>
<primary>
<rest>
<baseurl type="REST1.0">
http://pear.chiaraquartet.net/Chiara_PEAR_Server_REST/</baseurl>
<baseurl type="REST1.1">
http://pear.chiaraquartet.net/Chiara_PEAR_Server_REST/</baseurl>
</rest>
</primary>
</servers>
</channel>
如果我们要添加对我们自定义缩略图 REST 的支持,我们只需要添加另一个 <baseurl> 标签:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<channel version="1.0"
xsi:schemaLocation="http://pear.php.net/dtd/channel-1.0
http://pear.php.net/dtd/channel-1.0.xsd">
<name>pear.chiaraquartet.net/template</name>
<summary>Template example for book</summary>
<suggestedalias>te</suggestedalias>
<servers>
<primary>
<rest>
<baseurl type="REST1.0">
http://pear.chiaraquartet.net/template/Chiara_PEAR_Server_REST/
</baseurl>
<baseurl type="REST1.1">
http://pear.chiaraquartet.net/template/Chiara_PEAR_Server_REST/
</baseurl>
<baseurl type="MyBlogThumbnail1.0">
http://pear.chiaraquartet.net/template/thumbnails/
</baseurl>
</rest>
</primary>
</servers>
</channel>
在上一个示例中,新的 baseurl 被突出显示。这允许我们通过一行代码检索模板缩略图图像:
$thumbnail = $this->_rest->retrieveCacheFirst($this->_thumbnailBase .
$template . '/' . $version . 'thumbnail.png');
现在,我们终于有了足够的信息来实际实现我们的模板管理器!
设计轻量级安装插件:代码终于来了
对于我们的假 MyBlog,我们将使用这个目录结构:
MyBlog/
Template/
Exceptions.php
Fetcher.php
Interfaces.php
Lister.php
REST.php
Config.php
Main.php
admin.php
index.php
image.php
blogsetup.php
PEAR-嵌入发生在 Template/ 子目录中的文件中。博客通过 Config.php 和 Main.php 文件以抽象方式实现,实际的网页文件是 index.php、image.php 和 admin.php。我们目前不会关注假 MyBlog 博客的设计。如果你想要玩弄假 MyBlog,你可以按照以下步骤安装它:
$ pear channel-discover pearified.com
$ pear install pearified/Role_Web
$ pear run-scripts pearified/Role_Web
$ pear channel-discover pear.chiaraquartet.net
$ pear up chiara/MyBlog
$ pear run-scripts chiara/MyBlog
小贴士
来自 pear.chiaraquartet.net 的 MyBlog 包使用了一个在安装后完全没有提示用户的脚本。我们将在稍后更详细地介绍安装后脚本,因为它展示了 PEAR 多样性的另一面。
MyBlog_Template_IConfig 和 MyBlog_Template_Config
让我们深入探讨;首先,Interfaces.php:
<?php
interface MyBlog_Template_IConfig
{
function getTemplateChannel();
function getCurrentTemplate();
}
这已经很简单了!这允许灵活性,松散地将嵌入的 PEAR 与模板的配置耦合,并且始终是一个好主意。
接下来,让我们看看异常类,这是复杂性的另一个例子:
<?php
class MyBlog_Template_Exception extends Exception {}
?>
好吧,我知道你在想什么:这看起来并不复杂。实际上,你是对的。我只是在开玩笑。现在让我们来点真的;让我们从 MyBlog 包的配置类开始。getTemplateChannel() 和
getCurrentTemplate() 方法在我们的示例应用程序中只是硬编码的字符串,但让我们看看 getPearConfig() 方法:
/**
* Get a customized PEAR_Config object for our blog template system
* @return PEAR_Config
*/
function getPearConfig(){
static $done = false;
$config = PEAR_Config::singleton();
if ($done) {
return $config;
}
$config->set('php_dir', '@php-dir@' . DIRECTORY_SEPARATOR .
'MyBlog' . DIRECTORY_SEPARATOR . 'templates');
$config->set('data_dir', '@php-dir@' . DIRECTORY_SEPARATOR .
'MyBlog' . DIRECTORY_SEPARATOR . 'templates');
// restrict to the template channel
$config->set('default_channel', $this->getTemplateChannel());
return $config;
}
就像 Seagull 的包一样,我们获取 PEAR_Config 单例对象并对其进行定制。然而,我们的博客模板将只使用 php 或数据角色,并且我们正在安装到内部 PEAR 仓库中,因此我们可以利用 PEAR 的替换任务(参见第三章以刷新替换任务)来替换 @php-dir@ 为本地计算机上 php_dir 配置变量的值。
从本质上讲,这指示 PEAR 将模板安装到 @php-dir@/MyBlog/templates/,这正是我们希望它们所在的地方。
MyBlog_Template_REST
接下来,让我们跳入 REST 类。这个类只需要一个 PEAR_Config 对象来开始,并且像这样在 admin.php 中实例化:
$conf = new MyBlog_Config;
$config = $conf->getPearConfig();
$rest = new MyBlog_Template_REST($config, array());
再次强调使用的简便性。让我们看看完整的代码:
<?php
/**
* MyBlog_Template_REST
*
* PHP version 5
*
* @package MyBlog
* @author Greg Beaver <cellog@php.net>
* @copyright 2006 Gregory Beaver
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @version CVS: $Id$
* @link http://pear.chiaraquartet.net/index.php?package=MyBlog
* @since File available since Release 0.1.0
*/
/**
* Helper files from PEAR and our template system
*/
require_once 'PEAR/REST/11.php';
require_once 'PEAR/REST/10.php';
require_once 'PEAR/Dependency2.php';
require_once 'MyBlog/Template/Exceptions.php';
/**
* Perform needed remote server REST actions.
*
* This class implements multiple inheritance through the
* use of magic functions, and extends both PEAR_REST_11 and
* PEAR_REST_10, giving preference to PEAR_REST_11 methods.
*
* The class provides modified listAll in the listTemplates() method,
* and a way to retrieve a template thumbnail image with
* getThumbnail().
*/
class MyBlog_Template_REST extends PEAR_REST_11
{
private $_config;
private $_rest10;
private $_restBase;
private $_thumbnailBase;
private $_channel;
function __construct(PEAR_Config $config, $options = array())
{
parent::PEAR_REST_11($config, $options);
$this->_config = $config;
$this->_rest10 = new PEAR_REST_10($config, $options);
}
/**
* Implement multiple inheritance of REST_10 and REST_11
*
* @param string $func
* @param array $params
* @return mixed
*/
function __call($func, $params)
{
if (method_exists($this->_rest10, $func)) {
return call_user_func_array(array($this->_rest10, $func),
$params);
}
}
/**
* Retrieve the web location of a template's thumbnail image
*
* @param string $base URL to template REST as defined in
* channel.xml
* @param string $template Template name (package name on the
* template server)
* @param string $version Template version
*/
function getThumbnail($template, $version)
{
return $this->_rest->retrieveCacheFirst($this->_thumbnailBase
. $template . '/' . $version . 'thumbnail.png');
}
/**
* Retrieve the Base URL for a channel's template REST
*
* @param string $channel
* @return string
* @throws MyBlog_Template_Exception
*/
function getRESTBase($channel)
{
$reg = $this->_config->getRegistry();
if (PEAR::isError($reg)) {
throw new MyBlog_Template_Exception('Cannot initialize
registry: ' . $reg->getMessage());
}
$chan = $reg->getChannel($channel);
if (PEAR::isError($chan)) {
throw new MyBlog_Template_Exception('Cannot retrieve
channel: ' . $chan->getMessage());
}
if
($chan->supportsREST($this->_config->get('preferred_mirror',
null, $channel)) &&
$base = $chan->getBaseURL('MyBlogThumbnail1.0',
$this->_config->get('preferred_mirror', null,
$channel))) {
$this->_thumbnailBase = $base;
return $chan->getBaseURL('REST1.1',
$this->_config->get('preferred_mirror', null,
$channel));
}
throw new MyBlog_Template_Exception('Unable to retrieve
MyBlogThumbnail1.0 base URL for channel ' . $channel);
}
/**
* Set the channel that will be used for the template locating
*
* @param string $channel
*/
function setTemplateChannel($channel)
{
$this->_channel = $channel;
$this->_restBase = $this->getRESTBase($channel);
}
/**
* Retrieve information about all templates
*
* This code demonstrates the power of REST. The
* REST information retrieved is in fact the same
* information used by the list-all and remote-list
* commands. However, the list-all/remote-list commands
* do not return dependency and release information.
*
* This function uses dependency/release information to strip
* away templates that are not compatible with the current
* MyBlog version, or are not stable enough.
* @param string $base
* @return array
*/
function listTemplates()
{
$d2 = new PEAR_Dependency2($this->_config, array(),
array('package' => '', 'channel' => ''));
$packagesinfo = $this->_rest->retrieveData($this->_restBase .
'c/Templates/packagesinfo.xml');
if (PEAR::isError($packagesinfo)) {
return;
}
if (!is_array($packagesinfo) || !isset($packagesinfo['pi']))
{
return;
}
if (!is_array($packagesinfo['pi']) ||
!isset($packagesinfo['pi'][0])) {
$packagesinfo['pi'] = array($packagesinfo['pi']);
}
$ret = array();
$preferred_state = $this->_config->get('preferred_state',
null, $this->_channel);
// calculate the set of possible states sorted
// from most stable -> least stable
$allowed_states =
array_flip($this->betterStates($preferred_state, true));
foreach ($packagesinfo['pi'] as $packageinfo) {
$info = $packageinfo['p'];
$package = $info['n'];
$releases = isset($packageinfo['a']) ?
$packageinfo['a'] : false;
$deps = isset($packageinfo['deps']) ?
$packageinfo['deps'] : array('b:0;');
$version_numbers = array(
'latest' => false,
'stable' => false,
'beta' => false,
'alpha' => false,
'devel' => false,
);
if ($releases) {
if (!isset($releases['r'][0])) {
$releases['r'] = array($releases['r']);
}
if (!isset($deps[0])) {
$deps = array($deps);
}
foreach ($releases['r'] as $i => $release) {
$dep = unserialize($deps[$i]['d']);
if (isset($dep['required']) &&
isset($dep['required']['package'])) {
if (!isset($dep['required']['package'][0])) {
$dep['required']['package'] =
array($dep['required']['package']);
}
foreach ($dep['required']['package'] as
$pdep) {
if (!isset($pdep['channel'])) {
// skip uri-based dependencies
continue;
}
if ($pdep['name'] == 'MyBlog' &&
$pdep['channel'] ==
'pear.chiaraquartet.net') {
PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
$e = $d2->validatePackageDependency($pdep, true,
array());
PEAR::staticPopErrorHandling();
if (PEAR::isError($e)) {
// skip any template releases that cannot work
// with the current version of MyBlog
continue 2;
}
}
}
}
// skip releases that are not stable enough
if (!isset($allowed_states[$release['s']])) {
continue;
}
if (!$version_numbers['latest']) {
$version_numbers['latest'] = $release['v'];
}
if (!$version_numbers[$release['s']]) {
$version_numbers[$release['s']] =
$release['v'];
}
}
}
if (!$version_numbers['latest']) {
// no valid releases found, so don't list this
// template
continue;
}
$ret[$package] = array('versions' => $version_numbers,
'info' => $info);
}
return $ret;
}
/**
* Retrieve the download URL for a template
*
* @param string $templateName template package name to download
* @param string $version template version to download
* @throws MyBlog_Template_Exception
* @return string
*/
function getTemplateDownloadURL($templateName, $version)
{
$reg = $this->_config->getRegistry();
if (PEAR::isError($reg)) {
throw new MyBlog_Template_Exception($reg->getMessage());
}
$installed = $reg->packageInfo($templateName, 'version', $this->_channel);
if ($version === $installed) {
throw new MyBlog_Template_Exception('template version "'
. $version . '" is already installed');
}
MyBlogMyBlog_Template_REST$info = $this->getDownloadURL($this->_restBase,
array('channel' => $this->_channel,
'package' => $templateName,
'version' => $version),
$this->_config->get('preferred_state', null,
$this->_channel),
$installed);
if (PEAR::isError($info)) {
throw new MyBlog_Template_Exception($info->getMessage());
}
if (!isset($info['url'])) {
throw new MyBlog_Template_Exception('Template "' .
$templateName . '" cannot be installed');
}
if (!extension_loaded("zlib")) {
$ext = '.tar';
} else {
$ext = '.tgz';
}
return $info['url'] . $ext;
}
}
?>
MyBlog_Template_Lister
接下来,让我们看看模板列表器。模板列表器类,恰当地命名为 MyBlog_Template_Lister,通过以下简单代码实例化:
require_once 'MyBlog/Template/Lister.php';
require_once 'MyBlog/Config.php';
$blog_config = new MyBlog_Config;
$lister = new MyBlog_Template_Lister($blog_config->getPearConfig());
$lister->setConfigObject($blog_config);
主要的方法是 listRemoteTemplates(),调用方式如下:
list($info, $pager) = $lister->listRemoteTemplates(1);
传递的唯一参数是每页包含的模板数量。我们传递 1,因为有两个用于假 MyBlog 的示例模板可供安装。返回值是一个简单的数组,第一个元素是分页的数据数组,第二个是分页器对象。
这是一个代码重用的好例子。当我最初开始设计代码时,我认为编写自己的分页器最有意义,因为它看起来只需要 10 行代码。然而,当我开始进一步参与实现时,复杂性迅速失控,我很快转向使用来自 pear.php.net 的分页器包(pear.php.net/Pager)。这个精心设计的包也具有良好的文档,实现分页功能只需要 10 分钟。
从上面的示例用法中,$info 变量是我们应用程序中的一个数组,其格式是一个简单的数字索引的数组。
array(
array(
'name' => 'example1',
'version' => '1.0.0',
'summary' => 'sample template 1'
),
array(
'name' => 'example2',
'version' => '1.0.0',
'summary' => 'sample template 2'
)
);
这可以很容易地迭代以创建模板列表。到这一点,我们应该准备好查看整个 MyBlog_Template_Lister 类文件 Lister.php:
<?php
/**
* MyBlog_Template_Lister
*
* PHP version 5
*
* @package MyBlog
* @author Greg Beaver <cellog@php.net>
* @copyright 2006 Gregory Beaver
* @license http://www.opensource.org/licenses/bsd-license.php BSD
* License
* @version CVS: $Id$
* @link http://pear.chiaraquartet.net/index.php?package=MyBlog
* @since File available since Release 0.1.0
*/
/**
* Helper files from PEAR and our template system
*/
require_once 'PEAR/Config.php';
require_once 'Pager/Pager.php';
require_once 'MyBlog/Template/Interfaces.php';
require_once 'MyBlog/Template/REST.php';
/**
* List local and remote templates, also the currently active
* template.
* @package MyBlog
* @author Greg Beaver <cellog@php.net>
* @copyright 2006 Gregory Beaver
* @license http://www.opensource.org/licenses/bsd-license.php BSD
* License
* @version @package_version@
* @link http://pear.chiaraquartet.net/index.php?package=MyBlog
*/
class MyBlog_Template_Lister
{
/**
* Template Configuration object
*
* This is used to grab configuration information for
* the current setup
* @var Template_IConfig
*/
private $_templateConfig;
/**
* PEAR configuration object
*
* @var PEAR_Config
*/
private $_pearConfig;
/**
* Current template channel
*
* @var string
*/
private $_templateChannel;
/**
* Current template name
*
* @var string
*/
private $_currentTemplate;
/**
* Template REST object
*
* @var MyBlog_Template_REST
*/
private $_rest;
/**
* @param PEAR_Config $config
*/
function __construct(PEAR_Config $config = null)
{
if ($config === null) {
$config = PEAR_Config::singleton();
}
$this->_pearConfig = $config;
$this->_rest = new MyBlog_Template_REST($config, array());
}
/**
* Set our channel for retrieving templates
* @param string $channel
* @throws MyBlog_Template_Exception
*/
function setTemplateChannel($channel)
{
$reg = $this->_pearConfig->getRegistry();
if (PEAR::isError($reg)) {
throw new MyBlog_Template_Exception('Unable to initialize
Registry: ' . $reg->getMessage());
}
if (!$reg->channelExists($channel)) {
throw new MyBlog_Template_Exception('Channel "' .
$channel . '" is unknown');
}
// translate alias into actual channel name
$channel = $reg->channelName($channel);
$this->_templateChannel = $channel;
$this->_rest->setTemplateChannel($channel);
}
/**
* set the name of the current template package
* @param string $template
*/
function setCurrentTemplate($template)
{
$this->_currentTemplate = $template;
}
/**
* Set up the current template configuration, and
* extract the channel and current template name.
*
* @param Template_IConfig $config
*/
function setConfigObject(MyBlog_Template_IConfig $config)
{
$this->_templateConfig = $config;
$this->setTemplateChannel($config->getTemplateChannel());
$this->setCurrentTemplate($config->getCurrentTemplate());
}
/**
* Retrieve a listing of templates
*
* This method paginates the data, and prepares it for display by
* the view portion of our template lister.
* @param int $pageNumber Page number to retrieve
* @param int $templatesPerPage number of templates to display
* per-page
* @return array
* @throws MyBlog_Template_Exception indirectly, from internal
* REST calls
*/
function listRemoteTemplates($templatesPerPage = 15)
{
$info = $this->_rest->listTemplates();
if ($info === null || PEAR::isError($info)) {
return array();
}
$params = array(
'mode' => 'Jumping',
'perPage' => $templatesPerPage,
'delta' => 2,
'itemData' => $info);
$pager = Pager::factory($params);
$ret = array();
$data = $pager->getPageData();
foreach ($data as $template => $info) {
$ret[] = array(
'name' => $template,
'version' => $info['versions']['latest'],
'summary' => $info['info']['s']);
}
return array($ret, $pager);
}
MyBlogMyBlog_Template_Lister}
?>
MyBlog_Template_Fetcher
最后,让我们看看安装管理器类,我们将称之为 MyBlog_Template_Fetcher。这个类也是简单地实例化,但比其他类更复杂一些:
require_once 'MyBlog/Template/Fetcher.php';
require_once 'MyBlog/Template/REST.php';
$conf = new MyBlog_Config;
$config = $conf->getPearConfig();
$rest = new MyBlog_Template_REST($config, array());
$rest->setTemplateChannel($conf->getTemplateChannel());
$fetch = MyBlog_Template_Fetcher::factory($rest, $config);
MyBlog_Template_Fetcher 通过工厂方法实例化,因为它必须注册为 PEAR_Frontend 对象,以便 PEAR 安装程序可以使用它来显示输出(如前所述的 Seagull 框架部分)。
我们将使用的主要方法是 installTemplate(),其用法如下:
try {
$fetch->installTemplate($_GET['t'], $_GET['v']);
$out = '';
foreach ($fetch->log as $info) {
if ($info[0] == 'log') {
$out .= ' ' . htmlspecialchars($info[1]) .
'<br />';
} else {
$out .= htmlspecialchars($info[1]) . '<br />';
}
}
// this is safe because installTemplate throws an exception
// if the template or version are not valid PEAR package/version
// so input is validated by this point
$_SESSION['template'] = $_GET['t'];
define('MYBLOG_OUTPUT_INFO', $out);
} catch (MyBlog_Template_Exception $e) {
define('MYBLOG_OUTPUT_INFO', '<strong>ERROR:</strong> ' .
$e->getMessage());
}
由于 MyBlog_Template_Fetcher 类模仿 PEAR Frontend,我们需要定义三个方法,log()、outputData() 和 userConfirm()。前两个方法只是将它们的输入存储在一个内部数组中以便稍后显示,最后一个是一个占位方法,在我们的示例应用程序中不会使用。
最后,这是类列表:
<?php
/**
* MyBlog_Template_Fetcher
*
* PHP version 5
*
* @package MyBlog
* @author Greg Beaver <cellog@php.net>
* @copyright 2006 Gregory Beaver
* @license http://www.opensource.org/licenses/bsd-license.php BSD
* License
* @version CVS: $Id$
* @link http://pear.chiaraquartet.net/index.php?package=MyBlog
* @since File available since Release 0.1.0
*/
/**
* Helper files from PEAR and our template system
*/
require_once 'MyBlog/Template/REST.php';
require_once 'MyBlog/Template/Exceptions.php';
require_once 'PEAR/Frontend.php';
require_once 'PEAR/Downloader.php';
require_once 'PEAR/Installer.php';
require_once 'PEAR/Config.php';
require_once 'PEAR/Downloader/Package.php';
/**
* Control installation/upgrade of MyBlog templates
*
* This class makes full use of internal PEAR classes to
* download and install/upgrade templates. To simplify
* things, the class extends PEAR_Frontend and stores output
* from installation directly in the class, which can then
* be retrieved for proper formatting and display to the user
* by the MyBlog application.
*
* This class should be instantiated using the factory method as in:
* <code>
* $fetch = MyBlog_Template_Fetcher::factory($rest, $config);
* </code>
* @package MyBlog
* @author Greg Beaver <cellog@php.net>
* @copyright 2006 Gregory Beaver
* @license http://www.opensource.org/licenses/bsd-license.php BSD
* License
* @version @package_version@
* @link http://pear.chiaraquartet.net/index.php?package=MyBlog
*/
class MyBlog_Template_Fetcher extends PEAR_Frontend
{
/**
* @var Template_Fetcher_REST
*/
private $_rest;
/**
* @var PEAR_Config
*/
private $_config;
/**
* log messages from installation are stored here
*
* @var array
*/
public $log = array();
private function __construct(MyBlog_Template_REST $rest,
PEAR_Config $config)
{
$this->_config = $config;
$this->_rest = $rest;
}
/**
* Create a new MyBlog_Template_Fetcher object, and register it
* as the global frontend for PEAR as well
*
* @param MyBlog_Template_REST $rest
* @param PEAR_Config $config
* @return MyBlog_Template_Fetcher
*/
static function factory(MyBlog_Template_REST $rest,
PEAR_Config $config){
$a = new MyBlog_Template_Fetcher($rest, $config);
// configure this as the frontend for all installation
// processes
PEAR_Frontend::setFrontendObject($a);
return $a;
}
/**
* Record a message logged while installing
*
* This can be used later to display information on the
* template install/download
* process
* @param string $msg
*/
function log($msg){
$this->log[] = array('log', $msg);
}
/**
* Dummy function required to be a valid UI
*
* @return boolean
*/
function userConfirm(){
return true;
}
/**
* Record a message logged while installing
*
* This can be used later to display information on the
* template install/download
* process
* @param string $msg
*/
function outputData($msg, $command){
$this->log[] = array('out', $msg);
}
/**
* Given a template package name, download and install a template
*
* @param string $templatePath template package name
* @param string $version template package version to install
* @throws MyBlog_Template_Exception
*/
function installTemplate($template, $version){
// first, validate input
if (!preg_match(PEAR_COMMON_PACKAGE_NAME_PREG, $template)) {
throw new MyBlog_Template_Exception('SECURITY ALERT:
template is not ' . 'a valid package name, aborting');
}
if (!preg_match(PEAR_COMMON_PACKAGE_VERSION_PREG, $version))
{
throw new MyBlog_Template_Exception('SECURITY ALERT:
template version ' . 'is not a valid version, aborting');
}
$reg = $this->_config->getRegistry();
// default channel is set to the template channel
$existing = $reg->packageInfo($template, 'version',
$this->_config->get('default_channel'));
if (version_compare($existing, $version) === 0) {
// installed already
$this->log('Template set as active template');
return true;
}
// convert the template package into a discrete download URL
$info = $this->_rest->getTemplateDownloadURL($template,
$version);
if (PEAR::isError($info)) {
throw new MyBlog_Template_Exception($info->getMessage());
}
// download the template and install
// (use PEAR_Downloader/Installer)
$dl = new PEAR_Downloader($this, array('upgrade' => true),
$this->_config);
// download the actual URL to the template
$downloaded = $dl->download(array($info));
if (PEAR::isError($downloaded)) {
throw new MyBlog_Template_Exception
($downloaded->getMessage());
}
$errors = $dl->getErrorMsgs();
if (count($errors)) {
$err = array();
foreach ($errors as $error) {
$err[] = $error;
}
if (!count($downloaded)) {
throw new MyBlog_Template_Exception('template "' .
$template . '" installation failed:<br />' .
implode('<br />', $err));
}
}
$templatePackage = $downloaded[0];
$installer = new PEAR_Installer($this);
// always upgrade
$installer->setOptions(array('upgrade' => true));
$packages = array($templatePackage);
$installer->sortPackagesForInstall($packages);
PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
$err = $installer->setDownloadedPackages($packages);
if (PEAR::isError($err)) {
PEAR::staticPopErrorHandling();
throw new MyBlog_Template_Exception($err->getMessage());
}
// always upgrade
$info = $installer->install($templatePackage,
array('upgrade' => true));
PEAR::staticPopErrorHandling();
if (PEAR::isError($info)) {
throw new MyBlog_Template_Exception($info->getMessage());
}
if (is_array($info)) {
$this->log('Installation successful');
return true;
} else {
throw new MyBlog_Template_Exception('install of "' .
$template . '" failed');
}
}
MyBlogMyBlog_Template_Fetcher}
lightweight installer plug-inMyBlog_Template_Fetcher?>
这就是我们嵌入 PEAR 安装程序所需的所有内容,仅仅 610 行代码,包括大量的注释!
MyBlog 后安装脚本
为了完成安装,我们需要一个后安装脚本来初始化环境。这次,在我们了解它之前,让我们看看代码:
<?php
require_once 'MyBlog/Config.php';
require_once 'PEAR/Downloader.php';
require_once 'PEAR/PackageFile/v2/rw.php';
/**
* Post-installation script for the fake MyBlog blog.
*
* This script simply creates the templates/ subdirectory, if
* not present, and makes it world-writeable
* @version @package_version@
*/
class blogsetup_postinstall
{
private $_where;
/**
* @var PEAR_Config
*/
private $_config;
function __construct(){
$this->_where = '@php-dir@' . DIRECTORY_SEPARATOR .
'MyBlog' . DIRECTORY_SEPARATOR . 'templates';
}
/**
* Initialize the post-installation script
*
* @param PEAR_Config $config
* @param PEAR_PackageFile_v2 $pkg
* @param string|null $lastversion Last installed version.
* Not used in this script
* @return boolean success of initialization
*/
function init(&$config, &$pkg, $lastversion){
$this->_config = $config;
return true;
}
/**
* Run the script itself
*
* @param array $answers
* @param string $phase
*/
function run($answers, $phase){
$ui = PEAR_Frontend::singleton();
$blogconf = new MyBlog_Config;
$conf = $blogconf->getPearConfig();
$reg = $conf->getRegistry();
// we need the blog and template channels to be discovered
$conf->set('auto_discover', true);
if (!$reg->channelExists('pear.chiaraquartet.net/template',
true)) {
// make sure the registry directory exists, or this fails
System::mkdir(array('-p', $conf->get('php_dir')));
$dl = new PEAR_Downloader($ui, array(), $conf);
$dl->discover('pear.chiaraquartet.net/template');
}
if (!$reg->channelExists('pear.chiaraquartet.net', true)) {
// make sure the registry directory exists, or this fails
System::mkdir(array('-p', $conf->get('php_dir')));
$dl = new PEAR_Downloader($ui, array(), $conf);
$dl->discover('pear.chiaraquartet.net');
}
// for dependency purposes fake the MyBlog package in
// our sub-install
$reg->deletePackage('MyBlog', 'pear.chiaraquartet.net');
$fake = new PEAR_PackageFile_v2_rw;
$fake->setPackage('MyBlog');
$fake->setChannel('pear.chiaraquartet.net');
$fake->setConfig($this->_config);
$fake->setPackageType('php');
$fake->setAPIStability('stable');
$fake->setReleaseStability('stable');
$fake->setAPIVersion('1.0.0');
$fake->setReleaseVersion('@package_version@');
$fake->setDate('2004-11-12');
$fake->setDescription('foo source');
$fake->setSummary('foo');
$fake->setLicense('BSD License');
$fake->clearContents();
$fake->addFile('', 'foor.php', array('role' => 'php'));
$fake->resetFilelist();
$fake->installedFile('foor.php', array('attribs' =>
array('role' => 'php')));
$fake->setInstalledAs('foor.php', 'foor.php');
$fake->addMaintainer('lead', 'cellog', 'Greg Beaver',
'cellog@php.net');
$fake->setNotes('blah');
$fake->setPearinstallerDep('1.4.3');
$fake->setPhpDep('5.0.0');
$reg->addPackage2($fake);
do {
if (file_exists($this->_where)) {
if (OS_UNIX) {
if (!fileperms($this->_where) == 0777) {
chmod($this->_where, 0777);
$ui->outputData('set templates directory to
be world-writeable');
break;
}
$ui->outputData('templates directory already
initialized');
break;
} else {
$ui->outputData('templates directory ' .
$this->_where . ' already created');
break;
}
MyBlogpost-install script} else {
$ui->outputData('creating template directory ' .
$this->_where);
System::mkdir(array('-p', $this->_where));
chmod($this->_where, 0777);
}
} while (false);
if (file_exists($this->_where . DIRECTORY_SEPARATOR .
'default')) {
System::rm(array('-rf', $this->_where .
DIRECTORY_SEPARATOR . 'default'));
}
mkdir($this->_where . DIRECTORY_SEPARATOR . 'default');
copy('@php-dir@' . DIRECTORY_SEPARATOR . 'MyBlog' .
DIRECTORY_SEPARATOR . 'Template' . DIRECTORY_SEPARATOR .
'default' . DIRECTORY_SEPARATOR . 'body.tpl.php',
$this->_where . DIRECTORY_SEPARATOR . 'default' .
DIRECTORY_SEPARATOR . 'body.tpl.php');
copy('@php-dir@' . DIRECTORY_SEPARATOR . 'MyBlog' .
DIRECTORY_SEPARATOR . 'Template' . DIRECTORY_SEPARATOR .
'default' . DIRECTORY_SEPARATOR . 'head.tpl.php',
$this->_where . DIRECTORY_SEPARATOR . 'default' .
DIRECTORY_SEPARATOR . 'head.tpl.php');
$ui->outputData('default template copied');
return true;
}
}
?>
与所有后安装脚本一样,这个脚本有一个以文件名命名的类,其中 DIRECTORY_SEPARATOR 被替换为 '_' 在类名中,并附加 _postinstall。换句话说,由于这是 blogsetup.php 并且位于我们的 package.xml 的根目录中,我们的类名是 blogsetup_postinstall。脚本需要 init() 和 run() 方法。
这个脚本是必需的,因为我们将在应用程序的工作目录内设置一个定制的内部 PEAR 仓库。换句话说,模板将安装在自己的内部宇宙中,具有唯一的注册和配置设置。因此,目录结构需要看起来像以下这样:
pear/
.filemap
.registry/
pear.reg
...
...
[global PEAR registry]
.channel.pear.chiaraquartet.net/
myblog.reg
.channels/
pear.chiaraquartet.net.reg
MyBlog/
templates/
.filemap
.registry/
.channel.pear.chiaraquartet.net_template/
.channels/
pear.chiaraquartet.net.reg
pear.chiaraquartet.net_template.reg
安装模板的注册文件将存放在 pear/MyBlog/templates/.registry/.channel.pear.chiaraquartet.net_template/。
此外,在我们的设计中,默认模板被复制并始终可用,这样博客就可以直接使用。
提示
尽管我们没有在模拟博客或列表器类中实现这一点,但 MyBlog_Template_Fetcher 类也具备安装本地模板文件的能力,这使得博客维护者可以设计自己的模板,或者修改现有的模板并直接安装它们。作为一个挑战,看看你是否能实现本地列表安装的模板。提示:查看 PEAR/Registry.php 中 PEAR_Registry 类的代码,以及它在 PEAR/Command/Registry.php 中 pear list 命令中的使用。
最后要查看的新功能是如何检索和显示每个模板的缩略图。为此,我们将设置一个名为 image.php 的小文件,并且我们的 <img> 标签中的 src 属性将引用它以获取图像。
image.php 简单地接受一个模板名称和版本,然后获取远程缩略图。重要的是,这样一个文件实际上不是在读取本地文件并显示它们,因为这构成了严重的安全风险。例如,如果 image.php 简单地读取相对于当前路径的本地文件并显示它们,那么经过几次猜测后,类似以下请求将检索 /etc/passwd 文件:
image.php?i=../../../etc/passwd
在我们的案例中,如果请求的模板在远程服务器上没有缩略图,它将不会显示。以下是 image.php:
<?php
require_once 'MyBlog/Template/REST.php';
require_once 'MyBlog/Config.php';
$conf = new MyBlog_Config;
$a = new MyBlog_Template_REST($conf->getPearConfig());
$a->setTemplateChannel($conf->getTemplateChannel());
// sanitize input and retrieve a thumbnail image
// make certain that URL passed in fits on 1 line, so
// we don't magically send headers to the server by mistake
echo $a->getThumbnail(str_replace(array("\n", "\r"), array('', ''), $_GET['t']),
str_replace(array("\n", "\r"), array('', ''), $_GET['v']));
?>
再次注意突出显示的安全意识代码。安全必须始终是一个关注点!
模拟 MyBlog 包的其余部分
到目前为止,我们已经检查了所有 PEAR 特定的代码,因此让我们看看模拟 MyBlog 包(再次,可以从 pear.chiaraquartet.net 作为包 chiara/MyBlog 安装)。首先,让我们看看控制基本配置需求的 MyBlog_Config 类的代码:
<?php
/**
* For MyBlog_Template_IConfig interface
*/
require_once 'MyBlog/Template/Interfaces.php';
require_once 'PEAR/Config.php';
// hard-coded "database" stuff for demonstration purposes.
// edit this code to try other stuff
if (!isset($_SESSION['template'])) {
$_SESSION['template'] = '#default';
}
class MyBlog_Config implements MyBlog_Template_IConfig{
function getTemplateChannel(){
return 'pear.chiaraquartet.net/template';
}
function getCurrentTemplate(){
return $_SESSION['template'];
}
/**
* Get a customized PEAR_Config object for our blog
* template system
* @return PEAR_Config
*/
function getPearConfig(){
static $done = false;
$config = PEAR_Config::singleton();
if ($done) {
return $config;
}
$config->set('php_dir', '@php-dir@' . DIRECTORY_SEPARATOR .
'MyBlog' . DIRECTORY_SEPARATOR . 'templates');
$config->set('data_dir', '@php-dir@' . DIRECTORY_SEPARATOR .
'MyBlog' . DIRECTORY_SEPARATOR . 'templates');
// restrict to the template channel
$config->set('default_channel', $this->getTemplateChannel());
return $config;
}
}
简单至极,对吧?接下来,我们将查看主要的 MyBlog 类。为了实现 MyBlog,我选择使用出色的 Savant3 包(可以从 savant.pearified.com 频道安装,网址为 savant.pearified.com),这是一个使用 PHP 作为模板语言的 PHP 模板系统。对于我们的模拟博客,我们将有两个(或三个)模板文件,一个用于 <head> 元素,另一个可选的模板用于 <body> 标签的属性,第三个用于博客的内容。对于我们的示例模板,我们只使用 head.tpl.php 和 body.tpl.php。以下是主要的博客文件,MyBlog_Main:
<?php
require_once 'Savant3.php';
class MyBlog_Main extends Savant3{
/**
* Output the <head> block
*/
function doHead(){
// output user-specific stuff
$this->display('head.tpl.php');
// output plugin-related stuff (dummy, but here
// for example purposes)
$this->displayPluginHead();
}
/**
* Output any onload parameters, etc.
*/
function doBodyTag(){
try {
$onload = $this->fetch('onload.tpl.php');
if ($onload) {
echo 'onload="' . $onload . '"';
}
} catch (Savant3_Exception $e) {
// ignore
}
}
/**
* Display blog body
*
*/
function doBody(){
$this->display('body.tpl.php');
}
function displayPluginHead(){
return; // do nothing
}
}
?>
我们有几个未使用的方法,只是为了展示可以做什么。模板实际上是通过index.php显示的,它包含以下代码:
<?php
session_start();
require_once 'MyBlog/Main.php';
require_once 'MyBlog/Config.php';
$blog_config = new MyBlog_Config;
// default template is #default, so strip #
// other templates must be valid package names, and so
// can't contain #
$blog = new MyBlog_Main(array(
'template_path' => '@php-dir@' . DIRECTORY_SEPARATOR . 'MyBlog' .
DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR .
str_replace('#', '', $blog_config->getCurrentTemplate()),
'exceptions' => true));
$blog->title = 'Example MyBlog Blog';
$blog->content = 'blah blah blah here is my fake article';
?><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<?php $blog->doHead(); ?>
</head>
<body <?php $blog->doBodyTag(); ?>>
<?php $blog->doBody(); ?>
<a href="admin.php">Administer Blog</a>
</body>
</html>
实际的模板应该检查其完整性,所以这里提供了一个示例模板的head.tpl.php:
<title><?php echo $this->title; ?></title>
<link href="css.php/example1/index.css" type="text/css" />
以及body.tpl.php:
<div class="topbar">
<h1 id="pageTitle"><?php echo $this->title ?></h1>
</div>
<div class="leftbar">Left Bar
</div>
<div class="centerbar">Center Bar<br />
<?php echo $this->content; ?>
</div>
<div class="rightbar">Right Bar
</div>
现在我认为很清楚为什么我总是把 MyBlog 称为一个假博客!为了演示模板的安装过程,这里提供了一个来自 example1 模板的package.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<package packagerversion="1.4.11" version="2.0"
xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>example1</name>
<channel>pear.chiaraquartet.net/template</channel>
<summary>fake MyBlog template example 1</summary>
<description>fake MyBlog template example 1</description>
<lead>
<name>Greg Beaver</name>
<user>cellog</user>
<email>cellog@php.net</email>
<active>yes</active>
</lead>
<date>2006-08-19</date>
<time>13:14:58</time>
<version>
<release>2.0.0</release>
<api>1.0.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.opensource.org/licenses/bsd-
license.php">BSD license</license>
<notes>second release</notes>
<contents>
<dir baseinstalldir="example1" name="/">
<file name="body.tpl.php" role="php" />
<file name="head.tpl.php" role="php" />
</dir> <!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>5.1.0</min>
</php>
<pearinstaller>
<min>1.4.3</min>
</pearinstaller>
<package>
<name>MyBlog</name>
<channel>pear.chiaraquartet.net</channel>
<min>0.2.0</min>
<max>0.2.0</max>
</package>
</required>
</dependencies>
<phprelease />
<changelog>
<release>
<version>
<release>0.1.0</release>
<api>0.1.0</api>
</version>
<stability>
<release>alpha</release>
<api>beta</api>
</stability>
<date>2006-08-18</date>
<license uri="http://www.opensource.org/licenses/bsd-
license.php">BSD license</license>
<notes>first release</notes>
</release>
<release>
<version>
<release>1.0.0</release>
<api>1.0.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<date>2006-08-18</date>
<license uri="http://www.opensource.org/licenses/bsd-
license.php">BSD license</license>
<notes>first release</notes>
</release>
<release>
<version>
<release>2.0.0</release>
<api>1.0.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<date>2006-08-19</date>
<license uri="http://www.opensource.org/licenses/bsd-
license.php">BSD license</license>
<notes>second release</notes>
</release>
</changelog>
</package>
为了总结这一章,让我们看看admin.php,即管理控制中心,看看所有这些元素是如何结合在一起来显示模板、下载正确的模板版本以及安装它们的:
<?php
// silence potential notice
@session_start();
require_once 'MyBlog/Template/Lister.php';
require_once 'MyBlog/Config.php';
$blog_config = new MyBlog_Config;
$lister = new MyBlog_Template_Lister($blog_config->getPearConfig());
$lister->setConfigObject($blog_config);
if (isset($_GET['dodefault'])) {
unset($_GET['dodefault']);
$_SESSION['template'] = '#default';
}
if (isset($_GET['t']) && isset($_GET['v'])) {
require_once 'MyBlog/Template/Fetcher.php';
require_once 'MyBlog/Template/REST.php';
$conf = new MyBlog_Config;
$config = $conf->getPearConfig();
$rest = new MyBlog_Template_REST($config, array());
$rest->setTemplateChannel($conf->getTemplateChannel());
$fetch = MyBlog_Template_Fetcher::factory($rest, $config);
try {
$fetch->installTemplate($_GET['t'], $_GET['v']);
$out = '';
foreach ($fetch->log as $info) {
if ($info[0] == 'log') {
$out .= ' ' .
htmlspecialchars($info[1]) . '<br />';
} else {
$out .= htmlspecialchars($info[1]) . '<br />';
}
}
// this is safe because installTemplate throws an exception
// if the template or version are not valid PEAR
// package/version
// so input is validated by this point
$_SESSION['template'] = $_GET['t'];
define('MYBLOG_OUTPUT_INFO', $out);
} catch (MyBlog_Template_Exception $e) {
define('MYBLOG_OUTPUT_INFO', '<strong>ERROR:</strong> ' .
$e->getMessage());
}
unset($_GET['t']);
unset($_GET['v']);
}
?><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>MyBlog Administration</title>
</head>
<body>
<a href="index.php">Return to MyBlog</a><br />
<h1>MyBlog Administration</h1>
<?php
// this is defined in install.php
if (defined('MYBLOG_OUTPUT_INFO')): ?>
<h2>Installation Information:</h2>
<?php echo MYBLOG_OUTPUT_INFO; ?>
<?php endif;
?>
<h2>Choose a template</h2>
<?php
list($info, $pager) = $lister->listRemoteTemplates(1);
$links = $pager->getLinks();
echo $links['all'] . '<br />';
?>
<?php if ($blog_config->getCurrentTemplate() == '#default'): ?>
<span class="current_template">*</span>
<?php endif; ?>[Default Template] <em>(Standard, ships with
install)</em><a href="admin.php?dodefault=1">
Choose Default Template</a><br />
<?php
foreach ($info as $template): ?>
<?php if ($template['name'] ==
$blog_config->getCurrentTemplate()): ?>
<span class="current_template">*</span>
<?php endif; ?>
<img src="image.php?<?php echo 't=' .
htmlspecialchars(urlencode($template['name'])) . '&v=' .
htmlspecialchars(urlencode($template['version'])); ?>"
height="36" width="36" />
<?php echo $template['name'] ?> <em>(<?php
echo htmlspecialchars($template['summary']) ?>)</em>
Version <?php echo $template['version'] ?><a
href="admin.php?t=<?php
echo htmlspecialchars(urlencode($template['name']))
?>&v=<?php
echo htmlspecialchars($template['version'])
?>">Install/Upgrade</a><br />
<?php endforeach;
echo '<br />' . $links['all'];
?>
</body>
</html>
为了实验目的,这本书附带代码中的 MyBlog 包提供了两个package.xml文件。第一个,package1.xml,描述自己为 MyBlog 版本 0.1.0,并将显示模板 example1 版本 1.0.0 和 example2 版本 1.0.0 可供安装。测试这个之后,从 MyBlog 目录执行一个简单的:
$ pear upgrade package.xml
你将被升级到 MyBlog 版本 0.2.0。立即,你会注意到只有 example1 版本 2.0.0 可供安装。这是基于 example1 的package1.xml(版本 1.0.0)中包含了这个必需的依赖项:
<package>
<name>MyBlog</name>
<channel>pear.chiaraquartet.net</channel>
<min>0.1.0</min>
<max>0.1.0</max>
</package>
而package.xml(版本 2.0.0)中包含了这个必需的依赖项:
<package>
<name>MyBlog</name>
<channel>pear.chiaraquartet.net</channel>
<min>0.2.0</min>
<max>0.2.0</max>
</package>
这些依赖项确保模板只对它们兼容的博客版本可用。因此,如果你决定采用这个模型,你需要确保所有模板在它们的博客依赖项中都带有<max>元素,定义它们已知可以工作的最高版本。随着新版本的发布,模板可以带有更新的<max>标签发布,或者修改后发布。这样,工作模板将始终适用于不同的博客版本。
简而言之:所有复杂性都由 PEAR 安装器的内部管理,让你可以编写出色的程序!
对有抱负的人的改进
所有基于网络的插件安装系统都存在一个令人烦恼的问题,那就是目录权限的安全问题。为了安装某些东西,网络服务器的用户(nobody 或 apache 是常见的网络用户)必须对插件目录有写访问权限。这意味着任何在机器上有账户和公开网页的人都可以通过创建一个执行此操作的网页来对你的应用程序的插件目录进行读写操作。
在我们的示例 MyBlog 中,我做出了一个可疑的假设,即你是博客服务器的唯一所有者,不需要担心这类问题,并且没有实现一个系统来处理这个重要的安全问题。
然而,有一个简单的解决方案,需要一点工作,对有抱负的人来说是一个极好的练习。这个技巧是在页面上提供一个带有小锁的链接。用户在安装之前必须解锁目录,安装后必须上锁。
锁定页面包括递归遍历内部插件目录,并运行以下简单命令:
chmod($file_or_dir, 0444);
解锁是其相反操作,递归遍历内部插件目录并运行以下简单命令:
chmod($file_or_dir, 0777);
您可能会问,为什么没有博客或其他应用程序执行这项任务?有几个答案。首先,同样的任务可以非常容易地使用 shell 脚本来完成。换句话说,“让用户自己处理他们的安全问题。”此外,这个特定的安全问题还没有引起注意,因为它要求恶意黑客已经能够访问机器才能利用它——或者人们是这样认为的。
事实上,如果应用程序恰好存在 PHP 代码注入漏洞,这将允许恶意黑客注入代码,在服务器上创建恶意 PHP 脚本,从而通过插件目录可写性这一事实获得对服务器的控制!尽管这需要一个非常严重的漏洞才会成为问题,但可写目录可能会在拥有和失去对生产服务器的控制之间产生差异。
在开发过程中请记住这一点——安全性始终是一项重要任务!在设计时尽量像邪恶之人一样思考,这样您(和他人)将会有(和造成)更少的网络安全漏洞。
摘要
本章内容丰富多彩。在其中,我们研究了将插件嵌入网络应用中的常见实践方法。具体来说,我们考察了三个示例——MDB2(子包)、Serendipity(Spartacus)、Seagull(部分嵌入 PEAR 安装程序)。对于这些示例中的每一个,我们都权衡了它们各自方法的优缺点。
确定可能存在更好的做事方式后,我们学会了如何最有效地嵌入 PEAR 安装程序,以便创建一个插件管理器。
为了这个目的,我们仅用不到 1000 行代码创建了一个假博客程序,它能够无缝查询一个旨在分发模板的远程 PEAR 通道服务器。利用 PEAR 安装程序的内建类,我们的 MyBlog 网络应用可以智能地安装和升级模板,这些模板的复杂程度符合 PEAR 安装程序所期望的。
我们充分利用了 PEAR 安装程序内建的 REST 客户端来查询远程服务器,其下载能力,以及其包含文件事务的强大文件安装器。此外,我们还学会了如何扩展远程服务器的 REST 代码以包含缩略图图像,并指导 MyBlog 的管理页面显示这些缩略图图像。
最后,我想感谢您阅读这份关于激动人心且创新的 PEAR 安装程序的指南,并希望它能在您寻找完美网站和开发环境的过程中为您提供帮助!


浙公网安备 33010602011771号