编程

现代 PHP 特性解析 - PHP 8.0 及 8.1

3918 2022-11-02 08:17:36

本教程将向大家展示笔者在什么场景下会使用 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,
    }
}