PHP-速成课-全-
PHP 速成课(全)
原文:
zh.annas-archive.org/md5/a645d52b4680a001fe349ae8b04ab2c1译者:飞龙
前言

PHP 是驱动互联网的引擎之一:它在用户在网页上看到的内容和后台发生的事情(例如处理表单提交、与其他网站对话、与数据库交互)中都发挥着作用。该语言首次发布于 1995 年,但直到 2000 年代末,当我的计算机学生鼓励我将 PHP 引入他们的网页编程课程时,我才开始认真使用它。他们希望提高自己的技能,以便在就业市场上更具竞争力,因为他们认为互联网将继续在计算机工作中变得越来越重要。显然,他们是对的。 ## 本书适用人群
PHP 速成课程适合任何想要以实用、动手的方式学习 PHP 编程的人,无论你是否有编程经验。由于大多数 PHP 程序都是网页应用程序,了解用于表示网页内容和视觉样式的超文本标记语言(HTML)和层叠样式表(CSS)可能会有所帮助,但你不需要了解任何网页编程语言,如 JavaScript。
为什么选择 PHP?
PHP 目前已经进入第八个主要版本,因此它不仅经过了充分的测试,而且比以往更加快速和安全。它是一种免费、开源且维护良好的语言。虽然还有其他流行的网页编程语言,但大约 70%的互联网是由 PHP 程序驱动的,包括 Etsy、Facebook(使用其 PHP 方言 Hack)、Spotify、Wikipedia 和 WordPress。
PHP 也有一个相对较浅的学习曲线。我们将从几行代码开始,一旦你掌握了基础知识,我们将进入更大、更结构化的网页应用系统。
注意
该语言的最初版本发布为个人主页工具(PHP Tools)。然而,现在,PHP 是一个递归首字母缩略词,代表 PHP:超文本预处理器。
你将学到什么
在本书中,你将学习如何用 PHP 编程,从短小、简单的脚本到多文件、数据库驱动、登录安全的面向对象的网页应用程序。
在第一部分,语言基础部分,你将开始编写小型 PHP 程序脚本。本部分介绍了语言的一些基础知识,包括将不同类型的值存储在命名变量中、处理文本以及编写基于决策的逻辑。
第一章:PHP 程序基础 介绍了如何在在线编码环境和电脑上的编辑器中编写和执行 PHP 脚本。
第二章:数据类型 讨论不同类型的数据以及 PHP 如何自动在它们之间进行转换。
第三章:字符串与字符串函数 介绍如何在自己的代码中以及使用语言提供的一些内建文本函数来处理文本。
第四章: 条件语句 探索语言中的条件元素,如 if...else、switch 和 match,并提供了每种语句最合适的使用指南。你还将了解操作符,如逻辑比较,用于实现决策逻辑。
第五章: 自定义函数 介绍函数,它们是可重用的、自包含的代码序列,用于完成任务。
在 第二部分,数据处理部分中,你将使用循环来重复操作,并学习更复杂的数据结构,如数组和文件。
第六章: 循环 介绍通过结构重复操作来增加灵活性并避免代码重复。
第七章: 简单数组 介绍数组,这是一种存储和操作多个数据项的机制,通过单一变量名进行管理。
第八章: 高级数组 在上一章的基础上,深入探讨更复杂的键值映射和多维数组。
第九章: 文件和目录 探索如何以及何时在 PHP 脚本中使用函数与文件进行交互。
在 第三部分,Web 应用程序编程中,你将开始创建 Web 应用程序,包括接收和验证通过表单提交的数据。
第十章: 客户端/服务器通信和 Web 开发基础 介绍关于客户端、服务器以及 PHP 驱动的 Web 应用程序如何工作的基本概念。
第十一章: 创建和处理 Web 表单 介绍如何设计 Web 表单以及如何编写 PHP 脚本处理通过这些表单提交的数据。
第十二章: 验证表单数据 探索验证接收数据的方法,并涵盖一些典型的决策逻辑,根据接收到的值是否正确或缺失采取适当的行动。
第十三章: 组织一个 Web 应用程序 逐步探索模型-视图-控制器(MVC)软件架构方法,该方法将维护应用程序的职责分配给多个脚本,从而使应用程序能够增长而不至于变得无法管理。
第四部分,使用浏览器会话存储用户数据,介绍了会话,允许网站在页面请求之间记住数据;这对购物车和登录非常有用。
第十四章: 会话管理 介绍 PHP 功能,使 Web 应用程序能够记住跨时间段的信息。
第十五章: 实现购物车 介绍如何将商品添加到购物车,并如何在用户准备结账支付时记住这些商品。
第十六章: 认证与授权实现了安全认证(确定使用计算机系统的人的身份)和授权(决定用户是否被允许访问系统的特定部分)。
在第五部分,面向对象 PHP,你将学习到强大的面向对象编程(OOP)技术。
第十七章: 面向对象编程简介讨论了从函数式编程转向 OOP 方法的动机以及这两者之间的区别。
第十八章: 声明类和创建对象讲解了面向对象编程中类和对象的核心特性。
第十九章: 继承描述了面向对象编程中强大的继承概念,并讲解了如何在代码中实现它。
第二十章: 使用 Composer 管理类和命名空间介绍了在 PHP 编程语言中实现 OOP 解决方案的关键机制,并展示了 Composer 命令行工具如何提供帮助。
第二十一章: 使用 Twig 进行高效模板设计探索了基于继承的 Twig 库系统,用于开发页面模板,它简化了将应用数据与 HTML“装饰”并返回给用户的过程。
第二十二章: 结构化面向对象的 Web 应用程序介绍了一种常用的、可扩展的 Web 应用程序软件架构。
第二十三章: 使用异常处理错误讨论了异常的错误处理机制,这是许多编程语言的特性。
第二十四章: 记录事件、消息和事务展示了如何维护系统日志,包括将日志输出到日志文件或外部的云日志服务,这是大规模 Web 应用程序中常见的做法。
第二十五章: 静态方法、属性和枚举讲解了面向对象编程中类级别的静态成员,并讨论了枚举这一特殊类别,便于提供一组固定的可能值。
第二十六章: 抽象方法、接口和特性探索了如何在多个类之间共享方法,而不通过传统的继承过程。
在第六部分,数据库驱动的应用程序开发,详细介绍了如何编写与数据库系统通信的程序,并最终讨论了如何在程序代码中以及数据库中处理日期和时间。
第二十七章: 数据库简介介绍了数据库及其与 Web 应用程序的关系。
第二十八章: 使用 PDO 库进行数据库编程讨论了编写与数据库通信的代码。
**第二十九章: 编程 CRUD 操作 ** 展示了如何将数据库 CRUD(创建、读取、更新、删除)功能引入到网页应用中。
**第三十章: ORM 库与数据库安全 ** 介绍了通过对象关系映射(ORM)库自动化代码与数据库结构之间关系的好处,并概述了几个安全的数据库驱动网页开发的最佳实践。
**第三十一章: 处理日期和时间 ** 介绍了如何处理时间信息,包括如何处理时区和夏令时等歧义问题。
最后,附录介绍了如何设置开始使用 PHP 所需的工具。
**附录 A: 安装 PHP ** 讲解了如何在 macOS、Linux 和 Windows 操作系统上安装 PHP。
**附录 B: 数据库设置 ** 讲解了如何确保 MySQL 和 SQLite 数据库管理系统在本地计算机上正确设置。
**附录 C: Replit 配置 ** 讨论了如何重新配置更高级的 Replit 项目,以便与 Composer 依赖管理器和数据库管理系统等工具一起使用。
在线资源
本书的代码清单以及我建议的练习解答可以通过 github.com/dr-matt-smith/php-crash-course 下载。
每一章的末尾都包含了练习题。我建议你先自己尝试这些题目,再查看我的解答。
关于本书的更新和其他信息,请参见 nostarch.com/php-crash-course。
第一部分 语言基础
第一章:1 PHP 程序基础

本章将介绍两种创建和运行 PHP 程序的方法:使用在线编码环境和使用本地安装的编辑器。我们将尝试这两种方法,以练习一些关键的编程任务,比如打印文本消息、给变量赋值以及处理不同类型的数据。我们还将探索 PHP 语言的核心特性,包括注释、常量和表达式。
运行 PHP 的两种方法
通常,学习编程语言的最简单方法是使用一个已经为你配置好的在线环境。这样你可以立刻开始编码并实时查看结果,而不需要安装和配置语言引擎、代码编辑器、网络服务器或其他工具。另一方面,一些人更喜欢自己计算机上安装的编程环境,因为它提供了更多的自定义和控制。
在本节中,你将探索这两种方法,并写出你的第一个 PHP 程序。然后,你可以使用任意一种方法来跟随本书中的示例。
Replit 在线编码环境
有多个在线平台可以支持交互式 PHP 开发,并且可以为你运行 PHP 网络服务器。我们将重点介绍 Replit (replit.com),这是一个广受欢迎的服务,适合初学者的免费项目。要试用它,请访问 Replit 网站并创建一个账户。
注意
Replit 的名称源自 读-评估-打印循环(REPL),这是一种计算机环境,在这种环境中,程序员输入一个表达式,系统立即执行它,打印出响应并等待下一个输入。命令行终端就是一种 REPL,程序员输入单行命令,终端执行这些命令。
Replit 提供了两种官方的、预配置的模板来创建 PHP 项目:PHP CLI(即命令行界面)和 PHP Web 服务器。前者适用于仅在命令行终端窗口输出文本或处理数据文件的项目,而后者则用于 web 开发。我们来看看这两个模板,以便在学习 PHP 程序工作原理的过程中获得一些了解。
创建命令行界面项目
要创建一个 PHP 命令行界面项目,首先进入 Replit 账户的主页,然后点击 Create Repl。这将弹出一个窗口,你可以在其中搜索项目模板。在搜索框中输入 PHP。结果中应包括 Replit 的 PHP CLI 和 PHP Web 服务器模板。(在这些官方的 Replit 模板下,你还可能看到一些由 Replit 用户创建并标注为 PHP 语言的其他模板。)选择 PHP CLI,并为你的项目输入一个名称,或者使用默认的随机单词名称。然后点击 Create Repl 来启动项目。
网站将花费一些时间来设置你的新项目,包括创建文件和文件夹结构,并启动一个云虚拟机来运行它。当项目加载时,你将看到如图 1-1 所示的三列屏幕。

图 1-1:新 PHP CLI 项目屏幕,带有默认的“Hello, world!”脚本
左侧列显示项目的文件和文件夹,中间列是一个在线代码编辑器,右侧列是命令行终端输出(称为控制台)和 Replit 创建的虚拟计算机的交互式终端(称为Shell)。屏幕顶部的运行按钮会运行项目,此时所有输出将显示在控制台中。
一个典型的 PHP 项目包含一个或多个文件,称为脚本,并以.php 文件扩展名保存。在这种情况下,Replit PHP CLI 项目会自动启动一个名为main.php的预编写文件。该文件包含 PHP 代码,用于在控制台输出消息“Hello, world!”。编写一个显示此消息的程序是学习新语言时的一项传统。除了有趣,它还提供了一个机会,学习如何命名包含程序的文本文件,如何编写有效的语句,以及如何执行程序。更重要的是,“Hello, world!”脚本作为计算机系统上语言工具的基本测试:如果程序运行并成功输出“Hello, world!”消息,那么 PHP 就正常工作了。
清单 1-1 显示了 Replit 提供的默认“Hello, world!”脚本,它位于main.php中。
<?php
echo "Hello, world!\n";
清单 1-1:main.php 中的“Hello, world!”程序
脚本开头的<?php 是一个开头的 PHP 标签。此标签表示接下来的内容是 PHP 代码。在这个例子中,代码使用 echo 命令在控制台中打印出文本“Hello, world!”,后面跟着一个换行符(\n)。请注意,要打印的文本被双引号括起来。这些引号表示文本是一个字符串,它是一种由一系列字符组成的数据类型。我们将在第三章中详细讨论字符串和特殊字符,如 \n。
echo 语句是一条语句的例子,它是一个指示计算机执行某个任务的单一命令(在本例中是显示某些文本)。每个 PHP 语句必须以分号(;)结束,以表示命令结束,就像这个 echo 语句一样。可以把分号看作是句子结尾的句号;没有它,语句被认为是不完整的。
运行 main.php 脚本
点击绿色的运行按钮来运行main.php脚本。你应该会看到控制台打印出“Hello, world!”消息。恭喜你,你刚刚运行了第一个 PHP 程序!但是,当你点击“运行”时,实际上发生了什么呢?
PHP 是一种脚本化编程语言。这意味着一个名为解释器的程序会在执行 PHP 文件时将其内容翻译成机器代码。其他脚本化语言包括 Python 和 JavaScript。脚本化语言不同于 编译型 编程语言,如 C、C++ 和 Swift,这些语言的翻译过程发生在执行之前的独立步骤中。在这个额外的步骤中,所有程序文件都会被编译并优化为一个或多个可执行文件。
将 PHP 脚本转换为可执行代码的解释器通常被称为PHP 引擎。当你点击 Replit 的运行按钮来运行 main.php 文件时,Replit 调用 PHP 引擎,PHP 引擎随后读取文件内容并解释执行其中的代码行。对于简单的 PHP 脚本(例如我们的 Replit main.php),这些脚本仅由一个或多个按顺序执行的语句组成,整个过程相对简单。然而,几乎所有程序都涉及更复杂的决策逻辑,需要进行测试,以便代码能够动态响应事件并确定执行哪些语句以及按照什么顺序执行。PHP 引擎决定接下来做什么的方式被称为控制流程。我们将在讨论第四章和第六章中的条件语句和循环时进一步探讨这个概念。
点击 Replit 的运行按钮并不是执行 PHP 脚本的唯一方式。你还可以通过命令行调用 PHP 引擎,使用 php 命令并跟上你想要执行的脚本的名称。要尝试这个方法,切换到 Replit 项目右侧栏的 Shell 标签页,以打开交互式命令行终端。然后在 $ 提示符后输入以下内容:
$ **php main.php**
Hello, world!
php main.php 命令指示 PHP 引擎执行 main.php 脚本。像以前一样,这会输出 "Hello, world!" 消息。你可以使用相同的方法在本地计算机的命令行上执行 PHP 文件,在这种情况下,你不一定能够使用 Replit 方便的运行按钮。
创建 Web 服务器项目
PHP 主要用于开发 web 应用程序,因此现在让我们尝试通过使用 Replit 的 PHP Web 服务器模板来创建一个基本的基于 Web 的 PHP 项目。返回你的 Replit 主页,创建一个新项目,这次在模板搜索框中输入 PHP 后,选择 PHP Web Server 模板。你的新项目应该类似于图 1-2。

图 1-2:新的 PHP Web 服务器模板项目屏幕
左栏中显示的唯一文件是一个模板 index.php 文件。像这样的 index 文件具有特殊的意义:它代表着当你访问一个网站的首页时,默认显示的文件。(我们将在第三部分中详细讨论这如何工作。)该文件的内容显示在中间栏。右栏是控制台和终端标签,这里也是当我们运行 Web 服务器以显示渲染后的网页时,Webview 标签会出现的地方。
index.php 文件应包含 列表 1-2 中显示的代码。
<html>
<head>
<title>PHP Test</title>
</head>
<body>
❶ <?php echo '<p>Hello World</p>'; ?>
</html>
列表 1-2:index.php 中的 Web 服务器脚本
该文件的大部分内容不是 PHP 代码,而是创建通用网页所需的超文本标记语言(HTML),这一点可以通过文件开头和结尾的 和 标签看到。正如我们在 第 13 页 的《模板文本与 PHP 代码》中将进一步讨论的,许多 PHP 脚本将动态的 PHP 代码(用于即时解释和执行)与 HTML 这种静态文本混合在一起。在这个例子中,唯一的 PHP 代码是一个 echo 语句,用来显示文本 Hello World ❶。这些文本被包裹在 HTML 的
标签中,这意味着它将在渲染后的网页中作为一个正文段落显示,而整个 echo 语句被 PHP 标签(开头的 标签)包围,以表明它是实际的 PHP 代码,不同于周围的 HTML。
运行 Web 服务器项目
点击 运行 按钮,Replit 将启动一个 Web 服务器,托管 index.php 文件并运行 PHP 引擎来解释文件中的 PHP 代码。这一次,你不应在控制台标签中看到文本,而应在 Webview 标签中看到作为基本网页显示的 Hello World(参见 图 1-3)。

图 1-3:在 Replit Webview 面板中查看 index.php 脚本输出
在运行 Web 服务器时,Replit 会将临时页面发布到 replit.dev 域名。这意味着它提供了公共的网页,你可以在一个独立的浏览器标签中查看和交互,而不仅仅是通过 Replit 网站本身。要尝试此操作,请点击 Webview 面板中绿色的 {...}.replit.dev URL 地址栏。然后复制弹出窗口中显示的 URL,并将其粘贴到浏览器中新标签页中。你应该会看到相同的 Hello World 消息被渲染为一个独立的网页,与 Replit 界面分开。恭喜你,你已经发布了第一个 PHP 网站!
注意
如果你选择使用 Replit 跟随本书进行学习,你将需要做额外的配置来处理后续章节中的一些更复杂的项目。详情请参见 附录 C。
本地 PHP 安装
像 Replit 这样的在线编辑器非常好用,但它们在免费计划下可能会比较慢并受到限制,并且需要稳定快速的互联网连接。许多开发者更喜欢在自己的机器上进行本地开发。要做到这一点,第一步是安装 PHP。如果你还没有安装,请按照附录 A 中的指南,安装适合你操作系统的最新版本的 PHP。
一旦 PHP 安装完成,你将需要一个集成开发环境(IDE)来编写代码。IDE 是一个强大的文本编辑器,包含了许多有用的编程工具,如终端、复杂的查找和替换功能、代码拼写检查,甚至为常见任务自动生成代码。
本节将重点介绍使用 PhpStorm 进行本地 PHP 开发,PhpStorm 是 JetBrains 出品的一款流行 IDE。任何人都可以免费使用 30 天,之后许多人(如学生、教师、编程训练营的学员、用户组成员以及开源项目的参与者)可以获得免费许可证。访问 www.jetbrains.com/phpstorm/ 下载 PhpStorm,并按照安装说明进行操作。
注意
如果你不想使用 PhpStorm,其他免费 IDE 也提供了辅助 PHP 编程的插件,包括 Visual Studio Code、Eclipse 和 Apache NetBeans。
使用 PhpStorm 创建“Hello, world!”
让我们用 PhpStorm 创建一个“Hello, world!”项目,类似于 Replit 的 PHP CLI 模板中的默认脚本。打开 PhpStorm IDE,点击新建项目,从可能的模板列表中选择PHP 空项目。选择项目的位置,并将未命名的默认名称更改为你希望的项目名称。确保在项目名称的路径前加上正斜杠,如/program1。然后点击创建。
PhpStorm 会在你选择的项目名称下,在指定位置创建一个新文件夹。所有的项目文件都将包含在这个文件夹中;更复杂的项目可能还会有子文件夹,用于组织数据、程序文件、配置文件等。文件夹创建后,PhpStorm 会加载到项目编辑视图,如图 1-4 所示。

图 1-4:PhpStorm 的三个主要面板
PhpStorm 的左上面板显示项目文件夹及其内容。右上面板是用来编辑代码和数据文件的地方。点击应用程序窗口左侧列中的终端(>_)图标,在应用程序窗口底部打开命令行终端,在这里你可以输入命令并查看程序的文本输出。这个终端会自动打开到项目文件夹的位置。
我们准备向项目中添加一个基础的“Hello, world!”脚本。在应用窗口的左上面板中选择你的项目文件夹,然后从顶部菜单中选择 文件 ▸ 新建 ▸ PHP 文件。输入 hello 作为文件名(PhpStorm 会自动为你添加 .php 文件扩展名),然后点击 确定。你应该能在项目内容面板中看到这个新建的 hello.php 文件,并且该文件应该会在代码编辑面板中打开,文件中已经包含了指定文件内容为 PHP 代码所需的 PHP 开始标签(<?php)。现在编辑文件,使其与 列表 1-3 中的代码匹配。
<?php
print "Hello, world!\n";
列表 1-3:我们在 hello.php 中的 “Hello, world!” 程序
和我们在 Replit 命令行程序中一样,这段代码仅仅是打印出文本 Hello, world!,后面跟着一个换行符(\n)。注意,这次语句使用的是 print 而不是 echo 来显示文本。二者大致可以互换;关于更多信息,请参见下面的“print 还是 echo?”框。
要运行你的脚本,请打开终端面板(如果还没打开的话),然后在命令行中输入以下内容:
% **php hello.php**
Hello, world!
你应该能在终端的下一行看到 Hello, world! 消息。
另一种在 PhpStorm 中运行脚本的方法是点击绿色的运行按钮(位于绿色“bug”按钮旁边),该按钮位于应用窗口的右上角。这将执行当前正在编辑的文件。如果点击按钮后出现一个下拉菜单,提供 PHP 和 JS(JavaScript)两种运行脚本的方式,选择 PHP 选项。
如果你以这种方式执行脚本,屏幕底部应该会弹出一个运行面板,显示你正在使用的 PHP 引擎以及正在执行的脚本的位置。如果你在一台计算机上有多个 PHP 引擎版本,这些信息非常有用,能够帮助你测试脚本与不同引擎的兼容性。下面应该显示运行程序的输出,后面跟着一个退出代码 0,表示程序成功完成了执行。
在本地运行 PHP Web 服务器
安装 PHP 后,它会自带一个内置的 Web 服务器,用于在你的系统上本地测试 Web 开发项目。你可以通过使用 phpinfo() 函数来查看有关此 Web 服务器的信息(并验证它是否正常工作)。该函数生成一串很长的 HTML 文本,报告有关当前 PHP 安装的详细信息。运行一个调用此函数的脚本是测试任何 PHP 系统用于 Web 开发时的有用第一步,无论是在本地计算机上还是在托管的 Web 服务器上。
使用 PhpStorm(或你选择的其他 IDE),在名为 web_project_1 的文件夹中创建一个新项目。然后为该项目创建一个名为 index.php 的新文件。如前所述,index 这个名称表示这是 Web 服务器托管项目时会返回的默认文件。编辑文件,使其与 列表 1-4 中的内容一致。
<?php
phpinfo();
列表 1-4:我们在 index.php 中的 info Web 应用
在必需的 PHP 开始标签后,你使用语句 print phpinfo(); 来显示调用 phpinfo() 函数后生成的报告。你可以通过在网页浏览器中执行脚本来查看这个报告,它会以格式化良好的网页形式呈现。在 PhpStorm 中,选择 视图 ▸ 在浏览器中打开 ▸ 内置预览,或者在文件编辑面板中,将鼠标放在 PhpStorm 图标上并点击(参见图 1-5)。

图 1-5:使用 PhpStorm 的网页预览
启动内置预览应该会运行 PHP 网页服务器,并在 PhpStorm 中的示例浏览器窗口中显示 index.php 脚本的结果,如图 1-6 所示。

图 1-6:PhpStorm 预览中的 phpinfo() 函数输出
你可能希望滚动浏览此网页,了解更多关于系统 PHP 设置的信息。你将找到 PHP 引擎的版本、php.ini 配置文件的位置、启用的数据库扩展(如果有的话)、PHP 语言的主要贡献者名称等信息。
你也可以在真实的网页浏览器中查看 index.php 脚本的结果,比如 Google Chrome 或 Mozilla Firefox,而不是在 PhpStorm 中查看。(如果你使用的是其他 IDE,这可能是你唯一的选择。)首先,打开 IDE 的终端并输入以下命令:
% **php -S localhost:8000**
这条命令告诉 PHP 启动其内置的网页服务器,并使当前项目在localhost:8000上可用。这里,localhost 指代你的本地计算机系统,而 8000 设置了端口号。每个需要通过互联网发送和接收消息的应用程序都需要一个唯一的端口号;你可以把这些端口想象成同一地点的不同邮箱。用于测试目的的网页服务器通常使用 8000 或 8080 端口,而生产(线上)网页服务器通常使用 80 端口。就个人而言,我在本地开发时总是使用 8000 端口。
在网页服务器运行时,打开网页浏览器并在地址栏中输入 localhost:8000。你应该会看到与之前相同的 PHP 脚本输出。当你完成时,回到终端并按 CTRL-C 终止网页服务器。
请注意,要在浏览器中查看一个不是 index.php 命名的脚本,你需要将脚本的文件名附加到浏览器地址栏 URL 的末尾。例如,要将列表 1-3 中的 hello.php 脚本作为网页查看,你首先需要运行命令 php -S localhost:8000 启动 PHP 网页服务器,然后在浏览器中访问 localhost:8000/hello.php。
模板文本与 PHP 代码
PHP 是一种流行的网页开发语言,因为 PHP 脚本可以轻松地输出 HTML(或 CSS 或 JavaScript),以便在网页浏览器中显示。输出的某些部分通常是不变的模板文本,而其他部分则通过执行 PHP 程序语句动态生成。静态的模板文本和动态的代码生成文本的结合构成了几乎所有互动网站的基础。
举个例子,想象一下在一个在线零售网站查看购物车。网页设计师不需要为展示每种可能的购物车物品配置编写单独的脚本。他们只需编写一个脚本,将任何购物车中都会出现的元素的通用模板文本(如隐藏的 HTML 头元素、导航栏、公司标志等)与动态填写每个具体购物车项目的名称、价格、数量等细节所需的 PHP 代码混合在一起。
将模板文本与 PHP 代码混合的能力是我们为什么至今需要在脚本中加入开头的标签。任何在这些标签外的部分都被视为模板文本并将逐字输出;任何在这些标签内的部分都会被解析为 PHP 代码并相应执行。如果脚本完全由 PHP 代码组成,如列表 1-1、1-3 和 1-4 中所示,那么只需要开头的标签。
为了澄清模板文本和 PHP 代码之间的区别,我们来编写一个将两者结合的示例脚本。创建一个新项目(可以在线使用 Replit,或者在本地使用 PhpStorm),在该项目中创建一个名为hello2.php的新文件。编辑文件,使其完全匹配列表 1-5 的内容。
I am template text, not PHP code.
print "Hello, world!\n";
I am more template text.
列表 1-5:hello2.php 脚本,包含没有 PHP 代码块标签的模板文本
这个脚本的第一行和第三行是模板文本,执行脚本时应直接输出。中间一行是 PHP 代码,用于输出“Hello, world!”并换行。或者是吗?尝试在命令行终端输入php hello2.php运行这个脚本,看看结果:
% **php hello2.php**
I am template text, not PHP code.
print "Hello, world!\n";
I am more template text.%
输出会逐字复制文件中的三行文本,正如它们在文件中显示的那样。特别是中间那一行,包括了像print关键字、引号和分号等 PHP 代码元素,它们本不应显示。问题在于我们没有加上任何标识 PHP 代码的开闭标签,所以整个脚本被当作模板文本直接输出。
还注意到,输出的最后一行以新的终端提示符(此时是百分号符号)结束。这是因为,当它们出现在 PHP 脚本标签之外作为模板文本时,空格、制表符和换行符会被精确复制。因此,终端会立即接上新的提示符,输出从中断处继续,而不会额外添加换行符。如果我们在hello2.php脚本的末尾添加了一个空行,新的终端提示符将出现在自己的新行上。
让我们更新脚本,解决这两个问题。清单 1-6 显示了修订后的hello2.php版本,并且变更部分已加粗。
I am template text, not PHP code.
**<?php**
print "Hello, world!\n";
**?>**
I am more template text.
清单 1-6:修复 hello2.php,区分模板文本与 PHP 代码
我们在 print 语句前添加了一个打开的标签。这告诉 PHP 解释器,标签之间的内容应作为 PHP 代码进行解释和执行。我们还在脚本末尾添加了一个空行。
如果你重新运行这个脚本,PHP 引擎现在应该会找到包裹在 print "Hello, world!\n";语句前后的起始和结束 PHP 程序标签,因此,除了输出这些标签外,它还会执行那行代码,打印出“Hello, world!”以及换行符。以下是再次执行脚本后的结果:
% **php hello2.php**
I am template text, not PHP code.
Hello, world!
I am more template text.
%
这次注意到脚本的第一行和最后一行已原样输出为模板文本,而中间一行仅包含“Hello, world!”消息,表明它已成功作为 PHP 代码被解释执行。由于我们在清单 1-6 的脚本末尾添加了一个空行,新的终端提示符现在也出现在自己的新行上。
注释
注释是任何编程语言中有用的功能。它们是一种告诉 PHP 引擎忽略 PHP 代码块内某些文本的方式,因此这些文本既不会被输出,也不会被解释为应执行的代码。
注释在计算机程序中可以发挥多个作用。首先,它们是将人类可读的注释嵌入代码中的一种方式,例如对某个功能的解释,或者为什么以某种方式编写代码的原因,或者提醒自己需要做的事情。其次,将一行或多行代码变为注释是调试或尝试其他方法时暂时禁用该代码的一种好方法,而不需要完全删除代码。最后,注释还可以包含特殊内容,供预处理工具(如文档生成器或代码测试工具)使用。
和大多数语言一样,PHP 提供了几种定义注释的方式。单行注释以两个正斜杠(//)开头,格式如下:
// I am a comment and will be ignored.
斜杠后面的所有内容都会被视为注释,在执行代码时会被忽略。这意味着你可以在程序语句后面加注释,且程序语句本身仍会被执行,如下所示:
print 2 + 2; // Should print 4
在这里,执行 2 + 2 将会输出 4,但 PHP 引擎会忽略行尾的 // 应输出 4 注释。
以 /* 开头并以 */ 结尾的注释可以跨越多行。列表 1-7 展示了这种多行注释的语法。
/*
I
am
a multiline
comment.
*/
列表 1-7:多行注释示例
这种注释风格特别有用,尤其是当你有一段较长的代码块,希望临时禁用或注释掉时。
注意
如果你处理或维护的是多年前编写的遗留代码,你可能还会遇到较老的 shell 风格单行注释,它以#字符开头,而不是两个斜杠。尽管这些 shell 风格的注释在 PHP 程序中仍然有效,但// 语法是现代 PHP 编程中首选的单行注释风格。
变量
计算机程序的一个显著特点是它们是动态的,这意味着它们的行为每次执行时会根据不同的数据和事件发生变化。程序的核心就在于使用变量,即在代码中引用数据的命名值或引用。变量使你能够存储值并使用有意义的名称(标识符)引用它们。
它们被称为变量,因为它们所指的值在每次程序执行时可能会变化。例如,一个变量可能代表当前的日期或时间,而程序可能有逻辑根据该变量的值执行某些特定的操作。也许它会在用户生日时显示问候语,或者每天早上 6 点触发一个警报。另一个变量可能表示日志文件的大小,当该文件的大小超过某个阈值时,程序可能会自动备份该文件的内容并开始一个新的文件。
变量的值不仅会在程序的每次运行中发生变化;它们还可以在程序执行的过程中发生变化。例如,表示在线购物车总金额的变量将从 0 开始,并在添加或移除物品时进行更新。一个存储系统中同时登录用户数的变量也会随着程序的运行而变化。在繁忙时段,如果该值非常高,可能需要向系统添加更多的内存或磁盘空间。
创建变量
你可以通过给变量命名并为其赋值来创建一个 PHP 变量。例如,在这里,我们创建了一个名为 $age 的变量,并为其赋值 21:
$age = 21;
PHP 变量名必须以美元符号 ($) 开头,这是 PHP 代码与几乎所有其他编程语言的一个区别。给变量赋值是通过等号 (=) 完成的,这个等号在这里被称为赋值运算符。变量名放在等号的左边,值放在右边。由于设置变量值是一种语句,整个过程以分号结束。
赋值运算符右侧的代码是一个表达式。表达式 是一种能够产生单一值或可以被计算为单一值的东西。最简单的表达式就是一个字面量值,就像这个例子中的数字 21。字面量 是指以其本身的形式表达的值。例如 21(整数二十一)、3.5(浮点数三点五)、true(布尔值 true)和 "Matt Smith"(文本字符串 Matt Smith)都是字面量。
其他表达式更为复杂。它们可能涉及数学计算,包含其他变量,甚至如你在第五章中看到的那样,可能会调用一个函数。在这些情况下,表达式必须被 计算,也就是说,必须先确定其结果值,然后才将该值赋给变量。清单 1-8 展示了一些赋值语句的示例。
<?php
$username = "matt"; // A string literal
$total = 3 + 5; // A calculated expression
$numSlices = $numPizzas * 8; // A calculation with another variable
$timestamp = time(); // A function that returns a value
清单 1-8:将表达式的值赋给变量的示例
我们首先将字符串字面量 "matt" 赋值给 $username 变量。变量可以存储表示多种数据类型的值,我们将在第二章中详细讨论。接着,我们将 $total 变量赋值为计算结果 3 + 5。因此,该变量将存储数字 8。对于 $numSlices 变量的值,我们将另一个变量 $numPizzas 的值乘以 8(在 PHP 中,* 符号表示乘法)。最后,我们将 $timestamp 变量设置为调用 time() 函数得到的值。(你将在第五章中了解更多关于如何从函数中获取值的内容。)
如果你尝试执行清单 1-8 中的代码,它将无法正常工作。PHP 会产生类似以下的警告信息:
PHP Warning: Undefined variable $numPizzas in main.php on line 4
这里的问题是 $numPizzas 变量是 未定义的,意味着它还没有被赋值。在你第一次使用变量之前,始终为其赋值非常重要。 #### 使用变量
一旦你创建了一个变量,就可以在需要引用该变量值的任何地方使用它的名称。例如,清单 1-9 展示了一个程序,演示了如何使用变量来计算并打印给定披萨数量时的总披萨片数。创建一个包含此清单内容的 pizza.php 文件。
<?php
$numPizzas = 1;
$numSlices = $numPizzas * 8;
print $numSlices;
print "\n";
$numPizzas = 3;
$numSlices = $numPizzas * 8;
print $numSlices;
print "\n";
清单 1-9:在 pizza.php 中使用变量
首先,我们将数字值 1 赋给 $numPizzas 变量。然后,我们将 $numPizzas 的值乘以 8,赋给 $numSlices 变量。记住,变量必须以美元符号开始;随着你编写更多 PHP 代码,你会很快习惯这个规则。接着,我们使用打印语句显示 $numSlices 里的值,然后再使用另一个打印语句并加上 \n 换行符来创建一个换行。
如前所述,变量的值可以在程序运行时改变,因此接下来我们将 $numPizzas 变量的值从 1 更新为 3。然后我们再次通过将 $numPizzas 乘以 8 来更新 $numSlices 的值,并打印出新值。以下是命令行执行该程序的结果:
% **php pizza.php**
8
24
注意到 $numSlices 的值在程序执行过程中从 8 变为 24。这些值是基于 $numPizzas 变化的值计算得出的。你可以尝试自己修改 $numPizzas 变量中的数字,看看能得到多少片不同的披萨。
变量命名
在 PHP 中有一些命名变量的规则和约定。首先,如我们之前讨论的那样,所有变量名必须以美元符号开始。如果在引用变量时忘记加上美元符号,PHP 通常会报告一个未定义常量的致命错误,程序将崩溃。(我们将在下一节讨论常量。)
变量名中的第一个字符(即美元符号后的字符)必须是字母(或在某些情况下是下划线)。按照约定,这个字母应该是小写字母。虽然技术上可以使用大写字母,但通常首字母大写是保留给类名而非变量名的。(你将在第 V 部分中开始学习类和面向对象编程。)变量名中的其余符号可以是字母、数字或下划线。
单词变量名通常应该全部小写,例如 $name 或 $total。对于多词变量名,我们有两种常见的命名约定。一种是 蛇形命名法:所有字母小写,单词之间用下划线分隔,例如 $game_lives_remaining 或 $customer_number。另一种是 小驼峰命名法:第一个单词全部小写,后续单词的首字母大写,例如 $gameLivesRemaining 或 $customerNumber。
最重要的原则是,无论选择哪种命名约定,都要保持一致,尽可能遵循 PHP 的风格建议,最重要的是,选择能够清楚传达变量表示内容的名称。像 $customerNumber 这样的名称比像 $custNo 这样缩写的名称更清晰,当然也比像 $x 或 $variable 这样没有意义的变量名更好。
请记住,PHP 变量名是区分大小写的,因此像 $username 和 $userName 这样的标识符会被视为不同的变量。如果你在引用变量时大小写写错了(或其他地方输入错误的变量名),PHP 将不知道你指的是什么。列表 1-10 展示了一个示例。
<?php
$username = "matt";
print $userName;
列表 1-10: 拼写错误的变量名
我们将值 "matt" 分配给 $username 变量,然后尝试打印该变量的值。然而,由于 $userName 的大小写错误,在命令行执行这个脚本时会显示如下警告信息:
PHP Warning: Undefined variable $userName in main.php on line 3
因为 PHP 变量区分大小写,所以 PHP 引擎将 $userName 视为一个完全不同的变量,它之前并未赋值。在 PHP 看来,这与在示例 1-8 中未定义 $numPizzas 变量就尝试使用它是同一个问题。
请记住,虽然 PHP 中的某些部分(如变量名)区分大小写,但语言中的其他部分是大小写不敏感的,意味着大小写不会影响。例如关键字如 if、for、switch 和 print;数据类型如 int 和 string;值如 true 和 false;以及函数和方法名。因此,通常的做法是对语言关键字和数据类型使用小写,对函数和方法名使用小驼峰式命名法。本章末的练习建议了一种编码风格指南,帮助你了解更多关于这些约定的信息。
常量
有些值永远不会改变,例如 π(总是 3.14)或 pH 值的中性值(总是 7)。在代码中引用这些值时,最好使用 常量 而非变量。与变量不同,一旦常量被定义,它的值就不能被更新。按照惯例,常量的名称采用 大写蛇形命名法,即所有字母大写,单词之间用下划线连接,如 MAX_PROJECTS 或 NEUTRAL_PH。与变量不同,常量不以美元符号($)开头。
一些常量是内建于 PHP 语言中的。表 1-1 列出了几个示例。
表 1-1:内建 PHP 常量示例
| 常量 | 描述 | 值 |
|---|---|---|
| M_PI | π,圆周率与直径的比值 | 3.1415926535898 |
| M_E | e,欧拉常数 | 2.718281828459 |
| PHP_INT_MAX | 系统支持的最大整数值 | 对于 64 位系统,通常是 9223372036854775807 |
你也可以通过使用 define() 函数来创建自己的自定义常量。示例 1-11 中的脚本展示了一个例子。
<?php
define("MAX_PROJECTS", 99);
print "The maximum number of projects is: ";
print MAX_PROJECTS;
print "\n";
示例 1-11:定义和打印常量
我们调用 define() 函数来创建一个名为 MAX_PROJECTS 的常量,其值为 99。这样常量在代码中的任何地方都可以使用。接着,我们将常量的值作为消息的一部分打印出来。(如果多个打印语句连续执行且不包含换行符,它们会输出到同一行;最后的打印 "\n" 会添加换行符,确保下一次输出——在本例中是下一个命令行提示符——会出现在新的一行上。)运行此脚本的输出应该如下所示:
The maximum number of projects is: 99
请注意,当我们使用 define() 函数创建常量时,常量的名称必须用引号括起来。如果没有这些引号,创建常量将失败。例如,如果你写 define(MAX_PROJECTS, 99) 而没有引号,PHP 会将 MAX_PROJECTS 视为对一个先前定义的常量的引用,并报错。然而,一旦定义了常量,在引用时就不需要引号了。
注意
PHP 文档中令人困惑的是,将数值和布尔字面量称为 常量 ,虽然它也使用了 字符串字面量 这个术语。在阅读文档时,区分简单常量(字面值本身)和命名常量(例如通过 define() 函数创建的常量)是很有用的。
运算符和操作数
运算符 是我们在编程语言中用来操作数据的符号,比如加号(+)用于数值加法,等号(=)用于给变量赋值。操作数 是运算符操作的数据(字面量、变量或复杂表达式)。例如,数值加法运算符需要两个操作数:加号左边的一个数字和右边的另一个数字,像 2 + 2 或 $price + $salesTax。
PHP 有不同的运算符用于处理不同类型的数据。在本节中,我们将主要关注用于数值的运算符。在后面的章节中,我们还会考虑其他运算符,例如用于比较值的运算符(第二章)和用于操作逻辑真假值的运算符(第四章)。
算术运算符
PHP 有 算术运算符 用于基本的数学计算,例如加法(+)、减法(-)、乘法(*)和除法(/)。这些都是 二元运算符,意味着它们需要两个操作数。PHP 还提供了 ** 运算符用于将数字提高到给定的幂次,以及取余运算符(%),它将一个数字除以另一个数字并返回余数。表 1-2 总结了这些运算符。
表 1-2:六个二元(两个操作数)算术运算符
| 运算符 | 描述 | 示例表达式 | 表达式值 |
|---|---|---|---|
| 加法 | 返回两个操作数的和 | 3 + 1 | 4 |
| 减法 | 返回两个操作数的差值 | 10 - 2 | 8 |
| 乘法 | 返回两个操作数的乘积 | 2 * 3 | 6 |
| 除法 | 返回两个操作数的商 | 8 / 2 | 4 |
| 取余 | 返回第一个操作数除以第二个操作数的余数 | 8 % 3 | 2 |
| 指数运算 | 返回第一个操作数的第二个操作数次方 | 2 ** 3 | 8 |
就像数学运算一样,这些运算符有一个优先级顺序,它决定了当表达式包含多个运算时,如何对其进行求值。乘法、除法和取模的算术运算符具有较高的优先级,而加法和减法的运算符优先级较低。因此,在算术表达式 1 + 2 * 3 中,2 * 3 部分会先被计算出 6,然后 1 + 6 再进行计算,所以整个表达式的结果是 7。你可以使用括号强制优先级;例如,表达式(1 + 2) * 3 的结果是 9,而不是 7,因为括号强制加法部分先被计算为 3,再进行乘法计算。
注意
查看 PHP 文档中的完整运算符优先级列表,www.php.net/manual/en/language.operators.precedence.php 以获取所有 PHP 运算符的优先级顺序。
组合算术赋值运算符
我们已经讨论过基础的=赋值运算符,它将一个值赋给一个变量。其他赋值运算符,如+=和-=,将赋值与算术操作结合起来。这些组合运算符的存在,是因为通常我们需要获取变量中的值,进行计算后将结果重新存储回同一个变量,替换掉之前的值。
举个例子,假设我们有一个\(total 变量,用于跟踪在线购物车中商品的总成本。每次用户添加或移除购物车中的商品时,我们都想通过该商品的成本来改变\)total 的值。我们可以使用常规的=运算符,通过编写类似$total = \(total + 25 或\)total = \(total - 15 来实现。然而,使用组合算术赋值运算符,我们可以通过更简洁的语法完成相同的任务:\)total += 25 或\(total -= 15。这些语句指示 PHP 引擎通过将 25 加到之前的值上,或将 15 从之前的值中减去,来更新\)total 变量的值。
类似的算术赋值运算符也用于其他算术操作。例如,*=运算符将一个变量乘以一个给定值。其他运算符,如/=、%=和**=,分别用于除法、取模和指数运算,但这些运算符的使用频率较低。
增量和减量运算符
数字也可以通过特殊运算符进行递增(增加 1)或递减(减少 1):使用双加号(++)进行递增,使用双减号(--)进行递减。与结合的算术赋值运算符类似,++和--提供了更加简洁的语法,用于完成常见的编程任务。例如,要向\(age 变量的现有值添加 1,我们可以使用基本的赋值运算符并写成\)age = \(age + 1,或者使用算术赋值运算符并写成\)age += 1。然而,使用递增运算符时,我们只需写\(age++。同样,\)age--会从$age 的值中减去 1。这些是一元运算符的例子,意味着它们只需要一个操作数。
当你想在表达式中使用递增或递减操作的结果时,运算符在值之前或之后的位置很重要。假设\(age 包含整数值 21。表达式\)age++将返回\(age 的当前值(21),然后递增该变量。另一方面,++\)age 将首先应用递增操作,然后返回变量的新值(22)。示例 1-12 说明了这一区别。
<?php
$person1Age = 21;
print "Person 1 age = ";
❶ print ++$person1Age;
print "\nPerson 1 age (after increment) = ";
print $person1Age;
$person2Age = 21;
print "\nPerson 2 age = ";
❷ print $person2Age++;
print "\nPerson 2 age (after increment) = ";
print $person2Age;
示例 1-12:演示前后递增运算符的区别
在这里,我们将\(person1Age 和\)person2Age 都设置为 21,然后使用++递增每个人的年龄。然而,由于在一种情况下使用了++\(person1Age ❶,而在另一种情况下使用了\)person2Age++ ❷,所以打印出的结果不同,正如脚本输出所示:
Person 1 age = 22
Person 1 age (after increment) = 22
❶ Person 2 age = 21
Person 2 age (after increment) = 22
每次打印\(person1Age 时,我们都会在输出中看到 22。这是因为将++运算符放在变量前面,确保它的值在第一次打印之前从 21 递增到 22。相反,将++运算符放在\)person2Age 后面,则会先看到它原始的值 21 ❶,因为它强制递增操作发生在变量值在表达式中使用之后。
为了避免关于是否使用递增前后值的混淆,许多程序员选择使用两行代码:一行用于递增变量的值,另一行用于使用该新值,如示例 1-13 所示。
$person1Age = 21;
$person1Age++;
print $person1Age;
示例 1-13:将递增操作与打印语句分开
在这里,我们使用一个语句只对$person1Age 进行递增,另一个语句只打印该变量的值。这明确确保了最终输出将是 22。
摘要
本章中我们介绍了一些重要的 PHP 编程基础知识。我们探讨了两种创建和运行 PHP 程序的方式:使用 Replit 在线环境和在本地计算机上使用 PhpStorm IDE。我们涵盖了语句、表达式、变量、常量和运算符,这些都是计算机程序的关键构建块,并讨论了如何使用注释来帮助使代码对人类更易读,并在开发和调试程序时暂时禁用代码块。我们还首次了解了如何将 PHP 程序语句与不变的模板文本交织在一起,这是创建能够动态定制返回给用户网页的 web 应用程序的基本技术。
练习
1. 访问 PHP 网站,www.php.net,并了解该语言文档页面的布局。在你编写 PHP 程序时,这些文档将是一个很好的参考。
2. 访问另一个实用的在线 PHP 资源,PHP the Right Way (phptherightway.com)。这个网站汇集了 PHP 编程社区中的最佳实践。如今,社区中的几乎每个人都遵循相同的代码风格规范,这使得不同程序员的 PHP 代码易于阅读、理解和贡献。你可以在 phptherightway.com/#code_style_guide 上阅读这些规范,同时你还可以找到来自 PHP Framework Interop Group(PHP-FIG)的特定风格推荐链接,PHP-FIG 是一个由国际 PHP 专业人士组成的非正式小组,致力于推动语言的编码标准。
3. 使用注释禁用以下代码中的部分行,使得只有 Cat、Dog 和 Helicopter 被打印出来:
<?php
print "Cat\n";
print "Elephant\n";
print "Dog\n";
print "Helicopter\n";
print "Bus\n";
print "Spacecraft\n";
4. 编写一个脚本,创建一个包含你名字的 $name 变量,并使用该变量打印出“我的名字是你的名字”,然后加上换行符。
第二章:2 种数据类型

在本章中,我们将探讨 PHP 中可用的数据类型。我们还将考虑如何强制将一个值转换为指定的数据类型(类型转换),以及 PHP 在何种情况下会自动尝试转换数据类型,以便让表达式的各个部分能够协同工作(类型自动转换)。
数据类型是程序中值的分类,用于指定 PHP 引擎如何解释该值,从而确定可以对其应用哪些操作。例如,如果一个值是整数,PHP 引擎知道可以进行加法和乘法等操作,而且这些操作的结果仍然是整数;同时,PHP 引擎知道,整数除法的结果可能是另一个整数或浮动小数(十进制)数值。
了解哪些数据类型可用——以及何时和如何改变一个值的数据类型——在处理输入、执行计算和输出数据时至关重要。如果你不了解正在操作的数据类型,或者不知道这些数据在执行不同操作时如何反应,你可能会得到意外的结果。
PHP 数据类型
在第一章中,我们将单词“matt”存储在一个变量中,并将数字 99 赋值给一个常量。这些值属于不同的数据类型:一个是字符串,另一个是整数。总的来说,PHP 有 10 种内置数据类型,分为三类,如图 2-1 所示。

图 2-1:PHP 数据类型
目前,我们主要关注四种标量数据类型,它们一次只能保存一个值。我们还会关注一下特殊的 NULL 数据类型。在后面的章节中,你将学习两种复合数据类型——数组(在第八章和第九章中)和对象(在第 V 部分中),它们可以存储和操作多个值的集合。资源特殊类型以及可调用和可迭代的复合类型仅用于复杂和特殊的场景,在本书中不会讨论。
标量数据类型
四种标量(单值)数据类型分别是字符串、整数、浮点数和布尔值。字符串类型用于文本,整数类型用于整数,浮点数类型用于浮动小数(十进制)数字,而布尔值类型则用于布尔真/假值。
让我们使用 PHP 的交互模式来探索标量数据类型。该模式允许你在命令行中输入单个 PHP 语句并立即查看结果。我们将在接下来的章节中使用交互模式来快速演示基本概念并获取即时反馈,而不需要编写完整的 PHP 脚本。只需在命令行中输入 php -a 启动交互模式,然后输入以下内容:
php > **$username = "matt";**
php > **print gettype($username);**
string
在这里,我们再次将“matt”赋值给\(username 变量。然后我们使用 PHP 内置的 gettype()函数打印出变量的类型。输出确认\)username 包含的是字符串。
如果你之前看过或者写过像 Java 或 C#这样的强类型语言的代码,你可能已经注意到,在给变量赋值时不需要指定数据类型。PHP 是一种宽松类型的语言,这意味着相同的变量可以在不同的时间存储不同数据类型的值,而 PHP 引擎会自动推断表达式的数据类型。
注意
我们也可以在 PHP 中显式声明数据类型,稍后在第五章中,我们将开始编写函数时这样做。不过现在,当我们处理简单的变量时,我们将让解释器推断数据类型。
在\(唯一用户名(\)username)变量的例子中,值“matt”被推断为字符串。我们可以类似地为变量赋予数字值,无论是否有小数,PHP 会根据情况将其解释为整数或浮动数:
php > **$age = 21;**
php > **print gettype($age);**
integer
php > **$price = 9.99;**
php > **print gettype($price);**
double
在这里,我们看到变量\(age 的值是整数,因此它被解释为一个整数,而\)price 的值包含小数,所以它被解释为…一个双精度浮点数?尽管文档中将浮动值称为 float 数据类型,但出于历史原因(PHP 是一种古老的语言!),当在浮动值上使用 gettype()函数时,它返回 double——这是指用于存储浮动值的双精度格式。然而,PHP 只有一种浮动点数据类型,因此尽管其他编程语言可能对浮动数、双精度数、实数等有不同的精度和内存表示,但在 PHP 中,所有浮动点值都是 float 数据类型(无论 gettype()函数怎么说)。
接下来,我们尝试创建一个布尔类型的变量。请输入以下代码:
php > **$isDutyFree = true;**
php > **print gettype($isDutyFree);**
boolean
php > **print $isDutyFree;**
1
当我们在$isDutyFree 变量上使用 gettype()时,我们看到输出为布尔值。这是 PHP 中 bool 类型的别名;两者通常可以互换使用,但为了避免一些别名不起作用的情况,请始终在代码中使用 bool(在本书中我也会这样做)。
更有趣的是,注意当我们尝试打印$isDutyFree 的值时,我们在输出中看到的数字是 1,而不是 true。这并不是错误。这与布尔值如何被转换或转换成字符串有关。print 命令期望一个字符串,因此无论我们在 print 关键字后提供什么,PHP 引擎都会将其自动转换为字符串表达式。对于布尔类型,true 会被转换为字符串“1”,而 false 会被转换为空字符串(即一个没有内容的字符串,表示为一对空的引号:“”)。我们将在本章稍后讨论通过手动强制转换和自动类型转换进行的类型转换。
要查看$ isDutyFree 的实际布尔值,请使用内置的 var_dump()函数,而不是 print。这个有用的函数会输出有关变量的信息。当学习 PHP 和调试时,知道变量在代码执行的某一时刻的值非常有帮助:
php > **$isDutyFree = true;**
php > **var_dump($isDutyFree);**
bool(true)
var_dump()的输出确认$ isDutyFree 的数据类型是 bool,并且其值为 true。
特殊的 NULL 类型
PHP 有一种特殊的数据类型,用常量 NULL 或 null(不区分大小写)表示。在三种情况下,一个变量是 NULL。第一种情况是,变量从未被赋值,如此处所示:
php > **var_dump($lastName);**
Warning: Undefined variable $lastName in php shell code on line 1
NULL
当我们尝试对$ lastName 使用 var_dump()而没有为该变量赋值时,首先会收到一个警告,表示$ lastName 未定义。然后我们会看到该变量因为没有被赋值而评估为 NULL。
其次,如果一个变量显式地将常量 NULL 赋值给它,那么它的值就是 NULL:
php > **$firstName = NULL;**
php > **var_dump($firstName);**
NULL
在这里,我们看到一个重要的区别,即一个变量从未被赋值(如前面示例所示),与一个变量包含 NULL 值之间的差异。在前一种情况,var_dump()会产生警告,而在这种情况下我们不会收到警告;我们只会看到变量的值(NULL)被打印出来。将 NULL 赋给一个变量是可以的,就像给变量赋其他值一样。
最后,如果一个变量被unset,即使用内置的 unset()函数清除了其值,那么它将变为 NULL:
php > **$lastName = "Smith"**
php > **var_dump($lastName);**
string(5) "Smith"
php > **unset($lastName);**
php > **var_dump($lastName);**
Warning: Undefined variable $lastName in php shell code on line 1
NULL
在这里,我们为$ lastName 赋一个值,然后使用 unset()将其值删除。当我们尝试在 unset 之后对$ lastName 使用 var_dump()时,我们会收到与之前相同的警告,并看到它的值为 NULL。删除变量就像从未给它赋过值一样。
在更复杂的程序中处理变量和数据项时,有时需要设计逻辑来处理遇到 NULL 的情况。例如,如果你在创建数据库连接时遇到问题,连接变量将被设置为 NULL。在另一个例子中,如果你期望传递一个对象的引用(例如,已登录的用户),但没有这样的对象存在,那么该变量将为 NULL。我们将在第五部分和第六部分中讨论面向对象编程和数据库时,探讨这些情况。
测试数据类型的函数
PHP 有许多函数根据提供的变量或表达式是否属于某种数据类型来返回 true 或 false。这些函数包括 is_string()、is_int()、is_float()、is_bool()和 is_null()。这些函数非常有用,如果你需要在操作变量之前确认它是某种特定类型,或者反之,你需要检查变量是否不是 NULL。以下是这些函数的示例:
php > **$gpa = 3.5;**
php > **var_dump(is_string($gpa));**
bool(false)
php > **var_dump(is_int($gpa));**
bool(false)
php > **var_dump(is_float($gpa));**
bool(true)
php > **$middleName = NULL;**
php > **var_dump(is_bool($middleName));**
bool(false)
php > **var_dump(is_null($middleName));**
bool(true)
我们的变量$ gpa 包含一个小数值,因此只有 is_float()为 true。同样,$ middleName 包含 NULL,因此将其传递给 is_null()会返回 true。
PHP 的一些类型检查函数适用于更广泛的数据类型。例如,is_numeric()函数对 int 或 float 类型的变量返回 true:
php > **$gpa = 3.5;**
php > **$age = 21;**
php > **var_dump(is_numeric($gpa));**
bool(true)
php > **var_dump(is_numeric($age))**;
bool(true)
在这里,我们看到十进制值 3.5 和整数值 21 都通过了 is_numeric()测试。对于仅包含数字字符的字符串,is_numeric()同样返回 true,但如果混入了非数字字符,则返回 false:
php > **$price = "9.99";**
php > **var_dump(is_numeric($price));**
bool(true)
php > **$price = "9.99 dollars";**
php > **var_dump(is_numeric($price));**
bool(false)
当$price 包含字符串"9.99"时,is_numeric()返回 true。然而,当我们在字符串末尾添加美元这个词时,is_numeric()变为 false。
类型转换
在某些情况下,PHP 引擎会自动将一个值从一种数据类型转换为另一种数据类型。这被称为类型转换。请看这个示例:
php > **$answer = "1" + 3;**
php > **var_dump($answer);**
int(4)
在这里,我们尝试将字符串"1"和整数 3 相加。当 PHP 引擎计算这个表达式并将结果存储到$answer 时,它会看到加号运算符(+),并假设进行的是数值加法。因此,PHP 引擎会检查这两个操作数(加号两边的值),并尝试将它们解释为数字(浮动数或整数)。3 已经是一个整数,但字符串"1"会被转换(转换)成一个整数,以允许加法操作进行。最终,我们得到整数 4 作为答案。
在六种情况下,PHP 会自动将表达式转换为不同的类型:数值上下文、字符串上下文、比较上下文、逻辑上下文、函数上下文和位运算(整数和字符串)上下文。接下来我们将讨论其中的一些上下文。
数值上下文
当一个表达式包含算术运算符时,PHP 会尝试将操作数转换为整数或浮动数。这通常发生在一个或多个操作数是字符串的情况下,正如我们刚才看到的"1" + 3 示例。我们将在第五章中看到一个示例,说明函数参数如何被强制转换为整数。布尔值也可以转换为整数;true 变为整数 1,false 变为整数 0。
在数值上下文中,有两个重要的问题:结果会变成整数(int)还是浮动数(float),当字符串同时包含数字和非数字字符时会发生什么?
整数与浮动数
如果算术表达式中的任一操作数是浮动数(或不能解释为整数),则两个值都会被转换为浮动数,并执行浮动数运算。否则,两个值都会被转换为整数,并执行整数运算。例如,当两个操作数都是整数时,结果是整数:
php > **$answer = (1 + 1);**
php > **var_dump($answer);**
int(2)
当一个操作数是整数,另一个操作数是能转换为整数的数值字符串时,两个操作数都会变成整数,结果也是整数:
php > **$answer = (1 + "1");**
php > **var_dump($answer);**
int(2)
在将字符串转换为数字时,会忽略前导和尾随的空白字符,因此如果我们在字符串的两端添加空格,仍然会得到相同的结果:
php > **$answer = (1 + " 1 ");**
php > **var_dump($answer);**
int(2)
请注意,字符串中“1”字符前后多余的空格没有影响,字符串仍然会转换为整数 1。
当两个操作数都是数字且其中一个操作数是浮动数时,两个操作数都会转换为浮动数,结果也是浮动数:
php > **$answer = (1.5 + 1);**
php > **var_dump($answer);**
float(2.5)
为了得到 2.5 的结果,整数 1 会在幕后被转换为浮动数,然后才进行加法运算。当一个操作数是整数,另一个是可评估为浮动数的数字字符串时,也会发生相同的过程。两个操作数都会转换为浮动数,结果是浮动数:
php > **$answer = (1 + "9.9");**
php > **var_dump($answer);**
float(10.9)
简而言之,如果需要浮动数运算,PHP 会使用浮动数运算。否则,使用整数运算。
数字、前导数字和非数字字符串
PHP 区分数字字符串(其内容完全评估为整数或浮动数)和前导数字字符串(以数字字符开头,但也包括非数字字符,如字母或特殊符号)。当前导数字字符串被转换为整数或浮动数时,从第一个非数字字符开始的部分会被丢弃。如果一个字符串以非数字字符开头,那么整个字符串被视为非数字,即使它也包含数字,也无法转换为数字。
注意
关于字符串以非数字字符开头的规则的例外是以空格开头并后跟数字字符的特殊情况。这样的字符串会被视为数字字符串,因为前导(或尾随)空格会被忽略。
让我们尝试使用一个前导数字字符串进行数字加法,该字符串评估为整数:
php > **$answer = (1 + "1 dollar");**
Warning: A non-numeric value encountered in php shell code on line 1
php > **var_dump($answer);**
int(2)
这里“1 dollar”是一个前导数字字符串。当用于算术表达式时,开头的“1”会转换为整数,而结尾的“ dollar”会被忽略。请注意,PHP 会对此发出警告,但仍然进行类型转换。由于两个操作数都可以转换为整数,结果是整数。
带有前导数字字符串的加法(该字符串可评估为浮动数)也以相同的方式工作:
php > **$answer = (1 + "9.99 dollars");**
Warning: A non-numeric value encountered in php shell code on line 1
php > **var_dump($answer);**
int(10.99)
在这种情况下,“9.99 dollars”是一个前导数字字符串,其开头评估为浮动数 9.99。由于两个操作数都可以转换为浮动数,因此结果是浮动数。同样,由于前导数字字符串,PHP 会发出警告。
相反,如果你尝试在算术表达式中使用非数字字符串,将会得到一个类型错误,意味着无法使用给定的数据类型执行操作。这会中断代码的执行。下面是一个示例:
php > **$answer = (1 + "April 1");**
Warning: Uncaught TypeError: Unsupported operand types: int + string in
php shell code:1
Stack trace:
#0 {main}
thrown in php shell code on line 1
这里“April 1”是一个非数字字符串,因为它以字母开头,而不是数字。该字符串无法评估为数字,因此会触发错误。如果我们使用空字符串("")也会发生相同的错误:
php > **$answer = (1 + "");**
Warning: Uncaught TypeError: Unsupported operand types: int + string in
php shell code:1
Stack trace:
#0 {main}
thrown in php shell code on line 1
空字符串不像其他一些语言那样评估为 0。它是一个非数字字符串,因此在算术表达式中会导致类型错误。
字符串上下文
在某些情况下,PHP 会自动将值转换为字符串。首先,涉及打印或回显语句的表达式会被转换为字符串,因为这些命令期望后续的内容是字符串。例如,您已经看到,当布尔类型的值作为打印语句的一部分时,它们会被转换为字符串 "1"(表示 true)或 ""(表示 false)。类似地,数字也会转换为其字符串等价物。
其次,涉及字符串连接运算符(.)的表达式也会被转换为字符串,就像在字符串内部解析的表达式一样。我们将在第三章中讨论这些主题。
比较上下文
类型转换还会发生在比较两种不同数据类型的值时。比较表达式会评估为布尔值 true 或 false,通常您将其用于决策逻辑(如我们将在第四章中讨论的)。当 PHP 引擎看到一个比较运算符,例如 ==(表示等于)或 >(表示大于)时,它会知道这是一个比较表达式。PHP 有规则来确定这些表达式如何根据涉及的数据类型进行转换和评估。
相同值与相等值
PHP 在相同值和相等值之间做出了重要区分。只有当两个表达式在类型转换之前是相同的数据类型并且包含相同的值时,它们才被视为相同。相比之下,两个表达式如果在类型转换之后包含相同的值,则被视为相等。
我们使用不同的运算符来测试身份和相等性。三重等号 (=) 是 相同运算符,而双等号 () 是 等于运算符。考虑以下比较字符串 "1" 和整数 1 的示例:
php > **var_dump("1" === 1);**
bool(false)
php > **var_dump("1" == 1);**
bool(true)
首先,我们尝试使用相同运算符进行比较。由于操作数的数据类型不同,因此评估结果为 false。接下来,我们尝试使用等于运算符进行比较。这一次,结果为 true,因为 PHP 会在进行比较之前将字符串 "1" 转换为整数。
PHP 还有用于“不相同” (!==) 和“不等于” (!=) 的运算符。考虑这些整数和浮点数之间的比较:
php > **var_dump(1 !== 1.0);**
bool(true)
php > **var_dump(1 != 1.0);**
bool(false)
使用不相同运算符时,比较结果为 true,因为值的类型不同。使用不等于运算符时,数字首先被转换为相同类型。这样它们就具有相同的值,因此不等于比较结果为 false。
注意
PHP <> 运算符等价于 != 运算符;两者都表示“不同”。就个人而言,我总是使用 != 运算符,因为感叹号(!)在其他上下文中也表示“不是”。
字符串与数字
自 PHP 8.0 起,当将字符串与数字进行比较时,如果该字符串是数字字符串,则进行数字比较。否则,进行字符串比较。我们在测试整数 1 和数字字符串"1"的相等性时看到过数字比较。如果我们尝试使用一个以数字开头的字符串进行相同的比较,结果将为假:
php > **var_dump(1 == "1 dollar");**
bool(false)
尽管"1 dollar"以数字开始,但它不是一个完全的数字字符串。因此,PHP 将整数 1 转换为字符串"1"并进行字符串比较。这两个字符串不相等,因此结果为假。
仅使用数字比较完全数字字符串有两个重要的影响,即任何前导或尾部的空格会被忽略,并且空字符串不被认为是数字字符串,因此它不等于数字 0,如下所示:
php > **var_dump(0 == "");**
bool(false)
在这个比较中,由于空字符串不是数字,整数 0 被转换成字符串"0"。然后,字符串"0"和""被比较并发现它们不相等。
小于和大于
在处理数字时,使用小于(<)、大于(>)、小于或等于(<=)或大于或等于(>=)运算符是直接的,因为是否一个数字小于、等于或大于另一个数字非常清晰。以下是一些例子:
php > **var_dump(1 < 2);**
bool(true)
php > **var_dump(1 <= 1.01)**
bool(true)
php > **var_dump(2 >= 2);**
bool(true)
php > **var_dump(2 > 2);**
bool(false)
你也可以将这些运算符用于非数字数据类型,在这种情况下,PHP 有多种规则来评估比较。例如,布尔值 true 被认为大于 false,也大于 NULL:
php > **var_dump(true > false);**
bool(true)
php > v**ar_dump(true > NULL);**
bool(true)
字符串之间是逐个字符进行比较的,字母表中后面的字母被认为大于前面的字母:
php > **var_dump("abc" < "acb");**
bool(true)
小写字母被认为大于大写字母,但是:
php > **var_dump("a" > "B");**
bool(true)
字符串通常被认为大于任何数字,如以下情况所示:
php > **var_dump("abc" > 123);**
bool(true)
php > **var_dump("one" > 1000000);**
bool(true)
然而,存在一些对这些通用规则的例外。如我们之前讨论过的,当完全数字的字符串与数字进行比较时,字符串会先转换为数字,然后进行数字比较。例如,字符串"15"会变为整数 15 用于比较:
php > **var_dump("15" < 19);**
bool(true)
另一个例外是以特殊字符开头的字符串,如:! # $ % & ' () * + , - . /。这种字符串总是被认为小于数字:
php > **var_dump("*77" < 5);**
bool(true)
如果一个数字与一个以数字开头但包含其他字符的字符串进行比较,数字会被转换为字符串,字符串逐字符比较:
php > **var_dump("1a" > 10);**
bool(true)
php > **var_dump("1a" > 20);**
bool(false)
在第一个例子中,整数 10 在与字符串"1a"比较之前被转换为字符串"10"。第一个字符相同,但字符 a(字母)被认为大于字符 0(数字)。在第二个例子中,字符串"20"中的数字 2(经过转换)被认为大于字符串"1a"中的数字 1,因此该表达式为假。
了解这些字符串-数字和布尔比较规则是有用的,但依赖它们可能是危险的。最好使用验证逻辑先将字符串转换为数字,然后进行简单的数字比较。例如,我们将在第十二章中测试从网页表单接收的价格是否为数字。
飞船操作符
PHP 语言中相对较新的一个特性是飞船操作符(<=>)。与 true 或 false 不同,使用该操作符时,根据比较的两个表达式,会返回 0、1 或-1 的整数值。如果两个表达式相同(经过任何类型转换后),该操作符返回 0;如果第一个表达式大于第二个,返回 1;如果第二个表达式大于第一个,返回-1。例如:
php > **var_dump(11 <=> 22);**
int(-1)
php > **var_dump(55 <=> 22);**
int(1)
php > **var_dump("22" <=> 22);**
int(0)
在第一个案例中,我们看到输出是-1,因为 11 小于 22。在第二个案例中,我们看到输出是 1,因为 55 大于 22。在最后一个案例中,我们看到输出是 0,因为"22"和 22 经过类型转换后是相同的。
飞船操作符可能看起来像是小于、大于和等于操作符的奇怪结合。然而,它在将数据集合排序为所需的顺序时特别有用,因为某些排序函数需要这种 0、1 或-1 的编码方案。
逻辑和其他上下文
当期望逻辑值为真或假时,PHP 将把其他类型的值转换为布尔类型。这三种逻辑类型转换上下文如下:
-
逻辑操作符,如 AND (&&) 和 OR (||)
-
三元操作符(?)
-
条件语句,如 if 和 switch
我们将在第四章中讲解所有这些逻辑上下文。
类型转换也可能发生在函数上下文和位运算上下文中。我们将在第五章探讨函数上下文(即在函数签名中评估参数)。位运算上下文在网页应用中很少使用,超出了本书的范围。
类型转换
类型转换是指显式地将一个表达式或变量转换为所需的数据类型。手动类型转换与 PHP 引擎自动执行的类型转换相对立。要将表达式的值转换为特定类型,只需在表达式前加上括号并指定新数据类型。例如,(float)21 确保值 21 被处理为浮动数,而不是整数。以下是转换各种标量数据类型的示例:
php > **$age = (int)20.5;**
php > **var_dump($age);**
❶ int(20)
php > **$price = (string)9.99;**
php > **var_dump($price);**
string(4) "9.99";
php > **$inventory = (bool)0;**
php > **var_dump($inventory);**
❷ bool(false)
请注意,从浮动数转换为整数时,会截断小数点后的所有内容,实际上是向下舍入到最接近的整数 ❶。当从数字转换为布尔值时,0 变为 false ❷,而任何其他数字值(包括负数!)都变为 true。
类型转换是 PHP 中较少使用的特性之一。它的一个使用示例是轻松获取浮动数的整数部分,例如在比较两个时间戳时,获取秒数的整数部分。
总结
本章向你介绍了 PHP 的四种标量数据类型,以及特殊的 NULL 类型。你了解了包含 NULL 的变量与因未定义或未设置而评估为 NULL 的变量之间的区别,并且通过使用像 is_int()和 is_null()这样的函数,练习了测试变量是否为特定类型。稍后,这将帮助你编写代码,仔细测试值以应对从用户或外部数据源(如数据库)接收输入时可能出现的情况。
本章还向你展示了表达式的数据类型如何变化,可以通过类型转换自动或手动地进行。你学习了类型转换可能发生的上下文,以及评估不同数据类型的表达式的规则。理解何时以及如何进行类型转换将帮助你避免在处理混合类型数据时出现意外的结果。
练习
1. 编写一个脚本,使用整数转换将浮动数字向下取整。执行以下操作:
a. 创建一个包含 55.9 的$scoreFloat 变量。
b. 创建第二个变量\(scoreInt,将\)scoreFloat 转换为整数并赋值给它。
c. 打印出$scoreFloat 的类型,然后是它的值,再输出一个换行符。
d. 打印出$scoreInt 的类型,然后是它的值,再输出一个换行符。
你的程序输出应如下所示:
double scoreFloat = 55.9
integer scoreInt = 55
将$age 变量赋值为整数 21,并使用var_dump输出其值。然后将该变量赋值为 NULL,再次使用var_dump输出。最后,使用unset删除该变量,再次使用var_dump输出。请注意,当变量赋值为 NULL 与被 unset 时,输出结果是不同的。
第三章:3 字符串与字符串函数

在本章中,我们将深入探讨字符串,包括如何创建它们、如何组合它们,以及如何使用 PHP 的许多内置字符串函数来搜索、转换和处理它们。几乎每个 PHP 程序和 Web 应用程序都涉及文本,因此了解如何创建和操作字符串非常重要。
本章介绍了 PHP 的四种字符串写法风格:使用单引号或双引号,或者在较长的多行字符串中使用 heredoc 或 nowdoc。这些风格有不同的特点,例如能否嵌入变量或表示特殊符号。尽管这些风格有所不同,它们最终都会得到相同的结果:一个字符串数据类型的值,也就是一串字符。
空白字符
在我们讨论可见字符的字符串(如字母和数字)之前,先谈谈空白字符,即那些你看不见的字符。这些字符在打印时不会使用任何墨水(例如空格字符、制表符字符、换行符字符等等)。有时区分水平空白字符(如空格和制表符)与垂直空白字符(如换行符和垂直制表符)是很有用的。
当你编写代码时,你使用的空白字符的细节通常对代码的执行并不重要。只要表达式之间至少有一个空白字符(例如空格或换行符),是否有额外的空白字符(如额外的空格或制表符)并不重要。例如,PHP 引擎会将以下四个语句解释为完全相同,忽略变量名和等号两边的多个空格、制表符和换行符,以及字符串两边的空白字符:
$lastName = 'Smith';
$lastName = 'Smith';
$lastName = 'Smith' ;
$lastName
=
'Smith'
;
然而,当你声明或处理字符串表达式中的内容时,你必须精确地使用空白字符。否则,单词可能会粘在一起,没有空格,或者某些文本可能会出现在不同的行上。例如,这四个语句中的字符串都是不同的,因为多余的空白字符在引号内:
$lastName = 'Smith';
$lastName = ' Smith';
$lastName = 'Smith ';
$lastName = ' Smith ';
空白字符不仅仅来源于你写的代码。当你从用户或外部软件系统(如应用程序编程接口(API))获取输入字符串时,也可能会引入空白字符。在这种情况下,你通常需要验证该输入并修剪其开头和结尾的任何不必要的空白字符。同样,你可能希望将字符串中的任何制表符或换行符替换为单个空格(有时用户在输入时不小心按下功能键,导致输入中出现了意外的、不可见的空白字符)。你将在本章中学习如何做这些事情(例如,参见“移除所有不必要的空白字符”在第 60 页)。
PHP 中最简单的字符串类型是用单引号括起来的,例如 'matt smith'。几乎所有出现在单引号内的内容都会被字面处理,意味着如果打印该字符串,内容将按原样逐字符输出。
PHP 只有两个特殊的单引号字符串情况。首先,由于单引号用于限定字符串,因此必须有一种机制将单引号包含在字符串内部。否则,单引号会被解释为字符串的结束。解决方案是在单引号前加上反斜杠('),就像 'matt smith's string' 中那样。当这个字符串打印时,单引号会显示出来,但反斜杠不会显示,正如你在 PHP 的交互模式中所看到的那样:
php > **print 'matt smith\'s string';**
matt smith's string
这种在字符串中使用特殊字符序列,并让 PHP 解释为某个特定字符的技术被称为 转义。由于反斜杠需要用来转义单引号,因此也必须有一种方法在单引号字符串中指定反斜杠字符。为此,可以写两个反斜杠:\。
' 和 \ 这两个转义序列是 PHP 引擎在单引号字符串中唯一会解释为其他意义的字符。你可能知道的其他转义序列,例如 \n 用于换行,在单引号字符串中不会被识别。
然而,这并不意味着你不能在单引号字符串中包含换行符。为此,只需在字符串中你希望出现换行的地方添加换行符,PHP 会按你写的方式精确地再现这些换行符。实际上,PHP 要求用分号来声明语句结束的原因之一,是为了允许将单个语句分布在多行中。列表 3-1 展示了一个跨多行的字符串。
<?php
print 'the
cat
sat on
the mat!
';
列表 3-1:包含换行符的字符串脚本
这个脚本包括一个打印语句,输出包含多个换行符的字符串。以下是在命令行运行此脚本的输出:
the
cat
sat on
the mat!
输出包括了字符串中所有的换行符。 ### 字符串连接:连接
经常你需要将多个字符串、变量或常量组合成一个单一的字符串表达式。你可以通过 PHP 的字符串连接运算符实现,表示为句点(.)。考虑以下在 PHP 交互模式中的例子:
php > **$name = 'Matt Smith';**
php > **print 'my name is ' . $name;**
my name is Matt Smith
我们声明了变量 $name,其值是字符串 'Matt Smith'。然后,我们使用字符串连接运算符将这个变量的值与另一个字符串组合,并打印出结果的更长字符串。注意,在字符串 'my name is ' 和闭合的单引号之间有额外的空格。连接运算符不会在连接的字符串之间添加任何空格,所以这个额外的空格是为了防止像 isMatt 这样的字符串被打印出来。
当在一行中连接字符串时,良好的编程实践是在句点的两边添加空格,以提高可读性,如前面的示例所示。你也可以将这样的语句分布在几行上,这有助于使长字符串表达式更具可读性,正如清单 3-2 中的脚本所示。这展示了我个人的偏好,即缩进语句的每一行,并让每行以连接操作符开始,这样就能清楚地表明每一行都在向字符串表达式追加内容。
<?php
$name = 'Matt Smith';
print 'my name is '
. $name
. ', I\'m pleased to meet you!'
. PHP_EOL;
清单 3-2:一个用于连接并打印多个字符串的脚本
该脚本打印出一个由四个较短字符串连接而成的字符串,其中包括前一个示例中的两个字符串。请注意,我们已经转义了单引号,以在第三个字符串中的单词 I'm 中创建撇号。
表达式末尾的第四个字符串是特殊的字符串常量 PHP_EOL,代表行结束符。这是一个包含系统适用字符(或字符组)的字符串,用来将光标移动到命令行终端的下一行的开始位置(就像按下 ENTER 键一样)。由于不同操作系统在指定行结束符时采用的方式略有不同,过去曾经需要这种特殊常量。现在大多数操作系统的应用程序通常能够处理彼此的文件,因此这已经不再是问题,但该常量依然方便用来确保单引号字符串后面的下一个终端提示符出现在新的一行。以下是运行此脚本时在命令行中的输出:
% **php multi_line.php**
my name is Matt Smith, I'm pleased to meet you!
%
我将来自清单 3-2 的脚本保存为名为multi_line.php的文件。运行该脚本时,它将连接字符串并在同一行上打印结果。请注意,接下来的终端提示符(在本例中为百分号字符%)会出现在下一行,这要归功于 PHP_EOL 常量。
如果一个变量已经包含了一个字符串,我们可以使用连接赋值操作符(.=)将另一个字符串表达式追加到该变量的末尾。以下是使用 PHP 交互模式的示例:
php > **$name = 'Matt';**
php > **$name .= ' Smith';**
php > **print** **$name;**
Matt Smith
首先,我们将\(name 变量初始化为字符串'Matt'。然后,我们使用连接赋值操作符将'Smith'追加到\)name 的内容末尾。当我们打印该变量时,可以看到它现在包含了 Matt Smith。### 双引号字符串
PHP 字符串的第二种类型是用双引号括起来的,例如"Matt Smith"。双引号字符串与单引号字符串的不同之处在于,它们会被 PHP 引擎解析,即 PHP 引擎会处理它们,这意味着它们可以包含 PHP 变量。当你在双引号字符串中写入一个变量(以美元符号开头)时,PHP 引擎会查找变量的值,并在打印之前将其插入到字符串中。这通常比使用句点连接单引号字符串与变量内容更加方便,就像我们在上一节中所做的那样。以下是使用 PHP 交互模式的示例:
php > **$name = 'Matt Smith';**
php > **print "My name is $name \nI'm pleased to meet you";**
My name is Matt Smith
I'm pleased to meet you
首先,我们将字符串'Matt Smith'赋值给\(name 变量,就像之前一样。注意,我们本可以使用双引号表示这个字符串,但大多数 PHP 程序员通常仅在需要解析的字符串中使用双引号。接下来,我们打印一个包含\)name 变量的双引号字符串。输出显示 PHP 成功解析了这个字符串,并将 Matt Smith 替换为变量值。
注意,双引号字符串中包含了收缩词 I'm 中的单引号字符。这在双引号字符串中是完全有效的,并且不需要转义。我们的双引号字符串还包含了转义序列\n,用于在输出中间创建换行符。这是双引号字符串中可以使用的多个转义序列之一。表格 3-1 列出了其中一些常见的转义序列。
表格 3-1:双引号字符串中的常见转义序列
| 转义序列 | 描述 |
|---|---|
| \ | 反斜杠 |
| " | 双引号 |
| $ | 美元符号 |
| \n | 换行符 |
| \t | 制表符 |
特别需要注意的是,由于美元符号会让 PHP 引擎解析变量,所以在双引号字符串中包含实际的美元符号时,必须使用$。同样,要在字符串中包含双引号,必须使用"。
你不能在双引号字符串中包含常量,因为 PHP 引擎无法分辨字符串中的字符和常量的名称之间的区别。(回忆一下第一章,常量是没有美元符号的。)其中一个副作用是,你不能在双引号字符串中包含 PHP_EOL 常量来在字符串末尾创建换行符。相反,你应该使用\n 换行符转义序列。
注意
在极少数情况下,当你需要操作系统无关的 PHP_EOL 常量与双引号字符串一起使用时,可以通过字符串连接操作符将常量添加到字符串中,就像你在示例 3-2 中看到的那样。这种情况可能发生在脚本需要根据 PHP 引擎运行的系统精确输出适当的换行符和光标到行首的字符序列时(例如,确保系统文件具有正确的行尾符)。
处理变量名后的字符
当变量名后跟一个空格出现在双引号字符串中时,例如"my name is \(name \nI'm pleased to meet you",PHP 引擎可以轻松识别变量(\)name),并确认其值后应跟一个空格。即使是标点符号,如句号、逗号和冒号,紧跟在变量后面也没有问题,转义序列也是如此,因为这些字符在变量名中是不合法的。例如,在$name 变量后面紧跟逗号是完全可以的:
php > **$name = 'Matt Smith';**
php > **print "my name is $name, and I'm pleased to meet you";**
my name is Matt Smith, and I'm pleased to meet you
然而,如果你希望其他字符紧随变量后面成为字符串的一部分,情况就会变得稍微复杂一些。在变量名的第一个字母(或下划线字符)之后,变量名的字符可以是字母、数字和下划线,因此,如果在双引号字符串中紧跟这些字符,PHP 引擎会将它们视为变量名的一部分。例如,如果我们有一个$weight 变量,并希望其值后面紧跟 kg(例如 80kg),我们不能这样写:
print "$weightkg";
PHP 引擎会报错,提示没有名为$weightkg 的变量。解决这些更复杂的双引号字符串解析任务的方法是将变量名用花括号(大括号)括起来:
php > **$weight = 80;**
php > **print "my weight is {$weight}kg";**
my weight is 80kg
由于使用了花括号,PHP 可以顺利地在\(weight 的值后面直接打印出 kg。请注意,这是*类型转换*字符串上下文的一个例子,如第二章中介绍的那样。当字符串"my weight is {\)weight}kg"被解析时,$weight 的值会从整数 80 转换为字符串'80',并插入到最终字符串中。
包含 Unicode 字符
并非所有字符都可以直接通过键盘输入,或者属于当前计算机系统的语言设置,但这并不意味着你不能在双引号字符串中包含它们。Unicode是一个国际标准,用于声明和处理各种字符和符号,包括普通的英文字母、表情符号、其他字母表的字母等等。每个 Unicode 字符都有一个唯一的十六进制代码。例如,代码 1F60A 对应多个笑脸表情之一。
要在双引号字符串中使用 Unicode 字符,可以以转义序列\u 开始,然后在花括号中提供该字符的十六进制代码。Listing 3-3 展示了声明和打印多个 Unicode 字符的代码。
<?php
$smiley = "\u{1F60A}";
$elephant = "\u{1F418}";
$cherokeeTSV = "\u{13E8}";
print "this is a smiley unicode character\n$smiley\n";
print "followed by some elephants of course\n$elephant $elephant $elephant\n";
print "Cherokee letter TSV\n$cherokeeTSV\n";
Listing 3-3:显示各种 Unicode 字符的脚本
首先,我们声明变量\(smiley、\)elephant 和$cherokeeTSV,分别包含带有相应的笑脸和大象表情符号,以及切罗基 TSV 符号的双引号字符串。然后,我们打印一些包含这些变量的双引号字符串。结果如下:

请注意,我们本可以直接在被打印的双引号字符串中包含 Unicode 字符的转义序列,而不是先将它们赋值给变量。将它们放在变量中可以让我们更容易地在整个脚本中重用它们——例如,打印三只大象,而不仅仅是一只。(PHP 社区特别喜欢大象。)要查看完整的 Unicode 字符及其对应的十六进制代码,请访问home.unicode.org。### Heredocs
Heredoc是双引号字符串的替代品。它们与双引号字符串类似,都会被解析,因此可以包含变量,但它们的不同之处在于,heredoc 通常跨越多行。尽管双引号字符串也可以跨越多行,但许多程序员更喜欢使用 heredoc 来处理多行字符串,因为它们的语法使得它们与周围的代码更明显地区分开来。
要声明一个 heredoc,首先使用 heredoc 运算符(<<<),然后跟上你选择的字符序列,作为分隔符。接着,在新的一行开始编写字符串。当你写到字符串的结尾时,在独立的一行重复你选择的分隔符,后面加上分号以结束语句。
最常用的 heredoc 分隔符是 EOT(即end of text的缩写),但选择哪个分隔符并不重要,只要该字符序列没有出现在被声明的字符串中,并且 heredoc 的开头和结尾的分隔符匹配。为了提高代码的可读性,建议要么始终使用 EOT,要么选择与 heredoc 内容相关的有意义的分隔符,例如,如果它包含 SQL 语句,则使用 SQL,或者如果它包含 HTML,则使用 HTML。列表 3-4 展示了一个包含示例 heredoc 的脚本。
<?php
$age = 22;
$weight = 80;
❶ $message = <<<EOT
my age is $age
my weight is {$weight}kg
❷ EOT;
print $message;
列表 3-4:使用 EOT 分隔符声明并打印的 heredoc 字符串
这段代码创建了变量\(age 和\)weight,分别包含 22 和 80。然后,我们将一个 heredoc 表达式赋值给\(message 变量❶。heredoc 以<<<EOT 开始,它的内容是从下一行开始直到遇到换行符和 EOT;结束❷。最后,我们打印\)message 的内容。以下是结果:
% **php heredoc.php**
my age is 22
my weight is 80kg
%
我将脚本保存为heredoc.php并在命令行运行。请注意,变量\(age 和\)weight 在 heredoc 中成功解析,包括当我们使用大括号时,使得变量后面的字符可以立即输出。还要注意,接下来的命令行提示符在新的一行开始。这是因为 heredoc 在关闭分隔符前有一个空行;heredoc 可以包含换行符。
转义序列
你不能使用 " 转义序列在 heredoc 中写入双引号。如果你在 heredoc 中写入 ",反斜杠将成为字符串中的普通字符,就像双引号一样。在 heredoc 中你根本不需要这个转义序列:由于你不再使用双引号来界定字符串,在字符串内使用双引号不会引起混淆。
你可以在 heredoc 中包含其他转义序列,比如 \t 表示制表符,\n 表示换行。然而,这些转义序列并不是严格必要的,因为你可以直接在编写 heredoc 内容时使用 TAB 和 ENTER 键。如果你的编辑器支持 Unicode,你还可以直接在 heredoc 中输入 Unicode 字符。清单 3-5 展示了一个示例。

清单 3-5:在 heredoc 字符串中声明 Unicode 字符的脚本
我们通过使用 UNICODE 作为定界符来声明这个 heredoc。代码中的 Unicode 字符是直接输入到字符串中的,而不是通过转义序列创建的。运行这个脚本的输出与清单 3-3 的输出相同。
缩进
heredocs 的一个偶尔有用的特性是,如果在结束定界符之前出现了缩进(空格或制表符),PHP 引擎会尝试从 heredoc 的所有行中移除相同数量的缩进。例如,在清单 3-6 中,我们声明并打印了一个 heredoc 字符串,其中每一行(包括包含结束 TEXT 定界符的那一行)都缩进了四个空格。
<?php
$message = <<<TEXT
If the closing delimiter is indented
then that amount of indention
is removed from the lines of the string
TEXT;
print $message;
清单 3-6:带有缩进的 heredoc 脚本
由于这个 heredoc 的每一行都与结束定界符行有相同的缩进,因此在打印字符串时,所有缩进都会从每一行中移除。输出结果如下:
If the closing delimiter is indented
then that amount of indention
is removed from the lines of the string
如果 heredoc 中的任何行比结束定界符行有更多的缩进,这部分额外的缩进将保留在输出中。清单 3-7 展示了一个示例。
<?php
$message = <<<END
I'm the same indention as the ending delimiter (4 spaces)
I have 2 extra spaces
So have I!
I'm back to 4 spaces again
END;
print $message;
清单 3-7:保留 heredoc 中额外缩进的脚本
这个 heredoc 的第一行和最后一行都缩进了四个空格,结束定界符也有四个空格的缩进。heredoc 中间的两行多缩进了两个空格。在输出中,四个空格将从每一行的开头被去掉,留下中间两行的两个空格缩进,如下所示:
I'm the same indentation as the ending delimiter (4 spaces)
I have 2 extra spaces
So have I!
I'm back to 4 spaces again
移除 heredoc 缩进的这个特性主要在 heredoc 作为函数体的一部分时非常有用,因为在这种情况下,所有代码通常都有一定的缩进级别。这样,你就可以编写更整洁的 heredoc,使其符合周围代码的缩进约定。我们将在第五章中介绍函数。
注意
如果 heredoc 中的任何行缩进比结束定界符少,或者它们使用了不同类型的缩进(比如使用了制表符而不是空格),则在运行时会发生错误。
Nowdocs
PHP 字符串的最后一种风格是 nowdoc,它是一个使用 <<< 运算符和定界符编写的未解析字符串。本质上,nowdoc 对未解析的单引号字符串的作用就像 heredoc 对解析的双引号字符串的作用。声明 nowdoc 和 heredoc 的唯一区别是 nowdoc 的起始定界符必须用单引号括起来,如 <<<'EOL'。结束定界符则不使用单引号。
nowdocs 的一种用途是打印 PHP 代码。由于 nowdocs 是未解析的,任何代码,包括变量名,都将被字面复制到字符串表达式中。清单 3-8 显示了一个示例。
<?php
❶ $name = "Matt Smith";
❷ $codeSample = <<<'PHP'
$message = "hello \n world \n on 3 lines!";
$age = 21;
❸ print $name;
print $age;
PHP;
print $codeSample;
清单 3-8:声明一个包含未解析 PHP 代码的 nowdoc 脚本
首先,我们声明一个 $name 变量 ❶。然后,我们通过使用定界符 PHP 声明一个 nowdoc,并将其赋值给 \(codeSample 变量 ❷。(注意,起始定界符用单引号括起来,而结束定界符没有。)nowdoc 包含诸如变量声明(\)age)、带有转义字符的字符串以及对 $name 变量的引用 ❸。当我们打印 nowdoc 时,所有这些内容都没有被解析,正如你在输出中看到的:
$message = "hello \n world \n on 3 lines!";
$age = 21;
print $name;
print $age;
整个 nowdoc 被逐字打印,包括转义序列和字符 $name。nowdoc 中的程序语句没有被执行;它们仅仅成为声明的 nowdoc 字符串的一部分。需要注意的是,nowdoc 中的缩进被去除了,因为它与结束定界符的缩进相匹配。这与 heredoc 的行为一致。
我们已经完成了四种字符串风格的概述。选择使用哪一种通常是个人偏好的问题。一般来说,单引号最适用于不包含需要解析的变量的短字符串。对于没有解析的较长多行字符串,可以考虑使用 nowdocs。如果需要包含解析的变量,则对于较短的字符串使用双引号,或者对于较长的多行字符串使用 heredocs。
内置字符串函数
PHP 有超过 100 个内置函数,用于操作和分析字符串,涵盖从标准任务(如大小写转换)到更专业的任务(如实现哈希算法)。我们将在本节中介绍一些最常用的字符串函数。
如果你是从其他编程语言转到 PHP,可能已经习惯于将这些操作作为直接在字符串本身上调用的方法。然而,由于 PHP 最初并不是面向对象的,因此这些操作作为独立函数存在。
注意
有关 PHP 字符串函数的完整列表,请参见 www.php.net/manual/ref.strings.php。
转换为大写和小写
在处理用户输入时,你经常需要通过确保所有字符串都遵循相同的大小写规则来标准化字符串。这使得比较字符串更容易,或者以一致的格式将文本存储到数据库或发送到 API。为此,PHP 提供了调整字符串大小写的函数。strtolower() 和 strtoupper() 函数分别将所有字母转换为小写或大写。以下是一些使用 PHP 交互模式演示的示例:
php > **$myString = 'the CAT sat on the Mat';**
php > **print strtolower($myString);**
the cat sat on the mat
php > **print strtoupper($myString);**
THE CAT SAT ON THE MAT
我们声明一个包含大写字母和小写字母的字符串。将该字符串传递给 strtolower() 函数会将所有字母转换为小写,而传递给 strtoupper() 函数则会将所有字母转换为大写。
PHP 的 ucfirst() 函数将字符串的首字母大写(如果它还没有大写的话)。这在创建要输出给用户的消息时非常有用;将首字母大写有助于使消息看起来像语法正确的句子:
php > **$badGrammar = 'some people don\'t type with capital letters.';**
php > **print ucfirst($badGrammar);**
Some people don't type with capital letters.
相关的函数 lcfirst() 会将字符串的首字母转换为小写:
php > **$worseGrammar = 'SOME PEOPLE TYPE WITH ALL CAPS.';**
php > **print lcfirst($worseGrammar);**
sOME PEOPLE TYPE WITH ALL CAPS.
通常这不会对语法带来太大的改进,但它在某些情况下是有用的,例如,如果你正在编写一个输出代码的脚本。在这种情况下,为了遵循编程语言的命名约定(例如变量的命名),确保字符串的第一个字符是小写可能很重要。
ucwords() 函数将字符串中每个单词的首字母大写。如果单词之间用空格分隔,PHP 可以区分这些不同的单词:
php > **$mixedCaps = 'some peoPLE use CAPS spoRADically.';**
php > **print ucwords($mixedCaps);**
Some PeoPLE Use CAPS SpoRADically.
注意,如果单词中的后续字母是大写的,它们将保持不变。只有每个单词的首字母会受到影响。PHP 没有类似的函数可以将每个单词的首字母转换为小写。
搜索和计数
PHP 的多个内置字符串函数用于分析目的,例如报告字符串的长度、在字符串中查找字符或子字符串,或计算字符串中字符或子字符串的出现次数。(子字符串是一个较大字符串的部分。)不过,在我们研究这些函数之前,有必要区分字符串中字符的数量和字符在字符串中的位置。
以字符串 'cat scat' 为例。它包含八个字符(中间的空格也算),但是在 PHP 中,字符位置是从零开始编号的。因此,位置 0 上的字符是 c,位置 1 上的是 a,以此类推,直到位置 7 上的 t。这个从零开始计数的方式被称为 零基索引,它在计算机编程中很常见。使用这个系统,我们可以说子字符串 'cat' 在字符串中出现了两次,第一次出现在位置 0,第二次出现在位置 5:
cat scat
01234567
cat cat
有了这个概念,我们来试一下对字符串 'cat scat' 使用一些分析性字符串函数。首先,strlen() 函数报告字符串的长度,如下所示,使用 PHP 交互模式:
php > **$myString = 'cat scat';**
php > **print strlen($myString);**
8
正如预期的那样,这告诉我们 'cat scat' 的长度是 8 个字符。
substr_count() 函数用于计算子字符串在字符串中出现的次数。例如,在这里,我们计算子字符串 'cat' 出现的次数:
php > **$myString = 'cat scat';**
php > **print substr_count($myString, 'cat');**
2
我们向 substr_count() 函数传递两个字符串。第一个是我们要搜索的字符串所在的位置,在这里我们提供的是 $myString 变量。第二个是我们要搜索的字符串目标:在这个例子中是 'cat'。在计算机搜索术语中,这两个字符串通常被称为大海捞针中的针和海,来源于“在大海捞针”这一表达。
注意
在函数的括号中输入的项目,例如 $myString 和 'cat' 在上面的例子中,被称为参数。它们是函数执行任务所需要的数据。我们将在第五章详细讨论函数。
大多数涉及字符串搜索的 PHP 函数,包括 substr_count(),都是区分大小写的,因此在处理时需要注意字母的大小写。例如,如果我们尝试在 'cat scat' 中搜索子字符串 'Cat' 而不是 'cat',我们会得到计数为 0 的结果:
php > **$myString = 'cat scat';**
php > **print substr_count($myString, 'Cat');**
0
strpos() 函数报告子字符串在字符串中出现的起始位置(从零开始计数)。如果子字符串出现多次,只会给出第一次出现的位置。在这里,我们搜索子字符串 'cat' 的第一次出现:
php > **$myString = 'cat scat';**
php > **print strpos($myString, 'cat');**
0
与 substr_count() 相似,我们向 strpos() 函数传递两个字符串作为参数,第一个是大海(haystack),第二个是针(needle)。该函数报告 'cat' 第一次出现的位置为 0。
可选地,你可以向 strpos() 函数提供一个偏移量作为附加参数,这是一个告诉它从不同位置开始搜索子字符串的数字,而不是从字符串的开头开始搜索。在这里,我们告诉函数从位置 2 开始搜索:
php > **$myString = 'cat scat';**
php > **print strpos($myString, 'cat', 2);**
5
这一次,由于函数不是从字符串的开头进行搜索,它会识别出 'cat' 的第二次出现,位置为 5。如果函数没有找到目标子字符串,它会返回 false。
count_chars() 函数分析字符串中包含和不包含的字符。它是一个强大的字符串分析函数,你可能会在评估密码复杂度或进行数据加密和解密任务时使用。它有几个模式,可以通过在调用函数时指定数字来选择。在下面的例子中,我们使用模式 3,它生成一个新字符串,其中包含被分析字符串中所有唯一的字符:
php > **$myString = 'cat scat';**
php > **print count_chars($myString, 3);**
acst
我们在 $myString 上调用 count_chars() 函数,并指定模式 3。结果字符串显示了 'cat scat' 中每个字符的一个实例,并按字母顺序排列。它告诉我们,'cat scat' 仅包含字母 a、c、s 和 t。
注意
count_chars() 函数有其他模式来统计每个字符的出现次数,但结果是以数组形式返回的,所以我们在这里不讨论这些模式。我们将在 第七章 和 第八章 中讨论数组。
提取和替换子串
其他 PHP 函数通过提取字符串的一部分或用其他内容替换部分字符串来操作字符串。例如,substr() 函数提取字符串的一部分,从给定的位置开始。它是这样工作的:
php > **$warning = 'do not enter';**
php > **print substr($warning, 7);**
enter
我们声明一个字符串变量 'do not enter',然后将其传递给 substr() 函数。数字 7 告诉函数从位置 7 开始提取所有字符直到末尾,结果是 enter。
如果你使用负数,函数会从字符串的末尾开始计数来提取。例如,在这里我们使用 -2 来获取最后两个字符:
php > **$warning = 'do not enter';**
php > **print substr($warning, -2);**
er
你可以选择在函数调用中包括第二个数字来指定提取的长度。例如,在这里我们从位置 7 开始提取三个字符:
php > **$warning = 'do not enter';**
php > **print substr($warning, 7, 3);**
ent
strstr() 函数提供了另一种提取字符串部分的技术。默认情况下,它会查找字符串中第一次出现的子串,并从该子串开始提取字符串内容。例如,在这里我们查找子串 '@' 来提取电子邮件地址中的域名和扩展名:
php > **$email = 'the.cat@aol.com';**
php > **print strstr($email, '@');**
@aol.com
该函数查找第一个出现的 @ 符号并返回从该点开始到字符串结尾的所有内容,包括 @ 符号本身。我们也可以使用 strstr() 提取字符串中在第一次出现子串之前的所有内容。为此,向函数调用的末尾添加 true,如下所示:
php > **$email = 'the.cat@aol.com';**
php > **print strstr($email, '@', true);**
the.cat
再次,我们在字符串中查找第一个出现的 @ 符号,但这一次,感谢添加的 true,函数返回的是字符串中从开头到 @ 符号(不包括它)的内容。这给我们返回的是电子邮件地址中的用户名。
注意
strstr() 函数是区分大小写的,但 PHP 提供了一个不区分大小写的版本,称为 stristr() 。
str_replace() 函数查找字符串中所有子串的出现并将其替换为另一个子串。替换的结果作为一个新字符串返回,这意味着原始字符串本身没有被修改。以下是一个示例:
php > **$foodchain = 'dogs eat cats, cats eat mice, mice eat cheese';**
php > **print str_replace('eat', 'help', $foodchain);**
dogs help cats, cats help mice, mice help cheese
php > **print $foodchain;**
dogs eat cats, cats eat mice, mice eat cheese
当我们调用 str_replace() 时,需要提供三个字符串。第一个是要查找的子串(在此示例中为 'eat')。第二个是替换用的子串(在此示例中为 'help')。第三个是要搜索的字符串,我们将其赋值给 $foodchain 变量。该函数通过将所有 'eat' 替换为 'help' 来生成一个新字符串。然后我们打印 $foodchain 的值,确认它没有被函数调用所影响。
要使替换结果保持一定程度的永久性,可以将调用 str_replace() 的结果存储在一个新变量中,如下所示:
php > **$foodchain = 'dogs eat cats, cats eat mice, mice eat cheese';**
❶ php > **$friendchain = str_replace('eat', 'help', $foodchain);**
php > **print $foodchain;**
❷ dogs eat cats, cats eat mice, mice eat cheese
php > **print $friendchain;**
dogs help cats, cats help mice, mice help cheese
这次当我们调用 str_replace() 时,我们将结果赋值给 $friendchain ❶。原始的 $foodchain 变量仍然没有受到替换操作的影响 ❷,但至少现在我们已经有了一个修改后的字符串,可以在之后使用。
另一个替换函数是 substr_replace()。与指定要替换的子字符串不同,这个函数允许你指定替换操作应该发生的位置。以下是一个示例:
php > **$foodchain = 'dogs eat cats, cats eat mice, mice eat cheese';**
php > **print substr_replace($foodchain, 'help', 5, 3);**
dogs help cats, cats eat mice, mice eat cheese
当我们调用 substr_replace() 函数时,首先提供原始字符串(在 $foodchain 中)和替换字符串('help')。然后我们提供两个额外的数字作为参数。第一个数字 5 是原始字符串中替换开始的位置。第二个数字 3 是原始字符串中从指定位置开始替换的字符数。这样会将单词 eat(该单词长度为 3,从位置 5 开始)替换为单词 help,同时保留字符串的其余部分不变。
通过将替换长度设置为 0,我们可以使用 substr_replace() 向字符串中插入子字符串,而不对其进行其他修改,如下所示:
php > **$foodchain = 'dogs eat cats, cats eat mice, mice eat cheese';**
php > **print substr_replace($foodchain, 'don\'t ', 5, 0);**
dogs don't eat cats, cats eat mice, mice eat cheese
这将在 $foodchain 字符串的第 5 位开始插入单词 don’t,而不会替换任何字符。
去除空白字符
通常需要去除字符串开头或结尾的空白字符,这个任务叫做 trimming(修剪)。PHP 提供了三个修剪函数:
trim() 去除字符串开头和结尾的空白字符
ltrim() 去除字符串开头的空白字符(l 代表 left,即左侧)
rtrim() 去除字符串结尾的空白字符(r 代表 right,即右侧)
这三个函数的工作原理相同:给定一个字符串,它们会去除所有的空格、制表符、垂直制表符、换行符、回车符或美国标准信息交换码(ASCII)空字节字符,直到第一个非空白字符和/或字符串最后一个非空白字符为止。例如,这里我们使用 trim() 去除字符串开头和结尾的空格和换行符:
php > **$tooSpacey = " \n\nCAT\nDOG\n\n";**
php > **print trim($tooSpacey);**
CAT
DOG
输出显示,字符串的换行符以及开头和结尾的空白字符已经被去除,但请注意,CAT 和 DOG 之间仍然保留了一个换行符。trim 函数对字符串中间的空白字符没有影响。
你可以选择性地通过在调用函数时,在一个单独的双引号字符串中指定要去除的空白字符。以下是一个示例:
php > **$evenSpacier = "\n\n CAT\nDOG\n\n";**
php > **print ltrim($evenSpacier,** ❶ **"\n");**
CAT
DOG
❷
php >
这次我们使用字符串 "\n" ❶ 来指定仅去除换行符。在输出中,注意到单词 CAT 前的空格被保留,因为函数只忽略了除换行符外的其他字符。下一个提示符前的空行 ❷ 也表明,字符串末尾的换行符保持原样,因为 ltrim() 函数只影响字符串的开头部分。
移除所有不必要的空白字符
要去除字符串中的所有空白字符,而不仅仅是边缘上的空白,可以使用 str_replace() 来查找特定的空白字符,并将其替换为空字符串。例如,这里我们使用这种技巧去除字符串中的所有制表符字符:
php > **$tooTabby = "\tCat \tDog \t\tMouse";**
php > **print $tooTabby;**
Cat Dog Mouse
php > **print str_replace("\t", '', $tooTabby);**
Cat Dog Mouse
分配给 $tooTabby 的字符串包含多个制表符字符。将每个 "\t" 替换为 ''(空字符串)可以去除制表符,同时保留每个单词之间的普通空格。
清单 3-9 将这一技巧进一步推广,反复使用 str_replace() 来移除字符串中的任何空白字符,除了单词之间的单个空格字符。这包括去除制表符、换行符和多个连续的空格字符。
<?php
❶ $string1 = <<<EOT
the
cat sat
\t\t on the
mat
EOT;
❷ $noTabs = str_replace("\t", ' ', $string1);
$noNewlines = str_replace("\n", ' ', $noTabs);
❸ $output = str_replace(' ', ' ', $noNewlines);
$output = str_replace(' ', ' ', $output);
$output = str_replace(' ', ' ', $output);
$output = trim($output);
print "[$output]";
清单 3-9:在字符串中替换所有空白字符(除了单个空格字符)
我们使用 heredoc 来声明 $string1 变量,它包含制表符、换行符以及单词之间的多个空格 ❶。然后我们使用 str_replace() 函数两次,第一次将所有制表符替换为单个空格,第二次将所有换行符替换为单个空格 ❷。(我们没有将它们替换为空字符串,以防制表符或换行符是两个单词之间唯一的字符。)
接下来,我们反复使用 str_replace() 来将两个空格字符的任何实例替换为一个空格 ❸。需要三次函数调用,直到只剩下单个空格。(在第六章中,我们将深入探讨循环,这提供了一种更高效的方式来多次重复相同的代码,或直到满足特定条件。)为了确保万无一失,我们使用 trim() 来移除字符串开始或结束处的任何残留空白,然后再打印出结果字符串,字符串被方括号括起来,便于查看它的起始和结束位置。以下是运行该脚本的输出:
[the cat sat on the mat]
最终的字符串在前后没有空白字符,每个单词之间只有单个空格。所有多余的空白字符都已被移除。
重复与填充
一些 PHP 字符串函数通过重复一个字符或子字符串来生成更长的字符串。例如,要通过重复一个字符串一定次数来创建一个新字符串,可以使用 str_repeat(),像这样:
php > **$lonely = 'Cat';**
php > **print str_repeat($lonely, 5);**
CatCatCatCatCat
这会通过重复 'Cat' 五次,为我们孤单的字符串增添一些“伴侣”。
与重复密切相关的是 填充:一个字符或子字符串被反复添加到字符串的开头或结尾,直到字符串达到所需的长度。填充非常有用,例如,如果你要显示不同长度的多个数字,并希望它们的数字对齐。在这种情况下,你可能会在数字前面添加空格或零作为填充,示例如下:
12 // padded with spaces
1099
000001 // padded with zeros
000855
PHP 有一个 str_pad() 函数用于这样的填充任务。这里,举例来说,我们用连字符(-)填充字符串 'Cat',直到它的长度为 20 个字符:
php > **$tooShort = 'Cat';**
php > **print str_pad($tooShort, 20, '-');**
Cat-----------------
我们调用 str_pad(),提供原始字符串($tooShort)、所需长度(20)和用作填充的字符串('-')。默认情况下,PHP 会将填充添加到原始字符串的右侧,但你可以在函数调用中添加常量 STR_PAD_LEFT 或 STR_PAD_BOTH,以便将填充添加到左侧或将填充均匀地添加到两侧。以下是一些示例:
php > **$tooShort = 'Cat';**
php > **print str_pad($tooShort, 20, '-', STR_PAD_LEFT);**
-----------------Cat
php > **print str_pad($tooShort, 20, '-', STR_PAD_BOTH);**
--------Cat---------
在每种情况下,该函数会添加连字符,直到生成的字符串长度为 20 个字符。
总结
字符串是你在创建每个网页应用时可能都会用到的核心数据类型。在本章中,你学习了声明字符串的四种方式:单引号字符串、双引号字符串、heredoc 和 nowdoc。你看到双引号字符串和 heredoc 是如何解析的,因此可以包含变量,而单引号字符串和 nowdoc 则不能。你还尝试了 PHP 内置的字符串操作函数,并学习了如何使用 . 和 .= 运算符来连接字符串。
练习
1. 编写一个脚本,声明一个包含你名字的 $name 变量,并将其设置为单引号字符串。然后,使用字符串连接运算符 (.) 来将 $name 的内容与字符串 ' is learning PHP' 合并,并输出结果。当你运行脚本时,输出应如下所示:
Matt is learning PHP
2. 在一个脚本中,创建一个 $fruit 变量,内容为字符串 'apple'。然后,使用双引号字符串和 print 语句输出以下信息:
apple juice is made from apples.
修改你的脚本,使得 $fruit 包含 orange,从而输出以下内容:
orange juice is made from oranges.
提示:你需要使用花括号来从 $fruit 变量创建复数形式的水果名称。
3. 编写一个脚本,声明一个 heredoc 字符串变量 $happyMessage,内容如下(包括换行):

输出 $happyMessage 变量的内容。
4. 在一个脚本中,创建一个 $appleJuice 变量,内容为字符串 'apple juice is made from apples.' 然后,使用 str_replace() 函数创建一个新的字符串变量 $grapefruitJuice,内容为字符串 'grapefruit juice is made from grapefruits.' 尝试使用其他 PHP 函数进一步转换字符串。例如,将字符串的首字母大写,使其看起来像是一个语法正确的句子。
第四章:4 条件语句

在本章中,你将学习 PHP 语言中的条件元素,包括 if...else 语句、switch 语句和 match 语句。这些结构,以及三元运算符、空合并运算符和逻辑运算符等语言特性,使得编写动态代码成为可能,代码根据一组条件来决定执行什么操作。这些条件可能依赖于特定的输入(例如来自用户或软件系统如数据库或 API 的输入),也可能依赖于其他变化的数据(例如当前日期或时间,或者文件是否存在)。
条件为真或为假
在任何决策逻辑的核心都是布尔表达式,即返回真或假的代码。最简单的布尔表达式就是 true 或 false 的字面值。然而,在几乎所有情况下,我们都会编写包含某种测试的表达式。这个测试可能会检查变量的值,或者调用一个函数并检查它返回的值。无论哪种方式,测试最终都会评估为真或假。测试的示例包括以下内容:
-
一个变量是否包含特定的值?
-
变量是否有任何值被赋值?
-
文件或文件夹是否存在?
-
一个变量的值是否大于或小于另一个值?
-
一个函数是否根据提供的参数返回真或假?
-
字符串的长度是否大于某个最小值?
-
一个变量是否包含特定数据类型的值?
-
两个表达式是否都为真,还是只有一个为真,或者都不为真?
这些测试都会评估为真或假。它们构成了你可以在代码中使用的选择语句的条件。
if 语句
可能任何编程语言中最常见的条件语句是 if 语句。它允许你仅在某个条件为真时执行语句。否则,该语句会被跳过。在 PHP 中,if 语句的写法如下:
if (condition) statementToPerform;
从 if 关键字开始,后面跟着括号中的条件,条件就是评估为真或假的布尔表达式。通常做法是在 if 关键字后、左括号前添加一个空格。接下来是当条件为真时应该执行的语句。
列表 4-1 展示了一个 if 语句的示例。如果一天的小时数小于 12(假设使用 24 小时制时钟),它会打印“早上好”。
<?php
$hourNumber = 10;
if ($hourNumber < 12) print 'Good morning';
列表 4-1:一个 if 语句示例
首先,我们将变量\(hourNumber 设置为 10。然后我们使用 if 语句来测试条件:\)hourNumber 的值是否小于 12。由于 10 小于 12,条件为真,因此条件后的语句会被执行,打印出“早上好”。
在这个例子中,我们只想在条件为真时执行一条语句。但如果我们想执行多条语句呢?我们需要一种方法将这些语句分组,以便明确它们都是 if 语句的一部分。为此,我们可以在条件之后立即将这些语句用大括号括起来。大括号划定了一个语句组,这是 PHP 的一个结构,可以包含零个、一个或多个语句,并且 PHP 会将其视为一个单一的语句。列表 4-2 展示了一个带语句组的条件语句示例。
<?php
$hourNumber = 10;
if ($hourNumber < 12) {
print 'Good';
print ' morning';
}
列表 4-2:一个重构过的 if 语句,包含语句组
这个 if 语句产生与列表 4-1 相同的结果,但我们已将其重写为包含多个打印语句,每个语句对应消息中的一个单词。这些语句被大括号括起来,以便将它们分组。通常写法是将开括号写在条件的同一行,然后在后续行中写出语句组中的每个语句,最后在另起一行写出闭括号。根据约定,语句组中的每个语句都会缩进。
注意
即使你只有一个条件语句要执行,通常也会将该语句用大括号括起来,形成语句组。这样,所有的 if 语句都会遵循相同的风格,不管涉及多少语句。
if...else 语句
许多情况下需要程序在条件为真时执行一组操作,在条件为假时执行另一组操作。对于这些情况,使用 if...else 语句。列表 4-3 展示了一个例子,其中我们选择打印“Good morning”或“Good day”。
<?php
$hourNumber = 14;
if ($hourNumber < 12) {
print 'Good morning';
} else {
print 'Good day';
}
列表 4-3:一个 if...else 语句
这段代码再次检查 $hourNumber 的值是否小于 12。如果是,条件为真,执行 if 分支的语句,打印出“Good morning”,如同之前一样。然而,如果条件为假,并且 $hourNumber 不小于 12,我们将执行 else 分支的语句,打印出“Good day”。请注意,else 关键字出现在 if 分支的语句组的右大括号之后。然后,else 分支会有一个独立的语句组,用大括号括起来。
在这种情况下,$hourNumber 是 14(下午 2 点),因此条件判断为假,执行了 else 分支的语句。
嵌套 if...else 语句
if...else 语句在两个行动之间做出选择。如果你有多个行动可以选择,你有几个选项。一个方法是将更多的 if...else 语句嵌套在原有的 else 分支中。列表 4-4 展示了一个例子。这个脚本编码了以下逻辑:如果小时数小于 12,打印“Good morning”;如果小时数在 12 到 17(下午 5 点)之间,打印“Good afternoon”;否则,打印“Good day”。
<?php
$hourNumber = 14;
❶ if ($hourNumber < 12) {
print 'Good morning';
} else {
❷ if ($hourNumber < 17) {
print 'Good afternoon';
} else {
print 'Good day';
}
}
列表 4-4:嵌套的 if...else 语句
首先,我们有一个 if 语句测试小时数是否小于 12 ❶。如果这个条件不成立,else 语句将会被执行。else 语句的语句组是一个第二个(嵌套的)if...else 语句。这个第二个 if...else 语句的条件是小时数是否小于 17 ❷。(如果我们已经执行到这个步骤,就表示小时数不小于 12,因此实际上我们是在测试小时数是否介于 12 和 17 之间。)如果这个新的测试通过,系统会打印“下午好”。否则,我们进入嵌套 if...else 语句的 else 部分,打印“日安”。尝试使用不同的$hourNumber 值来观察它如何影响脚本的输出。
if...elseif...else 语句
在编程中,选择三种或更多行为是一个非常常见的模式,因此 PHP 提供了一种更简洁的语法,避免了嵌套的需要:在 if 语句和 else 语句之间,放置一个或多个 elseif 语句。PHP 引擎会首先测试 if 语句的条件。如果该语句为假,PHP 引擎会继续测试第一个 elseif 语句的条件,然后是下一个 elseif 语句,以此类推。当 PHP 找到一个成立的条件时,会执行该分支的语句,并跳过其余的条件检查。如果没有任何 if 或 elseif 条件成立,那么最后的 else 语句(如果有)将会被执行。
清单 4-5 展示了与清单 4-4 相同的逻辑,但它是使用 if...elseif...else 重写的。
<?php
$hourNumber = 14;
if ($hourNumber < 12) {
print 'Good morning';
❶ } elseif ($hourNumber < 17) {
print 'Good afternoon';
} else {
print 'Good day';
}
清单 4-5:用 if...elseif...else 简化嵌套的 if...else 语句
我们的第二个条件现在以 elseif 语句的形式出现在 if 语句之后 ❶,而不再需要嵌套在 else 语句中。你可以在 if 和 else 之间添加任意数量的 elseif 语句。
替代语法
PHP 为 if、if...else 和 if...elseif...else 语句提供了一种替代语法,使用冒号而不是大括号来区分代码的各个部分。这种语法在清单 4-6 中得到了展示,复现了清单 4-3 中的 if...else 语句。
<?php
$hourNumber = 14;
if ($hourNumber < 12):
print 'Good morning';
else:
print 'Good day';
endif;
清单 4-6:条件语句的替代语法
在这种替代语法中,if 语句的条件后面跟一个冒号(😃。这一行就像是一个大括号的开始,因此它与 else(或 elseif)关键字之间的所有语句都被认为是条件成立时需要执行的语句组。else 关键字同样后面跟一个冒号,而不是一个大括号。else 分支的语句组以 endif 关键字结束,标志着整个 if...else 结构的结束。
这种替代语法在 Web 应用程序中特别有用,因为 HTML 模板文本可能出现在 if 语句和 else 语句之间,而使用缩进的花括号在代码中可能会让人难以跟踪。同样,endif 关键字清楚地表示整体条件语句的结束。
逻辑运算符
PHP 的 逻辑运算符 用于操作或组合布尔表达式,产生一个单一的真或假值。通过这种方式,你可以编写比单纯比较两个值更复杂的条件判断语句,就像我们迄今为止所做的那样(例如,测试两个条件是否为真)。这些逻辑运算符执行如 AND、OR 和 NOT 等操作。运算符的总结见 表 4-1。
表 4-1:PHP 逻辑运算符
| 名称 | 运算符 | 示例 | 描述 |
|---|---|---|---|
| NOT | ! | !$a | 如果 $a 为假,则为真 | |
| AND | and&& | $a and \(b\)a && $b | 如果 $a 和 $b 都为真,则为真 |
| OR | 或|| | $a 或 \(b\)a || $b | 如果 $a 或 $b 为真,或两者都为真,则为真 |
| XOR | xor | $a xor $b | 如果 $a 或 $b 为真,但不能两者都为真,则为真 |
请注意,AND 和 OR 操作可以有两种写法:使用单词(and 或 or)或使用符号(&& 或 ||)。这两种写法执行相同的功能,但符号版本在表达式求值时优先级高于单词版本。(我们在 第一章 中讨论了运算符优先级的顺序,主要是在算术运算符的上下文中。)
NOT
感叹号(!)表示“NOT”操作符。该操作符用于否定一个布尔表达式或测试该表达式是否不为真。例如,清单 4-7 使用 NOT 操作符测试驾驶员的年龄。在爱尔兰,开车必须年满 17 岁。
<?php
$age = 15;
if (!($age >= 17)) {
print 'Sorry, you are too young to drive a car in Ireland.';
}
清单 4-7:使用 NOT (!) 操作符的 if 语句
if 语句检查 $age 是否 不 为真,即是否小于 17。由于 15 小于 17,因此运行脚本时应该看到以下消息被打印出来:
Sorry, you are too young to drive a car in Ireland.
请注意,我们将 $age >= 17 放在括号中,以将其与 NOT 操作符分开。这是因为 NOT 操作符通常优先级高于 >= 操作符,但我们希望在使用 ! 否定结果之前,先检查 \(age 是否大于或等于 17。如果我们写成 if (!\)age >= 17) 而没有内括号,PHP 会首先尝试计算 !$age。NOT 操作符要求布尔操作数,因此 $age 中的值 15 会被转换为 true(任何非零值都一样)。然后,由于 !true 为 false,表达式变成 false >= 17。
接下来,PHP 会尝试评估>=比较操作,因为其中一个操作数是布尔值,它也会尝试将第二个操作数转换为布尔值。因此,整数 17 会被转换为 true(因为它是非零的),从而得到表达式 false >= true,这个结果为 false。最终,在没有额外括号的情况下,!\(age >= 17 会对任何非零整数值的\)age 评估为 false。
为了避免所有这些类型转换和因缺少括号而可能导致的错误,我通常会在引入 NOT 运算符之前,为 if 语句创建一个临时布尔变量。例如,清单 4-8 展示了清单 4-7 的另一版本,增加了一个额外的变量来避免混合整数和布尔值的任何可能性。
<?php
$age = 15;
$seventeenAndOlder = ($age >= 17);
if (!$seventeenAndOlder) {
print 'Sorry, you are too young to drive a car in Ireland.';
}
清单 4-8:清单 4-7 的简洁版本,增加了一个布尔变量
我们使用\(seventeenAndOlder 变量来存储\)age >= 17 测试的布尔值。然后,if 语句使用 NOT 运算符测试$seventeenAndOlder 是否为假。虽然与清单 4-7 相比,这增加了一行代码,但由于我们将年龄测试的布尔表达式与 if 语句的条件分开,它更容易理解。
注意
将像 $age >= 17 这样的表达式放在括号内,在将其值赋给变量时并非必要。清单 4-8 使用括号来帮助使代码更易于阅读。
和
使用 AND 运算符的表达式在两个操作数都为真时为真。你可以使用关键字 and 或双重与符号(&&)来创建 AND 操作。例如,清单 4-9 中的 if...else 语句使用 AND 运算符来判断一个驾驶员是否满足两个条件才能申请驾照考试。在爱尔兰,必须通过理论考试并持有至少六个月的学习驾驶执照,才能申请驾照考试。
<?php
$passedTheoryTest = true;
$monthsHeldLearnersLicense = 10;
$heldLearnersLicenseEnough = ($monthsHeldLearnersLicense >= 6);
if ($passedTheoryTest and $heldLearnersLicenseEnough) {
print 'You may apply for a driving test.';
} else {
print "Sorry, you don't meet all conditions to take a driver's test.";
}
清单 4-9:使用 AND 运算符的 if...else 语句
我们将\(passedTheoryTest 变量声明为 true,并将\)monthsHeldLearnersLicense 的值设置为 10。然后,我们测试\(monthsHeldLearnersLicense 是否大于或等于 6,并将结果布尔值(此例为 true)存储在\)heldLearnersLicenseEnough 变量中。接下来,我们声明一个 if...else 语句,条件为\(passedTheoryTest 和\)heldLearnersLicenseEnough。由于两个值都为真,AND 操作也为真,因此消息“你可以申请驾照考试”将被输出。
尝试将\(passedTheoryTest 更改为 false 或将\)monthsHeldLearnersLicense 设置为小于 6 的值。此时,AND 操作应评估为 false,并且语句的 else 分支中的消息应该输出。
或
OR 运算在任一操作数或两个操作数都为真时返回 true。你可以使用关键字 or 或双竖线(||)来编写 OR 运算。清单 4-10 展示了一个使用 OR 运算符的 if 语句,判断密码是否未通过基本安全规则(通过包含字符串 'password' 或长度小于六个字符)。
<?php
$password = '1234';
$passwordContainsPassword = str_contains($password, 'password');
$passwordTooShort = (strlen($password) < 6);
❶ if ($passwordContainsPassword || $passwordTooShort) {
print 'Your password does not meet minimal security requirements.';
}
清单 4-10:一个使用 OR 运算符的 if 语句
我们声明了一个 \(password 变量,存储字符串 '1234'。然后我们声明了两个布尔变量来帮助测试。首先,\)passwordContainsPassword 被赋值为将变量 $password 和字符串 'password' 传递给内置的 str_contains() 函数的结果。如果第二个字符串参数(“needle”)在第一个字符串参数(“haystack”)中找到,函数返回 true,否则返回 false。由于此例中 \(password 变量不包含字符串 'password',\)passwordContainsPassword 的值为 false。另一个布尔变量 $passwordTooShort,如果 $password 的长度小于 6,则为 true,通过内置的 strlen() 函数进行测试。由于 $password 中的字符串 '1234' 长度小于六个字符,因此该变量将被赋值为 true。
最后,我们声明一个 if 语句,使用 OR 运算符(||)根据两个布尔变量 ❶ 创建条件。由于至少有一个变量为真,if 语句条件通过,并打印出一条消息,指示密码不安全:
Your password does not meet minimal security requirements.
尝试将 $password 的值更改为六个字符或更长的字符串(不是 'password'),例如 "red\(99poppy"。此时,\)passwordContainsPassword 和 $passwordTooShort 都不会为 true,因此 if 语句中的逻辑 OR 测试将为假,且不会打印任何消息。
异或
异或运算(XOR,exclusive OR)仅在两个操作数中只有一个为真时返回 true,而两个都不为真时返回 false。我们使用关键字 xor 来创建一个 XOR 表达式。清单 4-11 展示了一个使用 XOR 运算符的 if...else 语句。该代码判断一个甜点是否奶油丰富,但不至于过于奶油。 (卡仕达 和 冰淇淋就太多了!)
<?php
$containsIceCream = true;
$containsCustard = false;
if ($containsIceCream xor $containsCustard) {
print 'a nice creamy dessert';
} else {
print 'either too creamy or not creamy enough!';
}
清单 4-11:一个带有异或运算符的 if...else 语句
我们声明了两个布尔变量 $containsIceCream 和 $containsCustard,将其中一个设置为 true,另一个设置为 false。然后我们声明了一个 if...else 语句,条件为 $containsIceCream xor $containsCustard。由于 XOR 运算符的缘故,若这两个变量中只有一个为真,条件将评估为真,并打印出一条美味的奶油甜点消息。如果两个变量都不为真,或者两个都为真,则 XOR 表达式为假,相应地会打印出“奶油过多”或“奶油不足”的消息。
在此示例中,由于只有一个变量为 true,我们应该会得到一条漂亮的奶油甜点消息。尝试更改两个布尔变量的值,看看 XOR 表达式的结果如何受到影响。
switch 语句
switch 语句是一种条件结构,用于将一个变量与多个可能的值进行比较,或称为 case。每个 case 有一个或多个语句,如果其值与变量匹配(通过类型转换,因此它执行类似 == 的相等性测试),则这些语句会被执行。你还可以提供一个默认的 case,如果没有任何值匹配。如果你需要从三个或更多可能的路径中选择,switch 语句是 if...elseif...else 语句的一个方便替代方案,只要决策依据是单一变量的值。
清单 4-12 显示了一个 switch 语句,该语句根据 $country 变量的值打印相应的货币信息。
<?php
$country = 'Ireland';
❶ switch ($country) {
❷ case 'UK':
print "The pound is the currency of $country\n";
break;
❸ case 'Ireland':
case 'France':
case 'Spain':
❹ print "The euro is the currency of $country\n";
break;
case 'USA':
print "The dollar is the currency of $country\n";
break;
❺ default:
print "(country '$country' not recognized)\n";
}
清单 4-12:使用 switch 语句根据 $country 的值打印货币
首先,我们将 \(country 赋值为 'Ireland'。然后,我们开始一个 switch 语句,使用 switch 关键字并将要测试的变量放在括号中(\)country)❶。switch 语句的其余部分被一对花括号括起来。在 switch 语句内部,我们声明需要检查的 $country 的值,每个值都在各自的缩进的 case 子句中定义。每个 case 子句使用 case 关键字定义,后跟要测试的值,再后跟一个冒号(:)。然后,如果该 case 匹配,就在新的、更深的缩进行中写出要执行的语句。例如,如果 $country 的值是 'UK' ❷,则会打印出信息:The pound is the currency of UK(英镑是英国的货币)。
如果你希望相同的操作适用于多个 case,可以将这些 case 按顺序列出,只需在最后列出一次要执行的语句。例如,爱尔兰、法国和西班牙都使用欧元,因此我们按顺序列出了这些 case ❸。这些 case 后面的 print 语句 ❹ 将适用于它们中的任何一个;你不需要为每个 case 重复该语句。
我们的脚本为当 $country 的值为 'USA' 时增加了一个额外的 case。然后,switch 语句的最后部分使用 default 关键字声明一个默认的 case,而不是使用 case ❺。如果没有任何其他 case 与正在测试的变量匹配,默认的 case 会被执行。考虑到我们将 $country 设置为 'Ireland',脚本应该输出信息:The euro is the currency of Ireland(欧元是爱尔兰的货币)。
请注意,我们在每个 case 的语句组中都包含了 break 关键字,在每个 print 语句后面。这会中断,或跳出 switch 语句,防止执行该语句中的任何后续代码。理解 break 语句的作用非常重要。一旦找到匹配的 case,switch 语句主体中的所有剩余语句都会被执行,即使是其他不匹配的 case 的语句,除非遇到 break 语句中断执行。例如,如果我们从 清单 4-12 中删除所有的 break 语句,最终输出将是:
The euro is the currency of Ireland
The dollar is the currency of Ireland
(country 'Ireland' not recognized)
$country 的值是 'Ireland',而不是 'UK',因此第一个 case 不匹配,第一个打印语句被跳过。然而,一旦我们遇到 'Ireland' 的 case,接下来的三个打印语句就会执行,因为没有 break 语句来中断 switch 语句。这通常不是你希望从 switch 语句中得到的行为,因此几乎在每种情况下,你都需要在每个 case(或一组 cases)的末尾添加 break 语句,就像我们在 示例 4-12 中所做的那样。
match 语句
match 语句根据另一个变量的值为一个变量选择一个值。你可以使用 switch 语句来完成相同的任务,但 match 语句更加简洁。此外,match 语句依赖于严格比较(相当于使用 === 测试身份),而 switch 语句在进行任何类型转换后才进行比较(相当于使用 == 测试相等)。因此,当一个变量需要与多个相同类型的值进行比较,并且根据该测试执行的操作是为变量赋值时,使用 match 语句会更合适。
示例 4-13 展示了与 示例 4-12 中的 switch 语句相同的逻辑,但采用了 match 语句来实现。
<?php
$country = 'Ireland';
❶ $currency = match ($country) {
'UK' => 'pound',
'Ireland' => 'euro',
'France' => 'euro',
'Spain' => 'euro',
'USA' => 'dollar',
❷ default => '(country not recognized)'
};
print "The currency of $country is the $currency";
示例 4-13:使用 match 语句根据 $country 的值设置 $currency
我们将 match 语句写成 $currency 变量赋值的一部分 ❶。它由 match 关键字组成,后面跟着要检查的变量(括号内),接着是由逗号分隔的、用大括号包围的 arms 序列。每个 arm 的形式是 x => y,其中 y 是当 $country 的值与 x 匹配时赋值给 $currency 的值。与 switch 语句一样,我们还提供了一个默认的 arm,以防没有值匹配 ❷。在 match 语句之后,我们打印一条消息,包含 $country 和 $currency 的值。
与 示例 4-12 中的 switch 语句相比,这个 match 语句更简洁。赋值给 $currency 后,我们只需要写一个打印语句,而不需要为 switch 语句中的每个 case 写一个单独的打印语句。我们也不再需要所有的 break 语句;使用 match 语句后,一旦找到匹配,余下的语句会被忽略。
match 语句是 PHP 语言中新出现的一种语法。许多经验丰富的程序员仍然使用 switch,而在某些情况下,match 更加高效。(我有时也会犯这个错误。)一般来说,如果你需要测试一个变量的多个值,我建议你首先尝试使用 match 语句。只有在该方案不适用时,才应切换到 switch 语句。
三元运算符
PHP 的三元运算符(或三部分运算符)根据测试条件是否为真来选择两个值之一。这个运算符由两个独立的符号组成,一个问号(?)和一个冒号(:),并且按以下形式书写:
booleanExpression ? valueIfTrue : valueIfFalse
问号左侧写一个布尔表达式,它的值为真或假(例如,比较两个值)。问号右侧写两个由冒号分隔的值。如果布尔表达式为真,则选择冒号左侧的值(valueIfTrue)。如果布尔表达式为假,则选择冒号右侧的值(valueIfFalse)。通常,结果会赋值给一个变量。
本质上,三元运算符提供了一种更简洁的方式来编写 if...else 语句,只要 if...else 语句的目的是给变量赋值(而不是执行其他操作)。为了说明,列表 4-14 展示了根据\(region 的值选择\)currency 的两种方法:首先使用 if...else 语句,然后使用三元运算符。
<?php
$region = 'Europe';
❶ if ($region == 'Europe') {
$currency = 'euro';
} else {
$currency = 'dollar';
}
print "The currency of $region is the $currency (from if...else statement)\n";
$region = 'USA';
❷ $currency = ($region == 'Europe') ? 'euro' : 'dollar';
print "The currency of $region is the $currency (from ternary operator statement)\n";
列表 4-14:比较 if...else 和三元运算符语句
我们将\(region 赋值为'欧洲'。然后声明一个 if...else 语句,如果地区是'欧洲',则将\)currency 的值设置为'euro',否则设置为'dollar' ❶。我们打印一条消息以验证结果。接下来,我们将\(region 更改为'美国',并使用三元运算符重新赋值\)currency ❷。三元运算符表达式遵循与 if...else 语句相同的逻辑:如果\(region 等于'欧洲',代码将\)currency 设置为'euro',否则将$currency 设置为'dollar'。再次,我们打印消息以检查结果。以下是运行脚本后的输出:
The currency of Europe is the euro (from if...else statement)
The currency of USA is the dollar (from ternary operator statement)
第二行显示三元运算符已按预期工作,因为\(region 的值不是'欧洲',因此\)currency 的值被赋为'dollar'。正如你所见,在这种需要在两个可能的值之间做出简单选择的情况下,三元运算符非常简洁,只有一行代码,而 if...else 语句则有四行代码。
空合并运算符
另一种在两个值之间进行选择的运算符是空合并运算符,使用两个问号(??)表示。这个运算符根据变量是否为 NULL 来做出选择。使用空合并运算符的表达式的一般形式如下:
$variable = value ?? valueIfNull
首先,空合并运算符检查左侧的值,即 ?? 运算符左侧的表达式。这可以是一个变量,或者是一个返回值的函数。如果该表达式不是 NULL,则将值赋给 $variable。否则,空合并运算符右侧的值(valueIfNull)将被赋给该变量。这提供了一种后备机制,防止在变量未定义(或为 NULL)时抛出警告或错误。当你期望从用户那里获得一个值但没有提供,或者当你在数据库中查找记录而该记录不存在时,这种机制特别有用。
列表 4-15 显示了空合并运算符的使用示例。我们使用它来测试 $lastname_from_user 变量两次,第一次是在它还未被赋值时(因此是 NULL),第二次是在它被赋值之后。
<?php
❶ $lastname = $lastname_from_user ?? 'Anonymous';
print "Hello Mr. $lastname\n";
$lastname_from_user = 'Smith';
❷ $lastname = $lastname_from_user ?? 'Anonymous';
print "Hello Mr. $lastname\n";
列表 4-15:使用空合并运算符测试 NULL
首先,我们使用空合并运算符来设置 $lastname ❶ 的值。该运算符测试 \(lastname_from_user 变量,由于该变量尚未被赋值,因此为 NULL。因此,\)lastname 应该被赋予 ?? 运算符右侧的值(字符串 'Anonymous')。我们打印出一条消息以检查结果。接着,在给 $lastname_from_user 赋值后,我们使用相同的空合并运算符表达式再次设置 $lastname 的值 ❷。这次,由于 $lastname_from_user 包含非 NULL 值,该值应该被传递给 $lastname。以下是结果:
Hello Mr. Anonymous
Hello Mr. Smith
第一行显示,由于变量 \(lastname_from_user 为 NULL,\)lastname 被赋值为字符串 'Anonymous'。然而,第二次,$lastname_from_user 中的字符串 'Smith' 成功地存储在 $lastname 变量中并被打印出来。
小结
在本章中,你学习了用于编写做出决策的代码的关键字和运算符。计算机和编程语言的许多强大功能都建立在我们讨论的各种运算符和选择语句上。你看到 if 和 if...else 语句如何基于单一测试做出选择,尽管该测试本身可能会结合布尔表达式与逻辑运算符,如 AND 或 OR。你还看到了如何通过在 if 和 else 之间添加 elseif 分支来结合多个测试。然后你学习了其他条件结构,包括 switch 和 match 语句,它们会测试变量是否具有不同的可能值。这些结构允许你定义一个或多个在找到特定值时执行的语句。与这些结构紧密相关的是三元运算符和空合并运算符,它们都在两个可能的值之间做选择。
练习
1. 编写一个脚本,将一个名字赋值给 $name 变量,然后如果字符串的长度小于四个字符,则打印消息 "That is a short name"。
2. 编写一个脚本,用于确定洗衣机的大小。该脚本应检查变量$laundryWeightKg 的值,如果值小于 9,则打印“适合标准洗衣机”,否则打印“需要中型到大型洗衣机”。
3. 使用 switch 语句或 match 语句测试$vehicle 变量的值,并根据该值打印相应的消息。使用以下值/消息组合:
bus "滴滴声"
train "在轨道上行驶"
car "至少有三个轮子"
helicopter "可以飞行"
bicycle "学会了就永远不会忘记"
(上述内容无) "你选择了人迹罕至的道路"
4. 编写一个脚本,如果\(用户名字正确且\)密码正确,打印消息“您现在已登录”。否则,打印“凭证无效,请重试”。
第五章:5 自定义函数

在本章中,你将学习如何声明和使用你自己的函数,这些函数是命名的、独立的代码序列,旨在完成特定任务。你将看到函数如何促进代码的重用,因为将代码放入函数中要比每次执行该任务时都重新编写相同的代码序列更高效。函数还可以让你编写通过少量语句就能完成大量工作的程序,因为每个语句都可以调用隐藏在你函数中的复杂逻辑。
自定义函数通常会在与应用程序主程序语句不同的文件中声明。这源于PHP 标准推荐(PSRs),它是一系列关于 PHP 编程的指南和最佳实践。根据 PSR-1,文件应该要么声明符号(例如函数),要么产生副作用,但不能两者兼具。副作用是执行一段代码的具体结果,例如输出文本、更新全局变量、修改文件内容等等。
尽管函数本身可以产生这样的副作用,但声明一个函数(定义函数将做什么)与调用一个函数(让函数实际执行那个任务)是不同的。因此,函数应该在一个文件中声明,并在另一个文件中调用。为了遵循这一准则,本章首先介绍如何处理跨多个文件的代码基础知识,然后再转到函数部分。在第九章中,我们将更详细地讨论如何处理文件。
将代码分成多个文件
即使我们不考虑将函数声明放在单独文件中的最佳实践,仍然有必要将应用程序的代码拆分到多个文件中。考虑到一个复杂的应用程序可能包含成千上万行代码,如果所有代码都在一个庞大的文本文件中,那么浏览该文件并找到需要处理的特定代码段将非常困难。将代码组织成不同的文件使得项目更易于管理。
使用多个文件还促进了代码的重用性。一旦你开始编写自己的函数,你会发现将这些函数声明在不同的文件中,可以轻松地在项目的不同部分或完全不同的项目中重用这些函数。再举个例子,多个页面的网页应用程序通常在许多页面中包含相同的元素,例如 HTML 头部、底部和导航列表。与其为每个需要这些元素的页面重复写这些代码,不如将通用代码写在一个文件中。这样,如果需要对其进行更改(例如,更新网页徽标的图片引用),你只需在一个地方进行更改,而不是追踪并更新每个重复代码的实例。软件工程师称这一做法为不要重复自己(DRY)原则。
一旦你开始将应用程序的代码分布到多个文件中,就需要一种方法从一个文件中访问另一个文件的代码。在本节中,我们将探讨一些使这成为可能的 PHP 语言特性。
读取并执行另一个脚本
PHP 的 require_once 命令可以读取另一个文件的代码并执行它。为了了解这个命令是如何工作的,我们将创建两个脚本。其中一个是主脚本,它将使用 require_once 来访问另一个脚本中的代码。首先,创建一个名为main.php的文件,包含 Listing 5-1 中的代码。
<?php
print "I'm in main.php\n";
require_once 'file2.php';
print "I'm back in main.php\n";
Listing 5-1: 读取并执行来自不同脚本的代码的主脚本
在这个脚本中,我们打印出两条消息,指示我们正在主应用程序文件中。在两条消息之间,我们使用 require_once 命令来读取并执行 file2.php 脚本的内容。文件名作为字符串立即出现在命令后面。由于我们没有指定与文件名一起的目录路径(例如,Users/matt/file2.php),因此默认理解该文件与当前脚本位于同一文件夹中。这被称为相对路径:文件的位置是相对于当前脚本的位置确定的。
现在创建一个名为file2.php的文件,包含 Listing 5-2 中的代码。确保将此文件保存在与main.php相同的位置。
<?php
print "\t I'm printing from file2.php\n";
print "\t I'm also printing from file2.php\n";
Listing 5-2: 从另一个脚本读取并执行的 file2.php 内容
这个脚本包含两个打印语句,打印出消息,表示它们来自file2.php。请注意,每条消息的开头都有一个制表符转义字符(\t)。这样,这些消息就会缩进,而我们主脚本中打印的消息则不会,这是一个视觉提示,表明这些消息来自不同的脚本。
现在在命令行中输入 php main.php 来运行主脚本。以下是输出结果:
I'm in main.php
I'm printing from file2.php
I'm also printing from file2.php
I'm back in main.php
我们看到主脚本的第一条消息,接着是来自 file2.php 的两条缩进消息。这确认了由于主脚本中的 require_once 语句,file2.php 的内容已被读取并执行。最后,程序控制流返回主脚本,经过 require_once 语句后,我们看到了主脚本打印的最终消息。
注意
除了 require_once,PHP 还提供了另外三个命令,用于读取和执行声明在独立文件中的代码: require, include* 和 include_once。它们的工作方式类似;你可以在 PHP 文档中阅读它们的区别。在我编写的 99.99% 的 Web 应用程序中,我使用 require_once。*
创建绝对文件路径
常量 DIR 始终指向当前执行脚本的 绝对文件路径,即从根目录开始的完整文件路径。这是 PHP 的 魔术常量 之一,内置常量,其值根据上下文而变化。对于 DIR 来说,值取决于 DIR 被评估时所在文件的位置。
编写 require_once 语句时,最好尽可能使用 DIR:只需将 DIR 的值与任何剩余的相对路径信息连接起来,即可访问你试图读取和执行的文件。这可以避免混淆路径是与当前脚本(调用 require_once 命令的脚本)相关,还是与可能已经要求当前脚本的脚本相关。假设你有一系列的脚本,其中一个脚本要求另一个脚本,而那个脚本也要求另一个脚本。如果这些脚本位于不同的目录中,使用 DIR 魔术常量可以确保无论你在何处写 require_once 语句,你都能知道路径正确指向你希望读取和执行的文件。
要尝试使用 DIR,请按 列表 5-3 所示更新你的 main.php 文件。更改部分以黑色文字显示。
<?php
print "I'm in main.php\n";
$callingScriptPath = __DIR__;
print "callingScriptPath = $callingScriptPath\n";
❶ require_once __DIR__ . '/file2.php';
print "I'm back in main.php\n";
列表 5-3:使用 DIR 读取和执行不同脚本中的代码的主脚本
我们将 $callingScriptPath 变量赋值为 DIR 魔术常量的值,并打印包含该变量的消息。然后在 require_once 命令之后使用 DIR,明确表示 file2.php 脚本与此主脚本位于同一目录❶。请注意,我们使用字符串连接运算符(.)将 DIR 的值与字符串 '/file2.php' 结合,构建到另一个文件的绝对路径。以下是运行主脚本后的输出:
I'm in main.php
❶ callingScriptPath = /Users/matt/magic
I'm printing from file2.php
I'm also printing from file2.php
I'm back in main.php
如前所述,首先打印出来自 main.php 的消息。然后我们看到打印出的主脚本路径(DIR 的值)❶。对我来说,这是 /Users/matt/magic,即我电脑上该示例项目所在目录的路径。其余输出与之前相同,包含来自 file2.php 的消息,最后是主脚本打印的最终消息。
声明和调用一个函数
现在,让我们关注如何声明和使用我们的第一个自定义函数。这个函数将确定两个数字中哪个较小。按照最佳实践,我们将函数声明在一个文件中,my_functions.php,然后从另一个文件中调用它,main.php。开始一个新项目并创建包含列表 5-4 代码的 my_functions.php。
<?php
function which_is_smaller(int $n1, int $n2): int
{
if ($n1 < $n2) {
return $n1;
} else {
return $n2;
}
}
列表 5-4:在 my_functions.php 中声明函数
这里我们声明了一个名为 which_is_smaller() 的函数。我们从关键字 function 开始,后跟函数名称。按照约定,函数名使用蛇形命名法,全部小写字母并用下划线连接多个单词。这使得你可以编写有意义、易读的函数名(尽管遗憾的是,由于语言早期设计时的选择,并不是所有 PHP 内置函数都遵循这种命名约定)。
函数名后面跟着一对括号,括号内是函数的参数,它们是函数完成工作的输入。在这个例子中,我们有两个参数,$n1 和 $n2,代表我们希望函数比较的两个数字。每个参数名前面都有数据类型,以确保正确的数据类型传递给函数。例如,int $n1 表示参数 $n1 应该是一个整数。
注意
如果一个函数不需要任何参数,你仍然需要在函数名称后面包括一对空括号。
括号后面跟着一个冒号(:),然后是函数的返回类型。大多数函数会执行一些操作并产生一个结果值,然后该函数将这个值返回给调用它的脚本。返回类型指定了该值的数据类型。在这个例子中,函数将返回整数 $n1 或 $n2 中较小的一个,因此我们将返回类型设置为 int。
到目前为止我们写的代码已经定义了函数的签名,它是函数的名称、参数(及其类型)和返回类型的组合。PHP 引擎使用函数的签名来唯一标识该函数,识别我们何时调用它,验证传递给函数参数的数据是否合适,并确保函数返回一个合适的值。
接下来是函数的主体,它是一个被大括号括起来的语句组,包含每次调用函数时会执行的代码。我们 which_is_smaller()函数的主体由一个 if...else 语句组成,测试整数\(n1 是否小于整数\)n2。如果$n1 较小,则执行 return \(n1;语句。否则(如果\)n2 较小或等于$n1),则执行 return \(n2;语句。在这两种情况下,我们使用 return 关键字将值(\)n1 或$n2)传递给调用它的脚本。一旦函数到达 return 语句,函数会停止执行并将控制权交还给调用脚本。即使函数体内在 return 语句之后有其他语句,它们也不会在函数返回值后执行。
现在我们已经声明了一个函数,接下来让我们使用它。创建与my_functions.php位于同一位置的main.php文件,并输入清单 5-5 中显示的代码。
<?php
require_once __DIR__ . '/my_functions.php';
$result1 = which_is_smaller(5, 2);
print "the smaller of 5 and 2 = $result1\n";
$result2 = which_is_smaller(5, 22);
print "the smaller of 5 and 22 = $result2\n";
清单 5-5:从 main.php 调用 which_is_smaller()函数
我们使用 require_once 从my_functions.php文件中读取函数声明。这并不会调用函数,它只是使函数在main.php脚本中可用。接下来,我们通过编写函数名并在括号中跟上我们希望函数比较的值(5 和 2)来调用函数。这些值被称为参数;它们填充函数的参数值。注意,我们将函数调用作为\(results1 变量赋值语句的一部分进行调用。这样,函数的返回值将存储在\)results1 中,以供以后使用(在这种情况下,是在下一行代码中,那里会将其打印出来)。当一个函数有返回值时,通常会遵循这种调用函数并将结果赋值给变量的模式。
我们通过再次调用该函数来结束脚本,这次使用 5 和 22 作为参数。这就是函数的魅力:你可以根据需要调用它们多次,每次使用不同的输入值。我们将第二次函数调用的返回值存储在$result2 变量中,并再次打印出显示结果的消息。下面是运行main.php脚本的输出:
the smaller of 5 and 2 = 2
the smaller of 5 and 22 = 5
我们可以看到我们的函数工作正常。它返回 5 和 2 中的较小值 2,以及 5 和 22 中的较小值 5。
参数与实参
参数和实参这两个术语密切相关,经常互相混淆。当你声明一个函数时,参数是代表函数将要处理的输入的变量。如你在清单 5-4 中所见,参数会列在函数名称后的括号内。在我们的 which_is_smaller() 函数中,参数是 $n1 和 $n2。每个参数都是一个临时变量,仅在函数内部可见,在函数被调用时会被赋予一个值。这些变量只在函数执行期间存在。一旦函数执行完毕,局部参数变量会从计算机的内存中被丢弃。
变量在软件系统中“存在”的时间长度被称为作用域。在函数中声明的任何变量,包括参数,其作用域是局部的,仅限于函数本身。因此,你不能期望从函数声明外的任何代码访问函数的变量。在我们的例子中,我们不能在 main.php 中使用变量 $n1 和 $n2。相反,获取函数返回值的方法是使用返回语句。
当我们调用一个函数时,实参是我们在函数名称后的括号中传递给函数的具体值。这些实参为函数的参数提供值。例如,当我们调用 which_is_smaller(5, 22) 时,实参 5 被赋值给参数 $n1,实参 22 被赋值给参数 $n2。实参的顺序与参数的顺序相匹配。在这个例子中,实参是字面量,但实参也可以是变量,如下所示:
which_is_smaller($applesCount, $orangesCount)
就是这么简单。实参是在执行函数时传递的值,参数是在函数执行时创建的局部变量,由接收到的实参填充。因此,在函数执行期间,每个传递给函数的实参都会有一个对应的局部(临时)参数变量。(有一个例外是通过引用传递参数的特殊情况,我们将在本章后面讨论。)
错误来源于不正确的函数调用
在两种常见情况下,调用函数时会发生错误:如果你没有传递正确数量的实参,或者传递了错误数据类型的实参。(有关错误及其他类型警告的信息,请参见第 88 页的“错误、警告和通知”。)考虑调用我们自定义的 which_is_smaller() 函数:
$result = which_is_smaller(3);
该函数需要两个整数类型的实参,但我们只提供了一个。如果你尝试执行这个表达式,应用程序将停止运行,你会看到类似以下的致命错误:
PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function
which_is_smaller(), 1 passed in /Users/matt/main.php on line 9 and exactly 2
expected in /Users/matt/my_functions.php:2
如果你传递了错误数据类型的实参(即不能转换为函数声明中指定的参数数据类型的值),你也会遇到致命错误。考虑下面这个表达式,我们将非数字的字符串传递给 which_is_smaller() 函数:
$result = which_is_smaller('mouse', 'lion');
尝试执行该语句会产生类似于以下的错误消息:
PHP Fatal error: Uncaught TypeError: which_is_smaller(): Argument #1 ($n1)
must be of type int, string given, called in /Users/matt/main.php on line 10
and defined in /Users/matt/my_functions.php:2
发生了一个致命的类型错误(TypeError),因为我们的函数需要两个整数参数,但我们提供了字符串类型的参数。
类型转换
为了避免我们刚刚看到的那种类型错误(TypeError),当传入错误类型的参数时,PHP 引擎会尝试将这些参数转换成期望的数据类型。(有关类型转换的回顾,请参阅第二章。)清单 5-6 展示了在我们向which_is_smaller()函数传入非整数类型参数时的一些示例。更新你的main.php文件,使其与清单一致。
<?php
require_once __DIR__ . '/my_functions.php';
$result1 = which_is_smaller(3.5, 2);
print "the smaller of 3.5 and 2 = $result1\n";
$result2 = which_is_smaller(3, '55');
print "the smaller of 3 and '55' = $result2\n";
$result3 = which_is_smaller(false, -8);
print "the smaller of false and -8 = $result3\n";
清单 5-6:更新 main.php 脚本以演示类型转换
我们调用which_is_smaller()函数三次并打印结果。由于所有参数都可以转换为整数,因此这些函数调用都不会触发错误。首先,我们使用浮动类型 3.5 和整数 2 调用函数。浮动类型将转换为整数 3。接下来,我们使用整数 3 和字符串'55'作为参数。这一次,字符串将转换为整数 55。最后,我们传递布尔值 false 和整数-8 作为参数。false 将转换为整数 0。以下是运行脚本后的输出结果:
PHP Deprecated: Implicit conversion from float 3.5 to int loses precision in
/Users/matt/my_functions.php on line 2
the smaller of 3.5 and 2 = 2
the smaller of 3 and '55' = 3
the smaller of false and -8 = -8
当你运行脚本时,首先打印出来的应该是一个废弃警告消息,告知你当浮动类型 3.5 转换为整数 3 时,精度会丢失。此消息表示,在未来的某个时刻(可能是 PHP 9),PHP 将停止自动将带有小数部分的浮动类型转换为整数,因此代码有朝一日将停止工作并触发错误。在该消息之后,你应该能看到三次打印语句的结果,表明由于 PHP 的自动类型转换,这三次函数调用都正常执行了。
注意
当你遇到废弃警告消息时,阅读有关即将更改的讨论可能会很有帮助。例如,解释清单 5-6 中废弃消息输出的请求评论(RFC)文档可以在线查看,链接为 wiki.php.net/rfc/implicit-float-int-deprecate。
尽管这些函数调用在参数类型不正确的情况下仍然成功,但编写良好的程序应尽量避免依赖类型转换。注意类似我们刚刚遇到的废弃警告,并寻找方法修改代码,以便在没有警告或错误的情况下处理不同类型的值。在这种情况下,我们可以重构该函数,使用联合类型(在第 98 页的《联合类型》中讨论),这样就可以同时接受整数和浮动类型作为参数。
没有显式返回值的函数
不是每个函数都必须显式返回一个值。例如,你可以编写一个函数,仅仅打印一条消息而不返回任何内容给调用脚本。当一个函数没有显式的返回值时,应将其返回类型声明为 void。
为了演示,我们将声明一个函数,该函数打印出给定数量的星号,并在两侧用另一个填充字符进行填充,以实现固定的行长度。我们可以使用该函数创建 ASCII 艺术图像,即通过排列字符文本来形成的图像。启动一个新项目,并创建包含 列表 5-7 中代码的 my_functions.php 文件。
<?php
function print_stars(int $numStars, string $spacer): void
{
$lineLength = 20;
$starsString = str_repeat('*', $numStars);
$centeredStars = str_pad($starsString, $lineLength, $spacer, STR_PAD_BOTH);
print $centeredStars . "\n";
}
列表 5-7:在 my_functions.php 中声明 print_stars() 函数
在这里,我们声明了一个名为 print_stars() 的函数。该函数需要两个参数:$numStars 和 $spacer。整数 $numStars 是要打印的星号(*字符)数量。字符串 $spacer 是在星号两边作为填充的字符。在括号后面,我们使用 : void 来指示该函数不会显式返回任何值。
在函数体内,我们将要打印的行长度设置为 20 个字符。(由于这个值是硬编码在函数中的,所以每次调用该函数时,它的值都将相同;一个更灵活的替代方法是将 $lineLength 设置为一个参数。)然后,我们生成一个包含由 \(numStars 参数指定数量的星号的字符串(\)starsString)。接着,我们使用内置的 str_pad() 函数(在第三章中讨论)来创建一个 20 个字符长的字符串,$starsString 在其中居中,并且两侧对称地用 $spacer 参数中的字符进行填充。例如,如果 \(numStars 为 10,\)spacer 为 '.',则会生成字符串 '.....**********.....',即 10 个星号,两侧各有 5 个句点,总长度为 20。最后,我们打印出结果,并输出一个换行符。
请注意,我们没有在函数体内包含 return 语句。因为没有必要,函数的作用仅仅是构造并打印一个字符串。如果我们尝试从该函数返回一个值,会触发一个致命错误,因为我们将该函数声明为 void。
现在让我们使用我们的函数生成一个树的 ASCII 艺术图像。创建 main.php 文件,包含 列表 5-8 中的代码。
<?php
require_once __DIR__ . '/my_functions.php';
❶ $spacer = '/';
print_stars(1, $spacer);
print_stars(5, $spacer);
print_stars(9, $spacer);
print_stars(13, $spacer);
print_stars(1, $spacer);
print_stars(1, $spacer);
列表 5-8:在 main.php 中使用 print_stars() 函数生成树形图案的脚本
在通过 require_once 引入函数声明后,我们将填充字符设置为正斜杠 (/) ❶。然后我们调用 print_stars() 函数六次,打印出由 1、5、9 和 13 个星号组成的树形图案,并且再加上两行只有 1 个星号的树干。以下是在终端运行 main.php 脚本的输出:
/////////*//////////
///////*****////////
/////*********//////
///*************////
/////////*//////////
/////////*//////////
我们在暴风雨中创建了一棵树!
返回 NULL
即使函数声明为 void,它在技术上仍然有一个返回值:NULL。如果一个函数执行完毕没有返回值,函数会默认返回 NULL。为了证明这一点,我们可以再次调用 print_stars() 函数,并像处理有返回值的函数一样,将结果赋给一个变量。更新你的 main.php 文件以匹配 Listing 5-9。更改部分用黑色文本显示。
<?php
require_once __DIR__ . '/my_functions.php';
$spacer = '/';
print_stars(1, $spacer);
print_stars(5, $spacer);
print_stars(9, $spacer);
print_stars(13, $spacer);
print_stars(1, $spacer);
$result = print_stars(1, $spacer);
var_dump($result);
Listing 5-9: 更新 main.php 来存储并打印 print_tree() 函数的 NULL 返回值
我们像之前一样调用 print_stars() 函数,但这次我们将最后一次函数调用的返回值存储在 $result 变量中。然后使用 var_dump() 查看 $result 的内容。由于 print_stars() 没有显式的返回值,因此 $result 应该包含 NULL。以下是运行 main.php 脚本的输出:
/////////*//////////
///////*****////////
/////*********//////
///*************////
/////////*//////////
/////////*//////////
NULL
我们可以再次看到 ASCII 树,随后是从调用 var_dump() 得到的 NULL。这证明了尽管函数声明为 void,它仍然默认返回 NULL。
提前退出函数
声明为 void 的函数仍然可以使用 return 语句,只要该语句不包含值。如前所述,函数在遇到 return 语句时会立即停止执行,因此不带值的 return 提供了一种提前退出函数的机制。这在例如函数的某个参数出现问题时非常有用。你可以在函数开始时添加验证逻辑来检查参数,并使用 return 来提前停止函数的执行,如果一个或多个参数值不符合预期,则会恢复主调用脚本的执行。
我们一直在使用的 str_pad() 函数,如果填充字符串为空,会触发致命错误。为了避免程序崩溃,我们将更新 print_stars() 函数,首先检查 $spacer 字符串参数是否为空。如果为空,我们将使用 return 提前退出函数。修改 my_functions.php 以匹配 Listing 5-10。
<?php
function print_stars(int $numStars, string $spacer): void
{
if (empty($spacer)) {
return;
}
$lineLength = 20;
$starsString = str_repeat('*', $numStars);
$centeredStars = str_pad($starsString, $lineLength, $spacer, STR_PAD_BOTH);
print $centeredStars . "\n";
}
Listing 5-10: 向 print_stars() 函数中添加 return 语句以提前退出
我们在函数体开始时添加了一个 if 语句,使用内建的 empty() 函数来检查 $spacer 是否为空字符串。如果为空,我们使用不带值的 return 来提前结束函数执行,并将程序控制返回给调用脚本。如果函数执行通过了这个 if 语句,则表示 $spacer 不为空,这样我们的 str_pad() 调用应该能正常工作。
为了查看 return 语句是否有效,更新 main.php 脚本,如 Listing 5-11 所示。
<?php
require_once __DIR__ . '/my_functions.php';
$spacer = '';
print_stars(1, $spacer);
print_stars(5, $spacer);
print_stars(9, $spacer);
print_stars(13, $spacer);
print_stars(1, $spacer);
$result = print_stars(1, $spacer);
var_dump($result);
Listing 5-11: 更新 main.php 来调用 print_tree() 并传入一个空的填充字符串
我们将\(spacer 设置为空字符串,而不是斜杠,之后再调用 print_stars()。运行主脚本的输出现在应该只是 NULL。每次调用 print_stars()函数时,它都会提前返回,因为\)spacer 是空字符串,因此我们不再看到 ASCII 树形图。另一方面,我们也没有看到致命错误,因为返回语句阻止我们使用无效的参数调用 str_pad()。我们依然在输出中看到 NULL,这是 var_dump()调用的结果。这表明,当函数遇到没有返回值的返回语句时,它会返回 NULL,就像没有返回语句一样。
在函数内部调用函数
在一个函数的主体内调用另一个函数是完全合理的。事实上,我们已经多次这样做了,在 print_stars()函数内部调用了内置的 PHP 函数,如 str_repeat()和 str_pad()。同样,在其他自定义函数中调用你自己的自定义函数也是可能的,实际上这是非常常见的做法。
编程的强大之处在于将问题分解为更小的任务。你编写基本的函数来处理这些小任务,然后再编写更高层次的函数,将这些任务组合在一起解决更大的问题。最终,你的主应用脚本看起来非常简单:你只需要调用一到两个函数。诀窍在于,这些函数本身会调用多个其他函数,以此类推。
我们调用 print_stars()函数六次来生成一个 ASCII 树。让我们将这六次调用移到另一个函数 print_tree()中。这样,每次我们想打印一棵树时,主脚本中只需要一次函数调用。将新的 print_tree()函数添加到my_functions.php中,如 Listing 5-12 所示。
<?php
function print_stars(int $numStars, string $spacer): void
{
--snip--
}
function print_tree(string $spacer): void
{
print_stars(1, $spacer);
print_stars(5, $spacer);
print_stars(9, $spacer);
print_stars(13, $spacer);
print_stars(1, $spacer);
print_stars(1, $spacer);
}
Listing 5-12:将 print_tree()函数添加到 my_functions.php
我们在之前声明的 print_stars()函数后声明 print_tree()函数。它需要一个名为\(spacer 的字符串参数。在函数体内,我们编写了六个原始的 print_stars()调用。请注意,print_tree()函数的参数\)spacer 在调用 print_stars()时也充当了一个参数。这样,我们只需在调用 print_tree()时传入不同的字符串,就可以轻松地打印带有不同填充字符的星号树形图。
有了这个新函数,我们现在可以大大简化我们的主脚本。按照 Listing 5-13 所示更新main.php。
<?php
require_once __DIR__ . '/my_functions.php';
print_tree('/');
print_tree(' ');
Listing 5-13:通过 print_tree()函数简化 main.php 脚本
在读取函数声明文件后,我们调用 print_tree()两次生成两棵树。第一次我们像之前一样使用正斜杠作为间隔符,第二次我们使用空格字符。以下是结果:
/////////*//////////
///////*****////////
/////*********//////
///*************////
/////////*//////////
/////////*//////////
*
*****
*********
*************
*
*
我们的主脚本通过调用两次print_tree(),完成了原本需要调用 12 次print_stars()的任务。当然,那些print_stars()的调用仍然存在,但我们将它们隐藏在print_tree()的定义中,使我们的主脚本变得更加简洁。你可以开始看到函数在组织代码和促进可重用性方面的强大作用。
返回多个值和参数类型的函数
对于简单的情况,你通常可以编写一个执行某项任务并返回单一类型值或不返回任何值的函数。然而,其他时候,你可能希望通过允许函数根据情况返回不同数据类型的值来提高其可重用性。同样,你可能希望函数的参数能够接受不同数据类型的值,以确保代码能够应对输入验证问题。可空类型和联合类型提供了优雅的方法来允许多种类型,既适用于函数的返回值,也适用于函数的参数。
可空类型
编写通常返回一种类型值(如字符串或数字),但有时返回NULL的函数是非常常见的。例如,一个通常执行计算的函数,如果接收到无效输入,可能会返回NULL;或者一个从数据库中检索信息的函数,如果无法建立数据库连接,也可能返回NULL(我们将在第六部分讨论数据库时看到这一点)。为了实现这一点,可以通过在返回类型前立即添加问号(?)来声明函数的返回类型为可空。例如,在函数声明的第一行末尾添加: ?int意味着该函数将返回NULL或整数。
让我们通过一个尝试返回拼写出来的数字的整数值的函数来看看它是如何工作的(例如返回 1 而不是'one')。如果函数无法识别输入的字符串,它将返回NULL。开始一个新项目,创建包含列表 5-14 内容的my_functions.php文件。
<?php
function string_to_int(string $numberString): ❶ ?int
{
return match ($numberString) {
'one' => 1,
'two' => 2,
'three' => 3,
'four' => 4,
'five' => 5,
❷ default => NULL
};
}
列表 5-14:一个返回整数或NULL的函数
我们声明了string_to_int()函数,使用可空类型?int来表示该函数将返回NULL或整数 ❶。该函数接收字符串参数$numberString。它的主体是一个单一的返回语句,通过使用match表达式选择要返回的值。这是可能的,因为match表达式的值总是一个单一的结果。该表达式有五个子句,将字符串'one'到'five'分别匹配到相应的整数。第六个子句设置了默认情况 ❷,如果提供任何其他字符串,则返回NULL。通过这种方式,match表达式返回一个整数或NULL,就如同函数的可空返回类型所指示的那样。
现在我们将编写一个 main.php 文件,其中包含一个调用我们函数的脚本。当你调用具有可空返回类型的函数时,测试返回值是否为 NULL 是很重要的。清单 5-15 显示了如何操作。
<?php
require_once __DIR__ . '/my_functions.php';
❶ $text1 = 'three';
$number1 = string_to_int($text1);
❷ if (is_null($number1)) {
print "sorry, could not convert '$text1' to an integer\n";
} else {
print "'$text1' as an integer = $number1\n";
}
$text2 = 'onee';
$number2 = string_to_int($text2);
if (is_null($number2)) {
print "sorry, could not convert '$text2' to an integer\n";
} else {
print "'$text2' as an integer = $number2\n";
}
清单 5-15:一个调用可空类型 string_to_int() 函数的 main.php 脚本
我们将字符串 'three' 赋值给变量 $text1,然后将该变量传递给 string_to_int() 函数,将返回值存储在 $number1 ❶ 中。接下来,我们使用 if...else 语句测试 $number1 中的值是否为空(NULL) ❷。如果是,我们打印一条消息,说明该字符串无法转换为整数。否则,我们打印一条显示字符串及其对应整数的消息。然后,我们重复这个过程,使用字符串 'onee'。以下是输出:
'three' as an integer = 3
sorry, could not convert 'onee' to an integer
我们可以看到,当参数是字符串 'three' 时,函数返回整数 3,但当参数是拼写错误的字符串 'onee' 时,它返回 NULL。通过将 string_to_int() 函数声明为可空返回类型,我们可以灵活地以有意义的方式应对这种问题输入。
就像函数可以有可空返回类型一样,你也可以使用相同的问号语法来声明函数参数为可空类型,这意味着参数可以是 NULL 或其他类型。例如,参数列表 (?string $name) 意味着该函数接受一个 $name 参数,该参数可以是 NULL 或字符串。
我们不需要像在 清单 5-15 中那样,每次调用 string_to_int() 函数时都重复编写 if...else 语句。我们可以将函数的 NULL 或整数返回值作为参数传递给另一个函数,以生成适当的消息。因此,该函数需要能够接受一个可能为 NULL 或整数的参数。清单 5-16 显示了这样一个名为 int_to_message() 的函数。将该函数添加到你的 my_functions.php 文件的末尾。
function int_to_message(?int $number): string
{
if (is_null($number)) {
return "sorry, could not convert string to an integer\n";
} else {
return "an integer = $number\n";
}
}
清单 5-16:一个具有可空类型的 $number 参数的函数
该函数的签名包含一个名为 $number 的单一参数,其类型为可空的 ?int。这意味着传递给该函数的参数可以是 NULL 或整数。函数体使用了我们在 main.php 脚本中写的 if...else 语句,根据传递的数据类型返回相应的消息。
现在,通过移除重复的 if...else 语句并改为调用我们的新函数,我们可以大大简化主脚本。清单 5-17 显示了更新后的脚本。
<?php
require_once __DIR__ . '/my_functions.php';
❶ $text1 = 'three';
❷ $number1 = string_to_int($text1);
❸ print int_to_message($number1);
$text2 = 'onee';
$number2 = string_to_int($text2);
print int_to_message($number2);
❹ print int_to_message(string_to_int('four'));
清单 5-17:使用 int_to_message() 函数简化 main.php
请注意,由于生成消息的逻辑已经移到函数中,我们的主脚本现在变得更简洁了。对于每个输入,我们遵循三个基本语句的模式:声明一个字符串❶,存储调用string_to_int()函数时返回的整数(或 NULL)❷,并打印通过将该整数或 NULL 值传递给int_to_message()函数而返回的字符串❸。
如果我们真的想让代码更加简洁,可以将这三条语句合并成一行❹,在调用int_to_message()函数时,将string_to_int()函数放在括号内。这样,前者的返回值就会直接作为参数传递给后者,而无需使用中介变量。这种做法属于编程风格的选择。就我个人而言,我更倾向于使用中介变量,以防止一行代码过于复杂。
联合类型
如果你希望一个函数能够返回多种数据类型,可以使用联合类型来声明其返回值。这是一个值的可能数据类型的列表,类型之间用竖线分隔。例如,int|float表示一个值可以是整数或浮动值。联合类型既可以应用于函数参数,也可以应用于返回值。
可空类型本质上是联合类型的一种特殊类别,其问号语法提供了当某种可能的数据类型为 NULL 时的简便写法。例如,联合类型string|NULL与更简洁的可空类型?string相同。联合类型在代码中有多个非 NULL 类型时特别有用,比如int|float,或者有多个非 NULL 类型加上 NULL 时,比如string|int|NULL,表示数据类型可能是字符串、整数或 NULL。使用可空类型语法无法表达这种情况,因为你不能像写?string|int那样在联合类型中混合可空类型。你也不能在联合类型中包含void类型。
为了演示联合类型,我们将string_to_int()函数修改为string_to_number()函数,该函数根据传入的字符串返回整数、浮动值或 NULL。我们还将int_to_message()函数更新为number_to_message(),该函数可以接受整数、浮动值或 NULL 作为参数。更新my_functions.php以匹配示例 5-18。
<?php
function string_to_number(string $numberString):❶int|float|NULL
{
return match ($numberString) {
❷'half' => 0.5,
'one' => 1,
'two' => 2,
'three' => 3,
'four' => 4,
'five' => 5,
default => NULL
};
}
function number_to_message(string $text, ❸ int|float|NULL $number): string
{
❹if (is_int($number)) {
return "'$text' as an integer = $number\n";
}
❺if (is_float($number)) {
return "'$text' as a float = $number\n";
}
❻ return "sorry, could not convert '$text' to a number\n";
}
示例 5-18:使用联合类型作为函数的返回值和参数
首先,我们声明string_to_number(),它是我们string_to_int()函数的修订版。我们使用联合类型int|float|null来表示该函数将返回一个整数、浮动值或 NULL❶。就像以前的string_to_int()一样,这个函数接受一个字符串参数。我们在函数体的match语句中增加了一个新的条件,将字符串'half'匹配为浮动值 0.5❷,因此需要使用联合类型。
接下来,我们声明 number_to_message(),它是 int_to_message() 的修订版本,返回一个字符串。这个函数接受两个参数。第一个参数,字符串 \(text,将与传递给我们的 string_to_number() 函数的字符串相同。第二个参数,\)number,将是该函数的返回值,因此它可能是一个整数、一个浮点数或 NULL。因此,我们对参数 ❸ 使用相同的 int|float|NULL 联合类型。
在函数体中,我们首先测试 $number 是否包含一个整数值 ❹,如果是,我们返回一条消息,说明 $text 是一个整数。接下来,我们测试 $number 是否包含一个浮点数值 ❺,如果是,则返回一条适当的消息。最后,我们返回一条消息,说明 $text 无法转换为数字 ❻。如果之前的任何一个 return 语句被执行,执行就不会到达这一步,所以我们知道此时 $number 既不是整数也不是浮点数。因此,我们不需要将这个最终的 return 语句放在一个 else 子句或另一个 if 语句中,尽管我们可以这样做。
这种选择是个人的编程风格问题。我喜欢像这样用一个无条件的 return 语句来结束函数,这样我可以清楚地看到要返回的默认值。然而,一些程序员更喜欢用一个 else 子句来结束最后一个 if 语句,以此来传达默认值。无论哪种方式,执行结果都是一样的。
现在让我们测试一下我们的函数。更新你的 main.php 脚本以匹配 清单 5-19。
<?php
require_once __DIR__ . '/my_functions.php';
$text1 = 'three';
$number1 = string_to_number($text1);
print number_to_message($text1, $number1);
清单 5-19:在 main.php 中使用联合类型参数和返回值调用函数
我们调用 string_to_number() 函数,传入字符串 'three',并将结果存储在 $number1 变量中。然后我们将 $number1 传递给我们的 number_to_message() 函数,并打印它返回的消息。这段代码应该输出消息 'three' as an integer = 3。
可选参数
如果一个参数的值在每次调用函数时通常都是相同的,你可以在声明函数时为该参数设置一个默认值。实际上,这使得该参数成为可选的。只有当你知道你希望该值与默认值不同时,你才需要包含一个与该参数对应的参数。
PHP 的许多内置函数都有带有默认值的可选参数。例如,PHP 的 number_format() 函数,它接受一个浮点数并将其转换为字符串,有几个可选参数控制字符串的格式。在命令行输入 php -a 以在交互模式下尝试以下代码:
❶ php > **print number_format(1.2345);**
1
❷ php > **print number_format(1.2345, 2);**
1.23
❸ php > **print number_format(1.2345, 1, ',');**
1,2
number_format()函数的第一个参数是必需的,它是我们想要格式化的浮动数值。默认情况下,仅传递一个参数❶时,函数会返回去掉小数部分的数字字符串。当我们添加一个整数作为可选的第二个参数❷时,函数使用该整数来设置保留的小数位数。我们使用了 2 这个值来保留两位小数。默认情况下,小数分隔符使用句点,但如果我们添加一个字符串作为可选的第三个参数❸,则函数会使用该字符串作为小数分隔符。在这种情况下,我们使用逗号,这是欧洲大陆常见的小数分隔符。
Listing 5-20 显示了 number_format()函数的签名,取自 PHP 在线文档,用于说明如何声明参数的默认值。
number_format(
float $num,
int $decimals = 0,
?string $decimal_separator = ".",
?string $thousands_separator = ","
): string
Listing 5-20: 内置的 number_format()函数,包括具有默认值的可选参数
首先,请注意,当你有一个长参数列表时,可以将它们分散在多行中,以使代码更具可读性。该函数最多接受四个参数,但第二、第三和第四个参数都使用赋值运算符(=)在参数名后面赋予了默认值。例如,第二个参数\(decimals 的默认值为 0,因此当我们调用 number_format(1.2345)而没有提供第二个参数时,函数将使用\)decimals 的默认值,并将数字格式化为没有小数位。同样,\(decimal_separator 参数的默认值为句点,而\)thousands_separator 参数的默认值为逗号。
参数声明的顺序很重要。所有必填参数(没有默认值的参数)必须先列出,后跟可选参数。这是因为调用函数时,参数的顺序必须与声明的顺序匹配。如果可选参数在必填参数之前,而你又省略了可选参数,那么就无法知道你的第一个参数是对应于第二个参数的。此规则的唯一例外是当你使用命名参数时,正如我们稍后在本章讨论的那样。
现在我们已经了解了可选参数的工作方式,让我们为一个自定义函数添加一个可选参数。我们将重新回顾本章前面提到的 which_is_smaller()函数,并添加一个可选参数,用于控制当传入的比较值相等时函数的行为。返回到该项目的my_functions.php文件,并更新脚本以匹配 Listing 5-21。
<?php
function which_is_smaller(int $n1, int $n2, ❶ bool $nullIfSame = false): ?int
{
if ($n1 < $n2) {
return $n1;
}
if ($n2 < $n1) {
return $n2;
}
❷ if ($nullIfSame) {
return NULL;
}
❸ return $n1;
}
Listing 5-21: 更新 which_is_smaller()函数以包含一个可选参数
我们向函数中添加了第三个参数,布尔值\(nullIfSame,并给它设置了默认值 false ❶。由于这个默认值,当\)n1 和\(n2 被发现相等时,函数通常会返回\)n1 ❸。然而,如果用户在调用函数时传递 true 作为第三个参数来覆盖这个默认值,则会返回 NULL ❷。为了考虑这种情况,我们使用可空类型?int 来设置函数的返回类型。
这里 if 和 return 语句的顺序非常重要。只有当\(n1 和\)n2 相等时,代码才会进入 if (\(nullIfSame) ❷。由于\)nullIfSame 默认是 false,因此这个条件通常会失败,所以最终会执行 return \(n1; ❸。只有当用户将\)nullIfSame 设置为 true 时,函数才会返回 NULL。
更新项目的main.php文件,如清单 5-22 所示,以测试该功能。
<?php
require_once __DIR__ . '/my_functions.php';
$result1 = which_is_smaller(1, 1);
var_dump($result1);
$result2 = which_is_smaller(1, 1, true);
var_dump($result2);
清单 5-22:在 main.php 中调用 which_is_smaller(),带有和不带有可选参数
我们调用 which_is_smaller()两次,使用 var_dump()显示结果。第一次我们传入 1 和 1,并省略可选参数,因此$nullIfSame 将默认是 false。第二次,我们添加了 true 作为第三个参数,覆盖了默认值。以下是运行主脚本时的输出:
int(1)
NULL
第一行表示函数遵循了默认行为,当我们省略可选参数时,返回 1(第一个参数的值)。然而,当我们使用第三个参数将$nullIfSame 设置为 true 时,函数返回 NULL。
位置参数与命名参数
当你调用一个函数时,PHP 引擎默认会按位置解释参数,依据它们的顺序将其与函数的参数匹配。然而,你也可以通过使用命名参数来调用函数:你显式地将参数的值与相应参数的名称配对。在这种情况下,参数的顺序就不再重要。命名参数在函数有可选参数时特别有用。
要使用命名参数而非位置参数,你不需要以任何方式修改函数声明,尽管此时函数参数的名称变得更加重要。你需要做的只是,在调用函数时,在括号内包含参数名称(去掉美元符号),然后加上冒号(:)和所需的参数值。例如,要在调用 which_is_smaller()函数时使用命名参数将 true 作为$nullIfSame 参数的值传入,你应在参数列表中加入 nullIfSame: true。约定是在冒号后加一个空格。
清单 5-23 显示了更新后的main.php文件,增加了一个使用命名参数调用 which_is_smaller()的额外实例。
<?php
require_once __DIR__ . '/my_functions.php';
$result1 = which_is_smaller(1, 1);
var_dump($result1);
$result2 = which_is_smaller(1, 1, true);
var_dump($result2);
❶ $result3 = which_is_smaller(nullIfSame: true, n1: 1, n2: 1);
var_dump($result3);
清单 5-23:使用位置参数和命名参数调用 which_is_smaller()
新的 which_is_smaller() ❶调用在功能上与之前的调用等效,但我们使用了命名参数。因此,我们能够按不同于参数声明顺序的顺序列出参数:首先是\(nullIfSame,然后是\)n1,最后是$n2。以下是结果:
int(1)
NULL
NULL
输出的最后两行都是 NULL,表示最后两个函数调用通过位置参数和命名参数达成了相同的结果。
在这个例子中,每个函数调用要么完全使用位置参数,要么完全使用命名参数,但你也可以在同一次函数调用中混合使用两种参数方式。在这种情况下,位置参数必须排在前面,顺序与函数声明中的顺序一致,然后是你选择的顺序排列的命名参数。考虑以下例子:
$result = which_is_smaller(5, nullIfSame: true, n2: 5);
这里,第一个参数 5 没有名称。因此,PHP 会按照位置来处理它,并将其匹配到声明的第一个参数$n1。剩下的参数是有名称的,因此可以按任意顺序出现。相比之下,这是另一个调用该函数的例子:
$result = which_is_smaller(nullIfSame: true, 5, n2: 5);
这次我们首先用了命名参数\(nullIfSame。然后我们使用了一个没有名称的参数 5,可能是用来传递\)n1 参数的。然而,由于我们一开始就用了命名参数,PHP 引擎无法识别这一点,因此这个函数调用会触发一个错误。
跳过的参数
当一个函数有多个可选参数时,你可以使用命名参数仅设置你想要的可选参数,同时跳过其余的参数。这之所以有效,是因为命名参数让你不必遵循参数的顺序。任何跳过的参数将使用默认值。为了说明这一点,我们来创建一个打印自定义问候语的函数。创建一个新项目,并创建my_functions.php,使其符合列表 5-24。
<?php
function greet(
string $name,
string $greeting = 'Good morning',
bool $hasPhD = false
): void
{
if ($hasPhD) {
❶ print "$greeting, Dr. $name\n";
} else {
print "$greeting, $name\n";
}
}
列表 5-24:一个带有两个可选参数的 greet()函数
我们将 greet()函数声明为 void,因为它输出一条消息,但不返回任何值。该函数有一个必需的字符串参数\(name,以及两个带默认值的可选参数\)greeting 和\(hasPhD。函数体是一个 if 语句,它输出\)greeting 和\(name 的值,如果\)hasPhD 参数为真,则在两者之间插入标题 Dr. ❶。
现在我们来看看几种调用 greet()函数的方法。创建一个包含列表 5-25 中代码的main.php。
<?php
require_once __DIR__ . '/my_functions.php';
greet('Matt');
greet('Matt', hasPhD: true);
列表 5-25:一个主脚本调用 greet(),并跳过参数
第一次调用 greet()时,我们只传递字符串'Matt'作为参数。我们没有使用命名参数,因此这个参数会按位置匹配到$name 参数。其他参数会使用默认值,结果输出的消息是“Good morning, Matt”。
第二次调用 greet()时,我们使用位置参数'Matt'和命名参数 hasPhD: true。请注意,$hasPhD是函数声明中的第三个参数;我们跳过了第二个参数!这是完全没问题的。我们跳过的参数$message有一个默认值,感谢我们使用命名参数,PHP 引擎会清楚地知道哪些提供的参数与哪些函数参数匹配。最终我们应该得到消息:Good morning, Dr. Matt。
这是运行main.php脚本的输出:
Good morning, Matt
Good morning, Dr. Matt
输出正如我们所预期的那样。由于默认参数值和命名参数的结合,我们能够顺利跳过$message参数。
值传递与引用传递
默认情况下,PHP 函数使用值传递的方法将参数与参数匹配:参数的值会被复制并赋值(传递)给适当的参数,这些参数在函数的作用域内作为临时变量存在。通过这种方式,如果在函数执行过程中修改了任何参数的值,这些更改将不会对函数外部的任何值产生影响。毕竟,函数是处理原始值的副本。
另一种方法是引用传递:函数参数传递的是指向原始变量本身的引用,而不是副本。通过这种方式,如果一个变量作为参数传递给函数,函数可以永久改变该变量的值。为了指示引用传递参数,在声明函数时,请在参数名之前立即放置一个“&”符号。
我通常不推荐使用引用传递参数;事实上,在过去 20 年里,我无法想到任何我写过的引用传递参数。允许函数修改传递给它们的变量会使程序变得更加复杂,从而更难理解、测试和调试。尽管如此,熟悉这一概念仍然很重要,因为你可能会在别人编写的代码中遇到引用传递参数,包括在你可能希望在自己项目中使用的第三方库中。不了解如何使用引用传递参数就调用函数,可能会导致意外的结果。
注意
在某些编程语言中,程序员使用多个引用传递参数,作为函数“返回”多个值的一种方式,而无需使用return语句。然而,在现代 PHP 中有更好的方法,例如返回一个数组(参见第七章)或一个对象(参见第 V 部分)。
为了说明值传递和引用传递参数之间的区别,并展示为什么后者通常最好避免,我们将创建一个计算某人未来年龄的函数的两个版本。开始一个新项目并创建包含清单 5-26 内容的my_functions.php。
<?php
function future_age (int $age): void
{
$age = $age + 1;
print "You will be $age years old on your next birthday.\n";
}
清单 5-26:按值传递的 future_age()版本
在这里,我们声明了一个名为 future_age()的函数。它具有一个整数参数\(age,按通常方式声明,因此这是一个正常的值传递参数。由于不需要返回任何值,函数被声明为 void。在函数体内,我们将\)age 加 1,并打印出包含结果的消息。
现在在main.php中创建一个主脚本,包含清单 5-27 中显示的代码。
<?php
require_once __DIR__ . '/my_functions.php';
$currentAge = 20;
print "You are $currentAge years old.\n";
future_age($currentAge);
print "You are $currentAge years old.\n";
清单 5-27:测试按值传递的 future_age()版本
我们为\(currentAge 变量赋值为整数 20。然后我们打印出显示该变量值的消息。接着,我们调用我们的 future_age()函数,并将\)currentAge 作为参数传递。然后我们再打印出另一条消息,显示变量的值。这让我们可以查看函数调用前后$currentAge 的值。以下是结果:
You are 20 years old.
You will be 21 years old on your next birthday.
You are 20 years old.
输出的第一行和最后一行是相同的,表明调用 future_age()对\(currentAge 变量的值没有影响。实际上,当函数被调用时,会在函数的作用域内创建一个局部变量\)age,并将\(currentAge 的值复制到其中。这样,当函数将\)age 加 1 时,它是在不改变$currentAge 值的情况下进行的。这就是值传递参数的工作方式:它们不会对函数外部的作用域产生任何影响。
现在让我们修改我们的 future_age()函数,改为使用按引用传递的参数,看看这会有什么不同。更新你的my_functions.php文件,如清单 5-28 所示。
<?php
function future_age (int &$age): void
{
$age = $age + 1;
print "You will be $age years old on your next birthday.\n";
}
清单 5-28:按引用传递的 future_age()版本
这里唯一的变化是,在参数名称前加上了一个&符号,表示\(age 是按引用传递的参数。因此,\)age 将不再是一个局部变量,包含调用函数时传入参数的值的副本。相反,\(age 将成为该变量的引用,因此对\)age 所做的任何更改也将反映到该变量上。为了验证这一点,再次运行你的main.php脚本。这次你应该会看到以下输出:
You are 20 years old.
You will be 21 years old on your next birthday.
You are 21 years old.
请注意,在函数内部对\(age 参数加 1 也会使得函数外部的\)currentAge 变量加 1。除非用户的生日发生在函数调用和最终打印语句之间的那一瞬间,否则这可能不是我们想要的。这说明了使用按引用传递参数的危险:它们可能会改变通常在函数作用域之外的变量值。
总结
在本章中,我们探讨了如何通过声明和调用函数来促进代码的可重用性,函数是完成特定任务的一系列命名代码。你练习了在独立的.php文件中声明函数,然后通过 require_once 将其加载到主应用程序文件中,从而编写简洁、结构良好的脚本。你看到返回语句如何允许函数将值返回给调用脚本,同时也提供了一种提前终止函数的机制,并且你探索了如何通过可空和联合类型使函数能够灵活地接受或输出各种数据类型的值。
你了解了参数(函数内使用的变量)和实参(调用函数时传递给这些变量的值)之间的区别。你看到如何通过为参数设置默认值使其变为可选参数,以及如何使用命名参数按照任意顺序传入值,甚至可以跳过某些参数。最后,你了解了值传递和引用传递参数之间的区别,在你希望函数能够更新其作用域外的变量时,这是一个罕见的情况。
练习
1. 创建一个项目,其中包含独立的main.php和file2.php脚本。file2.php脚本应该打印出字符串 '456'。在你的main.php脚本中,首先打印出 '123',然后读取并执行 file2.php,接着打印出 '789'。最终输出应该是 123456789,但中间的 456 是从 file2.php 打印出来的。
2. 编写一个项目,声明一个 which_is_larger() 函数,该函数返回两个整数中的较大者。你的main.php脚本应当读取并执行声明该函数的文件,然后打印出以下参数传入函数的结果:
4 和 5
21 和 19
3 和 3
最后一种情况发生了什么,当参数相同时?
3. 修改你的 which_is_larger() 函数,使其能够接受整数或浮点数,并在两个数字相同的情况下返回整数、浮点数或 NULL。
4. 创建一个 my_functions.php 文件,声明一个无返回值的函数,用于打印出你名字的首字母的 ASCII 艺术风格。此函数应有两个参数,一个(\(character)为字符串,设置用于制作艺术作品的字符,另一个(\)spacer)为字符串,设置用于填充空白的字符。为每个参数指定合适的默认值。例如,由于我名字的第一个字母是 M,我的函数可能是 capital_m(string $character = 'M', string $spacer = ' '),如果没有传入任何参数,它可能会提供如下输出:
MM MM
MMMM MMMM
MM MMM MMM MM
MM MMMM MM
MM MM
MM MM
MM MM
接下来,编写一个 main.php 脚本,调用你的函数且不传入任何参数(使用默认值)。然后使用命名参数再次调用该函数两次,一次只传入主字符,另一次只传入填充字符。
第二部分 处理数据
第六章:6 循环

本章介绍了循环,一种控制结构,它允许你反复执行一系列语句。一旦你理解了如何使用循环,你将能够轻松创建脚本,处理大量数据,并用高效编写的代码执行重复性任务。
我们将介绍两种主要的 PHP 循环:while 循环和 for 循环。while 循环用于在满足特定条件之前重复一系列操作,而 for 循环用于重复固定次数的操作。我们还将了解 do...while 循环,它是 while 循环的一种变体。第四种类型的 PHP 循环是 foreach 循环,专门用于遍历数据集合,如数组。我们将在第七章介绍 foreach 循环时讲解数组。
while 循环
PHP 中的一种循环是 while 循环:一系列语句会在某个条件为真时反复执行。图 6-1 展示了这一过程是如何运作的。

图 6-1:while 循环的逻辑流程
当脚本包含 while 循环时,通常会有一些语句在循环开始之前执行。然后,PHP 引擎检查控制循环的条件。如果条件为假,则循环中的语句将永远不会执行,程序会跳过循环后面的语句。如果条件为真,循环中的语句会执行一次,然后再次检查条件。如果条件仍然为真,循环中的语句将再次执行。这个过程会继续进行,PHP 引擎会在每次新一轮重复前检查条件,直到条件为假,循环结束。
编写 while 循环时,从 while 关键字开始,后跟括号中的布尔条件,控制循环的执行。然后,将需要重复执行的语句放入大括号中。如果循环只包含一条语句,大括号是可选的,但大多数程序员仍然会使用大括号,以强调该语句是循环的一部分。
示例 6-1 显示了一个使用 while 循环来测试密码长度的脚本。该循环会不断提示用户输入新密码,直到密码足够长。
<?php
$password = "cat";
while (strlen($password) < 6) {
$password = readline("enter new password (at least 6 characters): ");
}
print "password now set to '$password'";
示例 6-1:一个要求密码长度为六个字符或更多的 while 循环
在循环之前,我们将 $password 变量设置为初始值 "cat"。然后我们声明 while 循环,使用 strlen($password) < 6 作为条件。在每次循环之前,PHP 会检查 $password 的长度。如果密码长度小于六个字符,条件为真,循环中的语句将执行。如果密码长度为六个字符或更多,条件为假,循环将结束。由于 $password 最初是三个字符,所以我们知道循环至少会执行一次。
在循环内,我们使用内置的 readline() 函数从用户那里获取密码。该函数将传递的字符串作为命令行提示符显示,然后读取用户在命令行输入的内容,直到按下 ENTER 键。我们将结果存储回 $password 变量中,因此在下一次循环重复之前,我们会检查新的值。当密码长度足够并且循环结束时,我们会将新值的 $password 返回给用户确认。
注意
这个程序 不是 一个良好的安全密码协议示例,因为密码会显示在屏幕上,任何人都可以看到。但它是一个演示 while 循环 的便捷示例。
以下是该脚本的一个示例运行。我在输入一个可接受的密码之前,先输入了几个过短的密码:
enter new password (at least 6 characters): **dog**
enter new password (at least 6 characters): **whale**
enter new password (at least 6 characters): **catdog123**
password now set to 'catdog123'
以下是每次 PHP 引擎检查 strlen($password) < 6 条件时的情况,发生在此脚本的运行过程中:
1. $password 包含 "cat",因此条件为真。循环执行一次。
2. $password 包含 "dog",所以条件依然为真。循环第二次执行。
3. $password 包含 "whale",所以条件依然为真。循环第三次执行。
4. $password 包含 "catdog123",所以条件为假。循环结束,最后的打印语句执行。
总的来说,PHP 引擎检查循环条件四次,并且循环内容重复执行三次。每次 while 循环的条件检查次数总比循环的重复次数多一次,因为条件检查发生在循环内容执行之前。试着将$password 的初始值改为至少六个字符的文本,然后再次运行脚本。你会发现脚本直接跳到最后的打印语句,因为第一次检查循环条件时条件值为假。
do...while 循环
do...while 循环是 while 循环的另一种形式:条件检查发生在每次循环重复之后,而不是之前。这样,循环中的语句保证至少执行一次,且循环的重复次数将与条件检查的次数相匹配。图 6-2 展示了这一过程是如何工作的。

图 6-2:do...while 循环的逻辑流程
使用 do...while 循环时,我们首先执行一次循环中的语句。然后测试循环条件。如果条件为假,程序会跳到循环后的语句,因此循环只执行一次。如果条件为真,循环语句会再次执行,依此类推,直到条件为假。
要编写 do...while 循环,首先使用 do 关键字,然后用大括号括起应重复执行的语句。闭括号之后,写上 while 关键字,然后在括号内写上循环条件。例如,示例 6-2 展示了如何通过使用 do...while 循环来重写 示例 6-1 中的密码检查脚本,而不是使用 while 循环。更改部分用黑色文字显示。
<?php
do {
$password = readline("enter new password (at least 6 characters): ");
} while (strlen($password) < 6);
print "password now set to '$password'";
示例 6-2:示例 6-1 的修改版本,使用了 do...while 循环
我们通过 do 关键字开始循环。然后,在 readline() 函数的语句之后,在闭括号所在的同一行,我们写上 while 关键字,然后在括号内写上与之前相同的 strlen ($password) < 6 循环条件。请注意,我们需要在条件后加上分号,以结束语句。
这与之前版本脚本的关键区别在于,我们不再需要在进入循环前为 $password 变量设置初始值。相反,在第一次运行循环时,我们从用户那里读取一个初始密码,然后测试其值并决定是否需要重复。一般来说,如果你知道希望循环中的语句至少执行一次,那么 do...while 循环可能比 while 循环更适合。
布尔标志
如果决定是否继续循环的逻辑比单一条件更复杂,通常通过使用一个布尔变量(称为标志)来控制循环会更加清晰。通常,你会将标志设置为 true,然后进入循环,使用该标志作为条件。循环本身会包含逻辑(可能是一个 if 语句系列),在循环准备结束时,将标志的值设置为 false。
例如,假设我们想要反复提示用户输入,直到他们输入 quit 或 q。我们可以通过以下方式使用 while 循环来实现:
while (($userInput != 'q') && ($userInput != 'quit')) {
这个循环的条件几乎难以阅读,如果我们想要监控第三个输入,它会变得更加复杂。通过布尔标志控制循环可以使代码更清晰,正如在示例 6-3 中所示。
<?php
$continueLooping = true;
while ($continueLooping) {
$userInput = readline("type something (or: quit): ");
if ($userInput == 'quit') {
$continueLooping = false;
}
if ($userInput == 'q') {
$continueLooping = false;
}
print "you typed '$userInput'\n";
}
print '--- I have left the loop! ---';
示例 6-3:使用布尔标志变量作为循环条件
我们首先创建布尔标志变量 $continueLooping 并将其设置为 true。然后,我们声明一个 while 循环,以该标志作为循环条件。请注意,这比之前的复合条件更清晰。由于条件最初为 true,我们将进入循环并至少执行一次循环语句组。在提示用户输入文本并将其存储在 $userInput 变量中之后,我们使用两个 if 语句来检查该变量是否包含 'quit' 或 'q'。这两个 if 语句代替了原来的复合循环条件;当用户要求退出时,它们将标志设置为 false,从而结束循环。在 if 语句之后,我们打印用户的输入。然后,在循环外部,我们打印一条消息,确认循环已经结束。
这是脚本的一个示例运行:
type something (or: quit): **the**
you typed 'the'
type something (or: quit): **cat sat**
you typed 'cat sat'
type something (or: quit): **on**
you typed 'on '
type something (or: quit): **the mat**
you typed 'the mat'
type something (or: quit): **quit**
you typed 'quit'
--- I have left the loop! ---
如你所见,脚本会打印出用户输入的任何文本。在这个例子中,循环在我输入quit后结束,但如果你输入字母q,它也会结束。
break语句
break关键字会立即退出循环,不允许任何剩余的语句在循环中执行。在前一个脚本的输出中,你可能注意到当我输入quit时,这个词会在循环的最后一个打印语句(你输入了'quit')中重复显示,然后才会终止循环。使用break语句后,当用户输入quit或q时,我们可以立即停止循环,而不会执行打印语句。
使用break还消除了布尔标志的需求。相反,我们可以通过写while (true)来直接使用布尔值true作为循环条件。由于true始终为真,这个循环理论上会永远重复,或者至少直到某个条件逻辑触发break语句为止。列表 6-4 展示了如何更新我们的用户输入脚本,使用while (true)和break。
<?php
❶ while (true) {
$userInput = readline("type something (or: quit): ");
if ($userInput == 'quit'){
break;
}
if ($userInput == 'q'){
break;
}
❷ print "you typed '$userInput'\n";
}
❸ print '--- I have left the loop! ---';
列表 6-4:列表 6-3 的更新版本,使用break语句退出while (true)循环
我们删除了\(continueLooping 布尔标志,并用字面量`true`替换了`while`循环的条件❶。在循环内部,我们仍然有两个`if`语句来测试`\)userInput是否包含字符串'quit'或'q',但这次每个if语句中都简单地包含了break关键字,以立即退出循环。这样,如果任何一个if`语句通过,我们将直接跳到最终的打印语句❸,而不会执行循环内部的打印语句❷。
这是更新后的脚本版本的一个示例运行:
type something (or: quit): **hello**
you typed 'hello'
type something (or: quit): **world**
you typed 'world'
type something (or: quit): **quit**
--- I have left the loop! ---
这次,当我输入quit时,循环不再打印“你输入了'quit'”这样的消息。相反,循环会立即结束,所以下一条消息是——我已经离开循环!——。
在许多情况下,是否使用break语句或布尔标志来终止循环,取决于个人偏好。如果你来自于不支持break语句的语言,布尔标志可能会感觉更自然。另一方面,如果需要测试多个条件来决定循环是否结束,break语句则更为实用。例如,在编写代码编译器或编程语言工具时,可能需要包含几十个甚至上百个测试。使用break语句可以避免你在执行完所有测试后,需要滚动浏览代码页面以查看后续循环语句中可能发生的情况。
for循环
for 循环是一种重复固定次数的循环。如果你确切知道你想要重复任务的次数(例如,给用户三次机会输入正确的密码,或者从试题库中随机选择 10 道题目测试学生),那么 for 循环可能比 while 循环更合适。for 循环围绕一个 计数器变量,通常称为 $i(即 迭代器 的缩写),它控制循环的重复次数。声明一个 for 循环需要三个表达式,所有表达式都涉及到这个计数器变量:
1. 一个表达式,用于将计数器初始化为起始值
2. 一个表达式,用于测试计数器的值,以决定何时停止循环
3. 一个表达式,用于在每次循环后递增(或递减)计数器
要声明一个 for 循环,这三个表达式会在 for 关键字后面依次写在一行内,并且被一对括号括起来。下面是一个示例:
for ($i = 1; $i <= 5; $i++) {
这里 $i = 1 将计数器变量初始化为 1。然后 $i <= 5 设置循环条件;只要 \(i 小于或等于 5,循环将继续执行。PHP 会在每次新一轮循环前检查这个条件。最后,\)i++ 使用增量运算符(++)告诉 PHP 在每次循环后将 1 加到 \(i 上。这样,\)i 就会在每个循环周期中获取一个新的值,并且这个值可以与循环条件进行比较。在这种情况下,第一次执行时,\(i 的值为 1,第二次执行时为 2,以此类推。当第五次执行时,\)i 的值为 5,\(i <= 5 的条件仍然成立,但在第五次循环结束时,\)i 会被递增到 6。此时,$i 不再小于或等于 5,因此循环在执行完五次后结束。
为了验证 for 循环是否按预期工作,让我们在循环体内填入一个简单的 print 语句,如示例 6-5 所示。这个脚本应该会打印相同的消息五次。
<?php
for ($i = 1; $i <= 5; $i++) {
print "I am a for loop\n";
}
示例 6-5:一个 for 循环的示例
我们声明一个 for 循环,使用我们刚刚讨论过的循环变量 $i 的相同表达式集。在大括号内,定义了循环语句组,我们写入一个 print 语句。下面是运行该脚本的输出:
I am a for loop
I am a for loop
I am a for loop
I am a for loop
I am a for loop
如你所见,由于 for 循环的计数器变量从 1 开始递增,直到不再小于或等于 5,消息确实打印了五次。
在循环中使用计数器
for 循环的一个优势是计数器变量 $i 可以在循环体内使用。这有助于处理数学任务或使用通过一系列整数索引或标识的有序数据集。例如,我们可能希望处理数据库表中所有 ID 为从 1 开始的整数序列的项(有关数据库的更多信息,请参见 第六部分)。或者我们可能希望顺序遍历一个整数索引数组中的所有元素(有关数组的更多信息,请参见 第七章)。
为了演示,列表 6-6 显示了我们原始 for 循环的更新版本,该版本将 $i 的值包含到打印的消息中。由于 $i 在每次循环重复时都有不同的值,现在每条消息都将是唯一的。
<?php
for ($i = 1; $i <= 5; $i++) {
print "I am repetition $i of a for loop\n";
}
列表 6-6:在 for 循环中使用计数器变量 $i
我们已经更新了循环的打印语句,以包括 $i 的值。以下是结果:
I am repetition 1 of a for loop
I am repetition 2 of a for loop
I am repetition 3 of a for loop
I am repetition 4 of a for loop
I am repetition 5 of a for loop
请注意,每一行输出中的数字会根据 $i 的值变化。输出有助于说明计数器变量是如何工作的:它从 1 开始,并随着每次循环重复增加 1。一旦 \(i 达到 6,\)i <= 5 条件不再成立,因此循环结束,而没有打印 "I am repetition 6 of a for loop"。
到目前为止,我们一直将 $i 初始化为 1,但你可以将其初始化为任何你想要的值。实际上,你会发现很多 for 循环的例子使用了从 0 开始的计数器变量。这是因为 for 循环通常与数组一起使用,数组是从 0 开始编号的项目集合。我们将在 第七章 中讨论数组,以及如何遍历它们。
当你将计数器变量初始化为 0 时,通常也会使用小于运算符 (<) 设置循环条件,而不是小于或等于运算符 (<=),如下所示:
for ($i = 0; $i < 3; $i++) {
只要 $i 小于 3,循环就会继续执行。由于 \(i 从 0 开始,循环将执行三次,\)i 分别为 0、1 和 2。
跳过循环语句
continue 关键字停止当前循环的重复,但与 break 关键字不同,它不会完全结束循环。相反,循环会立即跳到下一个重复。这在你想跳过某些循环次数时非常有用。例如,也许你正在从数据库中检索条目,并希望忽略某些值,或者你只想使用某些序列中的数字,例如从一个随机排列的数据集中每三个项目的随机样本。
列表 6-7 显示了一个包含 continue 语句的 for 循环示例。在这里,我们使用 continue 跳过计数器变量 $i 的奇数值,因此我们最终只打印偶数值。该列表还展示了如何让 for 循环递减计数器变量,而不是递增它。
<?php
for ($i = 8; $i > 0; $i--) {
❶ $odd = ($i % 2);
if ($odd) {
continue;
}
❷ print "I am an even number: $i\n";
}
列表 6-7:使用 continue 跳过 for 循环的部分
我们声明了一个 for 循环,其中 $i 计数器变量从 8 开始,并在每次重复后通过 $i-- 表达式递减。循环将倒计时到 1,然后当 $i 等于 0 时停止。在循环内部,我们使用取模运算符 (%) 测试当前 \(i 的值是偶数还是奇数 ❶。如果是偶数,\)i % 2 的结果为 0;如果是奇数,$i % 2 的结果为 1。不论哪种情况,我们将结果存储在 $odd 变量中,然后将该变量作为 if 语句的条件。
由于 if 语句需要一个布尔条件,$odd 将被强制转换为布尔值:当为 1 时为 true,0 时为 false。这样,当 $i 为奇数时,我们会执行 if 语句,该语句只包含 continue 关键字,用于中断当前循环的重复并跳到下一个。当 $i 为偶数时,我们不执行 if 语句的主体,从而通过执行 print 语句 ❷ 完成当前循环的重复。最终效果是我们只打印出 $i 的偶数值,输出如下所示:
I am an even number: 8
I am an even number: 6
I am an even number: 4
I am an even number: 2
由于我们的条件逻辑触发了循环中的 continue 语句,我们成功跳过了奇数。
注意
continue 关键字在 while 循环中的作用与在 for 循环中一样。同样,break 关键字也用于完全停止一个 for 循环。
处理最后一次重复
有时候,你可能希望在循环的最后一次重复时做一些不同的事情。对于 while 或 do...while 循环,直到循环结束时你才知道是否是最后一次重复,但对于 for 循环,你可以通过条件逻辑预见到最后一次重复,并编写代码以不同的方式处理这一重复。
比如说,我们使用一个 for 循环来构建一个字符串,其中包含用户输入的项列表,并且我们希望用逗号分隔每一项。我们可能会想写类似 Listing 6-8 这样的代码。
<?php
$message = "go to the market and buy: ";
$numItems = 3;
for ($i = 1; $i <= $numItems; $i++) {
$item = readline("type something to buy: ");
$message .= "$item, ";
}
print $message;
Listing 6-8: 一个使用 for 循环创建的项列表,并用逗号分隔
我们初始化一个 $message 变量,值为 "go to the market and buy: "。然后我们将 \(numItems 的值设为 3。这将是我们 for 循环的重复次数。接着,我们声明 for 循环,在该循环中,\)i 从 1 数到 $numItems(3)。每次循环时,我们提示用户输入一个要购买的商品,并将输入值存储在 $item 变量中。然后我们将 $item 的值附加到 $message 字符串后面,后面跟一个逗号和一个空格。当循环结束时,我们打印出构建好的 $message 字符串。
问题在于我们将循环的每次重复都视为相同,因此最终消息中的每一项后面都会跟一个逗号,包括最后一项。你可以在以下脚本的示例运行中看到这一点:
type something to buy: **bread**
type something to buy: **butter**
type something to buy: **apples**
go to the market and buy: bread, butter, apples,
我们的脚本已经构建了一个包含在命令行输入的三项内容的消息,但不幸的是,最后一项“apples”后面出现了逗号。我们可以通过添加一个测试来确定是否是最后一次循环迭代,然后只有在不是最后一次迭代时才添加逗号。清单 6-9 展示了如何更新脚本。
<?php
$message = "go to the market and buy: ";
$numItems = 3;
for ($i = 1; $i <= $numItems; $i++) {
$item = readline("type something to buy: ");
$message .= $item;
$lastIteration = ($i == $numItems);
if (!$lastIteration) {
$message .= ', ';
}
}
print $message;
清单 6-9:更新清单 6-8 的脚本以去掉最后的逗号
这次,我们首先仅将 $item 的值附加到 $message 字符串中,不加逗号和空格。然后,我们创建了一个布尔变量 $lastIteration,并赋予它表达式 $i == \(numItems 的值。这个表达式只有在循环的最后一次迭代时才为 true。接下来,我们使用 if 语句,并将 !\)lastIteration 作为条件。感谢 NOT 操作符(!),这个条件在所有循环迭代中都为 true,除了最后一次。在 if 语句内部,我们将逗号和空格附加到 $message。这样,除了最后一项,列表中的每一项后面都会有逗号。
下面是更新后的脚本运行示例:
type something to buy: **bread**
type something to buy: **butter**
type something to buy: **apples**
go to the market and buy: bread, butter, apples
我们不再在列表的最后一项“apples”后面加逗号,因为我们将 for 循环的最后一次迭代与其他迭代区分开来处理。
注意
一旦我们开始使用数组,就可以通过使用内置的 implode() 函数来避免这种特殊的最后一次迭代循环逻辑。它智能地在列表中的每一项之间添加分隔符,但不会在最后一项后添加。我们将在第七章中讨论这个问题。
另一种循环语法
PHP 提供了另一种语法来编写 while 和 for 循环,用冒号(:)而不是用大括号将循环内容括起来。就像我们在第四章中讨论的 if 语句的替代语法一样,这种编写循环的方式在将 PHP 语句与模板文本(如用于 web 应用程序的 HTML)结合使用时非常有用。为了演示,清单 6-10 使用替代语法重写了清单 6-1 中的密码设置 while 循环。
<?php
$password = "cat";
while (strlen($password) < 6):
$password = readline("enter new password (at least 6 characters): ");
endwhile;
print "password now set to '$password'";
清单 6-10:来自清单 6-1 的 while 循环的另一种语法
请注意,声明 while 循环的那一行以冒号结尾,而不是以大括号开头。我们使用 endwhile 关键字来表示循环的结束,而不是闭合大括号。
清单 6-11 同样展示了来自清单 6-6 的 for 循环的另一种语法。
<?php
for ($i = 1; $i <= 5; $i++):
print "I am repetition $i of a for loop\n";
endfor;
清单 6-11:来自清单 6-6 的 for 循环的另一种语法
再次注意声明循环的那一行以冒号结尾,并且使用 endfor 关键字来表示循环的结束。
注意
PHP 没有 do...while 循环的另一种语法。
避免无限循环
很容易不小心陷入一个无限循环,导致循环不断重复,因为停止条件永远无法满足。为了避免这种情况,确保控制循环的条件能够在适当的时候变为假是非常重要的。一种常见的错误写法是在循环条件相对的方向上设置递增表达式。例如,考虑以下代码:
for ($i = 1; $i > 0; $i++) {
这个递增表达式在每次重复后给\(i 加 1。同时,循环条件会测试\)i 是否大于 0。由于\(i 在每次重复时都在增加,它将始终大于 0,因此循环永远不会结束。解决方法是让\)i 递减,而不是递增,或者将循环条件改为某种小于比较。
使用 while 和 do...while 循环时,如果循环条件中的变量在循环语句中没有机会被正确改变,就容易陷入无限重复。例如,假设我们想编写一个脚本,将用户输入的价格累加,直到总和超过 100 美元,然后打印出结果。我们可能会不小心写出像列表 6-12 这样的代码,导致无限循环。
<?php
$total = 0;
do {
$costString = readline("enter item cost: ");
if (is_numeric($costString)) {
❶ $total = floatval($costString);
}
} while ($total < 100);
print "grant total = \$$total\n";
列表 6-12:一个无意中的无限 do...while 循环
我们将\(total 设置为 0,然后进入一个 do...while 循环,只要\)total 小于 100 就一直重复。在循环内部,我们从用户那里获取一行输入,并验证其是否为数字。如果是数字,我们将输入转换为浮动类型,并将该值存储在$total 中❶。
你看到问题了吗?我们本应该使用类似$total += floatval($costString);这样的代码来将最新输入的值加到已有的\(total 值中,但我们使用了常规的赋值操作符(=),而不是加法赋值操作符(+=)。因此,\)total 的值将始终是最后输入的值。如果用户输入的值大于 100,循环会结束,打印语句会回显该最后的值。否则,我们就会陷入无限循环,而无法真正计算累计总和。
无限 while 循环还会发生在循环条件中测试的变量永远不会变化的情况下,一旦进入循环,就无法退出。回到列表 6-12,例如,我们可能会使用一个\(grandTotal 变量来设置循环条件,如`while(\)grandTotal < 100)`,但在循环内部递增的是\(total 变量,而不是\)grandTotal。这样,$grandTotal 将永远不变,循环将永远执行下去。
总结
在本章中,我们研究了 while 循环、do...while 循环和 for 循环,这些循环提供了不同的方式来重复执行一系列语句。关键在于如何决定循环的重复次数:可以设置循环停止的条件,如 while 或 do...while 循环,或者指定固定的重复次数,如 for 循环。除了这些基本的循环结构外,我们还讨论了 break 和 continue 语句,分别提供了强制结束整个循环或当前循环重复的机制。掌握了像循环和选择语句这样的控制结构后,你将能够编写出执行重复任务并根据当前条件作出决策的复杂程序。
练习
1. 使用 do...while 循环持续接受用户输入的单词,直到输入的单词以大写字母开头。
提示:将输入的字符串与 ucfirst() 函数返回的值进行比较。
2. 使用 while (true) 循环配合 break 语句,持续接受用户输入的字符串,直到输入的字符串是数字。
提示:使用 is_numeric() 函数。
3. 在 for 循环中使用 continue 语句,打印出所有小于等于 21 的 3 的倍数(3、6、9,依此类推)。
第七章:7 简单数组

本章介绍了数组,这是一种复合数据类型,用于存储和操作多个数据项,在一个变量名称下。数组允许你将相关数据分组,并高效地对每个数据项应用相同的操作。
从本质上讲,数组是值与键的映射。每个值是你想要存储在数组中的数据,而它的键是与该值关联的唯一标识符,以便你可以从数组中访问它。在本章中,我们将重点讲解简单数组,它们使用整数作为键。你将学习如何创建和操作简单数组,并通过 foreach 循环遍历数组中的项目。在下一章中,我们将探讨如何通过使用字符串(以及其他数据类型)作为键来创建更复杂的数组,而不是使用整数。
创建数组并访问其值
让我们从创建一个简单数组开始,这个数组存储了某个地点的每月降水总量(列表 7-1)。
<?php
$rainfall = [10, 8, 12];
列表 7-1:声明一个简单数组
我们通过在方括号内使用逗号分隔的值序列来声明一个名为$rainfall 的数组:[10, 8, 12]。这是一个三元素数组,包含值 10、8 和 12。
默认情况下,PHP 会给每个数组值分配一个整数作为键。键是按顺序分配的,从零开始:第一个值(10)的键是 0,第二个值(8)的键是 1,第三个值(12)的键是 2。接受这个默认映射就是使$rainfall 成为一个简单数组(与我们将在第八章中探讨的具有自定义键值映射的复杂数组相对)。
现在我们有了一个数组,可以使用其键单独访问其值。在列表 7-2 中,我们将$rainfall 数组中的每个值连接成一个字符串消息并打印出来。
<?php
$rainfall = [10, 8, 12];
print "Monthly rainfall\n";
print "Jan: " . $rainfall[0] . "\n";
print "Feb: " . $rainfall[1] . "\n";
print "Mar: " . $rainfall[2] . "\n";
列表 7-2:通过整数键访问数组元素
我们通过在数组名称后面指定方括号中的键来访问数组中的项目。例如,\(rainfall[0]给我们返回\)rainfall 数组中的第一个值(10),然后我们将其与字符串"Jan: "连接。同样,我们可以通过\(rainfall[1]访问数组中的第二个元素。由于整数键从零开始,具有*n*个元素的数组的最后一个元素的键是*n* - 1。在这个例子中,我们通过\)rainfall[2]访问我们这个三元素数组的最后一个元素。以下是运行该脚本的输出:
Monthly rainfall
Jan: 10
Feb: 8
Mar: 12
值 10、8 和 12 已经成功地从数组中读取并使用它们的整数键 0、1 和 2 打印出来。
如果你尝试使用一个未分配的键访问数组元素,PHP 会给出警告。例如,假设我们在列表 7-2 的末尾添加以下打印语句:
print "Apr: " . $rainfall[3] . "\n";
这条语句将触发类似于以下的警告:
PHP Warning: Undefined array key 3 in /Users/matt/main.php on line 8
我们的数组只有三个元素,键分别为 0、1 和 2,因此没有与$rainfall[3]对应的元素。稍后我们将在本章中讨论如何通过首先确保数组中具有特定键的元素存在来避免类似的警告,然后再尝试访问它。
更新数组
通常,在创建数组后,你需要通过添加或删除元素来更新数组。例如,通常会从一个空数组开始,通过将一个空的方括号([])赋值给一个变量来创建它,然后随着脚本的执行,逐渐向其中添加元素。在本节中,我们将讨论一些常见的技术,用于更改数组的内容。
追加元素
如果你正在向一个数组添加新元素,你通常会希望将其添加到数组的末尾,这个操作被称为追加。这是一个非常常见的任务,PHP 通过简单的赋值语句使其变得非常容易。在等号左侧,你写数组的名称,后面跟着一个空的方括号;在等号右侧,你写你想要追加到数组中的值。例如,Listing 7-3 展示了一个脚本,它创建了一个空的动物数组,然后将元素追加到它的末尾。
<?php
$animals = [];
$animals[] = 'cat';
$animals[] = 'dog';
print "animals[0] = $animals[0] \n";
print "animals[1] = $animals[1] \n";
Listing 7-3: 追加元素到数组的末尾
我们通过写一个空的方括号来声明一个名为\(animals 的空数组。然后,我们一个一个地将两个元素添加到数组的末尾。例如,\)animals[] = 'cat' 将字符串值'cat'添加到数组的末尾。PHP 会自动为新元素分配下一个可用的整数作为键。在这个例子中,由于$animals 在添加'cat'时是空的,所以它的键被赋值为 0。当我们使用相同的符号将'dog'添加到数组时,该元素自动得到键 1。为了确认这一点,我们在脚本末尾打印数组的每个值,输出如下:
animals[0] = cat
animals[1] = dog
输出结果表明,'cat'成功映射到了数组的键 0,而'dog'映射到了键 1。PHP 引擎能够找到数组中最高的整数键,给它加 1,并将结果作为追加到数组时的下一个唯一整数键。
使用特定键添加元素
虽然更常见的做法是将元素追加到数组,并让 PHP 自动分配下一个可用的整数键,但你也可以在向数组添加元素时手动指定该元素的键。例如,\(heights[22] = 101 将值 101 添加到\)heights 数组,并为其分配整数键 22。如果该键已经存在值,则该值会被覆盖。因此,这种直接赋值技术通常用于更新数组中已存在的值,而不是添加一个全新的值。Listing 7-4 扩展了我们的$animals 数组脚本,演示了如何进行这种操作。
<?php
$animals = [];
$animals[] = 'cat';
$animals[] = 'dog';
$animals[0] = 'hippo';
var_dump($animals);
Listing 7-4: 使用指定键直接赋值数组元素
如之前所示,我们将 'cat' 和 'dog' 添加到 $animals 数组中。然后,我们通过直接将该字符串赋值给数组的键 0 来替换第一个数组元素的值为 'hippo'。这是运行该脚本后的输出:
array(2) {
[0]=>
string(3) "hippo"
[1]=>
string(3) "dog"
}
请注意,'hippo' 现在映射到了键 0,这表示它已经替换了原来的 'cat' 值。
当你用特定的键添加新数组元素时要小心。如果为你提供的键不存在的数组元素进行此操作,可能会破坏整数键的顺序。如果你使用了超出数组当前大小的键,就会发生这种情况。创建一个具有断裂整数键序列的数组是允许的,但如果你在其他地方编写的代码依赖于键的连续序列,这可能会引发问题。我们将在 第八章 中探讨非顺序和非整数键的数组。
添加多个元素
到目前为止,我们只能一次向数组中添加一个元素,但内置的 array_push() 函数可以一次将多个元素添加到数组的末尾。该函数接受可变数量的参数。第一个参数是你要更新的数组,后面是要附加的新值,你可以添加任意多个。例如,示例 7-5 重新展示了 示例 7-3 中的脚本,在该脚本中我们首先向 $animals 数组添加了元素并打印出来,然后使用 array_push() 将另外两个动物添加到数组末尾。
<?php
$animals = [];
$animals[] = 'cat';
$animals[] = 'dog';
array_push($animals, 'giraffe', 'elephant');
print "animals[0] = $animals[0] \n";
print "animals[1] = $animals[1] \n";
print "animals[2] = $animals[2] \n";
print "animals[3] = $animals[3] \n";
示例 7-5:使用 array_push() 将多个值附加到数组末尾
我们调用 array_push() 函数,将 $animals 数组和我们想要添加的两个字符串值,'giraffe' 和 'elephant',传入。由于新元素被添加到数组的末尾,它们会自动分配下一个可用的整数键值,分别是 2 和 3。我们通过访问这两个额外的元素并根据它们的键值打印出来,确认了这一点,连同两个原始元素一起输出:
animals[0] = cat
animals[1] = dog
animals[2] = giraffe
animals[3] = elephant
输出表明,'giraffe' 成功映射到键 2,'elephant' 映射到了键 3。
你可能已经注意到,当我们调用 array_push() 函数时,并没有将其作为赋值语句的一部分,函数调用位于等号的右侧,变量名在左侧用于捕获函数的返回值。这是因为 array_push() 会直接修改传递给它的数组。从这个意义上讲,array_push() 与我们在 第三章 中看到的字符串操作函数非常不同,后者是创建并返回一个新的字符串,而不是直接修改传递给它们的原始字符串。
array_push() 函数可以直接修改提供的数组,因为它的第一个参数是通过按引用传递的方式声明的。正如我们在第五章中讨论的,这意味着该函数会获得传入参数值的直接引用,而不是通过按值传递方式获得该参数值的副本。我们可以通过查看 PHP 文档中的函数签名来确认这一点:
array_push(array &$array, mixed ...$values): int
第一个参数前的与符号(&)即 &$array,表示这是一个按引用传递的参数。
由于 array_push() 是直接修改数组的,因此无需返回数组的副本,或者我们在调用该函数时使用赋值语句来捕获返回值。实际上,array_push() 确实有返回值,它返回一个整数,表示数组的新长度。如果你需要在更新数组时跟踪数组的长度,这个返回值就很有用;在列表 7-5 中我们并不需要这个返回值,所以我们只是独立地调用了该函数,而没有将结果赋值给变量。
移除最后一个元素
内置的 array_pop() 函数返回数组中的最后一个元素,并同时从数组中移除该元素。这是另一个按引用传递的函数示例,它会改变提供的数组。在列表 7-6 中,我们使用 array_pop() 来获取并移除 $animals 数组的最后一个元素。
<?php
$animals = [];
$animals[] = 'cat';
$animals[] = 'dog';
$lastAnimal = array_pop($animals);
print "lastAnimal = $lastAnimal\n";
var_dump($animals);
列表 7-6:使用 array_pop() 函数获取并移除数组中的最后一个元素
我们调用 array_pop(),将 $animals 数组作为参数传递,并将函数的返回值存储在 $lastAnimal 变量中。然后,我们打印出 $lastAnimal 和 $animals 数组,查看哪些元素还剩下。结果如下:
lastAnimal = dog
array(1) {
[0]=>
string(3) "cat"
}
变量 \(lastAnimal 中的字符串为 'dog',因为这是最后一个被添加到数组中的元素。\)animals 的 var_dump 显示,调用 array_pop() 后数组只包含 'cat',这演示了这个通过引用传递的函数如何能够改变传入的数组。
检索数组信息
我们已经考虑了修改数组的一些函数,但其他函数可以在不改变数组的情况下返回关于数组的有用信息。例如,count() 返回数组中元素的数量。如果你想检查一个数组是否为空(零个元素可能意味着购物车为空,或者没有从数据库中检索到记录),或者它是否包含比预期更多的项(也许某个客户有多个地址存档),count() 就非常有用。有时,知道数组中有多少项对于控制遍历数组的循环也很有帮助。在列表 7-7 中,我们使用 count() 来打印 $animals 数组中的总项数。
<?php
$animals = ['cat', 'dog', 'giraffe', 'elephant'];
print count($animals);
列表 7-7:计算数组中元素的数量
我们调用 count()函数,传入我们希望计数的数组名称,并打印结果。由于$animals 数组包含四个元素,因此此脚本应输出整数 4。
注意
sizeof() 函数是 count() 的别名。如果你看到一个使用 sizeof() 的脚本,知道它的功能与 count() 相同。
另一个分析性数组函数是 array_is_list()。PHP 区分了列表数组和非列表数组。为了被认为是列表,一个长度为n的数组必须拥有从 0 到n – 1 的连续编号键。array_is_list()函数接收一个数组,并根据该数组是否符合该定义返回 true 或 false。本章讨论的所有数组都符合列表的标准,因为它们依赖于 PHP 的默认行为,即从 0 开始顺序分配键。然而,在下一章,我们将探索具有非整数键的数组,以及 unset()函数,它可以删除一个具有特定键的数组元素,这可能打破连续的数字键链,使数组不再符合列表的定义。因此,array_is_list()函数对于在将数组传递给期望列表结构的代码之前评估数组是否符合要求非常有用。
array_key_last()函数返回给定数组最后一个元素的键。假设数组是一个有效的列表,且键是连续编号的,则 array_key_last()的返回值应该比 count()的返回值少 1。例如,在列表 7-7 的末尾调用 array_key_last($animals)将返回整数 3,因为这是数组中第四个(也是最后一个)元素的键。
之前我提到过,尝试访问一个不存在的数组键会触发警告。为了避免这种情况,在尝试访问数组键之前,使用 isset()函数检查该键是否存在。列表 7-8 展示了该函数的实际应用。
<?php
$animals = ['cat', 'dog', 'giraffe', 'elephant'];
❶ if (isset($animals[3])) {
print "element 3 = $animals[3]\n";
} else {
print "sorry - there is no element 3 in this array\n";
}
print "(popping last element [3])\n";
❷ array_pop($animals);
if (isset($animals[3])) {
print "element 3 = $animals[3]\n";
} else {
print "sorry - there is no element 3 in this array\n";
}
列表 7-8:使用 isset()测试数组键的存在
首先,我们创建了一个包含四个元素的\(animals 数组。然后,我们使用带有 isset()的 if...else 语句,只有当键为 3 的元素存在时才访问它(此时它应该存在)❶。接着,我们使用 array_pop()删除\)animals 数组中的最后一个元素(键为 3 的元素)❷。然后,我们重复相同的 if...else 语句。现在数组中没有键为 3 的元素,但由于我们在尝试访问该元素之前使用 isset()进行测试,因此我们不应该收到警告。看看脚本的输出:
element 3 = elephant
(popping last element [3])
sorry - there is no element 3 in this array
输出的第一行表示第一次调用 isset()返回了 true,触发了条件语句的 if 分支。最后一行显示第二次调用 isset()返回了 false,触发了 else 分支,避免了尝试访问一个不存在的数组元素。
遍历数组
访问数组元素并逐个处理它们是常见的操作。假设数组是一个列表,你可以通过使用一个计数器变量作为当前数组元素的键来使用for循环实现这一点。从 0 开始计数器并将其递增直到数组的长度,你可以依次访问每个元素。清单 7-9 使用for循环打印我们$animals 数组中的每个元素。
<?php
$animals = ['cat', 'dog', 'giraffe', 'elephant'];
$numElements = count($animals);
for ($i = 0; $i < $numElements; $i++) {
❶ $animal = $animals[$i];
print "$animal, ";
}
清单 7-9:使用for循环遍历数组
我们使用count()来查找数组的长度,并将结果存储到\(numElements 变量中。然后我们声明一个`for`循环,将计数器\)i 从 0 递增到\(numElements 的值(但不包括\)numElements 本身)。(我们可以将停止条件硬编码为\(i < 4,但使用变量可以使代码在数组长度变化时更具灵活性。)在循环语句组内,我们使用\)animals[\(i]来获取当前循环变量\)i 对应的数组元素,并将其存储到\(animal ❶。然后我们打印这个\)animal 字符串,后面跟着一个逗号和一个空格。当我们在终端运行此脚本时,输出如下:
cat, dog, giraffe, elephant,
每个数组元素按顺序打印出来。(别担心,我们稍后会修正那个多余的逗号。)
使用foreach循环
这种for循环方法是可行的,但由于遍历数组元素是一个非常常见的任务,PHP 提供了另一种循环类型——foreach循环,来更高效地完成此任务。foreach循环的核心语法是foreach ($array as $value);其中\(array 是要循环的数组名,而\)value 是一个临时变量,每次被赋值为数组中的一个元素。清单 7-10 展示了清单 7-9 的更新版本,使用了foreach循环而不是for循环。
<?php
$animals = ['cat', 'dog', 'giraffe', 'elephant'];
foreach ($animals as $animal) {
print "$animal, ";
}
清单 7-10:使用foreach循环优雅地遍历数组
我们通过使用foreach ($animals as $animal)来声明这个循环。在这里,\(animal 是一个临时变量,依次获取每个数组元素的值,我们然后在循环体内打印它。注意,我们不再需要担心确定数组的长度来设置循环的停止条件,也不需要像在`for`循环版本中那样手动访问每个数组元素(如\)animals[$i])。foreach循环会自动访问每个元素。结果与for循环版本相同,但foreach循环的语法更加优雅简洁。
foreach循环还有一个额外的好处,我们无需关心提供的数组是否是真正的列表。在for循环版本中,我们依赖于数组键的连续整数编号;如果某个键缺失,尝试访问该键时会出现警告。相比之下,foreach循环会简单地访问数组中的每个元素,无论键是什么。
访问键和值
foreach 循环的另一种语法允许你访问每个数组元素的键和值,而不仅仅是值。为此,可以按以下格式声明循环:foreach ($array as $key => $value)。这里,$array 是你想要遍历的数组,$key 是一个临时变量,保存当前元素的键,$value 是一个临时变量,保存当前元素的值。=> 操作符将键与值连接起来。在 第八章 中,当我们处理包含字符串和其他数据类型键的复杂数组时,会更加广泛地使用这种语法。
访问键和值使我们能够消除数组中最后一个元素后的多余逗号。回顾一下,array_key_last() 函数返回数组最后一个元素的键。通过将这个函数的返回值与 foreach 循环中的当前键进行比较,我们可以决定是否在每个元素后面打印逗号。 清单 7-11 展示了这一方法。
<?php
$animals = ['cat', 'dog', 'giraffe', 'elephant'];
foreach ($animals as $key => $animal) {
print "$animal";
if ($key != array_key_last($animals)) {
print ", ";
}
}
清单 7-11:一个修改后的 foreach 循环,访问每个数组元素的键和值
我们通过使用 foreach ($animals as $key => $animal) 来声明一个 foreach 循环。在每次循环中,$key 是当前数组元素的键,而 $animal 是当前数组元素的值。在循环内,我们首先打印出 $animal 中的字符串。然后,我们使用 if 语句来判断,如果当前元素的键 不 等于数组的最后一个键(可以通过 array_key_last() 函数获得),则打印出一个逗号和一个空格。这样应该会产生如下输出:
cat, dog, giraffe, elephant
我们已经成功地去掉了数组中最后一个元素后的逗号。
将数组转换为字符串
清单 7-11 中的代码本质上是打印一个包含数组元素的字符串,元素之间有一个分隔符(在本例中是逗号和空格)。这是一个常见的任务,因此 PHP 提供了一个内建函数 implode() 来自动执行此操作,无需使用任何类型的循环。
该函数接受两个参数:一个作为分隔符的字符串和一个要转换为字符串的数组。分隔符 位于 元素之间,而不是 每个 元素后面,因此代码不会在数组的最后一个元素后添加额外的分隔符。清单 7-12 展示了一个更新后的脚本,使用 implode() 而不是 foreach 循环。
<?php
$animals = ['cat', 'dog', 'giraffe', 'elephant'];
print implode(', ', $animals);
清单 7-12:使用 implode() 将数组转换为字符串
在这里,我们打印调用 implode() 函数对 $animals 数组进行处理后的结果。我们使用字符串 ', ' 作为分隔符,在数组元素之间添加逗号和空格。输出应该与 清单 7-11 完全相同,但 implode() 使得代码写起来更高效。
implode()函数可能已经让我们在这种情况下不再需要 foreach 循环,但不要被这个误导。foreach 循环在很多场景下都是正确的工具。一般来说,当你要应用到数组中每个元素的代码比简单地打印出该元素的值更复杂时,使用 foreach 循环通常是合适的。
带有可变数量参数的函数
数组的一个重要应用是,你可以用它们来声明接受可变数量参数的函数。当我们在第五章中创建自定义函数时,我们需要准确知道每个函数会接受多少个参数,这样我们才能定义具有相应参数数量的函数。然而,这并不总是可能的。
例如,假设我们想声明一个 sum()函数,它接受一个不确定数量的数字,将它们全部加起来并返回结果。我们不知道用户会传递两个数字、三个数字或更多作为参数,因此无法为每个数字创建一个单独的参数。相反,我们使用一个单一的参数来表示所有数字,并在参数名前写三个点(...)。这种语法告诉 PHP 将该参数视为数组,并将其填充为所有传入的参数,无论有多少个。
列表 7-13 展示了如何通过声明刚才描述的 sum()函数来实现这一方法。记住,函数应该在与调用它们的代码分开的文件中声明,所以创建一个名为my_functions.php的文件,包含此列表的内容。
<?php
function sum(...$numbers): int
{
$total = 0;
foreach ($numbers as $number) {
$total += $number;
}
return $total;
}
列表 7-13:一个接受可变数量整数参数并返回它们总和的函数
我们声明了 sum()函数,它返回一个整数。它有一个单一的参数,...\(numbers。由于省略号,函数接收到的任何参数都会作为元素分配给一个名为\)numbers 的本地数组。注意,使用省略号表示法时,我们没有为参数指定数据类型;我们知道整体的\(numbers 变量将是数组类型,尽管数组的单个元素可以是任何类型。在函数体内,我们将\)total 初始化为 0。然后我们使用 foreach 循环遍历\(numbers 数组的元素,将每个元素的值加到\)total 中。当循环结束时,我们返回$total,它保存了所有参数的总和。
注意
我们的 sum() 函数没有包含任何逻辑来确认 $numbers 中的元素实际上是数字。一个真实的函数需要某种形式的数据验证,如果传入的参数不是所有数字,它可能会返回 NULL 或以其他方式指示数据无效。还需要注意的是,PHP 已经有一个内置的 array_sum() 函数,用来计算数组中数字的总和。我们实现了自己的版本用于演示。
列表 7-14 展示了一个main.php脚本,用于读取 sum()函数的声明并通过传递不同数量的参数进行测试。
<?php
require_once 'my_functions.php';
print sum(1, 2, 3) . "\n";
print sum(20, 40) . "\n";
print sum(1, 2, 3, 4, 5, 6, 7) . "\n";
列表 7-14:一个调用 sum()并传入不同参数个数的主脚本
在使用 require_once 读取并执行my_functions.php之后,我们调用了三次 sum(),每次传入不同数量的参数,并打印结果。脚本输出如下:
6
60
28
打印的三个总和已正确计算。这表明我们的 sum()函数已经成功地将可变数量的参数收集到一个数组中。
数组复制与数组引用
假设你有一个包含数组的变量,并将其赋值给第二个变量。在一些语言中,例如 Python 和 JavaScript,第二个变量将被赋值为指向原始数组的引用。两个变量将指向计算机内存中的同一个数组,因此一个变量的变化也会影响另一个变量。而在 PHP 中,默认情况下是将第二个变量赋值为数组的副本。由于两个变量各自拥有独立的数组,因此一个变量的变化不会影响另一个变量。列表 7-15 回到我们的$animals 数组来说明这一点。
<?php
$animals = ['cat', 'dog', 'giraffe', 'elephant'];
$variable2 = $animals;
array_pop($variable2);
var_dump($animals);
列表 7-15:复制数组
我们声明了常规的\(animals 数组,然后将\)animals 赋值给\(variable2。这会在\)variable2 中创建一个数组的独立副本,因此我们对一个数组的操作不应影响另一个数组。为了证明这一点,我们使用 array_pop()删除\(variable2 数组中的最后一个元素,然后打印原始的\)animals 数组。结果如下:
array(4) {
[0]=>
string(3) "cat"
[1]=>
string(3) "dog"
[2]=>
string(7) "giraffe"
[3]=>
string(8) "elephant"
}
即使我们从\(variable2 数组中删除了最后一个元素('elephant'),\)animals 数组中的四个动物字符串仍然存在,因此这两个变量确实持有独立的数组。
如果你希望将第二个变量赋值为对原始数组的引用,而不是数组的副本,就像其他语言中常见的做法那样,可以在赋值时使用引用操作符(&)。列表 7-16 更新了列表 7-15 中的代码,展示了两者的区别。
<?php
$animals = ['cat', 'dog', 'giraffe', 'elephant'];
$variable2 = &$animals;
array_pop($variable2);
var_dump($animals);
列表 7-16:在赋值数组时使用引用操作符
这次我们在将\(animals 数组赋值给\)variable2 时,前缀加上了引用操作符(&)。这意味着一个变量的变化将影响另一个变量,因为两个变量都引用了内存中的同一个数组。更新后的脚本输出结果如下:
array(3) {
[0]=>
string(3) "cat"
[1]=>
string(3) "dog"
[2]=>
string(7) "giraffe"
}
输出结果显示,从\(variable2 数组中弹出元素 3 也同时删除了\)animals 数组中的元素 3。这确认了\(variable2 和\)animals 都引用了内存中的同一个数组。
这些数组赋值的方法没有哪种天生比另一种更好,它们只是不同。有时最好在操作数组之前先复制一份。例如,一个网页可能让用户编辑购物清单,同时提供一个取消按钮来撤销编辑。在这种情况下,你可能希望在确认更改之前处理购物清单数组的副本,因为如果用户点击取消,你可能需要恢复到原始数组。其他时候,最好让多个变量引用内存中同一个数组。也许代码包含选择多个数组中的一个的逻辑,因此需要将某个变量设置为所选数组的引用。
本节的关键点是,除非使用引用操作符(&),否则 PHP 默认会复制数组。如果你在学习 PHP 之前已经学习过其他编程语言,或者将来学习其他语言,那么理解赋值副本与赋值引用之间的区别,并了解语言默认使用的行为是很重要的。
将字符串视为字符数组
从某种程度上讲,你可以把字符串看作是一个个独立字符的数组。这在某些情况下很有用,因为你可能希望逐字符“遍历”字符串,执行加密、拼写检查等任务,就像遍历数组的元素一样。
正如你在第三章中看到的,字符串中的字符是从 0 开始编号的,就像简单数组中的元素一样。事实上,你可以使用相同的方括号表示法来访问数组元素,同时也可以访问字符串中的特定字符。例如,如果\(name 保存的是字符串'Smith',那么\)name[0]将返回'S',\(name[1]将返回'm',依此类推。字符串还支持*负*整数键,用于从字符串的末尾反向计数字符:\)name[-1]返回'h'(最后一个字符),$name[-2]返回't',依此类推。
注意
与字符串不同,数组本身并不会将负整数键解释为从数组末尾反向计数。相反, $animals[-1] 将被解释为 $animals 数组中一个实际键为 -1 的元素。虽然你可以手动将负整数作为键分配给数组元素,但我个人不记得曾经需要这么做。
列表 7-17 展示了使用数组键语法访问字符串中单个字符的示例。
<?php
$name = 'Smith';
$firstChar = $name[0];
$secondToLastChar = $name[-2];
print "first character = '$firstChar'\n";
print "second to last character = '$secondToLastChar'\n";
列表 7-17:使用方括号表示法访问字符串字符
我们将字符串 'Smith' 分配给\(name 变量。接下来,我们将字符串的第一个字符(\)name[0])复制到\(firstChar 变量,将倒数第二个字符(\)name[-2])复制到$secondToLastChar 变量。然后,我们打印出这些变量的值,输出如下信息:
first character = 'S'
second to last character = 't'
与数组不同,我们不能将字符串传递给 foreach 循环以循环遍历其所有字符。然而,我们可以使用 PHP 内置的 str_split() 函数将字符串转换为实际的字符数组,然后将该数组传递给 foreach 循环,如 示例 7-18 所示。
<?php
$name = 'Smith';
$characters = str_split($name);
foreach ($characters as $key => $character) {
print "character with key $key = '$character'\n";
}
示例 7-18:使用 str_split() 和 foreach 循环遍历字符串中的字符
我们将相同的 $name 字符串传递给 str_split()。默认情况下,该函数将字符串拆分为单个字符,将它们作为数组的元素,并返回结果,我们将其存储在 $characters 变量中。然后,我们使用 foreach 循环访问字符串的数组版本中的每个键和值并打印它们。结果如下:
character with key 0 = 'S'
character with key 1 = 'm'
character with key 2 = 'i'
character with key 3 = 't'
character with key 4 = 'h'
输出显示,在使用 str_split() 将字符串转换为数组后,我们已成功循环遍历原始字符串中的字符。
str_split() 函数有一个可选的第二个参数,用于控制结果数组中每个字符串元素的字符数。该参数的默认值为 1,将字符串拆分为单个字符,但如果我们调用 str_split($name, 2),例如,那么结果数组将包含两个字符的字符串:['Sm', 'it', 'h']。
其他数组函数
本章已讨论了处理数组的一些内置函数,但 PHP 还有许多其他函数。适用于数组的其他有用函数包括:
sort() 将数组的值按升序排列(对于字符串值按字母顺序,对于数字值按数字顺序)
usort() 根据用户定义的函数将值排序为自定义顺序
array_flip() 交换数组元素的键和值
array_slice() 返回一个包含现有数组子序列的新数组
array_walk() 对数组的每个元素调用用户定义的函数
array_map() 对数组的每个元素调用用户定义的函数,并返回一个包含结果的新数组
array_rand() 返回数组中的随机键
要查看完整的数组函数列表,请参阅 PHP 文档中的 www.php.net/manual/en/ref.array.php。
总结
在这一章中,我们开始探索数组,这是一种复合数据类型,它在单一的变量名下存储多个值,每个值都有自己的标识键。目前,我们主要关注简单数组,它们的键是整数。本章介绍了多种技术,用于添加、删除和访问数组元素,以及获取数组信息的函数,如 count() 和 isset()。你还学会了如何通过使用 foreach 循环依次处理每个数组元素。在某些情况下,PHP 提供了内置函数来处理常见的数组任务,例如 implode() 函数,它将数组中的所有元素连接成一个字符串。这些函数有时可以让你用一个函数调用替代整个循环和条件语句。
练习
1. 创建一个 $colors 数组,其中包含五种颜色的字符串名称。打印数组中的随机颜色。
提示:你可以通过调用 array_rand($colors) 获取一个有效的随机键。
2. 写一个 foreach 循环,在新的一行中打印出练习 1 中数组的每个颜色,格式如下:
color 0 = blue
color 1 = red
...
3. 使用 array_pop() 打印练习 1 中颜色数组的最后一个元素。然后使用 var_dump() 显示该元素已经从数组中移除。
4. 创建一个包含整数年龄 23、31 和 55 的数组。使用内置函数计算并打印数组中项的数量及其平均值。
第八章:8 高级数组

在本章中,我们将采用更复杂的方法来处理 PHP 数组,并探讨如何手动分配数组的键。这为使用有意义的字符串作为键提供了可能,而不是 PHP 默认的使用顺序整数的行为。我们还将讨论多维数组,其中数组元素的值本身就是另一个数组,并且我们将了解更多用于操作数组的函数和操作符。通过对 PHP 数组的更深入了解,您将开始看到它们如何存储和处理更复杂的数据结构。
显式声明数组键
我们已经讨论过 PHP 如何自动为数组元素分配顺序整数键,从 0 开始,在这种情况下,生成的数组将符合列表的定义。然而,您也可以在声明数组时使用双箭头操作符(=>)显式地将键映射到每个值,而不是依赖于这种默认行为。这样,您就不必遵循默认的键模式。例如,您可以使用非顺序的整数作为键,或从 0 以外的数字开始计数。无论哪种方式,生成的数组将不再被视为列表,但它仍然是一个有效的数组。为了说明,清单 8-1 显示了一个显式使用非顺序整数键的数组脚本。
<?php
$rainfallValues = [
0 => 10,
4 => 8,
3 => 12
];
print "-- Monthly rainfall --\n";
foreach ($rainfallValues as $key => $rainfallValue) {
print "$key: $rainfallValue\n";
}
var_dump(array_is_list($rainfallValues));
清单 8-1:显式声明不按顺序排列的整数数组键
在这里,我们声明了一个 $rainfallValues 数组。在数组的方括号内,我们使用 => 操作符显式地为每个数组元素分配键。例如,0 => 10 会向数组中添加一个值为 10、键为 0 的元素。键/值对通过逗号分隔,就像我们在第七章中没有显式声明键时用逗号分隔数组值一样。在这种情况下,我们还将每个键/值对放在了单独的缩进行中,以提高清晰度。声明数组后,脚本继续通过循环遍历它并打印出键/值对。
请注意,我们声明的数组键不是顺序的。第二个数组元素的键是 4,第三个元素的键是 3。这可能不是分配键的最直观方式,但如果这是我们想要的,PHP 完全可以接受。数组将不再符合列表的定义(因此脚本末尾的 array_is_list() 调用应该返回 false),但数组仍然有效。以下是运行脚本的输出:
-- Monthly rainfall --
0: 10
4: 8
3: 12
bool(false)
即使数组不是一个完整的列表,foreach 循环仍然有效,它会遍历数组的键/值对并将其打印出来。请注意,键为 4 的元素在键为 3 的元素之前打印出来。关键在于元素是如何声明的,而不是键本身的数字顺序。输出末尾的 false 确认了该数组不再符合列表的要求。
一旦你开始显式声明键,就不一定需要为每一个数组元素都声明一个键。如果一个元素没有声明键,PHP 将自动查找最近的整数键,递增它,并使用该键作为新的键。如果你希望数组的键按顺序排列,但不从 0 开始,这种方式会非常有用。
例如,假设你有一个学生班级,并希望创建一个数组,将学生的 ID 映射到他们的成绩。每个 ID 是一个七位数的数字,开头是年份,后面跟着三个数字,按顺序递增。例如,在 2025 年,第一位学生的 ID 为 2025001,下一位为 2025002,依此类推。在这种情况下,你可以显式声明第一个数组键,并让 PHP 自动分配其余的键。清单 8-2 展示了具体做法。
<?php
$studentGrades = [
2025001 => 'A',
❶ 'B',
'A',
'D',
'F'
];
print "-- Student grades--\n";
foreach ($studentGrades as $studentId => $grade) {
print "$studentId => $grade\n";
}
var_dump(array_is_list($studentGrades));
清单 8-2:显式声明第一个数组键,其余自动分配
在$studentGrades 数组中,我们显式地为第一个元素指定了键 2025001。然后,从第二个元素❶开始,我们只提供值。默认情况下,PHP 会将这些值映射到整数键 2025002、2025003 等。和之前一样,我们通过循环并打印键/值对来完成脚本,并测试该数组是否算作列表。输出如下所示:
-- Student grades--
2025001 => A
2025002 => B
2025003 => A
2025004 => D
2025005 => F
bool(false)
请注意,PHP 已按顺序分配了其余的键,从显式声明的键 2025001 开始递增。然而,尽管这些键是按顺序排列的,它们并不是从 0 开始的。因此,该数组并不是一个列表,输出末尾的 false 证明了这一点。
使用字符串作为键的数组
让我们进一步扩展我们的代码:既然我们已经显式分配了数组键,谁说它们一定得是整数呢?它们同样可以是字符串,在这种情况下,数组中的每个值都可以有一个有意义的名字作为键。例如,回到清单 8-1 中的$rainfallValues 数组,我们可以使用月份名称作为键,而不是使用整数。这种修改将更好地表明数组中的每个值代表一个月的降水总量。清单 8-3 已经相应地修正了脚本。
<?php
$rainfallValues = [
'jan' => 10,
'feb' => 8,
'march' => 12
];
print "-- Monthly rainfall --\n";
foreach ($rainfallValues as $key => $rainfallValue) {
print "$key: $rainfallValue\n";
}
var_dump(array_is_list($rainfallValues));
清单 8-3:将字符串用作数组键
这次我们将键'jan'分配给值 10,将键'feb'分配给值 8,将键'march'分配给值 12。我们使用和之前相同的=>运算符来将键与值配对。唯一的不同是,现在键是字符串。以下是脚本的输出:
-- Monthly rainfall --
jan: 10
feb: 8
march: 12
bool(false)
字符串键清晰地说明了值的实际含义。输出末尾的 false 显示该数组不是一个列表。这并不奇怪,因为这些键甚至不是整数。
使用字符串键从数组中访问单个值的方式和使用整数键访问数组值一样:在数组名后面提供键,并用方括号括起来。例如,以下是如何打印三月降水量的值:
print $rainfallValues['march'];
同样,你也可以使用方括号表示法,通过字符串键来添加或更新数组元素。在这里,我们为四月添加了新的降水总量:
$rainfallValues['april'] = 14;
这是一个简单的例子,但希望你可以开始看到 PHP 数组在构建有意义的数据集合方面的潜力。当你不需要面向对象编程(在第五部分中讨论)所提供的全部功能时,使用带有字符串键的数组可以帮助你处理那些值与任务相关联的键(如日期或月份、人名或 ID、或产品名称或代码)数据。
多维数组
到目前为止,我们所探索的数组都是一维的:它们包含一个元素序列,每个元素都是一个标量(单一)值,映射到一个键。然而,你也可以声明包含数组元素的数组,从而得到一个多维数组。例如,假设你想创建一个任务数组,每个任务需要的时间(以分钟为单位)。数组中的每个元素本身可以是一个数组,包含任务名称和其相关的持续时间,如下所示:
$tasksAndMinutes = [
['shopping', 30],
['gym', 60],
['nap', 15]
];
这里的\(tasksAndMinutes 是一个多维数组。它的第一个元素,['shopping', 30],是一个包含字符串任务名称和整数分钟数的两元素数组。其他数组元素遵循相同的格式。对于这样一个多维数组,我们把整个\)tasksAndMinutes 称为外部数组,它的元素称为内部数组。
处理多维数组的一种方式是使用一组嵌套的 foreach 循环,一个循环遍历外部数组的元素,另一个循环遍历每个内部数组的元素。然而,在$tasksAndMinutes 数组中,所有的内部数组具有相同的结构(这并不总是如此)。因此,在这种情况下,你可以利用对该结构的了解,通过使用单一的 foreach 循环来遍历外部数组,从每个内部数组中提取值。清单 8-4 展示了这一方法。
<?php
$tasksAndMinutes = [
['shopping', 30],
['gym', 60],
['nap', 15]
];
foreach ($tasksAndMinutes as $item) {
$task = $item[0];
$minutes = $item[1];
print "allow $minutes minutes today for task: $task\n";
}
清单 8-4:处理多维数组
我们像之前一样声明\(tasksAndMinutes 数组。接下来,我们声明一个 foreach 循环,遍历\)tasksAndMinutes 的元素,使用\(item 变量来表示当前元素。如我们所见,每个元素本身是一个包含任务名称和时间(分钟)的数组。因此,我们可以提取\)item 的第一个元素(使用整数索引 0)到\(task 变量,第二个元素(索引 1)到\)minutes 变量。然后,我们使用这两个变量打印关于当前任务的信息,输出如下:
allow 30 minutes today for task: shopping
allow 60 minutes today for task: gym
allow 15 minutes today for task: nap
在 foreach 循环中,时间和任务名称已经成功地从每个内层数组中提取出来。
在这个例子中,内部数组默认使用整数键,但如你所知,数组也可以使用非数字键。将每个内部数组中的值与有意义的字符串键(如 'task' 和 'minutes')配对,会让代码更加易读。例如,我们可以通过 $task = $item['task'] 来访问当前元素的任务,而不是 $task = $item[0]。列表 8-5 展示了这一改进。
<?php
$tasksAndMinutes = [
['task' => 'shopping', 'minutes' => 30],
['task' => 'gym', 'minutes' => 60],
['task' => 'nap', 'minutes' => 15],
];
foreach ($tasksAndMinutes as $item) {
$task = $item['task'];
$minutes = $item['minutes'];
print "allow $minutes minutes today for task: $task\n";
}
列表 8-5:重构列表 8-4,在内部数组中使用字符串键
这次我们显式地将字符串键 'task' 和 'minutes' 分配给 $tasksAndMinutes 中每个数组的值。然后,我们在 foreach 循环中使用这些有意义的键来提取当前正在处理的内部数组中的值。结果和之前完全一样,但代码更容易阅读。在 PHP 支持面向对象编程之前,像这样的具有良好标签的多维数组是许多程序数据相关功能代码的核心部分。 ### 更多数组操作
在第七章中,我们讨论了数组操作,如向简单数组的末尾添加元素和从末尾删除元素。现在我们已经探讨了复杂数组,让我们考虑更多的数组操作。我们将学习如何从数组的任意位置删除元素,如何使用数组运算符如并集(+)和展开(...),以及如何将数组元素提取到单独的变量中。
从数组中删除任何元素
你可以通过将元素的键传递给 unset() 函数来从数组中删除元素。与上一章中介绍的 array_pop() 函数不同,array_pop() 函数专门删除数组中的最后一个元素,而 unset() 可以从任何位置删除元素。与 array_pop() 不同,unset() 函数不会返回被删除的元素,它只是消失了。
当你开始使用字符串而非整数作为数组键时,使用 unset() 变得更加合适。对于字符串键,数组元素的顺序往往不再重要,因此基于键而非数组中的位置删除元素更有意义。列表 8-6 重新访问了 $rainfallValues 数组作为例子。
<?php
$rainfallValues = [
'jan' => 10,
'feb' => 8,
'march' => 12
];
unset($rainfallValues['feb']);
print "-- Monthly rainfall --\n";
foreach ($rainfallValues as $key => $rainfallValue) {
print "$key: $rainfallValue\n";
}
列表 8-6:使用 unset() 从数组中删除元素
我们使用 unset() 从 $rainfallValues 数组中删除键为 'feb' 的元素。然后,我们像之前一样遍历数组,打印剩余元素的详细信息。结果如下:
-- Monthly rainfall --
jan: 10
march: 12
注意,对于键为 'feb' 的元素没有打印任何数据,因为该元素不再存在于数组中。
注意
调用 unset() 在整个数组上,例如 unset($rainfallValues),将删除整个数组,就像在任何其他变量上调用 unset() 将清空该变量一样。 #### 合并和比较数组
你可以使用一些相同的加法、相等性和身份操作符来组合或比较数组,这些操作符适用于标量(单值)变量。表 8-1 总结了六个可用的数组操作符。
表 8-1:数组操作符
| 名称 | 符号 | 示例 | 描述 |
|---|---|---|---|
| 联合 | + | $a + $b | 返回一个包含数组 $a 和 $b 元素的数组。 |
| 展开 | ... | [1, ...$a] | 返回一个数组,数组的第一个元素是 1,接着是数组 $a 的元素。 | |
| 相等 | == | $a == $b | 如果数组 $a 和 $b 拥有相同的键/值对,则返回 true。 |
| 相同 | === | $a === $b | 如果数组 $a 和 $b 完全相同:它们具有相同的键/值对,元素的顺序和类型也相同,则返回 true。 |
| 不相等 | != 或 <> | $a != $b $a <> $b | 如果数组 $a 和 $b 没有相同的键/值对,则返回 true。 |
| 不相同 | !== | $a !== $b | 如果数组 $a 和数组 $b 不相同,则返回 true。 |
列表 8-7 展示了这些操作符的实际应用。
<?php
$cars1 = ['audi' => 'silver', 'bmw' => 'black'];
$cars2 = ['audi' => 'white', 'ferrari' => 'red'];
$names1 = ['matt' => 'smith', 'joelle' => 'murphy'];
$names2 = ['joelle' => 'murphy', 'matt' => 'smith',];
print_r($cars1 + $cars2);
var_dump($names1 == $names2);
var_dump($names1 === $names2);
❶ print_r(['rolls royce' => 'yellow', ...$cars1, ...$names1]);
列表 8-7:使用数组操作符
首先,我们声明一些示例数组来进行操作:$cars1 和 $cars2 以汽车品牌为键,汽车颜色为值,而 $names1 和 \(names2 以名字为键,姓氏为值。(注意,\)names1 和 $names2 拥有相同的元素,但顺序相反。)
然后我们将操作符应用到这些数组,并打印结果。我们使用联合 (+) 操作符来合并 $cars1 和 $cars2,使用相等 () 和相同 (=) 操作符来测试 $names1 和 $names2。我们还使用数组展开操作符 (...) 来创建一个新数组,其键为 'rolls royce',值为 'yellow',并包含 $cars1 和 $names1 数组的所有元素 ❶。注意,我们使用 print_r() 来显示返回数组的操作结果;该函数比 var_dump() 更简洁地展示数组。运行脚本后,得到如下输出:
❶ Array
(
[audi] => silver
[bmw] => black
[ferrari] => red
)
❷ bool(true)
bool(false)
❸ Array
(
[rolls royce] => yellow
[audi] => silver
[bmw] => black
[matt] => smith
[joelle] => murphy
)
输出的第一部分展示了 $cars1 + $cars2 ❶ 的结果。两个汽车数组都包含一个键为 'audi' 的元素,但数组不能有两个相同的键。因此,联合操作符从 $cars1 中取出 'audi' => 'silver' 元素,但忽略了 $cars2 中的 'audi' => 'white' 元素,结果是一个包含三个元素的数组。接下来,输出中的 true 和 false 结果 ❷ 表明 $names1 和 $names2 数组是 相等 的,因为它们有相同的键和值,但不是 完全相同 的,因为元素的顺序不同。最后的数组显示了使用展开操作符 (...) ❸ 的结果。新数组包含一个 'rolls royce' 元素,接着是 $cars1 和 $names1 中的元素。
值得强调的是,展开运算符 (...) 在这里的作用:它从一个数组中提取元素,并将它们一个一个地插入到另一个数组中。如果没有展开运算符,整个数组会作为单个元素插入到新数组中,从而创建一个多维数组,而不是将它的各个元素展开到新数组中。举个例子,假设我们在 示例 8-7 中,省略了 $cars1 前面的展开运算符 ❶,如下所示:
print_r(['rolls royce' => 'yellow', $cars1, ...$names1]);
结果数组将包含一个元素,该元素包含整个 $cars1 数组,如下所示:
Array
(
[rolls royce] => yellow
[0] => Array
(
[audi] => silver
[bmw] => black
)
[matt] => smith
[joelle] => murphy
)
现在数组中的第二个元素,键值为 0,本身是一个数组,包含了完整的 $cars1 内容。这个例子也展示了数组如何将整数键与非整数键混合使用。当整个 $cars1 数组作为元素添加到新数组时,由于没有手动指定键值,它会自动分配第一个可用的整数键 0。同时,新数组中的其他元素都显式地分配了字符串键。像这样混合键的数组是比较少见的;通常这样的数组表示某些东西出了问题,例如这里缺少了展开运算符。
将数组解构为多个变量
有时提取数组中的值并将它们赋值给单独的变量会很有用,这个过程叫做 解构。如果你知道数组中的元素数量,你可以通过一条语句解构它,如 示例 8-8 所示。
<?php
$rainfallValues = [10, 8, 12];
❶ [$jan, $feb, $march] = $rainfallValues;
print "-- Monthly rainfall --\n";
print "Jan: $jan \n";
print "Feb: $feb \n";
print "Mar: $march \n";
示例 8-8:将一个包含三个元素的数组解构为三个单独的变量
我们声明 $rainfallValues 数组包含三个元素。然后,我们将该数组解构到 \(jan、\)feb 和 $march 变量 ❶ 中。为此,我们在赋值运算符 (=) 的左边列出目标变量,并在右边提供包含整个数组的变量。最后,我们打印出这三个变量中的值,产生如下输出:
-- Monthly rainfall --
Jan: 10
Feb: 8
Mar: 12
注意,数组中的值已经成功地赋值到单独的 \(jan、\)feb 和 $march 变量中,并从这些变量中打印出来。 ### 回调函数与数组
回调函数,简称 回调,是一种不会直接调用的函数,而是作为参数传递给另一个函数。然后,另一个函数会为你调用回调函数。PHP 中有多个函数使用回调与数组结合。
例如,array_walk() 接收一个数组和一个回调函数作为参数,并对数组中的每个元素应用回调函数,从而在这个过程中改变原始数组。类似地,array_map() 接收一个数组和一个回调函数,应用回调函数到每个数组元素,并返回一个包含结果的新数组。array_walk() 和 array_map() 都被称为 高阶函数,因为它们将一个函数作为参数。
如果你在一个单独的文件中声明了一个函数(如第五章所讨论的),或者使用了 PHP 的内置函数,你可以通过将包含函数名的字符串传递给高阶函数来使用该函数作为回调。例如,假设我们声明了一个名为 my_function()的函数,并且想要通过使用 array_map()将它应用到$my_array 中的每个元素。下面是具体做法:
$my_new_array = array_map('my_function', $my_array);
我们将字符串'my_function'(所需回调的名称)和数组作为参数传递给 array_map(),它会对数组中的每个元素调用 my_function()。结果会以新数组的形式返回,我们将其存储在$my_new_array 变量中。
除了单独声明回调函数,另一种常见的做法是在高阶函数的参数列表中直接定义匿名(无名)回调函数。在我们在像 array_map()这样的高阶函数中使用匿名函数之前,先让我们单独考虑一下匿名函数,以便更好地理解语法。以下是一个简单的匿名函数,它接收一个数字并返回其值的两倍:
function (int $n): int {return $n * 2;}
该函数以 function 关键字开始,后面是函数的签名(int $n): int,表示该函数接收一个整数参数\(n 并返回一个整数值。请注意,函数签名中没有包括名称,因为该函数是匿名的。签名后面是匿名函数的主体,主体被花括号包围。该主体返回提供的\)n 参数的两倍值。
另一个选择是将匿名回调函数写成箭头函数,使用更简洁的语法,采用双箭头操作符(=>)来分隔函数的签名和主体。这种语法省去了return关键字、主体周围的花括号以及结束语句的分号。以下是我们倍增操作的箭头函数版本:
fn (int $n): int => $n * 2
现在,我们不再使用 function,而是以fn开始,这是声明箭头函数的保留关键字。接着是函数的签名,和之前一样。然后是双箭头操作符(=>),后面跟着一个定义函数返回值的表达式(在此例中为$n * 2)。没有了花括号、分号和 return 关键字,箭头函数变得非常简洁。
现在让我们尝试将这个箭头函数用作回调。列表 8-9 展示了如何将箭头函数传递给 array_map(),以便将数组中的每个值都加倍。
<?php
$numbers = [10, 20, 30];
$doubleNumbers = array_map(
❶ fn (int $n): int => $n * 2,
$numbers
);
var_dump($doubleNumbers);
列表 8-9:将箭头回调函数传递给 array_map()
我们声明了一个包含 10、20 和 30 的\(numbers 数组。然后,我们调用 array_map()函数。对于第一个参数,我们使用箭头函数语法声明了我们刚刚讨论过的双倍回调函数❶。注意,箭头函数以逗号结束,因为它是 array_map()函数参数列表的一部分。第二个参数是\)numbers 数组。array_map()函数会自动对数组中的每个元素应用箭头函数,并返回一个包含结果的新数组。我们将这个新数组存储在$doubleNumbers 变量中。下面是运行此脚本并打印结果数组的输出:
array(3) {
[0]=>
int(20)
[1]=>
int(40)
[2]=>
int(60)
}
\(doubleNumbers 数组包含值 20、40 和 60。这表示 array_map()函数成功访问了\)numbers 数组中的每个值,并对其应用了双倍箭头函数。
总结
数组是灵活的数据结构,尤其当我们开始为元素的值分配有意义的字符串键,而不是使用默认的整数键时。在这一章中,你学习了如何使用字符串键的数组。你还看到如何将数组嵌套在其他数组中,创建多维数组,并且如何对数组的每个元素应用回调函数(使用箭头函数语法编写)。像这样的技巧使得数组成为一个复杂数据表示和操作的高级结构。
练习
1. 使用字符串键的数组来存储以下姓名和身高(单位:米)对:
Fred 1.82
Joelle 1.55
Robin 1.70
编写一个 foreach 循环来遍历数组元素并打印它们。
2. 创建一个多维数组来表示以下关于电影的数据:
Back to the Future
duration 116
leadingActor Michael J. Fox
The Fifth Element
duration 126
leadingActor Bruce Willis
Alien
duration 117
leadingActor Sigourney Weaver
3. 声明一个包含从 1 到 9 的奇数(1、3、5、7、9)的数组,另一个包含偶数(2、4、6、8)的数组。使用数组扩展操作符(...)将两个数组合并,然后使用 PHP 的内置 sort()函数将它们按数字顺序排序。
第九章:9 文件和目录

许多应用程序需要你从文件中读取或写入数据。在本章中,我们将探索如何通过 PHP 脚本与文件进行交互。我们将主要关注简单的 .txt 文件,尽管我们也会简要介绍 PHP 如何处理其他常见的文本文件格式。
PHP 提供了许多内置函数来处理文件。有些函数一次性读取或写入文件,而其他更底层的函数则提供更细粒度的控制,允许你打开和关闭文件,并在特定位置选择性地读取或写入内容。并不是所有的 Web 应用程序都需要你处理外部文件,但掌握这些函数仍然很有用,以防有需要。举例来说,在 Web 应用程序之外,你可能会需要重新格式化文件中的数据,或者移动和重命名文件和目录。通过我们将在这里讨论的函数,你可以编写一个 PHP 脚本来自动化这个过程。
将文件读取到字符串中
如果你知道文件存在并且希望将其所有内容作为单个字符串读取到脚本中,你可以通过一个语句实现,只需调用内置的 file_get_contents() 函数。为了说明这一点,让我们首先创建一个待读取的文件。列表 9-1 显示了一个由 Jorge Suarez 编写的编程俳句文件(可以在 selavo.lv/wiki/index.php/Programming_haiku 找到)。创建一个名为 data.txt 的新文件,包含以下内容。
what is with this code?
oh my, looks like I wrote it
what was I thinking?
列表 9-1:包含编程诗歌的文本文件 data.txt
这个文件包含三行文本。换行符表示前两行以一个不可见的换行符结尾。
现在我们有了一个文件可以操作,我们可以编写一个脚本来读取并打印它的内容。在与 data.txt 相同目录下创建一个 main.php 文件,并输入 列表 9-2 中的代码。
<?php
$file = __DIR__ . '/data.txt';
$text = file_get_contents($file);
print $text;
列表 9-2:一个 main.php 脚本,用于读取并打印文件内容
首先,我们声明一个 $file 变量,包含文本文件的路径和文件名。由于文本文件和主脚本在同一目录下,我们通过将 DIR 魔术常量(主脚本所在位置的路径)与正斜杠和 data.txt 文件名连接起来,构建这个文件位置字符串。然后,我们使用 file_get_contents() 函数将文件内容读取到 $text 变量中。最后,我们打印出包含文件内容的字符串。
在终端运行主脚本,你应该看到诗句分布在三行上,正如在示例 9-1 中所显示的那样。这是因为文件中的不可见新行字符被传递到了$text 字符串中,就像可见字符一样。我们可以通过几种方式证明这些不可见字符的存在:检查文本文件的大小,或者通过将字符串中的新行替换为可见字符来验证从文件中读取的内容。为了更清楚地看到新行字符是如何成为文本文件的一部分的,让我们将data.txt的内容替换为示例 9-3 中的内容。
a
b
示例 9-3:一个简化版的 data.txt 文件
现在,文件只包含两个字符,每个字符位于单独的行上,我们可以更轻松地检查文件内容。更新main.php以匹配示例 9-4。
<?php
$file = __DIR__ . '/data.txt';
$text = file_get_contents($file);
$numBytes = filesize($file);
$newlinesChanged = str_replace("\n", 'N', $text);
print "numBytes = $numBytes\n";
print $newlinesChanged;
示例 9-4:一个更新后的 main.php 脚本,用于证明新行字符的存在
和之前一样,我们首先将文件内容读取到\(text 变量中。然后,我们使用内建的 filesize()函数读取文件的大小,它返回文件的字节数。在一个只有基本 ASCII 字符的文本文件中,每个字符(包括不可见字符)占用 1 个字节,所以我们应该预期结果是 3。接下来,我们生成另一个字符串,将\)text 中的每个新行字符("\n")替换为大写字母 N,并将结果存储在$newLinesChanged 变量中。最后,我们打印文件大小和更新后的字符串。以下是运行此脚本后在终端中输出的内容:
numBytes = 3
aNb
第一行确认文件只包含三个字符(字节)的数据:字母 a、一个新行字符和字母 b。第二行是代表文件内容的字符串,其中的新行字符被替换为可见字符:aNb 再次确认文件只包含三个字符,其中两个字母之间有一个新行字符。
确认新行的存在并不是一项简单的任务:在本章后面,我们将探索逐行处理文件内容的函数。这些函数依赖于不可见的新行字符来判断一行的结束和下一行的开始。
注意
file_get_contents() 函数也可以从网上读取文件,而不仅仅是从本地机器读取,只需传递文件位置的完整 URL。例如,尝试将 URL filesamples.com/samples/document/txt/sample1.txt 存储在 \(file *变量中,然后像示例 9-2 中那样调用* file_get_contents(\)file) 。你应该会得到一串无意义的拉丁文文本。
确认文件是否存在
之前的示例假设存在一个名为data.txt的文件。然而,在实际操作中,最好在尝试读取文件内容之前先测试文件是否存在。否则,如果你尝试打开或读取一个找不到的文件,你将收到类似以下的运行时警告:
PHP Warning: file_get_contents(/Users/matt/nofile.txt): Failed to open
stream: No such file or directory in /Users/matt/main.php on line 4
执行将在警告之后继续,如果脚本尝试操作不存在的文件内容,这可能会导致进一步的警告和错误。为了使你的代码更健壮并能应对缺失的文件,你可以使用内置的 file_exists()函数。它返回一个布尔值,确认所提供的文件是否存在。让我们通过更新main.php并参考示例 9-5 的内容来试一试。
<?php
$file = __DIR__ . '/data.txt';
$file2 = __DIR__ . '/data2.txt';
$text = "file not found: $file";
$text2 = "file not found: $file2";
if (file_exists($file)) {
$text = file_get_contents($file);
}
if (file_exists($file2)) {
$text2 = file_get_contents($file2);
}
print $text . "\n";
print $text2 . "\n";
示例 9-5:更新后的 main.php 脚本,在读取文件之前确认文件的存在
在这里,我们添加了\(file2,它是一个保存不存在的文件路径*data2.txt*的第二个变量。在尝试读取任何内容之前,我们将默认的文件未找到消息分配给\)text 和\(text2 变量。这样,即使我们未能读取文件内容,这些变量仍然会保存一些内容。接下来,我们使用 file_exists()函数,在两个连续的 if 语句中确保仅在找到相应文件时才尝试读取*data.txt*和*data2.txt*的内容。然后我们打印\)text 和$text2 的内容,每个后面跟一个换行符。结果如下:
a
b
file not found: /Users/matt/data2.txt
由于data.txt文件存在,它的内容已被读取到\(text 中(替代了默认的文件未找到消息),并被打印出来。与此同时,由于*data2.txt*文件不存在,打印\)text2 会显示一条指示文件无法找到的消息。#### “触摸”文件
Linux 和 macOS 具有 touch 文件终端命令,它会将指定文件的最后访问或修改时间戳更新为当前日期时间,或者如果文件不存在,则创建一个空文件。PHP 提供了几乎相同的 touch()函数,它提供了另一种在访问文件之前确保文件存在的方法。如果你不介意文件内容为空,你可以将示例 9-5 中的默认文件未找到消息和 if 语句替换为简单的 touch()语句,如示例 9-6 所示。
<?php
$file = __DIR__ . '/data.txt';
$file2 = __DIR__ . '/data2.txt';
touch($file);
touch($file2);
$text1 = file_get_contents($file);
$text2 = file_get_contents($file2);
print $text1 . "\n";
print $text2 . "\n";
示例 9-6:更新后的 main.php 脚本,在读取文件之前“触摸”文件
现在我们在使用 file_read_contents()读取文件之前,先将每个文件名传递给 touch()。这让我们可以安全地读取文件,无需使用 if 语句和 file_exists(),因为我们知道如果文件不存在,touch()会创建文件(尽管是空文件)。
确保目录存在
到目前为止,我们一直在处理与执行脚本位于同一目录中的文件,但文件也可能位于不同的目录中。在这种情况下,确认目录是否存在(并且如果不存在则可能需要创建它)非常重要,因为就像缺失的文件一样,不存在的目录会触发运行时警告。PHP 有两个内置函数来处理这种情况:is_dir()返回一个布尔值,确认指定的目录路径是否可以找到,mkdir()则尝试在指定路径创建目录。
注意
mkdir() 函数如果尝试创建的目录已存在,或者基于当前权限设置无法创建该目录,会抛出运行时警告。有关权限的更多信息,请参见第 163 页的《目录和文件权限》一章。
要尝试这些函数,请按照示例 9-7 中所示更新 main.php 的内容。
<?php
$dir = __DIR__ . '/var';
$file = $dir . '/data.txt';
if (!is_dir($dir)) {
mkdir($dir);
}
touch($file);
$text = file_get_contents($file);
print $text;
示例 9-7:一个更新后的 main.php 脚本,在目录不存在时创建该目录
我们将目标路径和文件名拆分为两个变量:\(dir 存储着文件所在目录的路径,\)file 存储着路径加文件名。我们将 \(dir 设置为执行脚本所在目录(__DIR__)下的*/var* 子目录;该子目录不存在。if (!is_dir(\)dir)) 语句检查 $dir 是否不是有效的目录路径,如果不是有效路径,则调用 mkdir() 创建该目录。现在我们可以安全地调用 touch() 创建文件,因为我们已经确认目录存在,然后读取文件,因为 touch() 会在文件不存在时创建该文件。
mkdir() 的默认选项是非递归的:如果目标目录的父目录不存在,它将无法创建该目录。然而,该函数有一个可选的递归参数;如果设置为 true,函数将同时创建任何缺失的父目录。示例 9-8 中展示了一个示例。
<?php
$dir = __DIR__ . '/sub/subsub';
$file = $dir . '/data.txt';
if (!is_dir($dir)) {
mkdir($dir, recursive: true);
}
touch($file);
$text = file_get_contents($file);
print $text;
示例 9-8:更新后的 main.php 脚本,如果目录缺失则递归创建目录
现在,目录路径包含一个位于当前执行脚本目录下的/sub 目录中的/subsub 子目录。在 if 语句内,我们调用 mkdir(),并将递归参数设置为 true。这确保了该函数不仅会创建 /subsub 目录,还会在必要时创建其父目录 /sub。我们必须将递归设置为命名参数,因为 mkdir() 还接受另一个可选参数来设置新目录的权限,而该参数在函数签名中排在递归参数之前。
将字符串写入文本文件
就像你可以使用 file_get_contents() 将文件内容读取到字符串中一样,你也可以使用互逆的 file_put_contents() 函数将字符串内容写入文本文件。如果目标文件不存在,file_put_contents() 会自动创建该文件,因此你不需要事先测试文件名。更新后的 main.php 脚本在示例 9-9 中展示了其用法。
<?php
$content = <<<CONTENT
the cat
sat
on the mat!
CONTENT;
$file = __DIR__ . '/newfile.txt';
file_put_contents($file, $content);
$text = file_get_contents($file);
print $text;
示例 9-9:一个 main.php 脚本将数据从字符串写入文件
首先,我们声明一个三行 heredoc 字符串 $content,使用 CONTENT 作为分隔符。然后,我们将 $file 变量设置为当前目录路径加上文件名 newfile.txt。接下来,我们调用 file_put_contents() 函数,将目标文件和要写入该文件的文本传递给它。这应该会创建一个包含 $content heredoc 内容的 newfile.txt 文件。为了确认文件已创建并包含文本内容,我们使用 file_get_contents() 读取文件中的文本,并将其存储到 $text 变量中,然后打印出来。以下是结果:
the cat
sat
on the mat!
输出与原始 heredoc 字符串一致,表明我们成功地将字符串写入 newfile.txt 并再次读取出来。
如果你试图写入的文件已经存在,file_put_contents() 的默认行为是完全替换(覆盖)该文件的内容。为了避免这种情况,可以在调用该函数时使用 FILE_APPEND 选项。这将把新文本添加到文件现有内容的末尾。列表 9-10 展示了一个示例,更新自列表 9-9。
<?php
$newContent = <<<CONTENT
the rat
spat
on the cat!
CONTENT;
$file = __DIR__ . '/newfile.txt';
file_put_contents($file, $newContent, FILE_APPEND);
$text = file_get_contents($file);
print $text;
列表 9-10:一个 main.php 脚本,将文本追加到文件末尾
这次我们创建一个不同的 heredoc 字符串,并通过将 FILE_APPEND 作为第三个参数调用 file_put_contents() 将其添加到 newfile.txt 文件中。这应该会将字符串追加到文件当前内容之后,输出确认了这一点:
the cat
sat
on the mat!
the rat
spat
on the cat!
尝试再次运行列表 9-10 中的代码,不使用 FILE_APPEND 选项。你会发现只有 $newContent 中的文本出现在输出中,因为文件中已有的文本被覆盖了。
管理文件和目录
除了读取和写入文件外,PHP 还提供了帮助管理现有文件和目录的函数。例如,你可以使用 unlink() 函数删除文件,或者使用 rmdir() 删除整个目录。如果成功,两个函数都会返回 true,否则返回 false。与读取文件一样,在尝试删除文件之前,测试文件或目录是否存在非常重要。否则,如果你对不存在的文件或目录调用 unlink() 或 rmdir(),你会收到警告(但执行会继续)。列表 9-11 展示了这些函数的实际应用。
<?php
$dir = __DIR__ . '/var';
$file = $dir . '/data.txt';
if (!is_dir($dir)) {
mkdir($dir);
}
touch($file);
var_dump(is_dir($dir));
var_dump(file_exists($file));
unlink($file);
rmdir($dir);
var_dump(file_exists($file));
var_dump(is_dir($dir));
列表 9-11:一个 main.php 脚本,用于创建并删除目录和文件
和之前的一些示例一样,我们在两个变量 $dir 和 \(file 中声明目标目录和文件名。然后,如果目录尚未存在,我们会创建该目录,并通过 touch() 创建文件。此时,我们应该确保在*/var* 目录中存在 *data.txt* 文件;我们通过调用 is_dir() 和 file_exists() 并使用 var_dump() 来确认这一点。接下来,我们使用 unlink(\)file) 和 rmdir($dir) 删除文件及其目录。最后,我们再次调用 var_dump(),以确保在脚本执行完毕后,目录和文件都不存在。如果你运行此脚本,你应该看到 true, true, false, false 的输出,确认目录和文件曾经存在并已成功删除。
另一个有用的文件管理函数是 rename(),它用于更改文件或目录的名称。例如,你可以使用以下语句将oldfile.txt重命名为newfile.txt:
rename('oldfile.txt', 'newfile.txt');
使用此函数时需要小心,首先要测试旧的文件或目录是否存在。还需要特别注意新文件或目录。如果你正在重命名一个文件,而另一个同名的文件已经存在,那么该文件将被覆盖而不会显示错误或警告,如果你需要被覆盖文件的内容,这可能会造成问题。如果你正在重命名一个目录,而新目录已经存在,则会生成一个警告,这也不是理想的情况,因为最好避免出现警告。如果你正在将文件重命名到另一个目录中,你还应该确保新目录存在,并且如果需要的话,该目录是可写的(这是 Windows 系统的要求)。有关此函数的更多信息,请参见 www.php.net/manual/en/function.rename.php。
将文件读取到数组中
PHP 内置的 file() 函数将文件的内容读取到一个数组中,而不是单一的字符串,每一行对应数组中的一个元素。当你想对每一行执行某个操作(例如,像以下示例一样显示行的内容及其行号),或者当每一行代表需要处理的数据集中的一个项时,这个功能非常有用,比如逗号分隔值(CSV)文件中的数据。Listing 9-12 展示了一个主脚本,演示了 file() 函数的用法。
<?php
$file = __DIR__ . '/data.txt';
$lines = file($file);
foreach ($lines as $key => $line) {
print "[$key]$line";
}
Listing 9-12: 一个用于循环遍历并打印文本文件每一行的 main.php 脚本
我们将文件信息(保存在\(file 变量中)传递给 file() 函数,后者会将*data.txt*的内容逐行读取到一个名为\)lines 的数组中。然后,我们使用 foreach 循环逐个打印数组的每个元素(文件中的一行),并显示其数字键。如果data.txt包含Listing 9-1中的三行俳句,则输出应该如下所示:
[0]what is with this code?
[1]oh my, looks like I wrote it
[2]what was I thinking?
你可以将可选的标志作为第二个参数传递给 file() 函数,例如,排除每行末尾的换行符(FILE_IGNORE_NEW_LINES)或完全忽略文件中的空行(FILE_SKIP_EMPTY_LINES)。
使用低级文件函数
file_get_contents() 和 file_put_contents() 函数会为你处理所有与文件相关的步骤,比如打开文件、访问文件内容以及再次关闭文件。在大多数情况下,这些函数已经足够满足需求。然而,有时你可能需要更低级地处理文件,可能是逐行,甚至逐字符地处理。在这种情况下,你可能需要通过一系列单独的低级函数调用来显式地管理文件访问的各个步骤。
PHP 的低级文件函数要求你使用 文件系统指针(或简称 文件指针),这是文件数据位置的引用。在内部,PHP 将文件视为 字节流(一个可以线性读取和写入的资源对象),文件指针提供对该字节流的访问。你可以通过调用 fopen() 并传入你想访问的文件路径来获得文件指针。你还需要传入一个字符串,指定 如何 与文件进行交互;例如,文件可以只为读取、只为写入、同时为读写等模式打开。表 9-1 显示了指定一些常见 fopen() 模式的字符串。
表 9-1:常见的 fopen() 模式
| 模式字符串 | 描述 | 文件指针位置 | 如果文件不存在的结果 |
|---|---|---|---|
| 'r' | 仅读 | 文件开头 | 警告 |
| 'r+' | 读写(覆盖) | 文件开头 | 警告 |
| 'w' | 仅写(覆盖) | 文件开头(并通过移除现有内容来截断文件) | 尝试创建文件 |
| 'a' | 仅写(追加) | 文件末尾 | 尝试创建文件 |
操作文件的典型步骤如下:
1. 以适当的模式打开文件并获取文件指针。
2. 如果需要,改变文件指针在文件中的位置。
3. 在文件指针的位置读取或写入。
4. 根据需要重复步骤 2 和 3。
5. 关闭文件指针。
示例 9-13 演示了这个过程。这个脚本通过使用低级的 fopen()、fread() 和 fclose() 函数,达到了与 示例 9-2(将文件内容读取为字符串)相同的效果。
<?php
$file = __DIR__ . '/data.txt';
$fileHandle = fopen($file, 'r');
$filesizeBytes = filesize($file);
$text = fread($fileHandle, $filesizeBytes);
fclose($fileHandle);
print $text;
示例 9-13:使用低级函数读取文件
首先,我们使用 fopen() 打开 data.txt,使用字符串 'r' 来指定只读模式。该函数返回一个文件指针,指向文件的开头,我们将其存储在 \(fileHandle 变量中。接下来,我们调用 filesize() 查找文件的大小(以字节为单位)。然后我们调用 fread() 函数,将文件指针和文件大小(\)filesizeBytes)传递给它,以将整个文件的内容读取到 $text 变量中。如果我们只想读取文件的一部分,可以在 fread() 函数的第二个参数中指定不同的字节数。(如果文件指针位于文件的某个位置而不是开头,我们也需要指定不同的字节数。)最后,我们通过将文件指针传递给 fclose() 函数来关闭文件。关闭文件可以使其被其他系统进程使用,并在脚本执行过程中发生错误时防止文件被损坏。
本示例展示了一些最常见的低级文件操作函数,但 PHP 还有许多其他函数。例如,fgets() 从当前文件指针位置读取一行(直到下一个换行符),fgetc() 从当前文件指针位置读取一个字符。feof() 函数接受一个文件指针,并根据指针是否处于文件末尾返回 true 或 false。这在如下循环中非常有用:
while (!feof($fileResource)) {
// Do something at current file pointer position
}
这里我们使用 NOT 运算符(!)来否定 feof() 的结果,因此循环将持续进行,直到指针到达文件末尾。在这种循环中,我们可能会使用 fgets() 从文件中读取一行,使用 fgetc() 读取下一个字符,或者使用 fread() 读取固定数量的字节。然后,循环中的逻辑将处理读取到的数据(如果成功读取),如果在读取过程中到达文件末尾,循环将终止。
一些函数只是用于操作和更改文件指针。例如,rewind() 将文件指针移回到文件的开头,ftell() 返回文件指针的当前位置信息,表示为从文件开头开始的字节数。fseek() 函数将文件指针移动到文件中的指定位置,该位置相对于当前指针位置、文件开头或文件末尾指定。
处理多个文件
让我们通过一个更复杂的示例,将本章迄今讨论的内容结合起来,程序化地从多个文件中提取数据,并将其汇总到一个新的摘要文件中。我们将尝试收集三名玩家的姓名和游戏分数,每个玩家的数据存储在单独的文件中(joe.txt、matt.txt 和 sinead.txt),对数据进行重新格式化,然后写入名为 total.txt 的输出文件中。清单 9-14 到 9-16 显示了我们想要处理的三个原始数据文件。
Joe
O'Brien
55
清单 9-14:joe.txt
Matthew
Smith
99
清单 9-15:matt.txt
Sinead
Murphy
101
清单 9-16:sinead.txt
请注意,每个数据文件的内容有点杂乱,存在随机位置的空行:清单 9-15 以一个空行结尾,清单 9-16 以两个空行开始和结束。尽管如此,每个数据文件的内容顺序是相同的:第一行包含玩家的名字,第二行包含他们的姓氏,第三行包含他们的整数分数。
在输出文件中,我们希望将每个玩家的所有数据合并到一行中,并显示所有三名玩家分数的总和。清单 9-17 展示了最终生成的total.txt文件应该如何显示。
Player = Joe O'Brien / Score = 55
Player = Matthew Smith / Score = 99
Player = Sinead Murphy / Score = 101
total of all scores = 255
清单 9-17:我们想要创建的合并后的 total.txt 文件
为了实现最终结果,我们需要分别处理每个数据文件的不同部分,因此不能仅仅使用 file_get_contents()将整个文件加载为一个字符串。更好的做法是使用 file()函数将每个文件读取为一个包含单独行的数组。
在处理多个文件时,PHP 名为 glob()的函数是一个强大的工具。它返回一个匹配给定模式的文件和目录路径数组。这对于识别并循环遍历指定位置的所有数据文件特别有用。例如,以下语句提供了一个包含/data子文件夹中所有.txt文件路径的数组,相对于执行脚本所在的位置:
$files = glob(__DIR__ . '/data/*.txt')
是一个通配符,代表任意数量的字符,因此'/data/.txt'将匹配给定文件夹中任何以.txt扩展名结尾的文件名。这正是我们在这个示例中收集玩家数据文件所需要的。
启动一个新项目,并创建一个包含前面在清单 9-14 至 9-16 中展示的文本文件joe.txt、matt.txt和sinead.txt的/data子文件夹。然后,在主项目文件夹中,创建一个名为main.php的脚本,并包含清单 9-18 中的内容。
<?php
$dir = __DIR__ . '/data/';
$fileNamePattern = '*.txt';
$files = glob($dir . $fileNamePattern); ❶
$outputFile = __DIR__ . '/total.txt';
touch($outputFile);
unlink($outputFile);
$total = 0;
foreach ($files as $file) {❷
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$firstName = $lines[0];
$lastName = $lines[1];
$scoreString = $lines[2];
$score = intval($scoreString);
$outputFileHandle = fopen($outputFile, 'a');
fwrite($outputFileHandle, "Player = $firstName $lastName / Score = $score\n"); ❸
fclose($outputFileHandle);
$total += $score;
}
$outputFileHandle = fopen($outputFile, 'a');
fwrite($outputFileHandle, "total of all scores = $total");
fclose($outputFileHandle);
print file_get_contents($outputFile); ❹
清单 9-18:处理多个文件的脚本
我们首先将执行脚本所在位置的/data子文件夹路径赋值给\(dir 变量,并将文件名模式字符串'*.txt'赋值给\)fileNamePattern,使用通配符表示任何.txt*文件。接着,我们调用 glob()函数,获取\(dir 中与\)fileNamePattern 匹配的所有文件数组,并将结果存储在\(files 变量中❶。由于 glob()函数的帮助,我们知道\)files 数组中的所有文件都存在,因此可以避免在尝试读取文件之前检查它们是否存在的麻烦。
接下来,我们将total.txt的路径赋值给$outputFile 变量。这个文件可能已经存在,也可能不存在,但我们希望每次运行脚本时都能生成一个新的输出文件。因此,我们使用 touch()函数来创建文件(如果它尚不存在),然后使用 unlink()函数删除该文件。现在,我们可以确保在将数据合并到total.txt时,文件是空的。
在将\(total 变量初始化为 0 之后,我们使用 foreach 循环❷遍历\)files 数组中的文件路径,将每个路径存储到临时变量\(file 中。对于每个文件,我们使用 file()函数将其内容读取到一个名为\)lines 的数组中。使用 FILE_IGNORE_NEW_LINES 和 FILE_SKIP_EMPTY_LINES 标志调用该函数,确保忽略行尾字符并排除空行。这意味着根据我们对每个数据文件的了解,\(lines 应该是一个包含三个元素的数组:第一个元素是玩家的名字,第二个元素是他们的姓氏,第三个元素是他们的分数(以字符串形式表示)。我们从数组中读取这些值到单独的\)firstName、\(lastName 和\)scoreString 变量,并使用内置的 intval()函数将分数从字符串转换为整数。
仍然在 foreach 循环内,我们调用 fopen()来获取输出文件(total.txt)的文件指针,采用写附加模式(通过模式字符串'a'指定),这意味着指针将定位到文件末尾。第一次通过循环时,total.txt文件不会存在,因此 fopen()会创建该文件。然后,我们使用 fwrite()将一个字符串附加到输出文件中,总结玩家的姓名和分数,并以换行符(\n)结束❸。我们通过 fclose()关闭输出文件,并将当前玩家的分数添加到$total 变量中。
最后,在 foreach 循环完成后,我们再次以写附加模式访问输出文件,并附加一个包含$total 值的最终字符串。然后,为了确保一切正常工作,我们调用 file_get_contents()函数读取输出文件到一个字符串中并打印结果❹。注意,我们直接从打印语句中调用该函数,而不是先将字符串存储到一个变量中。
如果运行main.php脚本,你应该得到之前在列表 9-17 中显示的total.txt文件。事实上,你可以随意多次运行该脚本,结果将始终相同,因为任何现有的total.txt文件都会在 touch()和 unlink()函数的组合操作下被删除。
严格来说,我们的main.php脚本并不是实现所需逻辑的最高效方式。我们不需要在每次执行 foreach 循环时都打开和关闭输出文件;我们可以在循环之前只打开一次文件,然后在附加总分后再关闭它。然而,每次通过循环时打开文件可以说明写附加模式的价值,这种模式将文件指针放置在文件的末尾。这样,任何新写入文件的内容都会被添加到现有内容之后。
JSON 和其他文件类型
PHP 不仅能处理 .txt 文件。例如,它还可以处理 JavaScript 对象表示法(JSON)以及其他基于文本的数据格式。对于 JSON 数据,内置的 json_encode() 函数可以将 PHP 数组转换为 JSON 字符串,而 json_decode() 函数则执行相反的操作。这种类型的转换特别顺畅,因为 JSON 数据与 PHP 数组一样,都是围绕键/值对构建的。示例 9-19 展示了这些函数的实际应用。
<?php
$filePath = __DIR__ . '/data.json';
$data = [
'name' => 'matt',
'office' => 'E-042',
'phone' => '086-111-2323',
];
$jsonString = json_encode($data);
file_put_contents($filePath, $jsonString);
$jsonStringFromFile = file_get_contents($filePath);
print $jsonStringFromFile;
$jsonArrayFromFile = json_decode($jsonStringFromFile, true);
print "\n";
var_dump($jsonArrayFromFile);
示例 9-19:一个将数组转换为 JSON 以及将 JSON 转换回数组的脚本
我们将 data.json 的路径存储在 $filePath 变量中。然后我们声明一个 $data 数组,将 'matt'、'E-042' 和 '086-111-2323' 分别映射到键 'name'、'office' 和 'phone'。接下来,我们使用 json_encode() 函数将数组转换为 JSON 格式的字符串,并将结果存储在 $jsonString 变量中。然后,我们使用 file_put_contents() 将 JSON 字符串写入 data.json 文件,就像我们写入 .txt 文件一样。
脚本的其余部分执行相同的反向操作。我们使用 file_get_contents() 从文件中读取 JSON 数据到 $jsonStringFromFile 变量中,并将其打印出来。该变量包含一个 JSON 字符串,但我们使用 json_decode() 将字符串转换为 PHP 数组,并通过 var_dump() 展示。我们需要提供 true 作为 json_decode() 函数的第二个参数,否则结果将是一个对象类型,而不是数组类型。以下是运行此脚本时在终端的输出:
{"name":"matt","office":"E 042","phone":"086 111 2323"}
array(3) {
["name"]=>
string(4) "matt"
["office"]=>
string(5) "E-042"
["phone"]=>
string(12) "086 111 2323"
}
第一行展示了我们写入并从 data.json 文件中读取的 JSON 字符串。该字符串由一个 JSON 对象组成,使用大括号括起来,包含三个由逗号分隔的键/值对。键与其对应的值通过冒号分隔。其余部分显示了 $jsonArrayFromFile 的内容,这是通过解码 JSON 数据创建的数组。注意 JSON 对象中的键/值对与 PHP 数组中的键/值对之间的直接对应关系。
对于 YAML(YAML 不是标记语言)文本数据文件,PHP 提供了多个函数。例如,yaml_parse() 和 yaml_emit() 函数类似于 json_decode() 和 json_encode(),用于在 YAML 字符串和 PHP 数组之间转换。PHP 还提供了直接的文件到字符串和字符串到文件的 YAML 函数:yaml_parse_file() 和 yaml_emit_file()。
对于 CSV 文件,PHP 提供了直接的文件到字符串和字符串到文件的函数 fgetcsv() 和 fputcsv()。str_getcsv() 函数接受一个 CSV 格式的字符串并将其转换为数组。然而,该函数存在一些缺陷。例如,它不会转义换行符,因此无法处理来自电子表格(如 Google Sheets 或 Microsoft Excel)的典型 CSV 文件。可能正因为这样 PHP 在处理 CSV 数据时不符合标准,导致它没有一个相应的函数来从数组创建 CSV 编码的字符串。
使用可扩展标记语言(XML)要复杂一些。PHP 用对象表示 XML 数据,因此你需要掌握面向对象编程的基础知识,才能使用像 simplexml_load_file() 这样的函数和 SimpleXMLElement 这样的类。然而,一旦你掌握了这些语言特性,PHP 提供了几种强大的方法来遍历和操作 XML 数据。我们将在 Part V 中讨论面向对象的 PHP。
总结
在本章中,我们使用了基本的 PHP 函数,如 file_get_contents() 和 file_put_contents(),用于读取和写入外部文件的数据。我们还讨论了 file() 函数,它将文件的每一行读取为单独的数组元素,以及像 fread() 和 fwrite() 这样的低级函数,它们允许你通过指针遍历文件。我们探讨了如何在与文件交互之前,确保文件或目录存在(或不存在),以及如何使用 glob() 获取所有匹配特定标准的文件的引用。虽然我们大部分时间都在处理 .txt 文件,但我们也涉及了一些 PHP 函数,用于处理 JSON、YAML、CSV 和 XML 数据格式。
练习
1. 在网上找一首打油诗,或者自己写一首。我找到了一首:
A magazine writer named Bing
Could make copy from most anything
But the copy he wrote
of a ten-dollar note
Was so good he now lives in Sing Sing
编写一个脚本,声明一个数组;数组的每个元素是打油诗中的一行。然后将这些行写入一个名为limerick.txt的文本文件中。
2. 在网上找到一个可以通过 URL 访问的示例 JSON 文件(例如,* jsonplaceholder.typicode.com *)。编写一个脚本,从 URL 读取 JSON 字符串,将其转换为数组,然后使用 var_dump() 显示该数组。
3. 在 data 文件夹中为游戏玩家及其最高分添加一个新的数据文件,以供 Listing 9-18 中的脚本处理。运行主脚本,你应该会看到输出文件中多了一行,并且新分数已添加到总分中。
第三部分 编程 Web 应用程序
第十章:10 客户端/服务器通信与网页开发基础

作为互联网语言,PHP 与网页客户端和网页服务器之间的通信紧密相关。在本章中,我们将了解客户端和服务器如何工作,检查它们之间传递的消息。我们还将学习如何有效地将 PHP 语句嵌入静态 HTML 代码中,以构建一个完整的 HTML 文本文件,网页浏览器客户端可以理解并在屏幕上呈现为网页。最后,我们将讨论典型的 PHP 网页应用程序的结构,包括对模型-视图-控制器(MVC)架构的初步了解。
不管你是否意识到,你可能每天都会使用客户端和服务器。当你检查你的电子邮件或社交媒体账户时,你正在使用一个客户端应用程序与服务器进行通信,请求更新。这类应用程序不断地向服务器发送请求;例如,你的电子邮件应用程序会向像 Google Gmail 或 Apple iCloud 这样的服务器请求,以便下载任何新邮件并更新手机上的信息和文件夹,确保与服务器上的变化保持同步。
你可以在两个地方运行网页服务器应用程序:本地计算机上,或者是一个可以公开访问的互联网计算机上。作为 PHP 程序员,你会在自己的机器上进行大量本地开发。当你觉得一个项目准备好了,你会在公共服务器上测试它,最后,在所有测试完成后将网站发布到线上。
HTTP 请求-响应周期
在基于网页的客户端/服务器通信的核心是HTTP 请求-响应周期。从高层次来看,客户端向服务器发送请求,然后服务器向客户端返回响应。响应本身可以是一个错误代码,或者是一个包含文本、图像文件、二进制可执行文件或其他内容的消息。图 10-1 展示了一个简单的请求-响应周期。

图 10-1:一个简单的 HTTP 请求-响应周期
客户端,即一个网页浏览器,发送请求,要求获取index.xhtml文件❶。服务器接收到并解码请求,然后搜索并成功找到请求的资源(文件)❷。服务器接着创建并返回一个响应,其主体是index.xhtml的 HTML 文本❸。最后,网页浏览器读取收到的 HTML,并在浏览器窗口中漂亮地显示网页内容给用户❹。
客户端可以发送不同类型的请求。两种最常见的请求方法是 GET 和 POST。HTTP GET 方法更简单,在使用 Web 浏览器客户端时,显示许多发送的内容在 Web 浏览器的 URL 地址栏中。例如,如果你使用 Google 搜索引擎搜索 cheese cake 这个短语,你会看到这些词出现在 URL 的末尾,发送查询给 Google 时是这样的:https://www.google.com/search?q=cheese+cake。实际上,每当你在浏览器地址栏中输入一个 URL 并按下回车键时,你就是在发送 GET 请求。
POST 方法可以隐藏请求消息体中发送的许多内容。因此,它通常用于更私密的网站操作。
除了 GET 和 POST,原始的 HTTP 1.0 定义了第三种方法 HEAD。它请求一个没有消息体的响应,只有头部,其中包含关于响应的一般信息。自从引入 HTTP 1.1 后,还允许使用其他五种方法(OPTIONS、PUT、DELETE、TRACE 和 CONNECT)。这些方法对于本书中讨论的 Web 开发层级并不需要,尽管它们对于复杂的 Web 应用程序来说是有用的。
响应状态码
每个由服务器返回的 HTTP 响应开头都有一个三位数的 HTTP 状态码,告诉客户端服务器尝试处理并完成请求的状态。所有符合 HTTP 标准的服务器必须使用一套标准代码,此外,不同的服务器还会使用自定义代码。最常见的代码是 200 OK,表示请求已成功完成,以及 404 Not Found,表示服务器无法找到请求的资源。
状态码的第一位数字表示服务器对请求的解释和处理的总体状态。以下是第一位数字的含义概述:
1nn (信息性状态码) 请求头已经接收并理解,进一步处理需要进行。换句话说,“到目前为止一切顺利,但还没有完成。”这些状态码相对不常见,它们是信息性状态码,用于当服务器需要向客户端传达某些信息,但不是完整的响应时。
2nn (成功) 请求已接收、理解并接受(例如,200 OK)。
3nn (重定向) 请求已理解,但客户端必须采取进一步行动,例如从选项中选择(300 多重选择)或如果资源已永久移动,按照新 URL 访问(301 永久移动)。
4nn (客户端错误) 请求无效(如 400 错误请求),或者由于客户端错误,服务器无法完成请求(如 404 未找到或 403 禁止访问)。
5nn (服务器错误) 服务器遇到错误或因其他原因无法完成请求。例子包括 500 服务器错误和 502 服务不可用。
你可以在 Todd Fredrich 的免费在线 REST API 教程中了解更多关于 HTTP 及其状态码的内容:www.restapitutorial.com。
一个 GET 请求的例子
让我们通过查看访问 No Starch Press 网站时发生的幕后情况,来看一个简单的请求-响应周期的例子。首先,你需要显示浏览器的请求-响应检查工具。在 Google Chrome 中,这些工具通常可以通过名为开发者工具的菜单项访问。打开开发者工具后,你会看到一个窗口,类似于图 10-2 底部的样子。

图 10-2:GET 请求 No Starch Press 主页
点击网络标签,你就可以开始记录和查看 HTTP 请求-响应周期了。在浏览器地址栏中输入nostarch.com。当你按下 ENTER 键时,你应该能看到主页出现。在开发者窗口的左侧找到“名称”列,定位到第一个文件,它应该是nostarch.com,然后点击它。点击头部以查看 HTTP 头部的总结,显示在图 10-3 中。
这个总结表示,HTTP 请求的目标是网址nostarch.com,请求方法为 GET(因为我们刚在地址栏中输入了网址)。HTTP 响应头中最重要的部分是 200 的成功状态码。
向下滚动 HTTP 头部内容,你将看到 HTTP 请求和 HTTP 响应头部的详细信息。在请求头部部分,你可以看到网页客户端愿意接受的文件类型列表,如 HTML、XML、图像等。你还可以看到内容可用的语言(例如,EN 代表英语)。对应地,响应头部则指示响应体中的实际内容类型,如 text/html、文件的最后修改日期等。
注意
现代大多数网站现在使用超文本传输协议安全(HTTPS),它使客户端和服务器能够交换证书,从而确保 HTTP 消息的加密传输。这就是为什么 No Starch Press 的网址以 https://开头的原因。HTTPS 已集成在许多 PHP 网页服务器中,因此我们此时不再深入讨论它。
现在点击响应标签查看响应体的内容,如图 10-3 所示。这是网页浏览器接收到的 HTML 文本,然后它会渲染成一个吸引人的图形网页,供你查看和交互。

图 10-3:HTTP 响应体中的 HTML 文本内容
在 HTML 代码的底部,你会看到一列 CSS 链接。当处理收到的 HTML 时,Web 客户端(浏览器)会寻找页面所需的其他内容文件,如 CSS 样式表、图像文件和 JavaScript 文件。浏览器会快速地(在现代网络速度下我们很少注意到这一点)向服务器发起额外的 HTTP 请求来获取这些文件,并且随着相应的 HTTP 响应到达,浏览器会渲染网页。这些从 Web 服务器收到的额外文件可以在 图 10-2 的名称列中看到,位于原始请求 nostarch.com 下面,它们的名称像 css_lQaZ 等等。
需要强调的是,并非每个 HTTP 请求都必须由用户通过输入 URL、点击链接或提交表单来发起。Web 浏览器可以在后台(异步地)发起额外的请求,以获取所需的资源,例如图像、CSS 文件和 JavaScript 文件。这些额外的请求可能会发送到与浏览器正在处理的 HTML 相同的 Web 服务器,或者发送到其他 Web 服务器(例如,下载免费的 Google 字体或 Bootstrap CSS 和 JavaScript)。
注意
JavaScript 代码也可以发起额外的 HTTP 请求,例如从远程网站获取数据。这被称为 异步 JavaScript 和 XML (AJAX),虽然许多类型的数据文件可以被检索,例如 JSON 和纯文本,因此这样的 HTTP 请求并不限于仅检索 XML 数据。这个话题超出了本 PHP 书的范围。
服务器的运作方式
我们已经在较高层次上讨论了客户端和服务器如何通过 HTTP 请求和响应进行通信。现在,让我们更仔细地看看 Web 服务器的工作原理。我们还将开始了解 PHP 如何在服务器的操作中发挥作用。
用于文件检索的简单 Web 服务器
简单 Web 服务器的任务是监听资源请求,并在收到请求时,识别请求的资源并返回包含资源的消息,或者如果找不到该资源,则返回错误消息。简单的 Web 服务器本质上是一个能够理解 HTTP 请求并发送 HTTP 响应的文件服务器。图 10-4 展示了一个简单的 Web 服务器。

图 10-4:简单的 Web 服务器与 Web 客户端的通信
通常,客户端发送一个 GET 请求 ❶,请求一个文件,如 index.xhtml、style.css 或 logo.png。服务器接收并解释该请求,然后搜索请求的资源(文件) ❷。如果文件找不到,服务器会创建并返回一个 404 Not Found 错误。如果文件找到,服务器将检索其内容 ❸。最后,服务器会创建并返回一个响应给客户端 ❹。响应体是请求文件的内容,其头部包括 200 OK 状态码。
这个过程的一个很好的类比是,一个简单的网页服务器就像图书馆中的图书管理员:图书管理员会去找到请求的书籍,并带回书籍或一条信息,说明书籍无法找到。
简单的网页服务器足以支持超文本或超媒体浏览不变的 HTML 页面集,例如常见问题解答(FAQ)及其回答段落,或者是很少需要更新的参考资料,如用户手册。简单的网页服务器是无状态的,这意味着相同的请求总是返回相同的文件。不同的客户端也会收到相同的文件。这通常被称为静态内容,表示它是固定不变的。回到图书管理员的类比,你不会期望图书管理员在取书时改变书的内容。
我们可以总结简单的无状态网页服务器的行为如下:
-
永不改变
-
无论用户是否曾经访问过,都一样
-
对每个用户都相同
大多数网页活动比仅仅点击链接获取特定的静态文档要更加互动。大多数现代网页项目需要动态交互性,即系统根据用户输入做出不同的响应。动态交互性包括处理网页表单、管理购物车、根据最近的浏览历史定制内容等任务。大多数 PHP 网页应用程序是动态网页服务器,接下来我们将深入探讨。
处理数据的动态网页服务器
对于一个基于网页的系统,要实现超越静态资源获取的交互性,除了基本的内容标记和超文本链接外,还需要其他技术。这些能力包括以下内容:
-
支持用户输入方法,如输入文本、点击按钮和从菜单中选择
-
可以根据不同的用户输入以不同方式处理和响应的简短代码脚本
-
浏览器将用户输入或数据发送到服务器程序,以便处理数据并生成交互式响应的方法
具备这些功能的动态服务器处理许多现代互联网活动,如在搜索引擎中输入关键词并呈现一个定制的、优先排序的链接页面,登录到个人电子邮件系统并查看自己的收件箱中的邮件,在线浏览商品目录并使用信用卡完成购买。
在本书中,我们最关注的是理解并能运行 PHP 脚本的动态网页服务器。图 10-5 展示了客户端与这样一个动态网页服务器的通信。

图 10-5:动态网页服务器与网页客户端的通信
在此模型中,客户端向服务器发送 HTTP 请求 ❶。然后,服务器程序解析请求并识别应执行哪个 PHP 服务器脚本 ❷。脚本执行 ❸ 并生成输出,例如 HTML 文本。运行 PHP 脚本还可能触发 Web 服务器上的其他操作,例如与数据库的通信,这是我们将在第六部分中探讨的内容。接下来,Web 服务器应用程序接收输出 ❹。最后,输出被打包到 HTTP 响应消息的主体中,并带着适当的头信息返回给最初发出请求的客户端 ❺。
路由过程
路由是 Web 服务器用来决定如何响应所收到的 HTTP 请求的过程;服务器检查请求并确定它认为客户端请求的操作,如请求文件、尝试使用包含在请求中的用户名和密码数据登录、从数据库中删除项等。在最简单的情况下,请求包含特定资源文件的有效路径,如/images/logo.jpg或/styles/homepage.css。在这种情况下,Web 服务器充当文件服务器,返回包含文件内容并带有适当头信息的 HTTP 响应消息。
如果请求了有效的公开可用的php文件路径,如/about.php,则该 PHP 脚本将被解释并执行,以构建返回给客户端的 HTTP 响应。如果没有请求特定的文件,几乎所有 Web 服务器都有定义的默认路由,通常会路由到主页文件,通常为index。简单的静态 Web 服务器会查找index.xhtml作为默认主页返回,而 PHP Web 服务器通常会首先查找index.php,如果没有找到默认的 PHP 文件,则可能会查找index.xhtml。如果没有请求文件且未找到索引文件,服务器将返回 404 Not Found 响应。
复杂的 PHP Web 应用程序将使用编码在默认index.php脚本中的逻辑来检查请求的 URL 路径的内容和模式,从而决定如何响应请求。使用这种逻辑来管理多功能网站复杂性的index.php 文件被称为前端控制器。
以下是 Web 浏览器用来向 Web 服务器发出请求的一些 URL 示例,并附有解释其含义:
tudublin.ie 在域名后没有指定路径,因此 Web 服务器将执行默认的主页脚本(如果是 PHP Web 服务器,则为index.php)。TU Dublin 主页的 HTML 内容会返回给客户端。
bbc.com/travel/columns/discovery 该路径包含通过斜杠分隔的文本,因此主页脚本执行逻辑,搜索站点数据库中与主要话题旅行和子话题发现相关的内容。
nostarch.com/sites/all/themes/nostarch/logo.png 路径包含一个静态资源文件,因此 Web 服务器会定位并返回logo.png图像文件的内容。
google.com/search?q=cheese+cake 路径包含指示搜索的文本,在正斜杠后面(search),然后是搜索文本(cheese cake),这是一个在问号字符(?)后面赋值的变量(q)。因此,Google 主页脚本会执行逻辑,搜索与cheese cake相关的网页。在第十一章中,你将学习如何通过变量在 URL 中传递数据。
在第十三章中,我们将研究如何编写 PHP 前端控制器逻辑来执行路由决策,类似于这里总结的内容。
模板
几乎所有 PHP 应用程序都设计用来运行网站。对于大多数 HTTP 请求,响应的内容是某种文本,如 HTML、JavaScript 或 CSS 文件,或者可能是编码为 JSON 或 XML 的数据。因此,PHP 的设计是为了方便输出文本(例如,使用 print 和 echo 命令)。
此外,正如第一章中所暗示的,语言还使得将预写文本(如 HTML)与通过执行 PHP 代码动态生成的文本混合变得容易。这个特性使得 PHP 成为模板语言:它可以将动态生成的值插入到 HTML 或其他文本的静态模板中。基于 PHP 的网站受益于这种动态输出,可能是通过数据库交互或与各种数据源(如 Google Maps、天气 API 等)的通信来实现的。
在前几章中,我们编写了纯 PHP 程序,这些程序仅包含 PHP 代码。一旦我们开始使用 PHP 作为模板语言,混合 PHP 语句和其他模板文本(通常是 HTML)变得更加常见。这使得我们可以将不变的 HTML 网页部分直接写为 HTML;任何需要动态变化的部分都可以通过我们在 PHP 语句中编写的逻辑来输出。方便的是,许多网页中的 HTML 包含大量相同的内容,例如页头、导航栏(可能仅通过突出显示访问的特定页面而发生变化)和页面布局 HTML 代码(例如,div、header 和 footer 元素的层次结构)。所有这些不变的静态内容都非常适合用于 PHP 模板。
理论上,通过编写大量的 print 语句,可以让纯 PHP 脚本输出 HTML,但这种方法会导致代码冗长且难以阅读。请查看清单 10-1,它通过使用纯 PHP print 语句来输出 HTML。
<?php
print '<!doctype html>';
print '<head><title>home</title></head>';
print '<body>';
print '<p>Welcome to My Great Website<br>';
❶ print 'today is ' . date('F d, Y');
print '<p>';
print '</body></html>';
清单 10-1:通过 print 语句输出 HTML
我们在这里使用的唯一真正的 PHP 逻辑是调用 date() 函数,以字符串形式获取当前日期,格式为 月 日, 年(例如,2025 年 1 月 1 日)❶。其他所有行都是输出不变的 HTML 的打印语句,这些打印语句并不是必需的。通过仅在需要的地方使用 PHP,并将其插入到 HTML 模板中,我们可以使代码更加简洁和可读。这正是我们在示例 10-2 中所做的,在那里不变的 HTML 被直接写入,正如它最终将出现在发送给客户端的 HTML 文本文件中。
<!doctype html>
<head><title>home</title></head><body>
<p>Welcome to My Great Website<br>
today is
❶ <?php
print date('F d, Y');
❷ ?>
</p>
</body></html>
示例 10-2:将 HTML 模板文本与 PHP 代码块混合
我们使用开头的 ❶ 和结尾的 ❷ PHP 标签将调用 date() 函数的打印语句包围起来,因为这是唯一需要 PHP 代码来动态生成内容的地方。与此同时,我们将其他所有内容编写为常规 HTML;不需要打印语句、引号或分号。图 10-6 显示了 PHP 如何查看并处理脚本内容。

图 10-6:PHP 如何处理混合模板文本和动态代码
首先,需要将一块模板文本逐字复制到文本输出中。接下来,需要解释和执行一块 PHP 代码(位于 之间),然后将结果添加到脚本的文本输出中。最后,需要将另一块模板文本逐字复制到输出文本中。PHP 脚本的多个部分输出的文本的临时存储区被称为 输出缓冲区。
假设示例 10-2 中的脚本是来自 web 浏览器的 HTTP 请求的一部分。当脚本中的所有 PHP 执行完成后,输出缓冲区中的文本将通过添加头部信息被封装成一个 HTTP 响应,并发送回浏览器客户端。然后,浏览器将渲染(绘制)网页以供用户查看,解释它在 HTTP 响应的主体文本中收到的 HTML,从而显示图 10-6 底部所示的简单页面。
PHP 标签
正如你刚才看到的,当你在模板文本中嵌入 PHP 代码时,重要的是使用开头的 标签来界定代码。相比之下,当编写仅包含代码的 PHP 脚本时,脚本应以开头的 标签。
你省略结尾标签有两个原因。首先,你不需要它,因为代码没有模板文本需要与 PHP 语句分隔开。其次,如果你包括了结尾的 PHP 标签,任何(无意的且不可见的)空白字符,如空格、制表符或换行符,都会在结尾标签后出现,并被解释为模板文本,可能会过早开始创建输出缓冲区。
短 echo 标签
到目前为止,我们一直关注 PHP 的主要 <?php 标签,但该语言还提供了一个简短回显标签,用 <?= 符号表示,它进一步简化了模板编写。这个标签可以让你避免编写冗长的命令,当你只想将表达式的结果输出为文本时。比如,显示一个变量的内容,或者是复杂计算或字符串连接的结果。例如,你可以用简短的回显标签 <?= $someVariable ?> 代替写 <?php print $someVariable; ?> 来输出 $someVariable 的值。
简短回显标签减少了输入,因为它省略了 print(或 echo),并且不需要结尾的分号。此外,任何有经验的 PHP 程序员看到简短回显标签时,能立刻识别出唯一的逻辑就是输出一个字符串。总体来说,简短回显标签的主要优势在于,当脚本主要包含 HTML 模板文本时,它不会让读者(或编写者)被多余的 PHP 代码块语法分心。动态生成的 PHP 代码值与周围的 HTML 更好地融合,正如清单 10-3 所示。
<?php
❶ $dateString = date('F d, Y', time());
?>
<!doctype html><head><title>home</title></head><body>
<p>Welcome to My Great Website<br>
❷ Today is <?= $dateString ?>
</p>
</body></html>
清单 10-3:使用 PHP 简短回显标签简化代码
在一个完整的 PHP 代码块中,包围在普通 PHP 标签之间,我们创建了一个包含格式化日期字符串的 $dateString 变量 ❶。这让我们可以简单地在模板中希望输出字符串的位置写 <?= $dateString ?>,使用简短回显标签 ❷。无需编写 print 语句或分号。
模型-视图-控制器架构
几乎所有的大型 Web 应用程序都会将不同的责任委派给不同的系统组件。大多数应用通过实现某种形式的模型-视图-控制器(MVC)架构来做到这一点。这是一种软件设计模式,用于区分软件的基础数据(模型)、数据如何显示给用户(视图)以及何时显示哪些数据的决策(控制器)。
本章中我们已经涉及了 MVC 架构的一些方面。我们提到过 PHP 应用程序如何根据传入的 HTTP 请求做路由决策(控制器任务),以及如何通过将动态生成的值注入静态 HTML 文本来使用 PHP 进行模板化(视图任务)。现在让我们再填补一些空白,看看 MVC 模式如何适应请求-响应周期。图 10-7 展示了 Web 应用程序中 MVC 架构的典型解释。

图 10-7:Web 应用程序常见的 MVC 架构
首先,Web 客户端发送一个 HTTP 请求❶。然后,控制器(主要应用逻辑)解析该请求并决定采取什么操作❷。这可能涉及检查任何存储的安全凭证和其他数据(如购物车内容),并决定在收到请求后采取的适当行动。通常,控制器需要读取数据存储的内容,如数据库系统、文件存储,甚至是运行在另一个服务器上的 API。这些数据就是 MVC 模式中的模型组件。如果收到的请求包含来自表单的数据,控制器可能需要更新或删除某些模型数据。
然后,控制器调用视图组件❸,例如模板文件,以创建响应内容并返回给用户。如果需要,控制器在调用视图组件时会传递从模型中收集的数据。最后,控制器将其创建的响应返回给 Web 客户端(并添加任何适当的头信息、响应代码等)❹。
本书中我们将多次回顾 MVC 模式,并进一步探讨如何构建 PHP Web 应用程序。如前所述,在第十三章中,我们将研究如何创建一个前端控制器脚本来管理架构中的控制器部分。在第二十一章中,我们将介绍 Twig 库,它简化了架构中视图部分的模板化。最后,在第六部分,从第二十七章开始,我们将探讨如何将 PHP 应用程序与数据库集成,以处理架构中的模型部分。
PHP Web 开发项目的结构
正如我们在第一章中讨论的那样,PHP 引擎自带一个内置的 Web 服务器,供测试使用,你可以通过命令行使用 php -S localhost:8000 命令运行它。默认情况下,这个命令会使当前命令行所在目录中的所有文件和文件夹通过 Web 服务器公开。例如,如果你的命令行导航到了主硬盘的根目录(例如 Windows 计算机上的*C:*),然后执行了 PHP Web 服务器命令,那么你就会将硬盘上的所有内容都暴露出来!从安全角度来看,这可能并不是一个好主意。
即使是在特定的 PHP 项目文件夹内,你也可能有一些文件或内容不希望公开发布,例如包含用户名和密码凭证的代码,用于数据访问的脚本,或仅应由授权用户访问的脚本。因此,在进行 PHP 网络开发项目时,通常(并强烈建议)在整个文件夹中创建一个public文件夹。该public文件夹(及其子文件夹,如果有的话)应只包含那些通过 web 服务器公开访问的文件,包括任何图片、音频文件、视频文件、CSS 样式表、JavaScript 文本文件以及网站所需的其他内容。任何响应来自 web 客户端的 HTTP 请求并需要执行的 PHP 脚本也应位于public文件夹中,而不应公开访问的其他内容应存放在项目目录结构中的其他位置。
组织一个安全的 web 应用程序的通常方法是,在项目的public文件夹中仅有一个名为index.php的 PHP 脚本。这个脚本(我们将在第十三章进一步讨论的前控制器)然后根据传入的 HTTP 请求和其他存储的数据,决定应该执行哪些其他非公开脚本。因此,一个典型的 PHP 项目文件夹结构如下所示:

通常,最好从项目的根文件夹而非public文件夹中进行命令行操作。由于这种做法非常常见,内置的 PHP 网络服务器提供了 -t 命令行选项,用于指定从哪个子文件夹提供网页。因此,在命令行界面导航到根项目目录后,你可以输入以下命令,仅通过端口 8000 提供public文件夹中的文件:
**php -S localhost:8000 -t public**
让我们测试这两种运行内置 PHP 网络服务器的方式:带和不带 -t 选项。首先,创建一个名为* 第十章的新空文件夹,并在该文件夹中创建一个名为index.php*的文件,文件内容为列表 10-4 中显示的代码。
<?php
❶ $total = 2 + 2;
?>
<!doctype html><html><head><title>Home page</title></head>
<body>
❷ <?= "total = $total" ?>
</body></html>
列表 10-4:一个简单的 index.php 文件
这个脚本仅包含两条 PHP 语句:在完整的 PHP 标签内,我们将 $total 变量设置为评估数学表达式 2 + 2 ❶ 的结果,并使用简短的 echo 标签输出该变量的内容 ❷。为了确保这个脚本能够正常工作,请将命令行界面导航到chapter10文件夹(如果你还没在该文件夹中,使用 cd 命令切换目录),然后运行内置 PHP 网络服务器,在不指定文件夹的情况下通过端口 8000 提供服务:
% **php -S localhost:8000**
打开 web 浏览器并访问localhost:8000,你应该能看到一个显示 PHP 输出语句结果的网页:total = 4。
现在让我们看看为什么发布项目文件夹的全部内容是一个坏主意。在你的chapter10文件夹中,还创建一个名为password.txt的文本文件,内容是 password=mysecret。然后在 Web 浏览器中访问localhost:8000/password.txt,你会发现这个文本文件也像index.php脚本一样可以公开访问(参见图 10-8)。

图 10-8:Web 服务器发布秘密密码
让我们通过创建一个名为public的子文件夹,并将index.php脚本移动到这个子文件夹中,同时将password.txt保留在主chapter10文件夹中,从而让这个文件更加安全。一旦完成此更改,按 CTRL-C 终止旧的 Web 服务器进程,然后重新启动 Web 服务器,这次限制它只访问public子文件夹中的内容:
% **php -S localhost:8000 -t public**
再次尝试在浏览器中访问localhost:8000和localhost:8000/password.txt。你应该仍然能够看到首页,因为它位于public文件夹中,但当你尝试访问password.txt文件时,应该会收到 404 错误,因为它不在public文件夹中。我们将在本书中遵循使用public文件夹来隔离仅应公开访问的资源的结构。
在本章中,我们探讨了 PHP Web 开发的基本概念。我们考虑了形成 Web 客户端/服务器通信基础的 HTTP 消息,并开始讨论路由的概念,即 Web 服务器如何评估 HTTP 请求路径的内容并决定返回哪些文件或执行哪些服务器脚本。我们还看到了 PHP 作为模板语言的应用,它使我们能够将动态的 PHP 语句与不变的模板文本混合。我们展示了一种使用短 echo 标签将 PHP 输出与 HTML 模板文本整洁地结合在一起的方法。
我们首次了解了 MVC 架构,这是分割和组织驱动 Web 应用程序的任务和数据的一种强大方式。最后,我们查看了 PHP Web 开发项目的典型结构。特别地,我们讨论了需要一个包含所有应公开访问资源的public子文件夹的必要性;任何不应公开访问的文件或脚本必须位于此子文件夹之外。
练习
1. 打开 Web 浏览器的开发者工具并访问一个你喜欢的网站。检查你 HTTP GET 请求的头部以及返回给浏览器的 HTTP 响应消息的正文。
2. 打开 Web 浏览器的开发者工具,访问一个提供表单的网页。填写表单并提交时,查看 HTTP 请求体。你应该能够看到通过 POST HTTP 方法发送到 Web 服务器的名称或值变量。
3. 编写一个“纯”PHP 脚本,全部写在一个 PHP 代码块中,完成以下任务:
a. 定义一个 PHP $pageTitle 变量,包含字符串 'Home Page'。
b. 输出 <!doctype html>
4. 重新编写第 3 题的答案,尽可能使用模板文本代替 PHP 代码。对于 PHP 代码,使用完整的代码块,包含标签。
5. 重新编写第 4 题的答案,使用短回显标签输出$pageTitle 变量中的值。
第十一章:11 创建与处理网页表单

在简单、可点击的链接之后,网页表单可能是人们与网站互动的最常见方式。在本章中,我们将探讨网页客户端如何向服务器脚本提交表单数据,并创建一系列发送数据的网页表单。我们还将练习编写服务器端的 PHP 脚本,以提取和处理传入的表单数据。你将学习如何处理来自各种网页表单元素的数据,并使用 GET 和 POST HTTP 请求发送这些数据。
网页表单只是网页的一部分,允许用户输入数据,然后将用户输入传递给服务器应用程序。网页表单的交互实例包括创建 Facebook 帖子、预订航班或娱乐票、以及输入登录信息。如你所见,网页上的每个表单都在开始和结束的 HTML


浙公网安备 33010602011771号