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( privateint|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);