PHP 8.0 新特性
PHP 8.0作为 PHP 语言的一个版本更新。它包含了很多新功能与优化项,包括命名参数、联合类型、注解、构造器属性提升、match 表达式、nullsafe、运算符、JIT,并改进了类型系统、错误处理、语法一致性。
类型系统与错误处理的改进:
- 算术/位运算符更严格的类型检测。
Abstract trait方法的验证。- 确保魔术方法签名正确。
- PHP 引擎 warning 警告的重新分类。
- 不兼容的方法签名导致 Fatal 错误。
- 操作符
@不再抑制 fatal 错误。 - 私有方法继承。
Mixed类型。- Static 返回类型。
- 内部函数的类型 Email thread。
- 扩展 Curl、 Gd、 Sockets、 OpenSSL、 XMLWriter、 XML 以 Opaque 对象替换 resource。
其他语法调整和改进:
- 允许参数列表中的末尾逗号、闭包 use 列表中的末尾逗号。
- 无捕获的 catche。
- 变量语法的调整。
- Namespace 名称作为单个 token。
- 现在 throw 是一个表达式。
- 允许对象的::class。
新的类、接口、函数:
- Weak Map 类。
- Stringable 接口。
- str_contains()、 str_starts_with()、 str_ends_with()。
- fdiv()。
- get_debug_type()。
- get_resource_id()。
- token_get_all()对象实现。
- New DOM Traversal and Manipulation APIs。
字符串与数字的比较逻辑
// PHP 8.0 之前 0 == 'foobar' // true // PHP 8.0 0 == 'foobar' // false
PHP 8 比较数字字符串(numeric string)时,会按数字进行比较。不是数字字符串时,将数字转化为字符串,按字符串比较。
新增字符串函数
对于字符串的一些操作。
str_contains():字符串中是否包含某个字符串。str_starts_with():字符串是否以某个字符串开头。str_end_with():字符串是否以某个字符串结尾。
// PHP 8.0 之前
// 1. 字符串中是否包含某个字符串
$str = 'Hello world!';
if (strpos($str,"world") !== false){
echo "Found";
}
// 2. 字符串是否以某个字符串开头
$found = 'Hello';
if (substr($str,0,count($found)) == $found){
echo 'Yes';
}
// 3. 字符串是否以某个字符串结尾
if (substr($str,-count($found),count($found)) == $found){
echo 'Yes';
}
// PHP 8.0
// 1. 字符串中是否包含某个字符串
$str = 'Hello world!';
if (str_contains($str,"world")){
echo 'Found';
}
// 2. 字符串是否以某个字符串开头
if (str_starts_with($str,"Hello")){
echo 'Yes';
}
// 3. 字符串是否以某个字符串结尾
if (str_end_with($str,'Hello')){
echo 'Yes';
}
null 空值安全检查(空安全运算符)
PHP8 中引入了 nullsafe 运算符,允许在一个对象链式调用对象方法或属性。在链式调用过程中,某个对象方法或属性可能是空的,如果是空的,就会报null错误。nullsafe 就很好的解决了这个问题。
?->
例如:用户登录后,在 blade 模板中显示用户名,我们会这么调用:auth()->user()->name,假设 user 中没有 name,那么这样调用就会报错。现在就用 PHP8 中的方法来解决这个问题。
// 案例一
Route::get('a', function () {
return auth()?->user()?->name;
});
// 案例二
Route::get('b', function () {
return auth()?->user()?->name;
});
案例一:当auth()调用user()时,user 再使用?->去调用 name 属性,如果 name 为null,那么就直接返回null。
案例二:当auth()调用user()时,使用?->,就是在说,有user()这个对象吗,有的话继续使用user()调用其值,没有的话就直接返回null。
构造器属性提升
现在我们可以直接在构造器__construct中直接声明类的属性。它简化类中属性声明并进行初始化赋值的操作。
// PHP 8.0 之前
class Client
{
private string $url;
public function __construct(string $url)
{
$this->url = $url;
}
}
// PHP 8.0
class Client
{
public function __construct (
private string $url,
) {}
}
// PHP 8.0 之前
class Point {
public float $x;
public float $y;
public float $z;
public function __construct(
float $x = 0.0,
float $y = 0.0,
float $z = 0.0
) {
$this->x = $x;
$this->y = $y;
$this->z = $z;
}
}
// PHP 8.0
class Point {
public function __construct (
public float $x = 0.0,
public float $y = 0.0,
public float $z = 0.0,
) {}
}
// PHP 8.0 之前
class User{
public $name;
public function __construct($name){
$this->name = $name;
}
}
$user = new User("lisa");
$user->name;
// PHP 8.0
class User{
public function __construct (public $name){
}
}
$user = new User("lisa");
$user->name;
联合类型
联合类型(Union Types)特性,用在类型提示或返回值有一个或者多个类型时。
// PHP 8.0 之前
class PostService
{
public function all(): mixed
{
if (! Auth::check()) {
return [];
}
return Post::query()->get();
}
}
// PHP 8.0
class PostService
{
public function all(): array|Collection
{
if (! Auth::check()) {
return [];
}
return Post::query()->get();
}
}
// PHP 8.0 之前
class Number {
/** @var int|float */
private $number;
/**
* @param float|int $number
*/
public function __construct($number) {
$this->number = $number;
}
}
new Number('NaN'); // Ok
// PHP 8.0
class Number {
public function __construct(
private int|float $number
) {}
}
new Number('NaN'); // TypeError
相较于以前的 PHPDoc 声明类型的组合,现在可以用原生支持的联合类型声明取而代之,并在运行时得到校验。
命名参数
命名参数能使我们的编码更加声明式–比如,不用再去猜测所调用函数的第三个参数指的什么。举
// PHP 8.0 之前
class ProcessImage
{
public static function handle(string $path, int $height, int $width, string $type, int $quality, int $compression): void
{
// logic for handling image processing
}
}
ProcessImage::handle('/path/to/image.jpg', 500, 300, 'jpg', 100, 5);
// PHP 8.0
class ProcessImage
{
public static function handle(string $path, int $height, int $width, string $type, int $quality, int $compression): void
{
// logic for handling image processing
}
}
ProcessImage::handle(
path: '/path/to/image.jpg',
height: 500,
width: 300,
type: 'jpg',
quality: 100,
compression: 5,
);
如上例所示– height 和 width 参数如果搞混了,可能会出现预期之外的结果。如果像上例这样,类和实例彼此紧挨着,那问题不大。但是如果这个方法来自于你从其他地方引入安装的一个包里,而这个包文档可能也没做好–这种情况下,使用命名参数将有助于包的使用者理解参数的顺序。不过使用该特性需要谨慎,因为包作者倾向于频繁地修改参数名称,并且通常不会将其当作破坏性修改。
Match 表达式
以前条件判断switch语句,当有多个caseMatch表达式对此作了改进。
// PHP 8.0 之前
switch (8.0) {
case '8.0':
$result = "Oh no!";
break;
case 8.0:
$result = "This is what I expected";
break;
}
echo $result; //Oh no!
// PHP 8.0
echo match (8.0) {
'8.0' => "Oh no!",
8.0 => "This is what I expected",
};
// This is what I expected
新的 match 类似于 switch,并具有以下功能:
- Match 是一个表达式,它可以储存到变量中亦可以直接返回。
- Match 分支仅支持单行,它不需要一个
break;语句。 - Match 使用严格比较。
// PHP 8.0 之前
switch (string $method) {
case 'GET':
$method = 'GET';
break;
case 'POST':
$method = 'POST';
break;
default:
throw new Exception("$method is not supported yet.");
}
// PHP 8.0
match (string $method) {
'GET' => $method = 'GET',
'POST' => $method = 'POST',
default => throw new Exception(
message: "$method is not supported yet.",
),
};
在 PHP8 种,Match语句,使用了更加精简的语法,且更又可读性。
在实例中使用::class
过去,如果你像传入类字符串到方法中,你必须使用类似于get_class这样的东西,看起来似乎没有意义。系统在调用时已经知道这个类了,因为你已经自动加载或创建了实例。如下例:
// PHP 8.0 之前
$commandBus->dispatch(get_class($event), $payload);
// PHP 8.0
$commandBus->dispatch(
event: $event::class,
payload: $payload,
);
// PHP 8.0 之前
class User {
}
$className = User::class;
// or
$user = new User();
get_class($user);
// PHP 8.0
class User {
}
$user = new User();
$className = $user::class;
无捕获的 catch 块
有时候,你并不需要获取抛出的异常,虽然并不一定常见。
// PHP 8.0 之前
try {
$response = $this->sendRequest();
} catch (RequestException $exception) {
Log::error('API request failed to send.');
}
// PHP 8.0
try {
$response = $this->sendRequest();
} catch (RequestException) {
Log::error('API request failed to send.');
}
箭头函数支持异常捕捉
// PHP 8.0 之前 $fn = fn() => 'ABC'; $fn(); $ex = fn() => throw new Exception(); // error // PHP 8.0 $fn = fn() => 'ABC'; $fn(); $ex = fn() =>throw new Exception(); try{ $ex(); }catch(Exception){ echo 'Failed'; }
对比:修复了箭头函数对异常的处理。并且可以去点捕捉异常时的参数名$exception(如果没有用到这个变量)。
注解功能
注解功能提供了代码中的声明部分都可以添加结构化、机器可读的元数据的能力,注解的目标可以是类、方法、函数、参数、属性、类常量。通过反射 API可在运行时获取注解所定义的元数据。因此注解可以成为直接嵌入代码的配置式语言。
通过注解的使用,在应用中实现功能、使用功能可以相互解耦。某种程度上讲,它可以和接口(interface)与其实现(implementation)相比较。但接口与实现是代码相关的,注解则与声明额外信息和配置相关。接口可以通过类来实现,而注解也可以声明到方法、函数、参数、属性、类常量中。因此它们比接口更灵活。
注解使用的一个简单例子:将接口(interface)的可选方法改用注解实现。我们假设接口 ActionHandler 代表了应用的一个操作:部分 action handler 的实现需要 setup,部分不需要。我们可以使用注解,而不用要求所有类必须实现 ActionHandler 接口并实现 setUp()方法。因此带来一个好处——可以多次使用注解。
// PHP 8.0 之前
class PostsController
{
/**
* @Route("/api/posts/{id}", methods={"GET"})
*/
public function get($id) { /* ... */ }
}
// PHP 8.0
class PostsController
{
#[Route("/api/posts/{id}", methods: ["GET"])]
public function get($id) { /* ... */ }
}
现在可以用 PHP 原生语法来使用结构化的元数据,而非 PHPDoc 声明。
<?php
interface ActionHandler
{
public function execute();
}
#[Attribute]
class SetUp {}
class CopyFile implements ActionHandler
{
public string $fileName;
public string $targetDirectory;
#[SetUp]
public function fileExists()
{
if (!file_exists($this->fileName)) {
throw new RuntimeException("File does not exist");
}
}
#[SetUp]
public function targetDirectoryExists()
{
if (!file_exists($this->targetDirectory)) {
mkdir($this->targetDirectory);
} elseif (!is_dir($this->targetDirectory)) {
throw new RuntimeException("Target directory $this->targetDirectory is not a directory");
}
}
public function execute()
{
copy($this->fileName, $this->targetDirectory . '/' . basename($this->fileName));
}
}
function executeAction(ActionHandler $actionHandler)
{
$reflection = new ReflectionObject($actionHandler);
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes (SetUp::class);
if (count($attributes) > 0) {
$methodName = $method->getName();
$actionHandler->$methodName();
}
}
$actionHandler->execute();
}
$copyAction = new CopyFile();
$copyAction->fileName = "/tmp/foo.jpg";
$copyAction->targetDirectory = "/home/user";
executeAction($copyAction);
