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

106 阅读15分钟

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 将确保它只包含这个文件一次。
requirePHP 将包含一个文件并对其进行评估。如果找不到或无法读取该文件,则会生成一个致命错误。
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 中不能转换为空类型。

标量类型

有四种标量类型:

类型别名包含
布尔代数学体系的boolTrueFalse
整数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关于路径、头和其他与服务器环境相关的信息的信息数组。
$_GETGET请求中发送的变量。
$_POSTPOST请求中发送的变量。
$_FILES作为POST请求的一部分上传的文件的关联数组。
$_COOKIE通过 HTTP cookies 传递给当前脚本的变量的关联数组。
$_SESSION包含当前脚本可用的会话变量的关联数组。
$_REQUESTPOSTGETCOOKIE请求变量。
$_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 ** 1312 的 13 次方

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

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

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

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

命令输出之后$a的值描述
$a = 1; 1 
echo $a++;12后缀
echo ++$a;33前缀
echo $a--;32后缀
echo --$a;11前缀

逻辑运算符

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 <=> 01
1 <=> 10
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

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

值/运算符每个位置的位数
FiftyoneoneZeroZerooneZero
Twenty-fiveZerooneoneZeroZeroone
操作1 和 01 和 10 和 10 和 01 和 00 和 1
结果ZerooneZeroZeroZeroZero

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

比特移位

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

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

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

操作操作导致二进制结果为十进制
Fifty 00110010 
50 >> 1右移00011001Twenty-five
50 << 1左移位01100100One 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 使用(波浪号)符号进行按位非运算。该运算符的作用是翻转值中的位,如果某个位被置位,它将被复位,如果没有被置位,它将被置位。

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

 
FiftyoneoneZeroZerooneZero
∼ (不)ZeroZerooneoneZeroone

结果的值(十进制)是-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