现代 PHP 特性解析 - PHP 8.0 及 8.1
本教程将向大家展示笔者在什么场景下会使用 PHP 8 新特性,并借此带领大家熟悉这些新特性。接下来我们一起来了解 PHP 8.0 主要的一些新特性。
构造器属性提升
这是我最常用的 8.0 特性之一,使用此特性笔者节约了许多敲打键盘的时间。参考例子:
// 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,
) {}
}
现在我们可以直接在构造器 constructor 中直接声明类的属性。现在我经常使用这一特性,它简化类中属性声明并进行初始化赋值的操作。
联合类型
另一个有意思的新特性是联合类型(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();
}
}
这一新特性有助于静态分析理解我们代码,也有助于我们理解自己的代码 – 即使只是粗略一瞥。我们看到 all
方法返回的要么是数组要么是 collection, 这意味着我们的代码将会更具可预测性。
命名参数
命名参数能使我们的编码更加声明式 – 比如,不用再去猜测所调用函数的第三个参数指的什么。举例:
// 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 表达式
以前当有多个 case
时,我们会使用大段的 switch
语句。老实说,看起来并不美观也不直观。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.",
),
};
match
语句使用了更加精简的语法,且更具可读性。性能上的改进有多少我不太清楚,不过对代码的编写确实更加友好。
在实例中使用 ::class
过去,如果你像传入类字符串到方法中,你必须使用类似于 get_class
这样的东西,看起来似乎没有意义。系统在调用时已经知道这个类了,因为你已经自动加载或创建了实例。如下例:
// PHP 8.0 之前
$commandBus->dispatch(get_class($event), $payload);
// PHP 8.0
$commandBus->dispatch(
event: $event::class,
payload: $payload,
);
在所有这些新特性中,这可能不是很重要的一个,不过它是在需要用到时我肯定会使用的一个。
无捕获的 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.');
}
像上例中,我们并不需要使用到 exception
实例对象。不过在实际开发中,我经常还是会需要用到的。
以上是 PHP 8.0 的一些新特性。 接下来,我们来看看 PHP 8.1 给我们带来了哪些新特性。
枚举 Enum
枚举是我最喜欢的 PHP 8.1 新特性之一。现在我可以把那些永远不会改变的数据存到枚举中,而不用再存入到一张永远不会修改的表格中。
// PHP 8.1 之前
class Method
{
public const GET = 'GET';
public const POST = 'POST';
public const PUT = 'PUT';
public const PATCH = 'PATCH';
public const DELETE = 'DELETE';
}
// PHP 8.1
enum Method: string
{
case GET = 'GET';
case POST = 'POST';
case PUT = 'PUT';
case PATCH = 'PATCH';
case DELETE = 'DELETE';
}
以上展示了语法上的不同,使用上又有什么差异呢?下例是我在 API 集成中通常会用到的一个 trait:
// PHP 8.1 之前
trait SendsRequests
{
public function send(string $method, string $uri, array $options = []): Response
{
if (! in_array($method, ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'])) {
throw new InvalidArgumentException(
message: "Method [$method] is not supported.",
);
}
return $this->buildRequest()->send(
method: $method,
uri: $uri,
options: $options,
);
}
}
// PHP 8.1
trait SendsRequests
{
public function send(Method $method, string $uri, array $options = []): Response
{
return $this->buildRequest()->send(
method: $method->value,
uri: $uri,
options: $options,
);
}
}
它让我的方法可以通过类型透视准确把握传入的参数,并减少由于不支持类型抛出异常的可能性。如果我们想要扩展支持,我们只需要在 Enum 中添加新的 case 。
数组解包
以前我们通常是进行复制或者合并数组。现在我们可以使用这一新特性,就可以实现数组的解包。
// PHP 8.1 以前
final class CreateNewClient implements CreateNewClientContract
{
public function handle(DataObjectContract $client, int $account): Model|Client
{
return Client::query()->create(
attributes: array_merge(
$client->toArray(),
[
'account_id' => $account,
],
),
);
}
}
// PHP 8.1
final class CreateNewClient implements CreateNewClientContract
{
public function handle(DataObjectContract $client, int $account): Model|Client
{
return Client::query()->create(
attributes: [
...$client->toArray(),
'account_id' => $account,
],
);
}
}
如你所见,代码量精简了许多。
构造器中使用 new 关键字
// PHP 8.1 之前
class BuyerWorkflow
{
public function __construct(
private null|WorkflowStepContract $step = null
) {
$this->step = new InitialBuyerStep();
}
}
// PHP 8.1
class BuyerWorkflow
{
public function __construct(
private WorkflowStepContract $step = new InitialBuyerStep(),
) {}
}
在我看来,这一特性至少在代码上更加干净。在构造器上使用这一特性,我们不用再去担心会不会有传入 null 值的可能问题 – 让类自己去处理这个问题。
只读属性
以前我需要将想要用 public
公开的属性改成 protected
或者 private
– 这就是说我不得不因此为这个类中再添加 getter。
// PHP 8.1 之前
class Post
{
public function __construct() {
protected string $title,
protected string $content,
}
public function getTitle(): string
{
return $this->title;
}
public function getContent(): string
{
return $this->content;
}
}
// PHP 8.1
class Post
{
public function __construct() {
public readonly string $title,
public readonly string $content,
}
}