PHP8-快速脚本参考-全-

PHP8 快速脚本参考(全)

原文:PHP 8 Quick Scripting Reference

协议:CC BY-NC-SA 4.0

一、使用 PHP

要开始用 PHP 开发,创建一个带有.php文件扩展名的纯文本文件,并在您选择的编辑器中打开它——例如,Notepad、jEdit、Dreamweaver、NetBeans 或 PHPEclipse。这个 PHP 文件可以包含任何 HTML,以及 PHP 脚本代码。首先为 HTML 5 web 文档输入以下最小标记。

<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>PHP Test</title>
 </head>
 <body></body>
</html>

嵌入 PHP

PHP 代码可以以几种不同的方式嵌入到 web 文档的任何地方。标准符号是用<?php?>来分隔代码。这被称为一个 PHP 代码块,或者仅仅是一个 PHP 块

<?php ... ?>

在一个 PHP 块内,PHP 引擎被称为处于 PHP 模式;在程序块之外,引擎处于 HTML 模式。在 PHP 模式下,所有内容都由 PHP 引擎解析(执行),而在 HTML 模式下,所有内容都不经过任何解析就被发送到生成的网页。

切换到 PHP 模式的第二个符号是第一个符号的简短版本,其中省略了php部分。虽然这种符号较短,但是如果 PHP 代码需要可移植性,那么较长的符号更好。这是因为可以在php.ini配置文件中禁用对短分隔符的支持。 1

<? ... ?>

第三种方法(现在已经过时)是将 PHP 代码嵌入到 HTML 脚本元素中,并将 language 属性设置为php。这种替代分隔符很少使用;PHP 7 中移除了对它的支持。

<script language="php">...</script>

您在遗留代码中可能遇到的另一个过时的符号是当脚本嵌入在 ASP 标记之间时。默认情况下,这种表示法是禁用的,但是可以从 PHP 配置文件中启用。长期以来,不鼓励使用这种符号。PHP 7 最终移除了启用它的功能。

<% ... %>

脚本文件中的最后一个结束标记可能会被省略,以使文件在 PHP 模式下结束。

<?php ... ?>
<?php ...

输出文本

用 PHP 打印文本是通过键入 echoprint 后跟输出来完成的。每条语句必须以分号(;)结尾,以便与其他语句区分开来。PHP 块中最后一个语句的分号是可选的,但是包含它是一个好习惯。

<?php
  echo  "Hello World";
  print "Hello World";
?>

也可以使用<?=打开分隔符生成输出。从 PHP 5.4 开始,即使禁用了短 PHP 分隔符,该语法仍然有效。

<?= "Hello World" ?>

请记住,网页上显示的文本应该始终位于 HTML body 元素中。

<body>
  <?php echo "Hello World"; ?>
</body>

安装 Web 服务器

要在浏览器中查看 PHP 代码,首先必须在安装了 PHP 模块的 web 服务器上解析代码。建立 PHP 环境的一个简单方法是下载并安装一个流行的 Apache web 服务器发行版,名为 XAMPP, 2 ,它预装了 PHP、Perl 和 MariaDB。它适用于 Windows、Linux 以及 OS X,并允许您在自己的计算机上试验 PHP。

安装 web 服务器后,将浏览器指向http://localhost以确保服务器在线。它应该显示index.php文件,默认情况下,该文件在 Windows 上位于C:\xampp\htdocs\下,在 Linux 上位于/opt/lampp/htdocs/下。htdocs是 Apache web 服务器在您的域中查找文件的文件夹。

你好世界

继续前面的内容,简单的 Hello World PHP web 文档应该如下所示:

<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>PHP Test</title>
 </head>
 <body>
  <?php echo "Hello World"; ?>
 </body>
</html>

要查看这个被解析成 HTML 的 PHP 文件,请将其保存到 web 服务器的htdocs文件夹(服务器的根目录)中,并使用诸如mypage.php之类的名称。然后将你的浏览器指向它的路径,这是本地网络服务器的http://localhost/mypage.php

当请求 PHP 网页时,脚本在服务器上被解析,并作为 HTML 发送给浏览器。如果查看网站的源代码,它将不会显示任何生成页面的服务器端代码,只显示 HTML 输出。

编译和解析

PHP 是一种解释型语言,不是编译型语言。访问者每次到达一个 PHP 网站,PHP 引擎都会编译代码并解析成 HTML,然后发送给访问者。这样做的主要优点是代码可以很容易地更改,而不必重新编译和重新部署网站。主要缺点是在运行时编译代码需要更多的服务器资源。

对于小型网站来说,缺少服务器资源很少是个问题。与执行数据库查询所需的时间等其他因素相比,编译 PHP 代码所需的时间也很少。然而,对于具有大量流量的大型 web 应用,编译 PHP 文件的服务器负载可能会很大。对于这样的站点,可以通过预编译 PHP 代码来消除脚本编译开销。这可以通过使用 PHP 加速器来实现,比如 WinCache、 3 ,它可以在编译状态下缓存 PHP 脚本。

一个只提供静态内容(对所有访问者都一样)的网站还有另一种可能,就是缓存完全生成的 HTML 页面。这提供了拥有动态站点的所有维护好处,并具有静态站点的速度。一个这样的缓存工具是 WordPress CMS 的 W3 总缓存 4 插件。

PHP 的每个新版本不仅改进了语言,还改进了 PHP 引擎的性能。特别是 PHP 8 增加了 JIT (just in time)编译器。这个编译器不断寻找频繁重复执行的 PHP 代码。然后,这些代码被编译成机器码并被缓存,这使得代码的执行速度甚至比常规的缓存预编译代码还要快。

评论

注释用于在代码中插入注释。它们对脚本的解析没有影响。PHP 有两种标准的 C++符号用于单行(//)和多行(/* */)注释。Perl 注释符号(#)也可以用来做单行注释。

<?php
  // single-line comment
  #  single-line comment
  /* multi-line
     comment */
?>

和 HTML 一样,空白字符——比如空格、制表符和注释——被 PHP 引擎忽略。这让你在如何格式化你的代码上有很大的自由度。

二、变量

变量用于存储数据,如数字或字符串,以便它们可以在代码中多次使用。

定义变量

变量以美元符号($)开始,后面跟着一个标识符,这是变量的名称。变量的一个常见命名约定是,除了第一个单词,每个单词最初都要大写。

$myVar;

可以使用等号或赋值运算符(=)给变量赋值。然后变量变成定义的初始化的

$myVar = 10;

一旦定义了变量,就可以通过引用变量名来使用它。例如,可以通过使用echo后跟变量名,将变量值打印到网页上。

echo $myVar; // "10"

请记住,变量名区分大小写。PHP 中的名称可以包含下划线和数字,但不能以数字开头。它们也不能包含空格或特殊字符,并且不能是保留关键字。

数据类型

PHP 是一种松散类型的语言。这意味着没有指定变量可以存储的数据类型。相反,变量的数据类型会自动更改,以保存分配给它的值。

$myVar = 1;   // int type
$myVar = 1.5; // float type

此外,变量值的计算方式也不同,这取决于使用它的上下文。

// Float type evaluated as string type
echo $myVar; // "1.5"

由于这些隐式类型转换,知道变量的基础类型并不总是必要的。然而,理解 PHP 在后台处理的数据类型是很重要的。这十种类型列于表 2-1 中。

表 2-1

PHP 数据类型

|

数据类型

|

种类

|

描述

|
| --- | --- | --- |
| (同 Internationalorganizations)国际组织 | 数量 | 整数。 |
| 漂浮物 | 数量 | 浮点数。 |
| 弯曲件 | 数量 | 布尔值。 |
| 线 | 数量 | 一系列字符。 |
| 排列 | 复合材料 | 值的集合。 |
| 目标 | 复合材料 | 用户定义的数据类型。 |
| 资源 | 特别的 | 外部资源。 |
| 请求即付的 | 特别的 | 函数或方法。 |
| 混合的 | 特别的 | 任何类型。 |
| 空 | 特别的 | 没有价值。 |

整数类型

整数是一个整数。它们可以用十进制(基数 10)、十六进制(基数 16)、八进制(基数 8)或二进制(基数 2)表示法来指定。十六进制数字前面有一个0x,八进制数字前面有一个0,二进制数字前面有一个0b

$myInt = 1234; // decimal number
$myInt = 0b10; // binary number (2 in decimal)
$myInt = 0123; // octal number (83 in decimal)
$myInt = 0x1A; // hexadecimal number (26 in decimal)

PHP 中的整数总是有符号的,因此可以存储正值和负值。整数的大小取决于系统字长,所以在 32 位系统上,最大可存储值是 2 ³²-1 。如果 PHP 遇到一个更大的值,它会被解释为浮点型。

浮点型

float 或 floating-point 类型可以存储实数。这些可以用十进制或指数记数法来指定。

$myFloat = 1.234;
$myFloat = 3e2; // 3*10² = 300

浮点的精度取决于平台。通常使用 64 位 IEEE 格式,可以保存大约 14 位十进制数,最大十进制值为 1.8×10 308

布尔类型

bool 类型可以存储布尔值,该值只能为 true 或 false。这些值由关键字truefalse指定。

$myBool = true;

零点类型

不区分大小写的常量null用于表示没有值的变量。这种变量被认为是特殊的空数据类型。

$myNull = null; // variable is set to null

与其他值一样,null 值的计算方式也不同,这取决于变量使用的上下文。如果评估为 bool,它将变为 false 作为一个数,它变成零(0);而作为字符串,就变成了空字符串("")。

$myInt = $myNull + 0;      // numeric context (0)
$myBool = $myNull == true; // bool context (false)
echo $myNull;              // string context ("")

默认值

从 PHP 8 开始,变量在使用前必须被定义。尝试使用未定义的变量将触发错误异常,从而停止脚本的执行。

// PHP 8
$myDefined = null;
echo $myDefined; // ok
echo $myUndefined; // error exception

在 PHP 8 之前,可以使用没有赋值的变量。这种未定义的变量将自动创建一个空值。

// Before PHP 8
echo $myUndefined; // created and set to null

尽管这种行为是允许的,但在使用变量之前定义变量一直是一种良好的编码实践,即使变量只是被设置为 null。提醒一下,PHP 8 之前的 PHP 版本在使用未定义的变量时会发出错误通知。

Notice: Undefined variable: myUndefined in C:\xampp\htdocs\mypage.php on line 10

三、运算符

数字运算符是一个符号,它使脚本执行特定的数学或逻辑操作。数字运算符可以分为五种类型:算术、赋值、比较、逻辑和按位运算符。

算术运算符

算术运算符包括四种基本算术运算,以及用于获得除法余数的模数运算符(%)。

$x = 4 + 2; // 6, addition
$x = 4 - 2; // 2, subtraction
$x = 4 * 2; // 8, multiplication
$x = 4 / 2; // 2, division
$x = 4 % 2; // 0, modulus (division remainder)

PHP 5.6 中引入了一个求幂运算符(**)。它将左边的操作数提升到右边操作数的幂。

$x = 4 ** 2; // 16, exponentiation

赋值运算符

第二组是赋值操作符,最重要的是赋值操作符(=)本身,它给变量赋值。

$x = 1; // assignment

组合赋值运算符

赋值运算符和算术运算符的一个常见用途是对变量进行运算,然后将结果保存回同一个变量中。使用组合赋值操作符可以缩短这些操作。

$x += 5; // $x = $x+5;
$x -= 5; // $x = $x-5;
$x *= 5; // $x = $x*5;
$x /= 5; // $x = $x/5;
$x %= 5; // $x = $x%5;

PHP 5.6 中添加的取幂运算符也接受了一个简写赋值运算符。

$x **= 5; // $x = $x**5;

递增和递减运算符

另一种常见的操作是将变量加 1 或减 1。这可以用增量(++)和减量(--)操作符来简化。

$x++; // $x += 1;
$x--; // $x -= 1;

这两个运算符都可以用在变量之前或之后。

$x++; // post-increment
$x--; // post-decrement
++$x; // pre-increment
--$x; // pre-decrement

无论使用哪个变量,变量的结果都是相同的。区别在于后操作符在改变变量之前返回原始值,而前操作符首先改变变量,然后返回值。

$x = 5; $y = $x++; // $x=6, $y=5
$x = 5; $y = ++$x; // $x=6, $y=6

比较运算符

比较运算符比较两个值并返回truefalse。它们主要用于指定条件,即评估为truefalse的表达式。

$x = (2 == 3);  // equal to (false)
$x = (2 != 3);  // not equal to (true)
$x = (2 <> 3);  // not equal to (alternative)
$x = (2 === 3); // identical (false)
$x = (2 !== 3); // not identical (true)
$x = (2 > 3);   // false,greater than (false)
$x = (2 < 3);   // less than (true)
$x = (2 >= 3);  // greater than or equal to (false)
$x = (2 <= 3);  // less than or equal to (true)

严格相等运算符===!==用于比较类型和值。这是必要的,因为常规的“等于”(==)和“不等于”(!=)运算符在比较操作数之前会自动执行类型转换。当不需要“等于”运算的类型转换功能时,使用严格比较被认为是一种好的做法。

$x = (1 ==  "1"); // true (same value)
$x = (1 === "1"); // false (different types)

PHP 7 增加了一个新的比较操作符,叫做飞船操作符 ( <=>)。它比较两个值,如果两个值相等,则返回 0;如果左边的值较大,则返回 1;如果右边的值较大,则返回–1。

$x = 1 <=> 1; // 0 (1 == 1)
$x = 3 <=> 2; // 1 (3 > 2)
$x = 1 <=> 2; // -1 (1 < 2)

逻辑运算符

逻辑运算符通常与比较运算符一起使用。如果左右两边都为真,则逻辑与(&&)计算为true,如果左右两边都为真,则逻辑或(||)计算为true。对一个布尔结果求反,有一个逻辑非(!)运算符。请注意,对于“逻辑与”和“逻辑或”,如果左侧已经确定了结果,则不会计算运算符的右侧。

$x = (true && false); // logical and (false)
$x = (true || false); // logical or (true)
$x = !true;           // logical not (false)

按位运算符

按位运算符可以处理二进制数字。例如,xor 运算符(^)打开运算符一侧设置的位,而不是两侧设置的位。

$x = 5 & 4;  // 101&100=100 (4) // and
$x = 5 | 4;  // 101|100=101 (5) // or
$x = 5 ^ 4;  // 101¹⁰⁰=001 (1) // xor (exclusive or)
$x = 4 << 1; // 100<<1=1000 (8) // left shift
$x = 4 >> 1; // 100>>1=10   (2) // right shift
$x = ~4;     // ~00000100=11111011 (-5) // invert

这些位操作符有速记赋值操作符,就像算术操作符一样。

$x=5; $x &= 4;  // 101&100=100 (4) // and
$x=5; $x |= 4;  // 101|100=101 (5) // or
$x=5; $x ^= 4;  // 101¹⁰⁰=001 (1) // xor
$x=5; $x <<= 1; // 101<<1=1010 (10)// left shift
$x=5; $x >>= 1; // 101>>1=10   (2) // right shift

请注意,与按位运算符一起使用的十进制数自动计算为二进制数。二进制记数法也可用于为按位运算指定二进制数。

$x = 0b101 & 0b100; // 0b100 (4)

运算符优先级

当表达式包含多个运算符时,这些运算符的优先级决定了它们的求值顺序。优先顺序见表 3-1 。

表 3-1

运算符优先级的顺序

|

在…之前

|

操作员

|

在…之前

|

操作员

|
| --- | --- | --- | --- |
| one | ** | Ten | & |
| Two | ++ - | Eleven | ^ |
| three | ~ -(一元) | Twelve | | |
| four | ! | Thirteen | && |
| five | * / % | Fourteen | || |
| six | + -(二进制) | Fifteen | = op= |
| seven | << >> | Sixteen | 和 |
| eight | < <= > >= <> | Seventeen | 异或运算 |
| nine | == != === !== <=> | Eighteen | 或者 |

举个例子,乘法的优先级高于加法,因此在下面一行代码中首先计算乘法。

$x = 4 + 3 * 2; // 10

圆括号可用于强制优先级。括号中的表达式在该语句中的其他表达式之前进行计算。

$x = (4 + 3) * 2; // 14

附加逻辑运算符

在优先表中,特别注意最后三个运算符:andorxorandor运算符的工作方式与逻辑&&||运算符相同。唯一的区别是它们的优先级较低。

// Same as: $x = (true && false);
$x = true && false; // $x is false (0)

// Same as: ($x = true) and false;
$x = true and false; // $x is true (1)

xor运算符是位^运算符的布尔版本。如果只有一个操作数为真,则计算结果为true

$x = (true xor true); // false

四、字符串

字符串是可以存储在变量中的一系列字符。在 PHP 中,字符串通常由单引号分隔。

$a = 'Hello';

串并置

PHP 有两个字符串操作符。点符号被称为串联运算符 ( .)。它将两个字符串合并为一个。它还有一个伴随的赋值操作符(.=),将右边的字符串追加到左边的字符串变量。

$b = $a . ' World'; // Hello World
$a .= ' World';     // Hello World

分隔字符串

PHP 字符串可以用四种不同的方式分隔。有两种常见的符号:双引号(" ")和单引号(' ')。它们之间的区别在于变量不是在单引号字符串中解析的,而是在双引号字符串中解析的。

$c = 'World';
echo "Hello $c"; // "Hello World"
echo 'Hello $c'; // "Hello $c"

除非需要解析,否则倾向于使用单引号字符串,这突出表明不进行解析。然而,双引号字符串被认为更容易阅读,这使得选择更多的是一个偏好问题。重要的是要始终如一。

除了单引号和双引号字符串,还有两种符号: heredocnowdoc 。这些符号主要用于包含较大的文本块。

继承字符串

heredoc 语法由<<<操作符后跟一个标识符和一个新行组成。然后包含该字符串,后跟一个包含标识符的新行,以结束该字符串。

$s = <<<LABEL
Heredoc string (with parsing)
Goodbye
LABEL;

变量在 heredoc 字符串中解析,就像双引号字符串一样。

$name = 'John';
$s = <<<LABEL
Hello $name
LABEL;
echo $s; // "Hello John"

Nowdoc 字符串

nowdoc 字符串的语法与 heredoc 字符串的语法相同,只是初始标识符用单引号括起来。在 nowdoc 字符串中不解析变量。

$s = <<<'LABEL'
Nowdoc string (without parsing)
LABEL;

在定义延伸多行的长字符串时,Heredoc 和 nowdoc 字符串对于提高可读性很有用。这些字符串中出现的任何特殊字符(如换行符或引号)都将被包含在内,而无需使用转义符。

转义字符

转义字符用于书写特殊字符,如反斜杠、换行符和双引号。这些字符前面总是有一个反斜杠(\)。表 4-1 列出了 PHP 中可用的转义字符。

表 4-1

转义字符

|

性格;角色;字母

|

意义

|

性格;角色;字母

|

意义

|
| --- | --- | --- | --- |
| \n | 新行 | \f | 换页 |
| \t | 横表 | $ | 美元符 |
| \v | 垂直标签 | ' | 单引号 |
| \e | 逃跑 | " | 双引号 |
| \r | 回车 | \ | 反斜线符号 |
| \u{} | Unicode 字符 |   |   |

例如,换行符在字符串中用转义字符(\n)表示。

$s = "Hello\nWorld";

注意,这个字符不同于在网页上创建换行符的<br> HTML 标签。

echo "Hello<br>World";

当使用单引号或 nowdoc 分隔符时,唯一有效的转义字符是反斜杠(\\)和单引号(\')字符。只有在单引号之前或字符串末尾才需要转义反斜杠。

$s = 'It\'s'; // "It's"

PHP 7 引入了 Unicode 转义字符,它提供了将 UTF 8 编码的字符嵌入字符串的能力。这样的字符被指定为花括号内的十六进制数。该数字最长可达六位数,前导零是可选的。

echo "\u{00C2A9}"; // © (copyright sign)
echo "\u{C2A9}";   // ©

字符引用

可以通过在字符串变量后的方括号中指定所需字符的索引来引用字符串中的字符,从零开始。这可用于访问和修改单个字符。

$s = 'Hello';
$s[0] = 'J';
echo $s; // "Jello"

strlen函数获取字符串参数的长度。例如,这可以用来改变字符串的最后一个字符。

$s[strlen($s)-1] = 'y';
echo $s; // "Jelly"

字符串比较

比较两个字符串的方法很简单,就是使用其中一个相等运算符。这不像在其他语言中那样比较内存地址。

$a = 'test';
$b = 'test';
$c = ($a === $b); // true

字符串函数

PHP 有许多处理和操作字符串的内置函数。这里可以看到一些更常用的字符串函数的例子。

$a = 'String';

// Search and replace characters in a string
$b = str_replace('i', 'o', $a); // Strong

// Insert text at specified position
$b = substr_replace($a, 'My ', 0, 0); // My String

// Get part of string specified by start and length
$b = substr($a, 0, 3); // Str

// Convert to uppercase
$b = strtoupper($a); // STRING

// Find position of the first occurrence of a substring
$i = strpos($a, 'i'); // 3 (starts at 0)

// Get string length
$i = strlen($a); // 6

五、数组

数组用于在单个变量中存储值的集合。PHP 中的数组由键值对组成。该键可以是整数(数值数组)、字符串(关联数组)或两者的组合(混合数组)。该值可以是任何数据类型。

数字数组

数字数组用数字索引存储数组中的每个元素。使用array构造函数创建一个数组。此构造函数接受一个值列表,这些值被分配给数组的元素。

$a = array(1,2,3);

从 PHP 5.4 开始,有了一个更短的语法,其中数组构造函数被方括号取代。

$a = [1,2,3];

一旦创建了数组,就可以通过将所需元素的索引放在方括号中来引用它的元素。请注意,索引从零开始。

$a[0] = 1;
$a[1] = 2;
$a[2] = 3;

数组中元素的数量是自动处理的。向数组中添加新元素就像给它赋值一样简单。

$a[3] = 4;

也可以省略索引,将值添加到数组的末尾。如果变量还没有数组,这个语法也会构造一个新的数组。

$a[] = 5; // $a[4]

要检索数组中某个元素的值,需要在方括号中指定该元素的索引。

echo "$a[0] $a[1] $a[2] $a[3]"; // "1 2 3 4"

关联数组

在关联数组中,键是一个字符串而不是一个数字索引,这给元素一个名称而不是一个数字。创建数组时,使用双箭头操作符(=>)来判断哪个键代表哪个值。

$b = array('one' => 'a', 'two' => 'b', 'three' => 'c');

使用元素名称引用关联数组中的元素。不能用数字索引引用它们。

$b['one'] = 'a';
$b['two'] = 'b';
$b['three'] = 'c';

echo $b['one'] . $b['two'] . $b['three']; // "abc"

双箭头运算符也可以与数字数组一起使用,以决定将值放在哪个元素中。

$c = array(0 => 0, 1 => 1, 2 => 2);

并非所有的键都需要指定。如果某个键未被指定,则该值被赋给先前使用的最大整数键后面的元素。

$e = array(5 => 5, 6);

混合数组

PHP 不区分关联数组和数值数组,所以两者的元素可以组合在同一个数组中。

$d = array(0 => 1, 'foo' => 'bar');

只是要确保用相同的键访问元素。

echo $d[0] . $d['foo']; // "1bar"

多维数组

多维数组是包含其他数组的数组。例如,二维数组可以通过以下方式构建。

$a = array(array('00', '01'), array('10', '11'));

创建后,可以使用两组方括号修改元素。

$a[0][0] = '00';
$a[0][1] = '01';
$a[1][0] = '10';
$a[1][1] = '11';

它们的访问方式也是一样的。

echo $a[0][0] . $a[0][1] . $a[1][0] . $a[1][1];

可以给这个键一个字符串名,使它成为一个多维关联数组,也称为哈希表

$b = array('one' => array('00', '01'));
echo $b['one'][0] . $b['one'][1]; // "0001"

通过添加额外的方括号组,多维数组可以有两个以上的维度。

$c[][][][] = "0000"; // four dimensions

六、条件

条件语句用于根据不同的条件执行不同的代码块。

如果语句

只有当括号内的条件被评估为true时,if语句才会执行。条件可以包括任何比较和逻辑运算符。

$x = 1;
// ...
if ($x == 1) {
  echo 'x is 1';
}

为了测试其他条件,if语句可以用任意数量的elseif子句来扩展。只有当所有先前的条件都为假时,才会测试每个附加条件。

elseif ($x == 2) {
  echo 'x is 2';
}

对于处理所有其他情况,可以在末尾有一个else子句,如果前面的所有条件都为假,则执行该子句。

else {
  echo 'x is something else';
}

如果只需要有条件地执行一条语句,可以省去花括号。但是,包含它们被认为是一种好的做法,因为它们可以提高代码的可读性。

if ($x == 1)
  echo 'x is 1';
elseif ($x == 2)
  echo 'x is 2';
else
  echo 'x is something else';

请记住,PHP 中变量的作用域不受本地代码块的限制,这与许多其他编程语言不同。因此,即使在代码块结束后,在条件语句中创建的变量仍然可以访问。

if ($x == 1) {
  $output = 'True';
}
else {
  $output = 'False';
}
echo $output; // ok

任何表达式都可以用作 if 语句的条件,不仅仅是布尔值。如下所示,任何类似零的表达式都将被计算为 false。所有其他值都被认为是真的。

if (0) {} // false
if (-0.0) {} // false
if ('') {} // false (empty string)
if (array()) {} // false (empty array)
if (null) {} // false (no value)
if (-1) {} // true (non-zero number)

交换语句

switch语句检查整数、浮点或字符串与一系列 case 标签之间的相等性。然后,它将执行传递给匹配的案例。该语句可以包含任意数量的 case 子句,并且可以以处理所有其他 case 的默认标签结束。

switch ($x)
{
  case 1: echo 'x is 1'; break;
  case 2: echo 'x is 2'; break;
  default: echo 'x is something else';
}

请注意,每个 case 标签后面的语句没有用花括号括起来。相反,语句以关键字break结束,以脱离开关。如果没有中断,执行将转到下一个案例。如果需要以相同的方式评估几个案例,这将非常有用。

替代语法

PHP 为条件语句提供了另一种语法。在这个语法中,if语句的左括号被替换为冒号,右括号被删除,最后一个右括号被替换为endif关键字。

if ($x == 1):     echo 'x is 1';
elseif ($x == 2): echo 'x is 2';
else:             echo 'x is something else';
endif;

类似地,switch 语句也有一个替代语法,它使用endswitch关键字来终止语句。

switch ($x):
  case 1:  echo 'x is 1'; break;
  case 2:  echo 'x is 2'; break;
  default: echo 'x is something else';
endswitch;

对于较长的条件语句,替代语法通常更可取,因为这样更容易看到这些语句的结束位置。

混合模式

在代码块中间切换回 HTML 模式是可能的。这提供了编写将文本输出到网页的条件语句的另一种方式。

<?php if ($x == 1) { ?>
  This will show if $x is 1.
<?php } else { ?>
  Otherwise this will show.
<?php } ?>

也可以以这种方式使用替代语法,以使代码更加清晰。

<?php if ($x == 1): ?>
  This will show if $x is 1.
<?php else: ?>
  Otherwise this will show.
<?php endif; ?>

当输出 HTML 和文本时,尤其是较大的块,这种编码风格通常是首选的,因为它更容易区分 PHP 代码和出现在网页上的 HTML 内容。

三元运算符

除了ifswitch语句,还有三元运算符(?:)。该操作符可以替换单个if/else子句。运算符有三个表达式。如果第一个求值为true,则返回第二个表达式,如果为false,则返回第三个。

// Ternary operator expression
$y = ($x == 1) ? 1 : 2;

在 PHP 中,这个操作符可以用作表达式和语句。

// Ternary operator statement
($x == 1) ? $y = 1 : $y = 2;

编程术语表达式指的是计算出一个值的代码,而语句是以分号或右花括号结束的代码段。

匹配表达式

PHP 8 增加了一个新的条件语句,叫做匹配表达式。它对 switch 语句进行了一些改进,包括更简洁的语法。考虑下面的 switch 语句。

switch ($x)
{
  case 1: $output = 'True' break;
  case 2: $output = 'False'; break;
  default: $output = 'Unknown';
}

该语句可以重写为如下所示的匹配表达式。请注意,没有使用 break 关键字,表达式以分号结束。

$output = match($x) {
    1 => 'True',
    2 => 'False',
    default => 'Unknown'
};
echo $output;

每种情况下只允许一个表达式。返回值可以忽略,因此前一个示例也可以用下面的方式键入。

match($x) {
  1 => $output = 'True',
  2 => $output = 'False',
  default => $output = 'Unknown'
};
echo $output;

switch cases 使用松散比较(==)进行匹配,而 match expression cases 使用严格比较(===)进行匹配。这有助于避免微妙的转换错误。

$x = '1'; // string type
switch ($x)
{
  case 1: $output = 'integer'; break;
  case '1': $output = 'string';
}
echo $output; // "integer"

$output = match($x) {
  1 => 'integer',
  '1' => 'string',
};
echo $output; // "string"

要以相同方式处理的条件可以组合在同一行上,用逗号分隔。请记住,匹配表达式必须有匹配条件或默认大小写。否则会发生错误,代码停止执行。

$x = 4;
match($x) {
  1, 2, 3 => echo 'case 1, 2, or 3'
};
// Fatal error: Uncaught UnhandledMatchError

七、循环

PHP 中有四种循环结构。这些用于多次执行特定的代码块。

While 循环

只有当条件为真时,while循环才会遍历代码块。只要条件保持为真,它就继续循环。注意,条件只在每次迭代(循环)开始时检查。

$i = 0;
while ($i < 10) { echo $i++; } // 0-9

正如条件 if 语句一样,如果代码块中只有一条语句,则可以省略循环的花括号。

$i = 0;
while ($i < 10) echo $i++; // 0-9

Do-while 循环

除了检查代码块之后的条件之外,do-while循环的工作方式与while循环相同。因此,它总是至少在代码块中运行一次。请记住,这个循环以分号结束。

$i = 0;
do { echo $i++; } while ($i < 10); // 0-9

For 循环

for循环用于遍历一个代码块特定的次数。它使用三个参数。第一个参数初始化一个计数器,并且总是在循环之前执行一次。第二个参数保存循环的条件,并在每次迭代之前进行检查。第三个参数包含计数器的增量,在每次迭代结束时执行。

for ($i = 0; $i < 10; $i++) { echo $i; } // 0-9

for循环有多种变化,因为任何一个参数都可以省略。例如,如果省略第一个和第三个参数,它的行为方式与while循环相同。

$i = 0;
for (;$i < 10;) { echo $i++; }

第一个和第三个参数也可以使用逗号运算符(,)拆分成几个语句。

for ($i = 0, $x = 9; $i < 10; $i++, $x--) {
  echo $x; // 9-0
}

函数的作用是:获取数组中元素的数量。与for循环一起,它可以用来遍历一个数值数组。

$a = array(1,2,3);

for($i = 0; $i < sizeof($a); $i++) {
  echo $a[$i]; // "123"
}

如果不需要跟踪迭代,foreach循环提供了更清晰的语法。这个循环对于遍历关联数组也是必要的。

Foreach 循环

foreach循环提供了一种简单的方法来遍历数组。在每次迭代中,数组中的下一个元素被赋给指定的变量(迭代器),循环继续执行,直到遍历完整个数组。

$a = array(1,2,3);

foreach ($a as $v) {
  echo $v; // "123"
}

有一个对foreach循环的扩展,通过在迭代器前添加一个键变量,后跟双箭头操作符(=>)来获得键的名称或索引。

$a = array('one' => 1, 'two' => 2);

foreach ($a as $k => $v) {
  echo "$k = $v "; // "one = 1 two = 2 "
}

替代语法

与条件语句一样,循环中的括号可以用冒号和endwhileendforendforeach关键字之一重写为替代语法。

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

for ($i = 0; $i < 10; $i++): echo $i; endfor;

$a = array(1,2,3);
foreach ($a as $v): echo $v; endforeach;

这样做的主要好处是提高了可读性,尤其是对于较长的循环。

破裂

有两个特殊的关键字可以在循环中使用— breakcontinue。关键字break结束一个循环结构的执行。

for ($i = 0; $i < 5; $i++)
{
  if ($i == 3) break; // end loop
  echo $i; // "012"
}

可以给它一个数值参数,指定要跳出多少个嵌套循环结构。

$i = 0;
while ($i++ < 10)
{
  for (;;) { break 2; } // end for and while
}

继续

可以在任何循环语句中使用continue关键字来跳过当前循环的剩余部分,并在下一次迭代的开始处继续。

for ($i = 0; $i < 5; $i++)
{
  if ($i == 2) continue; // start next iteration
  echo $i; // "0134"
}

这个关键字可以接受一个参数,指定它应该跳到多少个封闭循环的末尾。

$i = 0;
while ($i++ < 10)
{
  for (;;) { continue 2; } // start next while iteration
}

与许多其他语言相比,continue语句也适用于开关,其行为与break相同。因此,要从开关内部跳过一次迭代,需要使用continue 2

$i = 0;
while ($i++ < 10)
{
  switch ($i)
  {
    case 1: continue 2; // start next while iteration
  }
}

转到

PHP 5.3 中引入的第三个跳转语句是goto,它执行到指定标签的跳转。标签是一个名称后跟一个冒号(:)。

goto myLabel; // jump to label
myLabel: // label declaration

目标标签必须在相同的脚本文件和范围内。因此,goto不能用来跳转到循环结构中,只能跳出它们。

loop:
while (!$finished)
{
  // ...
  if ($try_again) goto loop; // restart loop
}

一般来说,最好避免使用goto语句,因为它会使执行流程难以跟踪。

八、函数

函数是可重用的代码块,只在被调用时执行。它们允许代码被分成更小的部分,更容易理解和重用。

定义函数

要创建一个函数,需要使用function关键字,后跟一个名称、一组括号和一个代码块。函数的命名约定 1 与变量的相同——使用一个描述性的名称,除了第一个单词以外,每个单词的首字母都大写。

function myFunc()
{
  echo 'Hello World';
}

一个函数代码块可以包含任何有效的 PHP 代码,包括其他函数定义。

调用函数

一旦定义了一个函数,就可以在页面上的任何地方调用它,方法是在它的名字后面加上一组括号。

myFunc(); // "Hello World"

函数名不区分大小写,但是使用与它们定义中相同的大小写是一个好习惯。

myfunc(); // "Hello World"
MYFUNC(); // "Hello World"

即使函数定义出现在脚本文件的更下面,也可以调用函数。

foo(); // allowed
function foo() {}

一个例外是,只有在满足特定条件时才定义函数。该条件代码必须在调用函数之前执行。

bar(); // error
if (true) { function bar() {} }
bar(); // ok

函数参数

函数名后面的括号用于向函数传递参数。为此,必须首先在函数定义中以逗号分隔的变量列表的形式指定相应的参数。然后可以在函数中使用这些参数。

function myFunc($x, $y)
{
  echo $x . $y;
}

使用指定的参数,可以使用相同数量的参数调用该函数。

myFunc('Hello', ' World'); // "Hello World"

准确地说,参数出现在函数定义中,而参数出现在函数调用中。然而,这两个术语有时会被错误地互换使用。

可选参数

可以通过在参数列表中为参数赋值来指定默认值。然后,如果在调用函数时未指定该参数,则使用默认值。为使其按预期工作,有默认值的参数声明在没有默认值的参数的右边是很重要的。

function myFunc($x, $y = ' Earth')
{
  echo $x . $y;
}

myFunc('Hello'); // "Hello Earth"

命名参数

PHP 8 引入了命名参数,通过指定相应参数的名称,允许参数以任意顺序传递。该特性通过允许跳过默认值来补充可选参数。

function myNamed($a, $b = 2, $c = 4)
{
  echo "$a $b $c";
}

myNamed(c: 3, a: 1); // "1 2 3"

可选参数和必需参数都可以命名,但是任何命名的参数都必须放在未命名的参数之后。

myNamed(1, c: 3); // "1 2 3"

可变参数列表

调用函数时不能使用少于其声明中指定的参数,但可以使用更多的参数。这允许传递可变数量的参数,然后可以使用几个内置函数来访问这些参数。为了一次得到一个参数,有一个func_get_arg函数。此函数采用单个参数,即要返回的参数,从零开始。

function myArgs()
{
  $x = func_get_arg(0);
  $y = func_get_arg(1);
  $z = func_get_arg(2);
  echo $x . $y . $z;
}

myArgs('Fee', 'Fi', 'Fo'); // "FeeFiFo"

还有两个函数与参数列表相关。函数func_num_args获取传递的参数数量,函数func_get_args返回一个包含所有参数的数组。它们可以一起用于允许函数处理可变数量的参数。

function myArgs2()
{
  $num = func_num_args();
  $args = func_get_args();
  for ($i = 0; $i < $num; $i++)
    echo $args[$i];
}

myArgs2('Fee', 'Fi', 'Fo'); // "FeeFiFo"

PHP 5.6 简化了可变参数列表的使用。从这个版本开始,参数列表可能包含一个可变参数,由省略号(...)标记表示,它接受可变数量的参数。可变参数表现为数组,并且必须始终是列表中的最后一个参数。

function myArgs3(...$args)
{
  foreach($args as $v) {
    echo $v;
  }
}

myArgs3(1, 2, 3); // "123"

作为一个补充功能,省略号还可以用来将一组值解包到一个参数列表中。

$a = [1, 2, 3];
myArgs3(...$a); // "123"

返回语句

return是一个跳转语句,它使函数结束执行并返回到它被调用的位置。

function myFunc()
{
  return; // exit function
  echo 'Hi'; // never executes
}

可以选择给它一个要返回的值,在这种情况下,它让函数调用计算该值。

function myFunc()
{
  // Exit function and return value
  return 'Hello';
}

echo myFunc(); // "Hello"

没有返回值的函数会自动返回 null。

function myNull() {}

if (myNull() === null)
  echo 'true'; // "true"

范围和寿命

通常,PHP 变量的作用域从声明它的地方开始,一直持续到页面的末尾。然而,在函数中引入了局部函数作用域。默认情况下,函数中使用的任何变量都被限制在这个局部范围内。一旦函数的作用域结束,局部变量就被销毁。

$x = 'Hello'; // global variable

function myFunc()
{
  $y = ' World'; // local variable
}

在 PHP 中,试图从一个函数中访问一个全局变量不起作用,而是创建一个新的局部变量。为了使一个全局变量可访问,该变量的范围必须通过用关键字global声明来扩展到函数。

$x = 'Hello'; // global $x

function myFunc()
{
  global $x; // use global $x
  $x .= ' World'; // change global $x
}

myFunc();
echo $x; // "Hello World"

从全局范围访问变量的另一种方法是使用预定义的$GLOBALS数组。变量通过其名称引用,名称被指定为不带美元符号的字符串。

function myFunc()
{
  $GLOBALS['x'] .= ' World'; // change global $x
}

与许多其他语言相比,控制结构(如循环和条件语句)没有自己的变量范围。因此,在这样的代码块中定义的变量在代码块结束时不会被销毁。

if(true)
{
  $x = 10; // global $x
}

echo $x; // "10"

除了全局和局部变量,PHP 还有属性变量;这些将在下一章讨论。

匿名函数

PHP 5.3 引入了匿名 函数,允许函数作为参数传递并赋给变量。匿名函数的定义类似于常规函数,只是它没有指定的名称。可以使用正常的赋值语法(包括分号)将函数赋给变量。这个变量可以作为一个函数来调用。

$say = function($name)
{
  echo "Hello " . $name;
};

$say("World"); // "Hello World"

匿名函数主要用作回调函数。这是一个作为参数传递给另一个函数的函数,该函数应该在执行过程中调用它。

function myCaller($myCallback)
{
  echo $myCallback();
}

// "Hello"
myCaller( function() { echo "Hello"; } );

通过这种方式,可以将函数注入到现有函数中,从而增加其通用性。例如,内置的array_map函数将其回调应用于给定数组的每个元素。

$a = [1, 2, 3];

$squared = array_map(function($val)
{
  return $val * $val;
}, $a);

foreach ($squared as $v)
  echo $v; // "149"

使用匿名函数的一个好处是,它们提供了一种简洁的方法来定义那些只在使用位置使用一次的函数。这也防止了这种一次性函数搞乱全局范围。

关闭

闭包是一个匿名函数,它可以捕获创建它的作用域的局部变量。在 PHP 中,所有的匿名函数都是闭包,它们是使用 Closure 类实现的。他们可以在函数头中用一个use子句指定要捕获的变量。

$x = 1, $y = 2;

$myClosure = function($z) use ($x, $y)
{
  return $x + $y + $z;
};

echo $myClosure(3); // "6"

箭头函数

PHP 7.4 引入了一种更简洁的匿名函数语法,称为箭头函数。与匿名函数一样,arrow 函数也是使用 Closure 类实现的,并支持相同的功能,不同之处在于 arrow 函数通过值自动从父作用域中捕获变量。因此,前面的例子可以简化如下。

$x = 1, $y = 2;
$myClosure = fn($z) => $x + $y + $z;
echo $myClosure(3); // "6"

箭头函数的主要优点是简洁,所以在指定匿名函数的参数时,它们使用保留关键字fn而不是function。他们也只允许一个单一的,隐式返回的表达式,所以他们不使用return关键字。

发电机

发生器是用于产生一系列值的函数。每个值都用一个yield语句返回。与 return 不同,yield语句保存函数的状态,允许它在被再次调用时从停止的地方继续。

function getNum()
{
  for ($i = 0; $i < 5; $i++) {
    yield $i;
  }
}

生成器函数的行为类似于迭代器;因此,它可以与foreach循环一起使用。循环继续进行,直到生成器不再产生更多的值。

foreach(getNum() as $v)
  echo $v; // "01234"

PHP 5.5 中引入了生成器。它们的使用在 PHP 7 中通过yield from语句得到了扩展,它允许一个生成器从另一个生成器、迭代器或数组中产生值。

function countToFive()
{
  yield 1;
  yield from [2, 3, 4];
  yield 5;
}

foreach (countToFive() as $v)
  echo $v; // "12345"

由于生成器只在需要时一次产生一个值,所以它们不需要一次性计算整个序列并存储在内存中。在生成大量数据时,这会带来显著的性能优势。

内置函数

PHP 自带了大量的内置函数,这些函数总是可用的,比如字符串和数组处理函数。其他函数取决于 PHP 编译时使用的扩展,例如,用于与 MySQL 数据库通信的 MySQLi 扩展。有关内置 PHP 函数的完整参考,请参见 PHP 函数参考。 2

九、类

一个是一个用来创建对象的模板。要定义一个,关键字class后跟一个名称和一个代码块。类的命名约定是混合使用大小写,这意味着每个单词最初都应该大写。

class MyRectangle {}

类体可以包含属性和方法。属性是保存对象状态的变量,而方法是定义对象能做什么的函数。属性在其他语言中也被称为字段属性。在 PHP 中,它们需要明确指定访问级别。在下面的例子中,使用了public访问级别,它提供了对属性的无限制访问。

class MyRectangle
{
  public $x, $y;
  function newArea($a, $b) { return $a * $b; }
}

为了从类内部访问成员,$this伪变量与单箭头操作符(->)一起使用。$this变量是对类的当前实例的引用,只能在对象上下文中使用。没有它,$x$y只会被视为局部变量。

class MyRectangle
{
  public $x, $y;

  function newArea($a, $b) {
    return $a * $b;
  }

  function getArea() {
    return $this->newArea($this->x, $this->y);
  }
}

实例化对象

要从封闭类的外部使用类的成员,必须首先创建该类的对象。这是使用new关键字完成的,它创建一个新的对象或实例。

$r = new MyRectangle(); // object instantiated

对象包含自己的一组属性,这些属性可以保存与类的其他实例不同的值。与函数一样,即使类定义出现在脚本文件的更下面,也可以创建类的对象。

$r = new MyDummy(); // allowed
class MyDummy {};

访问对象成员

要访问属于某个对象的成员,需要使用单箭头运算符(->)。它可用于调用方法或为属性赋值。

$r->x = 5;
$r->y = 10;
$r->getArea(); // 50

另一种初始化属性的方法是使用初始属性值。

初始属性值

如果一个属性需要有一个初始值,一个简洁的方法是在声明它的同时给它赋值。然后在创建对象时设置这个初始值。这种类型的赋值必须是常量表达式。例如,它不能是变量或数学表达式。

class MyRectangle
{
  public $x = 5, $y = 10;
}

构造器

一个类可以有一个构造函数,这是一个用来初始化(构造)对象的特殊方法。此方法提供了一种初始化属性的方法,这种方法不限于常量表达式。在 PHP 中,构造函数以两个下划线开头,后跟单词construct。像这样的方法被称为魔术方法

class MyRectangle
{
  public $x, $y;

  function __construct()
  {
    $this->x = 5;
    $this->y = 10;
    echo "Constructed";
  }
}

当创建此类的新实例时,调用构造函数,在此示例中,该构造函数将属性设置为指定的值。请注意,任何初始属性值都是在运行构造函数之前设置的。

$r = new MyRectangle(); // "Constructed"

因为这个构造函数不带参数,所以可以选择省略括号。

$r = new MyRectangle; // "Constructed"

就像任何其他方法一样,构造函数可以有一个参数列表。它可用于将属性值设置为创建对象时传递的参数。

class MyRectangle
{
  public $x, $y;

  function __construct($x, $y)
  {
    $this->x = $x;
    $this->y = $y;
  }
}

$r = new MyRectangle(5,10);

PHP 8 引入了构造函数属性提升,允许在构造函数参数列表中声明属性。任何以访问级别(如 public)为前缀的构造函数参数都将提升为新属性。这使得前面的类可以用下面不太冗长的方式重写。

class MyRectangle
{
  function __construct(public $x, public $y)
  {
    $this->x = $x;
    $this->y = $y;
  }
}

默认参数可以以与其他函数和方法相同的方式用于构造函数。提升的和非提升的参数都可以有默认值。请记住,默认值被分配给参数,而不是提升的属性。提升的属性需要在构造函数中赋值,就像这里所做的一样。

class MyRectangle
{
  public $x;
  function __construct($x = 5, public $y = 10)
  {
    $this->x = $x;
    $this->y = $y;
  }

}
$r = new MyRectangle;
echo $r->x + $r->y; // "15"

破坏者

除了构造函数,类也可以有析构函数。这个魔术方法以两个下划线开始,后跟单词destruct。一旦不再有对该对象的引用,就在 PHP 垃圾收集器销毁该对象之前调用它。

class MyRectangle
{
  // ...
  function __destruct() { echo "Destructed"; }
}

为了测试析构函数,可以使用unset函数来手动移除对象的所有引用。

$r = new MyRectangle;
unset($r); // "Destructed"

请记住,PHP 5 完全重写了对象模型。因此,类的许多特性,如析构函数,在早期版本的语言中不起作用。

区分大小写

变量名区分大小写,而 PHP 中的类名不区分大小写——函数名、关键字和内置构造(如echo)也是如此。这意味着名为MyClass的类也可以被引用为myclassMYCLASS

class MyClass {}
$o1 = new myclass(); // ok
$o2 = new MYCLASS(); // ok

对象比较

当对对象使用等于运算符(==)时,如果对象是同一类的实例,并且它们的属性具有相同的值和类型,则这些对象被认为是相等的。相反,只有当变量引用同一个类的同一个实例时,严格等于运算符(===)才返回true

class Flag
{
  public $flag = true;
}

$a = new Flag();
$b = new Flag();

$c = ($a == $b);  // true (same values)
$d = ($a === $b); // false (different instances)

匿名类

PHP 7 引入了对匿名类的支持。当只需要一个一次性的对象时,这样的类可以代替命名类。

$obj = new class {};

匿名类的实现以及由此创建的对象与命名类没有什么不同。例如,他们可以像使用任何命名类一样使用构造函数。

$o = new class('Hi')
{
  public $x;
  public function __construct($a)
  {
    $this->x = $a;
  }
};

echo $o->x; // "Hi";

闭包对象

PHP 中的匿名函数也是闭包,因为它们能够从函数范围之外捕获上下文。除了变量,这个上下文也可以是一个对象的范围。这创建了一个所谓的闭包对象,它可以访问该对象的属性。使用bindTo方法创建一个对象闭包。该方法接受两个参数:闭包所绑定到的对象和与之关联的类范围。若要访问非公共成员(私有成员或受保护成员),必须将类或对象的名称指定为第二个参数。

class C { private $x = 'Hi'; }

$getC = function() { return $this->x; };
$getX = $getC->bindTo(new C, 'C');
echo $getX(); // "Hi"

这个例子使用了两个闭包。第一个闭包$getC定义了检索属性的方法。第二个闭包$getX$getC的副本,对象和类范围已经绑定到它。PHP 7 简化了这一点,它提供了一种更简洁、性能更好的临时绑定方法,然后在同一个操作中调用一个闭包。

// PHP 7
$getX = function() { return $this->x; };
echo $getX->call(new C); // "Hi"

十、继承

继承允许一个类获取另一个类的成员。在下面的例子中,Square类继承自Rectangle,由extends关键字指定。Rectangle然后成为Square的父类,后者又成为Rectangle的子类。除了自己的成员,Square还获得了Rectangle中所有可访问(非私有)的成员,包括任何构造函数。

// Parent class (base class)
class Rectangle
{
  public $x, $y;
  function __construct($a, $b)
  {
    $this->x = $a;
    $this->y = $b;
  }
}

// Child class (derived class)
class Square extends Rectangle {}

当创建Square的实例时,现在必须指定两个参数,因为Square已经继承了Rectangle的构造函数。

$s = new Square(5,10);

Rectangle继承的属性也可以从Square对象中访问。

$s->x = 5;
$s->y = 10;

PHP 中的类只能从一个父类继承,并且在脚本文件中父类必须在子类之前定义。

重写成员

子类中的成员可以重新定义其父类中的成员,从而为其提供新的实现。要覆盖继承的成员,只需要用相同的名称重新声明它。如下所示,Square构造函数覆盖了Rectangle中的构造函数。

class Square extends Rectangle
{
  function __construct($a)
  {
    $this->x = $a;
    $this->y = $a;
  }
}

使用这个新的构造函数,只使用一个参数来创建Square

$s = new Square(5);

因为Rectangle的继承构造函数被覆盖,所以在创建Square对象时不再调用Rectangle的构造函数。如有必要,由开发人员调用父构造函数。这是通过在调用前面加上parent关键字和一个双冒号来实现的。双冒号被称为范围解析操作符 ( ::)。

class Square extends Rectangle
{
  function __construct($a)
  {
    parent::__construct($a,$a);
  }
}

parent关键字是父类名的别名,也可以使用。在 PHP 中,使用这种表示法可以访问继承层次结构中任意级别的被覆盖成员。

class Square extends Rectangle
{
  function __construct($a)
  {
    Rectangle::__construct($a,$a);
  }
}

像构造函数一样,如果父析构函数被重写,它不会被隐式调用。它也必须从子析构函数中用parent::__destruct()显式调用。

最终关键字

为了阻止子类重写方法,可以将其定义为final。类本身也可以定义为 final,以防止任何类扩展它。

final class NotExtendable
{
  final function notOverridable() {}
}

运算符的实例

作为一项安全预防措施,您可以通过使用instanceof操作符来测试一个对象是否可以被转换为一个特定的类。如果左边的对象可以被投射到右边的type而不会导致错误,那么这个操作符返回true。当对象是右侧类的实例或从右侧类继承时,情况确实如此。

$s = new Square(5);
$s instanceof Square; // true
$s instanceof Rectangle; // true

十一、访问级别

每个类成员都有一个可访问性级别,它决定了该成员在哪里可见。PHP 中有三个可用的:publicprotectedprivate

class MyClass
{
  public    $myPublic;    // unrestricted access
  protected $myProtected; // enclosing or child class
  private   $myPrivate;   // enclosing class only
}

私有访问

无论访问级别如何,所有成员都可以在声明它们的类(封闭类)中访问。这是唯一可以访问私有成员的地方。

class MyClass
{
  public    $myPublic    = 'public';
  protected $myProtected = 'protected';
  private   $myPrivate   = 'private';

  function test()
  {
    echo $this->myPublic;    // allowed
    echo $this->myProtected; // allowed
    echo $this->myPrivate;   // allowed
  }
}

与属性不同,方法不必指定显式的访问级别。除非设置为其他级别,否则它们默认为公共访问。

受保护的访问

受保护的成员可以从声明它们的类内部访问,也可以从扩展它们的任何类访问。

class MyChild extends MyClass
{
  function test()
  {
    echo $this->myPublic;    // allowed
    echo $this->myProtected; // allowed
    echo $this->myPrivate;   // inaccessible
  }
}

公共访问

公共成员可以不受限制地访问。除了可以访问受保护成员的任何地方之外,还可以通过对象变量访问公共成员。

$m = new MyClass();
echo $m->myPublic;    // allowed
echo $m->myProtected; // inaccessible
echo $m->myPrivate;   // inaccessible

做个关键词

在 PHP 5 之前,var关键字用于声明属性。为了保持向后兼容性,这个关键字仍然是可用的,并提供公共访问,就像public修饰符一样。

class MyVars
{
  var $x, $y; // deprecated property declaration
}

对象访问

在 PHP 中,同一类的对象可以访问彼此的私有和受保护成员。这种行为不同于许多其他不允许这种访问的编程语言。

class MyClass
{
  private $myPrivate;

  function setPrivate($obj, $val) {
    $obj->myPrivate = $val; // set private property

  }
}
$a = new MyClass();
$b = new MyClass();
$a->setPrivate($b, 10);

访问级别指南

作为指南,在选择访问级别时,通常最好尽可能使用最严格的级别。这是因为成员可以被访问的位置越多,它可以被错误访问的位置就越多,这使得代码更难调试。使用限制性访问级别还使得修改该类变得更加容易,而不会破坏使用该类的任何其他开发人员的代码。

附件

应该避免使用公共属性,因为它暴露了一个类的内部,这使得将来对该类的更改更加困难。一种更好的方法是通过 get 和 set 访问器方法提供对此类属性的访问。get 访问器检索属性,通常以“get”前缀命名。同样,set 访问器设置属性的值,通常以“set”前缀命名。

class Time
{
  private $minutes;

  function getMinutes() {
    return $this->minutes;
  }

  function setMinutes($val) {
    $this->minutes = $val;
  }
}

通过不直接公开属性,该类更加灵活。例如,该类中表示时间的底层属性可以从分钟更改为秒,而不会破坏使用该类的任何人的代码。set 访问器还可以验证输入,以验证它是该属性的有效值,从而使该类使用起来更安全。

class Time
{
  private $seconds;

  function getMinutes() {
    return $this->seconds / 60;
  }

  function setMinutes($val) {
   if ($val > 0) { $this->seconds = $val * 60; }
   else { $this->seconds = 0; }
  }
}

访问器的另一个优点是可以限制属性的读写访问。可以通过省略 set 访问器将属性设置为只读,或者通过省略 get 访问器将属性设置为只写。

十二、静态

static关键字可用于声明无需创建类实例就能访问的属性和方法。静态(类)成员只存在于一个副本中,该副本属于类本身,而实例(非静态)成员是作为每个新对象的新副本创建的。

class MyCircle
{
  // Instance members (one per object)
  public $r = 10;
  function getArea() {}

  // Static/class members (only one copy)
  static $pi = 3.14;
  static function newArea($a) {}
}

静态方法不能使用实例成员,因为这些方法不是实例的一部分。但是,它们可以使用其他静态成员。

引用静态成员

与实例成员不同,静态成员不能使用单箭头操作符(->)来访问。相反,要引用一个类中的静态成员,该成员必须以self关键字为前缀,后跟范围解析操作符(::)。self关键字是类名的别名,因此也可以使用类名。

static function newArea($a)
{
  return self::$pi * $a * $a; // ok
  return MyCircle::$pi * $a * $a; // alternative
}

同样的语法也用于从实例方法中访问静态成员。请注意,与静态方法不同,实例方法可以使用静态和实例成员。

function getArea()
{
  return self::newArea($this->r);
}

要从类外部访问静态成员,需要使用类名,后跟范围解析操作符(::)。

class MyCircle
{
  static $pi = 3.14;

  static function newArea($a)
  {
    return self::$pi * $a * $a;
  }
}

echo MyCircle::$pi; // "3.14"
echo MyCircle::newArea(10); // "314"

这里可以看出静态成员的优势;无需创建类的实例就可以使用它们。因此,如果方法独立于实例变量执行泛型函数,则应该将其声明为静态的。同样,如果只需要变量的一个实例,属性应该声明为静态的。

静态变量

局部变量可以声明为静态的,以使函数记住它的值。这样的静态变量只存在于局部函数的作用域中,但是当函数结束时,它不会丢失它的值。例如,这可以用来计算一个函数被调用的次数。

function add()
{
  static $val = 0;
  echo $val++;
}

add(); // "0"
add(); // "1"
add(); // "2"

静态变量的初始值只设置一次。请记住,静态属性和静态变量只能用常量初始化,而不能用表达式初始化,如另一个变量或函数返回值。

后期静态绑定

如前所述,self关键字是封闭类的类名的别名。这意味着关键字引用它的封闭类,即使它是从子类的上下文中调用的。

class MyParent
{
  protected static $val = 'parent';

  public static function getVal()
  {
    return self::$val;
  }
}

class MyChild extends MyParent
{
  protected static $val = 'child';
}

echo MyChild::getVal(); // "parent"

要获取类引用以评估实际调用该方法的类,需要使用static关键字而不是self关键字。这个特性被称为后期静态绑定,它是在 PHP 5.3 中添加的。

class MyParent
{
  protected static $val = 'parent';

  public static function getLateBindingVal()
  {
    return static::$val;
  }
}

class MyChild extends MyParent
{
  protected static $val = 'child';
}

echo MyChild::getLateBindingVal(); // "child"

十三、常量

一个常量是一个变量,它的值不能被脚本改变。因此,必须在创建常量的同时分配这样的值。PHP 提供了两种创建常量的方法:const修饰符和define函数。

常量

const修饰符用于创建类常量。与常规属性不同,类常量没有指定的访问级别,因为它们总是公开可见的。它们也不使用美元符号解析器标记($)。常量的命名约定都是大写,用下划线分隔每个单词。

class MyCircle
{
  const PI = 3.14;
}

常量在创建时必须赋值。像静态属性一样,常量只能用常量值初始化,而不能用表达式或变量初始化。类常量的引用方式与静态属性相同,只是它们不使用美元符号。

echo MyCircle::PI; // "3.14"

const修饰符可能不适用于局部变量或参数。但是,从 PHP 5.3 开始,const可以用来创建全局常量。这种常量是在全局范围内定义的,可以在脚本中定义它之后的任何地方访问。

const PI = 3.14;
echo PI; // "3.14"

规定

define函数可以创建全局和局部常量,但不能创建类常量。这个函数的第一个参数是常量的名称,第二个参数是它的值。

define('DEBUG', 1);

就像用const创建的常量一样,define常量在使用时没有美元符号,它们的值不能修改。

echo DEBUG; // "1"

像用const生成的常量一样,define的值可以是任何标量数据类型:整数、浮点数、字符串或布尔值。然而,与const不同的是,define函数允许在赋值中使用表达式,比如变量或数学表达式的结果。

define('ONE', 1);     // 1
define('TWO', ONE+1); // 2

默认情况下,常量区分大小写。然而,define函数接受第三个可选参数,该参数可以设置为true来创建一个不区分大小写的常量。

define('DEBUG', 1, true);
echo debug; // "1"

要检查常量是否已经存在,可以使用defined功能。该功能适用于用constdefine创建的常量。

if (!defined('PI'))
  define('PI', 3.14);

PHP 7 使得使用define函数创建常量数组成为可能。对用const创建的常量数组的支持从 PHP 5.6 开始就存在了。

const CA = [1, 2, 3];    // PHP 5.6 or later
define('DA', [1, 2, 3]); // PHP 7 or later

构建和定义

const修饰符创建了一个编译时常量,所以编译器用它的值替换了常量的所有用法。相反,define创建一个运行时常量,直到运行时才设置。这就是为什么定义常量可以被赋予表达式值,而const需要在编译时已知的常量值。

const PI = 3.14;   // compile-time constant
define('E', 2.72); // run-time constant

只有const可用于类常量,只有define可用于局部常量。然而,当创建全局常量时,constdefine都是允许的。在这些情况下,使用const通常更好,因为编译时常量比运行时常量稍快。主要的例外是当常量是有条件定义的,或者需要一个表达式值时,在这种情况下必须使用define

不变准则

通常,如果常量的值不需要更改,那么创建常量而不是变量是一个好主意。这确保了脚本中的变量不会被错误地更改,从而有助于防止错误。

魔法常量

PHP 提供了八个预定义的常量,如表 13-1 所示。这些被称为魔法常量,因为它们的值会随着使用场合的不同而变化。

表 13-1

魔法常量

|

名字

|

描述

|
| --- | --- |
| __ 行 _ _ | 文件的当前行号。 |
| FILE | 文件的完整路径和文件名。 |
| DIR | 文件的目录。 |
| __ 函数 _ _ | 函数名。 |
| CLASS | 包括命名空间的类名。 |
| __ 性状 _ _ | 包括名称空间的性状名称。 |
| __ 方法 _ _ | 类方法名。 |
| __ 名称空间 _ _ | 当前名称空间。 |

幻常量对于调试特别有用。例如,__LINE__的值取决于它出现在脚本中的哪一行。

if(!isset($var))
{
  echo '$var not set on line ' . __LINE__;
}

十四、接口

接口指定使用该接口的类必须实现的方法。它们用interface关键字定义,后跟一个名称和一个代码块。一个常见的命名约定是以一个小“i”开始命名接口,然后让每个单词最初大写。

interface iMyInterface {}

接口签名

接口的代码块可以包含实例方法的签名。这些方法不能有任何实现。相反,他们的身体被分号代替。接口方法必须总是公共的。

interface iMyInterface
{
  public function myMethod();
}

此外,接口可以定义常量。这些接口常量的行为就像类常量一样,只是它们不能被重写。

interface iMyInterface
{
  const PI = 3.14;
}

一个接口可能不从一个类继承,但它可能从另一个接口继承,这有效地将两个接口合并为一个。

interface i1 {}
interface i2 extends i1 {}

界面示例

下面的例子展示了一个名为iComparable的接口,它有一个名为Compare的方法。请注意,该方法利用类型提示来确保使用正确的类型调用该方法。这一功能将在后面的章节中介绍。

interface iComparable
{
  public function compare(iComparable $o);
}

Circle类通过在类名后使用implements关键字,后跟接口名来实现这个接口。如果这个类也有一个extends子句,那么implements子句需要放在它的后面。请记住,尽管一个类只能从一个父类继承,但它可以通过在逗号分隔的列表中指定接口来实现任意数量的接口。

class Circle implements iComparable
{
  public $r;
}

因为Circle实现了iComparable,所以它必须定义compare()方法。对于此类,该方法返回圆半径之间的差值。除了具有与接口中定义的方法相同的签名之外,实现的方法必须是公共的。它也可以有更多的参数,只要它们是可选的。

class Circle implements iComparable
{
  public $r;

  public function compare(iComparable $o)
  {
    return $this->r - $o->r;
  }
}

接口用法

接口允许类设计的多重继承,而没有与允许功能的多重继承相关的复杂性。要求特定类设计的主要好处可以从iComparable接口中看出,它定义了类可以共享的特定功能。它允许开发人员使用接口成员,而不必知道类的实际类型。为了说明这一点,下面的例子展示了一个简单的方法,它接受两个iComparable对象并返回最大的一个。

function largest(iComparable $a, iComparable $b)
{
  return ($a->compare($b) > 0) ? $a : $b;
}

这个方法适用于任何两个实现了iComparable接口的相同类型的对象。不管对象是什么类型,它都可以工作,因为该方法只使用通过该接口公开的功能。

界面指南

接口为没有任何实现的类提供设计。它是一个契约,通过它实现它的类同意提供某些功能。这有两个好处。首先,它提供了一种方式来确保开发人员实现某些方法。第二,因为这些类保证有某些方法,所以即使不知道类的实际类型也可以使用它们,这使得代码更加灵活。

十五、抽象

抽象类提供了部分实现,其他类可以在此基础上构建。当一个类被声明为抽象类时,这意味着除了正常的类成员之外,该类还可以包含必须在子类中实现的不完整方法。

抽象方法

在抽象类中,任何方法都可以被声明为抽象的。这些方法没有实现,只指定了它们的签名,而它们的代码块被分号替换。

abstract class Shape
{
  abstract public function myAbstract();
}

抽象示例

举个例子,下面的类有两个属性和一个抽象方法。

abstract class Shape
{
  private $x = 100, $y = 100;
  abstract public function getArea();
}

如果一个类从这个抽象类继承,那么它将被强制重写抽象方法。方法签名必须匹配,但访问级别除外,因为访问级别的限制较少。

class Rectangle extends Shape
{
  public function getArea()
  {
    return $this->x * $this->y;
  }
}

不可能实例化抽象类。他们只是作为其他类的家长,在一定程度上决定他们的执行。

$s = new Shape(); // compile-time error

然而,抽象类可以从非抽象(具体)类继承。

class NonAbstract {}
abstract class MyAbstract extends NonAbstract {}

抽象类和接口

抽象类在许多方面类似于接口。它们都可以定义派生类必须实现的成员签名,并且它们都不能被实例化。关键的区别是,首先,抽象类可以包含非抽象成员,而接口不能。第二,一个类可以实现任意数量的接口,但只能从一个类继承,不管是不是抽象的。

// Defines default functionality and definitions
abstract class Shape
{
  public $x = 100, $y = 100;
  abstract public function getArea();
}
// Class is a Shape
class Rectangle extends Shape { /*...*/ }

// Defines a specific functionality
interface iComparable
{
  function compare(iComparable $o);
}
// Class can be compared
class MyClass implements iComparable { /*...*/ }

抽象指南

抽象类提供了部分实现的基类,指示子类必须如何行为。当子类有一些相似之处,但在需要子类定义的其他实现中有所不同时,它们最有用。就像接口一样,抽象类是面向对象编程中有用的构造,可以帮助开发人员遵循良好的编码标准。

十六、性状

性状是一组可以插入到类中的方法。它们是在 PHP 5.4 中添加的,以支持更大的代码重用,而不会因为允许多重继承而增加复杂性。性状是用关键字trait定义的,后跟一个名称和一个代码块。命名约定通常与类相同,每个单词最初都是大写的。代码块只能包含静态方法和实例方法。

trait PrintFunctionality
{
  public function myPrint() { echo 'Hello'; }
}

需要 trait 提供的功能的类可以用关键字use包含它,后跟 trait 的名称。然后性状的方法的行为就好像它们是直接在那个类中定义的一样。

class MyClass
{
  // Insert trait methods
  use PrintFunctionality;
}

$o = new MyClass();
$o->myPrint(); // "Hello"

一个类可以使用多个性状,方法是将它们放在一个逗号分隔的列表中。类似地,性状可以由一个或多个其他性状组成。

遗传和性状

Trait 方法覆盖继承的方法。同样,类中定义的方法会覆盖由性状插入的方法。

class MyParent
{
  public function myPrint() { echo 'Base'; }
}

class MyChild extends MyParent
{
  // Overrides inherited method
  use PrintFunctionality;
  // Overrides trait inserted method
  public function myPrint() { echo 'Child'; }
}

$o = new MyChild();
$o->myPrint(); // "Child"

特质指南

单一继承有时会迫使开发人员在代码重用和概念清晰的类层次结构之间做出选择。为了实现更好的代码重用,可以将方法移动到类层次结构的根附近,但是这样一来,类就开始有了它们不需要的方法,这就降低了代码的可理解性和可维护性。另一方面,在类层次结构中加强概念上的整洁常常会导致代码重复,这可能会导致不一致。Traits 通过支持独立于类层次结构的代码重用,提供了一种避免单一继承缺点的方法。

十七、导入文件

同一个代码经常需要在多个页面上调用。这可以通过首先将代码放在一个单独的文件中,然后使用include语句包含该文件来实现。该语句获取指定文件中的所有文本,并将其包含在脚本中,就像代码已被复制到该位置一样。就像echoinclude是一个特殊的语言构造,而不是一个函数,所以不应该使用括号。

<?php
include 'myfile.php';
?>

当包含一个文件时,解析在目标文件的开头变为 HTML 模式,在结尾再次恢复 PHP 模式。因此,包含文件中需要作为 PHP 代码执行的任何代码都必须包含在 PHP 标记中。

<?php
// myfile.php
?>

包括路径

一个include文件可以被指定一个相对路径、一个绝对路径或者没有路径。相对文件路径相对于导入文件的目录。一个绝对文件路径包括完整的文件路径。

// Relative path
include 'myfolder/myfile.php';

// Absolute path
include 'C:/xampp/htdocs/myfile.php';

请注意,在 Windows 和 Linux 下,正斜杠“/”用作目录分隔符。至于大小写,默认情况下,文件路径在 Windows 上不区分大小写,在 Linux 上区分大小写。

当指定相对路径或未指定路径时,include首先在当前工作目录中搜索文件,默认为导入脚本的目录。如果在那里没有找到文件,include在失败前检查php.ini中定义的include_path1指令指定的文件夹。

// No path
include 'myfile.php';

除了include之外,还有另外三种语言结构可以将一个文件的内容导入到另一个文件中:requireinclude_oncerequire_once

需要

require构造包含并评估指定的文件。它与include相同,除了它如何处理故障。当文件导入失败时,require会暂停脚本并显示错误,而include只会发出警告。导入失败的原因可能是找不到文件,或者是运行 web 服务器的用户没有读取该文件的权限。

require 'myfile.php'; // halt on error

一般来说,对于任何复杂的 PHP 应用或 CMS 站点,最好使用require。这样,当密钥文件丢失时,应用不会试图运行。对于不太重要的代码段和简单的 PHP 网站来说,include可能就足够了,在这种情况下,即使包含的文件丢失,PHP 也会显示输出。

包含一次

include_once语句的行为类似于include,除了如果指定的文件已经被包含,它将不再被包含。

include_once 'myfile.php'; // include only once

需要一次

require_once语句的工作方式类似于require,但是如果文件已经被导入,它就不会导入该文件。

require_once 'myfile.php'; // require only once

在脚本的特定执行过程中,如果一个文件可能被多次导入,那么最好使用include_oncerequire_once语句。例如,这避免了由函数和类重定义引起的错误。

返回

可以在导入的文件中执行return语句。这将停止执行并返回到调用文件导入的脚本。

<?php
// myimport.php
return 'OK';
?>

如果指定了返回值,import 语句将计算该值,就像普通函数一样。

<?php
// myfile.php
if ((include 'myimport.php') == 'OK')
  echo 'OK';
?>

_ 自动装载

对于大型 web 应用,每个脚本中所需的包含数量可能相当大。这可以通过定义一个__autoload函数来避免。当一个未定义的类或接口被用来加载那个定义时,这个函数被自动调用。它接受一个参数,即 PHP 正在寻找的类或接口的名称。

function __autoload($class_name)
{
  include $class_name . '.php';
}

// Attempt to auto include MyClass.php
$obj = new MyClass();

编写面向对象的应用时,一个好的编码实践是为每个类定义准备一个源文件,并根据类名来命名文件。遵循这个约定,__autoload函数能够加载这个类——只要它与需要它的脚本文件在同一个文件夹中。

<?php
// MyClass.php
class MyClass {}
?>

如果文件位于子文件夹中,类名可以包含下划线字符来表示。然后需要在 __ autoload函数中将下划线字符转换成目录分隔符。

十八、类型声明

类型声明允许为属性、函数参数和函数返回类型指定类型。这允许 PHP 引擎强制使用指定的类型。

参数类型声明

PHP 的早期版本完全依赖于适当的函数文档,以便开发人员知道函数接受什么参数。为了让函数更加健壮,PHP 5 开始引入参数类型声明,允许指定函数参数的类型。表 18-1 显示了类型声明的有效类型,以及添加了这些类型的 PHP 版本。

表 18-1

类型声明

|

名字

|

描述

|

版本

|
| --- | --- | --- |
| 类别名 | 参数必须是该类的对象或子对象。 | PHP 5.0 |
| 接口名称 | 值必须是实现此接口的对象。 | PHP 5.0 |
| Self | 值必须是定义该方法的类的实例,或者是它的一个子类。仅适用于类和实例方法。 | PHP 5.0 |
| parent | 值必须是定义该方法的类的父类的实例,或者是它的一个子类。仅适用于类和实例方法。 | PHP 5.0 |
| array | 值必须是数组。 | PHP 5.1 |
| iterable | 值必须是实现可遍历接口的数组或对象。它可以与 foreach 循环一起使用。 | PHP 5.1 |
| callable | 值必须可以作为函数调用。 | PHP 5.4 |
| Bool | 值必须为 true 或 false。 | PHP 7.0 |
| Float | 值必须是浮点数。 | PHP 7.0 |
| Int | 值必须是整数。 | PHP 7.0 |
| string | 值必须是字符串。 | PHP 7.0 |
| mixed | 允许的任何值。 | PHP 8.0 |

通过在函数签名中将类型作为参数的前缀来设置类型声明。下面是一个使用 PHP 5.1 中引入的array伪类型的例子。

function myPrint(array $a)
{
  foreach ($a as $v) { echo $v; }
}

myPrint( array(1,2,3) ); // "123"

不满足类型提示会导致致命错误。这为开发人员提供了一种快速检测何时使用了无效参数的方法。

myPrint('Test'); // error

PHP 5.4 中增加了callable伪类型。有了这个类型提示,参数必须是可调用的函数、方法或对象。不允许使用像echo这样的语言结构,但是可以使用匿名函数,如下例所示。

function myCall(callable $callback, $data)
{
  $callback($data);
}

$say = function($s) { echo $s; };
myCall( $say, 'Hi' ); // "Hi";

要将一个方法作为一个callback函数传递,对象和方法名都需要作为一个数组组合在一起。

class MyClass {
  function myCallback($s) {
    echo $s;
  }
}

$o = new MyClass();
myCall( array($o, 'myCallback'), 'Hi' ); // "Hi"

PHP 7 中增加了标量类型的类型声明,包括 bool、int、float 和 string。下面是一个使用 bool 类型的简单示例。

function isTrue(bool $b)
{
  return ($b === true);
}

echo isTrue(true);  // "1"
echo isTrue(false); // ""

对于依赖于特定类型参数的函数,最好使用类型声明。这样,如果这个函数被错误地传递了一个不正确类型的参数,它会立即触发一个错误。如果没有类型声明,函数可能会无声无息地失败,使错误更加难以检测。

返回类型声明

PHP 7 中增加了对返回类型声明的支持,作为防止意外返回值的一种方式。返回类型在参数列表后声明,以冒号(:)为前缀。允许与参数类型声明相同的类型。

function f(): array {
  return [];
}

当与接口一起使用时,类型声明强制实现类匹配相同的类型声明。

interface I {
  static function myArray(array $a): array;
}

class C implements I {
  static function myArray(array $a): array {
    return $a;
  }
}

从 PHP 7.1 开始,void类型可以用作返回类型声明,指定函数不返回值。这样的函数可以完全省略 return 语句,或者使用空的 return 语句来退出函数。

function myFunc(): void {
  return; // empty return statement
}

PHP 8 在方法允许的返回类型列表中增加了static。这种方法必须返回定义该方法的类的对象。

class MyTest

{
  public function getNewObject(): static
  {
    return new MyTest();
  }
}

严格打字

PHP 中的默认行为是试图将不正确类型的标量值转换成期望的类型。例如,需要字符串的函数仍然可以用整数参数调用,因为整数可以转换成字符串。

function showString(string $s) {
  echo $s;
}

showString(5); // "5"

通过将以下声明作为特定源文件中的第一条语句,可以在该文件中启用强类型检查。

declare(strict_types=1);

这将影响标量类型的参数和返回类型声明,它们必须与函数中声明的类型完全相同。

showString(5); // Fatal error: Uncaught TypeError

可空类型

从 PHP 7.1 开始,一个类型声明可以通过在类型前加一个问号来标记为可空。除了其指定类型的任何常规值之外,可空类型还允许保存 null。

function nullOrInt(?int $i)
{
  echo $i === null ? 'null' : 'int';
}

echo nullOrInt(3); // "int"
echo nullOrInt(null); // "null"

工会类型

PHP 8 引入了联合类型,允许一个类型声明包含多个类型。声明联合类型时,每个允许的类型由竖线(|)分隔。

function squared(int|float $i): int|float
{
  return $i ** 2;
}

echo squared(2); // "4"
echo squared(1.5); // "2.25"

null这样的伪类型和像truefalse这样的子类型也可以用来组成联合类型。

function trueOrNull(true|null $j): true|null
{
  return $j === null ? true : null;
}

属性类型声明

PHP 7.4 中增加了类型化属性,允许属性指定它可以保存什么类型的值。在声明这样的属性时,可以使用任何类型(除了callable and void)。

class MyClass
{
  public int $a = 3;
}

如此处所示,PHP 引擎将阻止其他类型被赋给类型化属性。

$o = new MyClass;
$o->a = 5; // allowed
$o->a = 'string'; // fatal error: TypeError

未赋值的常规非类型化属性将为 null。相比之下,类型化属性将是未初始化的,这是一种不同于 null 的新的变量状态。

class MyType
{
  public int $a; // uninitialized
  public $b; // null
}

添加了未初始化,因为否则就不可能区分设置为 null 的可空属性和错误地未赋值的属性。从 PHP 8 开始,试图读取未初始化的类型化属性会导致致命错误。

$t1 = new MyType;
$o->t1 = 5;
$i = $o->t1; // ok

$t2 = new MyType;
$i = $o->t2; // error

十九、类型转换

给定变量的使用环境,PHP 会根据需要自动转换变量的数据类型。因此,很少需要显式类型转换。尽管如此,变量或表达式的类型可以通过执行显式类型转换来更改。

显式强制转换

显式强制转换是通过将所需的数据类型放在要计算的变量或值前面的括号中来执行的。在下面的示例中,显式强制将 bool 变量计算为 int。

$myBool = false;
$myInt = (int)$myBool; // 0

当 bool 变量作为输出发送到页面时,可以看到显式强制转换的一种用法。由于自动类型转换,false值变成空字符串;因此,它不会显示。首先将它转换成一个整数,false值显示为0

echo $myBool;      // ""
echo (int)$myBool; // "0"

表 19-1 中列出了允许的造型。

表 19-1

允许的类型转换

|

名字

|

描述

|
| --- | --- |
| (整数),(整数) | 转换为整数。 |
| (布尔值)、(布尔值) | 转换为布尔型。 |
| (浮点)、(双精度)、(实数) | 抛来抛去。 |
| (字符串) | 转换为字符串。 |
| (数组) | 转换为数组。 |
| (对象) | 转换为对象。 |
| (未设置) | 转换为 null。 |

举几个例子,数组转换将标量类型转换为只有一个元素的数组。它执行与使用数组构造函数相同的功能。

$myInt = 10;
$myArr = (array)$myInt;
$myArr = array($myInt); // same as above
echo $myArr[0]; // "10"

如果一个标量类型(比如 int)被转换成 object,它就成为内置stdClass类的一个实例。变量值存储在这个类的一个属性中,称为scalar

$myObj = (object)$myInt;
echo $myObj->scalar; // "10"

未设置的强制转换使变量的值为 null。尽管它的名字,它实际上并没有取消变量的设置。强制转换仅仅是为了完整性而存在,因为 null 被认为是一种数据类型。它在 PHP 7.2 中被弃用,不应再使用。

$myNull = (unset)$myInt;
$myNull = null; // same as above

集合类型

显式强制转换不会改变它前面的变量的类型,只会改变它在表达式中的求值方式。要改变变量的类型,可以使用settype函数,它有两个参数。第一个是要转换的变量,第二个是以字符串形式给出的数据类型。

$myVar = 1.2; // float variable
settype($myVar, 'int'); // convert variable to int

或者,可以通过将显式强制转换的结果存储回同一个变量来执行类型转换。

$myVar = 1.2;
$myVar = (int)$myVar; // 1

获取类型

settype相关的是gettype函数,它以人类可读的字符串形式返回所提供参数的类型。

$myBool = true;
echo gettype($myBool); // "boolean"

二十、变量测试

作为一种以 web 为中心的语言,在 PHP 中处理用户提供的数据是很常见的。在使用此类数据之前,需要对其进行测试,以确认其存在并具有有效值。PHP 为此提供了许多内置的构造。

没错

如果变量存在并且被赋予了一个非空值,那么isset语言构造返回true

isset($a); // false

$a = 10;
isset($a); // true

$a = null;
isset($a); // false

空的

empty构造检查指定的变量是否有空值——比如 null、0、false 或空字符串——如果是,则返回 true。如果变量不存在,它也返回true

empty($b); // true

$b = false;
empty($b); // true

Is_null

is_null构造可以用来测试一个变量是否被设置为空。

$c = null;
is_null($c); // true

$c = 10;
is_null($c); // false

从 PHP 8 开始,如果变量不存在,is_null构造会发出一个错误,因为它不应该与未初始化的变量一起使用。在 PHP 8 之前,is_null返回 true,并附带一个非致命错误通知。

// Prior to PHP 8
is_null($d); // true (undefined variable notice)

// As of PHP 8
is_null($d); // error (TypeError)

针对 null 的严格相等检查在功能上等同于使用is_null构造。使用该操作符通常是首选,因为它可读性更好,速度也稍快,因为它不涉及函数调用开销。

$c = null;
$c === null; // true

未设置

了解另一个有用的语言构造是unset,它从当前作用域中删除一个变量。

$e = 10;
unset($e); // delete $e

当通过使用global关键字在函数中访问一个全局变量时,这段代码实际上在$GLOBALS数组中创建了一个对全局变量的局部引用。因此,试图在函数中取消设置全局变量只会删除局部引用。要从函数的作用域中删除全局变量,必须直接在$GLOBALS数组上取消设置。

$o = 5; // global variable

function myUnset()
{
  // Make $o a reference to $GLOBALS['o']
  global $o;

  // Remove the local reference variable
  unset($o);

  // Remove the global variable
  unset($GLOBALS['o']);
}

取消设置变量与将变量设置为 null 略有不同。当一个变量被设置为 null 时,该变量仍然存在,但是它保存的内容立即被释放。相反,不设置变量会删除变量,但在垃圾收集器清除它之前,内存仍被认为是在使用中。撇开性能问题不谈,推荐使用unset,因为它使代码的意图更加清晰。

$var = null; // free memory
unset($var); // delete variable

请记住,大多数情况下,没有必要手动取消设置变量,因为当变量超出范围时,PHP 垃圾收集器会自动删除变量。但是,如果服务器执行内存密集型任务,那么手动取消设置这些变量将允许服务器在耗尽内存之前处理更多的并发请求。

零合并算子

PHP 7 中添加了空合并操作符(??),作为使用三元组和isset的常见情况的快捷方式。如果存在并且不为空,则返回第一个操作数;否则,它返回第二个操作数。

$x = null;
$name = $x ?? 'unknown'; // "unknown"

该语句相当于下面的三元运算,它使用了isset构造。

$name = isset($x) ? $x : 'unknown';

PHP 7.4 增加了空合并赋值操作符(??=)。该操作符提供了一种简洁的方法,仅当变量未赋值时才给变量赋值(null)。它也可以用于未定义的变量。

// Assign value if $name is unassigned
$name ??= 'unknown';

// Same as above
if(!isset($name)) { $name = 'unknown'; }

// Functionally same as above

$name = $name ?? 'unknown';

空安全运算符

在调用方法之前,有必要首先检查以确保其对象存在。这可以通过使用三元运算符来实现。

$result = $obj ? $obj->myMethod() : null;

PHP 8 引入了 nullsafe 操作符(?->)作为实现这一功能的更简洁的方法。如果对象不存在,操作符返回null

$result = $obj?->myMethod();

运算符可以在调用方法或获取属性时使用,但不能在写入属性时使用。

确定类型

PHP 有几个确定变量类型的有用函数。这些功能可以在表 20-1 中看到。

表 20-1

类型函数

|

名字

|

描述

|
| --- | --- |
| is_array() | 如果变量是数组,则为 True。 |
| is_bool() | 如果变量是布尔值,则为 True。 |
| is_callable() | 如果变量可以作为函数调用,则为 True。 |
| is_float(), is_double(), is_real() | 如果变量是浮点型,则为 True。 |
| is_int(), is_integer(), is_long() | 如果变量是整数,则为 True。 |
| is_null() | 如果变量设置为 null,则为 True。 |
| is_numeric() | 如果变量是数字或数字字符串,则为 True。 |
| is_scalar() | 如果变量是 int、float、string 或 bool,则为 True。 |
| is_object() | 如果变量是对象,则为 True。 |
| is_resource() | 如果变量是资源,则为 True。 |
| is_string() | 如果变量是字符串,则为 True。 |

举个例子,如果参数包含一个数字或一个可以计算为数字的字符串,is_numeric函数将返回true

is_numeric(10.5);   // true  (float)
is_numeric('33');   // true  (numeric string)
is_numeric('text'); // false (non-numeric string)

可变信息

PHP 有三个用于检索变量信息的内置函数:print_rvar_dumpvar_exportprint_r函数以人类可读的方式显示变量值。这对于调试非常有用。

$a = array('one', 'two', 'three');
print_r($a);

前面的代码产生以下输出。

Array ( [0] => one [1] => two [2] => three )

print_r类似的是var_dump,除了数值,还显示数据类型和大小。调用var_dump($a)显示这个输出。

array(3) {
  [0]=> string(3) "one"
  [1]=> string(3) "two"
  [2]=> string(5) "three"
}

最后是var_export函数,它以可以用作 PHP 代码的样式打印变量信息。下图显示了var_export($a)的输出。注意最后一个元素后面的逗号,这是允许的。

array ( 0 => 'one', 1 => 'two', 2 => 'three', )

var_export函数和print_r一起接受可选的第二个布尔参数。当设置为true时,该功能返回输出而不是打印输出。这给了var_export更多的用途,比如与eval语言结构结合使用。这个构造接受一个字符串参数,并将其作为 PHP 代码进行计算。

eval('$b = ' . var_export($a, true) . ';');

使用eval执行任意代码的能力是一个强大的特性,应该小心使用。不应该使用它来执行任何用户提供的数据,至少不应该在没有适当验证的情况下执行,因为这存在安全风险。不鼓励使用eval的另一个原因是,与goto类似,它使得代码的执行更加难以跟踪,这使得调试更加复杂。

二十一、重载

PHP 中的重载提供了在运行时添加对象成员的能力。这是通过让类实现重载方法__get__set__call__callStatic来实现的。请记住,PHP 中重载的含义不同于许多其他语言。

属性重载

__get__set方法提供了一种实现 getter 和 setter 方法的便捷方式,这些方法通常用于安全地读写属性。当使用不可访问的属性时,会调用这些重载方法,因为这些属性没有在类中定义,或者因为它们在当前范围内不可用。在下面的例子中,__set方法将所有不可访问的属性添加到$data数组中,__get安全地从该数组中检索元素。

class MyProperties
{
  private $data = array();

  public function __set($name, $value)
  {
    $this->data[$name] = $value;
  }

  public function __get($name)
  {
    if (array_key_exists($name, $this->data))
      return $this->data[$name];
  }
}

当设置一个不可访问的属性的值时,__set被调用,属性的名称和值作为它的参数。类似地,当访问一个不可访问的属性时,使用属性名作为参数调用__get

$obj = new MyProperties();

$obj->a = 1;  // __set called
echo $obj->a; // __get called

方法重载

有两种方法可以处理对一个类的不可访问方法的调用:__call__callStatic。对于实例方法调用,调用__call方法。

class MyClass
{
  public function __call($name, $args)
  {
    echo "Calling $name $args[0]";
  }
}

// "Calling myTest in object context"
(new MyClass())->myTest('in object context');

__call的第一个参数是被调用方法的名称,第二个参数是包含传递给该方法的参数的数值数组。这些参数对于__callStatic方法是相同的,它处理对不可访问的静态方法的调用。

class MyClass
{
  public static function __callStatic($name, $args)
  {
    echo "Calling $name $args[0]";
  }
}

// "Calling myTest in static context"
MyClass::myTest('in static context');

Isset 和 unset 重载

内置的构造issetemptyunset只作用于显式定义的属性,而不是重载的属性。这个功能可以通过重载__isset__unset方法添加到一个类中。

class MyClass
{
  private $data = array();

  public function __set($name, $value) {
    $this->data[$name] = $value;
  }
  public function __get($name) {
    if (array_key_exists($name, $this->data))
      return $this->data[$name];
  }

  public function __isset($name) {
    return isset($this->data[$name]);
  }

  public function __unset($name) {
    unset( $this->data[$name] );
  }
}

当在不可访问的属性上调用isset时,调用__isset方法。

$obj = new MyClass();
$obj->name = "Joe";

isset($obj->name); // true
isset($obj->age);  // false

当在不可访问的属性上调用unset时,__unset方法处理该调用。

unset($obj->name); // delete property
isset($obj->name); // false

如果同时实现了__isset__get,那么empty构造只对重载属性有效。如果__isset的结果为假,那么empty构造返回true。另一方面,如果__isset返回true,那么empty__get检索属性,并评估它是否有一个被认为是空的值。

empty($obj->name); // false
empty($obj->age);  // true

二十二、魔术方法

有许多方法可以在一个类中实现,以供 PHP 引擎内部调用。这些被称为魔术方法,它们很容易识别,因为它们都以两个下划线开头。表 22-1 列出了到目前为止已经讨论过的魔术方法。

表 22-1

魔术方法

|

名字

|

描述

|
| --- | --- |
| __construct(...) | 创建新实例时调用。 |
| __destruct() | 当对象没有剩余引用时调用。 |
| __call($name, $array) | 在对象上下文中调用不可访问的方法时调用。 |
| __callStatic($name, $array) | 在静态上下文中调用不可访问的方法时调用。 |
| __get($name) | 从不可访问的属性中读取数据时调用。 |
| __set($name, $value) | 将数据写入不可访问的属性时调用。 |
| __isset($string) | 当issetempty用于不可访问的属性时调用。 |
| __unset($string) | 当unset用于不可访问的属性时调用。 |

除此之外,还有六个魔术方法,和其他方法一样,可以在类中实现以提供某些功能。

表 22-2

更多魔术方法

|

名字

|

描述

|
| --- | --- |
| __toString() | 为对象到字符串的转换调用。 |
| __invoke(...) | 调用对象到函数的转换。 |
| __sleep() | 由serialize调用。执行清理任务并返回要序列化的变量数组。 |
| __wakeup() | 由unserialize调用以重建对象。 |
| __set_state($array) | 由var_export调用。该方法必须是静态的,并且其参数包含导出的属性。 |
| __clone() | 在对象被克隆后调用。 |

_toString

当在需要字符串的上下文中使用对象时,PHP 引擎会搜索名为__toString的方法来检索对象的字符串表示。

class MyClass
{
  public function __toString()
  {
    return 'Instance of ' . __CLASS__;
  }
}

$obj = new MyClass();
echo $obj; // "Instance of MyClass"

不可能定义一个对象在作为字符串以外的类型进行计算时的行为。

_invoke

方法允许一个对象被当作一个函数。调用对象时提供的参数被用作__invoke函数的参数。

class MyClass
{
  public function __invoke($arg)
  {
    echo $arg;
  }
}

$obj = new MyClass();
$obj('Test'); // "Test"

对象序列化

序列化是将数据转换为字符串格式的过程。这对于在数据库或文件中存储对象很有用。在 PHP 中,内置的serialize函数执行对象到字符串的转换,而unserialize将字符串转换回原始对象。serialize函数处理除资源类型以外的所有类型,例如,资源类型用于保存数据库连接和文件处理程序。考虑下面这个简单的数据库类。

class MyConnection
{
  public $link, $server, $user, $pass;

  public function connect()
  {
    $this->link = mysql_connect($this->server,
                                $this->user,
                                $this->pass);
  }
}

当这个类被序列化时,数据库连接丢失,保存连接的$link资源类型变量被存储为 null。

$obj = new MyConnection();
// ...

$bin = serialize($obj);   // serialize object
$obj = unserialize($bin); // restore object

为了更好地控制对象数据的序列化和非序列化,这个类可以实现__sleep__wakeup方法。

_sleep

__sleep方法由serialize调用,需要返回一个包含将被序列化的属性的数组。这个数组不能包含私有或受保护的属性,因为serialize不能访问它们。该方法还可以在串行化发生之前执行清理任务,诸如将任何未决数据提交给存储介质。

public function __sleep()
{
  return array('server', 'user', 'pass');
}

注意,属性以字符串形式返回到serialize$link资源类型指针不包含在数组中,因为它无法序列化。要重新建立数据库连接,可以使用__wakeup方法。

_wakeup

对序列化对象调用unserialize会调用__wakeup方法来恢复对象。它不接受任何参数,也不需要返回值。它用于重新建立资源类型变量,以及执行可能需要在对象取消序列化后完成的其他初始化任务。在本例中,它重新建立了 MySQL 数据库连接。

public function __wakeup()
{
  if(isset($this->server, $this->user, $this->$pass))
    $this->connect();
}

请注意,isset构造在这里用多个参数调用,在这种情况下,如果设置了所有参数,它只返回true

设置状态

var_export函数检索可用作有效 PHP 代码的变量信息。在下面的示例中,该函数用于对象。

class Fruit
{
  public $name = 'Lemon';
}

$export = var_export(new Fruit(), true);

因为对象是一个复杂类型,所以没有通用的语法来构造它和它的成员。相反,var_export创建以下字符串。

Fruit::__set_state(array( 'name' => 'Lemon', ))

为了构造对象,这个字符串依赖于为对象定义的静态__set_state方法。如图所示,__set_state方法采用一个包含每个对象属性的键值对的关联数组,包括私有和受保护成员。

static function __set_state(array $array)
{
  $tmp = new Fruit();
  $tmp->name = $array['name'];
  return $tmp;
}

有了在Fruit类中定义的这个方法,导出的字符串现在可以用eval构造来解析,以创建一个相同的对象。

eval('$MyFruit = ' . $export . ';');

对象克隆

将对象赋给变量只会创建对同一对象的新引用。要复制一个对象,可以使用clone操作符。

class Fruit {}

$f1 = new Fruit();
$f2 = $f1;       // copy object reference
$f3 = clone $f1; // copy object

克隆对象时,其属性会复制到新对象中。但是,它可能包含的任何子对象都不会被克隆,因此它们在副本之间共享。这就是__clone方法的用武之地。克隆完成后,在克隆的副本上调用它,它可用于克隆任何子对象。

class Apple {}

class FruitBasket
{
  public $apple;

  function __construct() { $apple = new Apple(); }

  function __clone()
  {
    $this->apple = clone $this->apple

;
  }
}

二十三、用户输入

当一个 HTML 表单被提交给一个 PHP 页面时,数据就可供该脚本使用了。

HTML 表单

HTML 表单有两个必需的属性:actionmethod。action 属性指定提交表单时表单数据传递到的脚本文件。例如,下面的表单向mypage.php脚本文件提交一个名为myString的输入属性。

<?php // myform.php ?>
<!doctype html>
<html>
<body>
  <form action="mypage.php" method="post">
    <input type="text" name="myString">
    <input type="submit">
  </form>
</body>
</html>

表单元素的第二个必需属性指定了发送方法,可以是 GET 或 POST。

通过邮件发送

提交表单时,浏览器加载 mypage.php 并传递表单数据。如果表单是使用 POST 方法发送的,那么接收脚本可以通过$_POST数组获得数据。表单输入属性的名称成为关联数组中的键。

<?php // mypage.php ?>
<!doctype html>
<html>
<body>
  <?php echo $_POST['myString']; ?>
</body>
</html>

用 POST 方法发送的数据在页面的 URL 上是不可见的,但是这也意味着页面的状态不能通过例如给页面添加书签来保存。

使用 GET 发送

POST 的替代方法是用 GET 方法发送表单数据,并使用$_GET数组检索它。然后变量显示在地址栏中,如果页面被书签标记并被重新访问,地址栏会有效地维护页面的状态。

// mypage.php
echo $_GET['myString'];

因为数据包含在地址栏中,所以变量不仅可以通过 HTML 表单传递,还可以通过 HTML 链接传递。然后可以使用$_GET数组相应地改变页面的状态。这提供了一种将变量从一页传递到另一页的方法,如下例所示。

<?php // sender.php ?>
<!doctype html>
<html>
<body>
  <a href="receiver.php?myString=Foo+Bar">link</a>
</body>
</html>

单击此网页上的链接时,receiver.php 文件可以访问传递给它的数据。以下示例显示页面上的数据。

<?php // receiver.php ?>
<!doctype html>
<html>
<body>
  <?php echo $_GET['myString']; // "Foo Bar" ?>
</body>
</html>

请求数组

如果使用 POST 或 GET 方法发送数据并不重要,那么可以使用$_REQUEST数组。这个数组通常包含$_GET$_POST数组,但也可能包含$_COOKIE数组。

echo $_REQUEST['myString']; // "Foo Bar"

数组$_REQUEST的内容可以在 PHP 配置文件 1 中设置,并且在不同的 PHP 发行版之间有所不同。出于安全考虑,通常不包含$_COOKIE数组。

安全问题

任何用户提供的数据都可以被操作;因此,在使用前应该对其进行验证和消毒。验证意味着您要确保数据在数据类型、范围和内容方面符合您的预期。例如,下面的代码验证一个电子邮件地址。

if(!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL))
  echo "Invalid email address";

净化就是禁用用户输入中潜在的恶意代码。这是通过根据使用输入的语言规则对代码进行转义来实现的。例如,如果数据被发送到一个数据库,它需要用mysql_real_escape_string函数清理,以禁用任何嵌入的 SQL 代码。

// Sanitize for database use
$name = mysql_real_escape_string($_POST['name']);

// Execute SQL command
$sql = "SELECT * FROM users WHERE user='" . $name . "'";
$result = mysql_query($sql);

当用户提供的数据作为文本输出到网页时,应该使用htmlspecialchars函数。它禁用任何 HTML 标记,以便显示用户输入,但不解释。

// Sanitize for web page use
echo htmlspecialchars($_POST['comment']);

提交数组

通过在表单中的变量名后加上数组方括号,可以将表单数据分组到数组中。这适用于所有表单输入元素,包括<input><select><textarea>

<input type="text" name="myArr[]">
<input type="text" name="myArr[]">

元素也可以被赋予它们自己的数组键。

<input type="text" name="myArr[name]">

提交后,该数组就可以在脚本中使用了。

$val1 = $_POST['myArr'][0];
$val2 = $_POST['myArr'][1];
$name = $_POST['myArr']['name'];

表单<select>元素有一个允许从列表中选择多个项目的属性。

<select name="myArr[]" size="3" multiple="true">
  <option value="apple">Apple</option>
  <option value="orange">Orange</option>
  <option value="pear">Pear</option>
</select>

当表单中包含此多选元素时,数组括号对于在脚本中检索所选值是必需的。

foreach ($_POST['myArr'] as $item)
  echo $item . ' '; // ex "apple orange pear"

文件上传

HTML 表单提供了一种文件输入类型,允许将文件上传到服务器。为了上传文件,表单的可选属性enctype必须设置为"multipart/form-data",如下例所示。

<form action="mypage.php" method="post"
      enctype="multipart/form-data">
  <input name="myfile" type="file">
  <input type="submit" value="Upload">
</form>

关于上传文件的信息存储在$_FILES数组中。该关联数组的关键字见表 23-1 。

表 23-1

$_FILES 数组的键

|

名字

|

描述

|
| --- | --- |
| 名字 | 上传文件的原始名称。 |
| tmp_name | 临时服务器副本的路径。 |
| 类型 | 文件的 Mime 类型。 |
| 大小 | 以字节表示的文件大小。 |
| 错误 | 错误代码。 |

接收到的文件只是临时存储在服务器上。如果脚本没有保存它,它将被删除。下面显示了一个如何保存文件的简单示例。该示例检查错误代码以确保文件已成功接收,如果已成功接收,则将文件移出临时文件夹进行保存。实际上,您可能还希望检查文件大小和类型,以确定是否要保留该文件。

$dest = 'upload/' . basename($_FILES['myfile']['name']);
$file = $_FILES['myfile']['tmp_name'];
$err  = $_FILES['myfile']['error'];

if($err == 0 && move_uploaded_file($file, $dest))
  echo 'File successfully uploaded';

在这个例子中可以看到两个新函数。move_uploaded_file函数检查以确保第一个参数包含一个有效的上传文件,如果是,它将它移动到 path 并重命名为第二个参数指定的文件名。指定的文件夹必须已经存在,如果函数成功移动文件,则返回true。另一个新功能是basename。它返回路径的文件名部分,包括文件扩展名。

超级全球

正如在本章中看到的,有许多内置的关联数组使外部数据对 PHP 脚本可用。这些数组被称为超级全局变量,因为它们在每个作用域中都自动可用。PHP 中有九个超全局变量,每个都在表 23-2 中有简要描述。

表 23-2

超级全球

|

名字

|

描述

|
| --- | --- |
| $GLOBALS | 包含所有全局变量,包括其他超全局变量。 |
| $_GET | 包含通过 HTTP GET 请求发送的变量。 |
| $_POST | 包含通过 HTTP POST 请求发送的变量。 |
| $_FILES | 包含通过 HTTP POST 文件上传发送的变量。 |
| $_COOKIE | 包含通过 HTTP cookies 发送的变量。 |
| $_SESSION | 包含存储在用户会话中的变量。 |
| $_REQUEST | 包含$_GET$_POST,可能还有$_COOKIE变量。 |
| $_SERVER | 包含有关 web 服务器及其请求的信息。 |
| $_ENV | 包含由 web 服务器设置的所有环境变量。 |

变量$_GET$_POST$_COOKIE$_SERVER$_ENV的内容包含在phpinfo函数生成的输出中。这个函数还显示 PHP 配置文件php.ini的一般设置,以及其他关于 PHP 的信息。

phpinfo(); // display PHP information

二十四、Cookie

一个 cookie 是一个保存在客户电脑上的小文件,可以用来存储与该用户相关的数据。

创建 Cookies

为了创建一个 cookie,使用了setcookie函数。在将任何输出发送到浏览器之前,必须调用此函数。通常用三个参数调用它,这三个参数包含 cookie 的名称、值和到期日期。

setcookie("lastvisit", date("H:i:s"), time() + 60*60);

这里的值是用date函数设置的,它返回一个根据指定的格式字符串格式化的字符串。到期日期以秒为单位,通常相对于通过time函数检索的当前时间(以秒为单位)进行设置。在本例中,cookie 在一小时后过期。

还可以提供两个可选参数:path 和 domain。这些用于限制可以从哪些页面访问 cookie。例如,只有在访问“fr.example.com”子域上的“foo”目录中的页面时,才会发送以下 cookie,例如在访问“fr.example.com/foo/example.php”时。

setcookie("lastvisit",
           date("H:i:s"),
           time() + 60*60,
           '/foo/',
           'fr.example.com');

一旦为用户设置了 cookie,该 cookie 将在用户下次查看同一域上的页面时发送。然后可以通过$_COOKIE数组访问它。

if (isset($_COOKIE['lastvisit']))
  echo "Last visit: " . $_COOKIE['lastvisit'];

删除 Cookies

可以通过使用旧的过期日期重新创建相同的 cookie 来手动删除 cookie。当浏览器关闭时,它将被删除。

setcookie("lastvisit", 0, 0);

二十五、会话

一个会话提供了一种跨多个网页访问变量的方法。与 cookies 不同,会话数据存储在服务器上。

开始会话

使用session_start功能开始一个会话。该函数必须在任何输出发送到网页之前出现。

<?php session_start(); ?>

session_start函数在客户端的计算机上设置一个 cookie,包含一个用于将客户端与会话相关联的 id。如果客户端已经有一个正在进行的会话,该函数将恢复该会话,而不是开始一个新的会话。

会话数组

会话启动后,$_SESSION数组用于存储会话数据以及检索会话数据。例如,页面视图计数用以下代码存储。第一次查看页面时,session 元素被初始化为 1。

if(isset($_SESSION['views']))
  $_SESSION['views'] += 1;
else
  $_SESSION['views'] = 1;

现在,只要在页面顶部调用session_start,就可以从域中的任何页面上检索这个元素。

echo 'Views: ' . $_SESSION['views'];

删除会话

保证会话持续到用户离开网站。之后,垃圾收集器可以删除该会话。要手动删除会话变量,可使用unset功能。为了删除所有会话变量,有一个session_destroy函数。

unset($_SESSION['views']); // destroy session variable
session_destroy(); // destroy session

会话和 Cookies

会话和 cookies 都用于跨站点页面存储数据。通常,会话用于在单次访问期间临时存储用户数据。另一方面,Cookies 用于长期存储数据,例如,保存用户的登录状态。请记住,cookies 存储在客户端,而会话数据存储在服务器端。因此,Cookie 数据可能会被恶意用户篡改,因此不应在 cookie 中存储任何密码或其他敏感数据。

二十六、命名空间

命名空间提供了一种避免命名冲突并将命名空间成员分组到一个层次结构中的方法。任何代码都可以包含在一个名称空间中,但是只有四种代码结构受到影响:类、接口、函数和常量。

创建名称空间

不包含在名称空间中的构造属于全局名称空间。

// Global code/namespace
class MyClass {}

要将该构造分配给另一个名称空间,需要定义一个名称空间指令。namespace 指令下的任何代码构造都属于该命名空间。名称空间的一个常见命名约定是全部使用小写字母。

namespace my;

// Belongs to my namespace
class MyClass {}

包含命名空间代码的脚本文件必须在任何其他代码、标记或空白之前,在文件顶部声明命名空间。Declare 语句是一个例外,因为它们必须放在名称空间声明之前。

<?php
namespace my;
class MyClass {}
?>
<html><body></body></html>

嵌套命名空间

名称空间可以嵌套任意多级,以进一步定义名称空间层次结构。像 Windows 中的目录和文件一样,名称空间及其成员用反斜杠字符分隔。

namespace my\sub;
class MyClass {} // my\sub\MyClass

替代语法

或者,可以用其他编程语言中常用的括号语法来定义名称空间。正如常规语法一样,在名称空间之外不能存在任何文本或代码。

<?php
namespace my
{
  class MyClass {}
?>
<html><body></body></html>
<?php }?>

可以在同一个文件中声明多个名称空间,尽管这被认为不是良好的编码实践。如果全局代码与命名空间代码结合使用,那么必须使用括号中的语法。然后,全局代码被封装在一个未命名的命名空间块中。

// Namespaced code
namespace my
{
  const PI = 3.14;
}

// Global code
namespace
{
  echo my\PI; // "3.14"
}

与其他 PHP 构造不同,同一个名称空间可以在多个文件中定义。这允许名称空间成员跨多个文件拆分。

引用命名空间

命名空间成员有三种引用方式:完全限定、限定和非限定。完全限定名总是可以使用的。它由全局前缀运算符(\)组成,后跟名称空间路径和成员。全局前缀运算符指示路径相对于全局名称空间。

namespace my
{
  class MyClass {}
}

namespace other
{
  // Fully qualified name
  $obj = new \my\MyClass();
}

限定名包括命名空间路径,但不包括全局前缀运算符。因此,只有在层次结构中当前命名空间之下的命名空间中定义了所需成员时,才能使用它。

namespace my
{
  class MyClass {}
}

namespace
{
  // Qualified name
  $obj = new my\MyClass();
}

成员名或非限定名只能在定义该成员的命名空间中使用。

namespace my
{
  class MyClass {}

  // Unqualified name
  $obj = new MyClass();
}

非限定类名和接口名只能解析为当前命名空间。相反,如果当前名称空间中不存在未限定的函数或常量,它们将尝试解析为同名的任何全局函数或常量。

namespace
{
  function myPrint() { echo 'global'; }
}

namespace my
{
  // Falls back to global namespace
  myPrint(); // "global"
}

或者,全局前缀运算符可用于显式引用全局成员。如果当前名称空间包含同名的函数,这将是必要的。

namespace my
{
  function myPrint() { echo 'local'; }
  \myPrint(); // "global"
  myPrint();  // "local"
}

命名空间别名

别名缩短了限定名以提高源代码的可读性。类、接口和命名空间的名称可以缩短。别名是用一个use指令定义的,该指令必须放在文件最顶层范围的名称空间名称之后。

namespace my;
class MyClass {}

namespace foo;
use my\MyClass as MyAlias;
$obj = new MyAlias();

使用带括号的语法,任何use指令都放在最顶层作用域中的左花括号之后。

namespace foo;
{
  use my\MyClass as MyAlias;
  $obj = new MyAlias();
}

可以选择省略as子句,以当前名称导入成员。

namespace foo;
use \my\MyClass;
$obj = new MyClass();

不可能批量导入另一个命名空间的成员。但是,在同一个use语句中导入多个成员有一个语法上的捷径。

namespace foo;
use my\Class1 as C1, my\Class2 as C2;

PHP 7 进一步简化了这种语法,允许将use声明放在花括号中。

namespace foo;
use my\{ Class1 as C1, Class2 as C2 };

除了类、接口和名称空间,PHP 5.6 还扩展了use结构来支持函数和常量别名。这些分别用use函数和use const构造导入。

namespace my\space {
  const C = 5;
  function f() {}
}

namespace {
  use const my\space\C;
  use function my\space\f;
}

请记住,别名仅适用于定义它们的脚本文件。因此,导入的文件不会继承父文件的别名。

名称空间关键字

在全局代码中,namespace关键字可以用作计算当前名称空间的常量或空字符串。它可以用于显式引用当前的名称空间。

namespace my\name
{
  function myPrint() { echo 'Hi'; }
}
namespace my
{
  namespace\name\myPrint(); // "Hi"
  name\myPrint(); // "Hi"
}

命名空间指南

随着 web 应用中组件数量的增长,名称冲突的可能性也在增加。解决这个问题的一个方法是在名称前加上组件的名称。但是,这会产生长名称,降低源代码的可读性。出于这个原因,PHP 5.3 引入了名称空间,允许开发人员将每个组件的代码分组到单独命名的容器中。

二十七、引用

引用是一个别名,允许两个不同的变量写入相同的值。可以对引用执行三种操作:按引用赋值、按引用传递和按引用返回。

通过引用分配

通过在要绑定的变量前放置一个&符号(&)来分配引用。

$x = 5;
$r = &$x; // r is a reference to x
$s =& $x; // alternative syntax

然后,该引用成为该变量的别名,可以完全像原始变量一样使用。

$r = 10; // assign value to $r/$x
echo $x; // "10"

通过引用传递

在 PHP 中,默认情况下函数参数通过值传递。这意味着传递了变量的本地副本;所以如果副本被更改,不会影响原始变量。

function myFunc($x) { $x .= ' World'; }

$x = 'Hello';
myFunc($x); // value of x is passed
echo $x; // "Hello"

要允许函数修改参数,它必须通过引用传递。这是通过在函数定义中的参数名称前添加一个&符号来实现的。

function myFunc(&$x) { $x .= ' World'; }

$x = 'Hello';
myFunc($x); // reference to x is passed
echo $x; // "Hello World"

默认情况下,对象变量也通过值传递。但是,实际传递的是指向对象数据的指针,而不是数据本身。因此,对对象成员的更改会影响原始对象,但用赋值运算符替换对象变量只会创建一个局部变量。

class MyClass { public $x = 1; }

function modifyVal($o)
{
  $o->x = 5;
  $o = new MyClass(); // new local object
}

$o = new MyClass();
modifyVal($o); // pointer to object is passed
echo $o->x; // "5"

相反,当对象变量通过引用传递时,不仅可以更改其属性,还可以替换整个对象,并将更改传播回原始对象变量。

class MyClass { public $x = 1; }

function modifyRef(&$o)
{
  $o->x = 5;
  $o = new MyClass(); // new object
}

$o = new MyClass();
modifyRef($o); // reference to object is passed
echo $o->x; // "1"

通过引用返回

通过引用返回函数,可以从函数中为变量赋值引用。返回引用的语法是在函数名前放置&符号。与通过引用传递相反,当调用函数绑定引用时,也使用&符号。

class MyClass
{
  public $val = 10;

  function &getVal()
  {
    return $this->val;
  }
}

$obj = new MyClass();
$myVal = &$obj->getVal();

请记住,引用不应该仅仅出于性能原因而使用,因为 PHP 引擎会自行处理这种优化。仅当需要引用类型的行为时才使用引用。

二十八、高级变量

除了作为数据的容器之外,PHP 变量还有其他的特性,这将在本章中讨论。这些都是不常用的功能,但了解这些功能是有好处的。

卷曲语法

变量名可以用花括号括起来显式指定。这就是所谓的卷曲复杂语法。为了说明这一点,即使变量出现在单词中间,下面的代码也会输出该变量。

$fruit = 'Apple';
echo "Two {$fruit}s"; // "Two Apples"

更重要的是,对于从表达式中形成变量名来说,花语法很有用。考虑下面的代码,它使用 curly 语法来构造三个变量的名称。

for ($i = 1; $i <= 3; $i++)
  ${'x'.$i} = $i;

echo "$x1 $x2 $x3"; // "1 2 3"

这里需要使用花语法,因为需要对表达式求值以形成有效的变量名。如果表达式只有一个变量,则不需要花括号。

for ($i = 'a'; $i <= 'c'; $i++)
  $$i = $i;

echo "$a $b $c"; // "a b c"

这种语法在 PHP 中被称为变量变量。

变量变量名

可变变量是一个可以通过代码改变名称的变量。例如,考虑下面的常规变量。

$a = 'foo';

这个变量的值可以用作变量名,方法是在它前面加一个美元符号。

$$a = 'bar';

$a的值foo现在成为了$$a变量的另一个名字。

echo $foo; // "bar"
echo $$a;  // "bar"

这种用法的一个例子是从数组中生成变量。

$arr = array('a' => 'Foo', 'b' => 'Bar');

foreach ($arr as $key => $value)
{
  $$key = $value;
}

echo "$a $b"; // "Foo Bar"

可变函数名

通过在变量后放置括号,其值被计算为函数的名称。

function myPrint($s) { echo $s; }

$func = 'myPrint';
$func('Hello'); // "Hello"

这种行为不适用于内置语言结构,比如echo

echo('Hello');  // "Hello"

$myecho = 'echo';
$myecho('Hello'); // error

可变类名

类似于变量函数名,类可以使用字符串变量来引用。这个功能是 PHP 5.3 中引入的。

class MyClass {}

$classname = 'MyClass';
$obj = new $classname();

通过字符串和字符串变量访问代码实体的机制也适用于类或实例的成员。

class MyClass
{
  public $myProperty = 10;
}

$obj = new MyClass();
echo $obj->{'myProperty'}; // "10"

二十九、错误处理

错误是开发人员需要修复的代码中的错误。当 PHP 中出现错误时,默认行为是在浏览器中显示错误消息。此消息包括文件名、行号和错误描述,以帮助开发人员纠正问题。

虽然编译和解析错误通常很容易检测和修复,但运行时错误可能更难发现,因为它们可能只在某些情况下发生,并且原因超出了开发人员的控制。考虑下面的代码,它试图使用fopen函数打开一个文件进行读取。

$handle = fopen('myfile.txt', 'r');

它依赖于这样一个假设,即所请求的文件将一直存在。如果由于某种原因,文件不在那里或者不可访问,该函数将生成一个错误。

Warning: fopen(myfile.txt):
failed to open stream: No such file or directory in C:\xampp\htdocs\mypage.php on line 2

一旦检测到错误,就应该纠正它,即使它只发生在异常情况下。

纠正错误

有两种方法可以纠正这个错误。第一种方法是在尝试打开文件之前进行检查,以确保文件可以被读取。PHP 方便地为这个任务提供了is_readable函数,如果指定的文件存在并且可读,这个函数将返回true

if (is_readable('myfile.txt'))
  $handle = fopen('myfile.txt', 'r');

第二种方法是使用错误控制操作符(@)。当前置到一个表达式时,该运算符禁止显示可能由该表达式生成的错误信息。PHP 8 改变了这个操作符的行为,因此只有非致命的错误消息会被屏蔽。

$handle = @fopen('myfile.txt', 'r');

要确定文件是否成功打开,需要检查返回值。查看文档, 1 可以发现fopen在出错时返回false

if ($handle === false)
{
  echo 'File not found.';
}

如果不是这样,那么可以用fread函数读取文件的内容。该函数从第一个参数中给出的文件处理程序中读取第二个参数中指定的字节数。

else
{
  // Read the content of the whole file
  $content = fread($handle, filesize('myfile.txt'));

  // Close the file handler
  fclose($handle);
}

一旦不再需要文件处理程序,最好用fclose关闭它;虽然,PHP 会在脚本完成后自动关闭文件。

误差等级

PHP 提供了几个内置常量来描述不同的错误级别。表 29-1 包括一些更重要的。

表 29-1

误差等级

|

名称

|

描述

|
| --- | --- |
| E_ERROR | 致命的运行时错误。执行停止。 |
| E_WARNING | 非致命运行时错误。 |
| E_NOTICE | 关于可能错误的运行时通知。 |
| E_USER_ERROR | 用户生成的致命错误。 |
| E_USER_WARNING | 用户生成的非致命警告。 |
| E_USER_NOTICE | 用户生成的通知。 |
| E_COMPILE_ERROR | 致命的编译时错误。 |
| E_PARSE | 编译时分析错误。 |
| E_STRICT | 建议更改以确保向前兼容。 |
| E_ALL | PHP 5.4 之前的所有错误,除了E_STRICT。 |

前三个级别代表 PHP 引擎生成的运行时错误。以下是触发这些错误的一些操作示例。

// E_NOTICE (<PHP8) – Use of undefined variable
$a = $x;

// E_WARNING – Missing file
$b = fopen('missing.txt', 'r');

// E_ERROR – Missing function
$c = missing();

PHP 8 将几个警告和通知重新分类为更严重的异常,这将在下一章讨论。例如,从 PHP 8 开始,使用未定义的变量会导致Error类型的异常。

错误处理环境

PHP 为设置错误处理环境提供了一些配置指令。error_reporting函数设置 PHP 通过内部错误处理程序报告哪些错误。错误级别常量具有位掩码值。这允许使用按位运算符对它们进行组合和相减,如下所示。

error_reporting(E_ALL | ~E_NOTICE); // all but E_NOTICE

错误报告级别也可以在php.ini中永久更改。从 PHP 8 开始,php.ini中的缺省值是E_ALL,所以所有的错误信息都会显示出来。这是开发过程中的一个很好的设置,可以通过在脚本的开头放置下面一行代码以编程方式进行设置。注意,为了向后兼容,可以添加E_STRICT,因为这个错误级别直到 PHP 5.4 才包含在E_ALL中。

// During development
error_reporting(E_ALL | E_STRICT);

当 web 应用上线时,原始的错误消息应该对用户隐藏起来。这是通过display_errors指令完成的。它确定内部错误处理程序是否将错误打印到网页上。默认值是打印它们,但是当网站运行时,最好隐藏任何潜在的原始错误消息。

// During production
ini_set('display_errors','0');

另一个与错误处理环境相关的指令是log_errors指令。它设置是否在服务器的错误日志中记录错误消息。默认情况下,该指令是禁用的,但是在开发过程中启用它来跟踪错误是一个好主意。

// During development
ini_set('log_errors','1');

ini_set功能设置配置选项的值。或者,这些选项都可以在php.ini配置文件中永久设置,而不是在脚本文件中。

自定义错误处理程序

内部错误处理程序可以用自定义错误处理程序重写。这是处理错误的首选方法,因为它允许您抽象原始错误,并向最终用户提供友好的自定义错误消息。

使用set_error_handler函数定义自定义错误处理程序。该函数接受两个参数:一个在出现错误时调用的回调函数,以及该函数处理的错误级别(可选)。

set_error_handler('myError', E_ALL | E_STRICT);

如果没有指定错误级别,错误处理器被设置为处理所有错误,包括E_STRICT。然而,用户定义的错误处理程序实际上只能处理运行时错误,并且只能处理除E_ERROR之外的运行时错误。请记住,对error_reporting设置的更改不会影响自定义错误处理程序,只会影响内部错误处理程序。

回调函数需要两个参数:错误级别和错误描述。可选参数包括文件名、行号和错误上下文,错误上下文是一个数组,包含触发错误的范围内的每个变量。

function myError($errlvl, $errdesc, $errfile, $errline)
{
  switch($errlvl)
  {
    case E_USER_ERROR:
      error_log("Error: $errdesc", 1, 'me@example.com');
      require_once('my_error_page.php');
      return true;
  }
  return false;
}

此示例函数处理级别为E_USER_ERROR的错误。出现这种错误时,会向指定的地址发送一封电子邮件,并显示一个自定义错误页面。通过从函数中返回其他错误的false,它们将由内部错误处理程序来处理。

引发错误

PHP 提供了用于引发错误的trigger_error函数。它有一个必需的参数,即错误消息,还有一个可选的参数,用于指定错误级别。误差等级必须是三个E_USER等级之一,默认等级为E_USER_NOTICE

if( !isset($myVar) )
  trigger_error('$myVar not set'); // E_USER_NOTICE

当您有一个定制的错误处理程序时,触发错误是很有用的,它允许您将定制错误和 PHP 引擎引发的错误的处理结合起来。

三十、异常处理

PHP 5 引入了异常,这是一种内置机制,用于在程序失败发生的上下文中处理程序失败。与通常需要由开发人员修复的错误不同,异常由脚本处理。它们代表了一种不规则的运行时情况,这种情况应该是有可能发生的,并且脚本应该能够自己处理。

Try-catch 语句

为了处理一个异常,必须使用一个try-catch语句来捕获它。该语句由一个包含可能导致异常的代码的try块和一个或多个catch子句组成。

try
{
  $div = invert(0);
}
catch (LogicException $e) {}

如果try块成功执行,程序将在try-catch语句后继续运行。然而,如果出现异常,执行将传递给第一个能够处理该异常类型的catch块。

抛出异常

当出现函数无法恢复的情况时,它可以生成一个异常,通知调用者函数已经失败。这是通过使用关键字throw完成的,后跟一个新的类实例Throwable或者它的一个子类。在下面的例子中,抛出了LogicException类型。它继承自异常类,而异常类又继承自Throwable1

function invert($x)
{
  if ($x == 0) {
    throw new LogicException('Division by zero');
  }
  return 1 / $x;
}

捕捉块

在前面的例子中,catch块被设置为处理内置的LogicException类型。如果try块中的代码可以抛出更多种类的异常,那么可以使用多个catch块,允许以不同的方式处理不同的异常。

catch (LogicException $e) {}
catch (RuntimeException $e) {}
// ...

为了捕捉更具体的异常,需要将catch块放在更一般的异常之前。比如LogicException继承自Exception,所以需要先抓。

catch (LogicException $e) {}
catch (Exception $e) {}

catch子句可以定义一个异常对象。这个对象用于获取关于异常的更多信息,比如使用getMessage方法对异常的描述。

catch (LogicException $e)
{
  echo $e->getMessage(); // "Division by zero"
}

从 PHP 8 开始,如果不需要使用异常对象,可以选择省略该对象。

catch (LogicException) {}

最终阻止

PHP 5.5 引入了finally块,它可以作为try-catch语句的最后一个子句添加。该块用于清理try块中分配的资源。无论是否有异常,它总是会执行。

$resource = myopen();
try { myuse($resource); }
catch(Exception $e) {}
finally { myfree($resource); }

再次引发异常

有时一个异常不能在第一次被捕获的地方被处理。然后可以使用关键字throw后跟异常对象来重新抛出它。

try { $div = invert(0); }
catch (LogicException $e) { throw $e; }

然后,异常沿着调用方堆栈向上传播,直到被另一个try-catch语句捕获。如果异常从未被捕获,它将成为一个级别为E_ERROR的错误,这会暂停脚本,除非定义了一个未被捕获的异常处理程序。

PHP 8 把throw从语句变成了表达式。这使得在任何允许表达式的地方抛出异常成为可能,比如在下面的语句中。

$name = $_GET['name'] ?? throw new Exception('name missing');

未捕获的异常处理程序

set_exception_handler函数允许捕获任何未被捕获的异常。它采用单个参数,即针对此类事件引发的回调函数。

set_exception_handler('myException');

回调函数只需要一个参数,即抛出的异常对象。

function myException($e)
{
  $file = 'exceptionlog.txt';
  file_put_contents($file, $e->getMessage(), FILE_APPEND);
  require_once('my_error_page.php');
  exit;
}

因为这个异常处理程序是在发生异常的上下文之外调用的,所以从异常中恢复会很困难。相反,此示例处理程序将异常写入日志文件并显示错误页面。为了停止脚本的进一步执行,使用了内置的exit构造。它与die构造同义,并且可以选择接受一个字符串参数,该参数在脚本暂停之前打印。

错误和异常

尽管抛出异常是为了由脚本处理,但会生成错误来通知开发人员代码中有错误。当涉及到运行时出现的问题时,异常机制通常被认为是优越的。然而,由于它直到 PHP 5 才被引入,所以直到 PHP 8,所有内部函数都继续使用错误机制。从 PHP 8 开始,许多错误被重新归类为Error异常。例如,在 PHP 8 中被零除会抛出一个DivisionByZeroError异常,而在以前的 PHP 版本中它会触发一个警告。

try {
  echo 1/0; // throws exception in PHP 8
}
catch(DivisionByZeroError $e){
  echo $e->getMessage();
}

Throwable有两个内置子类,Exception 和Error。从 PHP 8 开始,大多数内部函数抛出Error类型的异常,比如TypeErrorValueErrorDivisionByZeroError

对于用户定义的函数,开发人员可以自由选择异常或错误处理机制,但更现代的异常机制是首选。请记住,错误不能被try-catch语句捕获。同样,异常不会触发错误处理程序。

三十一、断言

Assert 是一个调试特性,可以在开发过程中使用,以确保条件始终为真。任何表达式都可以被断言,只要它的计算结果是truefalse

// Make sure $myVar is set
assert(isset($myVar));

像这样的代码断言有助于验证没有违反指定假设的执行路径。如果发生这种情况,就会显示一个错误(或 PHP 8 之前的警告),显示断言的文件和行号,这使得定位和修复代码中的错误变得很容易。

Fatal error: Assertion failed in C:\xampp\htdocs\mypage.php on line 3

可以包括断言的描述,如果断言失败,则显示该描述。PHP 5.4.8 中增加了对第二个参数的支持。

assert(isset($myVar), '$myVar not set');

从 PHP 7 开始,第二个参数也可以是在断言失败时抛出的异常对象。默认情况下,当断言失败时会抛出一个AssertionError

assert(false, new AssertionError('Assert failed'));

资产绩效

通过将ASSERT_ACTIVE选项设置为零,可以使用assert_options函数关闭断言。这意味着在调试完成并且开发代码变成生产代码之后,不需要从代码中移除断言。

// Disable assertions
assert_options(ASSERT_ACTIVE, 0);

在 PHP 7 之前,传递给断言的条件总是被求值,即使断言被关闭。为了避免生产代码中的额外开销,条件可以作为字符串传递,然后由assert进行评估。

assert('isset($myVar)');

在 PHP 7 中,assert 变成了一种语言结构,而不是一种函数,允许在产品代码中包含断言而不会造成任何性能损失。在 PHP 7 中完全跳过断言的方法是在php.ini配置文件中将zend.assertions配置指令设置为-1。在 PHP 8 中不推荐使用带有字符串参数的 assert,也不应该再使用了。

posted @ 2024-08-03 11:23  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报