本文属于专题文章《深入浅出 PHP 8》。
根据w3tech的数据,PHP 仍然是 Web 上使用最广泛的脚本语言之一,77.3%的网站使用 PHP 进行服务器端编程。PHP 8 带来了许多新特性和其他改进,我们将在本系列文章中进行探讨。PHP 8.0 添加了对多个函数和方法相关特性的支持,其中一些是对现有特性的改进,而另一些则是全新的特性。PHP 8.1 中增强的可调用语法可用于通过可调用对象创建匿名函数。命名函数参数可以与位置参数一起使用,另外还有一个好处,即命名参数没有顺序,可以通过它们的名称来传达含义。纤程(Fiber)是可中断的函数,增加了对多任务的支持。
重新定义了私有方法上的继承
对象继承是大多数面向对象语言(包括 PHP)所使用的编程范式。它可以从任何扩展类中重写公共和受保护的方法,以及在类中定义的类属性和常量。在 PHP 中,公共方法不能通过更严格的访问来重新实现,例如将 public 方法设为 private 。为了演示这一点,考虑一个扩展了类 A 的类 B,它重新实现了类 A 中一个公共方法。
<?phpclass A{ public function sortArray():string{ return "Class A method"; }} class B extends A{ private function sortArray():string{ return "Class B method"; }}$b=new B();echo $b->sortArray();
复制代码
运行时,脚本会生成如下的一条错误信息:
致命错误:B::sortArray()的访问级别必须是公共的(与类A一样)公共方法不能重新实现。
复制代码
相反,在类中定义的私有方法不是继承的,可以在扩展它的类中重新实现。例如,类 B 在下面的脚本中扩展了类 A,并重新实现了类 A 中一个私有方法。
<?phpclass A{ private function sortArray():string{ return "Class A method"; }} class B extends A{ private function sortArray(int $a):string{ return "Class B method"; }}
复制代码
在 PHP 8.0 之前,对扩展类中私有方法的重新声明应用了两个限制:不允许更改 final 和 static 修饰符。如果 private 方法被声明为 final ,则不允许扩展类重新声明该方法。如果私有方法被声明为静态的,那么它将在扩展类中保持静态。而且,如果私有方法没有static 修饰符,则不允许扩展类添加static 修饰符。在 PHP 8 中,这两个限制都被取消了。以下脚本在 PHP 8 中能正常运行。
<?phpclass A { private final static function sortArray():string{ return "Class A method"; }} class B extends A { private function sortArray(int $a):string{ return "Class B method"; }}
复制代码
PHP 8 中唯一的私有方法限制是强制使用 private final 构造函数,当使用静态工厂方法作为替代时,有时会使用private final来禁用构造函数。
<?phpclass A { private final function __construct(){ }} class B extends A { private final function __construct(){ }}
复制代码
该脚本生成如下的错误信息:
致命错误:不能重写最终方法A::__construct()
复制代码
可变参数可以替换任意数量的函数参数
在 PHP 8 中,单个可变参数可以替换任意数量的函数参数。考虑下面的脚本,其中类 B 扩展了类 A,并用一个可变参数替换函数 sortArray 的三个参数。
<?phpclass A { public function sortArray(array $arrayToSort, string $sortType, int $arraySize) { if ($sortType == "asc") { sort($arrayToSort); foreach ($arrayToSort as $key => $val) { echo "$key = $val "; } } elseif ($sortType == "desc") { rsort($arrayToSort); foreach ($arrayToSort as $key => $val) { echo "$key = $val "; } } }}class B extends A { public function sortArray(...$multiple) { $arrayToSort= $multiple[0]; $sortType=$multiple[1]; if ($sortType == "asc") { sort($arrayToSort); foreach ($arrayToSort as $key => $val) { echo "$key = $val "; } } elseif ($sortType == "desc") { rsort($arrayToSort); foreach ($arrayToSort as $key => $val) { echo "$key = $val "; } } }}
复制代码
可以使用多个参数调用类 B 中的 sortArray 函数。
$sortType="asc";$arrayToSort=array("B", "A", "f", "C");$arraySize=4; $b=new B();$b->sortArray($arrayToSort,$sortType,$arraySize);
复制代码
输出结果如下所示:
简化了可调用语法
可调用(callable)是可以被调用的 PHP 表达式,例如实例方法、静态方法或可调用对象。例如,可调用可用于为方法调用创建简短的表达式。在 PHP 8.1 中,可以用新的可调用语法:
AVariableCallableExpression(...)
AVariableCallableExpression 表示一个变量可调用表达式。省略号…包含在语法中。
为什么要使用新的可调用语法呢?让我们通过一些示例来回顾一下传统的可调用语法是什么样子的:
$f1 = 'strlen'(...);$f2 = [$someobj, 'somemethod'](...);$f3 = [SomeClass::class, 'somestaticmethod'](...);
复制代码
这有两个问题:
语法涉及字符串和数组
在创建可调用时,作用域不会被维护。为了演示这一点,请考虑如下用于对数组进行排序的脚本,其中 getSortArrayMethod() 方法返回 sortArray() 方法的可调用项,[$this,'sortArray'] 。
<?php class Sort { private array $arrayToSort; private string $sortType; public function __construct($arrayToSort,$sortType) { $this->arrayToSort = $arrayToSort; $this->sortType = $sortType; } public function getSortArrayMethod() { return [$this, 'sortArray']; } private function sortArray() { if ($this->sortType == "Asc") { sort($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } elseif ($this->sortType == "Desc") { rsort($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } else { shuffle($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } }}$sortType="Asc";$arrayToSort=array("B", "A", "f", "C");$sort = new Sort($arrayToSort,$sortType);$c = $sort->getSortArrayMethod();$c();
复制代码
该脚本会生成如下的错误信息:
致命错误:未捕获错误:调用私有方法Sort::sortArray()来自全局作用域
复制代码
使用 Closure::fromCallable([$this, 'sortArray']) 而不是 [$this, 'sortArray'] 可以解决作用域问题,但使用 Closure::fromCallable 方法会使调用变得冗长。新的可调用语法解决了作用域和语法冗长的问题。使用新的可调用语法,函数变为:
public function getSortArrayMethod() { return $this->sortArray(...); }
复制代码
数组根据输出进行排序:
新语法可以与涉及字符串和数组的传统语法结合使用,以解决作用域问题。创建可调用的作用域将保持不变。
public function getSortArrayMethod() { return [$this, 'sortArray'](...); }
复制代码
新的可调用语法也可以与静态方法一起使用,如下面的脚本所示,该脚本包含一个静态函数。
<?php class Sort { private array $arrayToSort; private string $sortType; public function __construct($arrayToSort,$sortType) { $this->arrayToSort = $arrayToSort; $this->sortType = $sortType; } public function getStaticMethod() { return Sort::aStaticFunction(...); } private static function aStaticFunction() { }}$sortType="Asc";$arrayToSort=array("B", "A", "f", "C");$sort = new Sort($arrayToSort,$sortType);$cStatic=$sort->getStaticMethod();$cStatic();
复制代码
输出结果与之前的相同:
以下是调用方法的等效方法:
return $this->sortArray(...); return Closure::fromCallable([$this, 'sortArray']);return [$this, 'sortArray'](...);
复制代码
以下是调用静态方法的等效方法:
return Sort::aStaticFunction(...); return [Sort::class, 'aStaticFunction'](...); return Closure::fromCallable([Sort::class, 'aStaticFunction']);
复制代码
即使函数声明了形参,也可以使用新的可调用语法。
<?php class Sort { private array $arrayToSort; private string $sortType; public function __construct($arrayToSort,$sortType) { $this->arrayToSort = $arrayToSort; $this->sortType = $sortType; } public function getSortArrayMethod() { return $this->sortArray(...); } private function sortArray(int $a,string $b) { if ($this->sortType == "Asc") { sort($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } elseif ($this->sortType == "Desc") { rsort($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } else { shuffle($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } }}
复制代码
如果一个方法声明了任意参数,则必须使用它的参数来调用可调用对象。
$sortType="Asc";$arrayToSort=array("B", "A", "f", "C");$sort = new Sort($arrayToSort,$sortType);$c = $sort->getSortArrayMethod();$c(1,"A");
复制代码
简化语法可用于任意的 PHP Callable 表达式
简化的可调用语法可以用于任意的 PHP 可调用表达式。用于对象创建的 new 运算符不支持可调用语法,因为可调用语法 AVariableCallableExpression(...) 没有指定构造函数参数的规定,这可能是必需的。以下是不支持的示例:
生成的错误信息为:
以下的脚本演示了受支持的所有可调用表达式。
<?php class Sort { private array $arrayToSort; private string $sortType; public function __construct($arrayToSort,$sortType) { $this->arrayToSort = $arrayToSort; $this->sortType = $sortType; } public function getSortArrayMethod() { return $this->sortArray(...); } public function getStaticMethod() { return Sort::aStaticFunction(...); } public static function aStaticFunction() { } public function sortArray(int $a,string $b) { if ($this->sortType == "Asc") { sort($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } elseif ($this->sortType == "Desc") { rsort($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } else { shuffle($this->arrayToSort); foreach ($this->arrayToSort as $key => $val) { echo "$key = $val "; } } } public function __invoke() {}}$sortType="Asc";$arrayToSort=array("B", "A", "f", "C"); $classStr = 'Sort';$staticmethodStr = 'aStaticFunction';$c1 = $classStr::$staticmethodStr(...);$methodStr = 'sortArray';$sort = new Sort($arrayToSort,$sortType);$c2 = strlen(...);$c3 = $sort(...); // 可调用对象$c4 = $sort->sortArray(...);$c5 = $sort->$methodStr(...);$c6 = Sort::aStaticFunction(...);$c7 = $classStr::$staticmethodStr(...);// 传统的可调用使用字符串,数组$c8 = 'strlen'(...);$c9 = [$sort, 'sortArray'](...);$c10 = [Sort::class, 'aStaticFunction'](...); $c11 = $sort->getSortArrayMethod();$c11(1,"A");$cStatic=$sort->getStaticMethod();$cStatic();
复制代码
尾逗号和可选/必选的参数顺序
PHP 8.0 的另一个新特性是支持在函数的参数列表末尾添加一个尾逗号,以提高可读性。任何尾逗号都将被忽略。尾逗号可能并不总是有用的,但如果参数列表很长,或者参数名称很长,则可能会有用,因此可以垂直列出它们。闭包使用列表也支持尾逗号。
PHP 8.0 不支持在必选参数之前声明可选参数。在必选参数之前声明的可选参数都是隐式的必选参数。
下面的脚本演示了必选参数的隐式顺序,以及尾逗号的使用。
<?php function trailing_comma_example( $the_very_first_arg_of_this_function, $the_second_arg_of_this_function, $the_third_arg_of_this_function = 1, $the_fourth_arg_of_this_function, $the_last_arg_of_this_function,){ echo $the_third_arg_of_this_function; } trailing_comma_example(1,2,null,3,4) ?>
复制代码
输出如下所示:
已弃用(不推荐):在必选参数$the_last_arg_of_this_function之前声明的可选参数$the_third_arg_of_tis_function将被隐式地视为必选参数
复制代码
可空参数不会被视为可选参数,可以使用 $param=null 形式或显式可空类型在必选参数之前声明,脚本如下所示:
<?php class A {} function fn1(A $a = null, $b) {} function fn2(?A $a, $b) {} fn1(new A,1); fn2(null,1);?>
复制代码
命名函数形参和实参
PHP 8.0 除了已经支持的位置形参和实参之外,还增加了对命名函数形参和实参的支持。命名参数在函数调用中的传递语法如下所示:
命名参数的一些好处如下所示:
<?phpfunction array_hashtable($key1,$key2,$key3,$key4,$key5){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5; echo "<br>";}// 使用位置实参:array_hashtable(0, 10, 50, 20, 25);// 使用命名实参:array_hashtable(key2: 0, key5: 25, key1: 10, key4: 50, key3: 20);?>
复制代码
输出结果为:
0 10 50 20 2510 0 20 50 25
复制代码
命名实参和位置实参可以在同一函数调用中使用。对相同的示例函数 array_hashtable 一起使用混合参数调用。
<?phpfunction array_hashtable($key1,$key2,$key3,$key4,$key5){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5; echo "<br>";}// 使用混合参数:array_hashtable(0, 10, 50, key5: 25, key4: 20); ?>
复制代码
输出结果为:
请注意,命名参数只能用于位置参数之后。下面的脚本颠倒了顺序,在命名参数之后使用位置参数:
<?phpfunction array_hashtable($key1,$key2,$key3,$key4,$key5){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5; echo "<br>";}// Using mixed arguments:array_hashtable(0, 10, key3: 25, 50, key5: 20); ?>
复制代码
该脚本生成的错误信息为:
即使使用命名参数,也不推荐在必选参数之前声明可选参数,脚本如下所示:
<?phpfunction array_hashtable($key1=0,$key2=10,$key3=20,$key4,$key5){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5; echo "<br>";}// 使用混合参数:array_hashtable(1,2,key3: 25, key4: 1,key5: 20); ?>
复制代码
输出将包括已弃用(不推荐)信息:
已弃用(不推荐):在必选参数$key5之前声明的可选参数$key1被隐式视为必选参数已弃用(不推荐):在必选参数$key5之前声明的可选参数$key2被隐式视为必选参数已弃用(不推荐):在必选参数$key5之前声明的可选参数$key3被隐式视为必选参数
复制代码
当在必选命名形参之后使用可选命名形参时,命名实参可用于跳过函数调用中的一个或多个可选形参,脚本如下所示:
<?phpfunction array_hashtable($key1,$key2,$key3=20,$key4=50,$key5=10){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5;echo "<br>";}// 使用混合参数:array_hashtable(key1:1, key2:2,key4: 25); ?>
复制代码
输出结果为:
你可以只使用可选参数的子集来调用函数,而不用考虑它们的顺序。
<?phpfunction array_hashtable($key1=0,$key2=10,$key3=20,$key4=50,$key5=10){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5; echo "<br>";}// 使用混合参数:array_hashtable(1,2,key4: 25); ?>
复制代码
输出结果如下所示:
即使使用可选参数的子集调用函数,也不能在命名参数之后使用位置参数,脚本如下所示:
<?phpfunction array_hashtable($key1=0,$key2=10,$key3=20,$key4=50,$key5=10){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5;echo "<br>";}// Using mixed arguments:array_hashtable(1,2,key4: 25,5); ?>
复制代码
生成的错误信息以下所示:
PHP 8.1 改进了命名实参特性,在解包实参后支持命名实参,脚本如下所示:
<?phpfunction array_hashtable($key1,$key2,$key3=30,$key4=40,$key5=50){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5; echo "<br>";}echo array_hashtable(...[10, 20], key5: 40); echo array_hashtable(...['key2' => 2, 'key1' => 2], key4: 50); ?>
复制代码
输出如下所示:
10 20 30 40 402 2 30 50 50
复制代码
但是,命名的参数不能覆盖前面的参数,脚本如下所示:
<?phpfunction array_hashtable($key1,$key2,$key3=30,$key4=40,$key5=50){ echo $key1.' '.$key2.' '.$key3.' '.$key4.' '.$key5; echo "<br>";}echo array_hashtable(...[10, 20], key2: 40); ?>
复制代码
输出如下所示:
致命错误:未捕获错误:命名参数$key2覆盖上一个参数。
复制代码
非静态方法不能被静态调用
在 PHP 8.0 之前,如果在静态上下文中调用非静态方法,或者静态调用,则只会收到一条已弃用(不推荐)的信息。使用 8.0,你现在会收到一条错误信息。此外, $this 在静态上下文中是未定义的。为了演示这一点,请考虑如下的脚本,其中使用静态语法 A::aNonStaticMethod() 调用了非静态方法 aNonStaticMethod() 。
<?phpclass A{ function aNonStaticMethod() { }} A::aNonStaticMethod();
复制代码
如果你运行这个脚本,将会得到如下的错误信息:
未捕获错误:非静态方法A::aNonStaticMethod()不能被静态调用
复制代码
纤程
PHP 8.1 添加了对纤程(Fiber)多任务的支持。纤程是一个可中断的函数,它具有自己的堆栈。纤程可以从调用堆栈中的任何位置挂起,然后再恢复。新的 Fiber 类是一个 final 类,它支持以下的公共方法:
| 方法 | 描述 |
|---|
| __construct(callable $callback) <br> | 创建新Fiber实例的构造函数。 该参数是启动纤程时要调用的可调用对象。 提供给Fiber::start()的参数将作为给定可调用对象的参数提供。 可调用对象根本不需要调用Fiber::suspend(),或者即使调用也不需要直接调用。对Fiber::suspend()的调用可能在调用堆栈中嵌套很深。 |
| start(mixed ...$args): mixed | 启动纤程。该方法在纤程挂起或终止时返回。在构造纤程时,为所使用的可调用函数提供了可变的参数列表。如果纤程返回,则从第一个挂起点返回混合值或返回NULL。如果调用该方法时纤程已经启动,则会抛出FiberError错误。 |
| resume(mixed $value = null): mixed | 恢复纤程,从Fiber::suspend()返回给定的混合值。 当纤程挂起或终止时返回。 返回的混合值实际上是从下一个挂起点返回的,如果纤程返回,则返回NULL。 如果纤程尚未启动、正在运行或已终止,则抛出FiberError错误。 |
| throw(Throwable $exception): mixed | 将给定的异常从Fiber::suspend()抛出到纤程中。 当纤程挂起或终止时返回。 参数是Throwable $exception。 返回的混合值实际上是从下一个挂起点返回的,如果纤程返回,则返回NULL。 如果纤程尚未启动、正在运行或已终止,则抛出FiberError错误。 |
| getReturn(): mixed | 获取纤程回调的混合返回值。 如果纤程没有返回语句,则返回NULL。 如果纤程未终止或纤程抛出异常,则抛出FiberError错误。 |
| isStarted(): bool | 如果纤程已经启动,则返回布尔值True。 |
| isSuspended(): bool | 如果纤程已挂起,则返回布尔值True。 |
| isRunning(): bool | 如果纤程正在运行,则返回布尔值True。 |
| isTerminated(): bool | 如果纤程已终止,则返回布尔值True。 |
| static suspend(mixed $value = null): mixed | 挂起纤程。 返回对Fiber->start()、Fiber->resume() 或 Fiber->throw() 的调用执行。可以使用Fiber::resume()或Fiber::throw()恢复纤程。不能从纤程外的主线程调用。 参数是从Fiber::resume()或 Fiber::throw()返回的混合值$value。返回的混合值提供给Fiber::resume()。<br>如果不在纤程内(即,如果从主线程调用),则抛出FiberError错误。 |
| static getCurrent(): ?Fiber | 返回当前正在执行的纤程实例,如果在主线程中则返回NULL。 |
纤程只能启动一次,但可以挂起并恢复多次。下面的脚本通过使用纤程在数组上执行不同类型的排序来演示多任务处理。纤程在每次排序后都会挂起,然后再恢复执行不同类型的排序。
<?php $fiber = new Fiber(function (array $arr): void { sort($arr); foreach ($arr as $key => $val) { echo "$key = $val "; } echo "<br/>"; Fiber::suspend(); rsort($arr); foreach ($arr as $key => $val) { echo "$key = $val "; } echo "<br/>"; Fiber::suspend(); shuffle($arr); foreach ($arr as $key => $val) { echo "$key = $val "; } }); $arrayToSort=array("B", "A", "f", "C");$value = $fiber->start($arrayToSort); $fiber->resume();$fiber->resume(); ?>
复制代码
输出如下所示:
0 = A 1 = B 2 = C 3 = f0 = f 1 = C 2 = B 3 = A0 = C 1 = f 2 = A 3 = B
复制代码
如果纤程在第一次挂起后没有再恢复,则只进行一种类型的排序,这可以通过注释掉对 resume() 的两次调用来实现。
//$fiber->resume();
//$fiber->resume();
复制代码
输出的是第一次排序的结果:
Stringable 接口和 __toString()
PHP 8.0 引入了一个名为 Stringable 的新接口,它只提供一个方法 __toString() 。 __toString() 方法如果在类中提供,将隐式实现 Stringable 接口。考虑提供 __toString() 方法的类 A。
<?php
class A {
public function __toString(): string { return " "; }}
echo (new A() instanceof Stringable);
复制代码
该脚本从 Stringable 的类型检查中返回 1。
然而,反之则不然。如果类实现了 Stringable 接口,则必须显式提供 __toString() 方法,因为该方法不会自动添加,比如:
<?php
class A implements Stringable {public function __toString(): string { } }
复制代码
新的标准库函数
PHP 8 引入了许多属于其标准库的新函数。
str_contains 函数返回一个 bool 值,用于指示作为第一个参数的字符串是否包含作为第二个参数的字符串。以下脚本将返回 false :
<?php
if (str_contains('haystack', 'needle')) { echo true;} else { echo false;}
复制代码
下面的脚本返回 1,或 true:
<?phpif (str_contains('haystack', 'hay')) { echo true;}else { echo "false";}
复制代码
str_starts_with 函数返回一个bool 值 ,指示作为第一个参数的字符串是否以作为第二个参数的字符串开头。以下脚本将返回 false 。
<?php
if (str_contains('haystack', 'hay')) { echo true;}else { echo "false";}
复制代码
下面的脚本将返回 1,或 true。
<?php
if (str_starts_with('haystack', 'needle')) { echo true;} else { echo false;}
复制代码
str_ends_with 函数返回一个bool 值 ,指示作为第一个参数的字符串是否以作为第二个参数的字符串结尾。以下脚本将返回 false 。
<?php
if (str_starts_with('haystack', 'needle')) { echo true;} else { echo false;}
复制代码
下面的脚本将返回 1,或 true。
<?php
if (str_starts_with('haystack', 'hay')) { echo true;} else { echo false;}
复制代码
fdiv 函数将两个数字相除并返回一个 float 值,脚本如下所示:
<?php
var_dump(fdiv(1.5, 1.3)); var_dump(fdiv(10, 2)); var_dump(fdiv(5.0, 0.0)); var_dump(fdiv(-2.0, 0.0)); var_dump(fdiv(0.0, 0.0)); var_dump(fdiv(5.0, 1.0)); var_dump(fdiv(10.0, 2));
复制代码
输出为:
float(1.1538461538461537)float(5) float(INF) float(-INF)float(NAN)float(5)float(5)
复制代码
fdatasync 函数在 Windows 上的别名为 fsync ,用于将数据同步到文件上的流中。为了演示它的用法,在包含要运行的 PHP 脚本的脚本目录中创建一个空文件 test.txt 。运行脚本:
<?php
$file = 'test.txt';
$stream = fopen($file, 'w');fwrite($stream, 'first line of data');fwrite($stream, "\r\n");fwrite($stream, 'second line of data');fwrite($stream, 'third line of data');
fdatasync($stream);fclose($stream);
复制代码
随后,打开 test.txt 文件会发现包含如下的文本:
first line of datasecond line of datathird line of data
复制代码
array_is_list 函数返回布尔值,用于指示给定的数组是否为列表。数组必须从 0 开始,键必须是连续的整数,并且顺序正确。下面的脚本演示了 array_is_list 函数:
<?php
echo array_is_list([]); echo array_is_list(['1', 2, 3]); echo array_is_list([0 => 'a', 'b']);
echo array_is_list([1 => 'a', 'b']); // falseecho array_is_list([1 => 'a', 0 => 'b']); // falseecho array_is_list([0 => 'a', 'b' => 'b']); // falseecho array_is_list([0 => 'a', 2 => 'b']); // false
复制代码
输出为:
魔术方法必须要有正确的签名
魔术方法是 PHP 中用于覆盖默认操作的特殊方法。它们包括如下的方法,其中构造函数 __construct() 可能是大家最熟悉的:
__construct(),__destruct(), __call(),__callStatic(),__get(), __set(),__isset(),__unset(), __sleep(),__wakeup(),__serialize(),__unserialize(),__toString(),__invoke(),__set_state(),__clone(),__debugInfo()
复制代码
从 PHP 8.0 开始,魔术方法定义的签名必须要是正确的,这意味着如果在方法参数或返回类型中使用类型声明,则它们必须与文档中的声明相同。新的 __toString() 方法的返回类型必须要声明为 string 。下面的演示将返回类型声明为 int :
<?php
class A {
public function __toString(): int { }}
复制代码
将生成如下的错误信息:
致命错误:A::__toString():返回类型在声明时必须是字符串
复制代码
但是,未通过定义声明返回类型的函数(如构造函数)不能声明返回类型,即使是 void 返回类型也不行。示例如下脚本所示:
<?php
class A { public function __construct():void { }}
复制代码
该脚本将返回如下的错误信息:
致命错误:方法A::__construct()不能声明返回类型
复制代码
所有魔术方法,除了少数例外(例如 __construct() )外,都必须声明为具有公共可见性。为了演示这一点,声明了一个带有 private 可见性的 __callStatic 。
<?php
class A {
private static function __callStatic(string $name, array $arguments) {}
}
复制代码
输出的警告信息为:
警告:魔术方法A::__callStatic()必须要具有公共可见性
复制代码
尽管可以省略混合返回类型,但方法签名也必须相同。例如,在下面的脚本中,类 A 声明了 __callStatic 而没有指定其返回类型,而类 B 将其第一个参数定义为int :
<?php
class A {
public static function __callStatic(string $name, array $arguments) {}
class B {
public static function __callStatic(int $name, array $arguments) {}
}
复制代码
输出的错误信息如下所示:
致命错误:B::__callStatic():参数 #1 ($name) 在声明时必须为字符串类型
复制代码
返回类型与内部类的兼容性
在 PHP 8.1 中,大多数内部方法,即内部类中的方法,都已经“试探性地”开始声明返回类型。试探性地暗示,虽然在 8.1 中只会引发不推荐(Deprecation)通知,但在 9.0 版中,则会输出错误条件信息。因此,任何扩展类都必须声明与内部类相兼容的返回类型,否则将会引发已弃用(不推荐)通知。为了演示这一点,扩展内部类 Directory 并重新声明没有返回类型的函数 read() :
<?php
class A extends Directory { public function read() { }}
复制代码
该脚本将生成已弃用(不推荐)通知:
已弃用(不推荐):A::read()的返回类型应与Directory::read(): string|false兼容,或者应使用#[\ReturnTypeWillChange]属性来临时抑制通知
复制代码
但是,以下脚本是可以的:
<?php
class A extends Directory { public function read():string { return ""; }}
复制代码
添加 #[\ReturnTypeWillChange] 属性能抑制已弃用(不推荐)通知:
<?phpclass A extends Directory { #[\ReturnTypeWillChange] public function read() { }}
复制代码
\SensitiveParameter 属性
虽然包含有关方法参数的详细信息的异常堆栈跟踪对调试非常有用,但你可能不希望输出某些敏感参数的参数值,例如与密码和凭据关联的参数值。PHP 8.2 引入了一个名为 \SensitiveParameter 的新属性,这样,如果使用 \SensitivyParameter 属性注解方法的参数,则该参数的值不会在异常堆栈跟踪中输出。
为了演示这一点,考虑下面的脚本,其中函数 f1 具有与 \SensitiveParameter 属性关联的参数 $password 。
<?php function f1( $param1 = 1, #[\SensitiveParameter] $password = “s@5f_u7”, $param3 = null) { throw new \Exception('Error');}
复制代码
为了演示 \SensitiveParameter 特性,该函数只是抛出一个异常。调用函数:
请注意,异常堆栈跟踪不包含 $password 参数的值,而是列出了 Object(SensitiveParameterValue) 。
Stack trace: #0 : f1(1, Object(SensitiveParameterValue), 'a') #1 {main}
复制代码
内置函数弃用/增强
内置函数 utf8_encode() 和 utf8_decode() 经常被误解,因为它们的名称意味着对任何字符串进行编码/解码。实际上,这些函数仅用于编码/解码 ISO8859-1,即“Latin-1”字符串。此外,它们生成的错误信息对于调试来说描述性不够。PHP 8.2 已经弃用了这些函数。下面的脚本使用了它们:
<?php $string_to_encode = "\x7A\x6A\xdB";$utf8_string = utf8_encode($string_to_encode);echo bin2hex($utf8_string), "\n";$utf8_string = "\x6A\x6B\xD3\xCB";$decoded_string = utf8_decode($utf8_string);echo bin2hex($decoded_string), "\n";
复制代码
对于 PHP 8.2,会输出已弃用(不推荐)通知:
已弃用(不推荐):函数utf8_encode()已弃用已弃用(不推荐):函数utf8_decode()已弃用
复制代码
在 PHP 8.2 中,函数 iterator_count 和 iterator_to_array 接受所有可迭代的对象。 iterator_to_array() 函数将迭代器的元素复制到数组中。 iterator_count() 函数对数组的元素进行计数。这些函数接受一个 $iterator 作为第一个参数。在 PHP 8.2 中,$iterator 参数的类型已从 Traversable 扩展为 Traversable|array ,以便接受任意的可迭代值。
下面的脚本演示了它们在 arrays 和 Traversables 中的使用。
<?php $a=array('1'=>'one', 'two', 'three', 'four');$iterator = new ArrayIterator($a);var_dump(iterator_to_array($iterator, true));var_dump(iterator_to_array($a, true));
var_dump(iterator_count($iterator));var_dump(iterator_count($a));
复制代码
输出如下所示:
array(4) { [1]=> string(3) "one" [2]=> string(3) "two" [3]=> string(5) "three" [4]=> string(4) "four" } array(4) { [1]=> string(3) "one" [2]=> string(3) "two" [3]=> string(5) "three" [4]=> string(4) "four" } int(4) int(4)
复制代码
总结
在这篇 PHP 8 系列文章中,我们讨论了与函数和方法相关的新特性,其中最突出的是命名函数的形参/实参、简化的可调用语法和被称为纤程(Fiber)的可中断函数。
在本系列的下一篇文章中,我们将介绍 PHP 类型系统的新特性。
原文链接:
https://www.infoq.com/articles/php8-functions-methods/
相关阅读:
PHP 8:注解、match 表达式及其他改进
PHP 8:类和枚举
评论