使用新的 PHP 8 运算符
PHP 8 引入了许多新的运算符。此外,PHP 8 还为这些运算符的使用引入了统一一致的方式。在本节中,我们将考察以下操作符:
-
可变参数运算符
-
空安全操作符
-
连接运算符
-
三元操作符
让我们先来讨论一下可变参数运算符。
使用可变参数运算符
variadics 运算符由普通 PHP 变量(或对象属性)前面的三个前导点(…)组成。该运算符实际上从 PHP 5.6 开始就已经存在了。它也被称为以下内容:
-
溅射(Splat)运算符
-
散点(Scatter)运算符
-
扩散(Spread)算子
在深入了解 PHP 8 对该运算符的改进之前,我们先来看看该运算符通常是如何使用的。
未知参数个数
变量运算符最常见的用途之一,是在定义一个参数数目未知的函数时使用。
在下面的代码示例中,multiVardump()
函数可以接受任意数量的变量。然后,它将 var_export()
的输出连接起来,并返回一个字符串:
// /repo/ch02/php7_variadic_params.php
function multiVardump(...$args) {
$output = '';
foreach ($args as $var)
$output .= var_export($var, TRUE);
return $output;
}
$a = new ArrayIterator(range('A','F'));
$b = function (string $val) { return str_rot13($val); };
$c = [1,2,3];
$d = 'TEST';
echo multiVardump($a, $b, $c);
echo multiVardump($d);
第一次调用函数时,我们提供了三个参数。第二次调用时,我们只提供了一个参数。由于我们使用了变量运算符,因此无需重写函数以适应更多或更少的参数。
有一个 |
清理剩余的参数
variadics 运算符的另一个用途是将剩余的参数真空化。通过这种技术,可以将必选参数与数量未知的可选参数混合起来。
在本例中,where()
函数产生一个 WHERE
子句,并被添加到结构化查询语言(SQL)的 SELECT
语句中。前两个参数是必须的:生成一个没有参数的 WHERE
子句是不合理的!点击此处查看代码:
// ch02/includes/php7_sql_lib.php
// other functions not shown
function where(stdClass $obj, $a, $b = '', $c = '',
$d = '') {
$obj->where[] = $a;
$obj->where[] = $b;
$obj->where[] = $c;
$obj->where[] = $d;
}
使用此函数的调用代码可能如下所示:
// /repo/ch02/php7_variadics_sql.php
require_once __DIR__ . '/includes/php7_sql_lib.php';
$start = '2021-01-01';
$end = '2021-04-01';
$select = new stdClass();
from($select, 'events');
cols($select, ['id', 'event_key',
'event_name', 'event_date']);
limit($select, 10);
where($select, 'event_date', '>=', "'$start'");
where($select, 'AND');
where($select, 'event_date', '<', "'$end'");
$sql = render($select);
// remaining code not shown
您可能已经注意到,由于参数数量有限,where()
必须被多次调用。这正是变量运算符的完美候选!下面是重写后的 where()
函数的样子:
// ch02/includes/php8_sql_lib.php
// other functions not shown
function where(stdClass $obj, ...$args) {
$obj->where = (empty($obj->where))
? $args
: array_merge($obj->where, $args);
}
由于 …$args
总是以数组形式返回,为了确保对函数的其他调用不会丢失子句,我们需要执行 array_merge()
操作。下面是重写后的调用程序:
// /repo/ch02/php8_variadics_sql.php
require_once __DIR__ . '/includes/sql_lib2.php';
$start = '2021-01-01';
$end = '2021-04-01';
$select = new stdClass();
from($select, 'events');
cols($select, ['id', 'event_key',
'event_name', 'event_date']);
limit($select, 10);
where($select, 'event_date', '>=', "'$start'",
'AND', 'event_date', '<', "'$end'");
$sql = render($select);
// remaining code not shown
结果 SQL 语句如下所示:
SELECT id,event_key,event_name,event_date
FROM events
WHERE event_date >= '2021-01-01'
AND event_date <= '2021-04-01'
LIMIT 10
前面的输出显示,我们的 SQL 生成逻辑生成了一条有效语句。
使用可变参数运算符作为替代
到目前为止,这些对于有经验的 PHP 开发人员来说都不陌生。PHP 8 中的不同之处在于,可变参数运算符现在可以用于 widening 的情况。
为了正确描述如何使用可变参数运算符,我们需要简要地回到面向对象编程(OOP)。如果我们把刚才描述的 where()
函数改写成一个类方法,它可能看起来像这样:
// src/Php7/Sql/Where.php
namespace Php7\Sql;
class Where {
public $where = [];
public function where($a, $b = '', $c = '', $d = '') {
$this->where[] = $a;
$this->where[] = $b;
$this->where[] = $c;
$this->where[] = $d;
return $this;
}
// other code not shown
}
现在,假设我们有一个 Select
类,它扩展了 Where
,但使用 可变参数操作符重新定义了方法签名。它可能是这样的:
// src/Php7/Sql/Select.php
namespace Php7\Sql;
class Select extends Where {
public function where(...$args) {
$this->where = (empty($obj->where))
? $args
: array_merge($obj->where, $args);
}
// other code not shown
}
使用可变参数运算符是合理的,因为制定 WHERE
子句所提供的参数数量是未知的。下面是使用 OOP 重写的调用程序:
// /repo/ch02/php7_variadics_problem.php
require_once __DIR__ . '/../src/Server/Autoload/Loader.php'
$loader = new \Server\Autoload\Loader();
use Php7\Sql\Select;
$start = "'2021-01-01'";
$end = "'2021-04-01'";
$select = new Select();
$select->from($select, 'events')
->cols($select, ['id', 'event_key',
'event_name', 'event_date'])
->limit($select, 10)
->where($select, 'event_date', '>=', "'$start'",
'AND', 'event_date', '<=', "'$end'");
$sql = $select->render();
// other code not shown
但是,当您尝试在 PHP 7 下运行此示例时,会出现以下警告:
Warning: Declaration of Php7\Sql\Select::where(...$args) should be compatible with Php7\Sql\Where::where($a, $b = '', $c = '', $d = '') in /repo/src/Php7/Sql/Select.php on line 5
请注意,代码仍然可以运行;但是,PHP 7 并没有将可变参数运算符视为可行的替代品。下面是在 PHP 8 下运行的相同代码(使用 /repo/ch02/php8_variadics_no_problem.php):

以下两个 PHP 文档参考资料解释了 PHP variadics 运算符背后的原因: |
现在让我们来看看 nullsafe
运算符。
使用空安全运算符
nullsafe 运算符用于对象属性引用链。如果链中的某个属性不存在(换句话说,它被视为 NULL
),操作符会安全地返回一个 NULL
值,而不会发出警告。
举例来说,假设我们有以下扩展标记语言(XML)文件:
<?xml version='1.0' standalone='yes'?>
<produce>
<file>/repo/ch02/includes/produce.xml</file>
<dept>
<fruit>
<apple>11</apple>
<banana>22</banana>
<cherry>33</cherry>
</fruit>
<vegetable>
<artichoke>11</artichoke>
<beans>22</beans>
<cabbage>33</cabbage>
</vegetable>
</dept>
</produce>
以下是扫描 XML 文档并显示数量的代码片段:
// /repo/ch02/php7_nullsafe_xml.php
$xml = simplexml_load_file(__DIR__ .
'/includes/produce.xml');
$produce = [
'fruit' => ['apple','banana','cherry','pear'],
'vegetable' => ['artichoke','beans','cabbage','squash']
];
$pattern = "%10s : %d\n";
foreach ($produce as $type => $items) {
echo ucfirst($type) . ":\n";
foreach ($items as $item) {
$qty = getQuantity($xml, $type, $item);
printf($pattern, $item, $qty);
}
}
我们还需要定义一个 getQuantity()
函数,首先检查该属性是否为空,然后再进入下一级,如下所示:
function getQuantity(SimpleXMLElement $xml,
string $type, string $item {
$qty = 0;
if (!empty($xml->dept)) {
if (!empty($xml->dept->$type)) {
if (!empty($xml->dept->$type->$item)) {
$qty = $xml->dept->$type->$item;
}
}
}
return $qty;
}
随着嵌套层级的加深,检查属性是否存在所需的函数也变得越来越复杂。这正是可以使用 nullsafe
操作符的地方。
请看同样的程序代码,但不需要 getQuantity()
函数,如下所示:
// /repo/ch02/php8_nullsafe_xml.php
$xml = simplexml_load_file(__DIR__ .
'/includes/produce.xml');
$produce = [
'fruit' => ['apple','banana','cherry','pear'],
'vegetable' => ['artichoke','beans','cabbage','squash']
];
$pattern = "%10s : %d\n";
foreach ($produce as $type => $items) {
echo ucfirst($type) . ":\n";
foreach ($items as $item) {
printf($pattern, $item,
$xml?->dept?->$type?->$item);
}
}
现在我们来看看 nullsafe
运算符的另一种用法。
使用 nullsafe 运算符使链条短路
nullsafe
操作符在一连串连接操作(包括对象属性引用、数组元素方法调用和静态引用)中使用时也很有用。
下面是一个返回匿名类的配置文件。它根据文件类型定义了不同的数据提取方法:
// ch02/includes/nullsafe_config.php
return new class() {
const HEADERS = ['Name','Amt','Age','ISO','Company'];
const PATTERN = "%20s | %16s | %3s | %3s | %s\n";
public function json($fn) {
$json = file_get_contents($fn);
return json_decode($json, TRUE);
}
public function csv($fn) {
$arr = [];
$fh = new SplFileObject($fn, 'r');
while ($node = $fh->fgetcsv()) $arr[] = $node;
return $arr;
}
public function txt($fn) {
$arr = [];
$fh = new SplFileObject($fn, 'r');
while ($node = $fh->fgets())
$arr[] = explode("\t", $node);
return $arr;
}
// all code not shown
};
该类还包括一个显示数据的方法,如以下代码片段所示:
public function display(array $data) {
$total = 0;
vprintf(self::PATTERN, self::HEADERS);
foreach ($data as $row) {
$total += $row[1];
$row[1] = number_format($row[1], 0);
$row[2] = (string) $row[2];
vprintf(self::PATTERN, $row);
}
echo 'Combined Wealth: '
. number_format($total, 0) . "\n";
}
在调用程序中,为了安全地执行 display()
方法,我们需要在执行回调之前添加 is_object()
extra 安全检查和 method_exists()
,如下代码片段所示:
// /repo/ch02/php7_nullsafe_short.php
$config = include __DIR__ .
'/includes/nullsafe_config.php';
$allowed = ['csv' => 'csv','json' => 'json','txt' => 'txt'];
$format = $_GET['format'] ?? 'txt';
$ext = $allowed[$format] ?? 'txt';
$fn = __DIR__ . '/includes/nullsafe_data.' . $ext;
if (file_exists($fn)) {
if (is_object($config)) {
if (method_exists($config, 'display')) {
if (method_exists($config, $ext)) {
$config->display($config->$ext($fn));
}
}
}
}
与前面的示例一样,nullsafe
操作符可用于确认 $config
作为对象是否存在。只需在第一个对象引用中使用 nullsafe
操作符,如果对象或方法不存在,操作符就会使整个链短路并返回 NULL
。
下面是使用 PHP 8 nullsafe
操作符重写的代码:
// /repo/ch02/php8_nullsafe_short.php
$config = include __DIR__ .
'/includes/nullsafe_config.php';
$allowed = ['csv' => 'csv','json' => 'json', 'txt' => 'txt'];
$format = $_GET['format'] ?? $argv[1] ?? 'txt';
$ext = $allowed[$format] ?? 'txt';
$fn = __DIR__ . '/includes/nullsafe_data.' . $ext;
if (file_exists($fn)) {
$config?->display($config->$ext($fn));
}
如果 $config
返回 NULL
,整个操作链将被取消,不会产生警告或通知,返回值(如果有的话)也是 NULL
。最终结果是,我们省去了编写三个额外的 if()
语句!
有关使用此运算符时其他注意事项的详细信息,请点击此处: https://wiki.php.net/rfc/nullsafe_operator 。 |
为了将格式参数传递给示例代码文件,您需要从浏览器运行代码,如下所示: http://localhost:8888/ch02/php7_nullsafe_short.php?format=json 。 |
接下来,我们看看连接运算符的变化。
连接运算符已被降级
尽管在 PHP 8 中连接运算符(例如句号(.
))的精确用法没有改变,但它在 优先级顺序 中的相对位置发生了极其重要的变化。在 PHP 的早期版本中,连接运算符的优先级与低阶算术运算符加号 (+
) 和减号 (-
) 相等。接下来,让我们看看传统优先级顺序的一个潜在问题:违背直觉的结果。
// /repo/ch02/php7_ops_concat_1.php
$a = 11;
$b = 22;
echo "Sum: " . $a + $b;
如果只看代码,你会以为输出的内容大致是 "Sum:33"
。但事实并非如此!请看看在 PHP 7.1 上运行时的输出结果:
root@php8_tips_php7 [ /repo/ch02 ]# php php7_ops_concat_1.php
PHP Warning: A non-numeric value encountered in /repo/ch02/
php7_ops_concat_1.php on line 5
PHP Stack trace:
PHP 1. {main}() /repo/ch02/php7_ops_concat_1.php:0
Warning: A non-numeric value encountered in /repo/ch02/php7_
ops_concat_1.php on line 5
Call Stack:
0.0001 345896 1. {main}()
22
此时,您可能想知道,因为代码从来不知道 11 + 22 的总和是 22,正如我们在前面的输出(最后一行)中看到的那样?
答案涉及优先顺序:从 PHP 7 开始,它始终是从左到右。因此,如果我们使用括号来使操作顺序更清晰,那么实际发生的情况如下:
echo ("Sum: " . $a) + $b;
11 与 "Sum: ",得到字符串 "Sum: 11"。然后将该字符串键入一个整数,得到 0 + 22 表达式,这就是我们的结果。
如果在 PHP 8 中运行同样的代码,请注意这里的区别:
root@php8_tips_php8 [ /repo/ch02 ]# php php8_ops_concat_1.php
Sum: 33
可以看到,算术运算符优先于连接运算符。使用括号后,代码在 PHP 8 中实际上就是这样处理的:
echo "Sum: " . ($a + $b);
最佳做法:使用括号可避免因依赖优先顺序而产生的复杂问题。有关降低连接运算符优先级的理由的更多信息,请访问: https://wiki.php.net/rfc/concatenation_precedence 。 |
现在我们把注意力转向三元算子。
使用嵌套三元运算符
三元运算符 对于 PHP 语言来说并不陌生。不过,在 PHP 8 中,三元运算符的解释方式有很大不同。这一变化与三元运算符传统的 左关联行为 有关。下面我们来看一个简单的例子来说明:
-
在这个示例中,假设我们使用
RecursiveDirectoryIterator
类和RecursiveIteratorIterator
类来扫描目录结构。开始的代码可能是这样的:// /repo/ch02/php7_nested_ternary.php $path = realpath(__DIR__ . '/..'); $searchPath = '/ch'; $searchExt = 'php'; $dirIter = new RecursiveDirectoryIterator($path); $itIter = new RecursiveIteratorIterator($dirIter);
-
然后,我们定义一个函数,用于匹配包含搜索路径
$searchPath
并以扩展名$searchExt
结尾的文件,如下所示:function find_using_if($iter, $searchPath, $searchExt) { $matching = []; $non_match = []; $discard = []; foreach ($iter as $name => $obj) { if (!$obj->isFile()) { $discard[] = $name; } elseif (!strpos($name, $searchPath)) { $discard[] = $name; } elseif ($obj->getExtension() !== $searchExt) { $non_match[] = $name; } else { $matching[] = $name; } } show($matching, $non_match); }
-
不过,有些开发人员可能会倾向于使用嵌套的三元操作符来重构这个函数,而不是使用
if / elseif / else
。下面是上一步中使用的相同代码:function find_using_tern($iter, $searchPath, $searchExt){ $matching = []; $non_match = []; $discard = []; foreach ($iter as $name => $obj) { $match = !$obj->isFile() ? $discard[] = $name : !strpos($name, $searchPath) ? $discard[] = $name : $obj->getExtension() !== $searchExt ? $non_match[] = $name : $matching[] = $name; } show($matching, $non_match); }
在 PHP 7 中,这两个函数的输出结果完全相同,如下图所示:

但在 PHP 8 中,嵌套的三元运算不再允许使用不带括号的三元运算。下面是运行相同代码块时的输出结果:

最佳做法:使用括号可避免嵌套三元运算的问题。有关三元操作嵌套差异的更多信息,请参阅本文: https://wiki.php.net/rfc/ternary_associativity 。 |
现在,您已经了解了新的 nullsafe
运算符。您还了解了现有的三个运算符—变量运算符、连接运算符和三元运算符—的功能是如何稍作修改的。现在,您可以很好地避免升级到 PHP 8 时的潜在危险。现在让我们来看看另一个新特性—箭头函数。