PHP7-Zend-认证学习指南-全-

PHP7 Zend 认证学习指南(全)

原文:PHP 7 ZEND CERTIFICATION STUDY GUIDE

协议:CC BY-NC-SA 4.0

一、PHP 基础

介绍

这一章关注 PHP 作为一门语言的本质细节。这是一个很大的章节,它涵盖了考试中最重要的一个方面。我能给的最好建议是不要浏览它。PHP 有很多地方都有一些独特之处,即使有几年的经验,您也可能不会全部遇到。

基本语言特征

PHP 中的所有语句都必须以分号结束。例外情况是,如果该语句恰好是结束标记之前的最后一条语句。

空白在 PHP 中没有语义意义。不需要排列代码,但是大多数编码标准都强制这样做,以提高代码的可读性。空白不能出现在函数名、变量名或关键字中间。

一行中允许有多条语句。

代码块由括号符号{ }表示。

PHP 语言结构和函数名不区分大小写,但是变量和常量名区分大小写。

<?php
ECHO "Hello World"; // works
$variable = "Hello World";
echo $VARIABLE; // won't work

将 PHP 插入网页

尽管 PHP 是一种通用的脚本语言,并且可以用于其他目的,但它最常被部署为用于构建网页的服务器端脚本语言。

PHP 解析器不会解析任何不包含在表示 PHP 脚本的标签中的内容。PHP 标签之外的内容只是简单地输出,没有检查。这使得 PHP 可以嵌入 HTML 中。

有几种方法来界定 PHP 脚本,但通常只使用下表中的前两种方法:

类型 打开 关闭 注意
标准 <?php ?>
回声 <?= ?>
短的 <? ?> 反对
脚本 <script language="php"> </script> 不要使用
动态服务器页面 <% %> 反对

标签允许你轻松地回显一个 PHP 变量,缩短的标签让你的 HTML 文档更容易阅读。它通常用在模板中,您希望将几个值输出到页面上的不同位置。使用短语法可以让你的模板代码更加整洁。

当它与标准开放代码中的等效代码一起显示时,它的用法是最容易理解的。以下两个标签是相同的:

<?= $variable ?>
<?php echo $variable ?>

Note

echo语句是结束标记前的最后一条语句,因此不需要分号来终止它。

您可以在开始和结束标记之间使用 PHP 逻辑,如下例所示:

Balance:
<?php
if ($bankBalance > 0): ?>
<p class="black">
<?php else: ?>
<p class="red">
<?php endif; ?>
<?= $bankBalance?>
</p>

让我们单步执行代码:

  1. PHP 解析器将输出Balance:而不对其求值,因为它不在 PHP 标签中。
  2. 然后 PHP 标签检查余额是否大于零并终止。只有当条件为true时,才输出<p class="black">标签;否则,输出<p class="red">标签。
  3. 我们使用echo标签语法来输出$bankBalance变量。
  4. 最后,由于 PHP 脚本已经关闭,结束段落标记不经解析就输出。

Note

这种方法也可以使用if语句的花括号语法。

在 PHP 程序中,省略文件中的结束标记?>是很常见的。这对于解析器来说是可以接受的,并且是防止在结束标记后出现换行符问题的有效方法。

这些换行符由 PHP 解释器作为输出发送,可能会干扰 HTTP 头或导致其他意想不到的副作用。通过不关闭 PHP 文件中的脚本,可以防止发送换行符。

Tip

要求在包含文件中省略结束标记是一个常见的编码标准,但这不是 PHP 的要求。

语言结构

语言结构不同于函数,因为它们被嵌入到语言中。

语言结构可以被解析器直接理解,不需要被分解。另一方面,函数在被解析之前被映射和简化成一组语言结构。

语言构造不是函数,因此不能用作回调函数。当涉及到参数和圆括号的使用时,它们遵循不同于函数的规则。

例如,echo在调用它的时候并不总是需要括号,如果你用多个参数调用它,那么你就不能使用括号。

<?php
// one parameter, no brackets
echo "hello\r\n";
// two parameters, brackets (syntax error)
//echo('hello', 'world');
// two parameters, no brackets
echo 'hello', 'world';

此外,echo不返回值,而每个函数总是返回值(或 null)。

关于保留关键字 1 的 PHP 手册页有一个完整的列表,但是这里有一些你应该熟悉的结构:

建造 用于
assert 调试命令,用于测试条件,如果条件不成立,则执行某些操作。
echo 向 stdout 输出一个值。
print 向 stdout 输出一个值。
exit 可选地输出消息并终止程序。
die 这是退出的别名。
return 终止函数并将控制权返回给调用范围,或者如果在全局范围内调用,则终止程序。
include 包括文件并对其进行评估。如果文件找不到或无法读取,PHP 将发出警告。
include_once 如果你指定了include_once,那么 PHP 将确保它只包含这个文件一次。
require PHP 将包含一个文件并对其进行评估。如果找不到或无法读取该文件,则会生成一个致命错误。
require_once 至于include_once,但是会产生致命错误而不是警告。
eval 该参数被评估为 PHP,并影响调用范围。
empty 根据变量是否为空返回一个布尔值。空变量包括空变量、空字符串、没有元素的数组、数值 0、字符串值0和布尔值false
isset 如果变量已经设置,则返回true,否则返回false
unset 清除变量。
list 从数组中一次分配多个变量。

可能出现的一个棘手的考试问题是理解printecho之间的细微差别。echo构造不返回值,甚至不返回 null,因此不适合在表达式中使用。然而,print构造将返回一个值。

不总是使用include_once()require_once()的原因是性能问题。PHP 跟踪支持这些函数功能的文件列表。这需要记忆,所以这些功能在必要时使用,而不是使用includerequire

评论

有三种标记注释的样式:

<?php
# Perl style comments
// C style comments
/*
   Multiline comment
*/

API 文档还可以符合外部标准,比如 PHPDocumentor 项目所使用的标准。 2 这个工具检查你的 API 风格注释,并自动为你创建文档。

API 文档看起来非常类似于多行注释:

<?php
/**
        API documentation has two asterisks, this is not a PHP
        syntax distinction, but is just a convention.
*/

代表数字

PHP 脚本中有四种表达整数的方式:

注释 例子 注意
小数 1234
二进制的 0b10011010010 通过前导0b0B识别
八进制的 02322 由前导0标识
十六进制的 0x4D2 通过前导0x0X识别

浮点数(在其他语言中称为双精度数)可以用标准的十进制格式或指数格式表示。

形式 例子
小数 123.456
指数的 0.123456e3 or 0.123456E3

Note

指数形式中的字母“e”不区分大小写,整数格式中使用的其他字母也是如此。

变量

在这一节中,我将重点介绍 PHP 如何处理变量。我假设你有足够的 PHP 经验,我不需要解释什么是变量或者如何使用它们。我们将会看到 PHP 提供的各种类型的变量,如何改变变量的类型,以及如何检查变量是否被设置。

变量类型

PHP 是一种松散类型的语言。重要的是不要认为 PHP 变量没有类型。它们肯定会,只是它们可能会在运行时改变类型,不需要在初始化时显式声明它们的类型。

PHP 将隐式地将变量转换为操作所需的数据类型。例如,如果一个操作需要一个数字,比如加法(+)操作,那么 PHP 将把操作数转换成数字格式。

在“转换变量”一节中,将向您介绍类型杂耍,您需要了解 PHP 在改变变量类型时遵循的规则。现在,你只需要知道 PHP 变量有一个类型,这个类型是可以改变的,虽然你可以显式地改变类型,但 PHP 会为你隐式地改变。

PHP 有三类变量——标量变量、复合变量和资源变量。标量变量是一次只能保存一个值的变量。复合变量一次可以包含几个值。

资源变量指向 PHP 本身没有的东西,比如操作系统提供的文件或数据库连接的句柄。这些变量不能强制转换。

最后,PHP 有 null 类型,用于没有设置值的变量。也可以将空值赋给变量,但是在 PHP 7.1 中不能转换为空类型。

标量类型

有四种标量类型:

类型 别名 包含
布尔代数学体系的 bool TrueFalse
整数 int 有符号数字整数
浮动 有符号数字双精度或浮点数据
线 二进制数据的有序集合

有些类型有别名。例如,考虑以下代码,它显示boolboolean的别名:

<?php
$a = (boolean)true;
$b = (bool)true;
var_dump($a === $b);  // bool(true)

PHP 中的字符串不仅仅是一个字符列表。在内部,PHP 字符串包含关于它们长度的信息,并且不是以 null 结尾的。这意味着它们可能包含二进制信息,如从磁盘读取的图像文件。换句话说,PHP 字符串是二进制安全的。

复合类型

有两种复合类型:数组和对象。这些在本书中都有自己的章节。

铸造变量

这是理解 PHP 的一个非常重要的部分,即使是非常有经验的开发人员也可能不知道 PHP 用来转换变量的一些规则。

PHP 隐式地将变量转换为执行操作所需的类型。

也可以使用以下两个选项之一显式转换变量:

  • 使用铸造操作员
  • 使用 PHP 函数

使用转换运算符时,需要将需要转换的数据类型的名称放在变量名前的括号中。例如:

<?php
$a = '123';        // $a is a string
$a = (int)$a;      // $a is now an integer
$a = (bool)$a;     // $a is now Boolean and is true

您可以将变量强制转换为 null,如下例所示,但这种行为在 PHP 7.2 中已被否决,因此即使 PHP 7.1 支持,您也不应该这样做。

<?php
$a = "Hello World";
$a = (unset)$a; // Deprecated in PHP 7.2
var_dump($a);   // NULL

还有一些 PHP 函数可以将变量转换成数据类型。它们以一种自我记录的方式命名:floatvalintvalstrvalboolval

此外,intdiv函数在返回两个整数相除的整数结果时,可能会将双精度值转换为整数。

您还可以对一个变量调用settype函数,该变量将所需的数据类型作为第二个参数。

关于 PHP 中变量的造型,需要记住一些规则。你应该仔细阅读关于杂耍类型 3 的手册页,因为杂耍类型中有许多陷阱。还要确保你阅读的网页链接到从类型杂耍网页。

我不会详尽地列出这些规则,而是将重点放在一些可能违背直觉或者经常出错的规则上。

从 float 到 integer 的转换不会将值向上或向下舍入,而是截断小数部分。

<?php
$a = 1234.56;
echo (int)$a;    // 1234 (not 1235)
$a = -1234.56
echo (int)$a;    // -1234

强制转换为布尔值的一些通用规则如下:

  • 空数组和字符串被强制转换为false
  • 字符串总是评估为布尔值true,除非它们有一个 PHP 认为“空”的值。
  • 如果数字不为零,包含数字的字符串的计算结果为true。回想一下,当对这些字符串调用empty()函数时,它们会返回false
  • 任何非零的整数(或浮点数)都是true,所以负数是true

对象可以定义魔法方法__toString()。如果您希望有一种自定义的方式将对象转换为字符串,则可以重载该方法。我们在“将对象转换为字符串”一节中讨论这个问题。

将字符串转换成数字的结果是 0,除非字符串以有效的数字数据开始(更多细节参见 PHP 手册 4 )。默认情况下,转换数的变量类型将是整数,除非遇到指数或小数点,在这种情况下,它将是浮点数。

以下是显示一些字符串转换的示例脚本:

<?php
$examples = [
    "12 o clock",
    "Half past 12",
    "12.30",
    "7.2e2 minutes after midnight"
];
foreach ($examples as $example) {
    $result = 0 + $example;
    var_dump($result);
}

/*
This outputs:
    int(12)
    int(0)
    double(12.3)
    double(720)

*/

浮点数和整数

在浮点数和整数之间进行转换时要非常小心。PHP 手册 5 中有一个很好的例子,说明了数字类型的内部实现细节会产生反直觉的结果:

<?php
echo (int) ( (0.1+0.7) * 10 ); // 7
echo (int) ( (0.1+0.5) * 10);  // 6

人们可能希望第一个例子显示8,但实际上内部浮点表示比8略少。

当 PHP 将浮点数转换成整数时,它会向零舍入,所以它变成 7。

这背后的原因是一些数在基数为 10 时是有理数,但在基数为 2 时是无理数。虽然 0.7 可以用 10 为基数表示为有理数,但是用 2 为基数表示就是无理数了。由于可用于存储该数字的位数有限,因此不可避免地会损失一些精度。

PHP 整数总是有符号的。整数的取值范围取决于 PHP 运行的系统。

您可以通过查询常量PHP_INT_SIZE在运行时确定整数的字节大小。常数PHP_INT_MAXPHP_INT_MIN将分别给出可以存储在一个整数中的最大值和最小值。其他数值类型也有类似的常数。它们列在 PHP 手册关于保留常量的页面中。 6

Caution

您不应该依赖精确到最后一位的浮点数。

您应该避免直接测试浮点数是否相等,而应该测试它们在给定的精度范围内是否相同,如下例所示:

<?php
$pi = 3.14159625;
$indiana = 3.2;
$epsilon = 0.00001; // degree of error

if(abs($pi - $indiana) < $epsilon) {
    echo "Those values look the same to me";
} else {
    echo "Those values are different";
}

这段代码检查五个精度的值是否相同。这个脚本将输出Those values are different,因为差值大于我们定义的误差程度。

命名变量

PHP 变量以美元符号$开始,PHP 变量名称遵循以下规则:

  • 名称区分大小写
  • 名称可以包含字母、数字和下划线字符
  • 名称不能以数字开头

对于 camelCase、StudlyCase 或 snake_case 的使用,编码约定有所不同,但所有这些格式都是有效的 PHP 变量名格式。

PHP 也允许变量的变量名。下面的例子可以很好地说明这一点:

<?php
$a = 'foo';
$$a = 'bar'; // $a is 'foo', so variable $foo is set
echo $foo;   // bar

PHP 7 将总是严格地从左到右评估访问。旧版本有一套复杂的规则来决定如何评估这种语法。令人高兴的是,PHP 7 更简单和一致,我不会担心解释旧版本。

下面是一个更复杂的例子,说明 PHP 如何从左到右进行计算:

<?php
$a = 'foo';
$$a['bar'] = 'Murky code';
// this assert passes
assert($$a['bar'] === $foo['bar']);
var_dump($foo);

/*
    array(1) {
      ["bar"]=>
      array(1) {
        ["baz"]=>
        string(10) "Murky code"
      }
    }
*/

使用变量变量名有几个注意事项。它们可能会影响你的代码安全性,也会让你的代码看起来有点晦涩。

检查变量是否已设置

如果设置了变量,命令isset()将返回true,否则返回false。最好使用这个函数,而不是检查变量是否为空,因为这不会导致 PHP 生成警告。

如果变量未设置,命令empty()将返回true,并且不会生成警告。这不是测试变量是否已设置的可靠方法。

Note

请记住,字符串“0”被认为是空的,但实际上是已设置的。

当变量超出范围时,变量将被取消设置,您可以使用命令unset()手动清除变量。我们将在本书的后面看到,垃圾收集器负责释放分配给未设置变量的内存。

常数

常数 7 类似于变量但不可变。它们与变量具有相同的命名规则,但是按照惯例将具有大写名称。

它们可以使用define 8 功能来定义,如图所示:

<?php
define('PI', 3.142);
echo PI;
define('UNITS', ['MILES_CONVERSION' => 1.6, 'INCHES_CONVERSION' => '2.54']);
echo "5km in miles is " . 5 * UNITS['MILES_CONVERSION'];
/*
  3.1425km in miles is 8
*/

define 的第三个参数是可选的,它指示常量名是否区分大小写。

你也可以使用const关键字来定义常量。常量只能包含数组或标量值,而不能包含资源或对象。

<?php
const UNITS = ['MILES_CONVERSION' => 1.6,
               'INCHES_CONVERSION' => '2.54'];
echo "5km in miles is " . 5 * UNITS['MILES_CONVERSION'];
/*
  5km in miles is 8
*/

只有const关键字可以用来创建命名空间常量,就像在这个例子中,我们在"Foo"命名空间中创建常量,然后尝试在"Bar"命名空间中引用它们。

<?php
namespace Foo;
const AVOCADO = 6.02214086;
// using define() will generate a warning
define(MOLE, 'hill');

namespace Bar;
echo \Foo\AVOCADO;
// referencing the constant we tried to define() results in a fatal error
echo \Foo\MOLE;

不能将变量赋给常量。

您可以使用静态标量值来定义常数,如下所示:

const STORAGE_PATH = __DIR__ . '/storage';

Note

请注意“神奇”常量__DIR__的使用,它是由 PHP 在运行时设置的,包含脚本在文件系统中驻留的路径。这些常数将在“神奇常数”一节中讨论。

constant()函数 9 用于检索常量的值。

<?php
const MILES_CONVERSION = 1.6;
echo 'There are ' . constant('MILES_CONVERSION') . ' miles in a kilometer';
/*
  There are 1.6 miles in a kilometer
*/

超级全球

PHP 有几个超级全局变量 10 可供脚本自动使用。超全局变量在每个作用域中都可用。

您可以更改超全局变量的值,但是通常建议您将一个局部范围的变量赋给超全局变量并修改它。你需要知道每个超级全球商店。

saberglobal 商店
$GLOBALS 存在于全局范围内的变量数组。
$_SERVER 关于路径、头和其他与服务器环境相关的信息的信息数组。
$_GET GET请求中发送的变量。
$_POST POST请求中发送的变量。
$_FILES 作为POST请求的一部分上传的文件的关联数组。
$_COOKIE 通过 HTTP cookies 传递给当前脚本的变量的关联数组。
$_SESSION 包含当前脚本可用的会话变量的关联数组。
$_REQUEST POSTGETCOOKIE请求变量。
$_ENV 通过 environment 方法传递给当前脚本的变量的关联数组。

超级全局有很多键,你应该熟悉它们。PHP 手册 11 有一个列表,你应该确保你已经阅读了手册页并理解了所有的键。

Tip

注意,$_SERVER['argv']包含发送给脚本的参数,这与$_ENV不同。认证考试需要这种详细程度的知识。

魔法常数

神奇常数是 PHP 自动提供给每个运行脚本的常数。有相当多的保留常数 12 你将需要知道误差常数,以及常用的预定义常数。 13

常数 包含
__LINE__ 正在执行的 PHP 脚本的当前行号
__FILE__ 正在执行的文件的完全解析(包括符号链接)名称和路径
__CLASS__ 正在执行的类的名称
__METHOD__ 正在执行的类方法的名称
__FUNCTION__ 正在执行的函数的名称
__TRAIT__ 代码在其中运行的特征的名称空间和名称
__NAMESPACE__ 当前命名空间

Note

这些神奇常数的值根据您使用它的位置而变化。

经营者

算术

你应该认识算术函数:

操作 例子 描述
添加 1 + 2.3
减法 4 – 5
分开 6 / 7
增加 8 * 9
系数 10 % 11 给出 10 除以 11 的余数
力量 12 ** 13 12 的 13 次方

这些算术运算符有两个参数,因此被称为二进制。

后面的一元运算符只接受一个参数,它们在变量之前或之后的位置会改变它们的工作方式。PHP 中有两种一元运算符,即前缀和后缀。它们是根据运算符出现在它所影响的变量之前还是之后而命名的。

  • 如果操作符出现在变量(前缀)之前,那么解释器将首先对它求值,然后返回改变后的变量。
  • 如果操作符出现在变量(后缀)之后,那么解释器将返回语句执行之前的变量,然后递增变量。

让我们展示它们对变量$a的影响,我们将该变量初始化为 1,然后对其进行操作:

命令 输出 之后$a的值 描述
$a = 1; 1
echo $a++; 1 2 后缀
echo ++$a; 3 3 前缀
echo $a--; 3 2 后缀
echo --$a; 1 1 前缀

逻辑运算符

PHP 同时使用符号和单词形式的逻辑操作符。符号形式运算符是基于 C 的。

操作员 例子 何时为真
and $a and $b $a$b都评价true
and $a && $b
or $a or $b 不是$a就是$b评估true
or $a &#124;&#124; $b
xor $a xor $b $a$b中的一个(但不是两个)为真
xor $a ^ $b
not !$a | $a不是true ( false)

最佳实践是不要在同一个比较中混合使用单词形式(例如,and)和符号(例如,&&),因为运算符的优先级不同。坚持只使用符号形式是最安全的。

在这个例子中,我们看到运算符优先级 14 导致变量$truth$pravda不相同,即使我们执行“相同的”逻辑运算符来派生它们。

这是因为逻辑运算符andor的优先级低于等式运算符=

<?php
$a = true;
$b = false;
$truth = $a and $b;  // true
$pravda = $a && $b;  // false
assert($truth === $pravda);
/*
    Warning: assert(): assert($truth === $pravda) failed
*/

三元运算符

PHP 以与其他 C 祖先语言相同的格式实现三元运算符。一般格式如下:

condition ? expression1 : expression2;

如果condition为真,那么expression1将被求值;否则expression2被评估。

下面是一个例子,它检查isset($a)的条件,并相应地将字符串值'true''false'分配给$b

<?php
$a = 'foo';
$b = (isset($a)) ? 'true' : 'false';
echo $b;    // true

上面的语法与下面的if语句相同:

<?php
$a = 'foo';
if (isset($a)) {
  $b = 'true';
} else {
  $b = 'false';
}
echo $b;    // true

如果三元运算符中省略了 true 值,则语句被计算为表达式,如下所示:

<?php
$a = true;
$b = $a ?: 'foo';
echo $b;   // 1

如果变量存在,这个三进制操作符的缩短版本不适合测试,因为在这种情况下解释器会抛出警告。

零合并算子

零合并运算符只是三元运算符的一个特例。它允许你整理当你使用isset给一个变量赋值时的语法。

<?php
// Long form ternary syntax
$sortDirection = (isset($_GET['sort_dir'])) ? $_GET['sort_dir'] : 'ASC';

// Equivalent syntax using the null coalescing operator
$sortDirection = $_GET['sort_dir'] ?? 'ASC';

// The null-coalesce operator can be chained
$sortDirection = $_GET['sort_dir'] ?? $defaultSortDir ?? 'ASC';

// The Elvis operator raises E_NOTICE if the GET variable is not set
$sortDirection = $_GET['sort_dir'] ?: 'ASC';

最好使用零合并操作符而不是 Elvis,因为如果没有设置变量,零合并操作符不会引起明显的错误。

宇宙飞船

spaceship 操作符用于比较两个不同的值,对于编写排序函数的回调函数特别有用,我们将在后面看到。

当左操作数分别小于、等于或大于右操作数时,返回-101

操作 价值
1 <=> 0 1
1 <=> 1 0
1 <=> 2 -1
'apples' <=> 'Bananas' 1
'Apples' <=> 'bananas' -1

飞船操作符使用标准的 PHP 比较规则。

我们将在后面的“字符串”一节中看到为什么在字符串比较中会有这种惊人的差异。

按位

按位运算符处理以二进制形式表示的整数位。在不同的变量类型上使用它们会导致 PHP 在对变量进行操作之前将其转换为整数。

有三种标准的逻辑位运算符:

操作员 操作 描述
& 按位AND 如果两个操作数位都被设置,则结果将有一个位被设置
&#124; 按位OR 如果一个或两个操作数都设置了位,则结果将设置该位
^ 按位XOR 如果一个且仅一个操作数(不是两个)设置了该位,则结果将设置该位。

按位运算符的结果将是根据这些规则设置了位的值。换句话说,操作数的每个位置中的位是相对于另一个操作数的相同位置中的对应位来评估的。

使用这些运算符时,考虑数字的二进制表示更容易。您可以通过比较位(从右到左)来计算结果的二进制表示,然后在完成后转换为十进制。

我们来看一下50 & 25这个例子。我将二进制表示放在三行的注释中。你可以看到,我通过检查那个位置的位是否在$a$b中被置位,计算出了$c的二进制表示。在这种情况下,两个位置中只有一个这样的位为真。

<?php
$a = 50;        // 0b110010
$b = 25;        // 0b011001
$c = 50 & 25;   // 0b010000
echo $c;        // 16

这里有一个表格格式,可能会更容易理解。我把每个数字的位放在列中。标有“操作”的行显示了发生的比较——对于两个值的每个位置,都应用了逻辑“与”运算符。

值/运算符 每个位置的位数
Fifty one
Twenty-five Zero
操作 1 和 0
结果 Zero

当我们回显结果时。PHP 给我们一个整数值,你可以很快确认你计算的二进制表示与它匹配,因为 2 的 4 次方是 16。

比特移位

PHP 也有运算符来左右移位。这些运算符的作用是将值的位模式向左或向右移位,同时在新创建的空白空间中插入设置为 0 的位。

为了理解这些运算符是如何工作的,想象一下你的数字以二进制形式表示,然后所有的 1 和 0 向左或向右移动。

下表显示了移位,一个向右,一个向左。

操作 操作 导致二进制 结果为十进制
Fifty 00110010
50 >> 1 右移 00011001 Twenty-five
50 << 1 左移位 01100100 One hundred

我在二进制形式中包含了足够多的前导零,以便更容易看出发生了什么。

你可以看到,当我们向右移动时,右边的位“丢失”了。当我们左移时,我们在右边插入设置为 0 的新位。

使用按位运算执行计算时一定要小心,因为整数溢出大小可能会因 PHP 部署的不同环境而异。

例如,尽管在 64 位系统中,两种操作的结果是相同的,但在 32 位整数系统中,它们不会:

<?php
$x = 1;
echo $x << 32;
echo $x * pow(2, 32);

第一行将回显 0,因为左移 32 位将用 0 位填充 32 位整数。第二行将使用数学库并输出 2 的 32 次方的正确值。

Tip

如果您想试验二元运算符,您会发现base_convert()函数非常有用。例如,要输出十进制数50的二进制表示,您可以echo base_convert(50, 10, 2) . PHP_EOL;

按位非

您不需要知道这个运算符背后的数学细节,所以不要花太多时间担心细节。如果你了解它对比特的影响,你应该准备好回答关于它的问题。

PHP 使用(波浪号)符号进行按位非运算。该运算符的作用是翻转值中的位,如果某个位被置位,它将被复位,如果没有被置位,它将被置位。

这一点可以通过例子得到最好的理解:

Fifty one
∼ (不) Zero

结果的值(十进制)是-51。

出于充实的目的,你可以在维基百科上阅读二进制补码。 15 它主要用于得到一个负数的二进制表示。

赋值运算符

PHP 使用符号=作为赋值操作符。下面一行将$a的值设置为123

<?php
$a = 123;

赋值运算符可以与几乎所有的二元运算符和算术运算符结合使用。此语法作为一种快捷方式,通过提供等效语句的示例可以得到最好的展示:

<?php
$a += 345;    // equivalent to $a = $a + 345;
$a .= 'foo';  // equivalent to $a = $a . 'foo';

任何赋值表达式的结果都是赋值后变量的值。

一个相当常见的打字错误是错误地遗忘了等式检查中的第二个=符号。考虑下面的例子,我们在if语句中使用赋值运算符,我们打算在这里使用等式运算符。

<?php
$foo = "hello";
if ($foo = "world") {
  echo "matches";
} else {
  echo "does not match";
}

如果这是一个等式操作符,if语句将为假,脚本将输出“不匹配”。然而,因为我们将字符串“world”赋给了变量$foo,所以结果是值“world”,当转换为布尔值时是true(参见“转换变量”)。

一些编码约定使用所谓的“Yoda 条件”来帮助解决这个错误。它利用了 PHP 不允许您更改常量值的事实。如果你总是把常数放在相等比较的左边,如果你输错了操作符,你会得到警告。代码可读性的代价是否值得是个人风格的问题。

参考运算符

默认情况下,PHP 按值分配所有标量变量。

PHP 进行了优化,使按值赋值比按引用赋值更快(参见“内存管理”一节),但是如果您想按引用赋值,可以使用如下的&操作符:

<?php
$a = 1;
$b = &$a;  // assign by reference
$b += 5;
echo $a;  // 6

PHP 总是通过引用来分配对象;如果您试图通过引用显式地创建它,PHP 将生成一个解析错误。

<?php
class MyClass {}
// Parse error: syntax error, unexpected 'new'
$a = &new MyClass;

比较运算符

PHP 使用以下比较运算符:

操作员 描述
> 大于
>= 大于或等于
< 不到
<= 小于或等于
<> 不平等
== 等价性;如果转换为相同的变量类型,值是等效的
=== 身份;值必须是相同的数据类型,并且具有相同的值
!= 不等价
!== 不相同

理解等价比较和同一性比较之间的区别很重要:

  • 如果操作数可以转换为通用数据类型并具有相同的值,则它们是等效的。
  • 如果操作数共享相同的数据类型并具有相同的值,则它们是相同的。

如果数组具有相同的键和值对,则它们是等效的。如果它们有相同的键和值对,顺序相同,并且键-值类型相同,则它们是相同的。

在数组上使用比较运算符时,它们的键数用于确定哪个更大或更小。

与标量变量相比,对象和数组都被认为比标量大。

<?php
$a = [1];
$b = 100;
echo $a <=> $b; // 1

对字符串使用比较运算符或对不匹配的变量类型使用比较运算符时要小心。有关更多信息,请参见“造型变量”一节。

两个以上的操作员

PHP 提供了一个操作符来抑制错误消息。只有当函数所基于的库使用 PHP 标准错误报告时,这才会起作用。

<?php
// Error messages will be suppressed
$dbConnection = @mysqli_connect(...);

@运算符来抑制 PHP 错误是不好的做法。最好使用 PHP 设置来抑制生产环境中的错误,并允许开发环境显示错误。让代码无声地失败而不产生错误,会让调试变得比它需要的要困难得多。

我们将讨论的最后一个运算符是反勾运算符。不常用,相当于调用shell_exec()命令。在下面的例子中,变量$a将包含运行 PHP 解释器的用户名。

<?php
// This is the equivalent of echo shell_exec('whoami');
echo `whoami`;

在网络环境中,这可能是www-data。这是 Nginx 和 Apache 的缺省设置,但是在命令行中将是登录的用户名。

控制结构

控制结构允许你分析变量,然后为你的程序选择一个方向。在这一节中,我们将会看到几种不同的控制结构,以及它们是如何在 PHP 中实现的。

条件结构

PHP 支持ifelseelseifswitch和三元条件结构。

If结构看起来像这样:

<?php
if (condition) {
  // statements to execute
} elseif (second condition) {
  // statements to execute
} else {
  // statements to execute
}

注意elseifelseif之间的间距是可选的。

If语句可以嵌套。

switch语句看起来像这样:

<?php
switch ($value) {
  case '10' :
    // statements to execute
    break;
  case '20'  :
    // statements to execute
    break;
  case '30'  :
    // statements to execute
    break;
  default:
    // statements to execute
    break;
}

一旦一个case匹配该值,代码块中的语句将被执行,直到它到达一个break命令。

如果您省略了break命令,那么switch中的所有后续语句都将被执行,直到遇到一个断点,即使case与该值不匹配。这在某些情况下很有用,但是如果您忘记使用break语句,也会产生意想不到的结果。

为了说明这一点,请考虑以下示例:

<?php
$value = 10;
switch ($value) {
    case '10' :
        echo "Value is 10";
        // no break statement
    case '20'  :
        echo "Value is 20";
        break;
    case '30'  :
        echo "Value is 30";
        break;
    default:
        echo "Value is not 10,20, or 30";
        break;
}
// Value is 10Value is 20

Note

如果在默认的case之后包含case语句,它们将不会被检查。

PHP 最基本的循环是while循环。它有两种形式,如图所示:

<?php
while (expression) {
  // statements to execute
}

do {
  // statements to execute
} while (expression)

它们之间的区别在于,在第一种形式中,表达式在循环开始时求值,而在第二种形式中,表达式在循环结束时求值。

这意味着如果表达式为假,那么while循环在第一种情况下根本不会运行,但在第二种情况下至少会运行一次。

for循环语法显示了 PHP 的 C 根目录,如下所示:

<?php
for ($i = 0; $i < 10; $i++) {
  // do something
}

与 C 语言一样,执行第一条语句来初始化循环。第二个条件在每个循环开始时计算,最后一条语句在每个循环结束时执行。循环将继续运行,直到条件评估为false

要迭代数组,可以使用foreach,如下所示:

<?php
$arr = [
  'a' => 'one',
  'b' => 'two',
  'c' => 'three'
];

foreach ($arr as $value) {
    echo $value;   // one, two, three
}

foreach ($arr as $key => $value) {
    echo $key;    // a, b, c
    echo $value;  // one, two, three
}

打破循环

在 PHP 中有两种方法可以停止循环的迭代— breakcontinue

使用continue具有停止当前迭代并允许循环处理下一个评估条件的效果。这允许您让任何进一步的迭代发生。

使用break具有停止整个循环的效果,并且不会发生进一步的迭代。

break语句接受一个可选的整数值,该值可用于脱离多层嵌套循环。如果没有指定值,则默认为1

命名空间

名称空间有助于避免库或其他共享代码之间的命名冲突。名称空间将把项目封装在其中,这样它们就不会与在其他地方声明的项目冲突。

它们可用于避免类的过度描述性名称,将库细分为几个部分,或将常量的适用性限制在一个代码部分。

类将代码封装成可实例化的单元。命名空间将函数、类和常数分组到空间中,在空间中它们的名称是唯一的。

名称空间声明必须紧接在开始的<?php标记之后,并且在它之前不能有其他语句。

名称空间影响常量,但是您必须用关键字const而不是define()来声明它们。

一个文件中可以有两个名称空间,但是大多数编码标准都强烈反对这样做。为此,您将命名空间的代码放在大括号中,如下例所示:

<?php
namespace A {
  // this is in namespace A
}
namespace B {
  // this is in namespace B
}
namespace {
  // this is in the global namespace
}

Note

这种用法不是标准做法;在大多数情况下,名称空间声明不包括大括号,文件中的所有语句只存在于一个名称空间中。

完全限定的命名空间名称

如果您在一个名称空间中工作,那么解释器将假定名称是相对于当前名称空间的。考虑将该类作为以下示例的基础:

<?php
namespace MyApp\Helpers;

class Formatters
{
    public static function asCurrency($val) {
        // statement
    }
}

如果我们想从另一个名称空间使用这个类,我们需要提供一个完全限定的名称空间,如下例所示:

<?php
// this file is in a different namespace
namespace MyApp\Lib;

// we must specify the full path to the namespace that the class is in
echo MyApp\Helpers\Formatters::asCurrency(10);

或者,您可以使用use语句导入一个名称空间,这样您就不必一直使用长格式:

<?php
// this file is in a different namespace
namespace MyApp\Lib;

// the "use" keyword imports the namespace
use MyApp\Helpers\Formatters;
// we no longer have to provide a full reference
echo Formatters::asCurrency(10);

您可以在名称前加一个反斜杠,表示您打算使用全局名称空间,如下例所示:

<?php
namespace MyApp;

throw new \Exception('Global namespace');

在这个例子中,如果我们没有用反斜杠指示全局范围,解释器将在MyApp名称空间中寻找一个名为Exception的类。

配置

我强烈建议你做一些实际的工作来配置 PHP。你可以在你的电脑 16 上设置一个虚拟机,在上面安装 Linux 17 ,给你动手体验。

有几个 Windows 和 Mac 包为 PHP 提供了一体化配置,但是您应该确保找到配置文件并浏览它们。

其中可以设置或改变设置

PHP 提供了一种灵活的配置策略,基本配置设置可以被用户配置文件覆盖,甚至在运行时被 PHP 自己覆盖。

这个最好参考手册。在这里复制它只会导致陈旧的信息。请参考以下链接:

菲律宾比索.ini

PHP.ini 文件定义了每个 PHP 环境的配置。这里的环境指的是 PHP 如何运行——例如通过命令 shell,作为 FPM 进程,或者在 Apache2 中作为一个模块。

每个环境在主配置目录下都会有一个目录,在 Ubuntu 上默认是/etc/php/7.0/

Windows 机器使用注册表来存储php.ini的位置。实际的键名在 PHP 的不同版本之间有所不同,但是将遵循类似于下面的模式:HKEY_LOCAL_MACHINE\SOFTWARE\PHP。如果 Windows 计算机在注册表指定的位置找不到该文件,它将返回到许多默认位置查找该文件,包括 Windows 系统目录。

除了php.ini文件之外,还可以指定一个目录,PHP 将扫描该目录中的附加配置文件。您可以使用php_ini_scanned_files()函数获得包含的文件列表,以及包含的顺序。

每当服务器(apache)或进程(fpm/cli)启动时,就会读取配置文件。这意味着如果您对 PHP 配置进行了更改,您将需要重新加载 Apache2 服务器或重启fpm服务。相反,对 CLI 配置的更改将在下次从 shell 运行 PHP 时生效。

可以在 PHP.ini 文件中使用 OS 环境变量,语法如下:

; PHP_MEMORY_LIMIT is taken from environment
memory_limit = ${PHP_MEMORY_LIMIT}

用户 INI 文件

PHP 在 FastCGI 模式下运行时会检查这些文件(PHP 5.3+)。这是在使用fpm模块时的情况,而不是在 CLI 或 Apache2 中。

PHP 将首先在脚本运行的目录中检查这些文件,并回溯到文档根目录。文档根在您的主机文件中配置,并反映在$_SERVER['DOCUMENT_ROOT']变量中。

这些 INI 文件将覆盖php.ini中的设置,但只会影响标记为PHP_INI_PERDIRPHP_INI_USER的设置。请参考前面的链接,查看设置列表以及可以更改的位置。

主配置文件有两个与用户 INI 文件相关的指令。第一个是user_ini_filename,控制 PHP 查找的文件的名称。

第二个是user_cache_ttl,控制从磁盘读取用户文件的频率。

INI 文件的 Apache 版本

如果您正在使用 Apache,那么您可以使用.htaccess来管理用户 INI 设置。它们的搜索方法与fastcgi文件相同。

您必须将您的vhost配置中的AllowOverride设置设为您想要读取.htaccess文件的任何目录中的true

表演

大量 PHP 性能问题与部署环境有关,这超出了本参考的范围。

在 Zend 考试的上下文中值得一提的一个潜在的性能部署问题是在生产中使用xdebug扩展。顾名思义,这个扩展是用于调试的,不应该安装在生产中。

另一个部署问题是保持 PHP 版本最新。PHP 一直在改进它的性能,迁移您的代码以跟上新的 PHP 版本是一个好主意。

Tip

使用最新版本的 PHP 是提高性能的好方法。PHP 7 比 PHP 5 快 30%(在我的测试中),有些人声称它甚至更快。PHP 7.2 比 PHP 7.1 快。

当考虑 Zend 检查的性能时,我们关注内存管理和操作码缓存。

内存管理

在 PHP 中优化内存性能需要对语言的内部数据类型表示的工作原理有所了解。

PHP 使用一个名为zval的容器来存储变量。zval容器包含四条信息:

描述
Value 变量设置为的值。
Type 变量的数据类型。
Is_ref 一个布尔值,指示此变量是否是引用集的一部分。记住变量可以通过引用来赋值。这个布尔值帮助 PHP 决定一个特定的变量是普通变量还是对另一个变量的引用。
Refcount 这是一个跟踪有多少变量名指向这个特定的zval容器的计数器。当我们声明一个新变量来引用这个变量时,这个refcount就会增加。

变量名被称为符号,存储在一个符号表中,该符号表对于变量出现的范围是唯一的。

符号指向 zval 容器

在关于引用操作符的那一节,我提到 PHP 对按值赋值进行了优化。PHP 实现这一点的方法是,当值发生变化时,只将它复制到一个新的 zval,并在开始时将新的符号指向同一个 zval 容器。这种机制称为“写入时复制”。 18

这里有一个例子来说明:

<?php
$a = "new string";
$b =& $a;
// the variable b points to the variable a
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'b' );
// change the string and see that the refcount is reset
$b = 'changed string';
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'b' );

该脚本的输出如下:

a: (refcount=2, is_ref=0)='new string'
b: (refcount=2, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'
b: (refcount=1, is_ref=0)='changed string'

我们可以看到,在我们改变$b的值之前,它引用的是与$a相同的zval容器。

数组和对象

数组和对象使用它们自己的符号表,与标量变量分开。他们也使用zval容器,但是创建一个数组或对象会导致几个容器被创建。

考虑这个例子:

<?php
$arr = ['name' => 'Bob', 'age' => 23 ];
xdebug_debug_zval( 'arr' );

该脚本的输出如下所示:

arr: (refcount=1, is_ref=0)=array (
        'name' => (refcount=1, is_ref=0)='Bob',
        'age' => (refcount=1, is_ref=0)=23)

我们可以看到创建了三个zval容器,一个用于数组变量本身,一个用于它的两个值。

就像标量变量一样,如果数组中的第三个成员与另一个成员具有相同的值,那么 PHP 不会创建新的zval容器,而是增加 refcount 并将重复的符号指向同一个zval

数组和对象中的内存泄漏

当复合对象包含对自身作为成员的引用时,可能会发生内存泄漏。这更有可能发生在有对象的用例中,因为 PHP 总是通过引用来分配对象。例如,可能在诸如 ORM 模型中可能发现的父子关系中。

PHP 手册在引用计数基础页面上有一系列解释这一点的图表。 19 当你取消设置一个引用自身的复合对象时,问题就出现了。

在这种情况下,符号表中清除了对用于包含变量的zval结构的引用。PHP 不会遍历复合对象,因为这会导致递归,因为它会跟踪指向自身的链接。这意味着变量中作为自身引用的成员不会被取消设置,并且zval容器不会被标记为空闲。没有符号指向这个zval容器,所以用户不能自己释放内存。

PHP 将在请求结束时清除这些引用。请记住,PHP 并不打算成为一种长期运行的语言,而是被设计成一种为 web 应用上下文中的特定请求服务的文本处理器。

碎片帐集

垃圾收集器清除循环引用,即复杂对象包含引用自身的成员的引用。

当根缓冲区已满或函数gc_collect_cycles()被调用时,PHP 将启动垃圾收集。

垃圾收集器只会在实际需要做一些事情的时候导致速度变慢。在没有泄漏的较小脚本中,这不会导致性能下降。

垃圾收集在长时间运行的脚本中或者在那些重复产生内存泄漏的脚本中可能是有益的,例如使用泄漏的 ORM 模型处理大量的数据库记录。

操作码缓存

PHP 被编译成一系列中间指令,由运行时引擎按顺序执行。这些指令被称为操作码或字节码,每次脚本运行时都会发生这个过程。

字节码由运行时引擎解释;因此,PHP 既是预编译的,也是解释的。

操作码缓存为脚本存储转换后的指令。对脚本的后续调用不需要在运行之前解释脚本。

2013 年,Zend 将他们的优化引擎贡献给了 PHP。它被称为 opcache,是 PHP 5.5 版本的发行版中的一部分,可能是最常用的 PHP 操作码缓存。

Note

Opcache 内置于 PHP 7.1 中,在您的php.ini 20 设置中默认启用。

注意设置opcache.revalidate_freq。这决定了 PHP 在重新编译源文件之前扫描源文件变化的时间间隔(秒)。您可以将它设置为0来告诉 PHP 总是扫描变化。PHP 不会对每个请求扫描文件超过一次。

除了 PHP 内置的缓存,还有许多第三方操作码缓存可用(如果你感兴趣,请参见维基百科 21 )。

Tip

使用操作码缓存可以显著提高性能。

扩展ˌ扩张

PHP 扩展扩展了核心语言提供的功能。默认情况下,它们中的许多都被启用到 PHP 的标准存储库发行版中。其他扩展需要手动下载安装。

PECL 是 PHP 扩展库。它提供了一种在 Linux 上下载和安装扩展的简单方法。Windows 机器需要手动编译和安装扩展,但是通常它们是以编译的形式发布的,你只需要编辑你的 INI 文件来启用它们。

PHP 包括几个不能用编译标志从 PHP 中删除的扩展。这些扩展包括反射、数组、日期和时间、SPL 和数学等核心功能。你应该可以相信他们正在安装。

安装扩展

通过使用“扩展名”设置来指定扩展名的文件名,可以通过php.ini文件启用扩展名,就像下面的mcrypt一样:

extension=mcrypt.so;

您可以使用您的php.ini文件中的设置来设置扩展目录,如下所示:

extension_dir = "/usr/lib/php5/20121212/mcrypt.so"

不同的系统可以提供安装和启用扩展的便利方式。可以使用pecl命令行实用程序安装 PECL 扩展。

检查已安装的扩展

如果您调用phpinfo()或使用更具体的命令get_loaded_extensions(),将显示已安装的扩展。

从 shell 运行php –m将显示已安装的扩展列表。

您可以通过调用extension_loaded()来检查是否加载了扩展。如果您在扩展中使用默认情况下不加载的函数,建议您这样做。下面是 PHP 手册中的一个例子:

<?php
if (!extension_loaded('gd')) {
    if (!dl('gd.so')) {
        exit;
    }
}

Chapter 1 Quiz

Q1:在 HTML 中包含 PHP 时,你应该避免使用以下哪个标签?

| <?php |
| <? |
| <?= |
| 以上都不是;这些都很好 |

Q2:在 PHP 中,下列哪一项不区分大小写?选择所有适用的选项。

| 变量名 |
| 类名 |
| 命名空间 |
| 功能名称 |

Q3:这个脚本会输出什么?

| 没什么;它不会跑 |
| Hello world |
| Hello |
| 因变量b未定义而产生的错误消息 |
| 一条错误消息和单词"Hello" |

<?php
$a = "Hello";
$B = " world";
ECHO $a . $b;

Q4:这个脚本会输出什么?

| Exception caught in A |
| Error caught in global scope: Call to undefined function C() |
| Error caught in global scope: Call to undefined function b() |
| 以上都不是 |

<?php

function A() {
    try {
        b();
    } catch (Exception $e) {
        echo "Exception caught in " . __CLASS__;
    }
}

function B() {
    C();
}

try {
    A();
} catch (Error $e) {
    echo "Error caught in global scope: " . $e->getMessage();
}

Q5:这个脚本会输出什么?

| Exception caught in A |
| Error caught in global scope: Call to undefined function C() |
| 1 |
| Error caught in global scope: Call to undefined function b() |
| 以上都不是 |

<?php

function A() {
    try {
        b();
    } catch (Exception $e) {
        echo "Exception caught in " . __CLASS__;
    }
}

function B() {
    echo 5 / "five";
}

try {
    A();
} catch (Error $e) {
    echo "Error caught in global scope: " . $e->getMessage();
}

Q6:这个脚本会输出什么?

| Caught Exception: ChildException |
| Caught MyException: ChildException |
| Caught MyException: MyException |
| 没有任何东西 |
| 与未捕获的异常相关的错误消息 |

<?php

class MyException extends Exception {}
class ChildException extends MyException {}

try {
    throw new ChildException;
} catch (Exception $e) {
    echo "Caught Exception: " . get_class($e);
} catch (MyException $e) {
    echo "Caught MyException" . get_class($e);
}

Q7:以下哪些设置可以在运行时使用ini_set()功能进行配置?

| output_buffering |
| memory_limit |
| max_execution_time |
| extension |

Q8:这个脚本的输出是什么?

| -1 |
| 0 |
| 1 |
| 10 |
| apples |

<?php

$a = "apples" <=> "bananas";
$b = $a ?? $c ?? 10;
echo $b;

Q9:这个脚本的输出是什么?

| -1 |
| 0 |
| 1 |
| 10 |

<?php
echo 10 <=> 10 << 1;

Q10:这个脚本的输出是什么?

| 这将产生一个错误,因为常量只能保存标量值。 |
| 这将产生一个错误,因为您不能使用define()来声明一个数组常量。 |
| 这将产生错误,因为在声明常数时不能使用表达式或函数。 |
| 1 |
| 2 |
| 4 |

<?php
define('A', 1);
const B = 2;
define('C', [A * A, B * B]);
echo(C[1]);

Footnotes 1

https://php.net/manual/en/reserved.php

2

https://www.phpdoc.org/

3

https://php.net/manual/en/language.types.type-juggling.php

4

https://secure.php.net/manual/en/language.types.string.php#language.types.string.conversion

5

https://secure.php.net/manual/en/language.types.integer.php

6

https://php.net/manual/en/reserved.constants.php

7

https://php.net/manual/en/language.constants.syntax.php

8

https://php.net/manual/en/function.define.php

9

https://php.net/manual/en/function.constant.php

10

https://php.net/manual/en/language.variables.superglobals.php

11

https://php.net/manual/en/reserved.variables.server.php

12

https://secure.php.net/manual/en/reserved.constants.php

13

https://secure.php.net/manual/en/language.constants.predefined.php

14

https://php.net/manual/en/language.operators.precedence.php

15

https://en.wikipedia.org/wiki/Two%27s_complement

16

http://www.oracle.com/technetwork/server-storage/virtualbox/downloads/index.html

17

https://www.ubuntu.com/download/server

18

https://en.wikipedia.org/wiki/Copy-on-write

19

https://php.net/manual/en/features.gc.refcounting-basics.php

20

https://github.com/php/php-src/blob/master/php.ini-production#L1763

21

https://en.wikipedia.org/wiki/List_of_PHP_accelerators

二、函数

函数是可以用来执行一系列指令的代码包。任何有效的代码都可以在函数内部使用,包括对其他函数和类的调用。

在 PHP 中,函数名不区分大小写,可以在定义之前引用,除非在条件代码块中定义。函数可以是内置的、由扩展提供的或用户定义的。函数不同于语言结构。

争论

函数的自变量,也称为参数,允许您将值传递到函数范围内。参数以逗号分隔的列表形式传递,并从左到右进行计算。

参数类型声明

您可以指定可以作为参数传递的变量类型。

这很有用,因为 PHP 是一种松散类型的语言,如果你精确地指定你期望的变量类型,那么你的函数将会更可靠,你的代码也更容易阅读。另一个优点是给出类型提示有助于 IDE 给出更有意义的提示。

如果您的函数被调用,并且传递的变量是不正确的类型,那么 PHP 7 将抛出一个TypeError异常。

要指定预期的参数类型,可以在参数定义前添加类型名称,如下所示:

<?php

// $itemName must be a string and $details must be an array
function addToShoppingCart(string $itemName, array $details) {}

/*
$paymentObject must be an object that either:
implements the PaymentProviderInterface interface,
or is any child of a class that does
*/
function requestPayment(PaymentProviderInterface $paymentObject) {}

/*
$employee must be an object that is either:
an instance of the Employee class,
or is any child of a class that does
*/
function calculateWage(Employee $employee) {}

// $callback must be a callable
function performCalculation(callable $method) {}

在前一个例子中,我已经展示了我们可以告诉 PHP 期待标量变量、复合变量(数组和对象)和可调用变量。我们将在本章的后面讨论什么是可调用的。

下表总结了可以声明的类型。

类型 描述
类名或接口 参数必须是指定类或接口的实例或子级。
self 参数必须是当前类的实例。
array
bool
float
int
string
iterable 该参数必须是数组或instanceof可遍历的。
callable 该参数必须是有效的可调用参数。

Note

当我说“祖先”类时,我指的是你的类的任何超类:父类、父类的父类等等。同样,我用“孩子”这个词来表示孩子、孙子、曾孙等等。

不能使用类型别名。例如,不能用boolean代替bool作为类型声明;如果你这样做,PHP 将期待一个名为boolean的类的实例,如下所示:

<?php
function A(boolean $a) {var_dump($a);}
A(true);
// Fatal error: Uncaught TypeError: Argument 1 passed to A() must be an instance of boolean, boolean given,

有两种方法可以强制标量类型提示:强制(默认)和严格。

您可以通过在文件顶部放置一个declare指令来配置每个文件的模式。这将影响 PHP 强制函数参数和函数返回类型的方式。

Note

按文件设置strict模式。

在强制模式下,PHP 会自动尝试将错误类型的变量强制转换为期望的类型。

在下面的例子中,脚本输出了"string",因为 PHP 将我们传递的整数静默地转换为一个字符串。

<?php
function sayHello(string $name) {
    echo gettype($name);
}

sayHello(100);  // string

然而,如果我们要指定严格模式,那么 PHP 将生成一个TypeError,如下例所示:

<?php
declare(strict_types=1);
function sayHello(string $name) {
    echo gettype($name);
}

sayHello(100);
/*
Fatal error: Uncaught TypeError: Argument 1 passed to sayHello() must be of the type string, integer given,
*/

替代空类型语法

PHP 7.1 引入了一种新的方法来输入可能为空的提示变量。可以用问号作为类型提示的前缀,以指示变量可以是 null 或指定的类型。这里有一个例子:

<?php

function myFunc(?MyObject $myObj)
{
    echo "hello world";
}
// this is allowed
myFunc(null);
// this produces a fatal error: Too few arguments
myFunc();

Note

该参数不是可选的;你必须显式地传递null或者一个指定类型的对象。

可选参数

您可以为参数指定默认值,使其成为可选参数。

Note

如果你没有给一个函数提供所有的强制参数,PHP 7 将抛出一个ArgumentCountError 1 。您只能省略可选的传递参数。

在下面的例子中,如果用户没有提供消息,该函数假定它将是world

<?php
function sayHi($message = 'world') {
    echo "Hello $message";
}
sayHi();

重载函数

在其他编程语言中,重载通常是指用相同的名称声明多个函数,但参数的数量和类型不同。PHP 认为重载提供了动态“创建”属性和方法的手段。

PHP 不会让你重新声明同一个函数名。然而,PHP 允许你用不同的参数调用一个函数,并提供了一些函数来访问调用函数的参数。

以下是其中的三个函数:

函数 返回
func_num_args() 向该函数传递了多少个参数
func_get_arg($num) | 参数号$num(从零开始)
func_get_args() 所有参数作为数组传递给函数

下面的示例展示了函数如何接受任意数量的任意类型的参数,以及如何访问这些参数:

<?php
function myFunc() {
    foreach(func_get_args() as $arg => $value) {
        echo "$arg is $value" . PHP_EOL;
    }
}
myFunc('variable', 3, 'parameters');
/*
0 is variable
1 is 3
2 is parameters
*/

下面的代码说明了 PHP 7 和 PHP 5 之间一个模糊的区别:

<?php
function myFunc($data) {
    $data = 'Changed';
    echo func_get_arg(0);
}

在 PHP 5 中,这输出Variable,但是在 PHP 7 中,它输出Changed。这表明,在 PHP 7 中,如果你改变函数中参数的值,那么func_get_arg()返回的值将是新值,而不是原始值。

变量学

PHP 5.6 引入了显式接受可变数量参数的变量。通过在参数列表中使用...标记,可以指定函数将接受可变数量的参数。

可变参数作为数组在函数中可用。

如果将普通固定参数与可变语法混合使用,那么可变参数必须是参数列表中的最后一个参数。

PHP 手册 2 有一个非常清晰的例子,展示了强制参数、可选参数和可变参数之间的交互:

<?php
function parameterTypeExample($required, $optional = null, ...$variadicParams) {
    printf('Required: %d; Optional: %d; number of variadic parameters: %d'."\n",
        $required, $optional, count($variadicParams));
}

f(1);
f(1, 2);
f(1, 2, 3);
f(1, 2, 3, 4);
f(1, 2, 3, 4, 5);

这将输出:

$req: 1; $opt: 0; number of params: 0
$req: 1; $opt: 2; number of params: 0
$req: 1; $opt: 2; number of params: 1
$req: 1; $opt: 2; number of params: 2
$req: 1; $opt: 2; number of params: 3

注意,可变参数是作为普通数组$params提供的。

参考

默认情况下,PHP 通过值向函数传递参数,但也可以通过引用传递参数。您可以通过将参数声明为按引用传递来实现这一点,如下例所示:

<?php
function addOne(&$arg) {
    $arg++;
}
$a = 0;
addOne($a);
echo $a; // 1

&操作符将参数标记为通过引用传递。对函数中该参数的更改将会改变传递给它的变量。

如果函数参数没有被定义为引用,那么您就不能在该参数中传递引用。

此代码将生成一个致命错误:

<?php
function addOne($arg) {
  $arg++;
}
$a = 0;
addOne(&$a); // fatal error as of PHP 5.4.0
echo $a;

可变函数

变量函数在概念上类似于变量变量名。它们最容易用一个语法例子来解释:

<?php
function foo() {
    echo 'Foo';
}
$var = 'foo';
$var(); // calls foo()

我们可以看到,如果 PHP 遇到一个后面有括号的变量名,它就会对该变量求值。然后,它会查找名称与该评估匹配的函数。如果找到匹配的函数,就执行它;否则,将会产生正常的错误。

Note

我们前面看到的语言结构不是函数。您不能将它们用作变量函数。

你可以调用任何可调用的变量函数。稍后我们将在“可调用、Lambdas 和闭包”一节中讨论可调用。

返回

使用return语句将会阻止更多的代码在你的函数中执行。如果你从根作用域中return,那么你的程序将终止。

<?php
return "hello";
echo "This is never run";

如果没有使用关键字return为函数指定返回值,PHP 将返回NULL

在“生成器”部分,我们处理关键字yield。这些与returns非常相似,可以在这里顺便提及,但也非常重要,可以在后面有自己的章节。

生成器允许您编写一个函数,该函数将生成一个数组的连续成员,您可以迭代该数组,而无需将整个数据集保存在内存中。在生成的值列表的末尾,生成器可以选择返回一个最终值。

在 PHP 7 中,我们可以指定我们期望返回什么类型的变量。我们将在下一节讨论这一点。

返回类型声明

我们之前看了如何声明函数参数的变量类型。您还可以指定函数将返回的变量类型。

为此,在参数大括号后放置一个冒号和类型名。返回类型的类型与为参数指定的类型相同。

让我们看一个例子:

<?php
function getFullName(string $firstName, string $lastName): string {
    return 123;
}
$name = getFullName('Mary', 'Moon');
echo gettype($name);  // string

因为 PHP 默认处于强制模式,所以当它从函数返回时,会将整数123转换为字符串。如果我们声明了strict模式,那么 PHP 将生成一个TypeError,就像我们在看参数类型声明时一样。

Note

不能只为一个返回或参数类型声明指定strict模式。如果指定strict模式,两者都会影响。

返回无效

如果函数将返回 null,您可以指定它将返回"void" (PHP 7.1+),如下例所示:

<?php
function sayHello(): void {
    echo "Hello World";
}
// Hello World
sayWorld();

Caution

试图指定它将返回null将导致致命错误。

通过引用返回

可以声明一个函数,使它返回一个变量的引用,而不是变量的副本。PHP 手册指出,你不应该这样做作为一个性能优化,而是只有当你有一个有效的技术理由这样做。

要将一个函数声明为通过引用返回,需要在它的名字前面放置一个&操作符:

<?php
function &getValue() {...}

然后,在调用该函数时,您还将&操作符放在调用的前面:

<?php
$myValue = &getValue();

在这个调用之后,$myValue变量将包含对getValue()函数返回的变量的引用。

Note

注意通过引用返回(这是允许的)和在运行时通过引用传递参数(这是不允许的)之间的区别。

函数本身必须返回一个变量。例如,如果您试图返回一个类似于1的数字文字,将会产生一个运行时错误。

这方面的两个用例是工厂模式和获取资源,如文件句柄或数据库连接。

函数中的可变范围

和其他语言一样,PHP 变量的范围是定义它的上下文。PHP 有三个层次的作用域——全局、函数和类。每次调用函数时,都会创建一个新的函数范围。

有两种方法可以将全局范围变量包含到函数中:

<?php
$glob = "Global variable";
function myFunction() {
    global $glob; // first method
    $glob = $GLOBALS['glob']; // second method
    $glob = "Changed";
}
myFunction();
echo $glob;  // Changed

注意,这两种方法的效果是一样的,都允许你在myFunction()中使用$glob变量,并让它引用在全局范围中声明的$glob变量。

Caution

大多数编码标准强烈反对全局变量,因为它们会在编写测试时引入问题,会引入奇怪的上下文问题,并使调试更加困难。

λ和闭包

PHP 中的 lambda 是一个匿名函数,可以存储为变量。

<?php
$lambda = function($a, $b) {
    echo $a + $b;
};
echo gettype($lambda); // true
echo (int)is_callable($lambda); // 1
echo get_class($lambda); // Closure

我们可以看到,在 PHP 中,lambdas 和闭包是作为从Closure 3 类实例化的对象来实现的。

Lambda 变量和闭包都可以用在接受可调用的函数中。

Note

你可以使用is_callable()函数来检查一个变量是否是可调用的。

PHP 中的闭包是一个匿名函数,它封装了变量,这样一旦变量的原始引用超出范围,就可以使用它们。另一种说法是,匿名函数“封闭”了其定义范围内的变量。

在 PHP 的实际语法中,我们这样定义闭包:

<?php
$string = "Hello World!";
$closure = function() use ($string) {
  echo $string;
};
$closure();

这看起来与 lambda 几乎相同,但是请注意就在代码块开始之前出现的use ($string)语法。

这样做的效果是获取存在于闭包相同范围内的$string变量,并使其在闭包内可用。

Note

您可以使用用于变量函数的语法来调用 lambdas 和闭包。

在这个 lambda 示例中,函数只能访问传递给它的参数,而不会传递来自包含范围的任何内容。调用echo $string会导致警告,因为变量不存在。

早期和晚期绑定

变量有两种绑定方式:早期和晚期。

在早期绑定中,在运行时使用变量之前,我们知道变量的值和类型。这通常是以某种静态声明的方式完成的。参数中使用的变量值将是定义闭包时的值。

相比之下,当我们使用后期绑定时,我们不知道变量的类型或值是什么,直到我们调用闭包。当需要对变量进行操作时,PHP 会将变量强制转换为特定的类型和值。

当将变量绑定到闭包时,PHP 将默认使用早期绑定。如果要使用后期绑定,应该在导入时使用引用。

当您浏览一个简单的示例时,这一切会变得更加清晰:

<?php
$a = "some string";
// early binding (default)
$b = function() use ($a) {
    echo $a;
};
$a = "Hello World";
// some string
$b();

这里我们使用默认(早期)绑定方法将$a的值绑定到 lambda $b

当我们定义λ时,$a的值是“某个字符串”。因此,当我们调用 lambda 时,值"some string"被输出,即使我们在声明 lambda 后改变了$a的值。

如果我们指定将$a用作参考,那么输出将是"Hello World",如下所示:

<?php
$a = "some string";
// late binding (reference)
$b = function() use (&$a) {
    echo $a;
};
$a = "Hello World";
// Hello World
$b();

将闭包绑定到作用域

当您创建一个闭包时,它“封闭”了当前的作用域,因此可以认为它被绑定到了一个特定的作用域。Closure类有两个方法——bindbindTo——它们允许你改变变量绑定的范围:

<?php
class Animal {
    public function getClosure() {
        $boundVariable = 'Animal';
        return function() use ($boundVariable) {
            return $this->nature . ' ' . $boundVariable;
        };
    }
}
class Cat extends Animal {
    protected $nature = 'Awesome';
}
class Dog extends Animal {
    protected $nature = 'Friendly';
}
$cat = new Cat;
$closure = $cat->getClosure();
echo $closure(); // Awesome Animal
$closure = $closure->bindTo(new Dog);
echo $closure(); // Friendly Animal

这段代码中有两件重要的事情需要注意。

首先,将闭包绑定到不同的对象会返回原对象的副本,因此您必须将调用bindTo()的结果赋给一个变量。

第二,新的闭包会有相同的绑定变量和主体,但是有不同的绑定对象和范围。在前面的例子中,当我们绑定到新对象时,$boundVariable被复制到新的闭包中。

自动执行闭包

您可以使用与 JavaScript 非常相似的语法在 PHP 7 中创建自执行匿名函数:

<?php
(function() {
    echo 'Self-executing anonymous function';
    echo $definedInClosure = 'Variable set';
})();
var_dump(isset($definedInClosure));  // bool(false)

注意,在这个例子中,我们在闭包内部定义的变量没有在闭包的范围之外定义。您可以使用这种结构来避免污染您的全局范围。

可召回商品

PHP 5.4 引入了可调用函数作为函数的类型提示

它们是一些函数接受的回调,例如usort()

函数(如usort())的可调用函数可以是以下之一:

  • 内联匿名函数
  • lambda 或闭包变量
  • 表示 PHP 函数(但不是语言结构)的字符串
  • 表示用户定义函数的字符串
  • 一个数组,在第一个元素中包含对象的实例,在第二个元素中包含要调用的函数的字符串名称
  • 包含类中静态方法名称的字符串(PHP 5.2.3+)

Note

你不能使用语言构造作为可调用的。

在 PHP 手册的可调用页面中有所有这些的例子。 4

Chapter 2 Quiz

Q1:下面这段代码的输出是什么?

| Int |
| Float |
| Fatal error: Uncaught TypeError |

<?php
declare(strict_types=1);
function multiply(float $a, float $b): int {
    return $a * $b;
}
$six = multiply(2, 3);
echo gettype($six);

Q2:有些 PHP 函数,比如echo,在调用它们时不需要使用括号。这是真的吗?

| 是的,因为你可以这样称呼它:echo "hello"; |
| 是的,因为echo是个特例。 |
| 不,因为echo是一个语言结构,而不是一个函数。所有的 PHP 函数都要求你在调用它们的时候使用括号。 |
| 不会,因为所有的 PHP 函数都要求你在调用它们的时候使用括号,除了echo,它只在你使用不止一个参数的时候需要括号。 |

Q3:不能使用empty()作为usort()函数的回调。

| True |
| False |

Q4:以下代码的输出是什么?

| Nothing |
| Hello World |
| 错误信息和"Hello World" |
| 只是一条错误消息 |

<?php
(function Hello() {
    echo "Hello World!";
})();

Q5:以下代码的输出是什么?

| int |
| double |
| float |
| 这将生成一个TypeError |

<?php
declare(strict_types=1);
function multiply(float $a, float $b): float {
    return (double)$a * (double)$b;
}
$six = multiply(2, 3);
echo gettype($six);

Q6:以下代码的输出是什么?

| 1 |
| 2 |
| 4 |
| 这会产生一个通知错误 |

<?php
function complicated($compulsory, ...$extras) {
    echo "I have " . func_get_args() . " arguments";
}
complicated(1,2,3,4);

Q7:在下面的函数中,如何引用值为cat的参数?

| $animal |
| $extras[1] |
| $extras[2] |
| 这会产生一个错误 |

<?php
function complicated($compulsory, ...$extras, $animal) {
    // I want to reference the variable with the value "cat"
}
complicated(1,2,3,"cat");

问题 8:这段代码会输出什么?

| 你好 |
| 世界! |
| 你好世界! |
| 这会产生一个错误 |

<?php
if (!is_callable(function(){echo "Hello";})) {
    function sayHello() {
        echo "World!";
    }
}
sayHello();

问题 9:这段代码会输出什么?

| A |
| B |
| C |
| 这会产生一个错误;函数不能有命名空间 |

<?php
namespace A;
function Hello() { echo __NAMESPACE__; }
namespace B;
function Hello() { echo __NAMESPACE__; }
namespace C;
\B\Hello();

Q10:这段代码会输出什么?

| A |
| B |
| C |
| 这会产生一个错误;闭包没有在命名空间 C 中定义 |
| 这会产生一个错误;函数和闭包不能有命名空间 |

<?php
namespace A;
$closure = function() { echo __NAMESPACE__; };
namespace B;
$closure = function() { echo __NAMESPACE__; };
namespace C;
$closure();

Footnotes 1

我们在关于错误处理的第十一章中处理这类错误。

2

https://secure.php.net/manual/en/migration56.new-features.php

3

https://php.net/manual/en/class.closure.php

4

https://php.net/manual/en/language.types.callable.php

三、字符串和模式

PHP 字符串是一系列字节,不包含任何关于如何将这些字节转换成字符的信息。

PHP 存储字符串的长度及其内容,并且不依赖终止字符来表示字符串的结尾。这有助于使字符串二进制安全,因为字符串中的空字符不会引起混淆。

在 32 位系统上,一个字符串可以长达 2 GB。在 64 位 PHP 系统中,字符串的长度没有特别的限制。

声明字符串

在 PHP 中,字符串可以声明为简单类型或复杂类型。不同之处在于,复杂字符串将根据控制字符和变量进行计算。

简单字符串在'single quote marks'中声明,而复杂字符串在"double quote marks"中声明。

在这个例子中,换行符在Hello Bob之后输出,但是在简单字符串中,输出的是文字字符。

<?php
$name = 'Bob';
$a = 'Hello $name\n';
$b = "Hello $name\n";
echo $a;       // Hello $name\n
echo $b;       // Hello Bob

还要注意,变量$name被评估为字符串"Bob",并在输出时被插入到复杂变量$b中。我们将在下一节更详细地讨论这个问题。

嵌入变量

复杂字符串的主要优点之一是 PHP 将解析它们并自动计算其中包含的变量名。

当使用不计算的简单字符串时,您需要终止字符串并将变量连接到它。

在 PHP 中,变量名由一个$标记。当解析器在字符串中遇到一个时,它会尝试通过添加尽可能多的字母数字字符来形成一个有效的变量名。

以下示例说明了将变量连接到字符串和将它们嵌入复杂字符串之间的区别。

<?php
$catfood = "Cheeseburgers";
echo 'I can haz $catfood';         // I can haz $catfood
echo 'I can haz ' . $catfood;      // I can haz Cheeseburgers?
echo "I can haz $catfood?";        // I can haz Cheeseburgers?

注意,第一个字符串是用单引号标记的,所以$catfood不是一个变量。而是作为文字字符输出。要在简单的字符串中包含变量,您需要将它们连接起来,如第二个示例所示。

第三个echo语句显示了一个在复杂字符串中计算变量名的例子。解析器遇到$符号,然后获取它后面所有合法的变量名字符。问号符号不允许出现在变量名中,所以 PHP 将变量$catfood的文字值插入到字符串中。

也可以用双引号包含数组和对象语法:

<?php
$dogfood = ['Pellets'];
$catfood = new stdClass();
$catfood->favorite = "Cheeseburger";
echo "$dogfood[0]";             // Pellets
echo "$catfood->favorite";      // Cheeseburger

PHP 允许使用花括号来明确告诉解析器必须对字符串的一部分求值。

例如,当输出数组中的元素时,可能无法立即确定方括号是作为字符串中的标点符号还是作为引用数组中元素的语法,这是必要的。

让我们看一些它的用法的例子:

<?php
$burger = "Cheeseburger";
echo "I can haz {$burger}";         // I can haz Cheeseburger
echo "I can haz ${burger}";         // I can haz Cheeseburger
echo "I can haz $burgers";          // no variable $burgers
echo "I can haz {$burger}s";        // I can haz Cheeseburgers
echo "I can haz { $burger }";       // I can haz { Cheeseburger }

请注意,在大括号和要计算的变量之间不能使用空格。因为大括号明确表示字符串中变量的结尾,所以可以包含紧跟其后的字符。在之前的例子中,我们看到"{$burger}s"被渲染为芝士汉堡。

让我们看一个混合了数组和对象属性语法的例子,来演示花括号是如何起作用的:

<?php
$catfood = new stdClass();
$catfood->name = "Cheeseburgers";
$cat = new stdClass();
$cat->canhaz = [$catfood];
echo "$cat->canhaz[0]->name";       // array to string conversion
echo "{$cat->canhaz[0]->name}";     // Cheeseburgers

控制字符

当 PHP 遇到一个复杂的字符串,一个它用双引号声明的字符串,它将计算它的变量和控制字符。

控制字符由代码后面的反斜杠标记。使用反斜杠后跟除控制字符以外的任何字符将导致显示反斜杠。

<?php
echo "Hello \World"; // Hello \World

关于转义序列 1 的 PHP 手册页有一个可以使用的控制字符列表,但这里是以表格的形式:

顺序 意义
\n 换行
\r 回车
\t 标签
\v 垂直标签
\e 逃跑
\f 换页
\\ 反斜线符号
`| 顺序 意义
--- ---
\n 换行
\r 回车
\t 标签
\v 垂直标签
\e 逃跑
\f 换页
\\ 反斜线符号
美元符号
[0-7]{1,3} 匹配这个正则表达式的序列用八进制表示
\x[0-9A-Fa-f]{1,2} 匹配序列采用十六进制表示法
\u{{0-9a-f}{1,6}} 匹配序列是一个 Unicode 码点,它将作为码点 UTF-8 表示输出到字符串

表情符号有 Unicode 端点,所以我们可以像这样输出大象:

<?php

echo "\u{1F418}";  // A456636_1_En_3_Figa_HTML.gif

当然,Unicode 的一个更正式的用例是国际化(i18n)。稍后我们将了解更多相关信息。

Heredoc 和 Nowdoc

heredoc 是声明跨越多行的字符串的一种便捷方式。您可以用一种简单的格式声明字符串,而不必添加多个换行符。

Heredoc 字符串被评估为控制字符和变量,就像双引号字符串一样。

heredoc 的常见用途包括创建 SQL 查询,或者为电子邮件或网页创建格式化的 HTML 片段。你也可以用它们来初始化变量,或者任何你想使用跨多行字符串的地方。

Nowdoc 是在 PHP 5.3.0 中引入的,它将单引号字符串转化为双引号字符串。换句话说,不对 nowdocs 进行特殊字符和变量的计算。

这里文档使用如下语法:

<?php
echo <<<HEREDOC
  This is a heredoc string, note:
  1) the capitalization of the tag
  2) the tag name follows variable naming rules
  3) where the closing tag is
HEREDOC;

Note

结束标记必须从新行的第一个字符开始。

通过用单引号将标签括起来,可以指定字符串是 nowdoc 而不是 heredoc,如下所示:

<?php
echo <<<'NOWDOC'
This is a nowdoc string, note:
    1) Single quotes around the label
    2) Variables will not be evaluated
    3) Control characters will not be evaluated
NOWDOC;

引用字符串中的字符

通过使用方括号或花括号来表示要引用的从零开始的整数位置,可以引用字符串中的位置。

<?php
$hello = "world";
echo $hello[0]; // w
echo $hello{1}; // o

Caution

请记住,字符串是一系列字节,您引用的是字节位置。如果您的字符集每个字符使用一个以上的字节,您将不会得到您期望的结果。

在其当前版本中,如果您试图写入字符串的负位置,或者如果您没有指定整数位置,PHP 将发出一个范围警告。

写入超出范围的位置将导致字符串被空格填充以容纳缺失的部分。

<?php
$hello = "world";
$hello[10] = "*";
echo $hello; //  world     *

请注意前面示例中的尾随星号。

PHP 和多字节字符串

PHP 将字符串实现为一个字节数组,用一个整数表示缓冲区的长度(不以空值结束)。PHP 不存储关于字符串如何编码的信息。

可变宽度编码方案使用不同长度的代码对字符集进行编码。多字节编码使用不同数量的字节来编码字符。

多字节编码允许在计算机上编码和表示大量的字符。你在 PHP 中经常会遇到的编码方案之一是 UTF-8。 2 这是 PHP 将尝试使用的多字节编码的默认方案。

PHP 中的原生字符串函数假设字符串是一个单字节数组,所以像substr()strpos()strlen()strcmp()这样的函数不能处理多字节字符串。

您应该使用这些函数的多字节等价物,例如mb_substr()

统一码

Unicode 试图统一所有代表字符的代码集。Unicode 定义了代码点,这些代码点是字符的抽象概念。一个 Unicode 码点代表一个字符,写成这样:U+0041。该数字被指定为大写字母“A”。

Unicode 可以存储的字符没有限制。最初对于 Unicode 是两个字节有一些混淆,但这与编码方案有关,而与 Unicode 本身无关。

Note

Unicode 本身不是编码系统。编码是表示 Unicode 字符的方式。

UTF-8 将从 0 到 127 的所有码点存储在一个字节中。这涵盖了英语字母表、数字和一些符号的整个范围。127 以上的码点存储在多个字节中(最多 6 个字节)。

因为 0-127 的 Unicode 码位与 0-127 的 ASCII 表相匹配,所以用 UTF 8 编码的英语文本看起来就像用 ASCII 编码的一样。

只有那些用重音符号书写字符的人最终会得到一个与 ASCII 编码不同的文件。有数百种编码方案可以存储部分 Unicode 码位,但不是全部。

如果您使用这些编码之一并遇到无法表示的 Unicode 字符,您将看到一个问号或一个空框。

例如,如果您的编码方案适合存储希伯来语字符,而您试图在其中存储俄语字符,您将得到一串问号而不是俄语字符,因为编码方案不支持它们。

告诉客户端字符串是如何编码的

您无法确定地检测一个字符串是如何编码的(除非您自己编码),使用您的输出的客户端也无法确定。除非客户端知道字符串是如何编码的,否则它无法自信地显示它。作为一名 PHP 程序员,你的工作是告诉客户你的 HTML 输出是如何编码的。

您应该指定在Content-Type HTTP 头中使用的字符编码方案。这让客户端知道您的输出是如何编码的,从而知道如何正确显示它。

将内容类型作为一个meta标签放在 HTML 中稍微不太令人满意,因为除非客户端知道编码类型,否则它将无法读取 HTML 来确定编码。你这样做可以逃脱惩罚,但最好不要这样做。

在编码方案之间切换

mbstring扩展提供了许多功能,可以用来帮助检测编码方案并在编码方案之间进行转换。

mb_detect_encoding()函数将遍历可能的编码列表,并尝试确定字符串是如何编码的。

您可以使用mb_detect_order()功能或通过提供 CSV 或数组形式的编码列表来更改检测顺序。

您可以使用mb_convert_encoding()在编码格式之间转换字符串。

实际例子

这个例子展示了 PHP 中字符串行为的一些方面。它用三种不同的方式声明了一个数组,然后对每种方式运行一些字符串命令来说明一些要点。

<?php
$waysToSayHello = [
        'emoji' => "\u{1F44B}",
        'latinchars' => "Hello",
        'accentedChars' => "ça va?"
    ];
foreach ($waysToSayHello as $method => $string) {
    echo "$method : encoding [" . mb_detect_encoding($string, 'ISO-8859-1') . '] ' .
        'encoding [' . mb_detect_encoding($string, ['ASCII','UTF-8']) . '] ' .
        'strlen [' . strlen($string) . '] ' .
        'mb_strlen [' . mb_strlen($string) . '] ' .
        'first character[' . $string[0] . ']';
    echo "\r\n";
}
/*

emoji : encoding [ISO-8859-1] encoding [UTF-8] strlen [4] mb_strlen [1] first character![A456636_1_En_3_Figb_HTML.gif

latinchars : encoding [ISO-8859-1] encoding [ASCII] strlen [5] mb_strlen [5] first character[H]

accentedChars : encoding [ISO-8859-1] encoding [UTF-8] strlen [7] mb_strlen [6] first character![A456636_1_En_3_Figc_HTML.gif

*/

记住 PHP 不在字符串中存储编码信息,所以它只能猜测字符串是如何编码的。mb_detect_encoding函数将检查字符串并尝试确定它是什么。

它通过将字符串与编码方案列表进行比较,并选择第一个方案来对字符串进行有效编码。您可以指定编码(按顺序)让 PHP 尝试或依赖默认编码。这解释了为什么对于同一个字符串,mb_detect_encoding的输出是不同的——我们给了 PHP 不同的提示。

请注意,strlen()功能的输出与mb_strlen不同。PHP 函数strlen返回字符串中有多少字节,而不是多少字符。

最后,请注意,如果我们使用数组表示法来访问字符串中的某个位置,只有当字符串以单字节格式编码时,我们才能获得有意义的结果。

匹配字符串

当您试图匹配不同的变量类型时,在 PHP 中比较字符串应该以适当的谨慎程度进行。在第一章“造型变量”一节中,我们检查了与造型相关的手册页。确保您熟悉 PHP 如何将各种变量类型转换为字符串。

使用像><这样的比较操作符可能并不总是像预期的那样工作。通常预期 PHP 会使用字母顺序来计算这些操作符的字符串。

PHP 使用字符的 ASCII 值进行比较,而不是使用字母排序。小写字母比大写字母具有更高的 ASCII 值,因此您可能会遇到小写字母放在大写字母后面的情况,如下所示:

<?php
$a = "PHP";
$b = "developer";
if ($a > $b) {
    echo "$a > $b";
} else {
    echo "$a < $b";
}
// developer comes before PHP in the alphabet
// but this script outputs
// PHP < developer

回想一下“转换变量”一节中讨论的将字符串转换为整数的规则。在下面的例子中,字符串被转换为一个整数值12,它等于浮点值12.00,因此消息被回显。

<?php
$a = "12 o'clock";
$b = 12.00;
if ($a == $b) {
    echo "The mouse ran up the clock";
}

除非您对正在比较的字符串有把握,否则您应该考虑使用标识运算符===来进行这种比较。

除了使用操作符,PHP 还提供了许多字符串比较函数。

strcmp()是一个执行二进制安全字符串比较的函数。它以两个字符串作为参数,如果str1小于str2,则返回< 0;如果str1大于str2,则为> 0,如果它们相等,则为0

Tip

还记得宇宙飞船操作员吗?运算符可用于任何变量类型,但strcmp专用于字符串。

还有一个名为strcasecmp()的不区分大小写的版本,它首先将字符串转换成小写,然后进行比较。

此示例显示了不同之处:

<?php
$a = "PHP";
$b = "developer";
$comparison = strcmp($a, $b);
echo $comparison . PHP_EOL; // -20
$caseInsensitive = strcasecmp($a, $b);
echo $caseInsensitive . PHP_EOL; // 12

函数strncmp()strcasencmp()只能用来比较两个字符串的前“n”个字符。

PHP 有一个非常强大的函数叫做similar_text(),可以计算两个字符串之间的相似度。对于较长的文本,这可能是一个计算量非常大的过程,所以在使用之前要小心。还要注意你传递参数的顺序很重要,所以similar_text($a, $b) != similar_text($b, $a)

另一个函数levenshtein()可以用来计算两个字符串之间的 Levenshtein 距离。Levenshtein 距离被定义为将str1转换为str2所需要替换、插入或删除的最少字符数。

要比较子字符串,可以使用二进制安全的substr_compare()函数。

PHP 有两个函数可以让你处理字符串的发音。soundex()函数根据字符串的发音来计算音调。发音相同的琴弦会有相同的soundex键。

类似地,metaphone()函数为相似的发声字符串创建相同的键。它比soundex()更准确,因为它知道英语发音的基本规则。当然,这在其他语言中很可能帮助不大!

还有另外两种比较字符串的方法,但是会在关于安全性的第六章中讨论。hash_equals()函数是一种比较字符串的定时攻击安全方式,而password_verify()是一种检查密码是否与哈希匹配的安全方式。稍后您将更详细地了解它们,但请记住它们是字符串函数。

提取字符串

字符串中的单个位置可以用与数组元素相同的语法引用。字符串中的所有位置总是从零开始,即字符串中的第一个字符是位置 0。

<?php
$string = 'abcdef';
echo $string[0];    // a

您可以使用substr()函数返回字符串的一部分或片段。substr()的 PHP 手册显示了该命令的语法,如下所示:

`string substr ( string $string , int $start [, int $length ] )`

您可以看到它有两个强制参数和一个可选参数。起始参数和长度参数都可以是正数或负数。如果起始值大于字符串的长度,substr()将返回false。如果起始值为正(或 0),则返回的字符串片段从字符串的第start个位置开始,从开始算起。

否则,如果为负,切片从字符串末尾的第start个位置开始。

<?php
echo substr("abcdef", 2) . PHP_EOL;    // cdef
echo substr("abcdef", -2) . PHP_EOL;   // ef

如果省略 length,如前面的示例所示,那么切片将从切片起点继续到字符串的结尾。如果长度是正数,那么最多返回length个字符。如果长度是一个负数,那么在字符串的末尾会省略掉许多字符:

<?php
echo substr("abcdef", 0, 2) . PHP_EOL;    // ab
echo substr("abcdef", 0, -2) . PHP_EOL;   // abcd

如果长度给定并且是0FALSENULL,则返回一个空字符串。当开始参数大于或等于字符串时,也会发生同样的情况。

PHP 手册 3 给出了更多的例子:

<?php
echo substr('abcdef', 1);     // bcdef
echo substr('abcdef', 1, 3);  // bcd
echo substr('abcdef', 0, 4);  // abcd
echo substr('abcdef', 0, 8);  // abcdef
echo substr('abcdef', -1, 1); // f

搜索字符串

因为 PHP 是为 web 编写的,所以它在处理字符串方面特别强。您应该知道字符串操作函数的来龙去脉。本节介绍用于搜索字符串的函数。强烈建议您尝试这些函数,并阅读它们的手册页。Zend 考试非常适合奖励经验,而不是手册的百科知识。

有用的提示

对 PHP 的一个常见抱怨是,很难判断搜索字符串和数组时参数的顺序。

PHP 搜索参数有一个$haystack,我们正在搜索一个$needle。比较用于strpos()array_search()的参数顺序:

<?php
$arr = ['a', 'b', 'c', 'd', 'e', 'f' ];
$str = 'abcdef';
echo strpos($str, 'c') . PHP_EOL;
echo array_search('c', $arr) . PHP_EOL;

乍一看好像有时候是$needle参数先来,有时候是$haystack参数先来。

然而,当你记得 PHP 使用底层 C 库并且一致的规则是:

  • 对于字符串搜索功能,顺序始终是$haystack然后是$needle
  • 对于数组搜索功能,顺序总是$needle然后$haystack

下一个有用的提示是记住0false的区别。尽管布尔值 false 的计算结果为0,但是如果将其转换为整数,则数字0与布尔值false并不相同。这里有一个例子,我们似乎在字符串"abcdef"中找不到字母“a”:

<?php
$string = 'abcdef';
if (strpos($string, 'a') == false) {
  echo "False negative!" . PHP_EOL;
}

记住字符串是从零开始的,所以第一个位置是位置 0。strpos()正在返回整数 0,因为它在第一个位置找到了“a”。我们使用等式运算符==来检查strpos()的结果,因此我们错误地报告字母“a”没有出现在这个字符串中。

Tip

为了处理确实找不到子串的情况,应该使用 identity ===操作符。

搜索功能快速概述

PHP 有几个函数用来搜索字符串。一般来说,不区分大小写的函数在前缀后有一个“I”。下表列出了字符串搜索函数的 PHP 手册定义。

功能 用于
substr_count() 返回字符串中子字符串出现的次数。
strstr() 在字符串中搜索子字符串,并返回干草堆中出现在第一个匹配项之后的部分。如果没有找到匹配项,则返回false。注意使用strpos()更好,因为它更快。
stristr() 不区分大小写的版本strstr()
strchr() 返回第一次出现针之前的字符串部分。
strpos() 返回指针第一次出现的位置。 4
stripos() 不区分大小写的版本strpos()
strspn() 查找完全由给定掩码中包含的字符组成的字符串的起始段的长度。 5
strcspn() 返回不包含掩码中任何字符的 subject 的初始段的长度。换句话说,它搜索字符串中任何掩码字母的第一个匹配项,并返回在找到它之前存在的字符数。 6

替换字符串

PHP 有三个替换字符串的函数。

str_replace()及其不区分大小写的版本str_ireplace()可用于基本替换。

<?php
echo str_replace('foo', 'bar', 'Delicious food'); // Delicious bard

它们都有三个强制参数——搜索字符串、替换字符串和要操作的字符串。如果您传递可选的第四个变量(它是一个引用参数),它将被设置为 PHP 执行的替换次数。

搜索和替换参数都可以是数组。这使您可以在一次调用中替换多个值,如下例所示:

<?php
$string = "I like black hot coffee";
$search = ['black', 'coffee'];
$replace = ['green', 'tea'];
echo str_replace($search, $replace, $string); // I like green hot tea

您可以使用substr_replace()函数来替换子字符串。substr_replace()用替换中给出的字符串替换由 start 和(可选)length 参数分隔的字符串副本。

strtr()是另一个替换子字符串和字符的函数。如果只提供了两个参数,第二个参数应该是替换对的数组。否则,它需要三个参数,如 PHP 手册中的示例所示,它用于将带重音符号的字符转换为英语格式的字符:

<?php
$address = "09479 Huopainenkylä, Pöhjois-Karjala";
$address = strtr($address, "äåö", "aao");
echo $address; // 09479 Huopainenkyloa, Pohjois-Karjala

替换字符串最灵活、最强大的方法是使用preg_match()函数,它允许您使用正则表达式来查找要替换的字符串片段。在本章后面的“字符串模式:正则表达式”一节中,你会学到更多关于正则表达式的知识。

格式化字符串

printf()函数用于输出格式化的字符串。你应该仔细阅读 PHP 手册 7 并确保你已经练习过使用它。一般的用法是指定一个格式化字符串和需要放入其中的值。

<?php
$minutes = 60;
$timeUnit = "an hour";
printf("There are %u minutes in %s.", $minutes, $timeUnit);

在这个例子中,您会注意到第一个参数printf()有两个用百分比符号标记的占位符。以下参数是必须进行类型转换并插入到这些占位符中的值。

有许多符号可用于格式化参数。你可以在 PHP 网站上找到这个列表, 8 但是为了方便起见,我把它包含在这里:

标志 格式
%% 文字百分比字符。不需要任何参数。
%b 该参数被视为整数,并以二进制数表示。
%c 该参数被视为一个整数,并表示为带有该 ASCII 值的字符。
%d 该参数被视为整数,并表示为(有符号的)十进制数。
%e 该参数被视为科学符号(例如,1.2e+2)。从 PHP 5.2.1 开始,精度说明符代表小数点后的位数。在早期版本中,它被视为有效位数(少一位)。
%E %e相似,但使用大写字母(例如 1.2E+2)。
%f 该参数被视为一个浮点数,并表示为一个浮点数(支持区域设置)。
%F 该参数被视为一个浮点数,并表示为一个浮点数(不区分语言环境)。从 PHP 4.3.10 和 PHP 5.0.3 开始可用。
%g %e%f中较短的一个。
%G %E%f中较短的一个。
%o 该参数被视为一个整数,并表示为一个八进制数。
%s 参数被视为并显示为字符串。
%u 该参数被视为整数,并表示为无符号十进制数。
%x 该参数被视为整数,并以十六进制数(小写字母)表示。
%X 该参数被视为整数,并以十六进制数(大写字母)表示。

PHP 格式是区域敏感的,这影响了它们表示数字和日期的方式。例如,如果您将区域设置为荷兰语,那么日期将以荷兰语输出。PHP 手册上的一个例子显示了这一点:

<?php
// Set locale to Dutch
setlocale(LC_ALL, 'nl_NL');
// Output: vrijdag 22 december 1978
echo strftime("%A %e %B %Y", mktime(0, 0, 0, 12, 22, 1978));

Caution

区域设置信息是按进程维护的,而不是按线程。

如果您在多线程服务器 API 上运行 PHP,如 Windows 上的 IIS、HHVM 或 Apache,您可能会在脚本运行时遇到语言环境设置的突然变化,尽管脚本本身从未调用过setlocale()

这是由于其他脚本同时在同一个进程的不同线程中运行,使用setlocale()改变了整个进程的语言环境。

在 POSIX 系统上,您可以使用 shell 命令locale –a列出它支持的所有语言环境。在 Windows 机器上,MSDN 上有列出地区的页面,您可以在控制面板中查看这些页面。

格式化数字

number_format()函数是格式化数字的一种简单方法。

number_format()不支持区域设置,因此不会自动为您选择分隔符。默认情况下,千位分隔符是逗号,不显示小数位。

该函数的参数包括要格式化的数字、要显示的小数位数、小数点字符和千位分隔符。

您可以向该函数传递一个、两个或四个参数。这里有一个例子:

<?php
$number = 1234.5678;
// 1,235
echo number_format($number) . PHP_EOL;
// 1,234.568
echo number_format($number, 3) . PHP_EOL;
// 1.234,57
echo number_format($number, 2, ',', '.') . PHP_EOL;

要格式化货币,可以使用money_format()功能。它是区域感知的,并使用由主机系统设置的信息。

<?php
// Locale is British English
setlocale(LC_MONETARY, 'en_GB');
echo money_format('%.2n', "5000000.123");
// Locale is Denmark
setlocale(LC_MONETARY, 'da_DK');
echo money_format('%.2n', "5000000.123");

输出如下所示:

£5,000,000.12
kr 5.000.000,12

字符串模式:正则表达式

正则表达式是一组匹配字符串的规则。规则被写成一个字符串,使用一种描述您正在搜索的模式的格式。正则表达式有几种风格;PHP 使用 Perl 兼容的正则表达式(PCRE)。

学习正则表达式时,应该找一个自己喜欢的在线正则表达式测试器。有几个可供选择,它们使得处理表达式和查看它们如何匹配字符串变得更快。 9

定界符

正则表达式由出现在表达式中每个模式的开头和结尾的字符分隔。通常使用正斜杠,但#!也很常见。

可以使用任何字符,但是需要在表达式中对分隔符进行转义,因此标准做法是选择不太可能出现在搜索表达式中的分隔符。例如,如果您要搜索目录以找到匹配某个模式的目录,正斜杠字符可能不是分隔符的最佳选择。

元字符

元字符被解释为在搜索模式中有意义。如果您打算将它们作为表达式的文字部分,则需要对它们进行转义。下表列出了它们。

性格;角色;字母 意义
\ 通用转义字符
^ 主题或行的开始
` 性格;角色;字母
--- ---
\ 通用转义字符
^ 主题或行的开始
主题或行尾
. 匹配除换行符以外的任何字符
[ 开始定义一个角色类
] 结束定义字符类
&#124; 备用分支的开始(如“或”)
( 子模式的开始
) 子模式的结尾
? 零或一个量词
* 零个或多个量词
+ 一个或多个量词
{ 最小/最大数量化开始
} 结束最小/最大量化

在本节中,我们将在此基础上继续学习,但现在只需注意这些符号在正则表达式或模式中传达了某种意义。你需要在参加考试前熟悉它们。

通用字符类型

Regex 为您提供了一种方式来指定搜索字符串中的字符可以是任何特定类型。您可以使用反斜杠(转义)元字符来指定它们,然后提供该类型的字母。

下表列出了 PCRE 中可用的字符类型。

标志 字符类型
\d 任何十进制数字
\h 任何水平空白字符
\s 任何空白字符
\v 任何垂直空白字符
\w 任何“单词”字符
\D 任何不是十进制数字的字符
\H 任何不是水平空格的字符
\S 任何非空白字符
\V 任何不是垂直空白字符的字符
\W 任何“非单词”字符

您应该立即发现大写符号是小写符号的反码。

“单词”字符是任何字母、数字或下划线字符。其中包含的实际字符是区域设置敏感的。

边界

单词边界是字符串中当前字符和前一个字符都不匹配\wW的位置。

换句话说,它是字符串中一个单词开始或结束的位置,或者是其中一个字符匹配\w而另一个匹配W的位置。

标志 分界线
\b 单词边界
\B 不是一个单词边界
\A 主题的开始
\Z 主题结尾或结尾换行
\z 主题结束
\G 主题中的第一个匹配位置

Tip

PHP 使用 PCRE 表达式。您可以在 http://www.pcre.org/original/doc/html/pcrepattern.html 的原始规范文档中找到此表。

字符类别

字符类是定义搜索字符串中可以匹配的字符集的非常灵活的方法。通过在模式中指定一个小的字符序列,您可以在搜索字符串中匹配一个大得多的字符集。

您在元字符表中看到,您通过将字符类放在方括号中来创建它。字符类的一个例子是[A-Z],它代表大写字母表中的所有字母。

您也可以在字符类中使用所有的通用类型,因此[A-Z\d]将匹配所有的大写字母和数字。

匹配不止一次

应用于字符串"abc123ABCabc"的表达式/[A-Z\d]/将匹配"1"字符。换句话说,它匹配搜索字符串中与表达式匹配的第一个字符。

如果您回头参考元字符表,您可以看到,+符号可以用来指定您想要一个或多个模式。所以对字符串"abc123ABCabc"应用的表达式/[A-Z\d]+/将匹配"123ABC"字符。 10

您可以使用大括号来限制匹配的数量。语法最好显示在一个表中,您可以将表达式与字符串"abc123ABCabc"进行匹配:

表示 限制 输出
/[A-Z\d]+/ 一个或无限 公元前 123 年
[A-Z\d]{3} 正好三个 One hundred and twenty-three
[A-Z\d]{3,} 三个或更多 公元前 123 年
[A-Z\d]{3,5} 三到五点之间 123AB
[A-Z\d]{50} 正好 50 不匹配

捕获组

捕获组由括号描述,允许您对组应用限定符。它们还生成存储匹配值的编号组,并且可以在表达式的其他地方引用它们。

在本例中,我们围绕单词“cheeseburger”创建了一个捕获组,并使用该组来指定零个或一个匹配项。

<?php
$subject = "I can haz Cheeseburgers";
$pattern = "/I can haz (Cheeseburger)?/";
$matches = [];
preg_match($pattern, $subject, $matches);
var_dump($matches[0]);

这输出string(22) "I can haz Cheeseburger"。请注意,字符串末尾的“s”不匹配。

Tip

作为练习,在您最喜欢的编辑器中使用正则表达式,看看如果使用主题“I can haz”(字符串末尾没有空格)会发生什么。

您可以使用非捕获组来优化您的查询。当你不需要捕捉比赛时,你应该使用这些。

他们通过在您的组的开头放置一个?:标记来进行标记。前面的例子可以写成/I can haz (?:Cheeseburger)?/。注意,这个表达式仍然会像以前一样将字符串返回给 PHP,但是它不会将字符串Cheeseburger作为一个组存储,以供表达式引用。

令人困惑的是,?是一个量词,也表示一个非捕获组。请记住,量词不能出现在组的开头,因为没有什么可以量化的。

贪婪和懒惰

默认情况下,匹配是“贪婪”的,将匹配尽可能多的字符串。考虑一个你将要使用的例子。假设您想要匹配 HTML 标签,那么您可以尝试以下方法:

<?php
$subject = "Some <strong>html</strong> text";
$pattern = "/<.*>/";
$matches = [];
preg_match($pattern, $subject, $matches);
var_dump($matches[0]);  // string(21) "<strong>html</strong>"

这会输出string(21) "<strong>html</strong>",这显然比您想要的 HTML 标签要多。

这要归咎于贪婪;*量词是贪婪的,试图找到最长的可能匹配。它返回强标签的开始<和结束标签的最后>之间的字符,这是最长的可能匹配。

相比之下,惰性搜索返回最短的可能匹配。可以通过给量词加一个问号(?)来修改量词,让它变懒。

<?php
$subject = "Some <strong>html</strong> text";
$pattern = "/<.*?>/";  // note the pattern has changed
$matches = [];
preg_match($pattern, $subject, $matches);
var_dump($matches[0]);  // string(8) "<strong>"

修改量词还有很多选择,但是它们超出了本书的范围。

获取所有匹配项

到目前为止,您的表达式只返回搜索字符串匹配部分的第一个匹配项。假设您想要查找字符串中的所有匹配项。

PCRE 有一个全局修饰符(后面会详细介绍),但是 PHP 使用一个名为preg_match_all()的独立函数来返回所有匹配。

<?php
$subject = "Some <strong>html</strong> text";
$pattern = "/<.*?>/";
$matches = [];
preg_match_all($pattern, $subject, $matches);
var_dump($matches);

/*
 array(1) {
                [0] =>
                        array(2) {
                        [0] => string(8) "<strong>"
                        [1] => string(9) "</strong>"
                }
        }
*/

命名组

您可以通过将?<name>添加到打开组的括号的开头来命名捕捉组。例如:

<?php
$subject = "test@example.com";
$pattern = "/^(?<username>\w+)@(?<domain>\w+).(?<tld>\w+)/";
$matches = [];
if (preg_match($pattern, $subject, $matches)) {
  var_dump($matches);
}

在这个例子中,我们将匹配模式的第一部分命名为username,第二部分命名为domain,第三部分命名为tld。这是一个有点幼稚的例子,因为它不适用于像test@example.co.uk这样的电子邮件地址,但是它确实可以显示语法。前面的示例输出如下:

array(7) {
              [0] => string(16) "test@example.com"
              'username' => string(4) "test"
              [1] => string(4) "test"
              'domain' => string(7) "example"
              [2] => string(7) "example"
              'tld' => string(3) "com"
              [3] => string(3) "com"
      }

所以您能够引用$matches['username']并接收"test"作为响应,这很方便。

图案修改器

您可以在表达式的结束分隔符后添加修饰符。下表列出了修饰符。

修饰语 功能
i 该表达式不区分大小写。
m 多线模式。字符串可以跨多行,换行符被忽略。^和`
--- ---
i 该表达式不区分大小写。
符号将匹配行的开始和结束,而不是匹配字符串的开始和结束。
s .元字符也将匹配换行符。
x 忽略空白,除非你转义它。
e 这会导致 PHP 代码被评估,这是非常不鼓励的。从 PHP 5.5 开始不推荐使用它,在 PHP 7 中将生成警告,因为它不再被支持。
U 这使得量词在默认情况下是懒惰的,在它们后面使用?反而会将它们标记为贪婪的。
u 这告诉 PHP 将模式和字符串视为 UTF-8 编码。这意味着匹配的是字符而不是字节。

Chapter 3 Quiz

Q1:你不能使用大于或小于运算符来比较字符串变量和整数变量。只能用等价运算符比较字符串和整数值。

| 真实的 |
| 错误的 |

Q2:你可以使用 ________ 函数在字符串之间进行二进制安全的不区分大小写的比较。

| <=> |
| strcmp |
| strcasecmp |
| stricmp |

Q3:搜索字符串的 PHP 函数总是有按什么顺序排列的参数。

| $haystack, $needle |
| $needle, $haystack |
| 这取决于功能 |

Q4:strspn($subject, $mask)函数是做什么的?

| 在字符串$subject中搜索子字符串$mask |
| 返回$subject中字符串的最大长度,该字符串只包含$mask中的字母 |
| 返回包含$mask中所有字母的$subject中字符串的最小长度 |
| 这是一种从$subject字符串中拼接出由$mask指定的字符串的二进制安全方式 |

q5:strstr($haystack, $needle)函数是做什么的?

| 这是比strpos()更快的选择 |
| 这是对strpos()的二进制安全替代 |
| 它返回出现在第一个$needle实例之后的那部分$haystack |
| 它返回字符串$needle第一次出现在$haystack中的位置 |

Q6:这段代码的输出是什么?

| 0 |
| Cats do nothing but sleep |
| Cats da nathint but sleep |
| 这会产生一个错误 |

<?php
$fact = "Dogs do nothing but sleep";
$fact = strtr($fact, "Dog", "Cat");
echo $fact;

问题 7:在下面的文本中,哪一个正则表达式将识别两个电子邮件地址(并且只识别电子邮件地址)?选择尽可能多的适用项。

“打翻圣诞树盯着 kittens@catsaregreat.com 墙,玩食物被灰尘弄糊涂或者今天要去抓红点今天要去抓红点。”。

| [a-z]。[a-z.]+ |
| \b[a-z]+@[a-z]+com\b |
| \b[a-z]+@[a-z.]+\b |
| (\b[a-z]
@\b)([a-zA-Z\d]+) |
| (\S)@(\w)。(\S*) |

Q8:这段代码的输出是什么?

| abcdefgh12345678 |
| 没有任何东西 |
| 一个警告 |
| 致命的错误 |

<?php
echo substr("abcdefgh12345678");

问题 9:如果运行这段代码,如何检索第一个电子邮件地址?

| echo $matches[0]; |
| echo $matches[0] . $matches[1] . $matches[2] |
| 你不能;有一个语法错误 |
| 不能;这不会匹配字符串中的任何内容 |
| 不能;这将产生一个错误,因为模式无效 |

<?php
$subject = "purr for no reason or eat prawns daintily with a claw then lick paws mycat@catsaregreat.com clean wash down prawns with a lap of carnation milk then retire to the warmest spot on the couch to claw";
$pattern = "#(\S*)@(\w*).(\S*)#";
$matches = [];
preg_match($pattern, $subject, $matches);
// how do I echo the full email address?

Q10:preg_replace_callback()函数用来做以下哪一项?

| 使用回调函数来提供替换字符串,而不是静态字符串 |
| 使用返回匹配列表的回调来替换 |
| 指定一个函数,在preg_replace()完成运行后调用 |
| 没有这个功能 |

Footnotes 1

https://php.net/manual/en/regexp.reference.escape.php

2

https://en.wikipedia.org/wiki/UTF-8

3

https://php.net/manual/en/function.substr.php

4

https://secure.php.net/manual/en/function.strpos.php

5

https://secure.php.net/manual/en/function.strspn.php

6

https://secure.php.net/manual/en/function.strcspn.php

7

https://php.net/manual/en/function.printf.php

8

https://secure.php.net/manual/en/function.sprintf.php

9

例如,网站 https://regex101.com/ 是一个玩正则表达式的好地方。

10

https://regex101.com/r/EXsPkY/2

四、数组

在这一章中,我们将会看到 PHP 数组。PHP 数组被实现为一个有序的映射,将值与键关联起来。PHP 中有三种类型的数组:索引数组、关联数组和多维数组。

PHP 有许多数组函数,涵盖了函数的许多常见用法。在编写函数来操作数组之前,应该先检查是否已经有一个函数。它们是用 C 实现的,所以要达到同样的效果,它们比用 PHP 编写的任何函数都要快得多。阵列手册页 1 在一个地方列出了它们,你要确保你学习了这个页面和每个函数的手册页。

这本书太长了,无法详尽地列出每一个功能。本章不是重复这些信息,而是集中在对这些函数的分组和解释上。

声明和引用数组

我们不会详细讨论什么是数组,而是直接讨论 PHP 中用于声明数组的语法。

数组被创建为一组用逗号分隔的值对。

<?php
// numeric index, auto assigned key
$arr = array(10, 'abc', 30);
// numeric index, key explicitly set
$arr = array(0 => 10, 1 => 'abc', 2 => 30 );
// associative
$arr = array('name' => 'foo', 'age' => 20);
// short syntax
$arr = ['name' => 'foo', 'age' => 20];

如果你没有指定一个键,那么 PHP 将分配一个自动递增的数字键。在这个例子中,前两个赋值是相同的,因为 PHP 会自动分配这个键。

密钥可以是数字或字符串。数组可能包含数字和字符串键的混合。

以数字为关键字的数组称为枚举型。前两个例子是列举性的。包含关键字字符串的数组称为关联数组。最后两个例子是关联数组。

声明数组有两种语法形式;选一个是编码风格的问题。

<?php
$shortForm = ['this', 'is', 'short'];
$longForm = array('this', 'is', 'short');

数组可以是嵌套的。换句话说,数组值本身可以是一个数组。这些被称为多维数组。

可以使用[]操作符引用单个数组元素,如下所示:

<?php
$arr = ['name' => 'foo', 'age' => 20];
echo $arr['age']; // 20

如果您没有在括号中指定一个键,PHP 会认为您正在尝试引用一个新元素。您可以使用它将元素添加到数组的末尾:

<?php
$arr = [0 => 'id', 'name' => 'foo', 'age' => 20];
$arr[] = 'example';
print_r($arr);

这将输出以下内容:

Array
(
[0] => id
[name] => foo
[age] => 20
[1] => example
)

注意,PHP 通过递增数组中最高的数字键来选择键。

创建数组的函数

有很多 PHP 函数返回一个数组,但是我将介绍几个与数组直接相关的函数。

函数explode()用于将一个字符串拆分成一个数组。举个例子解释最简单:

<?php
// The delimiter is a string of any length
$delimiter = ',';
// This string is broken up by the delimiter
$source = '1, abc, 2, def, 3, ghi';
// The limit determines how many elements explode will return
$limit = -2;
// create an array by splitting the source
$arr = explode($delimiter, $source, $limit);
print_r($arr);

该函数有三个参数。第一个是用作分隔符的字符串。通常,这只是单个字符(就像使用 CSV 时的逗号),但它可以是任意长度。

第二个参数是一个字符串,包含由分隔符分隔的元素列表。

第三个参数限制 PHP 将返回的项数。默认情况下,它被设置为PHP_INT_MAX,因此 PHP 将返回尽可能多的条目。如果是负数,PHP 将返回除最后一个$limit金额之外的所有元素。零限值被视为与 1 相同。

这个例子指定-2作为限制,所以 PHP 返回除最后两个元素之外的所有元素。

该示例的输出是:

Array
(
    [0] => 1
    [1] =>  abc
    [2] =>  2
    [3] =>  def
)

implode()功能 2 以相反的方式操作。它将数组中的元素连接到一个字符串中,该字符串由您提供的字符串分隔。

preg_split()是另一个将字符串拆分成数组的函数。它类似于explode(),但是它使用正则表达式来分隔字段,而不是使用文字字符串。它被记录在 PHP 手册中。

你可以使用str_split()函数 4 将一个字符串分解成一个组块数组。它有两个参数:要分割的字符串,以及用于数组中每个元素的块的长度。

<?php
$input = '12345678';
$arr = str_split($input, 3);
print_r($arr);

此示例将字符串分解为包含长度为 3 的元素的数组,如下所示:

Array
(
    [0] => 123
    [1] => 456
    [2] => 78
)

注意,字符串不能被块大小整除,所以最后一个元素只有两个字符长。如果块大小大于字符串的长度,则整个字符串作为数组的唯一元素返回。如果您尝试使用负的块长度,该函数将返回FALSE

数组运算符

PHP 数组可以测试等价性和同一性。我们在比较运算符一节中看到,如果数组具有相同的键和值对,那么它们就是等价的。如果它们具有相同的键和值对,顺序相同,并且键-值类型相同,则它们是相同的。

+运算符将产生两个数组的并集。

当使用+ union 操作符时,PHP 将操作符右边的数组附加到左边。如果一个键同时存在于两个数组中,那么左边的数组值将用于该键。

<?php
$a = ['a' => 'hello', 'b' => 'world'];
$b = ['a' => 'goodbye', 'c' => 'cruel'];
echo implode(' ', $a + $b);  // hello world cruel

在前面的例子中,两个数组都有键a。因此,数组的并集将具有这个键的来自$a的值,因为$a在并集操作符的左边。

例子 名字 结果
$a + $b 联盟 $b被追加到$a之后。如果一个键存在于两个数组中,那么来自$a的值被放入联合中。
$a == $b 平等 TRUE如果$a$b有相同的键值对
$a === $b 身份 TRUE如果$a$b有相同的键值对,相同的类型,相同的顺序。
$a != $b 不平等 TRUE如果$a不等于$b
$a <> $b 不平等 TRUE如果$a不等于$b
$a !== $b 非同一性 TRUE如果$a$b不相同。

让我们快速看一个例子:

<?php
$a = ['a', 'b', '1'];
$b = ['a', 'b', 1];
$c = ['1', 'b', 'a'];
$d = [2 => 1, 0 => 'a', 1 => 'b'];

var_dump($a == $b);     // true
var_dump($a === $b);    // false
var_dump($a == $c);     // false
var_dump($a == $d);     // true
var_dump($a === $d);    // false

我们可以看到$a等于$b,因为键值对是相同的。然而,它们并不等价,因为第三个元素的类型在$a中是一个字符串,在$b中是一个整数。

$a$c不相等,即使它们具有相同的值。如果数组具有相同的键值对,则认为它们是相等的。在这种情况下,我们没有指定一个键,所以 PHP 为每个值分配了一个自动递增的键。因此,即使值相同,它们的键值对也不匹配。

$a$d相等是因为键-值对相同,但不相同是因为它们不在同一顺序。

PHP 数组键的适当联系

PHP 数组是从零开始的。

PHP 数组键区分大小写:$arr['A']$arr['a']是不同的元素。

密钥只能是字符串或整数。其他变量类型在存储之前被转换为这些类型之一。

包含十进制有效整数的字符串将被转换为整数类型。

<?php
$a = [
"2" =>"hello",
    0x03 =>"world",
    0b100 => ' this is ',
"04" =>"PHP",
    8.7 =>"!!!!"
];
var_dump($a);
/*
array(5) {
  [2]=>
  string(5) "hello"
  [3]=>
  string(5) "world"
  [4]=>
  string(9) " this is "
  ["04"]=>
  string(3) "PHP"
  [8]=>
  string(4) "!!!!"
}
*/

在前面的例子中,我们看到字符串"2"被转换为整数 2。十六进制和二进制格式都转换成十进制。字符串"04"不会被转换为整数,因为它包含八进制表示,而不是十进制表示。

PHP 在将浮点数转换为整数时,会将浮点数舍入为零。另一种说法是,数字的小数部分被截断。例如,float 133.7 将转换为整数值 133(而不是向上舍入到 134)。

布尔值也可以转换为整数。布尔值true评估为整数 1,而false变为整数 0。

Null 被视为空字符串。所以空键将存储在键''下。

复合变量(对象和数组)和资源不能用作键。如果你试图这样做,PHP 会发出一个警告"illegal offset type"

键是唯一的;如果一个数组中的多个元素使用同一个键(如上转换后),那么 PHP 将使用最后一个键作为值,并覆盖所有前面的值。

Tip

这是一个很好的时间来回顾你的杂耍类型!

填充数组

您可以使用range()函数根据您指定的值范围将值添加到数组中。您可以指定范围的开始、结束和步长。

PHP 手册中有许多有用的例子,但这里有一个是基于其中一条评论的:

<?php
print_r(array_combine(range(1, 10, 2),range(1,5)));

这将输出以下内容:

/*
  Array
  (
    [1] => 1
    [3] => 2
    [5] => 3
    [7] => 4
    [9] => 5
  )
*/

另一个名为array_fill()的命令可以让你用一个值填充一个数组。它接受起始索引、要填充多少值以及要插入的值的参数。

<?php
print_r(array_fill(10, 5, 'five'));

该脚本输出:

Array
(
[10] => five
[11] => five
[12] => five
[13] => five
[14] => five
)

与此相关的是函数array_fill_keys()。这个函数将用一个特定的值填充一个数组,并让你指定使用什么键。

<?php
$keys = range(1, 10, 2);
$value = "PHP";
print_r(array_fill_keys($keys, $value));
/*
Array
(
    [1] => PHP
    [3] => PHP
    [5] => PHP
    [7] => PHP
    [9] => PHP
)
*/

推动、弹出、换档和取消换档(天啊!)

这四个命令用于在数组中添加或删除元素。

功能 影响
array_shift() 将一个元素从数组 5 的开头移开
array_unshift() 将一个或多个元素添加到数组的开头 6
array_pop() 从数组 7 的末尾弹出该元素
array_push() 将一个或多个元素推到数组 8 的末尾

您可能会注意到,您可以用这些函数轻松地实现队列和堆栈。

从数组中删除元素的命令会将元素返回给您,并将所有元素下移。数字键被减少,直到它们从 0 开始计数,而文字键保持不变。

<?php
$stack = array("one", "two", "three", "four");
$fruit = array_shift($stack);
print_r($stack);

在输出中,您会注意到"two"现在的键是 0,而之前的键是 1:

/*
  Array
  (
    [0] => two
    [1] => three
    [2] => four
  )
*/

比较数组

你在本章的前面已经看到,可以使用等式运算符==和等式运算符===来比较数组。当应用于数组时,如果数组具有相同的键和值,则相等运算符返回true,而不管它们是什么类型。如果数组具有相同的键和值,相同的顺序,并且相同的变量类型,那么 identity 操作符将只返回true

<?php
$arr = ['1', '2', '3'];
$brr = [1, 2, 3];
var_dump($arr === $brr); // false
var_dump($arr == $brr);  // true

有专门用于数组比较的 PHP 函数,使得更复杂的比较成为可能。

array_diff()

array_diff()函数接受一组数组作为参数。它将返回一个数组,其中包含第一个数组中不存在于任何其他数组中的值。

这个例子使用array_diff()$_POST超全局中提供的输入参数与预定义的必需参数列表进行比较。

<?php
$requiredKeys = ['username', 'password', 'csrf_token'];
$missingKeys = array_diff($requiredKeys, array_keys($_POST));
if (count($missingKeys)) {
    throw new UnexpectedValueException('You need to provide [' . print_r($missingKeys, true) . ']');
}

这段代码查找所有在所需列表中但不在 post 数组中的键,并创建一个名为$missingKeys的数组来包含它们。这使您可以验证表单是否已完全填写。

array_diff_assoc()array_diff()的关联版本,考虑了数组键及其值。为了看出区别,我们可以用一个非常简单的例子:

<?php
$a = ['a' => 'apple', 'b' => 'banana'];
$b = ['a' => 'apple', 'd' => 'banana'];
print_r(array_diff($a, $b));
print_r(array_diff_assoc($a, $b));
/*
Array
(
)
Array
(
    [b] => banana
)
*/

array_diff()的结果是一个空数组,但是array_diff_assoc()返回一个由[b] => banana组成的数组,因为值banana的键是第一个数组中的b和第二个数组中的d

array_intersect()

函数array_intersect()也将数组列表作为参数。它计算第一个数组中的哪些值也存在于所有其他数组中。

<?php
$birds = ['duck', 'chicken', 'goose'];
$net = ['dog', 'cat', 'chicken', 'goose', 'hamster'];
print_r(array_intersect($net, $birds));

这将输出$net$birds中的元素:

Array
(
  [2] => chicken
  [3] => goose
)

请注意,键被保留。

array_intersect_assoc()包含匹配元素时的索引检查。如果将它应用于示例中的数组,它将返回一个空数组。返回值为空,因为尽管数组中的值匹配,但它们的索引不匹配。

用户定义的匹配函数

PHP 提供了允许您指定自己的比较函数的函数。

array_udiff()为例。它接受一个数组参数列表,后跟一个可调用的作为最后一个参数。

让我们考虑一个简单的例子,我们想要比较数组的小写值。更现实的用例可能涉及更复杂的操作,比如对对象的操作。

<?php
$birds = ['duck', 'chicken', 'goose'];
$net = ['Dog', 'Cat', 'Chicken', 'Goose', 'Hamster'];
$diff = array_udiff($net, $birds, function($a, $b){
    $a = strtolower($a);
    $b = strtolower($b);
    if ($a < $b) {
        return -1;
    } elseif ($a > $b) {
        return 1;
    } else {
        return 0;
    }
});
print_r($diff);

这段代码输出$net中的元素,这些元素在$birds的列表中没有匹配的动物。我们使用一个自定义函数来进行比较,首先将两个字符串都转换成小写。

Array
(
    [0] => Dog
    [1] => Cat
    [4] => Hamster
)

请注意以下几点:

  • 来自手册 9 :“如果第一个参数被认为分别小于、等于或大于第二个参数,比较函数必须返回小于、等于或大于零的整数。”
  • 对于任何将可调用对象作为参数的函数,都可以使用闭包作为可调用对象。
  • 您可以将 lambdas 用作可调用函数,也可以用于任何将可调用函数作为参数的函数。在这个例子中,我们使用了λ。
  • 比较函数采用两个参数作为要比较的值。

有一些 PHP 函数允许你指定自己的可调用函数来比较键、值或两者。

比较函数的快速列表

下表显示了用于执行不同功能的阵列。

有类似的功能来执行交集。它们有相同的命名约定和参数,所以我不在这里列出它们。

功能 用于
array_diff 计算数组的差 10
array_diff_assoc 使用附加索引检查计算数组的差异
array_udiff 使用数据比较回调函数 11 计算数组的差
array_udiff_assoc 通过附加索引检查计算数组的差异,并通过回调函数 12 比较数据
array_udiff_uassoc 使用附加索引检查计算数组的差异,并通过回调函数比较数据和索引

请注意,array_udiff_uassoc()将两个可调用函数作为参数,一个用于值,最后一个参数用于索引。查看手册页 13 确保你已经学习了它所有的相关功能。

组合数组

PHP 提供了一些有用的函数来帮助组合数组。

combine_array($keys, $values)函数通过使用一个键数组和另一个值数组来创建一个数组。如果数组中的元素数量不匹配,它将返回FALSE,否则将返回一个关联数组。

您可以使用array_replace($array1, $array2, ...)将一个数组中的值顺序替换为其他数组中的值。它接受两个或多个数组作为参数,并从左到右处理它们。

它遵循以下规则来确定最终结果:

  • 如果第一个数组有一个键不在第二个数组中,那么键-值对保持不变。
  • 如果第二个数组有一个不在第一个数组中的键,则将第二个数组中的键-值对插入到第一个数组中。
  • 如果第二个值有一个键也在第一个数组中,那么第二个数组中的值将替换第一个数组中的值。

让我们来看一个using array_replace()的例子:

<?php
$input = ['a', 'b', 'c'];
$replace = [3 => 'd', '1' => 'q'];
$replaceTwo = [2 => 1, 1.3 => 'Z'];
$output = array_replace($input, $replace, $replaceTwo);
echo implode(", ", $output); // a, Z, 1, d

我已经将这些信息放在一个表格中,这样您就可以看到这些规则是如何应用的。该函数从左到右工作,用前一个数组替换每个后续参数。

钥匙 $输入 $replace $replaceTwo $输出
Zero a a
one b q Z Z
Two c one one
three d d

Note

字符串键1被转换为整数,浮点键1.3也被转换为整数。两者的值都为 1,因此将替换该位置的值。

函数将合并一个或多个数组。人们可能期望它在合并时遵循与+操作符相同的规则,但是在某些情况下它的行为完全不同。考虑这个例子:

<?php
$arrOne = [
  // integer
  0 => 'One 0',
  // string
  'a' => 'One a',
  // non-empty in One, but empty in Two
  'Overwrite' => 'Not empty',
];

$arrTwo = [
  0 => 'Two 0',
  1 => 'Two 1',
  'b' => 'Two b',
  'Overwrite' => '',
];

print_r($arrOne + $arrTwo);

print_r(array_merge($arrOne, $arrTwo));

一会儿我将向您展示这段代码的输出。在代码输出中,您应该注意两件事:

  • array_merge()函数重新索引数字键,但是操作符没有。
  • array_merge()函数不会用空值覆盖非空值,但操作符会。

正如所承诺的,下面是显示差异的输出:

Array
(
    [0] => One 0
    [a] => One a
    [1] => Two 1
    [b] => Two b
)
Array
(
    [0] => One 0
    [a] => One a
    [1] => Two 0
    [2] => Two 1
    [b] => Two b
)

拆分数组

有几个函数可以用来拆分数组。下表列出了它们。我们将在书中详细讨论一些,但是你应该确保你也阅读了手册。

功能 习惯
array_chunk 将数组分割成块。 14
array_column 从输入数组中返回单个列,例如,数据库查询结果的数组。
array_slice 提取数组中的一个数组。
array_splice 返回数组的一部分,并用原始数组中的其他内容替换它(参数通过引用调用)。 十五
extract 创建以数组的键命名的变量,这些变量包含数组中的值。使用这个函数会导致代码混乱,因为不清楚变量是在哪里定义的。
array_rand 选择一个数组中的随机键。

在这些函数中,唯一可能比较棘手的是array_splice()。它不仅返回值(提取的切片),而且因为输入数组是通过引用传递的,所以它还会影响您调用它的数组。

更复杂的是,您可以选择用替换数组替换从输入数组中提取的切片。

让我们看一个例子:

<?php
$input = [1,2,3];
$replacement = ['hello', 'world'];
// $slice contains the piece we extract
$slice = array_splice($input, 1, 1, $replacement);
// $input is passed by reference and so is amended
print_r($input);

这个脚本查找输入数组中从位置 1 开始、长度为 1 的部分。我们知道数组是从零开始的,所以位置 1 的值是2array_splice()函数返回找到的棋子的数组。因此,$slice将是一个包含值2的单个元素的数组。

array_splice()的输入数组参数通过引用传递,因此将被函数修改。我们用替换数组替换提取的切片。

因此,该脚本的输出是:

Array
(
    [0] => 1
    [1] => hello
    [2] => world
    [3] => 3
)

解构数组

list()语言构造用于根据变量的索引给数组中的变量赋值。下面是其用法的一个基本示例:

<?php
$array = ['one', 'two', 'three'];
list($a, $b, $c) = $array;
echo $a; // one
echo $b; // two
echo $c; //three

PHP 7 为list()引入了一个语法变化,使得它在创建索引数组时表现得更加一致。在 PHP 7 中,变量是按照你写的顺序赋值的,而在 PHP 5 中,它们是按照相反的顺序赋值的。如果你看到一个例子,那就更有意义了:

<?php
$array = ['one', 'two', 'three'];
list($indexedArray[0], $indexedArray[1], $indexedArray[2]) = $array;
var_dump($indexedArray);

在 PHP 7 中,这将输出:

array(3) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  string(5) "three"
}

在 PHP 5 中,顺序相反,输出如下:

array(3) {
  [2]=>
  string(5) "three"
  [1]=>
  string(3) "two"
  [0]=>
  string(3) "one"
}

使用数组计算

PHP 提供了几个方便的函数,让您可以对数组执行数学计算,而不需要手动遍历它们。

功能 返回
array_count_values 数组中每个唯一值出现的次数
array_product 数组中所有值的乘积
array_sum 数组中所有值的总和
count 数组中有多少个元素
sizeof 这是count()的别名

Note

空数组的乘积是 1,而不是 0。 16

遍历数组

有两种方法可以循环访问数组,一种是使用游标,另一种是循环访问数组。

遍历数组

一个枚举 PHP 数组可以通过增加一个索引计数器来循环,但是这对关联数组不起作用。更好、更健壮的方法是使用foreach()构造。

它可以让你快速查看foreach()使用的两种可能的语法,然后继续。如果你正在考虑参加考试,你应该已经熟悉了它的用法,所以这是为了其他语言的程序员的利益。

<?php
$arr = [
    'a' => 'apple',
    'b' => 'banana',
    'c' => 'cherry'
];
foreach($arr as $value) {
    echo $value . PHP_EOL;
}
foreach($arr as $key => $value) {
    echo $key . ' = ' . $value . PHP_EOL;
}

第一个foreach()循环将遍历数组并将数组值传递给代码块。第二个foreach()循环遍历它并传递键和值。

默认情况下,PHP 将值传递给一个foreach()循环的代码块。如果您更改代码块中的值,它不会对代码块之外产生影响。但是,您可以通过在值前面加一个&符号来标记要通过引用传递的值。

Caution

通常人们会对你在foreach()循环中使用引用表示不满。

我们将在下面的代码示例中看到这一点,该示例还演示了在foreach块中声明的变量在包含范围中被定义。循环结束后,它将保存循环中的最后一个值。但是,依赖这个特性会使代码更难阅读。

<?php
$arr = [1,2,3];
foreach ($arr as $value) {
    $value += 1;
}
echo implode(', ', $arr) . PHP_EOL;   // 1, 2, 3
echo $value . PHP_EOL;                // 4
foreach ($arr as &$value) {
  $value += 1;
}
echo implode(', ', $arr) . PHP_EOL;   // 2, 3, 4
echo $value;

从 PHP 5.5 开始,list()构造可以用在foreach()循环中来解包嵌套数组。这在处理数据库结果时特别有用。

下面是一个使用列表的示例:

<?php
// assigning to scalars
list($animal, $food, $mood) = ['cat', 'cheeseburgers', 'grumpy'];
echo "{$animal}s eat $food except when they're $mood." . PHP_EOL;

// assigning to an array
$info = [];
list($info[0], $info[1], $info[2]) = ['cat', 'cheeseburgers', 'grumpy'];
var_dump($info);

/*
cats eat cheeseburgers except when they're grumpy.
array(3) {
  [0]=>
  string(3) "cat"
  [1]=>
  string(13) "cheeseburgers"
  [2]=>
  string(6) "grumpy"
}
*/

Note

关键字each也可以用于循环数组,但在 PHP 7.2.0 中不推荐使用(所以在 PHP 7.1 中也不要使用它)

使用数组游标

每个数组都有一个指向当前元素的光标或指针。许多 PHP 函数使用光标来决定对哪个元素进行操作。

以下是基本的光标功能:

功能 表演
reset 将光标移动到数组的开头 17
end 将光标移动到数组的末尾
next 推进光标 18
prev 向前移动光标
current 返回光标指向的元素的值
key 返回光标指向的元素的键

可以使用相同的语法迭代对象,但是知道它们实现接口迭代器是很重要的。

游标的一个不太常见的用法如下:

<?php
$arr = [
  'a' => 'apple',
  'b' => 'banana',
  'c' => 'cherry'
];
while (list($var, $val) = each($arr)) {
  echo "$var is $val" . PHP_EOL;
}

list()是一种语言结构,从提供的数组中分配变量。each()函数从数组中返回当前的键和值对,并移动数组光标。

遍历数组

array_ walk()函数将一个用户可调用的函数应用于数组中的每个元素。它有两个参数——对数组的引用和可调用的。

这个可调用函数将被传递两个参数。第一个是数组中元素的值,第二个是它的索引。

一些内部函数,例如strtolower()如果接收到太多参数会抛出警告,因此不适合作为array_walk()的回调。

Note

如果需要回调函数来改变数组的值,应该确保通过引用传递第一个参数。

下面是一个将数组中的所有元素转换为大写的示例:

<?php
$arr = [
  'a' => 'apple',
  'b' => 'banana',
  'c' => 'cherry'
];
array_walk($arr, function(&$value, $key) {
  $value = strtoupper($value);
});
print_r($arr);

注意,我通过引用将值传递到我的 lambda 函数中,因此在 lambda 中更改它将影响到$arr变量。

如果我们使用strtoupper()作为回调,PHP 会产生警告。作为一个练习,试着找出为什么会这样。

排序数组

PHP 提供了几个排序函数。

它们遵循一种命名惯例,即基本的sort函数以r为前缀表示反向,以a为前缀表示关联。

所有排序函数都将数组引用作为其参数,并返回一个指示成功或失败的布尔值。

功能 用于
sort 按字母顺序排列数组
rsort 反向字母排序
asort 关联排序
arsort 反向关联排序
ksort 键短
krsort 反向键排序
usort 用于排序的用户定义的比较函数
shuffle 伪随机排序

关联排序将按值排序,并维护索引关联。请看他们其中一个手册页 19 的例子。

所有函数(除了usort())都接受一个可选参数来指示sort标志。这些标志是预定义的常量:

意义
SORT_REGULAR 正常比较项目;不要换类型。
SORT_NUMERIC 将项目转换为数值,然后进行比较。
SORT_STRING 将项转换为字符串,然后进行比较。
SORT_LOCALE_STRING 使用区域设置将项转换为字符串。
SORT NATURAL 使用自然顺序排序,像函数natsort()
SORT_FLAG_CASE 可以与SORT_STRINGSORT_NATURAL结合使用,对字符串进行不区分大小写的排序。

自然顺序排序

自然排序是对人类有意义的排序顺序。这是一种字母排序顺序,但多个数字被视为一个字符。

功能natsort()不带标志,与设置了SORT_NATURAL标志的sort()相同。

作为一个例子,让我们从一个在人眼看来已经排序的字符串开始,对它进行洗牌,然后使用两种排序形式来看看它是如何排序的:

<?php
$a = $b = explode(' ', 'a1 a2 a10 a11 a12 a20 a21');
shuffle($a);
shuffle($b);
natsort($a);
sort($b);
print_r($a);
print_r($b);

注意,我使用了explode函数将一个字符串分解成一个数组。

这将输出:

Array
(
    [5] => a1
    [2] => a2
    [0] => a10
    [4] => a11
    [6] => a12
    [3] => a20
    [1] => a21
)
Array
(
    [0] => a1
    [1] => a10
    [2] => a11
    [3] => a12
    [4] => a2
    [5] => a20
    [6] => a21
)

标准 PHP 库(SPL) : ArrayObject 类

SPL 库包括ArrayObject类,允许你从数组中创建对象。这些对象可以使用手册页上列出的ArrayObject类的方法。

这让你可以把数组当作对象来处理,就像 PHP 手册 20 中的例子一样:

<?php
$fruits = array("d" =>"lemon", "a" =>"orange", "b" =>"banana", "c" =>"apple");
$fruitArrayObject = new ArrayObject($fruits);
$fruitArrayObject->ksort();
foreach ($fruitArrayObject as $key => $val) {
  echo "$key = $val\n";
}

当构造一个ArrayObject时,您传递一个输入,它可以是一个数组或者一个对象。

您还可以选择指定标志:

影响
ArrayObject::STD_PROP_LIST 当作为列表(var_dumpforeach等)访问时,对象的属性具有其正常功能。).
ArrayObject::ARRAY_AS_PROPS 条目可以作为属性来访问(readwrite)。 21

这些标志可以用setFlags()方法设置,如手册中的例子所示:

<?php
// Array of available fruits
$fruits = array("lemons" => 1, "oranges" => 4, "bananas" => 5, "apples" => 10);

$fruitsArrayObject = new ArrayObject($fruits);

// Try to use array key as property
var_dump($fruitsArrayObject->lemons);
// Set the flag so that the array keys can be used as properties of the ArrayObject
$fruitsArrayObject->setFlags(ArrayObject::ARRAY_AS_PROPS);
// Try it again
var_dump($fruitsArrayObject->lemons);

此示例将输出:

        NULL
        int(1)

Chapter 4 Quiz

Q1:PHP 键区分大小写吗?这个脚本的输出会是什么?

| 这会产生一个错误 |
| 2 |
| 4 |
| 以上都不是 |

<?php
$arr1 = ["A" => "apple", "B" => "banana"];
$arr2 = ["a" => "aardvark", "b" => "baboon"];
echo count($arr1 + $arr2);

Q2:这个脚本会输出什么?

| Found |
| Nothing |
| Warning: in_array() expects parameter 2 to be array |
| 以上都不是 |

<?php
$arr = [
  'a' => 'apple',
  'b' => 'banana',
  'c' => 'cherry'
];
$keys = array_keys($arr);
if (in_array($keys, 'a')) {
  echo "Found";
}

Q3:这个脚本会输出什么?

| 0 |
| 1 |
| 2 |
| 3 |
| 以上都不是 |

<?php
$birds = ['duck', 'chicken', 'goose'];
$net = ['dog', 'cat', 'chicken', 'goose', 'hamster'];
echo count(array_intersect_assoc($net, $birds));

Q4:这个脚本会输出什么?

| 这会产生一个错误 |
| int(1) |
| string(6) "lemons" |
| 以上都不是 |

<?php
// Array of available fruits
$fruits = array("lemons" => 1, "oranges" => 4, "bananas" => 5, "apples" => 10);
$fruitsArrayObject = new ArrayObject($fruits);
$fruitsArrayObject->setFlags(ArrayObject::ARRAY_AS_PROPS);
// Try to use array key as property
var_dump($fruitsArrayObject->lemons);

Q5:这个脚本会输出什么?

| 这会产生一个错误 |
| 2 |
| 3 |
| 5 |

<?php
$a = array('one','two');
$b = array('three','four','five');
echo count($a + $b);

Q6:这个脚本会输出什么?

| 这会产生一个错误 |
| 3 |
| 2 |
| 1 |

<?php
$a = array('three','four','five');
$b = array('one','two');
echo count($a - $b);

Q7:以下代码的输出是什么?

| 这会产生一个错误 |
| 2 |
| 3 |
| 4 |

<?php
$source = '12,23,34';
$arr = str_split($source, 2);
echo count($arr);

问题 8:这段代码会输出什么?

| 调用krsort()时出错 |
| 调用 array_ flip()时出错 |
| 您不能引用键'PHP',因为数组中有多个键 |
| 1 |
| 5 |
| 6 |

<?php
$keys = range(1, 6, 2);
$arr = array_fill_keys($keys, 'PHP');
krsort($arr);
$arr = array_flip($arr);
echo $arr['PHP'];

问题 9:这段代码会输出什么?

| A: 1; B: 2 A: 3; B: 4 |
| Notice: Undefined offset: 1 |
| Undefined variable $a |
| 以上都不是 |

<?php
$array = [
  [1, 2],
  [3, 4],
];
foreach ($array as list($a, $b)) {
  echo "A: $a; B: $b" . PHP_EOL;
}

Q10:这段代码会输出什么?

| 这会产生一个错误 |
| 1 |
| 3 |
| 5 |

<?php
$arr = [1,2,3,4,5];
$spliced = array_splice($arr, 2, 1);
$number = array_shift($arr);
echo $number;

Footnotes 1

https://php.net/manual/en/ref.array.php

2

https://php.net/manual/en/function.implode.php

3

https://php.net/manual/en/function.preg-split.php

4

https://php.net/manual/en/function.str-split.php

5

https://secure.php.net/manual/en/function.array-shift.php

6

https://secure.php.net/manual/en/function.array-unshift.php

7

https://secure.php.net/manual/en/function.array-pop.php

8

https://secure.php.net/manual/en/function.array-push.php

9

https://php.net/manual/en/function.array-udiff.php

10

https://php.net/manual/en/function.array-diff.php

11

https://php.net/manual/en/function.array-udiff.php

12

https://php.net/manual/en/function.array-udiff-assoc.php

13

https://php.net/manual/en/function.array-diff-uassoc.php

14

https://php.net/manual/en/function.array-chunk.php

15

https://php.net/manual/en/function.array-slice.php

16

https://en.wikipedia.org/wiki/Empty_product

17

https://secure.php.net/manual/en/function.reset.php

18

https://secure.php.net/manual/en/function.next.php

19

https://php.net/manual/en/function.asort.php

20

http://php.net/manual/en/class.arrayobject.php

21

https://secure.php.net/manual/en/class.arrayobject.php

五、面向对象的 PHP

面向对象的代码比过程代码运行得慢,但是更容易建模和操作复杂的数据结构。PHP 从 3.0 版本开始就支持面向对象编程,从那以后它的对象模型得到了广泛的扩展和改造。

这本书不打算教面向对象编程,而是将重点放在 PHP 的实现上。你应该至少有一些 PHP 编程的经验。

Tip

这是认证考试最重要的三个部分之一。

声明类和实例化对象

使用关键字class声明类。

<?php
class ExampleClass
{
    // class code
}

可以使用与变量相同的规则来命名类。您的编码标准将决定您使用的大小写约定。

要从一个类中实例化一个对象,可以使用new关键字:

<?php
$exampleObject = new ExampleClass();
// If you are not passing constructor parameters you can omit the brackets if you choose
$anotherObject = new ExampleClass;

我们稍后将处理细节,但是下面的概要参考表显示了继承和特征的语法和限制。

概念 句法 限制
从一个类继承 class A extends A_Parent 类可能只有一个父类
接口继承 Interface A extends B, C 接口可以继承多个接口
从抽象类继承 Interface A extends B, C 接口可以继承多个接口
实现接口 class A implements A_Interface 类可以实现多个接口
特点 class Foo { use A_trait; } 类可以使用多个特征

对象分配总是通过引用。

请注意,在下面的示例中,当我们更改复制对象的属性时,原始对象也会随之更改。事实上,这两个变量在内存中占据相同的空间,因为引用是指向原始数据的指针。我们并没有制作该对象的全新副本。

<?php
$a = new stdClass();
$a->property = "Hello World";
// object assignment is by reference
$b = $a;
$b->property = "Assigned by reference";
// $a has also changed because $b is a pointer to $a
var_dump($a);
/*
object(stdClass)#1 (1) {
  ["property"]=>
  string(21) "Assigned by reference"
}
*/

当我们在“使用对象”一节中学习到关键字clone时,我们将会更详细地讨论这个问题。

自动加载类

应该在使用类之前定义它们,但是可以在需要时使用自动加载来加载类。与控制 PHP 在哪里寻找类的编码标准(如 PSR4)一起,这可能是一个不可或缺的特性。

Tip

Zend 考试不会问你关于 PSR4 的问题,但是 FIG 组提出的标准在 PHP 世界里非常重要。

PHP 中的自动加载是通过spl_autoload_register()函数完成的。PHP FIG 组网页上给出了一个符合 PSR4 的实现, 1 但是让我们看一个来自 PHP 手册 2 的更简单的演示作为例子:

<?php
function my_autoloader($class) {
  include 'classes/' . $class . '.class.php';
}

spl_autoload_register('my_autoloader');

// Or, using an anonymous function as of PHP 5.3.0
spl_autoload_register(function ($class) {
  include 'classes/' . $class . '.class.php';
});

使用spl_autoload_register()可以指定 PHP 在无法加载类时将调用什么函数。您可以在此函数中包含文件,并声明该类。如果 PHP 在这个函数运行后找不到这个类,那么它将抛出一个致命错误。

可见性或访问修饰符

方法或属性的可见性可以通过在声明前加上publicprotectedprivate来设置。

  • 可以从任何地方访问公共类成员。
  • 受保护的类成员可以从类内部及其子级进行访问。
  • 私有类成员只能从类本身内部访问。

如果你没有明确指定可见性,那么它将默认为public

接口只能包含public方法。任何实现该接口的类都必须匹配该方法的可见性,因此这些方法在其中也将是公共的。

abstract类中的方法可以有任何可见性。扩展抽象类的类中的方法必须具有相同或更少限制的可见性。

实例属性和方法

从类中创建的具体对象也称为实例。当你从一个类中创建一个对象时,你被称为实例化该对象。本节重点介绍属于对象的属性和方法。我们将看看这些是什么,PHP 语法如何工作,命名规则,以及如何使用它们。

性能

通过使用一个可见性修饰符后跟属性名来声明类属性。属性名遵循与变量相同的命名规则。

<?php
class Properties
{
    // You do not have to specify a default value
    public $email;
    // A scalar value is an expression
    protected $name = 'Alice';
    // An array is an expression
    protected $accounts = ['cheque', 'savings'];
    // You can use a constant expression as a default value
    private $balance = 60 * 5;
}

属性可以初始化为默认值。它们可以用表达式初始化,但不能用函数初始化。

<?php
class BrokenPropertyInit
{
    private $lastLogin = time();  // won't run
}

该示例不会运行,因为您无法使用函数初始化 class 属性。

方法

方法是作用域构造中的函数。它们是在函数中通过使用可见性修饰符后跟函数声明来声明的。如果省略可见性修饰符,该方法将具有公共可见性。

<?php
class MethodExample
{
  private $name;

  // explicitly specified visibility
  public function setName($name) {
      $this->name = $name;
  }

  // public visibility by default
  function getName($name) {
      return $this->name;
  }
}

方法可以使用$this伪变量访问非静态对象属性。

$this伪变量在对象中定义,指的是对象本身。

静态方法是在没有实例化对象的情况下声明的,因此$this不可用。

静态方法和属性

将一个方法或属性声明为 static 使得它不需要类的具体实现就可以使用。

因为可以在没有实例化对象的情况下调用静态方法,所以伪变量$this在这些方法中是不可访问的。静态方法和属性可以应用任何可见性修饰符。

您不应该静态地调用非静态方法。这将生成一个弃用警告:

<?php
class A
{
  // this is not a static method
  public function sayHello()
  {
    echo "Hello World";
  }
}
// Deprecated: Non-static method A::sayHello() should not be called statically
A::sayHello();

引用静态属性或方法是使用范围解析运算符完成的,它是一个双冒号。

<?php
class MyClass
{
    // Static functions are declared with the static keyword
    public static function sayHello() {
        echo "Hello World" . PHP_EOL;
    }

    public function someFunction() {
        // self refers to "this class", like $this refers to an object
        self::sayHello();
    }
}
// Static functions can be accessed with the scope resolution operator.
MyClass::sayHello(); // Hello World
$object = new MyClass();
$object->someFunction(); // Hello World

当我们从类内部引用一个静态属性时,我们可以使用selfparentstatic来引用它。我们将在本章的“后期静态绑定”一节中讨论static关键字。

当从类外部引用静态类成员时,可以用类名作为范围解析运算符的前缀。在前面的例子中,我们用MyClass::sayHello()引用了静态函数。

静态属性

静态属性也用关键字static声明,可以用范围解析操作符访问。

例如:

<?php
class Foo
{
  // Static properties are declared with the static keyword
  private static $message = 'Hello World';

  public function __construct() {
    // Static properties can be accessed with the scope resolution operator.
    echo self::$message;
  }
}
$foo = new Foo;     // Hello World
echo Foo::$message; // PHP Fatal error: Cannot access private property         Foo::$message

在这个例子中,我们使用关键字self访问构造函数中的static属性。为了演示静态属性可以应用任何可见性,我们尝试从类外部访问它,并收到一个致命错误。

使用对象

这是本章非常重要的一节,你应该密切注意细节。我们将介绍“浅”拷贝和“深”拷贝的区别,并看看数组变量是如何被转换成其他变量类型的。我们将看到如何存储一个对象供以后使用(或者将它传递给另一个程序),还将看到一些通过别名类名可以玩的把戏。

复制对象

就像赋值一样,PHP 总是通过引用传递对象。我们不是制作对象的整个副本,而是说“数据可以在这个位置找到”。我们将在本书的“内存管理”一节中更多地讨论 PHP 内存分配。

如果要创建对象的副本,必须使用clone()关键字。

<?php
// creating a shallow copy of an object
$objectCopy = clone $originalObject;

PHP 将创建该对象的浅层副本。在浅层复制中,如果源包含对变量或其他对象的引用,那么这些引用将被复制到新对象中。这意味着原始对象和克隆对象共享对同一目标对象的引用。

相比之下,深层副本创建被引用对象的新版本,并将对这些对象的引用插入到克隆对象中。这种方法速度更慢,成本更高,因为它需要创建更多的对象。克隆的对象将包含对原始对象引用的对象的新副本的引用。

当克隆一个对象时,PHP 会尝试执行对象中的__clone()方法。您可以重写此方法,以包含您自己的克隆对象行为。不能直接调用此方法。

Tip

如果你想要一个对象的深度克隆,你可以在神奇的方法__clone()中实现这个逻辑。

序列化对象

对象序列化是通过serialize()unserialize()函数完成的。这些函数支持任何类型的 PHP 变量,除了资源。

当一个对象被序列化时,PHP 将尝试对它调用__sleep()方法,当它被非序列化时,调用__wakeup()函数。这些是神奇的方法,你可以在你的类中实现它们来改变 PHP 处理这些事件的方式。

序列化一个对象给出了可以存储在 PHP 中的任何值的字节流表示。资源无法序列化。PHP 中的字符串可以包含字节流,因此可以将序列化的对象放入其中。

该字符串将引用被序列化的对象的类,并将包含与之关联的所有变量。对对象外部任何内容的引用都不能被存储,并且将会丢失,但是对对象内部任何内容的循环引用将会保留。

当您取消序列化对象时,PHP 必须声明该类。如果没有定义类,它将无法创建正确类型的对象,而是创建一个没有方法的类型__PHP_Incomplete_Class_Name的对象。

这是一个简单的例子,我们序列化和反序列化一个对象。

<?php
$objectOriginal = new A;
$string = serialize($objectOriginal);
file_put_contents('serialize.txt', $string);
// in another PHP file
$string = file_get_contents('serialize.txt');
$objectCopy = unserialize($string);

围绕序列化对象有很多潜在的安全问题。我们将在关于安全性的第六章中讨论它们,但是当你考虑非序列化的第二个(可选)参数时,记住这一点是值得的。此参数有助于减轻攻击,对手可以更改传递给unserialize(). 3 的参数值

unserialize 的第二个参数让您指定 PHP 应该愿意取消序列化的内容。始终使用它是安全的最佳做法。

价值 意义
省略 PHP 可以实例化任何类的对象
FALSE 不接受任何课程
TRUE 接受所有课程
类名数组 仅接受指定的类别
任何其他值 Unserialize()将返回 false 并发出一个E_WARNING

下面是一个更全面的例子,说明如何在 PHP 中取消对象的序列化:

<?php
class A {
    public function __wakeup() {
        echo "Good morning";
    }
};
class B {}
$a = new A();
$stored = serialize($a);
unset($a);
// this works because the class name is allowed
$a = unserialize($stored, ['allowed_classes' => [A::class]]);
// this creates __PHP_Incomplete_Class because the class doesn't match
$b = unserialize($stored, ['allowed_classes' => [B::class]]);
// this creates __PHP_Incomplete_Class because no classes are allowed
$c = unserialize($stored, ['allowed_classes' => false]);
// this works because all classes are allowed
$d = unserialize($stored, ['allowed_classes' => true]);
// this generates a warning because the parameter type is incorrect
$e = unserialize($stored, ['allowed_classes' => 'Not boolean or array']);

Caution

不要使用serialize()向用户传递数据。宁可用json_encode!为什么不呢?因为“所有用户输入都是潜在邪恶的”这句口头禅。你不想让用户有机会通过unserialize()运行他们的代码。

数组和对象之间的转换

我们在 PHP 基础知识一章中讨论了变量的造型。我们应该注意,也可以使用相同的语法在数组和对象之间进行转换。让我们看看:

<?php
$array = [
    'key' => 'value',
    'nested_array' => [
            'another_key' => 'different_value'
        ]
    ];
$object = (object)$array;
var_dump($object);

在这个例子中,我使用了(object)转换语法来强制数组成为一个对象。PHP 将产生一个对象StdClass,它具有与数组的键相对应的属性。此代码输出:

object(stdClass)#1 (2) {
  ["key"]=>
  string(5) "value"
  ["nested_array"]=>
  array(1) {
    ["another_key"]=>
    string(15) "different_value"
  }
}

Note

嵌套数组不会转换为嵌套对象。

可以使用(array)转换语法将一个对象转换成一个数组。如果我们在代码清单的末尾运行命令assert((array)$object === $array);,代码将会无错地完成,因为断言通过了。

将对象转换为字符串

您可以通过声明__toString()方法来定义如何将对象转换为字符串。当 PHP 试图将你的对象转换成字符串时,它会调用这个方法并返回结果。

<?php
class User
{
    private $firstName = 'Example';
    private $lastName = 'User';
    function __toString() {
        return $this->firstName;
    }
}
$user = new User;
// 'echo' expects a string type so PHP will implicitly cast the object to string
echo $user; // Example

这使您可以构建和格式化对您的对象有意义的字符串。如果您没有在对象上声明这个方法,那么 PHP 将生成一个可捕捉的致命错误,告诉您它不能将对象转换为字符串。

类别别名

PHP 允许您使用class_alias()函数为类创建别名。该函数接受三个参数——原始类名、为其创建的别名以及一个可选的布尔值,该值指示如果找不到类,是否必须调用自动加载程序。

乍看之下,可能无法立即看出类别名的用例是什么。它们的主要用例是有条件地导入名称空间。

use关键字是在编译时而不是运行时处理的。这意味着不可能使用条件逻辑来更改要导入的名称空间。class_alias()函数允许您有条件地导入名称空间。

例如,您可能希望根据 memcached 扩展是否可用来交换使用哪个类来缓存数据库。在下面的代码中,我们不能用关键字use导入替代类,但是通过使用类别名,我们可以改变cache引用的类。

<?php
if (extension_loaded('memcached')) {
    class_alias('Memcached', 'Cache');
} else {
    class_alias('InternalCacheProvider', 'Cache');
}
class Database
{
    // The cache class is aliased to either Memcached or the InternalCacheProvider
    public function __construct(Cache $cache) {}
}

构造函数和析构函数

构造函数是从类实例化对象时运行的方法。类似地,当对象被卸载时,会产生一个析构函数。

它们的声明如下例所示:

<?php
class constructorExample
{
  // called when instantiated
  public function __construct() {

  }
  // called when unloaded
  public function __destruct() {

  }

  // PHP4 style constructor - deprecated in PHP7

  public function constructorExample() {

  }

}

构造函数优先级

在 PHP 4 中,构造函数方法通过与定义它们的类同名来识别。这种形式的构造函数在 PHP 7 中已被否决。

<?php
class constructorExample
{
  // PHP4 style constructor - deprecated in PHP7
  public function constructorExample() {
    echo "Constructed!";
  }
}
$test = new constructorExample;

为了向后兼容,如果找不到__construct()函数,PHP 7 将搜索与类同名的函数。该功能在 PHP 7 中已被否决,并将在未来的 PHP 版本中被删除。

如果我们从这个类构造一个对象,我们不会收到弃用警告。为什么不呢?PHP 7.1 首先寻找一个现代风格的构造函数,如果它存在,它将调用它。如果没有现代的构造函数,PHP 7.1 将寻找一个被否决的构造函数,如果它存在,它将生成一个警告并调用它。

构造函数参数

如果一个类构造函数带有一个参数,那么在实例化该类的一个实例时,您需要传递它。

<?php
class User {
  public function __construct($name) {
        $this->name = $name;
        }
}
$user = new User('Alice');

这里我们将字符串"Alice"传递给构造函数。这方面的一个实际例子是依赖注入。4

遗产

PHP 在其对象模型中支持继承。如果你扩展了一个类,那么子类将继承父类的所有非私有属性和方法。换句话说,子类将拥有父类的公共和受保护元素。您可以在子类中重写它们,但是它们将具有相同的功能。

PHP 不支持一次从多个类继承。

导致类继承的语法非常简单。在声明类时,我们只需指出它所扩展的类的名称,如下例所示:

<?php

class ParentClass
{
    public function sayHello() {
        echo __CLASS__;
    }
}

class ChildClass extends ParentClass
{
    // nothing in this class
}

$kid = new ChildClass;
$kid->sayHello(); // ParentClass

在这个例子中,ChildClass被声明为扩展了ParentClass。它继承了sayHello()方法。

如果我们要定义一个继承自ChildClassGrandChildClass,那么它也将继承所有的ParentClass方法。事实上,继承链中的任何类都将继承其祖先的所有方法和属性。

Note

神奇常数__CLASS__给出了当前正在执行的类的名称。我们在子类中调用继承的方法,但是它在父类中执行函数,因此报告类名为ParentClass

最后一个关键字

PHP 5 引入了final关键字。您可以将它应用于整个类,也可以应用于类中的特定方法。关键字final的作用是防止类被扩展或者方法被覆盖。所有最终属性和方法的可见性都是公共的。

将类或函数标记为 final 有助于避免在扩展类时错误地更改行为。

如果您试图覆盖子类中的 final 方法,或者如果您试图声明一个扩展了标记为 final 的类的类,PHP 将发出致命错误。

您可以通过在类或方法的定义前使用final关键字将类或方法标记为 final,就像下面这个例子,我将函数标记为 final:

class Employee
{
    final public function calculateWage(float $hourlyRate, int $numHoursWorked)
    {
        return $hourlyRate * $numHoursWorked;
    }
}

让我们看另一个例子,它显示了产生的错误并突出了关键字的有用性。

以下示例中的代码清单没有使用任何关键字final,因此运行时不会出错,并为雇员计算出一份相当丰厚的工资。我已经注释了两行,分别显示了当我们将类或方法标记为 final 时将会抛出的错误。

<?php
// Fatal error: Class Oops may not inherit from final class (Employee)
final class Employee
{
    \final public function calculateWage(float $hourlyRate, int $numHoursWorked)
    {
        return $hourlyRate * $numHoursWorked;
    }
}
// Fatal error: Class CannotExtendFinalClass may not inherit from final class (Employee)
class Oops extends Employee {
    // Fatal error: Cannot override final method Employee::calculateWage() in /in/afkAJ on line 17
    public function calculateWage(float $hourlyRate, int $numHoursWorked) {
        if ($this->employeeName === 'Andrew') {
            return 1000000;
        }
        return $hourlyRate * $numHoursWorked;
    }
}
$oops = new Oops;
$oops->employeeName = 'Andrew';
echo $oops->calculateWage(10.00, 50);

Note

这与 Java 中 final 的使用有些不同,Java final 关键字的 PHP 等价物是const

最重要的

子类可以声明一个与父类同名的方法,前提是该方法在父类中没有被标记为 final。

子级中的方法参数签名必须与父级相似;例如,下面的代码将生成一条警告,指出子声明需要与父声明兼容:

<?php
class Employee
{
    public function calculateWage(float $hourlyRate, int $numHoursWorked)
    {
        return $hourlyRate * $numHoursWorked;
    }
}

class Oops extends Employee {

    public function calculateWage(int $hourlyRate, int $numHoursWorked) {
        return $hourlyRate * $numHoursWorked;
    }
}

如果一个函数像这样被覆盖并在子类上被调用,那么父类将不会被调用。

这适用于构造函数和析构函数,但在这些情况下,通常是这样解决的:

<?php
class ChildClass extends ParentClass
{
    public function __construct() {
        parent::__construct();
        // more constructor functions here
    }
}

parent::__construct()的调用将调用父类的构造函数方法。当控制流返回到子节点时,将调用其构造函数中的其余函数。

如果子类覆盖了父类的方法,那么子类的可见性不能低于父类。

换句话说,如果父方法是公共的,那么子方法就不能重写为受保护的或私有的方法。

接口

接口允许您指定一个类必须实现什么方法,而无需指定实现的细节。

它们通常用于在面向服务的架构范例中定义契约,但是也可以在您想要规定未来的类如何与您的代码交互时使用。

接口中的所有方法都必须声明为公共的,并且它们本身不能有任何实现。

接口不能有属性,但是可以有常量。

接口的声明如下所示:

<?php
interface PaymentProvider
{
    public function showPaymentPage();
    public function contactGateway(array $messageParameters);
    public function notify(string $email);
}

一个类将被声明为实现它,如下所示:

<?php
class CreditCard implements PaymentProvider
{
    public function showPaymentPage() {
        // implementation
    }

    public function contactGateway() {
        // implementation
    }

    public function notify(string $email) {
        // implementation
    }
}

通过列出用逗号分隔的接口名称,类可以一次实现多个接口。

类可能只从一个类继承,但可能实现许多接口。

抽象类

PHP 支持抽象类,即包含一个或多个抽象方法的类。abstract方法是已经声明但还没有实现的方法。

在下面这个abstract类的例子中,函数girlDescendingStairs()是一个抽象方法。它是使用abstract关键字定义的,没有任何实现。注意这里没有用于abstract方法的代码块。

<?php
abstract class Paintings
{
    abstract protected function girlDescendingStairs();
    protected function persistenceOfMemory() {
        echo " I have an implementation so this is not an abstract method ";
    }
    public function __construct() {
        echo "I cannot be constructed!";
    }
}

无法构造一个abstract类;我们不能从类Paintings创建新对象。

抽象类是用来扩展的。扩展abstract类的类必须定义父类中所有标记为抽象的方法。如果子类不实现这些方法,它们也必须被标记为抽象,因此子类也将是抽象的。

当一个子类扩展一个abstract类时,它必须定义具有相同或更少限制的可见性的抽象方法。

子类中声明的方法的签名必须与抽象方法的签名匹配。这意味着该方法的必需(非可选)参数的数量和类型必须相同。

私有方法不能标记为抽象。让我们看看如何扩展抽象类:

<?php
abstract class Paintings
{
    abstract protected function girlDescendingStairs();
    protected function persistenceOfMemory() {
        echo "I have an implementation so this is not an abstract method";
    }
    public function __construct() {
        echo "I am being constructed!";
    }
}
class Foo extends Paintings {
    public function girlDescendingStairs() { echo "Whee!"; }
}
$foo = new Foo;  // I cannot be constructed!
$foo->girlDescendingStairs();  // Whee!

我定义了一个新的类,我把它想象成Foo,它扩展了抽象类。我已经实现了抽象方法girlDescendingStairs,并将可见性从protected更改为限制较少的范围public。我还没有覆盖抽象类定义的非抽象方法。

Foo类没有抽象方法,所以我可以从它构造一个对象。注意,当我这样做时,父类的构造函数被调用,因此Foo错误地报告它不能被构造。

匿名类

PHP 7 引入了匿名类,允许你动态定义一个类,并从中实例化一个对象。下面是一个使用匿名类的简单例子:

<?php
$object = new class('argument') {
    public function __construct(string $message) {
        echo $message;
    }
};

请注意我们是如何内联定义类的,并且可以使用与从命名类创建对象类似的语法来传递参数。这段代码将输出字符串"argument",因为调用了构造函数,并将字符串“argument”传递给了它。

匿名对象的一个用例是扩展一个命名类;例如,如果要重写方法或属性。不必在单独的文件中声明一个类,您可以创建一个一次性的内联实现。

反射

PHP 反射 API 允许您在运行时检查 PHP 元素并检索关于它们的信息。

反射 API 是在 PHP 5.0 中引入的,因为 PHP 5.3 在默认情况下是启用的。

使用反射的一个常见地方是在单元测试中。反射有用的一个例子是测试类中私有属性的值。您可以使用反射使私有属性可访问,然后进行断言。

有几个反射类允许您检查特定类型的变量。这些类中的每一个都是根据您可以用来检查的变量类型来命名的。

班级 用于检查
ReflectionClass 班级
ReflectionObject 目标
ReflectionMethod 对象的方法
ReflectionFunction 像 PHP 核心函数或用户函数这样的函数
ReflectionProperty 性能

PHP 手册 5 有关于反射类及其方法的详尽文档。

我们简单看一个使用ReflectionClass的例子。

<?php
$reflectionObject = new ReflectionClass('Exception');
print_r($reflectionObject->getMethods());

传递给反射类的构造函数的参数要么是类的字符串名称,要么是类的具体实例(对象)。

ReflectionClass对象有几个方法允许您检索关于被检查类的信息。在前面的例子中,我们输出了一个包含了Exception类所有方法的数组。

类型提示

类型提示允许您指定函数的参数应该是什么类型的变量。

在下面的例子中,我们指定传递给函数printArray()的参数$arr必须是一个数组。

<?php
function printArray(array $arr) {
  echo "<pre>" . print_r($arr,true) . "</pre>";
}

// The parameter to the function must be a class that implements the PaymentProvider interface
function sendNotificationToPaymentProvider(PaymentProvider $paymentProvider)
{
  $paymentProvider->contactGateway($messageParameters);
}

function sayHello(string $name)
{
  echo "Hello " . $name;
}

在 PHP 5 中,如果你传递了一个错误类型的参数,那么就会产生一个可恢复的致命错误。在 PHP 7 中,抛出了一个TypeError异常。

从 PHP 7 开始,类型提示被称为“类型声明”。我将使用这个新的命名法,但是这些术语在 PHP 的上下文中是可以互换的。

您可以将复合类型、可调用类型和标量变量类型指定为类型提示。此外,如果将NULL用作函数的默认参数,则可以使用NULL类型提示。

<?php
function nullExample(null $msg = null) {
    echo $msg;
}

如果您指定一个类作为类型提示,那么它的所有子类和实现都将是有效的参数。

<?php
class A {}
class B extends A {}
class C extends B {}

function foo(A $object) {}
$testObj = new C;
foo($testObj);  // no error produced

在这个例子中,我们的函数需要一个 A 类的对象。我们正在传递一个 B 类的对象。因为 B 继承自 A,所以我们的代码将会运行。

如果您提供的类名是一个接口,那么 PHP 将允许任何实现该接口的对象(或者是实现该接口的类的祖先)通过。

类别常数

常数是不可变的值。

类常量允许您在每个类的基础上定义这样的值;它们不会在类别的执行个体之间变更。从该类创建的所有对象都具有相同的类常量值。

类常量遵循与变量相同的命名规则,但是没有前缀符号$。按照惯例,常量名称以大写形式声明。

让我们考虑一个例子:

<?php
class MathsHelper
{
    const PI = 4;

    public function squareTheCircle($radius) {
        return $radius * (self::PI ** 2);
    }
}

echo MathsHelper::PI; // 4

类常量是公共的,因此可以从所有范围访问。当我们从类外部访问它时,我们使用范围解析操作符和声明它的类名。

该值必须是常量表达式,而不是(例如)变量、属性或函数调用。

与传统常量一样,类常量可能只包含标量值。

后期静态绑定

后期静态绑定是在 PHP 5.3.0 中引入的,是一种在静态继承的上下文中引用被调用类(相对于调用类)的方法。

这个想法是引入一个关键字来引用运行时最初调用的类,而不是定义该方法的类。

我们决定使用关键字static,而不是引入一个新的保留字。

转移呼叫

“转移”呼叫是由parent::static::引入的静态呼叫或由功能forward_static_call()调用的呼叫。

如果类因为没有定义方法而退回到继承的类,对self::的调用也可以是转发调用。

后期静态绑定的工作原理是将类存储在最后一个“非转发调用”中。换句话说,后期静态绑定解析将在完全解析的静态调用处停止。

我将详细介绍一个 PHP 手册示例的修改示例。

<?php
class A {
    public static function foo() {
        echo static::who();
    }

    public static function who() {
        return 'A';
    }
}

class B extends A {
    public static function test() {
        A::foo();
        parent::foo();
        self::foo();
    }
}

class C extends B {
    public static function who() {
        echo 'C';
    }
}

C::test(); // ACC

ACC 的输出一开始可能与直觉相反,但是让我们慢慢来。

C::test()的呼叫被完全解决,因此 C 类最初被存储为最后一个非转移呼叫。

函数 C 中没有test()方法,所以调用被隐式转发给它的父类。所以类 B 中的test()方法被调用。

对 A::foo()的调用

test()中的第一个调用专门将类 A 命名为作用域。这意味着呼叫已完全解决。被存储为最后一个非转移呼叫的类被更改为。

调用 A 中的foo()方法,并解析static关键字,以找到对哪个类调用who()方法。

最后一个非转发调用是对 A 中的一个类的,因此调用了 A 中的who()方法。

对 parent::foo()的调用

test()中的下一个调用引用了 B 的父类,因此该调用被显式转发给 B 的父类,即 a。

这是一个转发的调用,因此存储为最后一个完全解析的静态调用(即 C)的值保持不变。

调用 A 中的foo()方法,并解析static关键字,以找到对哪个类调用who()方法。

最后一个非转发调用是对 C 中的一个类的,因此调用了 C 中的who()方法。

调用 self::foo()

类 B 没有定义foo()方法,因此调用被隐式传递给父类 a。

这是一个转发的调用,因此作为最后一个完全解析的静态调用(即 C)存储的值保持不变。

这导致在类 a 中解析静态关键字时调用类 C 的who()方法。

魔术(__*)方法

PHP 将任何名称以两个下划线为前缀的方法视为神奇的方法。PHP 在对象生命周期的特定时间“神奇地”调用这些方法(无需您调用它们)。我喜欢把它们想象成类似于在事件中被调用的钩子。当对象发生相关事件时,PHP 调用 magic 方法。

PHP 并没有为这个类提供一个实现,在你的类中重写这个方法是由你作为程序员来决定的。

魔法方法只适用于类;它们不是独立的功能。

有 15 个预定义的神奇函数,建议避免用双下划线前缀命名其他函数。

__get 和 __set

当 PHP 试图读取(获取)或写入(设置)不可访问的属性时,就会调用这些神奇的方法。

<?php
class BankBalance {
    private $balance;

    public function __get($propertyName) {
        // echo "No property " . $propertyName;
        return "No value";
    }
        public function __set($propertyName, $value) {
        echo "Cannot set $propertyName to $value";
    }
}
$myAccount = new BankBalance();
$myAccount->balance = 100;
// Cannot set balance to 100No value
echo $myAccount->nonExistingProperty;

__get()方法传递了正在查找的属性的名称。您可以为方法中缺少的属性返回值,或者按照您喜欢的方式处理它。

在这个例子中,被注释的代码可以被替换为逻辑来处理丢失的属性,不存在的属性将被设置为No value

一个附加参数$value被传递给__set()

_ _ 已设置和 _ _ 未设置

通过在不可访问的属性上调用isset()函数或empty()来触发__isset()方法。

通过在不可访问的属性上调用unset()函数来触发__unset()方法。

两种方法都接受一个字符串参数,该参数包含作为参数传递给函数的属性的名称。

您可以使用这些神奇的方法让isset()empty()unset()函数处理私有的和受保护的属性。

__call 和 __callStatic

如果您试图在一个对象上调用一个不存在的方法,就会调用这些神奇的方法。唯一不同的是__callStatic()响应静态调用而__call()响应非静态调用。

<?php
class Politician {
    public function __call($method, $arguments) {
        echo __CLASS__ . ' has no ' . $method . ' method';
    }
}

$jacob = new Politician();
$jacob->honesty();  // Politician has no honesty method

在这两种情况下,都会向 magic 方法传递一个字符串,该字符串包含调用试图查找的方法的名称,以及一个传递的参数数组。

_ _ 调用

当你试图将一个对象作为一个函数来执行时,这个神奇的方法__invoke()就会被调用。

<?php
class Square
{
    public function __invoke($var) {
        return $var ** 2;
    }
}
$callableObject = new Square;
echo $callableObject(10); // 100

Caution

这种语法可能会与变量函数名混淆,所以要小心。

_ _ _ debug info

这个神奇的方法由var_dump()在转储对象时调用,以确定应该输出哪些属性。

默认情况下,var_dump()将输出对象的所有公共、受保护和私有属性。

<?php
class Dictatorship {
    private $wmd = 'Nuke';
    public $oil = 'Lots';

    // we are going to hide our wmd
    public function __debugInfo() {
        return [
            'oil' => $this->oil
        ];
    }
}

$country = new Dictatorship();
var_dump($country);

/*
object(Dictatorship)#1 (1) {
  ["oil"]=>
  string(4) "Lots"
}
*/

这个例子将防止$wmd变量包含在var_dump()中。

更多神奇功能

我们已经在“构造函数和析构函数”一节中讨论了__construct()__destruct()函数。

我们已经在“序列化对象”一节中讨论了__sleep()__wake()

我们在讨论“复制对象”时查看了__clone(),在“将对象转换为字符串”一节中查看了__toString()

标准 PHP 库(SPL)

标准 PHP 库是类和接口的集合,是解决常见编程问题的方法。从版本 5.0.0 开始,它就可用 PHP 编译。

这些课分成几类。关于类的完整列表,请参考 SPL 的 PHP 手册。 6

种类 用于
数据结构 标准数据结构,如链表、双向链表、队列、栈、堆等。
迭代程序
例外
文件处理
ArrayObject 用数组函数访问对象。
SplObserverSplSubject 实现观察者模式。

SPL 还提供了几个功能。它们大多属于广泛的反射和自动加载类别。

数据结构

第一类函数是数据结构。如果你已经熟悉了数据结构,你会很高兴知道 SPL 实现了多种数据结构。这些包括双向链表、堆、数组和映射。

数据结构在编程算法中非常有用。

迭代程序

迭代器允许你遍历对象和集合。迭代器维护一个指向元素的游标。

PHP 迭代器将允许你在容器的所有元素中前进或后退光标。它们还允许您执行其他操作,例如,ArrayIterator将允许您对数组执行排序。

如果没有 PHP 提供的类,您将需要自己实现这些迭代器,但幸运的是,所有这些艰苦的工作都由善良的 PHP 作者完成了。

迭代器的列表相当广泛。我不认为你需要列出它们,但是你应该知道它们是 SPL 的一部分。它们将至少提供光标移动能力和一些可能的额外功能。

例外

SPL 也包括标准的Exception类。抛出特定于已发生的错误类型的异常是一种好的做法。这使得编写正确处理异常的catch块变得更加容易。

SPL 引入了一些异常类,这使得抛出特定的异常更加方便。

SPL 异常分为两类——逻辑异常和运行时异常。这些类别中的每一个都有许多异常类,这些异常类关注可能发生的特定种类的错误。

如果它们出现在问题中,你至少应该能够认出它们。

逻辑异常

  • LogicException(延长Exception)
  • BadFunctionCallException
  • BadMethodCallException
  • DomainException
  • InvalidArgumentException
  • LengthException
  • OutOfRangeException
  • Runtime例外情况
  • RuntimeException(延长Exception)
  • OutOfBoundsException
  • OverflowException
  • RangeException
  • UnderflowException
  • UnexpectedValueException

文件处理

SPL 也提供帮助处理文件的课程。

SplFileInfo类为单个文件的信息提供了一个高级面向对象的接口。它提供了一些方法,您可以使用这些方法来查找文件的名称、大小、权限和其他属性。您还可以判断该文件是否是一个目录,是否是可执行的,以及许多其他功能。

SplFileObject类为文件提供了一个面向对象的接口。您可以使用它来打开和读取文件。在处理文件时,有前进或后退文件、查找特定位置的方法以及其他有用的功能。

SplTempFileObject类为临时文件提供了一个面向对象的接口。您可以像使用任何其他输出文件一样使用该文件,但是它会在脚本完成后被删除。例如,您可以在图像处理或验证文件上传时使用它。

数组对象

SPL 还包括各种各样的类和接口。第一个是ArrayObject,允许对象作为数组工作。

当你构造一个ArrayObject时,你可以传递一个数组作为它的参数。最终的对象将具有模拟 PHP 数组函数的方法。

ArrayObject有相当多的限制,但是它的优势之一是你可以定义自己的迭代方式。

观察者模式

最后,让我们看看 SPL 中包含的两个接口— SplObserverSplSubject。注意,这些是接口而不是类,所以您需要实现实际的行为。

这两个接口一起实现了观察者模式。

观察者模式是一种软件设计模式,在这种模式中,一个名为 subject 的对象维护一个名为 observer 的依赖者列表,并自动通知它们任何状态变化,通常是通过调用它们的方法之一。这种模式主要用于实现分布式事件处理系统。

使用这些接口将使您的代码更具可移植性,因为其他库将能够与您的主题和观察者进行交互。

发电机

生成器为您提供了一种创建迭代器对象的简单方法。

使用迭代器和生成器的好处是,您可以构建一个对象,无需计算整个数据集就可以遍历它。这节省了处理时间和内存。

用例可能是替换通常返回数组的函数。该函数将计算所有的值,分配一个数组变量来存储它们,并返回数组。

生成器只计算和存储一个值,并将其输出给迭代器。当迭代器需要下一个值时,它调用生成器。当生成器用完所有值时,它可以退出或者返回一个最终值。

生成器可以像任何迭代器一样被迭代,如下例所示:

<?php
function generator() {
    for ($i = 0; $i < 99; $i++) {
        yield $i;
    }
}

foreach (generator() as $value) {
    echo $value . " ";
}

Yield 关键字

关键字yield类似于一个函数 return,除了它用于在暂停生成器执行时将一个值返回给迭代器。

生成器的范围在调用之间保持不变。在生成器让步后,变量不会失去它们的值。

<?php
function exampleGenerator() {
  // some functions
  $data = yield $value;
}

用钥匙屈服

使用生成器可以生成作为函数关联数组的键值对。

如果没有显式地使用键,那么 PHP 将把产生的值与递增的顺序键配对,就像对枚举数组一样。

生成键值对的语法类似于声明关联数组:

<?php
function myGenerator() {
    // some functions
    yield $key => $value;
}

产生空值

不带参数调用 yield 会导致它产生一个带有自动递增顺序键的NULL值。

通过引用产生

生成器函数可以通过引用产生变量,这样做的语法是在函数名前面加上一个&符号。

<?php
function &referenceGenerator() {
    // some functions
    yield $value;
}

从发电机返回

在您的生成器完成处理后,您可以从中返回一个值。这使得生成器的最终值更加明确。

<?php
function sowCrops() { return 'wheat'; }
function millWheat() { return 'flour'; }
function bake($flour) { return 'cupcake'; }
function generator() {
    $wheat = yield sowCrops();
    $flour = yield millWheat();
    return bake($flour);
};
$gen = generator();
foreach ($gen as $key => $value) {
    echo $key . ' => ' . $value . PHP_EOL;
}
echo $gen->getReturn();
/*
0 => wheat
1 => flour
cupcake
*/

这个语法清楚地表明了生成器的返回值是什么。如果没有它,您将需要假设最后产生的值是返回值。

发电机委托

生成器委托让您将处理值的责任委托给另一个可遍历的对象或数组。

这样做的语法是yield from <expression>:

<?php
function generator() {
    $a = [1,2,3];
    yield from $a;
    yield from range(4,6);
    yield from sevenAteNine();
}
function sevenAteNine() {
    for ($i=7; $i<10;$i++) {
        yield $i;
    }
}
$gen = generator();
foreach ($gen as $value) {
    echo $value . PHP_EOL;
}

在这个例子中,我们使用三种方式将生成委托给另一个可遍历的对象或数组。

运行这段代码的结果是从 1 数到 9。

特征

特性是在 PHP 5.4.0 中引入的,旨在减轻单一继承语言的一些限制。

Note

性状不满足真正遗传的“是-是”关系。如果你熟悉其他语言的 mixins,它们更类似于那些。

trait 包含一组方法和属性,就像一个类一样,但是不能自己实例化。相反,trait 包含在一个类中,然后该类可以使用它的方法和属性,就好像它们是在类本身中声明的一样。

换句话说,特征被放在一个类中,无论方法是在特征中定义的还是在使用该特征的类中定义的,都没有关系。您可以将 trait 中的代码复制并粘贴到类中,并以同样的方式使用它。

trait 中包含的代码旨在封装可重用的属性和方法,这些属性和方法可以应用于多个类。

声明和使用特征

我们使用trait关键字来声明一个特征;为了将它包含在一个类中,我们使用了use关键字。一个类可以使用多个特征。

<?php
trait Singleton
{
    private static $instance;

    public static function getInstance() {
        if (!(self::$instance instanceof self)) {
            self::$instance = new self;
        }
        return self::$instance;
    }
}

class UsingTraitExample
{
    use Singleton;
}

$object = UsingTraitExample::getInstance();
var_dump($object instanceof UsingTraitExample); // true

在这个例子中,我们声明了一个 trait,它包括实现 singleton 模式所需的方法和属性。

当我们想让一个新类遵循单例模式时,我们可以通过使用 trait 来实现。我们不必在类中实现该模式,也不必在继承层次结构中包含该模式。

命名空间特征

如果特征有冲突的名字,PHP 将产生一个致命的错误,但是特征可以在名称空间中定义。

如果您试图在一个不在同一个名称空间层次结构中的类中使用 trait,那么您将需要在包含它时指定完全限定名。

继承和优先

特征可能不会扩展其他特征或类,但是您可以简单地在另一个特征中使用一个特征。

使用特征在类中声明的方法优先于在特征中声明的方法。然而,trait 中的方法将覆盖由类继承的方法。

更简单地说,性状和类别的优先顺序如下:

类成员>特征方法>继承方法

冲突解决

如果两个特征试图插入一个同名的方法,PHP 将产生一个致命错误,除非你明确地解决这个冲突。

PHP 允许您使用insteadof操作符来指定您希望它使用的冲突方法。

这允许您排除一个 trait 方法,但是如果您想保留两个方法,您需要使用as操作符。as操作符允许您包含一个冲突的方法,但是使用不同的名称来引用它。

下面是一个很长的例子来说明这种用法:

<?php
 trait Dog {
    public function makeNoise() {
        echo "Woof";
    }

    public function wantWalkies() {
        echo "Yes please!";
    }
 }

trait Cat {
    public function makeNoise() {
       echo "Purr";
    }

    public function wantWalkies() {
        echo "No thanks!";
    }
}

class DomesticPet
{
    use Dog, Cat {
        Cat::makeNoise insteadof Dog;
        Cat::wantWalkies as kittyWalk;
        Dog::wantWalkies insteadof Cat;
    }
}

$obj = new DomesticPet();
$obj->makeNoise();   // Purr
$obj->wantWalkies(); // Yes please!
$obj->kittyWalk();   // No thanks!

Note

单独使用as是不够的。你仍然需要使用insteadof来排除你不想用的方法,然后你只能使用as来做一个新的方法来引用旧的方法。

能见度

您可以通过扩展关键字use将可见性修饰符应用于函数,如下例所示:

<?php
trait Example {
    public function myFunction() {
        // do stuff
    }
}

class VisbilityExample {
    use Example {
        myFunction as protected;
    }
}

$obj = new VisbilityExample();
$obj->myFunction(); // PHP Fatal error:  Call to protected method

我们指定该方法应该在类中被保护,即使它在 trait 中被声明为 public。您可以在块中包含多个函数,每个函数都有自己的可见性。

Chapter 5 Quiz

Q1:以下哪一个不是有效的 PHP 类名?

| exampleClass |
| Example_Class |
| Example_1_Class |
| 1_Example_Class |
| 它们都是有效的类名 |

Q2:这段代码运行后,属性$name将包含什么?

| Dozy |
| Asleep |
| Rested |
| 这段代码不会运行 |

<?php
class SleepyHead {
    protected $name = "Dozy";

    public function __serialize() {
        $this->name = "Asleep";
    }

    public function __unserialize() {
        $this->name = "Rested";
    }
}

$obj = unserialize(serialize(new SleepyHead()));

Q3:为了让脚本输出"Castor",我们可以用下面哪个语句替换注释行?

| $twin = $star; |
| $twin = clone($star); |
| $twin &= $star; |
| $twin = new clone($star); |

<?php
$star = new StdClass;
// replace this line
$star->name = "Castor";
$twin->name = "Pollux";
echo $star->name; // must be Castor

Q4:假设对象 A 有一个属性是对象 B 的实例,如果我们克隆了对象 A,那么 PHP 是否也会克隆它的属性之一 B?

| 是 |
| 不 |
| 您不能克隆包含对其他对象的引用的对象 |

Q5:不能用相同的名字声明两个函数。选择尽可能多的适用项。

| 真实的 |
| 假的;您可以在不同的名称空间中声明它们 |
| 假的;您可以在它们的构造函数中声明不同数量的参数,PHP 将选择与您的实例化相匹配的定义 |
| 假的;您可以在不同的范围内声明它们 |

Q6:当你对一个对象调用json_encode函数进行序列化时,PHP 会调用哪个神奇的方法?

| __sleep |
| __wake |
| __get |
| __clone |
| 这些都不是 |

对或错:接口只能指定公共方法,但是你的类可以按照你喜欢的方式实现它们。

| 真实的 |
| 假的;接口可以指定任何可见性 |
| 假的;您根本无法在实现时更改可见性 |
| 假的;您只能将可见性更改为不太可见的可见性 |

Q8:这段代码的输出会是什么?

| Hello World |
| I have the world |
| 错误 |
| 以上都不是 |

<?php
class World {
    public static function hello() {
        echo "Hello " . __CLASS__;
    }
}

class Meek extends World {
    public function __call($method, $arguments) {
        echo "I have the world";
    }
}
Meek::hello();

问题 9:在特征、类和继承方法中声明的函数的优先级是以下哪一个?

| 继承方法➤特征方法➤类成员 |
| 类成员➤特质方法➤继承方法 |
| 类成员➤特质方法➤继承方法 |
| 特征方法➤类成员➤继承的方法 |

问题 10:对或错:受保护的方法不能调用私有方法,即使它们在同一个类中。

| 真实的 |
| 错误的 |

Footnotes 1

http://www.php-fig.org/psr/psr-4/examples/

2

https://php.net/manual/en/function.spl-autoload-register.php

3

https://www.owasp.org/index.php/PHP_Object_Injection

4

https://en.wikipedia.org/wiki/Dependency_injection

5

https://php.net/manual/en/class.reflectionclass.php

6

https://php.net/manual/en/book.spl.php

六、安全

安全性是 web 应用的主要关注点。甚至像联合国这样的主要组织也曾被黑客利用非常简单的安全漏洞攻击过。

我认为没有完全安全的系统。我保护应用的目的有两个。首先,我的目标是让攻击者获得访问权限的时间尽可能长。我的下一个目标是把他们能找到的任何信息的价值降到最低。换句话说,我从不认为我的系统是不可渗透的,我总是使用深度防御。

这降低了黑客入侵我的应用的可行性——这将需要很长时间才能进入,当他们进入时,他们需要花费相当大的努力来获得任何有价值的信息。

当你被老虎追赶时,你不需要跑得比老虎快。你只需要比你旁边的小伙子跑得快。

Note

安全的主要缺陷之一是社会工程。关于社会工程的讨论不在 Zend 考试的范围之内,但是你必须记住,不仅仅是你的代码和服务器是你数据的入口点。

配置

配置 PHP 的最佳方法是确保您与最新版本保持同步,并使用它们带来的改进。

如果您没有使用 PHP 最新的稳定版本,而是使用旧版本,那么您应该有一个非常充分的理由。

确保你的操作系统打了补丁。定期应用安全更新,并确保及时了解安全新闻。

只有在有机会确保其他包更新不会对您的堆栈或测试环境产生负面影响时,您才应该应用它们。很可能你的发行版库管理员会小心不去破解常用的栈,但是如果你使用了一个不常用的栈或者从你的库之外安装了软件,那么升级的时候就要小心了。

错误和警告

您应该将 PHP 配置为在生产过程中隐藏警告和错误。错误和警告可以让人了解代码的内部工作方式,比如目录名和正在使用的库。这类信息可以帮助他们利用您的堆栈中的漏洞。

您可以在您的php.ini文件中或者在运行时用error_reporting()函数设置错误报告。两者都有一个数字参数,通常是根据预定义的错误常数构建的表达式形式。

这些是推荐的生产设置,以及 PHP 7.1 默认的 1 生产设置:

环境 价值
display_errors Off
log_errors On
error_reporting E_ALL & ∼E_DEPRECATED & ∼E_STRICT

这些也是你可以假设在你的 Zend 考试中设置的设置,当然,除非问题另有说明。

在开发中,您的error_reporting设置应该是E_ALL,并且您的代码必须在没有警告的情况下运行——不要使用被否决的函数。

旗帜是如何工作的

您可能想知道这些标志是如何设置的,为什么我们要对它们使用按位运算符。我会试着解释一下,以便更容易理解你的配置设置。

将二进制格式的数字想象成一系列 1 和 0。二进制数中的每个位置都是一个与选项相关联的标志。如果该位置的数字为 1,则标志打开,选项设置。

现在,E_ALL是一个被选择的数字。如果你var_dump(E_ALL),你得到输出int(32767),也就是0b111111111111111

每个选项都是一个数字,被选择为具有一个且仅一个位设置。比如E_NOTICE8,二进制是0b1000,E_DEPRECATED8192,二进制是0b10000000000000。请注意,您可以根据需要在左侧填充多个 0,使其长度相同。

按位运算符翻转一个数中的位,所以∼E_NOTICE就是0b0111

按位运算符&比较两个数的位位置。如果两个数在位置集合中都有一位,那么结果就是true。因此,E_ALL & ∼E_NOTICE已经设置了所有的位,除了那个表示E_NOTI CE 开启的位。

结果是您将error_reporting设置为一个数字,该数字具有为您想要打开的选项设置的位。

禁用函数和类

您可以在您的php.ini文件中使用disable_functionsdisable_classes指令来防止函数和类被使用。这些设置只能在您的php.ini文件中设置,不能在运行时或在目录ini文件中更改。

需要禁用的常用函数包括允许 PHP 执行系统命令的函数:execpassthrushell_execsystem

DirectoryIteratorDirectory类通常也被禁用,因为它们可能被攻击者利用。

Hint

禁用这些功能是一种“黑名单”方法。有创造力的对手会把这看作一个障碍,而不是不可逾越的障碍。

PHP 作为 Apache 模块

如果 PHP 作为 Apache 模块运行,那么它将使用与 Apache 服务器相同的用户运行。这意味着它将拥有与 Apache 用户相同的权限和访问权。

最佳实践是为 Apache 设置一个用户,而不是作为“nobody”运行它。Apache 用户应该对文件系统具有有限的访问权限,并且不应该在sudoers列表中。

您应该使用 PHP open_basedir设置来限制 PHP 可以访问的目录。您可以将它与设置doc_root进行对比,后者影响 PHP 将从哪些目录提供文件。

Note

设置open_basedir不受安全模式影响,但doc_root受影响。

如果您保留一个目录,其中存储了由该目录之外的用户上传的文件,您可以使攻击者上传和执行文件变得更加困难。

PHP 作为 CGI 二进制文件

我不知道是否还有人将 PHP 作为 CGI 二进制文件运行,但是这个话题仍然在 Zend 的教学大纲中。我试图理解为什么 Zend 觉得理解他们很重要。

我认为在传统配置中理解这些问题的价值在于,在现代设置中也有类似的问题。例如,“向 PHP 传递不受控制的请求”的配置缺陷 2 似乎与 CGI 中绕过权限检查的技巧非常相似(稍后将会介绍)。

PHP-FPM 使用 FastCGI 协议运行,这是对 CGI 的改进。这一部分与 PHP-FPM 无关,因为请求是通过套接字传递给它的,不会受到 URL 的影响。

出于考试目的,您需要了解三个配置参数以及它们在 CGI 攻击环境中的作用。我在这里列出它们,然后更详细地解释它们。

环境 功能
cgi.force_redirect 阻止 PHP 执行,除非 web 服务器调用它。如果设置为 on,那么 PHP 将不会响应类似 http://yoursite.com/cgi-bin/php/ ...的请求。
doc_root 设置文档根目录。如果您将safe_mode设置为 on,那么 PHP 将不会为这个目录之外的文件提供服务。
user_dir 为 web 用户设置主目录。

doc_rootuser_dir设置并不仅仅与 CGI 安全相关,应该作为一般安全设置的一部分进行设置。

恶意 CGI 参数

通常 URL 中问号后面的查询信息通过 CGI 接口作为命令行参数传递给解释器。这适用于任何被 web 服务器用作 CGI 的二进制文件。

根据惯例,URL http://my.host/cgi-bin/php?/etc/passwd 会尝试将/etc/passwd传递给 PHP 二进制文件。通常,CGI 解释器打开并执行命令行中第一个参数指定的文件。

然而,当作为 CGI 二进制文件调用时,PHP 拒绝解释命令行参数。这使得它不会受到依赖于向二进制文件传递参数的攻击。

绕过权限检查

通常使用“友好的”URL,将搜索引擎友好的可读 URL 发送给脚本。例如,URL https://yourhost.com/user/nico.php 可能映射到对 https://yourhost.com/cgi-bin/php/user/belieber.php 的实际请求。

通常,web 服务器会检查权限,并验证访问者必须访问/user/目录。如果访问者被允许,它将创建重定向的请求。

如果访问者访问重定向的目标,即包含cgi-bin的完整 URL,那么 web 服务器将检查他们访问/cgi-bin/目录的权限,而不是将要提供的实际目录。

这意味着访问者只需在cgi-bin中从 PHP 请求文件,就可以绕过保护用户目录的权限检查。恶意访问者可以访问 web 服务器上 PHP 可以读取的任何文件。

cgi.force_redirectdoc_rootuser_dir指令用于防止 PHP 访问私有文档。

cgi.force_redirect设置阻止了从 URL 直接调用 PHP 只有在从 Apache 等 web 服务器的重定向上调用 PHP 时,它才会执行。

当使用 PHP 作为 CGI 二进制文件时,您应该考虑将 PHP 二进制文件移出文档树,并将可执行 PHP 脚本与静态脚本分开。

运行 PHP-FPM

PHP-FPM 允许你很容易地设置多个池,每个池可以在不同的用户下运行。如果你有多个客户端,那么你应该确保每个客户端的网站都作为自己的用户运行。客户端用户不应该对其主目录之外的文件有任何访问权限。

以下是池文件中的一些设置示例:

[pool1]

user = site1

group = site1

listen = /var/run/php5-fpm-site1.sock

listen.owner = www-data

listen.group = www-data

我们将名为pool1的池设置为以组site1中的用户site1的身份运行。我们将监听所有者和组设置为 web 服务器用户,以便 Nginx/Apache 可以读写套接字。

一旦我们设置了池运行的用户,我们将配置文件权限以限制它只能访问网站所在的目录。这将防止客户使用文件读取功能来读取另一个客户的目录内容。

Tip

一些文件,比如 WordPress 站点中的wp-config.php,有可预测的名字,保护用户目录不被其他用户访问非常重要。

我们配置 Nginx 来传递 PHP 请求,如下所示:

location ∼ \.php$ {

  try_files $uri = 404;

  fastcgi_split_path_info ^(.+\.php)(/.+)$;

  if (!-f $document_root$fastcgi_script_name) {

    return 404;

  }

  include         fastcgi_params;

  fastcgi_index   index.php;

  fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;

  fastcgi_param   SERVER_NAME $host;

  fastcgi_pass unix:/var/run/php5-fpm-pool1.sock;

}

每个站点将请求传递给与其自己的池相关联的套接字。PHP 脚本作为池中指定的用户运行,并被锁定在我们为他们设置的文件权限中。

Note

在我们的 Nginx 配置中,我们加入了try_files $uri = 404;来防止 Nginx 手册中提到的攻击。3

通过将每个池锁定在自己的chroot监狱中,可以获得额外的安全层。请记住,您需要确保 PHP 需要访问的文件(比如日志目录或 ImageMagick 之类的二进制文件)在 jail 中是可用的。

会话安全性

您需要注意的两个重点领域是“会话劫持”和“会话固定”。除了本章之外,你应该学习 PHP 手册中关于会话安全的内容。

会话劫持

HTTP 是一种无状态协议,web 服务器可以同时为多个不同的访问者提供服务。

服务器需要能够区分客户机,并通过为每个客户机分配一个会话标识符来做到这一点。可以通过调用session_id()来检索会话标识符。它是在调用session_start()函数后创建的。

当客户端向服务器发出后续请求时,它们提供会话标识符,这允许服务器将请求与会话相关联。

客户端可以使用 cookies 或 URL 参数来提供会话。饼干是首选,但并不总是可用。如果 PHP 不能使用 cookie,它将自动透明地使用 URL,除非您在php.ini文件中设置了session.use_only_cookies设置。

很明显,如果您能够向服务器提供其他人的会话标识符,您就可以伪装成那个用户。

图 6-1 显示了恶意用户 Bob 能够拦截 Alice 发送给服务器的消息的场景。Bad Bob 读取请求并提取会话标识符(包含在 HTTP 请求的 cookie 标题中)。然后,他能够将该会话标识符呈现给服务器,服务器现在无法将他与 Alice 区分开。

A456636_1_En_6_Fig1_HTML.jpg

图 6-1。

Bob steals Alice’s session identifier and masquerades as her

获得另一个用户的会话标识符可以通过几种方式实现。

  • 如果会话标识符遵循一个可预测的模式,那么攻击者可以尝试确定它对用户来说是什么。PHP 使用一种非常随机的方式来生成会话标识符,所以您不需要担心这一点。
  • 通过检查客户端和服务器之间的网络流量,攻击者可以读取会话标识符。您可以设置session.cookie_secure=On使会话 cookies 仅在 HTTPS 可用,以减轻这种情况。HTTPS 还将加密被请求的 URL,因此如果会话标识符作为参数在请求中传递,它将被加密。
  • 针对客户端的攻击,如 XSS 攻击或在其计算机上运行的特洛伊木马程序,也可能泄露会话标识符。这可以通过设置session.cookie_httponly指令来部分缓解。

强制 PHP 只使用 cookies 不会减轻对这种攻击的利用。对手可以轻松设置 cookie 值。

会话固定

会话固定利用了 web 应用中的一个弱点。一些应用在对用户进行身份验证时不会为用户生成新的会话 ID。相反,它们允许使用现有的会话 ID。

当对手在 web 服务器上创建会话时,攻击就会发生。他们知道该会话的会话 ID。然后他们欺骗用户使用这个会话并验证自己。然后,攻击者能够使用已知的会话 ID,并拥有经过身份验证的用户的权限。

有几种方法可以设置会话 ID,实际使用的方法取决于应用如何接受标识符。

最简单的方法是在 URL 中传递会话标识符,就像这样的 http://example.org/index.php?PHPSESSID=1234

降低会话固定风险的最佳方法是在每次特权级别改变时调用函数session_regenerate_id(),例如在登录后。

您可以在配置文件中设置session.use_strict_mode=On。这个设置将强制 PHP 只使用它自己创建的会话标识符。它将拒绝用户提供的会话标识符。这将减少操纵 cookie 的企图。

设置session.use_cookies=Onsession.use_only_cookies=On将阻止 PHP 接受来自 URL 的会话标识符。

提高会话安全性

不要依赖单一的策略来减少攻击,而是使用多层安全措施。除了我已经提到的缓解策略之外,您还应该执行以下操作:

  • 检查 IP 地址在两次呼叫之间保持不变。对于在信号塔之间移动的移动电话来说,这并不总是可行的,因此在这样做之前,请检查您的用例。
  • 使用短会话超时值来减少固定窗口。
  • 为用户提供注销调用session_destroy()的方法。

这些都不是特别有效,但每一个都有助于提高您的整体安全性。

跨站点脚本

跨站点脚本(XSS)攻击是将恶意代码注入良性站点的攻击。通常,像 JavaScript 这样的恶意浏览器端代码被放在网站上,供客户端下载和运行。

这种攻击是有效的,因为客户端认为代码来自它信任的网站。该代码可以访问会话标识符、cookies、HTML 存储数据和其他与站点相关的信息。

XSS 攻击有几种主要类型:存储攻击、反射攻击和 DOM 攻击。

在存储 XSS 攻击中,对手可以在服务器上的存储位置输入内容。例子可以是显示在网站上并存储在数据库中的用户评论。当网站向另一个访问者输出用户评论列表时,他们会收到恶意代码。

在反射 XSS 攻击中,对手可以让网站直接输出一些东西。这种攻击最常见的形式是表单填充错误,用以前提交的字段预先填充输入字段,或者输出错误的字段值。例如,通过将访问者发送到包含恶意代码的特制 URL 作为错误消息,攻击者可以欺骗客户端在受信任站点的上下文中执行该错误消息。

DOM 攻击完全发生在页面中。恶意代码是从页面中的元素读取的,对代码的调用是在页面本身中进行的。

此外,XSS 攻击可以分为服务器端或客户端攻击。服务器端攻击是指服务器发送恶意代码。当通过不安全的 JavaScript 调用使用不受信任的用户提供的数据更新 DOM 时,会发生客户端 XSS。

缓解 XSS 袭击

要遵循的最重要的规则是绝不允许将未转义的数据输出到客户端。在允许将数据发送到客户端之前,请始终过滤数据并去除有害标签。

Tip

记住这句口头禅“过滤输入,转义输出”。

三个有用的功能是htmlspecialchars()htmlentities()strip_tags()。有关如何使用这些函数来帮助减轻 XSS 的更多详细信息,请参阅本章后面的“转义输出”一节。

Tip

在显示输出之前对其进行转义的最安全的方法是使用filter_var($string, FILTER_SANITIZE_STRING)

由于 URL 和 HTML 中可以使用各种各样的格式来输出数据,因此将代码列入黑名单是不安全的。您应该将您想要允许的特定标签列入白名单。看看 OWASP 过滤规避备忘单 4 看看有多少种方法可以规避黑名单。

您还需要在 HTML 页面的 JavaScript 中减少 XSS,但这超出了本手册的范围。

跨站点请求伪造

CSRF 攻击利用了网站对客户端的信任。在这些攻击中,对手诱骗客户端在信任该客户端的网站上执行命令。

最常见的表单是向表单输入发送一个POST请求。

假设 Alice 登录到她的银行网站,该网站有一个允许她向另一个账户转账的表单。Chuck 知道该表单的端点以及它有哪些输入字段。他设法骗过了 Alice 的网络浏览器,向该表单发送了一个POST请求,指示银行向他的账户转账。银行信任 Alice 的 web 浏览器,因为它有一个有效的会话并执行请求。

Chuck 欺骗 Alice 的 web 浏览器有很多方法,包括使用 iframes 和 JavaScript。

为了减少这些请求,您应该生成一个唯一的非常随机的令牌,并存储在 Alice 的会话中。当您输出表单时,您包括这个令牌,以便当 Alice 提交表单时,她也提交这个令牌。在处理表单之前,检查提交的令牌是否与存储在会话中的令牌相匹配。

Chuck 无法知道 Alice 会话中的令牌是什么,因此无法将其包含在他的POST中。您的代码将拒绝他诱骗 Alice 发出的请求,因为它没有有效的令牌。

实际的银行在执行敏感操作时通常会要求一个人重新进行身份认证,并且在此过程中通常会要求双重身份认证。

SQL 注入

SQL 注入是网络上最常见的攻击形式,也是最容易防御的攻击形式之一。当攻击者可以在 SQL 语句中插入恶意命令供数据库执行时,就会发生 SQL 注入。

许多数据库设置允许数据库将文件写入磁盘。该特性允许黑客通过使用数据库将 PHP 脚本写入 web 服务器将为其服务的目录来创建后门。

这意味着 SQL 注入的影响不仅限于损害您的数据库,还可能导致攻击者能够在您的数据库上执行任意代码。

SQL 注入的核心问题在于,一条 SQL 语句混合了数据和语法。通过允许用户提供的数据与函数语法结合,我们创造了恶意数据干扰语法的可能性。

准备好的陈述

开始减轻 PHP 语言中 SQL 注入的最有效的方法是专门使用准备好的语句与数据库进行交互。这将有助于排除大多数 SQL 注入攻击,但本身并不足以做到万无一失。

预准备语句非常重要,如果底层驱动程序不支持它们,PDO 驱动程序会模仿它们。

准备好的语句分三步工作:

  1. 设置带有数据占位符的语句。
  2. 将实际数据绑定到语句。
  3. 执行准备好的语句。

可以将新数据绑定到已经执行过的语句,然后用新语句再次运行它。数据库引擎不必再次解析 SQL,除了安全优势之外,这还提高了性能。

此代码给出了一个如何准备、绑定和执行语句的示例:

<?php

$stmt = $dbh->prepare("SELECT * FROM REGISTRY where name = ?");

$stmt->bindParam(':name', $_GET['name'], PDO::PARAM_STR, 12);

$stmt->execute();

Note

PDO::prepare()函数返回一个类型为PDOStatement的对象。

我们直接使用了GET变量,所以我们不需要对它进行转义,因为它被绑定为带有PDOStatement::bindParam()的变量,并且不能改变将要运行的 SQL 的语法。

PHP 中的其他数据库驱动程序也支持预处理语句。下面是 MySQL 5 手册中的一个例子:

/* Prepared statement, stage 1: prepare */

if (!($stmt = $mysqli->prepare("INSERT INTO test(id) VALUES (?)"))) {

  echo "Prepare failed: (" . $mysqli->errno . ") " . $mysqli->error;

}

/* Prepared statement, stage 2: bind and execute */

$id = 1;

if (!$stmt->bind_param("i", $id)) {

  echo "Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error;

}

if (!$stmt->execute()) {

  echo "Execute failed: (" . $stmt->errno . ") " . $stmt->error;

}

逃避

减轻 SQL 注入的一个不太有效的方法是在将特殊字符发送到数据库之前对其进行转义。这比使用预准备语句更容易出错。

如果你试图对特殊字符进行转义,你必须使用数据库特定的函数(如mysqli_real_escape_string()PDO::quote(),而不是像addslashes()这样的通用函数。

总则

您还应该始终使用拥有应用运行所需的最少权限的用户连接到数据库。永远不要允许您的 web 应用以 root 用户的身份连接到数据库。

如果您在服务器上托管多个数据库,请为服务器上的每个数据库使用不同的用户,并确保他们的密码是唯一的。这将有助于防止一个站点上的 SQL 注入攻击影响其他站点的数据库。

确保您使用的是最新版本的 MySQL,并在客户端 DSN 中强制使用字符集。在某些易受攻击的编码方案中,有一种非常微妙的方法可以使用不匹配的字符集来部署 SQL 注入;参见本StackOverflow6 的第二个答案(非公认答案)进行阐述。

远程代码注入

远程代码注入是一种攻击,对手可以让服务器包含并执行他们的代码。

将字符串作为代码计算的函数

eval()exec()system()这样的函数容易受到远程代码注入攻击。如果您正在执行一个包含用户输入的变量,他们将能够使用转义字符插入命令。

您可以通过使用escapeshellargs()对传递给 shell 命令的参数进行转义来缓解这个问题。escapeshellcmd()函数将对 shell 命令本身进行转义。

Tip

如果您没有明确地使用这些函数,您应该在您的php.ini中禁用它们。这不是万无一失的,但它可以帮助。

assert()函数用于确保某个条件为真,如果不为真,则采取一些措施。它对于调试很有用,但是在生产中应该关闭它。您可以使用assert_options() 7 函数来配置 assert 的行为方式并将其关闭。

如果你给assert()传递一个字符串值,那么 PHP 将会像 PHP 代码一样计算这个字符串。这将允许攻击者在您的服务器上执行代码,如果他们能够控制您传递给assert()的参数的话。

<?php

function rce(string $a) {

    assert($a);

}

rce('print("hello")'); // hello

在 PHP 7.2 中,向assert传递一个字符串是不赞成的,这段代码会生成一个警告,但是仍然会计算这个参数。

游戏包括并要求

如果 PHP 配置设置allow_url_include打开,include()require()都允许包含 URL 指定的文件。

最常见的情况是,人们在 URL 中使用一个GET变量来确定要包含的一些动态内容。这是一个非常业余的错误。

例如,一个站点可以有一个像 http://example.com/index.php?sidebar=welcome 这样的 URL,然后动态地将welcome.php文件包含到侧边栏中。

对手可以提供一个 URL 而不是“welcome”字符串,并让他们自己的代码以与 web 服务器用户相同的权限级别在服务器上执行。

为了解决这类问题,您可以将allow_url_fopen转换为OFF,对您要包含的变量使用basename(),以便移除路径,并且只根据白名单进行包含。

<?php

$page = $_GET['page'];

$allowedPages = array('adverts','contacts','information');

if ( in_array($page, $allowedPages) ) {

    include basename($page . '.html');

}

电子邮件注入

用户可以提供允许他们更改邮件正文或收件人列表的十六进制控制字符。

例如,如果您的表单允许此人输入其电子邮件地址作为电子邮件的“发件人”字段,则以下字符串将导致其他收件人被包括为邮件的抄送和密件抄送收件人:

sender@example.com%0ACc:target@email.com%0ABcc:anotherperson@emailexample.com,stranger@shouldhavefiltered.com

攻击者还可以提供自己的正文,甚至更改所发送消息的 MIME 类型。这意味着您的表单可能会被垃圾邮件发送者用来发送邮件。

您可以用几种方法来防止这种情况。

请确保您在发送邮件时正确过滤了输入内容。filter_var()函数提供了许多标志,您可以使用它们来确保您的输入数据符合期望的模式。

<?php

$from = $_POST["sender"];

$from = filter_var($from, FILTER_SANITIZE_EMAIL);

// send the email

您也可以安装并使用 Suhosin PHP 扩展。它提供了suhosin.mail.protect指令来防止这种情况。

你可以实现一个 tarpit 来减慢机器人的速度或者无限期地困住它们。看看 GitHub 8 上的msigley/PHP-HTTP-Tarpit作为一个 tarpit 的例子。

在设置邮件服务器时,您必须确保它没有被配置为允许 Internet 上的任何人使用它来发送邮件的开放中继。您还应该考虑关闭防火墙上的端口 25 (SMTP ),这样外部主机就无法到达您的服务器。

过滤器输入

在处理安全问题时,最好做好最坏情况的打算,并假设所有输入都被感染,并且所有用户行为都是恶意的。您应该只使用您已经手动确认安全的输入。

输入的格式可能会被过滤器忽略,然后被浏览器解析。我前面提到的 XSS 规避备忘单中有很多使用特殊字符来规避检测的例子。

输入可能使用非标准字符集,过滤功能可能无法正确理解该字符集。使用过滤 SQL 时,应该使用数据库本机过滤函数。

PHP 有一个非常强大的过滤功能,filter_var(),可以用来执行许多不同的过滤和消毒操作。你可以在 PHP 手册中找到过滤器的列表。

还有几个函数可以用来检查各种类型的字符串。它们有地区意识,因此会考虑语言字符。如果字符串只包含过滤器中的字符,函数将返回true,否则返回false

功能 过滤
ctype_alnum() 仅限字母数字字符
ctype_alpha() 仅限字母字符
ctype_cntrl() 字符串仅是控制字符
ctype_digit() 字符串只能是数字
ctype_graph() 仅可打印的字符和空格
ctype_lower() 只有小写字母
ctype_print() 可打印字符
ctype_punct() 任何非空白或字母数字的可打印内容
ctype_space() 检查空白字符
ctype_upper() 只有大写字母
ctype_xdigit() 十六进制数字

通常在客户端执行过滤,例如在浏览器中使用 JavaScript。这还不够,还必须在服务器端进行过滤和验证。

转义输出

编写安全 PHP 代码的基本规则之一是过滤输入并转义输出。

在发出数据之前,必须确保它对客户端是安全的。回想一下 XSS 攻击是如何工作的,作为一个例子来说明为什么您需要确保您发送给客户端的内容得到适当的处理。

如果您发送给客户端的数据包含执行代码的指令,那么它将盲目地执行代码。您必须确保只发送您打算让客户端执行的代码,而不是攻击者注入的代码。

与过滤输入一样,您不能依赖客户端来过滤发送给它的输出。并非所有客户端都启用了 JavaScript,黑客有可能绕过客户端过滤。

过滤输出最安全的方法是使用带有FILTER_SANITIZE_STRING标志的filter_var()。可能会有对您来说限制太多的用例,在这种情况下,您将需要查看像htmlspecialchars()strip_tags()htmlentities()这样的函数。

htmlspecialchars()htmlentities()功能有相似的效果,你应该确保你理解不同之处。

不同之处在于,htmlentities()将编码任何具有 HTML 实体表示的东西,而htmlspecialchars()将只编码在 HTML 中有特殊意义的字符。

<?php

$string = '© 1982 Sinclair Research Ltd.';

echo htmlentities($string); // &copy; 1982 Sinclair Research Ltd.

echo PHP_EOL;

echo htmlspecialchars($string); // © 1982 Sinclair Research Ltd.

此表显示了将由htmlspecialchars()转换的字符。

性格;角色;字母 成为
&(与号) &amp;
"(双引号) &quot;
'(单引号) '
<(小于) &lt;
>(大于) &gt;

这两个函数都将一个标志作为第二个参数。您应该确保至少知道这三个标志,因为它们对于转义您输出的 JavaScript 非常重要:

描述
ENT_COMPAT 转换双引号,而不是单引号
ENT_QUOTES 转换双引号和单引号
ENT_NOQUOTES 不转换任何报价

当转义一个 JavaScript 字符串时,应该使用ENT_QUOTES标志。

字符串的编码可以在第三个参数中指定。在 PHP 7.1 中,这两个函数的默认编码都是 UTF-8。

避免原木中毒

如果您正在记录错误消息、信息消息等,您需要对您记录的内容采取一些预防措施。

显然,您绝不能记录用户密码或信用卡等敏感信息。如果你把它传递给一个日志记录函数,那么一定要混淆它。因此,信用卡号将是日志文件中的一系列星号,而不是实际的号码。

确保在记录之前过滤掉可执行代码和个人信息。

您还应该知道日志中毒攻击是如何工作的。该漏洞基于您的代码不正确地包含本地文件。如果您允许用户输入来确定包含哪个文件,那么攻击者可以操纵该输入来包含日志文件。如果日志文件包含恶意代码,那么它将被解释和运行。

攻击者需要做的就是将他们的代码放入您的日志文件,这非常容易做到。例如,他们可以通过创建一个请求,将包含他们想要运行的命令的字符串注入到日志中,来毒害您的 web 服务器日志。另一个例子是,攻击者可以 SSH 到您的服务器,并使用恶意代码作为他们的用户名来毒害您的身份验证日志文件。

为了帮助您理解影响,让我们来看一个漏洞利用的例子。假设您的代码运行在本地主机上,容易受到本地文件包含的攻击,并接受需要显示的图像的名称。

首先,我们使用命令nc localhost 80连接到 web 服务器。然后,我们向服务器发出以下请求:

GET /<?php passthru($_GET['cmd']); ?> HTTP/1.1

Host: localhost

Apache 将在日志文件中写一行,如下所示:

127.0.0.1 - - [08/Apr/2016:13:57:38 +0000]

"GET /<?php system($_GET['cmd']); ?>

HTTP/1.1" 400 226

"<?php passthru($_GET['cmd2']); ?>"

"<?php passthru($_GET['cmd']); ?>"

我把我的日志分成多行,但是很明显在你的日志文件中,它们都在同一行。

利用漏洞的下一步是向包含日志文件的站点发出请求(这要求您的站点中存在这样的漏洞)。

http://localhost/?file=/var/log/apache2/access.log&cmd=ls -la

很多事情都需要出错才能让你变得脆弱:

  • web 服务器用户需要对目标日志文件的读取权限
  • 您的代码必须允许攻击者包含目标文件
  • 您不能在您的配置中禁用execpassthrusystem

加密和哈希算法

加密和哈希是不同的概念,你应该确保你理解的区别。加密是双向操作;你可以加密和解密。哈希是一种单向操作,从设计上来说,获取哈希并将其转换为原始字符串是困难的或耗时的。

您应该将密码作为哈希存储在数据库中。这样,如果攻击者获得了您的数据库的副本,他们仍然无法获得用户密码,除非他们能够反转哈希。通常,反转散列会花费大量的时间,希望您有足够的时间来注意到安全漏洞,并提醒您的用户需要更改他们的密码。

计算一个散列值所需的时间将决定黑客通过暴力破解密码所需的时间。

PHP 中的加密

PHP 中的加密由mcrypt模块提供,需要单独安装和启用。mcrypt模块提供多种加密功能和常量。

可用的算法取决于安装 PHP 的操作系统。您不应该尝试编写自己的加密算法实现。

Zend 认证考试并不特别强调加密。

哈希函数

像 MD5 和 SHA1 这样的旧哈希算法计算速度非常快,所以你不能在任何涉及安全的地方使用它们。它们在编程的其他领域仍然非常有用,但不是在任何你依赖它们作为单向操作的地方。

PHP 5.5.0 引入了password_hash()函数,它提供了一种生成安全散列的便捷方式。

对于老版本的 PHP,应该使用crypt()函数。

默认情况下,password_hash()函数使用bcrypt算法来散列密码。bcrypt算法有一个参数,包括在返回散列结果之前应该对密码运行多少次。这被称为算法的“成本”。

通过增加算法必须运行的次数,可以增加计算哈希所需的时间。这意味着随着计算机变得更快,你可以增加你的bcrypt算法的迭代次数来保护你的密码免受暴力攻击。

您可以使用password_info()函数来检索关于如何计算散列的信息。这个函数会告诉你算法的名字,成本,和盐。

password_needs_rehash()函数将一个散列与您指定的选项进行比较,看它是否需要被重新散列。这将允许您更改用于散列密码的算法,例如随着时间的推移增加成本。

安全随机字符串和整数

PHP 有两个函数可以让你方便地生成加密安全的整数和字符串。这些函数可以在 PHP 运行的任何平台上运行。

功能 因素 返回 描述
random_bytes Int $length | 字节串 | 生成一个长度为$length字节的随机字符串
random_int Int $minint $max 随机整数 $min$max指定的范围内生成一个随机整数

下面是一个使用random_bytes的例子:

<?php

// get a string that contains 8 random bytes

$randomBytes = random_bytes(8);

$printableVersion = bin2hex($randomBytes);

echo $printableVersion; // d7e263202be1b99b

PHP 生成的字符串不一定是可打印的,所以我使用bin2hex()函数将其转换为十六进制字符串。十六进制需要两个字符来显示一个字节,所以我们最后输出的字符串是 16 个字符长(是我们生成的随机字节数的两倍)。

加盐密码

salt 字符串是添加到密码中的附加字符串。它应该为每个密码随机生成。它用于帮助字典攻击和预先计算的彩虹攻击变得更加困难。

您可以为password_hash()函数指定一个 salt,但是如果您忽略它,PHP 将为您创建一个。PHP 手册指出,预期的操作模式是让它为密码创建随机 salt。

crypt()函数接受一个 salt 字符串作为第二个参数,但是如果您不提供自己的 salt,它不会自动生成 salt。PHP 5.6.0+会发出通知,如果你没有提供一个盐。

检查密码

如果攻击者有可能精确地测量运行您的密码检查例程所需的时间,他们将能够收集到有助于他们破解密码的信息。这些攻击被称为定时攻击。

PHP 5.5.0 password_verify()函数是一个定时攻击9——比较password_hash()创建的哈希的安全方式。

如果您无法使用此功能,您需要计算用户提供的密码的哈希,然后将哈希与存储的哈希进行比较。比较哈希值容易受到计时攻击。

PHP 5.6.0 引入了hash_equals()函数,这是一种比较字符串的定时攻击安全的方法。在比较crypt()生成的散列时,应该使用这个函数。

错误消息的快速注释

你不应该向别人确认他们输入了不正确的用户名。您的错误消息应该是他们输入了不正确的用户名或密码。您向攻击者提供的信息越少,他们获得系统访问权限的时间就越长。

文件上传

文件上传是 web 应用的一个主要风险,需要通过多种方式来保护。

回想一下,$_FILES[]超级全局包含关于客户端上传的文件的信息。您应该将该数组中的所有内容都视为可疑,并确保手动确认每条信息。

PHP 处理文件上传的方式是将它们保存到一个临时目录中。您可以在那里对它们进行操作,然后将它们移动到您想要的位置。

您应该检查您正在处理的文件是一个有效的上传文件,并且客户端试图伪造它的文件名和在临时文件夹中的位置。

使用is_uploaded_file()功能来确保你所引用的文件确实被上传了。使用move_uploaded_file()而不是其他方法将它从临时目录移动到最终位置。

当引用一个文件时,使用basename()函数去掉路径,以防止有人盗用文件名。

不要信任用户指定的 MIME 类型。忽略用户提供的 MIME 类型,如果需要,使用finfo_file()来确定 MIME 类型。

如果你允许一个用户上传一张图片,你应该在上面使用一个 GD image 函数,比如getimagesize()来确认它是一张有效的图片。如果此功能失败,则该文件不是有效的图像。

生成自己的文件名来存储文件,不要使用用户提供的文件名。强烈建议对文件名使用随机散列,并通过检查 MIME 类型来手动设置扩展名。

确保存储文件的文件夹只允许 web 服务器用户访问。

如果您不需要提供上传的文件,那么将 uploads 文件夹放在文档根目录之外。

数据库存储

除了避免 SQL 注入,您还应该在与数据库交互时应用一些安全原则。

您应该为不同的代码环境分离数据库服务器。您的 QA、测试、开发和生产服务器应该都使用不同的数据库服务器,并且不应该能够访问彼此的数据库。

您必须阻止 Internet 访问您的数据库服务器。

这可以通过以下方法实现:使用防火墙关闭端口,禁止外部通信;使用没有路由到 Internet 的专用子网;或者将数据库服务器配置为仅侦听特定主机。

仅仅改变数据库监听的端口是不够的。我甚至认为这不值得麻烦,因为对攻击者来说它甚至不是减速带,只是让您的同事更难使用您的服务器环境。

如果在一台数据库服务器上运行多个应用,请确保每个应用在服务器上都有自己的用户名和密码。每个应用用户应该只拥有最少的权限,并且永远不能读取其他应用的数据库。

避免使用可预测的用户名,并确保使用安全的密码。例如,我通常使用随机生成的第 4 版 UUID 作为密码。

在将敏感数据放入数据库之前,使用mcrypt()mhash()对其进行加密。

您应该不时地检查您的数据库日志。您将能够发现企图注入攻击和其他模式,这将让您识别漏洞或收紧代码区域。

避免在线发布您的密码

一个很好的建议是避免在网上发布你的数据库或 API 证书,这样人们就可以读到它们。好吧,我是在开玩笑,但说真的,你什么时候会把你所有的访问凭证公布给全世界和他的狗看?

您可以这样做的一种情况是提交一个 Git 存储库,并将其推送到 GitHub 或 Bitbucket 之类的服务。

确保任何配置文件都被您的版本控制系统忽略,并且永远不会被提交或推送到上游存储库。有一些从 GitHub 获取凭证的机器人会因为这些错误惩罚你。

与此链接相关的一个题外话是,您不应该将 Amazon 凭据硬编码到应用中。相反,设置一个 IAM 角色,允许访问您想要使用的服务,并将该角色应用到您的虚拟机。

Chapter 6 Quiz

Q1:display_error配置设置的推荐生产设置为On

| 真实的 |
| 错误的 |

Q2:使用 HTTPS 加密你的登录页面将有助于防止会话劫持和会话固定。

| 真实的 |   |
| 错误的 | * |

Q3:您可以通过使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 配置设置,强制将会话专门包含在 cookies 中。

| session.cookie_secure |
| session.use_cookies |
| session.use_trans_sid |
| 以上都不是 |

问题 4: CSRF 是指攻击者在用户不知情的情况下,诱骗用户的浏览器或设备发出请求。它利用了服务器对浏览器的信任。您可以通过在表单中包含一个 CSRF 令牌来避免这种情况,每当访问者加载页面时,该令牌就会增加 1。

| 真实的 |
| 错误的 |

q5:crypt()password_hash()函数都允许您指定 salt,但是如果您不指定,将会为您生成一个适当随机的 salt。

| 真实的 |
| 错误的 |

Q6:浏览器通过操作系统调用来确定文件类型,并在请求中发送该信息。您可以相信这一点,以确定存储文件时使用的扩展名。

| 真实的 |
| 错误的 |

Q7:因为 PHP 在结束运行时会删除临时文件,所以您应该首先确保使用copy()函数将临时文件放在一个永久的位置。

| 真实的 |
| 错误的 |

Q8:默认情况下,PHP 被配置为能够包含存储在 URL 上的源代码。

| 真实的 |
| 错误的 |

问题 9:防止 XSS 的一个充分的对策是在你的内容之前使用strip_tags()功能。

| 真实的 |
| 错误的 |

Q10:除非 PHP 安全模式开启,否则open_basedir配置设置无效。它限制了 PHP 可以访问的目录。

| 真实的 |
| 错误的 |

Footnotes 1

https://github.com/php/php-src/blob/master/php.ini-production

2

https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#passing-uncontrolled-requests-to-php

3

https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/

4

https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet

5

https://dev.mysql.com/doc/apis-php/en/apis-php-mysqli.quickstart.prepared-statements.html

6

https://stackoverflow.com/a/12202218/821275

7

https://secure.php.net/manual/en/function.assert-options.php

8

https://github.com/msigley/PHP-HTTP-Tarpit

9

https://en.wikipedia.org/wiki/Timing_attack

七、数据格式和类型

本章分为六大部分:

  • 可扩展置标语言
  • 肥皂
  • REST web 服务
  • 数据
  • 日期和时间
  • PHP SPL 数据结构

虽然这个主题不是 Zend 考试的三个高度重要的领域之一,但是你可以期待从这个部分中被问到一些相对详细的问题。

可扩展置标语言

XML 代表可扩展标记语言,是一种以结构化方式存储数据的方法。使用 XML 的一个优点是,它是一种公认的数据标准,因此是一种在系统之间交换数据的便捷方式。

在业界,作为数据交换过程,已经从 XML 转向 JSON,但是 XML 仍然与日常实践相关,并且是 Zend 考试的一部分。

XML 的基础知识

这不是一本关于 PHP 的入门书,所以我不会极其详细地介绍 XML 的所有元素。如果我们深入到那个细节层次,这本书就太长了。确保您至少熟悉下表中的所有术语,因为我们在研究 PHP 的 XML 处理能力时会用到它们。

学期 描述
标准通用标识语言 标准化通用标记语言。XML 是它的一个子集。
文档类型声明 DTD 用合法元素和属性的列表定义了 XML 文档结构的合法构件。
实体 实体可以声明在 XML 文档的其余部分中不允许的名称和值。例如,HTML 将<声明为一个实体来表示小于符号<。这些声明也可以用作快捷方式,并在整个文档中保持拼写和值的一致性。
元素 元素是 XML 文档的基本构造块。元素可以嵌套并包含元素,也可以包含值。元素可能有属性。
格式良好的 XML 格式良好的文档是符合 XML 1.0 规范规定的语法规则的文档,因为它必须满足物理和逻辑结构。 1
有效的 根据 DTD 验证的 XML 文档既“格式良好”又“有效”。

如果您对这些定义不太确定,那么请务必阅读关于 XML 的全面教程,并阅读本节的链接脚注。

格式良好且有效的

让我详细解释一下这些术语的意思,因为知道它们的区别很重要。

一个文件是良构的,如果:

  • 它只有一个根元素
  • 标签被正确地打开和关闭
  • 根据以下列表,它的所有实体都是格式良好的:
    • 它们只包含正确编码的 Unicode 字符
    • 不会出现像<&这样的语法标记
    • 标记名必须完全匹配,并且不能包含符号

如果文档格式良好并且符合 DTD,则该文档是有效的。

Note

PHP 不要求 XML 文档是有效的,但是它要求 XML 文档格式良好,以便用标准库来解析它们。

XML 处理指令

处理指令允许文档包含应用的指令。它们包含在<??>标记中,看起来像这样,例如:

<?PITarget PIContent?>

一个用例可能是通知应用某个元素是特定的数据类型,如下例所示:

<?var type="string" ?>

最常见的用法是包含 XSLT 或 CSS 样式表,如下所示:

<?xml-stylesheet type="text/xsl" href="style.xsl"?>
<?xml-stylesheet type="text/css" href="style.css"?>

用 PHP XSL 进行 XML 转换

PHP XSL 扩展允许 PHP 应用 XSLT 转换。

尽管这通常用于应用样式表,但重要的是要知道许多其他形式的转换也是可能的。

XSL 是一种表达 XML 文档样式表的语言。它类似于 CSS,因为它描述了如何显示 XML 文档。

XSL 定义了 XSLT,它是 XML 文档的转换语言,允许将 XML 文档处理成其他文档。

XSLT 处理器接收一个输入 XML 文件、一些 XSLT 代码,然后生成一个新文档。摘自维基百科知识共享的图 7-1 说明了这一点。

A456636_1_En_7_Fig1_HTML.jpg

图 7-1。

XSLT processor

这方面的一个用例可以是创建一个可以由浏览器呈现的 XHTML 文档。

输入 XML 将从一个 PHP 程序接收,该程序包含关于从哪里检索 XSL 样式表的处理指令。浏览器将检索这个样式表,并在其中应用 XSLT 代码来生成 XHTML。

首字母缩略词 事实真相
可扩展样式表语言(Extensible Stylesheet Language 的缩写) 表达样式表的语言
可扩展样式表语言转换 将 XML 处理成另一个 XML 文档的转换语言

PHP 手册 2 中有一个简单的例子,说明如何使用 PHP 通过 XSL 转换 XML 文件:

<?php

$xslDoc = new DOMDocument();
$xslDoc->load("collection.xsl");

$xmlDoc = new DOMDocument();
$xmlDoc->load("collection.xml");

$proc = new XSLTProcessor();
$proc->importStylesheet($xslDoc);
echo $proc->transformToXML($xmlDoc);

用 PHP 解析 XML

PHP 中有两种类型的 XML 解析器。有几个解析 XML 的 PHP 扩展,但是它们都属于这两种类型之一。

所有的 PHP XML 扩展都使用相同的底层库,因此可以在它们之间传递数据。

所有 XML 例程都需要启用LibXML扩展和Expat库。这两者在 PHP 中都是默认启用的。

树形分析器

树解析器试图一次解析整个文档,并将其转换成树结构。很明显,如果您试图解析一个非常大的文档,这可能会带来问题。

PHP 中有两种树解析器:

  • SimpleXML
  • 数字正射影像图

基于事件的分析器

这些解析器比树解析器更快,消耗的内存更少。它们通过一个节点一个节点地读取 XML 文档来工作,并为您提供了一个机会来挂钩与这个读取过程相关的事件。

基于事件的解析器的两个例子是:

  • 的成员
  • XML Expat 解析器

XML Expat 解析器是一个基于非验证事件的解析器,也内置在 PHP 的核心中。它不需要 DTD,因为它不验证 XML,只要求 XML 格式良好。

错误代码

PHP 手册 3 列出了几个 XML 错误代码。这个列表是底层libxml库的 733 个错误代码的子集。

这里是您应该熟悉的 XML 常量的部分列表,因为它们比其他代码更常见。

字首码 描述
XML_ERROR_SYNTAX XML 的格式不正确。
XML_ERROR_INVALID_TOKEN 您在 XML 中使用了无效字符。
XML_ERROR_UNKNOWN_ENCODING 无法解析您的 XML,因为无法确定编码方案。
XML_OPTION_CASE_FOLDING 默认情况下启用,并将元素名称设置为大写。
XML_OPTION_SKIP_WHITE 跳过源文档中多余的空白。

字符编码

当 PHP 解析 XML 文档时,它会执行一个称为源代码编码的过程来读取文档。

支持三种编码形式:

  • UTF-8
  • ISO-8859-1(默认)
  • 美国-阿斯凯

UTF-8 是一种多字节编码方案,这意味着单个字符可以由多个字节表示。其他两种方案都是单字节的。

PHP 在内部存储数据,然后在将数据传递给函数时执行目标编码。

默认情况下,目标编码设置为与源编码相同,但这是可以更改的。但是,在创建解析对象后,不能更改源编码。

如果解析器遇到源编码不能表示的字符,它将返回一个错误。

如果目标编码方案不能包含字符,那么该字符将被降级以适应编码方案。实际上,这意味着它们被一个问号所取代。

XML 扩展

XML 扩展允许您创建 XML 解析器和定义处理程序。您应该熟悉以下功能。

功能 使用
xml:parser_create($encoding) 用指定的编码创建 XML 分析器。
xml:parser_create_ns($encoding, $separator=":") 使用支持 XML 命名空间的指定编码创建 XML 分析器。
xml:parser_free($xmlparser) 释放 XML 解析器。
xml:set_element_handler($xmlparser, $start, $end) | 这告诉解析器在 XML 文档中每个元素的开头和结尾调用哪个函数。您可以通过FALSE来禁用特定的处理程序。$start$end都必须是可调用的,并且通常是存在于作用域中的函数的字符串名称。

处理元素开始的函数必须接受三个参数:

  • XML 解析器资源
  • 一个字符串,它将包含正在分析的元素的名称
  • 元素具有的属性数组

结束处理函数必须接受两个参数:

  • XML 解析器资源
  • 一个字符串,它将包含正在分析的元素的名称

xml:set_object($xmlparser, $object)函数允许在对象中使用 XML 解析器。这意味着您可以将对象的方法设置为用于设置元素处理程序的函数。

xml:parse_into_struct($parser, $xml, $valueArr, $indexArr)函数将 XML 字符串解析成两个并行的数组结构,一个(index)包含指向 values 数组中适当值的位置的指针。这最后两个参数必须通过引用传递。

数字正射影像图

DOM 是文档对象模型的缩写。DOMDocument类对于处理 XML 和 HTML 很有用。

它使用 UTF-8 编码,需要libxml2扩展(Gnome XML 库)和expat库。它是一个树解析器,在创建内部树表示之前将整个文档读入内存。

下面是一些DOMDocument语法的基本示例:

<?php
$domDoc = new DomDocument();
$domDoc->load("library.xml");
// $domDoc->loadXML($xmlString);
// $domDoc->loadHTMLFile("index.html");
// $domDoc->loadHTML($htmlDocumentString);
$domDoc->save(); // (to a file in XML format)
$xmlString = $domDoc->saveXML();
$htmlDocumentString = $domDoc->saveHTML();
$domDoc->saveHTMLFile(); // (to a file in HTML format)
$xpath = new DomXpath($dom);
$elements = $xpath->query("//*[@id]"); // find all elements with an id
echo "I found {$result->length} elements<br>";
if (!is_null($elements)) {
    foreach ($elements as $element) {
        echo "<br/>[". $element->nodeName. "]";

        $nodes = $element->childNodes;
        foreach ($nodes as $node) {
            echo $node->nodeValue. "\n";
        }
    }
}

您应该熟悉 DOM 类的以下方法:

方法 描述
createElement 创建一个节点元素,该元素可以用 node 类的appendChild方法追加。
createElementNS createElement一样,但是支持带有名称空间的文档。
saveXML 将 XML 树转储回字符串。
save 将 XML 树转储回文件中。
createTextNode 创建类DOMText的新实例。

DOM 节点

DOMNode类用于处理 DOM 树中的节点。

您可以通过调用DOMDocument的这些方法之一来检索节点:

  • getElementById
  • getElementsByTagName
  • getElementsByTagNameNS

这些方法返回一个DOMNodeList对象,可以使用foreach()遍历该对象。

getElementById()函数要求您指定哪个属性属于类型id。你可以通过包含一个定义它的 DTD 或者通过调用setIdAttribute()函数来做到这一点。无论是哪种情况,都必须验证文档才能调用函数。

当使用insertBefore()插入一个节点作为兄弟节点时,您需要引用父节点并指定您想要在之前插入新节点的兄弟节点。此示例显示了语法:

<?php
$xmlString = <<<XML
<root>
<teams>
<team>Silverbacks</team>
<team foo="winner">Golden Eyes</team>
</teams>
</root>
XML;

$domDoc = new DOMDocument();
$domDoc->loadXML($xmlString);
$xpath = new DomXPath($domDoc);
$team2 = $xpath->query('teams/team[2]');
$parent = $xpath->query('teams');
$textElement = $domDoc->createElement('team', 'Bearhides');
$parent->item(0)->insertBefore($textElement, $team2->item(0));

在本例中,我们希望在两个现有团队之间插入一个新团队。为此,我们找到了团队和家长。

Note

这些变量包含DOMElements。我们不能使用parent()方法,因为它是在DOMNode类上定义的。

您应该熟悉DOMNode类的这些方法。

方法 描述
appendChild 在子节点的末尾添加一个新的子节点。
insertBefore 在引用节点前添加新的子节点。
parentNode 节点的父节点,如果没有父节点,则为 null。
cloneNode 克隆一个节点,也可以克隆它的所有后代节点。
setAttributeNS 将名称空间为namespaceURI且名称为name的属性设置为给定值。如果该属性不存在,将会创建它。

Note

您需要将一个节点作为参数传递给这些函数。

如果你试图使用appendChild(),那么你必须首先使用一个类似DOMDocument::createElement()的函数来创建节点。

SimpleXML

SimpleXML 是一个扩展,它牺牲了对复杂需求的健壮处理,而支持提供一个简单的接口。它需要 simpleXML 扩展,并且只支持 XML 规范的 1.0 版本。

Caution

是一个树解析器,在解析文档时将整个文档加载到内存中。这可能使它不适合非常大的文档。

SimpleXML 提供了一种面向对象的方法来访问 XML 数据。它创建的所有对象都是SimpleXMLElement类的实例。元素成为这些对象的属性,属性可以作为关联数组来访问。

创建 SimpleXML 对象

您可以使用过程方法或通过面向对象的方法创建 SimpleXML 对象:

<?php
// procedural from string variable
$xml = simple_xml:load_string($string_of_xml);
// procedural from file
$xml = simple_xml:load_file('filename.xml');
// object oriented from variable
$xml = new SimpleXMLElement($string_of_xml);

迭代 SimpleXML 对象

children()方法返回子对象的可遍历数组。

您可以创建一个算法来检查节点的子节点,然后递归地遍历它们。PHP 手册页上有这样一个例子。

检索信息

功能 行动
SimpleXMLElement::construct() 创建一个新的SimpleXMLElement对象。
SimpleXMLElement::attributes() 标识元素的属性。
SimpleXMLElement::getName() 检索元素的名称。
SimpleXMLElement::children() 返回给定节点的子节点。
SimpleXMLElement::count() 返回一个节点有多少个子节点。
SimpleXMLElement::asXML() 以格式良好的 XML 字符串形式返回元素。
SimpleXMLElement::xpath() 在当前节点上运行xpath查询。

语言

XPath 4 是一种定义 XML 文档各部分的语言。它将 XML 文档建模为一系列节点,并使用路径表达式在文档中导航和选择节点。

SimpleXMLElement::xpath()对 XML 数据运行 XPath 查询,并返回与指定路径匹配的子数组。

W3Cschools 的网站上有 XPath 用法的例子。 5

应该注意,与 PHP 结构不同,XPath 结果不是从零开始的。XPath /college/student[1]/name将返回第一个学生,而不是第二个学生,如果它是从零开始的,就会出现这种情况。

包含xpath结果的 PHP 数组是从零开始的。换句话说,如果您将结果存储在名为$array的数组变量中,那么$array[0]将对应于上例中的college/student[1]/名称。

您可以使用这样的 XPath 来检索文本值:/college/student/name[text()]

您可以像这样指定范围:/college/student[attendance<80]/name

在 DOM 和 SimpleXML 之间交换数据

函数simple_xml:import_dom()将把一个 DOM 节点转换成一个SimpleXML对象。

您可以使用dom_import_simplexml()SimpleXML对象转换为 DOM。

肥皂

SOAP 6 最初是简单对象访问协议的首字母缩写。业界发布了 1.0 和 1.1 版本。从版本 1.2 开始,该标准由 W3C 控制,缩写已经消失,使得 SOAP 只是一个普通的名称。

PHP SOAP 扩展用于编写 SOAP 服务器和客户端。它要求启用libxml,这是默认 PHP 安装的情况。

SOAP 缓存功能在php.ini文件中用soap.wsdl_cache_*设置进行配置。

如果 SOAP 可用,那么它会提供一组预定义的常量。这些常量与 SOAP 版本、编码、身份验证、缓存和持久性相关。

只有两个 SOAP 函数:

  • is_soap_fault()返回 SOAP 调用是否失败。
  • use_soap_error_handler()用于 SOAP 服务器,并设置 PHP 是否应该使用 SOAP 错误处理程序。如果设置为 false,则使用 PHP 错误处理程序,而不是向客户端发送 SOAP 错误。

SOAP 的其余功能在类中提供。

肥皂有什么作用

SOAP 允许定义和交换复杂的数据类型,并为各种消息传递模式提供了一种机制,其中最常见的是远程过程调用(RPC)。

这实际上允许开发人员在服务器上执行一个函数,将复杂数据作为参数传递给它,并接收复杂数据。

SOAP web 服务是由 WSDL (Web 服务描述语言)定义的。大多数人把这个首字母缩略词读作“whizz-dill”。

WSDL 使用 XML 结构定义数据类型。它还描述了可以远程调用的方法,指定了它们的名称、参数和返回类型。

服务器和客户端之间的 SOAP 消息以称为 SOAP 信封的 XML 结构发送。

使用 SOAP 服务

SoapClient类用于连接和使用 SOAP 服务。

可以解析一个 WSDL 文件来发现哪些方法是可用的,然后以一种易于使用的方式呈现给你。

<?php
$client = new SoapClient("http://example.com/login?wsdl");
$params = array('username'=>'name', 'password'=>'secret');
// call the login method directly
$client->login($params);

// If you want to call __soapCall, you must wrap the arguments in another array as follows:
$client->__soapCall('login', array($params));

在前面的示例中,我们连接到一个示例 WSDL,并使用两种不同的方法调用 login 方法。注意,使用SoapClient::__soapCall()方法需要将参数包装在一个数组中。

SOAP 服务提供 WSDL 并不是强制性的。如果需要使用这样的服务,可以将 null 作为 WSDL 文件传递,但是需要提供关于服务端点的信息。您必须提供位置和 URI 选项,并且可以选择提供有关 SOAP 服务版本的其他信息,如下例所示:

<?php
$client = new SoapClient(null,
    ['location' => 'http://example.com/soap.php',
        'uri' => 'http://test-uri/',
        'style'    => SOAP_DOCUMENT,
        'use'      => SOAP_LITERAL));
    ]);

当构造SoapClient类时,可以将 trace 参数设置为 true,以便调试原始 SOAP 信封头和主体。

以下两个调试命令要求跟踪为真,并允许您检查请求的详细信息:

  • SoapClient::__getLastRequestHeaders()
  • SoapClient::__getLastRequest()

提供肥皂服务

SoapServer类提供一个 SOAP 服务器。它支持版本 1.1 和 1.2,并且可以在有或没有 WSDL 服务描述的情况下使用。

下面是一个设置 SOAP 服务器的示例:

<?php
$options = ['uri'=>'http://localhost/test'];
$server = new SoapServer(NULL, $options);
$server->setClass('MySoapServer');
$server->handle();

我们可以看到,我们首先创建了带有一系列选项的服务器。在本例中,我们没有在第一个参数中提供 WSDL,因此我们必须在选项数组中提供服务器名称空间的 URI。

一旦我们有了一个SoapServer类的实例,我们就传入它将用来服务请求的类名。连接到服务器的 SOAP 客户端可以调用该类中的方法。

除了设置一个类之外,你还可以使用一个具体的对象来处理 SOAP 请求,方法是用SoapServer::setObject()函数将它作为一个参数传递。

REST Web 服务

REST 是表述性状态转移的缩写,是一种架构风格,而不是 PHP 扩展或命令集。REST 有几个旨在提高 web 服务性能和可维护性的约束。

Tip

将“面向服务的架构”与“微服务架构”进行比较,前者通常在 SOAP 中实现,后者通常在 REST 中实现。

REST 有几个类似于 HTTP 请求类型的动词。这导致了一些混乱,但是需要注意的是,REST 并不一定要使用 HTTP 作为传输层来进行通信。HTTP 恰好对 REST 非常方便,因为它是无状态的,请求类型可以很好地转换成 REST 动词。

REST 公开了链接到资源的统一资源标识符(URI)。这些链接被称为 REST 端点。根据用来访问它们的 HTTP 类型,它们将对资源执行一个动作(改变它的状态)。HTTP 类型用于通知要执行的 REST 动词。

REST 关注资源并提供对这些资源的访问。资源可以是类似于“用户”的东西。就像数据库模式表示用户实体一样,REST 将在 JSON 或 XML 结构中表示用户。

一个表示应该是服务器和客户端都可读的。REST 可以用来传输 JSON 和/或 XML。我们稍后会更详细地讨论这个问题。

在 PHP 中,REST APIs 最常见的用途之一是为支持 AJAX 的前端提供服务,比如用 Angular 或 ReactJS 编写的前端。

应用和资源状态

REST 服务器不应该记住应用的状态,客户机应该发送执行所需的所有信息。

这意味着对服务器的每个请求都是独立的。如果对服务器的请求失败,不会影响其他请求的成功或失败。这提高了应用的可靠性。

服务器不负责记住应用处于什么状态,而是依赖客户端发送处理请求所需的所有信息。这意味着客户端存储和维护应用状态(而不是服务器)。

应用无状态对水平伸缩有着重要的影响。因为没有单独的服务器维护状态,所以请求可以到达组中的任何服务器并被正确处理。

REST 提供访问的资源的状态应该在请求之间持续。资源状态在服务器上维护。

休息动词

REST 有几个用于改变服务器上资源状态的动词。

动词既可以作用于单个资源,也可以作用于一组资源。

| 资源 | GET | PUT | POST | DELETE |
| 募捐 | 列出您可以从中检索成员的 URIs | 用另一个集合替换该集合 | 在集合中创建新条目 | 删除整个集合 |
| 单一实体 | 检索单个元素的表示 | 替换该元素,如果它不存在,则创建它 | 创建新成员 | 删除成员 |

PUTPOST看起来很相似,但是有一个重要的区别。POST要求您为一个元素指定所有必需的属性,并将创建一个新元素。PUT将替换您为现有记录指定的属性,除非您正在创建新记录,否则您不需要提供所有属性。

为了举例说明,让我们考虑一个有名字和头衔的用户。首先,我们POST创建一个名为“Alice”、头衔为“Mrs”的新用户。然后爱丽丝毕业了,成为了一名医生,所以我们PUT到她的记录中,只包括了“博士”这个头衔。我们不需要指定她的名字,因为我们不需要,她的名字也不会被改变。

恨死我了

HATEOAS 代表“超文本是国家的引擎”。在这个概念中,来自服务器的响应将包括关于客户端接下来可以采取什么动作的信息。这些选项将用超文本标记出来。

目的是让客户不需要预先知道REST服务的端点。相反,当他们进行查询时,将为他们提供通过应用所需的端点。

让我们考虑一个例子:

GET /account/12345 HTTP/1.1

HTTP/1.1 200 OK
<?xml version="1.0"?>
<account>
<account_number>12345</account_number>
<balance currency="usd">100.00</balance>
<link rel="deposit" href="/account/12345/deposit" />
<link rel="withdraw" href="/account/12345/withdraw" />
<link rel="transfer" href="/account/12345/transfer" />
<link rel="close" href="/account/12345/close" />
</account>

在前面的例子中,从 HATEOAS, 7 上的维基百科页面,我们正在检索一个银行账户的信息。服务器用可用于进一步操作的 URIs 列表进行响应。例如,如果帐户余额为负,服务器可能不包括取款的链接。服务器通过暴露与上一次操作相关的附加 URIs 来引导客户端通过 API。

请求标题

HTTP 允许在其请求中传递标头。REST 客户机将使用这些来指示服务器它们正在提供什么以及它们期望得到什么。

REST 客户机应该使用 accept 头向服务器指出它想要返回哪种内容(表示)。例如,如果客户机将 accept 头设置为text/xml,它就告诉服务器它需要一个 XML 格式的响应。

客户端还将设置一个Content-Type头,通知服务器其有效负载的 MIME 类型。有关更多详细信息,请参见响应标题中的部分。

回应标题和代码

Content-Type头是由服务器发送的,它定义了被发送的主体的 MIME 类型。例如,服务器可以将content-type设置为application/json,以表明响应的主体包含 JSON 格式的文本。

服务器还将设置一个状态代码,通知客户端请求的结果。这里列出了一些常见的代码,但还有更多。 8

密码 意义
200 请求处理成功
201 资源已创建
202 资源已接受处理,但尚未处理
400 错误的请求(客户端错误)
401 未经授权;客户端在访问该资源之前必须进行自我验证
403 禁止;客户端已经对自己进行了身份验证,但是没有权限访问该资源
500 服务器或应用错误

Tip

在响应正文中发送与 HTTP 响应代码相矛盾的消息是非常糟糕的做法。

在 Zend 框架中,术语“上下文切换”指的是根据程序是响应 REST 请求还是其他请求来改变程序的输出。

例如,对于普通请求,您可以用 HTML 页面来响应,如果请求是通过XMLHttpRequest (AJAX)发出的,您可以用 JSON 来响应。

您也可以用 XML 或 JSON 来响应,这取决于客户机指示它需要什么类型的内容作为响应。

另一个例子是用不同的布局来响应,这取决于使用的是哪种浏览器(例如,移动设备还是桌面设备)。

您应该熟悉服务器对同一 URL 的调用做出不同响应的概念,这取决于客户端如何设置其请求。

发送请求

curl扩展是 PHP 中发送 REST 请求的常用方式。Curl 允许您指定头和请求类型。

有包装了curl函数的库。其中比较受欢迎的是 Guzzle, 9 这款软件安装和使用都很简单。它提供了非常广泛的特性,在我撰写本文时,我认为它是 PHP 请求客户端的最佳选择。

数据

JSON 是 JavaScript 对象符号的首字母缩写。在 PHP 中,它经常与 Ajax 一起使用,Ajax 是异步 JavaScript 和 XML 的缩写。

JSON 允许您将对象序列化为字符串,以便它可以在服务之间传输。Ajax 是传输字符串的一种方式。

这些技术一起允许您在浏览器中的 JavaScript 应用和服务器上的 PHP 应用之间进行通信。

缺省情况下,JSON 扩展加载在 PHP 中,并提供处理与 JSON 相互转换的方法。

它提供了许多常量,包括:

常数 意义
JSON_ERROR_NONE 确认是否发生了 JSON 错误。
JSON_ERROR_SYNTAX 确认解析 JSON 时是否有语法错误,并帮助检测编码错误。
JSON_FORCE_OBJECT 如果一个空的 PHP 数组被编码,这个选项将强制它被编码为一个对象。

该扩展提供了三个功能。

json_decode()将一个字符串作为第一个参数,并返回一个对象。如果第二个参数设置为 true,它将返回一个关联数组。

从 PHP 5.3 开始,提供了两个额外的选项— $depth$options。深度是指递归深度,目前唯一的选择是JSON_BIGINT_AS_STRING,它将大整数的浮点数转换为字符串。

如果超过递归深度,json_decode()将返回NULLjson_last_error_msg()将返回"Maximum stack depth exceeded"。如果阵列的层数超过您指定的可接受深度,就会发生这种情况。

例如,考虑以下代码:

<?php
$arr = [
    "fruits" => [
        "apple" => ["taste" => "sweet", "color" => "yellow"],
        "banana" => ["taste" => "sour", "color" => "green"],
        "cherry" => ["taste" => "sweet", "color" => "red"]
    ],
    "vegetables" => "yuck"
];
$str = json_encode($arr);
$decode = json_decode($str, true, 1);
echo json_last_error_msg(); // Maximum stack depth exceeded

该数组有两层,因为每个水果都包含一个数组。我们指定只解码一个深度级别,因此$decode将是NULL,脚本将输出"Maximum stack depth exceeded"

json_encode()将任何类型的变量(除了资源)作为参数,并返回 JSON 表示。它有两个可选参数——$depth$options——与前面描述的相同。

json_last_error()返回前一个函数中出现的最后一个错误代码,而json_last_error_msg()返回一个字符串消息。

Tip

记住第六章的内容,JSON 是序列化传输到客户端的数据的首选方式。

日期和时间

PHP 提供了几个从服务器获取日期和时间的函数。您应该在配置中设置一个默认时区,或者在运行时在脚本中设置它。您应该设置时区来匹配您的服务器所在的时区,以便 PHP 可以正确地解释服务器时间。这也让您的脚本知道夏令时等调整。

PHP 5.2 引入了DateTime类,它处理大范围的日期和时间计算。建议使用这个类,而不要使用像date()time()这样的函数。

要创建一个新的DateTime对象,您可以向它传递一个它可以解析的字符串。它可以理解多种字符串格式,如下例所示:

<?php
$strings = [
    'Next monday',
    'Yesterday',
    '', // now
    '2016-12-25',
    '25 December 2016',
    '-1 week',
    '+1 days'
];
foreach ($strings as $example) {
    $dateTime = new DateTime($example);
    echo $dateTime->format(DateTime::COOKIE) . PHP_EOL;
}

本例中数组中的所有字符串都将被理解。

如果日期格式不明确,那么您可以使用DateTime::createFromFormat()命令来创建对象。

例如,日期 2013 年 6 月 3 日将被一个美国人写成 06-03-2013,而世界上的其他人将写成 03-06-2013。如果您向 PHP 提供这些字符串中的任何一个,它都不知道您是指 2013 年 6 月 3 日还是 2013 年 3 月 6 日。

要解决这种不确定性,您可以指定在字符串中使用哪种格式,如下所示:

<?php
$dateTime = DateTime::createFromFormat('d-m-Y', '06-03-2013');
echo $dateTime->format(DateTime::COOKIE);

该脚本将输出类似于欧洲中部时间 2013 年 3 月 6 日星期三 12:56:42 的内容。注意,如果您在创建一个DateTime类时省略了时间,将使用脚本运行的时间。

格式化日期

在这些例子中,我们使用了由DateTime提供的一个类常量来格式化我们的日期。

手册中列出了这些常量,它们是日期显示或存储的常见用例。它们出现在下表中:

常数 格式
ATOM Y-m-dTH:i:sP
COOKIE l, d-M-Y H:i:s T
ISO8601 Y-m-dTH:i:sO
RFC822 D, d M y H:i:s O
RFC850 l, d-M-y H:i:s T
RFC1036 D, d M y H:i:s O
RFC1123 D, d M Y H:i:s O
RFC2822 D, d M Y H:i:s O
RFC3339 Y-m-dTH:i:sP
RSS D, d M Y H:i:s O
W3C Y-m-dTH:i:sP

这些是字符串常量,包含日期和时间格式代码。格式化代码被替换为一个由DateTime类生成的值。例如,符号“Y”被替换为存储日期的四位数年份。

显然,声明常量的目的是让你不必去记忆字符串,所以不用担心学习格式。我包含了格式化字符串,因为它们很好地表明了常用的格式。

日期和时间格式代码区分大小写。例如,“Y”是两位数的年份,“Y”是四位数的年份。

格式化字符串中未被识别为格式化字符的字符将被原封不动地放入输出中。因此,字符串“Y-m-d”在输出时将包括年、月和日之间的连字符,就像这样“2015-12-25”。

您可以在手册页上找到 PHP 日期和时间格式代码的列表, 10 ,但这里是上表中的那些:

密码 用…替换 示例
Y 整整四位数的年份 One thousand nine hundred and ninety-nine
M 两位数的月份,带前导零 06
d 一个月中的某一天,带前导零的两位数 Fourteen
D 三个字母的文本日 杀了他,Wed
H 带前导零的 24 小时制小时 00, 09, 12, 23
i 两位数的分钟,带前导零 05,15,25,45
s 两位数秒,带前导零 05,15,25,45
P 与格林威治时间(GMT)的差异,小时和分钟之间有冒号(PHP 5.1.3+) +02:00
O 与格林威治时间(GMT)的时差(小时) +0200
T 时区缩写 是啊,这个

日期计算

使用DateTime类方法modify()可以执行最简单的计算。例如,要查找一个月后的日期和时间,您可以执行以下操作:

<?php
$dateTime = new DateTime();
$dateTime->modify('+1 month');
echo $dateTime->format(DateTime::COOKIE) . PHP_EOL;

然而,PHP 提供了一种更加灵活的方式来处理日期计算。

DateInterval类用于存储固定的时间量(年、月、日、小时等)。)或一个相对时间字符串,其格式为DateTime的构造函数支持的格式。

DateTime类允许你从DateTimeadd()sub()出一个DateInterval。它将处理闰年和其他时间调整,同时这样做。

为了在创建一个DateInterval对象时指定一个固定的时间量,我们向它的构造函数传递一个字符串。该字符串总是以P开头,然后以降序列出每个日期单元的编号。可选地,出现字母T,然后包括时间单位。

举几个例子来说明这一点更有意义:

线 描述
P14D 14 天
P2W 两周
P2W5D 这是无效的;不能在一个字符串中同时指定周和日;这些星期将被忽略
P2WT5H 两周零五个小时
P1Y2M3DT4H5M 一年,两个月,三天,四小时,五分钟

请注意:

  • 每个字符串都以 P 开头
  • 单位数位于表示单位的字母之前
  • 时间单位通过字母 T 与日期单位分开
  • 单位按降序排列

下面是一个代码示例:

<?php
$dateTime = DateTime::createFromFormat('d-m-Y H:i:s', '01-12-2016 13:14:15');
$dateInterval = new DateInterval('P1M2DT3H4M5S');
$dateTime->add($dateInterval);
echo $dateTime->format(DateTime::COOKIE) . PHP_EOL;

此代码输出日期和时间,即 12 月 1 日 13:14:15 之后的一个月、两天、三小时、四分钟和五秒。

手动日期计算

有时,您需要使用 UNIX 风格的时间戳。这个时间戳是一个数字,它保存了自 UNIX 纪元 1970 年 1 月 1 日以来经过的秒数。时间戳的一个优点是它不受时区的限制。

有几个 PHP 函数可以让你创建一个时间戳。strtotime()函数是一种非常灵活的将日期时间描述转换成时间戳的方法。它足够智能,可以识别像“下周一”或“+1 年”这样的短语,以及像“2017 年 4 月 1 日”这样更普通的字符串。

mktime()函数接受每个小时、分钟、秒、月、日或年的参数。mktime()返回给定参数的 UNIX 时间戳。如果参数无效,函数返回FALSE

请注意,参数的顺序不会随着单元大小的增加而增加,而是按照“h i s m d y”的顺序。

您可以从右到左省略参数,在这种情况下,它们将默认为当前日期值。所以如果当前年份是 2016 年,你在没有指定年份的情况下调用mktime(),PHP 会假设你指的是 2016 年。

如果您传递给mktime()的参数大于允许的值,mktime()会认为您引用的是下一个周期。

例如,十二月有 31 天。如果您调用mktime(0, 0, 0, 12, 32, 2016),那么您将获得下个月第一天的时间戳;换句话说,2017 年 1 月 1 日。

比较日期

DateTime::diff()方法允许您比较两个DateTime对象之间的差异。它返回一个DateInterval,包含所表示的两个日期之间的时间段。

注意,DateTime类为您处理时区和夏令时转换。

让我们试着找出离圣诞节还有多长时间。

<?php
$now = new DateTime();
$christmas = new DateTime('25 december');
if ($now > $christmas) {
    $christmas = new DateTime('25 december next year');
}
$interval = $christmas->diff($now);
// 97 days until Christmas
echo  $interval->days . ' days until Christmas' . PHP_EOL;

请注意这段代码中的以下内容:

  • 不向构造传递任何参数都使用当前日期和时间。
  • 我们可以使用像><==这样的数学运算符来比较DateTime对象。
  • 当创建一个DateTime时,我们可以使用相当灵活的语言,例如“明年 12 月 25 日”用于当前日期在圣诞节和新年之间的情况。
  • diff()方法返回一个DateInterval
  • 对象有许多公共属性,可以访问这些属性来度量年、月,在本例中是天。

PHP SPL 数据结构

标准 PHP 库(SPL)是用来解决常见问题的接口和类的集合。它包括几个帮助你处理标准数据结构的类。

与数据结构相关的接口

在我们看 SPL 数据结构类之前,有必要看一下它们实现的一些接口。这使得记住这些类有什么功能变得相当容易。

迭代程序

Iterator接口扩展了Traversable接口。

Iterator接口 11 定义了用于在集合中移动的五种方法。

方法 目的
current 返回当前元素
key 返回当前元素的键
next 向前移动到下一个元素
rewind 将迭代器倒回到第一个元素
valid 检查当前位置是否有效

可穿越

实现可遍历接口 12 的类可以使用foreach()进行循环。

这个接口不能自己实现,它只能通过实现一个告诉类如何迭代集合的接口来实现。

实际上,这意味着要实现可遍历接口,您必须实现IteratorIteratorAggregate接口。

数组式访问

该接口提供了以数组形式访问对象的能力。为此,您需要实现四种方法:

方法 目的
offsetExists 偏移是否存在
offsetGet 要检索的偏移量
offsetSet 为指定的偏移量赋值
offsetUnset 取消偏移设置

如果您的类实现了此接口,那么您将能够在引用从它实例化的对象时使用数组语法。

可数的

如果您的类实现了Countable接口,您将能够使用count()函数来找出它有多少个元素。

Countable接口有一个名为count的抽象方法。当你在一个对象上调用 PHP 函数count()时,这个方法将被调用,这个对象是从一个实现接口的类实例化而来的。

<?php
class BadCount implements Countable
{
  public function count()
  {
    return 42;
  }
}
$a = new BadCount;
echo count($a);  // 42

在这个简单的例子中,这个类中的count()方法总是返回数字42。在一个更复杂的例子中,我们可以在这里实现逻辑,定义如何返回对象的计数。

列表

列表是元素的有序集合。同一个值可能在一个列表中出现多次。双向链表中的每个元素都包含一个到链中上一个和下一个元素的链接。

SplDoublyLinkedList 13 类实现了IteratorArrayAccessCountable接口。此外,它还实现了一些方法,让您可以更改迭代器的行为,以及在列表的前面或后面添加或删除条目。

SplStack14 扩展了SplDoublyLinkedList类。它本质上是一个SplDoublyLinkedList,你调用了setIteratorMode() 15 ,并设置列表使用IT_MODE_LIFO进行迭代,并在模式IT_MODE_KEEP下运行。这告诉迭代器像堆栈一样遍历列表(后进先出),遍历元素而不是删除它们。

SplQueue16 也扩展了SplDoublyLinkedList类。它实现了方法enqueuedequeue,这两个方法分别将一个元素添加到队列的末尾或删除队列前面的元素。

Caution

SplStackSplQueue类都继承自SplDoublyLinkedList类,所以你可能会错误地调用它们的错误方法。

这里有一个使用堆栈的例子,展示了一些你可以使用的方法。下表显示了堆栈中包含的值。

密码 堆栈包含
<?php
$stack = new SplStack(); Null
$stack->push(5); 5
// this uses array syntax to add a new element
$stack[] = 4; 5, 4
// now we push another number to the end of queue
$stack->push(3); 5, 4, 3
// this inserts the number 100 into position 1
// elements below it are shuffled down
$stack->add(1, 100); 5, 100, 4, 3
// this returns the last value in the queue
echo "Pop: " . $stack->pop() . PHP_EOL; 0, 100, 4
foreach ($stack as $key => $value) {
echo "$key => $value" . PHP_EOL;
}

这段代码的输出如下:

Pop: 3
2 => 4
1 => 100
0 => 5

Note

密钥以降序(2,1,0)包含在堆栈中。

堆是树状结构,其中父节点可以有零个、一个或多个子节点。堆定义了一个比较规则,允许您确定一个节点是大于还是小于另一个节点。在堆中,父节点总是等于或大于其子节点。比较函数用于确定一个节点是大于还是小于另一个节点。

Note

SplHeap类是一个抽象类。使用时需要实现compare函数。

SplHeap类实现了Iterator 17 接口,这意味着您可以使用foreach()在其中移动。

SplMaxHeap类从SplHeap扩展而来,在顶部保持最大值。它通过为您实现compare()功能来实现这一点。同样,SplMinHeap类将最小值保持在顶部。

Note

SplMinHeapSplMaxHeap只是扩展了SplHeap并实现了compare()来提供定向排序的类。

让我们看一个简单的堆的例子:

<?php
class MyHeap extends SplHeap
{
  function compare($a, $b)
  {
    return $a <=> $b;
  }
}

$heapExample = new MyHeap;
$heapExample->insert(10);
$heapExample->insert(5);
$heapExample->insert(15);

while ($heapExample->valid()) {
  echo $heapExample->current() . PHP_EOL;
  $heapExample->next();
}

这段代码按照降序输出数字,因为当我们插入数字时,它会应用compare()函数来确定将它们放在哪里。

Note

如果我们修改代码并扩展SplMinHeapSplMaxHeap而不是SplHeap,输出与之前的代码相同!

我可以听到你烦恼地说SplMinHeap应该把最低的值放在最上面,那么为什么输出显示 15 仍然在最上面呢?答案是因为SplMinHeapSplMaxHeap类提供的只是compare()函数的默认实现,我们在类定义中覆盖了它。

您可以扩展SplMinHeap,但是只要您的compare()函数保持不变,就像前面的例子一样,您将始终拥有一个最大堆。为了获得一个最小堆的工作实现(在我们的例子中),你需要或者交换飞船操作符的操作数,或者完全避免实现compare()函数,而使用在SplMinHeap中声明的函数。

数组

SplFixedArray 18 结构以连续的方式存储数据,可通过索引访问。它比普通的 PHP 数组更快,但也不太灵活,因为它是固定长度的,只能使用整数作为索引。

SplFixedArray类实现了Iterator接口和ArrayAccess接口。

地图

映射是一种保存键值对的结构。PHP 数组是一种映射,因为它存储整数(或字符串)键的值。

SplObjectStorage提供了从对象到数据的映射,或者如果您忽略数据,它可以作为一个对象集。

SplObjectStorage不是抽象类,可以直接实例化。它实现了CountableIteratorSerializableArrayAccess接口。

因为它实现了ArrayAccess接口,所以您可以使用数组语法来引用结构内部对象的数据,如下所示:

<?php
$bucket = new SplObjectStorage();
$file = new StdClass;
$metaData = ['name' => 'passwords.xslx', 'size' => '102400'];
$bucket[$file] = $metaData;

在本例中,我们将数据(元数据)映射到一个对象的特定实例(文件)。

SPL 数据结构概述

| SplHeap | 堆是一个树集合,其中父级的子级必须始终具有低于其父级的值。有不同类型的堆。 |
| SplMaxHeap | 这是一种最大值保存在堆顶部的堆。 |
| SplMinHeap | 在这种类型的堆中,最小值保存在顶部。 |
| SplPriorityQueue | 这是一个队列,其中每个元素都有一个与之关联的“优先级”。一个用例的例子是带宽管理,其中特定类型的流量比其他流量具有更高的优先级。 |
| SplFixedArray | 这是一个更快的数组实现,但是它限制了您使用一个固定长度的数组,该数组只包含整数。 |
| SplObjectStorage | 这个类提供了一种方便的方法来映射对象及其数据。 |

还有一个名为DS的扩展,它提供了可选的数据结构。你可以在 PHP 网站 19 上找到它的文档,在 GitHub 上找到它的源代码。你不需要在 Zend 考试中知道它。

Chapter 7 Quiz

Q1:对还是错?无法在目标 XML 编码方案中编码的字符会生成错误。

| 真实的 |
| 假的;他们会发出警告 |
| 假的;它们符合编码方案(转换成问号) |
| 以上都不是 |

Q2:对还是错?如果请求失败,服务器不可能发送带有 HTTP 状态代码 200 的REST响应。

| 真实的 |
| 错误的 |

Q3:这段代码会输出什么?

| 语法错误;它不会运行 |
| 没什么;没有错误消息,所以echo语句不输出任何内容 |
| 超过了最大堆栈深度 |
| 致命错误,json_decode的第二个参数不能是" " " |

<?php
$arr = [
  "fruits" => [
    "apple" => ["taste" => "sweet", "color" => "yellow"],
    "banana" => ["taste" => "sour", "color" => "green"],
    "cherry" => ["taste" => "sweet", "color" => "red"]
  ],
  "vegetables" => "yuck"
];
$str = json_encode($arr);
$decode = json_decode($str, true, 1);
echo json_last_error_msg();

Q4:您应该为您的 PHP 应用设置默认时区。您可以使用以下哪种方法来实现这一点?选择尽可能多的适用项。

| 使用功能set_date_default_timezone() |
| 编辑php.ini |
| 在 PHP 上使用 Linux time()命令 |
| 使用 PHP 的ini_set()函数,像这样:ini_set('date.timezone', 'Europe/Edinburgh'); |

Q5:这段代码会输出什么?

| 4 |
| 5 |
| A fatal error will occur |

<?php
$stack = new SplStack();
$stack->push(5);
$stack[1] = 4;
echo $stack->pop();

Q6:下面的 PHP 代码有什么问题?

| 语法错误;它根本不会跑 |
| 登录方法的参数需要像这样传递:$client->login([$params]); |
| 你不能直接调用SoapClient上的方法 |
| 没什么不对;这会有用的 |

<?php
$client = new SoapClient("http://example.com/login?wsdl");
$params = array('username'=>'name', 'password'=>'secret');
// call the login method directly
$client->login($params);

Q7:这段代码会输出什么?

| 语法错误;它不会跑 |
| 银背企鹅 |
| 金色的眼睛 |
| 它将生成一个警告,因为xpath将无法评估 |

<?php
$xmlString = <<<XML
<root>
<teams>
<team>Silverbacks</team>
<team>Golden Eyes</team>
</teams>
</root>
XML;
$xml = new SimpleXMLElement($xmlString);
$result = $xml->xpath('teams/team[1]');
echo $result[0];

Q8:可以用 ______ 函数将 SimpleXML 对象转换成 DOM。

| dom_import_simplexml() |
| 简单 xml:导入 dom() |
| 简单 xml:导出 dom() |
| 以上都不是 |

Q9:这个脚本的输出是什么?

| 这将产生致命的错误 |
| 在团队列表的开头有一个新团队的 XML 文档 |
| 两个团队之间有一个新团队的 XML 文档 |
| 以上都不是 |

<?php
$xmlString = <<<XML
<root>
<teams>
<team>Silverbacks</team>
<team foo="winner">Golden Eyes</team>
</teams>
</root>
XML;
$domDoc = new DOMDocument();
$domDoc->loadXML($xmlString);
$textElement = $domDoc->createElement('team', 'Bearhides');
$result = $domDoc->xpath('teams/team[2]');
$result[1]->insertBefore($textElement);
echo $domDoc->saveXML();

Q10:下面的代码会输出什么?

| 这将产生致命的错误 |
| 未来一年、两个月、三天、四小时零五分钟的日期 |
| 以上都不是 |

<?php
$dateTime = new DateTime();
$interval = new DateInterval('P1Y2M3D4H5M');
$dateTime->add($interval);
echo $dateTime->format(DateTime::COOKIE);

Footnotes 1

https://en.wikipedia.org/wiki/Well-formed_document

2

https://php.net/manual/en/xsltprocessor.transformtoxml.php

3

https://php.net/manual/en/xml.error-codes.php

4

https://en.wikipedia.org/wiki/XPath

5

https://www.w3schools.com/xml/xml:xpath.asp

6

https://en.wikipedia.org/wiki/SOAP

7

Https://en.wikipedia.org/wiki/HATEOAS

8

https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

9

http://docs.guzzlephp.org/en/stable/

10

https://php.net/manual/en/function.date.php

11

https://php.net/manual/en/class.iterator.php

12

https://php.net/manual/en/class.traversable.php

13

https://php.net/manual/en/class.spldoublylinkedlist.php

14

https://php.net/manual/en/class.splstack.php

15

https://php.net/manual/en/spldoublylinkedlist.setiteratormode.php

16

https://php.net/manual/en/class.splqueue.php

17

https://php.net/manual/en/class.iterator.php

18

https://php.net/manual/en/class.splfixedarray.php

19

https://docs.php.net/manual/en/book.ds.php

八、输入输出

在这一章中,我们将会看到 PHP 是如何管理输入输出的。我们将研究如何读写文件系统和网络。

文件

有两组主要的函数来处理文件:一组处理文件资源,另一组处理文件名。

请记住,资源是一种不能直接存储在 PHP 中的变量。文件资源是操作系统文件句柄。

所有处理文件资源的函数都以一个字母f开始,然后有一个动词描述它们的功能。例如,fopen()打开一个文件资源。

使用文件字符串名称的函数都以单词file开头,后面跟一个描述它们做什么的动词。例如,file_get_contents()接受一个字符串文件名并返回该文件的内容。

打开文件

功能fopen()用于打开文件。它返回一个资源变量,该变量是文件的句柄。

您必须向fopen()传递两个参数:

  • 文件系统中文件的名称
  • 您要用来打开它的文件模式

文件模式

文件可以在不同的模式下打开。文件模式描述了我们将如何与文件交互。

文件模式与操作系统文件权限相关。例如,如果 PHP 用户对一个文件只有读权限,那么以写模式打开它的尝试将被操作系统拒绝。如果我们尝试使用较低的权限(比如只读),那么操作系统将为我们创建一个文件句柄。

当我们指定一个模式时,我们传达两条关于我们打算如何使用文件的信息:

  • 无论我们是在阅读,写作,还是两者都有
  • 我们是想把文件指针放在文件的开头还是结尾

文件指针就像一个迭代器光标。它存储下次读取时将返回的文件位置。

下表总结了常见的文件模式。

方式 读/写 指针 行为
r R 开始
r+ RW 开始
w W 开始 截断现有文件或创建一个新文件(如果它不存在)
w+ RW 开始
a W 开始 如果文件不存在,则创建一个新文件,如果存在,则保留当前文件
a+ RW 开始
x W 不适用的 尝试创建新文件进行写入;如果文件已经存在,返回FALSE并生成E_WARNING
x+ RW
c W 开始 如果文件不存在,则尝试创建该文件;如果存在,将光标放在文件的前面
c+ RW

您会注意到在文件模式中添加一个+符号的效果是表明您也想执行与默认模式相反的模式。因此,当我们覆盖一个文件时,如果我们添加一个+符号,那么我们就表示我们也想读取这个文件。然而,行为保持不变,因此我从表中省略了它,以便于阅读。

当使用w模式覆盖一个文件时,PHP 会将文件截断为零字节。如果您希望文件被新数据覆盖,这很有用。

如果文件已经存在,x模式将返回FALSE并生成警告。如果您想要避免覆盖您想要保留的数据,这是很有用的。

c模式将创建一个文件(如果存在)或打开一个现有文件。对于现有文件,指针将被设置到文件的开头。

文件模式标志

通过将两个标志添加到模式字符串的末尾,可以指定这两个标志。默认标志是由您的 SAPI 和您正在使用的 PHP 版本定义的,所以出于兼容性的考虑,您应该指定它们。

您可以指定一个b标志来表明您正在处理二进制文件。这意味着不会翻译任何字符。当您处理图像或其他二进制文件时,这是必要的。

在 Windows 服务器上,您可以指定一个t标志来将\n转换为\r\n

Tip

为了保持代码的可移植性,您应该使用b标志并确保您的代码使用正确的行尾。

读取文件

您可以使用fread()函数读取文件资源。

<?php
$handle = fopen('info.txt', 'r');
while (!feof($handle)) {
    echo fread($handle,1024);
}

在这个例子中,我们使用了文件函数feof(),当文件指针在文件末尾时,这个函数返回TRUE,否则返回FALSE。在一个while循环中使用它的效果是继续循环,直到我们到达文件的末尾。

fread()函数有两个参数。第一个是保存文件资源的变量,第二个是要读取的字节数。如果到达文件的末尾,那么fread()将停止读取。

下面是另外四个 PHP 函数,可以更方便地读取文件:

功能 习惯
fgetcsv() 从文件指针中读取一行并解析 CSV 字段
file_get_contents() 获取一个字符串文件名并将结果读入一个字符串
readfile() 读取一个字符串文件名并将内容写入输出缓冲区
file() 将整个文件读入一个数组

写入文件

写入文件是通过二进制安全的fwrite()函数完成的。fputs()是该函数的别名。

fwrite()函数有两个参数——要写入的文件资源和要写入文件的字符串。

对于fgetcsv()函数有一个写对应物,即fputcsv(),它将一个数组格式化为 CSV,并将该行写入一个文件。除了文件资源和数组的参数之外,还需要可选参数来定义 CSV 格式。

如果你想把格式化的字符串写到一个文件中,你应该使用fprintf(),它的工作方式类似于printf()命令。

如果您想将文件的内容转储到连接的客户端,您可以使用fpassthru()。该函数将从当前文件位置开始,将文件的其余部分写入输出缓冲区。

最后,有一个方便的函数可以快速地将字符串写入文件。函数file_put_contents()不需要你提供文件资源,只需要文件名和你想写的字符串。

下面是一些正在使用的函数的简单示例:

<?php
$filename = 'test.csv';
$dataString = '1,2,3,4,5';
file_put_contents($filename, $dataString);
$handle = fopen($filename, 'r');
$myData = fgetcsv($handle);
echo gettype($myData); // array
echo count($myData); // 5

在示例中,我们使用快捷函数file_put_contents()将字符串输出到文件中。我们不需要一个可用的文件资源,因为file_put_contents()为我们处理这个。在我们写完字符串后,我打开一个文件资源从中读取,并使用fgetcsv()从文件中读取。该字符串是一个有效的 CSV 列表,因此$myData包含一个包含五个元素的数组。

文件系统功能

PHP 有一个将你连接到文件系统的函数的扩展列表。我们将在这一章中讨论其中的一些,但是正如我经常做的那样,我将让你参考 PHP 手册以获得详尽的列表。

目录

这组函数允许您遍历、创建和删除目录。

功能 使用
chdir() 改变 PHP 当前的工作目录。
chroot() 将正在运行的进程的根目录更改为指定的目录,并将 PHP 的工作目录设置为/
rmdir() 删除目录。
readdir() 返回作为参数传递的目录句柄中下一个条目的名称。条目按照文件系统存储它们的顺序返回。
scandir() 读取由 string 参数指定的目录,并返回它包含的文件和目录的列表。

scandir()和readdir()的区别在于它们取的参数。其中readdir()使用目录句柄,scandir(接受目录名作为字符串。

Caution

这可能令人困惑,因为文件函数的命名约定(f*file*)似乎不适用于目录。

文件信息

我们在安全性一章中提到了这些函数,但是还有其他需要获取文件信息的用例。

PHP 提供了finfo_open()函数,该函数返回一个fileinfo资源的新实例。您为它提供了两个参数——一个预定义的选项常量和一个 magic 数据库文件的字符串位置。

魔术数据库文件是一种用于描述文件类型的格式,也由 Unix 标准命令使用,file. 1 如果您不提供魔术数据库的路径,那么 PHP 将使用它附带的路径。

一旦 PHP 知道如何识别文件,你就可以使用finfo_file()函数来获得关于文件的信息。它至少需要两个参数——您刚刚创建的fileinfo资源和您想要检查的文件的字符串名称。

下面是 PHP 手册 2 中的一个例子:

<?php
$finfo = finfo_open(FILEINFO_MIME_TYPE);
foreach (glob("*") as $filename) {
    echo finfo_file($finfo, $filename) . "\n";
}
finfo_close($finfo);

这两个函数都具有面向对象的使用风格,如 PHP 手册中的示例所示:

<?php
// finfo will return the mime type
$finfo = new finfo(FILEINFO_MIME, "/usr/share/misc/magic");

/* get mime-type for a specific file */
$filename = "/usr/local/something.txt";
echo $finfo->file($filename);

管理文件

你可以用 PHP 来管理文件。下表列出了一些常用功能。

功能 目的
copy 复制文件。
unlink 删除文件。
rename 重命名文件。你可以用它在目录间移动文件。
chmod 设置文件权限。
chgrp 更改文件的组。
chown 更改文件的所有者(仅限超级用户)。
umask 更改当前的 umask。

确定文件系统对象的类型

验证文件和目录是否存在,以及您是否有适当的权限以您想要的方式使用它们,这是一个很好的编程实践。

PHP 提供了一些函数,如果匹配作为参数传递的字符串的对象满足测试,这些函数将返回布尔值。这些函数接受一个字符串参数,该参数是文件或目录的名称。

在下表中,检查针对的是找到的与参数中给定的名称相匹配的对象。

功能 检查
is_dir 是一个目录
is_file 是一个文件
is_readable 是一个文件或目录,可以读取
is_writeable 是一个文件或目录,可以写入
is_executable 是一个文件或目录,可以被执行
is_link 是一个符号链接
is_uploaded_file 由一个POST请求上传

如果没有找到与参数中给出的名称相匹配的文件系统对象,所有函数都将返回FALSE

魔法文件常量

PHP 有几个神奇的常数,你可以用在当前执行的文件中。

常数 涉及
__LINE__ 当前正在执行的文件的行
__FILE__ 文件的完整路径和文件名
__FUNCTION__ 当前函数名
__CLASS__ 范围内的类的名称
__METHOD__ 正在执行的方法的名称

这些常量在编写调试日志时非常有用。例如,我通常以__METHOD__标记开始我的所有日志消息,这样就可以立即清楚日志消息是在哪个类和方法中生成的。

PHP 中的流是一种概括文件、网络、数据压缩和其他共享一组公共功能和用途的操作的方式。

溪流几乎就像一条传送带,一件一件地向你涌来。在 PHP 中,你也可以沿着传送带跳跃,寻找一个位置,而不是等待它来找你。

流是以您可能认识的格式引用的:

scheme://target

例如, http://www.php.nethttp方案和target指定为 PHP 网站的 URL。

流包装

包装器是将流翻译成特定编码或协议的代码对象。PHP 手册 3 中有一个在该语言中实现的包装器列表,stream_wrapper_register()函数让你定义自己的包装器。

草案 使用
file:// 访问本地文件系统
http:// 访问 HTTP(s)URL
ftp:// 访问 FTP URL
php:// 访问各种 I/O 流
compress.zlib:// 压缩流
data:// 数据(RFC 2397)
glob:// 查找与模式匹配的路径名
phar:// PHP 存档
ssh2:// 安全外壳 2
rar:// 压缩包
ogg:// 音频流
expect:// 流程交互流

您可以访问的 PHP 流有stdinstdoutstderrinputoutput、fd、memorytempfilter

注意,为了提高可读性,我省略了所有这些流的协议。当你使用它们的时候,它们都应该以php://协议为前缀,比如stdin就是php://stdin

作为读取流的例子,让我们看看如何读取一个PUT请求的主体。在您职业生涯的某个时候,您将编写一个 REST API,并且需要读取和解析客户端向您的服务器发出的PUT请求的主体。这种请求类型没有像GETPOST那样的超级全局,那么它是如何实现的呢?答案就在php://input流里!

<?php
// reads the PUT body
$input = file_get_contents('php://input');
// parses the input into an array
parse_str($input, $params);
print_r($params);

过滤

流过滤器可以应用于流,并对离开流的数据执行转换操作。

过滤器 功能
string.rot13 用 ROT13 编码数据
string.toupper 将字符串转换为大写
string.tolower 将字符串转换为小写
string.strip_tags 从字符串中去除 XML 标记
convert.* 例如,根据算法转换数据
mcrypt.* 使用libmcrypt提供对称加密
mdecrypt.* 使用libmcrypt的解密过滤器
zlib.* 使用 ZLIB 库来压缩和解压缩数据

使用stream_filter_append()函数将这些过滤器附加到流上。您可以独立地将筛选器应用于流的读取和写入方向。

<?php
$handle = fopen("files.php", 'a+');
stream_filter_append($handle, 'string.rot13');
while (!feof($handle)) {
    echo fread($handle,1024);
}

您可以向stream_filter_append()提供第三个参数,将其附加到读或写流中。该参数是预定义常数STREAM_FILTER_READSTREAM_FILTER_WRITESTREAM_FILTER_ALL之一。默认情况下,筛选器附加到读取和写入。

该示例将输出如下内容:

<?cuc
$unaqyr = sbcra("svyrf.cuc", 'n+');
fgernz_svygre_nccraq($unaqyr, 'fgevat.ebg13');
juvyr (!srbs($unaqyr)) {
    rpub sernq($unaqyr,1024);
}

流上下文

流上下文是一组选项的包装器,可以修改流的行为。

您用stream_context_create()函数创建一个上下文。您向它传递两个可选参数,这两个参数都是关联数组。第一个参数是选项,第二个是上下文参数数组。

每种类型的流都有自己的一组上下文选项。PHP 手册中有详尽的列表。

当前唯一可用的参数是一个 callable,当流上发生事件时将调用该参数。这些事件都是预定义的STREAM_NOTIFY_*常量。

回调函数的原型在 PHP 手册中,还有一个 HTTP 流的 notify 事件的例子。

举个例子,如果你正在下载一个文件,你可以设置你的回调函数来响应STREAM_NOTIFY_FILE_SIZE_IS事件,如果文件太大就中止下载。这个例子阻止我们下载大于一千字节的 www.example.com 的主页。

<?php
function callback($notification_code,
    $severity,
    $message,
    $message_code,
    $bytes_transferred,
    $bytes_max)
{  
    if ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) {
        if ($bytes_max > 1024) {
            die("Download too big!");
        }
    }
}

$context = stream_context_create();

stream_context_set_params($context,
    ["notification" => "callback"]);

$handle = fopen('http://www.example.com', 'r', false, $context);

fpassthru($handle);

您可以使用stream_context_set_params()功能更改选项和参数,而stream_context_get_params()将返回流的当前参数。

Chapter 8 Quiz

Q1:假设 web 服务器用户拥有data.csv文件,并且在这个脚本运行之前它包含字符串"Hello World"。这段代码的输出会是什么?

| 字符串(0)" " |
| 字符串(1)"," |
| 字符串(1) "1 " |
| 字符串(1) "2 " |
| 这将产生一个错误 |

<?php
file_put_contents('data.csv', '1,2,3,4,5');
$handle = fopen('data.csv', 'c+');
$data = fgetcsv($handle, 2);
var_dump($data[1]);

Q2:这段代码会输出什么?

| 这将产生一个错误 |
| 1,2,3,4,5,6,7,8 |
| 6,7,8,1,2,3,4,5 |
| 6,7,8 1,2,3,4,5 |
| 6,7,8,1,2,3,4,5 |
| 1,2,3,4,5 6,7,8 |

<?php
file_put_contents('test.csv', '1,2,3,4,5');
$handle = fopen('test.csv', 'c');
fputcsv($handle, ['6', '7', '8']);
fclose($handle);
echo file_get_contents('test.csv');

Q3:如果你正在编写一个 REST 接口,并且需要读取在一个PUT请求中发送的参数,你怎么做?

| 参考$_REQUEST超级全球 |
| 参考$_POST超级全球 |
| 阅读php://input stream |
| 阅读http://input stream |

Q4:我想在我的 PHP 程序运行时将日志条目写入一个文件。我不想丢失旧的日志条目,我需要我的日志条目有适当的日期顺序,最新的条目在旧的条目之后。我应该用什么文件模式打开我的文件?

| r |
| a |
| x |
| c |

Q5:假设我获取的文件是一个有效的 GIF 格式图像,并且我在 Linux 中运行 PHP。这段代码的输出会是什么?

| 这会产生一个错误 |
| GIF 图像数据,版本 89a,400x400 |
| JPEG 图像数据,400x400 |
| image/gif |
| image/jpeg |
| 无法重命名文件 |
| 以上都不是 |

<?php
// This is a valid GIF image
$url = ' https://goo.gl/QycgqH';
file_put_contents('earth.gif', file_get_contents($url));
if (!rename('earth.gif', 'earth.jpeg')) {
    throw new RuntimeException('Could not rename the file.');
}
$finfo = new finfo();
echo $finfo->file('earth.jpeg') . PHP_EOL;

Footnotes 1

[en.wikipedia.org/wiki/File_(command)](https://en.wikipedia.org/wiki/File_(command)

2

https://php.net/manual/en/function.finfo-file.php

3

https://php.net/manual/en/wrappers.php

九、Web 功能

PHP 是一种为网络而创造的语言。它最初的目的是让制作网页变得更容易,现在它仍然非常关注服务器端脚本。本章着眼于使它成为世界上最流行的服务器端 web 编程语言之一的一些语言特性。

请求类型

HTTP 有几种不同的请求方法 1 ,它们通常被称为 HTTP 动词。HTTP 规范 2 相当详细地列出了每个动词的用途。您的应用应该遵守这个规范,以便与使用它的客户端兼容。

动词 习惯
GET 检索指定资源的表示形式
HEAD GET相同,但没有任何响应体
POST 向服务器提交一个条目,通常会导致诸如添加新资源之类的更改
PUT 用请求负载中的资源替换指定的资源
PATCH 对指定的资源应用部分修改
DELETE 删除指定的资源
CONNECT 发起一个 HTTP 隧道 3
选择 描述目标资源的通信选项
TRACE 对目标资源执行消息环回测试

请求日期

在典型的生产 web 环境中,PHP 接受 web 服务器传递给它的请求。它运行并处理请求,然后终止并等待下一个请求。web 服务器可以随请求传递数据,这些数据构成了 PHP 运行的上下文 4 的一部分。

HTTP 请求由三部分组成:URL、头部和主体。数据可以包含在请求的任何部分中,并且可以用于 PHP 应用,如下所示:

来源 进来了 可用于
GET 请求 URL 中的参数 $_GET
POST 请求的正文 $_POST
PUT 请求的正文 php://input阅读
PATCH 请求的正文 php://input阅读
饼干 “cookie”标题 $_COOKIE
上传的文件 请求的正文 $_FILES

如果 PHP 正在从命令行处理一个请求,那么$_SERVER['argv']包含一个传递的参数数组,而$_SERVER['argc']包含传递的参数个数。

除了 HTTP 请求中包含的数据之外,PHP 还可以接受来自其运行环境的数据。例如,您可以在 docker 容器中运行 PHP,并设置一个包含数据库服务器地址的环境变量。您可以使用$_ENV超全局变量在 PHP 脚本中引用它。 5

请求超全局

$_REQUEST超全局是一个关联数组,默认包含$_GET$_POST$_COOKIE的内容。

php.ini设置variables_order决定了ENVGETPOSTCOOKIE变量中的哪一个出现在$_REQUEST数组中以及顺序。 6

如果同一变量存在于多个请求类型中,它将采用该设置值序列中最后一个的值。

例如,假设配置被设置为EGPCS,表示POSTGET之后。那么如果$_GET['action']$_POST['action']都被设置,那么$_REQUEST['action']将包含$_POST['action']的值。

因为您不能确定$_REQUEST中的数据到底来自哪里,所以您应该小心使用这个数组。在代码中引入不确定性会使测试变得复杂,并可能影响安全性。

邮政

按照惯例,POST请求用于向网站发送数据,并指示它创建一个新的实体。在 CRUD 范例中,这是一个写操作。

接收过帐数据

POST请求中发送的变量包含在主体中。与在 URL 中传递变量的GET请求形成对比。

如果用户提交一个表单,那么浏览器会将这些值编码到请求的正文中并发送给你。类似地,指向 API 端点的应用需要将变量编码到请求体中。PHP 将在$_POST变量中提供它们。

例如,下面是一个将POST作为 name 变量的值发送给网站的例子。这个请求将被用来添加一个叫做Ron的人到粉丝俱乐部。

POST  HTTP/1.1
Host: bieberfanclub.com
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache

name=Ronald

如果运行bieberfanclub.com的应用正在运行 PHP,那么$_POST数组将是一个数组,包含一个名为name的元素,其值为Ronald

POST发送变量有三个好处:

  • POST数据可以用特定的字符集编码,而GET则不是这种情况。
  • 因为变量是在消息体中发送的,所以可以发送的数据量不受 URL 长度的限制。
  • POST允许您上传文件,但GET不允许。

这两种方法在安全性上没有区别。

HTTP 协议中对 URL 的长度没有限制,但是对浏览器和其他客户端有限制。一般来说,不要创建超过 2000 个字符的 URL。

发送帖子数据

当您想向另一个应用发出一个POST请求时,您需要负责将变量编码到主体中。最简单的方法是使用curl扩展。 7 Curl 支持多种协议,可以轻松地按照您的需要设置您的请求。

使用curl包括以下过程:

  1. 初始化一个curl会话。
  2. 为会话设置选项。
  3. 执行会话(打电话)。
  4. 关闭会话并释放资源。

让我们看看如何使用curl来设置你之前看到的请求,其中你POST将包含值Ronald的变量name添加到比伯粉丝俱乐部。

<?php
// We specify the url when we initialize the curl resource
$curlResource = curl_init("https://requestb.in/13fkcqj1");
// This array contains the variables we want to POST
$postData = ['name' => 'Ron'];
// Tell curl to do a application/x-www-form-urlencoded POST
curl_setopt($curlResource, CURLOPT_POST, true);
// We specify the values to POST
curl_setopt($curlResource,CURLOPT_POSTFIELDS, $postData);
// Execute the request and store the response
$response = curl_exec($curlResource);
// If there is an error it will be stored in $err
$err = curl_error($curlResource);
// Close the handle
curl_close($curlResource);

如果您运行这段代码,您将能够在 https://requestb.in/13fkcqj1?inspect 看到结果。

Tip

可以传递一个你想要设置的所有选项的数组,而不是多次调用它。

得到

请求通常用于从服务器获取单个实体或一组实体。你可以把它想象成从服务器读取数据。

接收获取数据

GET请求中发送的变量被编码到 URL 中。以下是如何将变量编码到 URL 中的示例:

http://bieberfanclub.com/topfan.php?name=Ron&rank=cheerleader

变量以一个问号开始,并用和符号分隔。每个变量都是一个键值对,等号表示值。

PHP 会自动使 URL 中传递的变量在$_GET superglobal 中可用。

可以使用如下语法通过GET传递数组:

http://example.com/users.php?sort[col]=name&sort[order]=desc

您可以像这样访问这些变量:

<?php
echo $_GET['sort']['col'];
echo $_GET['sort']['order'];

发送获取数据

PHP 包含了一个函数,可以非常容易地构建 URL 字符串来传递您的GET数据。

<?php
$getData = ['fans' => ['Ron', 'Jonathan', 'Anne Frank']];
// fans%5B0%5D=Ron&fans%5B1%5D=Jonathan&fans%5B2%5D=Anne+Frank
echo http_build_query($getData);

函数将一个数组转换成一个正确的 URL 编码的查询字符串。

URL 的 HTTP 规范只允许使用非常有限的字符集。任何不在此集合中的字符都必须进行编码。 8 PHP 提供了urlencode()函数,它将正确地编码一个字符串以用作 URL 的一部分。urldecode()函数将把一个编码的字符串转换回它的原始表示。

<?php
$getData = ['fans' => ['Ron', 'Jonathan', 'Anne Frank']];
// fans%5B0%5D=Ron&fans%5B1%5D=Jonathan&fans%5B2%5D=Anne+Frank
$encodedString = http_build_query($getData);
// fans[0]=Ron&fans[1]=Jonathan&fans[2]=Anne Frank
echo urldecode($encodedString);

在这个例子中,我们正在解码由http_build_query()生成的正确 URL 编码的字符串,这样我们就可以看到一个数组是如何在一个参数中编码的。

请求用于替换整个实体或集合。通常,PUT请求会要求您指定一个实体的所有强制属性。这是一个写操作,因为它用您提供的状态替换了一个实体。

PATCH请求的相似之处在于它们都是用来替换数据的,但是PATCH请求只会替换你提供的实体的一部分。例如,如果一个用户有一个名字、姓氏和电子邮件字段,您将能够使用一个PATCH请求来更改其中一个字段,而保持其他字段不变。API 服务器通常不实现PATCH,而是要求你使用PUT

接收 PUT 数据

PHP 没有为PUT提供一个超级全局变量。要访问它,您需要读取php://input流。你可以使用parse_str()函数将它转换成一个数组:

<?php
$putVariables = [];
parse_str(file_get_contents("php://input"), $putVariables);

发送上传数据

PUT数据的传输和POST完全一样,所以curl是 PHP 中最简单的发送方式。

<?php
$data = ["fan" => "Ron"];
$curlResource = curl_init();
$options = [
    CURLOPT_URL => 'https://requestb.in/oxk2utox',
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_POSTFIELDS => $data
];
curl_setopt_array($curlResource, $options);
$response = curl_exec($curlResource);

在前面的例子中,我们告诉curl发出一个PUT请求,我们规定传递的值与我们对POST所做的完全一样。

注意,我们使用curl_setopt_array()函数来一次设置多个curl选项,而不是多次调用curl_setopt()

会议

HTTP 是一种无状态协议,这意味着一旦事务结束,客户机和服务器之间的连接就会丢失。此外,当 PHP 处理完一个请求并且它的应用状态丢失时,它就会终止。

会话是服务器为访问者的连续请求保存应用状态的一种方式。

诸如用户是否登录之类的信息可以存储在会话中。使用会话的另一个例子是在线购物网站,访问者购物车中的内容可以存储在会话中。

会话信息存储在服务器上,并与唯一标识符相关联。对于每个请求,客户端将向服务器发送会话标识符,这允许服务器将请求与特定会话相关联。

如果您有多个 web 服务器,那么您需要找到一种方法,要么在它们之间共享会话信息,要么确保访问者总是被定向到保存其会话信息的服务器。

不需要记住用户是谁或保留任何偏好的网站不需要使用会话。这种网站的一个例子是提供对所有访问者都一样的静态内容。

PHP 默认支持会话,但是可以通过php.ini中的配置设置禁用它们。

开始会话

PHP 中的一个会话在您调用函数session_start()时启动,或者如果您的php.ini配置指定了session.auto_start = 1则自动启动。

如果您正在使用session_start(),那么您必须确保在任何输出被发送到客户端之前调用这个函数。

当会话开始时,用户被分配一个随机的唯一会话标识符,称为session id。如果启用了session.use_trans_sid配置设置,会话 ID 或者存储在客户端的 cookie 中,或者通过 URL 传递。

接受来自 URL 的会话可能有风险,最好通过session.use_only_cookies设置将 PHP 配置为只使用 cookies。第六章关于安全有更多的信息。

会话标识符和会话变量

会话扩展使保存会话标识符的 SID 预定义常数可用。您也可以使用session_id()函数来获取或设置它。

您可以使用函数session_regenerate_id()为客户端创建一个新的会话标识符。您应该在调用session_start()之后立即调用它,以帮助防止会话固定。

一旦会话开始,超全局$_SESSION就可以作为包含会话变量的关联数组使用。

结束会话

要正确结束会话,您应该做三件事:

  1. $_SESSION数组设置为空数组。
  2. 将会话 cookie 过期时间设置为过去。
  3. 调用函数session_destroy()

步骤 2 的作用是让客户端浏览器知道它可以删除包含会话标识符的 cookie。但是,不能保证客户端会这样做。当然,如果您没有使用基于 cookie 的会话,那么就没有必要这样做。

会话处理程序

PHP 支持创建自己的会话处理程序,但默认情况下,PHP 会话存储在磁盘上,并使用serialize()unserialize()命令来编码和解码数据。

除了基于磁盘的会话,PHP 还附带了一个 memcached 会话处理程序,可以在php.ini中配置。

如果你想写自己的会话处理程序,你应该实现SessionHandler接口。这将允许您使用存储会话的替代方法,并自定义如何编码和解码会话数据。

文件上传

在这一节中,我们将重点讨论文件上传是如何工作的以及与之相关的 PHP 语法。请务必结合本节学习第六章中关于文件上传的章节。

表单允许通过“多方”HTTP POST事务上传文件。

您可以通过声明如下所示的表单来指定您想要在 HTML 中使用多部分表单数据对您的POST进行编码:

<form enctype="multipart/form-data" action="" method="post">

请注意,我将“action”属性留空。默认情况下,HTML 表单将提交给提供它的 URI。

限制上传的大小

你不希望人们上传大量的文件来填满你的磁盘。要管理人们可以上载的文件的大小,您可以在浏览器和服务器上限制文件的大小。

要告诉客户端限制上传的大小,您可以像这样在表单中添加一个输入:

<input type="hidden" name="MAX_FILE_SIZE" value=" 1000000" />

限制浏览器的大小只是为了改善用户体验。用户很容易禁用或更改限制来绕过限制。

您应该配置 PHP 来限制POST操作的大小。post_max_size设置限制了任何POST可以包含的最大数据量。upload_max_filesize用于限制可以上传的文件大小。

临时文件

PHP 将上传的文件存储在一个临时位置,并让表单POST接收到的脚本可以使用它。

您可以在临时位置处理文件,然后有选择地将其移动到永久位置。当脚本运行结束时,PHP 会自动删除临时文件,所以如果你想保留它,你必须移动它。

除了创建临时文件,PHP 还将填充$_FILES超全局数组。表单中上传的每个文件在数组中都有一个条目。

您需要意识到,$_FILES数组中的信息很容易被欺骗,因此您应该手动验证每一条信息。

每个文件由一个数组表示在$_FILES超全局中,并将键入名称、类型、大小、临时文件名和错误代码。

钥匙 描述
name 存储在客户端上的文件的原始名称
type 客户端提供的 MIME 类型
size 文件的字节大小
tmp_name 文件在其临时位置的名称
error 错误代码,如果上传成功,则为UPLOAD_ERR_OK

Note

第六章有更多关于处理文件上传的信息。

形式

表单允许用户向 PHP 脚本提交数据。

当用 HTML 声明一个表单时,需要指定它用来向服务器发送信息的方法。虽然您可以选择GETPOST,但是您应该确保您选择的请求方法与您想要做的相匹配。

PHP 自动使表单数据在两个超级全局变量中的一个中对您的脚本可用,这两个超级全局变量是$_GET$_POST,这取决于表单用来发出请求的方法。

表单元素

这些超全局变量很容易被客户端编辑,所以应该小心过滤,不要相信它们。

表单域名中的点和空格被转换为下划线。例如,考虑 HTML 输入标签:

<input name="email.address" type="text">

它包含的值将根据 forms 方法放在$_GET['email_address']$_POST['email_address']中。

HTML 表单中的数组

可以使用 HTML 中的语法将表单数据转换为数组:

<form action="formhandler.php" method="POST">
<input type="text" name="name[first]">
        <input type="text" name="name[last]">
        <input type="submit">
</form>

这将导致$_POST$_GET成为如下所示的数组:

array(
  'name' => array(
    'first' => '',
    'last' => ''
  )
)

数组帮助的最有用的方式之一是将输入分组在一起。

考虑一个可以有多个值的复选框:

<h1>What pets do you want in your home?</h1>
<form action="formhandler.php" method="POST">
<input type="checkbox" name="pets[]" value="cats" id="lotsacats">
<label for="lotsacats">Lots of Cats</label>
<input type="checkbox" name="pets[]" value="dog" id="adog">
<label for="adog">Just a dog</label>
<input type="submit">
</form>

如果该人在提交表单前勾选了两个框,那么$_GET$_POST数组将包含以下内容:

array(
  'pets' => array('cats', 'dog')
)

这使得复选框更加整洁和易于使用。你可以在 PHP 手册中了解更多。 9

从列表中选择多个项目

最后,如果您希望用户能够从一个select列表中选择多个项目,您将需要使用一个数组:

<select name="var[]" multiple="yes">

注意,select的名称是一个数组,所以用户选择的每个值都将被添加到超级全局数组中的"var"数组中。

饼干

Cookies 允许您在客户端设备上存储少量(4 到 6 KB)数据。客户端将读取它们,并在每次请求时发送它们。

您可以在 cookie 中存储任何类型的信息,但是它们通常与会话相关联。PHP 可以将其会话标识符存储在 cookie 中。会话信息存储在服务器上,并通过 cookie 中的标识符与客户端匹配。当您开始会话时,PHP 会为您做这件事。默认情况下,PHP 会话 cookies 在用户关闭浏览器之前一直有效。

您无法控制客户端设备上的 cookies。客户可以随时编辑或删除它们。这意味着你既不应该相信与它们一起发送的信息,也不应该依赖它们的存在。您也不应该将敏感信息存储在 cookies 中。

如果您想删除 cookie,您可以设置一个过去的到期日期。这将让客户端知道不再需要 cookie,可以将其删除。你不能保证客户会尊重这一点。

服务器将使用Set-Cookie响应头设置一个 cookie。客户端将使用Cookie请求头将其包含在未来的请求中。

设置 Cookies

setcookie()函数用于设置一个 cookie。PHP 手册中解释了这些参数,并按照下表的顺序给出:

参数 用于
value 在 cookie 中存储标量值。
expire cookie 过期时的 Unix 纪元时间戳。在 cookie 过期之前,你不能依赖它,因为人们删除他们的 cookie 是很常见的。
path cookie 将在其上可用的域的基本路径。如果设置为/,那么它在所有路径上都可用;否则,它将在该路径及其所有子路径上可用。
domain 该 cookie 将在此域及其下的所有子域中可用。您只能设置与提供 cookie 的域相匹配的 cookie。
secure 告诉客户端,如果 cookie 是通过 HTTPS 加密连接发送的,它应该只发送 cookie。
httponly 告诉客户端应该只使用 HTTP 发送 cookie,而不要让 JavaScript 等脚本语言使用它。在一定程度上,这有助于减少对支持 XSS 和会话固定的客户端的攻击。

Cookies 只能存储标量值。但是,您可以使用如下所示的语法:

<?php
setcookie("user[name]", "Alice");
setcookie("user[email]", "alice@example.com");

下一次用户向站点发出请求时,$_COOKIE变量将包含如下内容:

Array
 (
        [PHPSESSID] => jlm5od9ngqi3krmu6fkjcebcb4
        [user] => Array
     (
         [name] => Alice
         [email] => alice@example.com
     )

)

注意user是一个数组,cookie 值也包含 PHP 会话标识符。

正在检索 Cookies

您可以使用$_COOKIE超级全局变量来访问 cookie 信息。

记住,这个数组是用客户机发送的 cookie 中的信息填充的。这意味着,如果您使用setcookie()来创建或更改 cookie,那么当客户端发出新请求时,$_COOKIE数组将只包含新信息。

HTTP 头

HTTP 头随客户端的请求和服务器的响应一起发送。它们用于传递关于 HTTP 消息的信息,比如提供了什么类型的信息,以及作为回报将接受什么。

HTTP 头采用明文字符串中的名称-值对的形式。每个标题后面都有一个回车符和换行符。标准中没有限制,但是大多数服务器和客户机对一个请求/响应中可以发送的报头长度和报头总数有限制。

PHP 将自动为您发出有效的头,但是在一些情况下,您可能想要发送自己的头。

发送邮件头

PHP 函数header()让你发送一个头到客户端。您只能在任何正常内容发送到客户端之前发送邮件头。在包含的 PHP 文件中省略结束标签?>的原因之一是为了避免在标签后出现换行符。该字符将作为 HTML 内容发送,并阻止您发送标题。

发送到header()的参数如下:

参数 描述
标题字符串 包含要设置的标头的字符串。比如"Cache-Control: no-cache, must-revalidate"
替换 布尔值,指示此标头是否必须替换以前发送的同名标头。
响应代码 与标头一起发送的 HTTP 响应代码。

头有两种特殊情况。第一种是以字符串"HTTP/"开头的头。这些可以用来显式设置 HTTP 响应代码,如 PHP 手册 10 中的这个例子:

<?php
header("HTTP/1.0 404 Not Found");

第二种特殊情况是使用"Location"标题。这个头向客户端表明他们正在寻找的文档在您指定的位置。

Note

如果您使用这个头,PHP 将自动设置一个 302 HTTP 状态码,除非您已经设置了一个 2xx 或 3xx 头。

这里有一个例子:

<?php
header("Location: http://www.example.com/");
exit;

在此示例中,服务器将使用状态代码 302 进行响应,客户端将被重定向到示例域。

注意发送重定向头后出口语言结构的用法。您的代码在发送标头后会继续运行,除非您停止它。这取决于客户端是否尊重您的重定向头。如果他们决定不尊重它,您的代码将继续输出,他们将看到它生成的任何输出。确保在发送重定向头后显式终止程序。

跟踪标题

headers_list()函数将返回一个准备发送或已经发送到客户端的头数组。您可以通过调用headers_sent()来确定报头是否已经发送。

如果您想阻止发送特定的标题,您可以使用headers_remove()功能从要发送的列表中取消设置标题。

HTTP 认证

PHP 可以向客户端发送一个头,让它弹出一个需要认证的对话框。当用户在对话框中输入用户名和密码时,PHP 脚本的 URL 会被再次调用。

在第二次调用时,PHP 将在$_SERVER数组中有三个预定义的变量。它们是PHP_AUTH_USERPHP_AUTH_PWAUTH_TYPE,分别被设置为用户名、密码和认证类型。

然后,您应该使用您认为合适的任何方法对用户进行身份验证,比如对照数据库检查用户和密码。

PHP 手册页 11 中给出了 HTTP 认证的例子:

<?php
 if (!isset($_SERVER['PHP_AUTH_USER'])) {
     header('WWW-Authenticate: Basic realm="My Realm"');
     header('HTTP/1.0 401 Unauthorized');
     echo 'Text to send if user hits Cancel button';
     exit;
 } else {
     echo "<p>Hello {$_SERVER['PHP_AUTH_USER']}.</p>";
     echo "<p>You entered {$_SERVER['PHP_AUTH_PW']} as your password.</p>";
}

在这个例子中,我们只是输出了$_SERVER数组中变量的内容,但是在现实生活中,我们会执行某种形式的认证。

客户端发送的密码是 base64 编码的,以标准化字符集,但没有执行哈希或加密。这是一种非常脆弱的保护网站的方式,除非你使用 HTTPS,否则在你的客户端和服务器之间的任何人都可以读取密码。

HTTP 状态代码

HTTP 状态代码与响应一起发送,并遵循互联网工程任务组制定的标准以及行业内使用的实际标准。

HTTP 状态代码被分成 100 个代码的范围。该范围内的所有状态代码将具有类似的含义,如下表所示。

范围 一般含义
200 请求成功
300 请求需要被重定向
400 客户端有一个错误
500 服务器端有一个错误

你不需要为你的考试记住所有不同的状态代码。当你在现实生活中编写 API 时,你将能够访问维基百科 12 并为你的响应选择正确的代码。

对于你的考试,你只需要知道最重要的几个:

密码 状态
200 OK;请求成功。
201 已创建;该请求导致创建新资源。
301 永久移动;资源将总是在指定的位置找到。
400 错误的请求;请求中的某些内容格式不正确或妨碍了它的执行。
401 未经授权;客户端没有被授权进行此请求。
403 禁止;(经过身份验证的)客户端不允许发出此请求。
418 我是茶壶;客户端试图向服务器发送咖啡制作协议,服务器实际上是一个茶壶。好吧,这不是一个重要的身份代码,但了解它很有趣。
500 内部服务器错误;服务器无法完成请求,无法做出更恰当的响应。通常与崩溃或错误配置相关联。

使用 API 时,HTTP 状态代码非常重要。如果你正在编写一个 API,你应该确保你为错误发送了正确的状态码。

例如,如果请求失败,您在正文中发送了一条错误消息,您应该确保 HTTP 状态代码是 400 而不是 200。

当您使用 PHP 时,您会对状态代码更加熟悉,但是如果有疑问,您应该查找一个列表,并确保您发送的是一个适当的响应。

输出控制功能

不是立即从脚本中输出,而是将输出存储在一个缓冲区中,然后立即刷新整个缓冲区,这通常非常有用。这对于在将输出发送给用户之前对输出进行转义,或者使用 PHP 内置的压缩例程来压缩发送给兼容浏览器的响应非常有用。

ob_start()功能用于启动输出缓冲。您的脚本通常会在响应正文中输出的任何内容都将被存储到缓冲区中。该函数采用一个可选参数,该参数是可调用的,当输出或丢弃缓冲区时将调用该参数。

下面是一个设置回调函数的例子。我将脚本的输出作为注释包含在内。

<?php
function escapeOutput(string $buffer): string {
    return htmlentities($buffer);
}

ob_start("escapeOutput");

// &lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;
echo '<script>alert("xss");</script>';

在本例中,当脚本结束时,缓冲区被隐式刷新。您可以使用ob_flush()函数 14 显式刷新缓冲区并输出其内容。当您正在编写 CLI 脚本并希望能够看到进度时,这可能是有用的,而另一个(可能是不好的)用例可能是为长轮询 JavaScript 客户机编写服务器。

<?php
ob_start();
// this is a cli script
for ($i=1; $i<5; $i++) {
    echo $i;
    // each character is output one by one
    ob_flush();
    sleep(1);
}

ob_flush函数将输出缓冲区,并允许进一步缓冲输出。将其与ob_end_flush, 15 进行比较,后者将输出缓冲并禁用任何进一步的缓冲。

Note

如果您的 web 服务器正在缓冲输出,并且您想尝试覆盖这种行为并直接向浏览器发送内容,那么可以使用flush()函数。然而,这并不总是有效的。

PHP 有一个内置的方法来帮助你在通过网络发送数据之前压缩数据。如果您启用它,那么 PHP 将检测浏览器是否能够支持压缩数据,如果是,使用 GZIP 算法来减少响应的大小。要设置它,您需要指定压缩函数作为对ob_start()的回调。更多例子请看 PHP 手册 16 ,这里有一个简单的例子:

<?php
ob_start("ob_gzhandler");
?>
{"string": "my json api output is compressed now"}

Chapter 9 Quiz

Q1:假设variables_order被设置为 PHP 的默认值。对于下面的 HTTP 请求,$_REQUEST['biggestfan']的值是多少?

| Ron |
| Ronald |
| 别的东西 |
| 以上都不是 |

POST  HTTP/1.1
Host: thebeebfanclub.com?biggestfan=Ron
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache

biggestfan=Ronald

Q2:这些输入的每一个都有一个合适的构造形式,适合你的网站。将输入框中输入的名字放入一个变量的正确方法是什么,你可以像这样访问这个变量$_POST['justin]['numberonefan']

| <input type="text" name="justin.numberonefan"> |
| <input type="text" name="justin(numberonefan)"> |
| <input type="text" name="justin[numberonefan]"> |
| <input type="text" name="justin_numberonefan"> |
| 以上都不是 |

Q3:cookie 是一种存储信息的可靠方式,不会浪费服务器资源。选择最正确的选项。

| 是的,在客户端存储信息可以节省服务器磁盘空间 |
| 不,信息的副本仍保留在会话数据中 |
| 不,他们不可靠 |
| 是的,将所有会话数据存储在 cookies 中是很常见的 |

Q4:这段代码会输出什么?选择所有适用的选项

| 一个通知,因为$ a 未定义 |
| 警告,因为$a未定义 |
| 警告,因为您无法启动会话 |
| 包含session_id的随机字符串 |

<?php
echo $a;
session_start();
echo session_id();

q5:cookie 的domain属性用来做以下哪一项?

| 限制 cookie 对网站的哪一部分有效 |
| 指定您的网站的名称 |
| 阻止浏览器将此 cookie 发送到其他网站 |
| 以上都不是 |

Footnotes 1

https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods

2

https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9

3

https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT

4

[en.wikipedia.org/wiki/Context_(computing)](https://en.wikipedia.org/wiki/Context_(computing)

5

https://php.net/manual/en/reserved.variables.environment.php

6

https://php.net/manual/en/ini.core.php#ini.variables-order

7

https://php.net/manual/en/book.curl.php

8

https://en.wikipedia.org/wiki/Percent-encoding

9

https://secure.php.net/manual/en/faq.html.php#faq.html.arrays

10

https://php.net/manual/en/function.header.php

11

https://php.net/manual/en/features.http-auth.php

12

https://en.wikipedia.org/wiki/List_of_HTTP_status_codes

13

https://en.wikipedia.org/wiki/Hyper_Text_Coffee_Pot_Control_Protocol

14

https://php.net/manual/en/function.ob-flush.php

15

https://php.net/manual/en/function.ob-end-flush.php

16

https://secure.php.net/manual/en/function.ob-gzhandler.php

十、数据库和 SQL

数据库是一种保存数据的工具,这些数据是您打算经常引用并需要长期保存的。

PHP 使用扩展与一系列数据库进行交互。例如,要与 MySQL 数据库交互,可以使用mysqli扩展提供的函数。

Note

mysqli结尾有个“我”。这是现已废弃的mysql扩展的替代品。像“预准备语句”这样的特性只在新的扩展中可用。

PHP 还提供了抽象层,在代码和数据库之间提供了一个应用层。我们将在本书中探讨 PDO (PHP 数据对象)。

在本书中,我们将重点关注关系数据库,但在传递关系数据库的替代方案时,这是值得一提的。MongoDB 是一个非常受欢迎的 NoSQL 数据库,它提供了一个 PHP 驱动程序,允许你连接到数据库。我们将关注原生关系数据库,MongoDB 驱动程序不太可能包含在您的 Zend 考试中。

Zend 考试要求你了解基本的 SQL。如果问题没有特别说明,假设的环境将是 MySQL。

数据库基础

让我们首先确定关系数据库的一些概念是清楚的。

键施加约束,比如PRIMARYUNIQUE。主键可以在一列或多列上定义。它保证数据库中的每一行对于键中的列都有唯一的值组合。行的主键不能为空值。

一个表只能有一个主键。外键也可以在一列或多列上定义。它引用了另一个表上的主键。这是一个唯一的引用,因此被引用表中只有一行链接到包含外键的表。

图 10-1 用外键category_id表示产品所属的类别。

A456636_1_En_10_Fig1_HTML.jpg

图 10-1。

Both tables have primary keys named id

指数

索引是实现键约束所需的数据结构。索引加快了检索记录的速度。数据库引擎将在磁盘或内存中创建一个结构,其中包含索引列中的数据。这种结构针对查找进行了优化,有助于数据库更快地找到表中的行。

每当向表中插入一行时,都需要更新索引。这增加了编写的开销。

没有索引就不能有键,但是可以索引没有键的列。如果您不想强制实现惟一性,但又想加速在WHERE子句中包含这些列的SELECT语句,那么您可以这样做。

键和索引之间的绑定非常紧密,在 MySQL 中它们被认为是同义词。

关系

关系是关系数据库的核心特征。通过声明表之间的关系,您可以实施参照完整性并最大限度地减少脏数据。

有几种类型的关系。

关系 描述
一对一 父表中的一行只能引用子表中的一行。
一对多 父表中的一行可以被子表中的多行引用。
多对多 父表中任意数量的行可以被子表中任意数量的行引用。

通过表之间的关系,您可以将逻辑上相关的数据存储在一个表中,与其他数据区分开来。

例如,您可以有一个products表来存储您销售的产品的信息。产品属于类别。一个类别可以包含许多不同的产品。这意味着 category 表中的一行可以被 products 表中的多行引用。

SQL 数据类型

SQL 数据库表中的列具有分配给它们的数据类型。正如 PHP 变量类型一样,SQL 类型可以存储不同格式的数据。

每个数据库管理器实现的 SQL 数据类型略有不同,并且在不同类型之间有不同的优化。

我们讨论一些常见的数据类型,避免关注任何特定的 SQL 实现。

数字类型

整数的类型在存储它们的值所用的字节数上有所不同。

下表说明了 MySQL 数据库的整数大小: 1

整数类型 字节 (带符号的)值 (无符号)值
BIGINT eight -9223372036854775808 转+ 9223372036854775807 0 到 18446744073709551615
INTEGER four -2147483648 转+2147483647 0 到 4294967295
MEDIUMINT three -8388608 至+8388607 0 到 16777215
SMALLINT Two -32768 至+32767 0 到 65535
TINYINT one -128 至+127 0 到 255

Tip

MySQL 允许你为整数指定一个参数,这个参数实际上是一个显示值,不影响底层存储。这是一个相当普遍的误解,认为参数是为了精度。

非整数类型可以存储在NUMERICDECIMAL值中。SQL-92 标准规定,NUMERIC类型必须具有规定的精确精度,而DECIMAL类型必须至少同样精确。这些数据类型的实现因供应商而异。

它们都采用相同的参数:

NUMERIC(21,3)

第一个参数指定精度的总位数,第二个参数指定必须存储多少位十进制精度。

在本例中,我们将存储一个总共有 21 位的数字,其中 3 位出现在小数点后。

字符类型

SQL 允许将字符存储在固定长度或可变长度的字符串中。

固定长度的字符串在磁盘上总是被分配相同数量的字节。在某些数据库实现中,这有助于提高读取性能。权衡的结果是,如果存储在固定长度数据存储中的字符串比分配的字符数短,则存储的字符会比实际需要的多。

可变长度字符串可以膨胀到给定的限制大小。数据库引擎根据字符串的长度分配存储。数据库实现将存储被存储的字符串的长度。这将是至少一个字符来表示字符串的结尾,但是在一些引擎中,每个可变字符串将导致更大的存储开销。

一般来说,当存储一个字符串时,你知道它总是有一个特定的长度,比如一个散列,你应该把它存储在一个固定长度的字符字段中。这将提高性能,并且不会导致存储浪费。

使用 SQL

我们不会关注 SQL 的任何具体实现,而是尝试使用通用语句。Zend 考试不会测试你对特定数据库引擎的了解,但会期望你了解基本的 SQL 语法。

创建数据库和表

CREATE语句可以用来创建数据库和表格。创建数据库很简单;您只需指定数据库的名称:

CREATE DATABASE mydatabase;

创建表时,可以指定要存储在其中的列的列表。对于每一列,可以指定名称、数据类型和属性。

CREATE TABLE IF NOT EXISTS users (
  id int unsigned NOT NULL AUTO_INCREMENT,
  name varchar(255) NOT NULL,
  email varchar(255) NOT NULL,
  password varchar(60) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY users_email_unique (email)
);

删除数据库和表

CREATE的逆运算是DROP语句。

DROP TABLE category;
DROP DATABASE mydatabase;

如果您指定了外键,数据库将不会允许您删除违反其中一个约束的表。

例如,参考我们的产品和类别示例。如果我们尝试删除 category 表,但仍有产品引用它,数据库引擎应该不允许该操作。

检索数据

SELECT语句用于检索数据。SELECT的语法可能非常复杂,是不同供应商之间差异最大的语句之一。你需要了解基本的用法和加入你的 Zend 认证。

在这个简单的伪代码查询示例中,我们从表中检索价格超过 100 个货币单位的产品名称。我们指定希望结果按价格降序返回。

SELECT name
  FROM products
  WHERE price > 100
  ORDER BY price DESC

您可以指定用逗号分隔的多个列名,或者使用通配符*来接收所有列。

PHP 接收的数据格式取决于您用来调用查询的驱动程序和函数。您通常会收到一个对象或数组,其中包含与列相对应的键/属性。

插入新数据

INSERT语句用于在数据库中创建新行。您需要提供一个列列表以及要插入其中的值。标记为NOT NULL的列是必需的,在创建行时必须指定一个值。

INSERT INTO products
  (name, price, category_id) VALUES
  ('cheeseburger', 100, 3)

如果不指定列名,SQL 将假定您按照列在表中出现的顺序提供值。如果你改变了你的表的结构,这可能是一个缺点。

否则,如示例所示,先指定列名,然后指定值。这些值按顺序分配给列。因此,在这个例子中,产品的名称被设置为'cheeseburger',它的价格是 100,它被放入 ID 值为 3 的类别中(无论它是什么)。

更新数据

UPDATE语句接受类似于INSERT语句的值列表,以及类似于SELECT语句的可选WHERE子句。

您必须指定现有数据要更新到的值,以及必须更新的行的标准。

UPDATE products
  SET price = price + 100
  WHERE category_id = 3;

汇总数据

您可以使用数据库执行计算并将结果发送给您。

声明 返回
AVG 数据值的平均值
SUM 找到的所有数据值的总和
COUNT 找到了多少条记录
DISTINCT COUNT 找到了多少条唯一记录
MIN 数据集中的最小值
MAX 数据集中的最高值

使用这些函数如下所示:

SELECT AVG(price) FROM products;

分组数据

您可以告诉 SQL 在将数据返回给您之前,按列或列的组合对数据进行分组。这通常与聚合函数一起使用很有用。

让我们考虑一个例子,我们想找出我们每个客户购买的总销售额。

SELECT email, SUM( sales_value )
        FROM  `transactions`
        GROUP BY email

在本例中,我们对具有相同电子邮件地址的事务进行分组。SQL 数据库引擎将应用SUM语句,将每个组中的销售值相加,然后返回该值。

我在SELECT语句中包含了电子邮件地址,因此输出将包含客户的电子邮件地址,以及包含其电子邮件地址的所有交易的销售额总和。

连接

联接用于根据提供的标准连接表。这允许您从相关表中检索信息。

在产品和类别数据库中,您可以通过将categories表连接到products表来检索产品的类别名称:

SELECT *
FROM  products
JOIN categories ON categories.id = products.category_id

我们将categories表连接到products表,并向 SQL 发出如何匹配行的指令。如果categories表中的id列与products表中的category_id列相匹配,那么将包含该表中的一行。

连接类型

有几种连接表的方法。

连接类型 影响
INNER JOIN 选择在两个表中都有匹配值的记录,如上例所示
LEFT OUTER JOIN 从左表中选择具有匹配右表记录的表
RIGHT OUTER JOIN 从右表中选择与左表记录匹配的记录
FULL OUTER JOIN 选择与左表或右表记录匹配的所有记录

这些连接可以用图解法表示,如图 10-2 所示。

A456636_1_En_10_Fig2_HTML.jpg

图 10-2。

Many ways to join tables

准备好的声明

当您向 SQL 引擎发出命令时,它必须解析该命令才能执行它。执行完该语句后,SQL 将丢弃编译后的代码,结果是需要单独解析使用相同 SQL 命令的重复调用。显然,这导致了重复劳动。

您可以通过使用准备好的语句来避免 SQL 重复工作,这些语句将成为 SQL 存储的已解析代码模板,以供多次重用。

预准备语句还提供了显著的安全优势。参数被绑定到准备好的语句,并且不作为代码字符串的一部分。这意味着您的参数不可能侵入代码,这意味着您不再需要担心转义代码来防止 SQL 注入。在您不再担心数据进出数据库之前,请记住存储的 XSS 攻击的可能性。

<?php
// prepare and bind
$stmt = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $password);

// set parameters
$username = "bob";
$password = password_hash("password", PASSWORD_BCRYPT);

// run the statement
$stmt->execute();

处理

事务是一组 SQL 语句,要么全部成功,要么无效。

事务完成后,数据库不能使任何表约束失效,并且必须处于所有更改都已持久化的状态。数据库必须有某种方法来确保事务可以同时运行而不会相互干扰,例如,通过增加另一个事务所依赖的主键。

总之,事务是一组 SQL 语句,必须以“全有或全无”的方式成功完成。运行后,数据库必须处于一致状态,并且必须能够从错误中恢复。

交易的语法因供应商而异,但是有三个重要的陈述。

  • 一条语句将标记事务块的开始。其后的 SQL 语句将是事务的一部分。
  • 有两个语句可以结束事务。其中一个将告诉 SQL 继续进行事务正在进行的所有更改。
  • 另一个 end 语句将告诉 SQL,无论出于什么原因,您都希望放弃事务,而是恢复到事务开始时数据库所处的状态。

以下是三种最常见的供应商陈述:

关系型数据库 数据库备份方法 神谕
START TRANSACTION BEGIN TRANSACTION START TRANSACTION
COMMIT COMMIT TRANSACTION COMMIT
ROLLBACK ROLLBACK WORK ROLLBACK

PHP 数据对象(PDO)

PDO 是一个数据抽象层,它为您提供了一个与多个数据源交互的接口。使用 PDO 时,无论供应商是谁,您都可以使用相同的函数与数据库进行交互。

理解 PDO 是一个访问抽象层并且不抽象 SQL 或数据类型是很重要的。您传递给PDO::query()或准备好的语句的 SQL 必须对您正在连接的供应商有效。

PDO 使用数据库适配器连接到数据库。这些适配器类实现了 PDO 接口,并将特定于供应商的函数作为常规扩展函数公开。

PDO 是在 PHP 配置文件中配置的。运行时,您可以使用PDO::setAttribute()功能更改选项。

PDO 扩展提供了几个预定义的常量。对于 Zend 考试,您不需要记住所有的内容,但是可以浏览 PHP 手册并熟悉它们。

PDO 将模拟不支持预准备语句的数据库的预准备语句,但是将使用数据库的本机预准备语句功能。

连接到 PDO

要用 PDO 连接到数据库,需要创建一个 PDO 类的实例。如果需要,构造函数接受数据库源(DSN)和用户名/密码的参数。

<?php
try {
    $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
} catch (PDOException $e) {
    echo "Error connecting to database: " . $e->getMessage();
}

如果连接到数据库时出现错误,将会抛出一个PDOException。注意异常的堆栈跟踪可能包含完整的数据库连接细节是非常重要的。一定要抓住它,不要让它被展示出来。

要在完成后关闭连接,可以将 PDO 变量设置为null

$dbh = null;

数据库连接会在运行脚本结束时自动关闭,除非您将它们持久化。持久数据库连接不会被关闭,而是被缓存以供脚本的另一个实例使用。这减少了每次 web 应用运行时需要连接到数据库的开销。

PDO 的交易

PDO 也提供了事务命令,但是没有模拟正确的事务处理。这意味着您只能在本地支持事务的数据库上使用 PDO 事务函数。这些功能是PDO::beginTransaction()PDO::commit()PDO::rollBack()

<?php
$dsn = 'mysql:host=localhost;dbname=example';
$pdo = new PDO($dsn, 'dbuser', 'dbpass');
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, TRUE);
$pdo->setAttribute(PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION);
$password = password_hash("password", PASSWORD_BCRYPT);
try {
    $pdo->beginTransaction();
    $pdo->exec("
          INSERT INTO users
              (username, password)
          VALUES
              ('bob', '{$password}'");
    // some more update or insert statements
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack();
    echo 'Rolled back because: ' . $e->getMessage();
}

在这个例子中,我们连接到数据库with PDO,并启动一个事务。

我们将所有的 PDO 事务功能包装在一个try...catch块中。如果 PDO 语句运行失败,它将抛出一个PDOException。我们使用catch块回滚事务。

获取 PDO 结果

我们使用PDO::fetch()方法 2 从 PDO 结果中检索数据。PDO 将保持一个光标来遍历结果集,并使用它来确定返回给你的元素。

PDO 将以您在第一个参数fetch()中指定的格式将数据返回给您。

提取样式 返回
PDO::FETCH_ASSOC 返回一个以数据库列为关键字的关联数组。
PDO::FETCH_NUM 返回由结果集返回的按列号索引的数组。
PDO::FETCH_BOTH 返回一个数组,该数组同时包含ASSOCNUM样式提取的索引。
PDO::FETCH_BOUND 返回 true 并将结果集中列的值赋给 PHP 变量,这些变量是用PDOStatement::bindColumn()方法绑定的。
PDO::FETCH_CLASS 返回所请求类的新实例,该实例将结果集的列映射到该类中的命名属性。
PDO::FETCH_INTO 更新所请求类的现有实例,映射为FETCH_CLASS
PDO::FETCH_OBJ 返回一个匿名对象,其属性名对应于结果集中的列名。
PDO::FETCH_LAZY 组合PDO::FETCH_BOTHPDO::FETCH_OBJ并在访问时创建对象变量名。
PDO::FETCH_NAMED 至于PDO::FETCH_ASSOC,返回一个关联数组。如果有多个同名列,则该键引用的值将是具有该列名的行中所有值的数组。

在 PDO 准备发言稿

并非所有数据库引擎都支持预处理语句,这是 PDO 为不支持预处理语句的适配器模拟的唯一特性。

在 PDO 中,预处理语句的语法与使用本地函数非常相似。

<?php
$stmt = $dbh->prepare("INSERT INTO users (name, email) VALUES (:name, :value)");
$stmt->bindParam(':name', $name);
$stmt->bindValue(':email',’alice@example.com’);

// insert one row
$name = 'one';
$stmt->execute();

浏览这个例子,我们看到使用了prepare()方法来创建语句对象。

我们使用两种不同形式的绑定参数来演示这种差异。

在第一个例子bindParam()中,我们将一个变量绑定到语句参数。当语句执行时,参数将采用执行时变量的值。

绑定变量的第二种方法是bindValue(),将一个文字绑定到语句参数。如果您在bindValue()中使用了变量名,那么将使用绑定时的变量值。在语句执行之前对变量的更改不会影响参数值。

您也可以在调用 execute 时将要绑定的值作为数组传递,如下例所示:

<?php
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status=:status');
$stmt->execute(['email' => $email, 'status' => $status]);
$user = $stmt->fetch();

在 SQL 语句中只能绑定值,而不能绑定像表名或列这样的实体。您只能绑定标量值,而不能绑定像数组或对象这样的复合变量。

多次致电 PDO 准备陈述

您已经看到,bindParam()方法在语句执行时将变量值插入到语句参数中。您可以看到,使用bindParam()允许您重复调用准备好的语句,在每次调用中使用不同的参数值。

方法closeCursor()用于清除数据库光标,并将语句返回到可以再次执行的状态。当先前执行的语句仍有未预取的行时,一些数据库在执行准备好的语句时会出现问题。

Chapter 10 Quiz

Q1:你可以使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ 函数从数组中构建一个适合用于GETPOST的 HTTP 查询字符串。

| http_build_query() |
| http_build_param() |
| parse_url() |
| urlencode() |

Q2:PHP 函数encodeurl()用于:

| 确保 URL 是 UTF 8 编码的。 |
| 将 URL 中的保留字符转换为%编码符号。 |
| 从数组中构建一个适合于GET参数的字符串。 |
| 没有这个 PHP 函数。 |

Q3:使用准备好的语句有什么好处;选择尽可能多的适用选项?

| 它们比使用普通查询更安全。 |
| 对于重复查询,它们更快。 |
| 您可以在不同的数据库供应商中使用相同的查询。 |
| 以上都不是。 |

问题 4:我将客户信息存储在一个表中。每一行都有一个account_id,它是一个名为accounts的表的外键。对于 ID 为123的客户,如何从地址表中选择postcode列?

| 从“帐户”中选择“邮政编码”,其中 customer _ id = 123 |
| 从“accounts”中选择“postcode”作为“acc ”,在“cust . id ”=“ACC . id”上加入“customers”作为“cust ”,其中“c ”. id = 123 |
| 在' c . account _ id = a . id '上,从' accounts '中选择'邮政编码'作为' a ',加入' customers '作为' c ',其中' c.id = 123 |
| 在“c . id ”=“a . id”上,从“accounts”中选择“postcode”作为“a”完全外部连接“customers”作为“c ”,其中“c . id”= 123 |

Footnotes 1

https://dev.mysql.com/doc/refman/5.7/en/integer-types.html

2

https://secure.php.net/manual/en/pdostatement.fetch.php

十一、错误处理

错误处理是 PHP 5.6 和 PHP 7.1 之间一些最大变化的原因,尽管我们在本书的其他相关地方已经提到了这个重要的话题,但是单独讨论它还是有意义的。

可投掷物品

在本章中,我们将会看到ErrorException类。现在,你需要知道的是它们都实现了 PHP 7 中引入的Throwable接口。

Tip

PHP 定义一个新的不扩展Exception类的Error类的原因是为了保持与 PHP5.6 代码的向后兼容性。

可抛出的界面

异常和错误异常都实现了Throwable接口,所以您可以在一个块中捕获这两种类型,如下所示:

<?php
try {
    // ...code

} catch (Throwable $e) {
    echo "A class that inherits from either Exception or ErrorException was caught";
}

一会儿我们将看到 PHP 用来比较ErrorExceptionsExceptionscatch块的匹配规则。

你可以在 PHP 手册的Throwable接口中找到定义的方法, 1 但是为了你的方便,这里把它们列出来:

Throwable {
/* Methods */
abstract public string getMessage ( void )
abstract public int getCode ( void )
abstract public string getFile ( void )
abstract public int getLine ( void )
abstract public array getTrace ( void )
abstract public string getTraceAsString ( void )
abstract public Throwable getPrevious ( void )
abstract public string __toString ( void )
}

Tip

这些方法对于记录错误非常有用。

错误

在 PHP 的旧版本中,错误和异常的处理方式非常不同。错误是引擎中产生的东西,只要它不是致命的,就可以由用户定义的函数来处理。

问题是有几个错误是致命的,无法由用户定义的错误处理程序来处理。

这意味着你不能优雅地处理 PHP5.6 中的致命错误。有几个副作用是有问题的——比如运行时上下文的丢失,析构函数不会被调用,以及处理它们很笨拙。

在 PHP 7 中,致命错误现在是异常,并且更容易处理。

Note

只有致命错误才会引发错误异常。您需要用错误处理函数来处理非致命错误。

下面是一个在 PHP 7.1 中捕捉致命错误的例子。请注意非致命错误是如何被捕获的。

<?php
try {
    // generates a notice error (not caught)
    echo $thisVariableIsNotSet;
    // this would be a fatal error (is caught)
    badFunction();
} catch (Error $e) {
    echo "Error caught: " . $e->getMessage();
}

如果试图访问无效变量,该脚本将输出一个错误通知。在 PHP 的早期版本中,试图调用一个不存在的函数会导致致命错误,但是在 PHP 7.1 中,您可以发现它。

以下是该脚本的输出:

Notice: Undefined variable: thisVariableIsNotSet in /in/lQC3F on line 5
Error caught: Call to undefined function badFunction()

误差常数

PHP 有很多与错误相关的常量 2 。这些常量在配置 PHP 来隐藏或显示某些类的错误时使用。

以下是一些更常见的错误代码:

密码 描述 脚本 抛出错误?
E_DEPRECATED 如果您使用不推荐使用的语言功能,解释器将生成这种类型的警告。 继续运行
E_STRICT E_DEPRECATED类似,这表明您正在使用一种当前不标准的语言特性,将来可能无法工作。 继续运行
E_PARSE 无法解析您的语法,因此您的脚本无法启动。 根本不会跑
E_NOTICE 信息性消息。 继续运行
E_WARNING 这些是非致命警告。 继续运行
E_ERROR 脚本无法继续运行,正在被终止。 中止,除非您用错误处理程序来处理它
E_RECOVERABLE_ERROR 这个错误可能足够危险,足以致命,但引擎并没有处于无法继续运行的状态。 中止,除非您用错误处理程序来处理它

使用错误处理函数

set_error_handler()函数 3 用于告诉 PHP 如何处理不是Error异常类实例的标准引擎错误。对于致命错误,不能使用错误处理函数;你必须抓住他们作为Error的例外。

set_error_handler()接受一个可调用的 4 作为其参数。PHP 中的可调用函数有两种指定方式:一种是表示函数名的字符串,另一种是传递包含对象和方法名的数组(按此顺序)。

Note

您可以将对象中的受保护方法和私有方法指定为可调用的。

还可以传递 null 来告诉 PHP 使用标准的错误处理机制。

如果您的错误处理程序没有终止程序并返回,那么您的脚本将在错误发生后的行继续执行。

PHP 将参数传递给错误处理函数。如果想在函数中使用它们,可以选择在函数签名中声明它们。

<?php

function myHandler(int $errNo, string $errMsg, string $file, int $line) {
    echo "Error #[$errNo] occurred in [$file] at line [$line]: [$errMsg]";
}

set_error_handler('myHandler');

try {
  // This does not throw an Error
  5 / 0;
} catch ( Throwable $e ) {
  echo 'Caught error : ' . $e->getMessage();
}

/*
  Error #[2] occurred in [/in/Xa0Td] at line [11]: [Division by zero]
*/

在前面的例子中,我们将数字五除以零。在 PHP 中,这会导致警告,所以不会抛出Error。然而,我们已经将函数myHandler()设置为客户错误处理程序,因此当 PHP 遇到警告时会调用它。

用户错误处理程序无法捕获导致脚本终止的错误;这些包括E_ERRORE_PARSEE_CORE_ERRORE_CORE_WARNINGE_COMPILE_ERRORE_COMPILE_WARNING

显示或抑制非致命错误消息

通常,您希望在生产过程中隐藏所有系统错误消息,并且您的代码应该在不生成警告或消息的情况下运行。如果您要显示一条错误消息,请确保它是您自己生成的,并且不包含可能帮助攻击者侵入您的系统的信息。

在您的开发环境中,您希望显示所有错误,以便您可以修复与它们相关的所有问题,但是在生产环境中,您希望禁止向用户发送任何系统消息。

为此,您需要在您的php.ini文件中使用以下设置来配置 PHP:

  • display_errors可设置为 false 以抑制消息
  • log_errors可用于在日志文件中存储错误信息
  • 可以设置error_reporting来配置哪些错误触发报告

最佳实践是优雅地处理应用中的错误。在生产中,您应该记录未处理的错误,而不是允许它们显示给用户。

Note

我们在第一章中看到了错误抑制运算符@。记住最好避免使用它。

error_log()函数 5 可用于向系统错误处理程序之一发送消息。您不应该将它与error_log配置选项混淆。配置选项指定如何处理日志,而函数用于发送消息。

你也可以使用error_log()函数来发送电子邮件,但就我个人而言,我不会这样做,我宁愿用代码或使用类似滚动条的服务来实现。 6

错误处理功能

PHP 有很多与错误处理相关的函数78。下表提供了它们的摘要。在这一章中,我们将会看到其中的大部分。

功能 目的
debug_backtrace 生成回溯。
调试 _ 打印 _ 回溯 打印回溯。使用该函数时要小心,因为它会产生大量输出!
error_clear_last 清除最近的错误。
error_get_last 获取最后发生的错误。
error_log 向定义的错误处理例程发送错误消息。
error_reporting 设置报告哪些 PHP 错误。
还原错误处理程序 恢复以前的错误处理函数。
还原异常处理程序 恢复先前定义的异常处理函数。
设置错误处理程序 设置用户定义的错误处理函数。
设置异常处理程序 设置用户定义的异常处理函数。
触发器 _ 错误 生成用户级错误/警告/通知消息。
用户 _ 错误 trigger_error的别名。

例外

异常是面向对象编程的核心部分,最初是在 PHP 5.0 中引入的。异常是一个程序状态,它需要特殊的处理,因为它没有以预期的方式运行。

你可以使用一个异常来改变程序的流程,例如,如果不满足某些前提条件,就停止做一些事情。

如果您没有捕捉到异常,它就会在调用堆栈中冒泡。让我们看一个例子:

<?php
function A() {
  // The exception thrown in C will bubble up to A
  // because it is not handled in C or B
  try {
    B();
  } catch (Exception $e) {
    echo "Caught exception in " . __METHOD__;
  }
}

function B() {
  // we're not catching exceptions in B
  C();
}

function C() {
  // we do not catch the exception where it is thrown
  throw new Exception('Bubble');
}
A();
/*
Outputs:
  Caught exception in A and the program ends successfully
*/

这个程序调用函数 A,函数 A 调用 B,B 调用 C。函数 C 抛出一个异常,但是我们没有在 C 中捕捉到它。异常冒泡到 B,但是也没有在 B 中捕捉到。异常继续冒泡到 A,在那里我们确实捕捉到了它。

如果我们没有在 A 中捕捉到异常,那么它将会冒泡到全局范围,在那里我们将有最后一次捕捉它的机会。如果一个异常没有被捕获,那么 PHP 会寻找一个默认的异常处理程序,最终如果没有处理程序,就会导致一个致命的错误。

扩展异常类

PHP 包括几个标准的异常类型,标准 PHP 库(SPL)还包括更多。虽然您不必使用这些异常,但是这样做意味着您可以使用更细粒度的错误检测和报告。

ExceptionError类都实现了Throwable接口 9 ,并且像任何其他类一样,可以被扩展。这允许您创建灵活的错误层次结构,并定制您的异常处理。

只有实现了Throwable类的类才能与throw关键字一起使用。换句话说,你不能声明你自己的基类,然后把它作为异常抛出。

例如,让我们创建一个异常类,我们可以用它来表示出现了表单验证问题:

<?php
class ValidationException extends Exception { }

function myValidation() {
    if (empty($_POST)) {
        throw new ValidationException('No form fields entered');
    }
}

让我们看看语法,然后更详细地讨论异常:

<?php
class ParentException extends Exception {}
class ChildException extends ParentException {}

try {
    // some code
    throw new ChildException('My Message');
} catch (ParentException $e) {
    // matches this class because of inheritance
    echo "Parent Exception :" . $e->getMessage();
} catch (ChildException $e) {
    // matches this class exactly
    echo "Child Exception :" . $e->getMessage();
} catch (Exception $e) {
    // matches this class because of inheritance
    echo "Exception :" . $e->getMessage();
}

这个例子的输出是Parent Exception :My Message

在这个例子中,我们抛出了一个继承自ParentExceptionChildException,它又扩展了基类Exception

这些块按照从上到下的顺序进行评估。将执行第一个匹配的块。

被抛出的异常的类与作为参数给定给catch子句的类名相匹配。

匹配标准是这些类别是:

  • 完全一样,或者
  • 抛出的异常是catch语句中异常的祖先

在这个例子中,我们抛出了一个继承自ParentException的异常ChildException。因此,异常与第一个catch块匹配,代码被执行。

我将基Exception放在了catch块列表的底部,因为所有自定义异常都继承自它,这使得它成为一个总括。

异常层次结构

到目前为止,我们已经知道ErrorsExceptions都实现了Throwable接口。我们刚刚看到ErrorException类都可以扩展。

内置的 PHP 7 异常层次结构如下所示:

interface Throwable
    |- Exception implements Throwable
        |- ...
    |- Error implements Throwable
        |- TypeError extends Error
            |- ArgumentCountError
        |- ParseError extends Error
        |- ArithmeticError extends Error
            |- DivisionByZeroError extends ArithmeticError
        |- AssertionError extends Error

正如您所看到的,有几个预定义的错误类在Error下面形成了一个层次结构。下表总结了它们的用途:

班级 目的
TypeError 当传递给函数的参数与其对应的声明参数类型不匹配时,或者当函数没有返回预期的类型时,就会抛出TypeError
ArgumentCountError 当传递给用户定义的函数或方法的参数太少时,抛出ArgumentCountError
ParseError 当解析 PHP 代码时出现错误,例如,当您调用eval()或包含一个文件时,就会抛出一个ParseError
ArithmeticError 当您尝试以负的量进行位移位或调用intdiv()时,会发生算术错误,这将导致当前系统上的值超出整数的限制。
DivisionByZeroError 如果你试图除以零,就会出现一个DivisionByZeroError
AssertionError 当用assert()语言构造的断言失败时,抛出一个AssertionError

处理异常

健壮的代码可以遇到错误并处理它。以明智的方式处理异常提高了应用的安全性,并使日志记录和调试更加容易。管理应用中的错误还可以让您为用户提供更好的体验。在这一节中,我们将介绍如何捕获和处理代码中出现的错误。

捕捉异常

记得在本章的前面,我们这样定义了一个ValidationException:

<?php
class ValidationException extends Exception { }

function myValidation() {
    if (empty($_POST)) {
        throw new ValidationException('No form fields entered');
    }
}

让我们从那里继续,想象我们正在调用myValidation()函数,并且想要捕捉异常。捕捉异常的语法如下:

<?php
try {
    // assume that if there is validation problem this throws a ValidationException
    myValidation();
} catch (ValidationException $e) {
    echo "Validation exception caught ";
    echo $e->getMessage();
} catch (Exception $e) {
    echo "General exception type caught";
}

注意这里有两个catch子句。异常将从上到下与子句匹配,直到异常的类型与catch子句匹配。

Note

匹配标准是这些类要么完全相同,要么抛出的异常类是catch语句中Exception类的祖先。

由于myValidation抛出了一个ValidationException,我们期望它在第一个块中被捕获,但是如果函数中抛出了任何其他类型的异常,那么它将在第二个catch块中被捕获。

还要注意方法getMessage()正在异常对象上被调用。基本Exception类中的其他方法将给出错误代码、堆栈跟踪和其他信息。关于Exceptions 11 的 PHP 手册对于异常对象的原型是最好的参考。

有可能在catch块中抛出异常。这使您可以捕捉异常,然后在需要时将其作为不同的类型重新抛出。

Tip

你应该总是把你的catch块从最具体的排在顶部,到最一般的排在底部——记住catch块是贪婪的!

一个catch块可以指定多个异常类,用管道字符(|)分隔它们。

在下面的例子中,catch块将匹配属于MyException类或AnotherException类的异常。

<?php

class MyException extends Exception {}
class AnotherException extends Exception {}

try {
    throw new AnotherException;
} catch (MyException | AnotherException $e) {
    echo "Caught : " . get_class($e);
}
/*
Caught : AnotherException
*/

Note

一个try块必须至少有一个catch块。

最终块

我们要看的最后一个子句是finally。无论是否抛出异常,这段代码都将被执行。它在try程序块完成后或异常程序块完成后执行。

finally块的一个常见用途是关闭一个文件句柄,但是finally可以用在您希望代码总是被执行的任何地方。

<?php
try {
  // perform some functions
} catch (Exception $e) {
  // handle the error
} finally {
  // always run these statements
}

设置默认异常处理程序

任何未被捕获的异常都会导致致命错误。如果您想优雅地响应没有被catch块捕获的异常,那么您需要设置一个函数作为默认的异常处理程序。

为此,您可以使用set_exception_handler()函数,该函数接受一个 callable 作为其参数。您的脚本将在 callable 执行后终止。

函数restore_exception_handler()会将异常处理程序恢复到之前的值。

Chapter 11 Quiz

Q1:这段代码会输出什么?

| 发现错误! |
| 作为会话 ID 的随机字符串 |
| 一个通知错误和一个警告 |
| 两行格式化的行,每一行包含关于错误的信息 |

<?php
$handler = function($errorNumber, $errorMessage, $file, $line) {
  echo "Error [$errorNumber] in [$file] at line [$line]: '[$errorMessage]'\r\n";
};
set_error_handler($handler);
try {
    echo $a;
    session_start();
    echo session_id();
} catch (Throwable $e) {
    echo "Error caught!";
}

Q2:这段代码会输出什么?

| 没有,这运行没有错误 |
| 一个正常的致命错误 PHP 消息 |
| 关于错误的格式化信息行 |
| 以上都不是 |

<?php
$handler = function($errorNumber, $errorMessage, $file, $line) {
  echo "Error [$errorNumber] in [$file] at line [$line]: '[$errorMessage]'\r\n";
};
set_error_handler($handler);
this_function_is_not_defined();

Q3:这段代码会输出什么?

| 没有,这段代码运行没有错误 |
| 哎呀! |
| 一个 PHP 致命错误 |
| 以上都不是 |

<?php
class IndianaError extends ArithmeticError {}
define('PI', 3);
try {
    if (is_int(PI)) {
      throw new IndianaError('Oops');
    }
} catch (Exception $e) {
    echo $e->getMessage();
}

Q4:这段代码会输出什么?

| 你好世界 |
| 两行信息 |
| 一个 PHP 致命错误 |
| 以上都不是 |

<?php
set_error_handler(function($errorNumber, $errorMessage, $file, $line) {
  debug_print_backtrace();
});
trigger_error('Hello world', E_USER_WARNING);

Q5:这段代码会输出什么?

| 捕获到异常! |
| 接住了! |
| 发现错误! |
| DivisionByZeroError抓到了! |
| 以上都不是 |

<?php
try {
    echo 50/0;
} catch (Exception $e) {
    echo "Exception caught!";
} catch (Throwable $e) {
    echo " Throwable caught!";
} catch (Error $e) {
    echo "Error caught!";
} catch (DivisionByZeroError $e) {
    echo "DivisionByZeroError caught!";
}

Footnotes 1

https://php.net/manual/en/class.throwable.php

2

https://php.net/manual/en/errorfunc.constants.php

3

https://php.net/manual/en/function.set-error-handler.php

4

https://php.net/manual/en/language.types.callable.php

5

https://php.net/manual/en/function.error-log.php

6

https://rollbar.com/

7

https://php.net/manual/en/errorfunc.configuration.php#ini.error-log

8

https://php.net/manual/en/ref.errorfunc.php

9

https://php.net/manual/en/class.throwable.php

10

[en.wikipedia.org/wiki/Robustness_(computer_science)](https://en.wikipedia.org/wiki/Robustness_(computer_science)

11

https://php.net/manual/en/language.exceptions.php

十二、练习

这一章只讲问题。所有这些问题的目的是帮助你确定你需要复习的领域。考试包括非常注重细节的问题,你需要确保你熟悉常用的功能。

我没有把问题按章节分类。真正的考试有侧重于教学大纲多个领域知识的问题。

Q1:当你运行这段代码时会发生什么?

| 由于没有捕获到异常,将会出现致命错误 |
| 当您试图抛出异常时,将会出现致命错误 |
| 脚本运行时没有任何输出,因为您没有对异常使用getMessage() |
| 以上都不是 |

<?php
class CustomException { }
throw new CustomException('Error!');

Q2:这个脚本会输出什么?

| 语法错误;它根本不会跑 |
| 这取决于你使用的 PHP 版本 |
| 致命的错误 |
| one |

<?php
function addOne($arg) {
$arg++;
}

$a = 0;
addOne(&$a);
echo $a;

Q3:这个脚本的输出会是什么?

| 语法错误;它不会运行 |
| True |
| False |
| 以上都不是 |

<?php
$a = function($a) {
return is_callable($a);
};
$b = function($b) use ($a) {
return $a($b);
};
echo $b($a) ? 'True' : 'False';

Q4:这个脚本的输出是什么?

| 句法误差 |
| Zero |
| one |
| Two |
| nine |

<?php
$a = 3;
echo $a >> 1;

q5:display_error配置设置的推荐生产设置为On

| True |
| False |

q6:session_generate_id()函数用于创建一个会话标识符,当一个人登录时应该调用它来帮助减轻会话固定攻击。

| True |
| False |

Q7:当你对一个对象调用json_encode函数进行序列化时,PHP 会调用哪个神奇的方法?

| __sleep |
| __wake |
| __get |
| __clone |
| 这些都不是 |

问题 8:这个脚本会输出什么?

| 语法错误;它根本不会跑 |
| array |
| object |
| string |
| 以上都不是 |

<?php
$emptyArray = [];
$encode = json_encode($emptyArray, JSON_FORCE_OBJECT);
$decode = json_decode($encode);
echo gettype($decode);

问题 9:以下脚本的输出是什么?

| 语法错误;这不会跑 |
| 数组是[Equal][Identical] |
| 数组是[Equal][Not Identical] |
| 数组是[Not Equal][Not Identical] |
| 数组是[Not Equal][Identical] |
| 以上都不是 |

<?php
$arr1 = [1,2,3];
$arr2 = array("1" => 2, 0 => "1", 2 => 3 );
$equal = $arr1 == $arr2 ? 'Equal' : 'Not Equal';
$identical = $arr1 === $arr2 ? 'Identical' : 'Not Identical';
echo "The arrays are [$equal] and [$identical]";

Q10:这个函数的输出是什么?

| One thousand two hundred and thirty-five |
| One thousand two hundred and thirty-four point five six eight |
| 1.234,57 |
| 以上都不是 |

<?php
$number = 1234.5678;
echo number_format($number, 2, ',', '.') . PHP_EOL;

Q11:在将字符串传递到数据库之前,应该使用类似addslashes()的函数对字符串进行转义,这样就不可能在您的网站上使用 SQL 注入攻击。

| True |
| False |

Q12:这段代码的输出是什么?

| 是 |
| 不 |
| 语法错误;这不会跑 |

<?php
class A
{
    public $name = '0';
    private $surname = '0';
    public function __isset($property)
    {
        return true;
    }
}
$a = new A;
$empty = empty($a->name);
$set = isset($a->surname);
if ($empty === $set) {
  echo "Yes";
} else {
  echo "No";
}

Q13:如果你没有指定可见性修饰符,PHP 默认选择 private,这样你的代码是安全的。

| True |
| False |

Q14:您可以使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 函数来确保变量适合显示并且不包含任何空格。

| ctype_alpha |
| ctype_print |
| ctype_graph |
| filter_var |

Q15:这段代码会输出什么?

| 语法错误;它不会跑 |
| 它永远不会结束运行 |
| 它会产生一个错误,因为函数nest()在全局范围内不存在 |
| hello world |
| 以上都不是 |

<?php
function bird($message) {
    function nest($string) {
        echo $string;
    }
    nest($message);
}
bird('hello');
echo " ";
nest('world');

Q16:代码会输出什么?

| 将会出现错误 |
| Zero |
| one |
| Two |

<?php
$a = 0;
$b = $a++;
$a = $a + 1;
echo --$a;

问题 17:您可以使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 功能来确保文件确实上传了,并且不是您操作系统上的不同文件。

| check_file_uploaded() |
| finfo_file() |
| is_uploaded_file() |
| 以上都不是 |

Q18:这段 PHP 代码的输出是什么?

| 语法错误 |
| a 未设置,B 已设置 |
| a 未设置,B 未设置 |
| a 被设置,B 被设置 |
| 将产生一个警告并输出"A is not set and B is not set" |
| 它将显示一个致命错误,说明变量未找到 |

<?php
echo (isset($a)) ? "A is set" : "A is not set";
echo " and ";
echo (empty($b)) ? "B is not set" : "B is set";

q19:PUTPOST都是幂等的。

| True |
| FalsePOST是幂等的,但PUT不是 |
| FalsePUT是幂等的,但POST不是 |
| False;也不是幂等的 |
| FalseREST是无状态的,所以没有幂等的 |

问题 20:您可以在定义类之前对其进行实例化,如下例所示:

| True |
| False |

<?php
$foo = new ExampleClass();
echo $foo;
class ExampleClass {}

问题 21:如果您没有使用预准备语句,并且希望在使用 MySQL 数据库时对字符串进行转义,您可以使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 函数。

| mysql_real_escape_string() |
| real_escape_string() |
| mysqli_real_escape_string() |
| addslashes() |
| mysqli_escape_string() |
| 以上都不是 |

问题 22:这个脚本会输出什么?

| 语法错误;这不会跑 |
| foo |
| bar |
| Hello world |

<?php
$a = "foo";
$$a = "bar";
$a = "Hello world";
echo ${"foo"};

问题 23:考虑下面的代码,输出会是什么?

| 语法错误;这不会跑 |
| True |
| False |

<?php
$a = "0.0";
$b = (int)$a;
if ( (boolean)$a === (bool)$b) {
echo "True";
} else {
echo "False";
}

问题 24:这个脚本会输出什么?

| foo |
| bar |
| 以上都不是 |

echo "Apples"<=>"bananas" ? 'foo' : 'bar';

问题 25:这是一个棘手的问题,所以请仔细阅读并预测代码的输出。记住,md5()的第二个参数导致散列以原始二进制格式返回,而不是十六进制字符串。

| 语法错误;这不会跑 |
| 16 |
| 32 |
| 这将导致一个错误,因为您不能定义一个与 PHP 函数名同名的函数 |
| 以上都不是 |

<?php
namespace A;
function md5($value) {
return \md5($value . ' Extra saltiness');
}
echo strlen(md5('Hi', true));

问题 26:以下哪种类型的错误会阻止脚本执行?

| 通知;注意 |
| 警告 |
| 语法错误 |
| 致命错误 |

问题 27:这个脚本的输出是什么?

| bool(true) |
| bool(false) |

<?php
$a = true;
$b = false;
$truth = $a && $b;
$pravda = $a and $b;
var_dump($truth == $pravda);

Q28:如果你想让 PHP 显示除通知之外的所有错误,你会使用php.ini中的哪个设置?

| error_reporting= -E_NOTICE |
| error_reporting=E_ALL - E_NOTICE |
| error_reporting= ∼E_NOTICE |
| error_reporting= E_ALL & ∼E_NOTICE |

问题 29:私有方法只能被定义它们的类访问,所以这段代码将输出一个空数组。

| True |
| False |

<?php
class Mirror {
    private function showMeGorgeous($me) {
        echo $me;
    }
}
$refObj = new ReflectionClass('Mirror');
print_r($refObj->getMethods());

问题 30:假设您从命令行运行这个脚本,而不是在 web 浏览器中。输出会是什么?

| hello <strong>hello</strong> |
| <strong>hello</strong> <strong>hello</strong> |
| hello hello |
| 以上都不是 |

<?php
class SetMissing {
    public function __set($name, $value) {
        $this->$name = filter_var($value, FILTER_SANITIZE_STRING);
    }
}
$obj = new SetMissing();
$obj->example = "<strong>hello</strong>";
echo $obj->example . PHP_EOL;
$obj->example = "<strong>hello</strong>";
echo $obj->example;

Q31:如果你实现了__sleep()函数,你需要确保它返回一个包含你想要序列化的实例变量的名字和值的关联数组。

| True |
| False |

问题 32:这段代码的输出会是什么?

| Yes please! |
| No thanks! |
| Woof |
| Purr |
| 以上都不是 |

<?php
trait Dog {
public function makeNoise() {
echo "Woof";
}
  public function wantWalkies() {
echo "Yes please!";
}
}

trait Cat {
public function makeNoise() {
echo "Purr";
  }
public function wantWalkies() {
    echo "No thanks!";
}
}

class DomesticPet
{
use Dog, Cat {
Cat::makeNoise insteadof Dog;
Cat::wantWalkies as kittyWalk;
Dog::wantWalkies insteadof Cat;
}
}
$obj = new DomesticPet();
$obj->kittyWalk();

Q33:这段代码的输出是什么?

| 语法错误;这不会跑 |
| 是 |
| 不 |
| 致命错误 |

<?php
class A
{
    public $name = '0';
    private $surname = '0';
    public function __isset($property)
    {
        return true;
    }
}
$a = new A;
$empty = empty($a->name);
$set = isset($a->surname);
if ($empty === $set) {
  echo "Yes";
} else {
  echo "No";
}

q34:PHP 键区分大小写吗?这个脚本的输出会是什么?

| 它们不区分大小写;这将输出2 |
| 它们区分大小写;这将输出4 |
| PHP 密钥转换成整数;这输出2 |
| 以上都不是 |

<?php
$arr1 = ["A" => "apple", "B" => "banana"];
$arr2 = ["a" => "aardvark", "b" => "baboon"];
echo count($arr1 + $arr2);

问题 35:这个脚本会输出什么?

| Zero |
| 1 |
| 2 |
| 语法错误;你不能在一行上赋值两个变量 |
| 以上都不是 |

<?php
$a = "0";
$c = $b = empty($a);
$d = ++$b + $a;
echo $d;

问题 36:这段代码会输出什么?

| 5 |
| 6 |
| 5.79 |
| 以上都不是 |

<?php
$a = 1.23;
$b = 4.56;
$c = (int)$a + (int)$b;
echo (double)$c;

问题 37:这个脚本会输出什么?

| one |
| two |
| three |
| 以上都不是 |

<?php
$arr = ["one", "two", 1.5 => "three"];
echo $arr[1];

问题 38:接口只能指定公共方法,但是你的类可以按照你喜欢的方式实现它们。

| True |
| False |

问题 39:通过使用 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 配置设置,您可以强制会话仅包含在 cookies 中。

| session.cookie_secure |
| session.use_cookies |
| session.use_trans_sid |
| 以上都不是 |

Q40:这段代码的输出是什么?

| 印第安纳 |
| Three hundred |
| Nine hundred |
| 986.962699801 |
| four hundred |
| One thousand six hundred |

<?php
define('PI', 3.14159625);
define('_PI', "3.1459625");
$radius = 10;
if (PI == _PI) {
    $area = (int)PI * $radius ** 2;
    echo $area;
} else {
    echo "Indiana";
}

Q41:这个脚本的输出是什么?

| 句法误差 |
| Hello World! |
| PHP 注意:使用未定义的常量HELLO |
| 以上都不是 |

<?php
function HelloWorld() {
echo HELLO;

}
const HELLO = "Hello World!";
HelloWorld();

Q42:这段代码会输出什么?

| 语法错误 |
| 1 |
| 1.1 |
| 以上都不是 |

<?php
function add(int $a, int $b): integer {
    return $a + $b;
}
echo add(5.7, -4.6);

Q43:这个脚本的输出是什么?

| 语法错误;这不会跑 |
| undefined |
| integer |
| double |
| boolean |

<?php
$a = 0b0010;
$b = 0b0001;
echo gettype($a & $b);

Q44:这段代码的输出会是什么?

| 空 |
| 1NULL |
| Hello world!NULL |
| A syntax error |

<?php
$result = echo print("Hello world!");
var_dump($result);

Q45:您已经在命令行上调用了 PHP,并且想要访问您在 PHP 脚本中传递的参数。哪个超级全球将包含这些?

| $GLOBALS |
| $_ARGV |
| $_SERVER |
| $_ARGUMENTS |

Q46:函数gc_collect_cycles()用于执行以下哪项操作?

| 没有这个功能 |
| 将脚本暂停一定数量的处理器周期 |
| 启动垃圾收集以释放内存 |
| 刷新opcode缓存 |

Q47:这段代码会输出什么?

| 这会产生一个通知错误 |
| 这会产生致命的错误 |
| 捕捉到错误 |
| 以上都不是 |

<?php
try {
    // generates a notice error (not caught)
    echo $thisVariableIsNotSet;
} catch (Error $e) {
    echo "Error caught";
}

Q48:这段代码会输出什么?

| Ron loves Justin | * |
| Ron adores Justin |   |
| $name $emotion Justin |   |
| 以上都不是 |   |

<?php
$emotion = "loves";
$theBeeb = function($name) use ($emotion) {
    echo "$name $emotion Justin";
};
$emotion = "adores";
$theBeeb("Ron");

Q49:假设PlutoGrumpy类都在class.definitions.php文件中声明,这个文件和这个脚本在同一个目录下。运行这个脚本会发生什么?

| 该脚本将生成一个致命错误,因为您需要在类名后面加上括号 |
| 当您第二次尝试包含该脚本时,该脚本将生成一个错误 |
| 该文件将被包括一次,脚本不会输出任何东西 |
| 脚本将失败,因为您不能对spl_autoload_register使用变量;只有字符串文字 |
| 以上都不是 |

<?php
$a = function() {
include('class.definitions.php');
};
spl_autoload_register($a);
// Class Pluto is defined in class.definitions.php
$planet = new Pluto;
// Class Grumpy is defined in class.definitions.php
$dwarf = new Grumpy;

问题 50:以下哪些与请求相关的信息不能被客户轻易更改?

| 远程 IP 地址 |
| 会话数据 |
| Cookie 数据 |
| 用户代理人 |
| 所有这些都很容易被客户更改 |

posted @ 2024-08-03 11:23  绝不原创的飞龙  阅读(0)  评论(0编辑  收藏  举报