自2020年底发布以来,PHP 8一直是游戏规则的改变者。在本教程中,我将介绍所有最新功能以及我何时可能选择使用它们的真实示例。

在我职业生涯的早期,我就爱上了PHP语言,从那时起,我一有机会就会鼓吹它作为一种语言。然而,自从8.*版本发布后,我就没有再夸大过任何事情。相反,我已经能够完全依靠该语言的事实了。让我们来回顾一下PHP 8.0版本中的几个突出特点。

构造函数属性提升

这已经成为我最常用的8.0功能之一,并为我节省了许多代码。让我们把它分解一下。

// Before 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,
    ) {}
}

我们现在可以直接在构造函数中为对象设置属性作为参数,而不必手动分配它们。我现在几乎一直都在使用它,因为它节省了精力,而且它还保留了构造函数中包含的属性——因此您可以立即了解更多关于对象的信息,而无需滚动。

联合类型

另一个被发布的奇妙功能是联合类型。这就是一个类型提示变量或一个返回类型可以是一个或多个类型。这对静态分析很有帮助,因为你可能在一个方法内有条件返回。让我们看一个例子。

// Before 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 方法将返回一个数组或一个集合,这意味着我们的代码更可预测,我们也知道应该如何处理它。

命名参数

这是我最近可能过度使用的另一个功能。我发现使用命名参数可以使我们的代码具有声明性–不再需要猜测那个函数的第三个参数在你的代码库中意味着什么。让我们看看接下来的例子。

// Before 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,
);

正如你在上面的例子中所看到的–把高度和宽度弄错了,会产生与你预期不同的效果。由于类和实现是紧挨着的,这就相对容易了。现在想象一下,这个方法来自你安装的一个包,这个包的文档文档可能并不清晰明确–那么使用命名的参数可以让你和其他使用你的代码库的人了解这个方法的这些参数的顺序。然而,这仍然应该谨慎使用,因为库的作者往往更频繁地改变参数名称,而且并不总是被认为是破坏性的变化。

匹配表达式

我所接触到的每个人都喜欢这个改进,这是一个显著的改进。过去,我们使用包含多个 case 的大型 switch 语句,老实说 – 这不是最好的理方式。让我们看一个例子。

// Before 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这样的方法,这总是让人感觉有点毫无意义。系统当时其实已经知道该类,因为您已经自动加载它或创建了一个新实例。让我们看一个例子。

// Before PHP 8.0
$commandBus->dispatch(get_class($event), $payload);
// PHP 8.0
$commandBus->dispatch(
    event: $event::class,
    payload: $payload,
);

就功能而言,这可能不是一个引人注目的东西,但它绝对是我频繁使用的方式。

无捕获区块

有时在构建一个应用程序时,你可能不需要访问可能被抛出的异常。虽然对我来说,这种情况很少发生。不过,你的情况可能有所不同。让我们看一个例子。

// Before 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.');
}

我们不需要捕捉异常,因为我们在这种情况下并没有使用它。如果我们想包含一个来自异常的消息,那么要确保你捕获了这个异常。正如我所说,这不是我经常使用的方法,因为我通常想使用抛出的异常。

枚举

可爱的枚举,全世界代码库中无意义的数据库表和浮动常量的救星。Enums已经迅速成为PHP 8.1中我最喜欢的功能之一–我现在可以把我的角色推到Enums中,而不是把它们放在一个永远不会改变的表中。我可以将HTTP方法设置为Enums,而不是常量或我从未真正想使用的类的公共静态属性。让我们来看看。

// Before 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集成中使用一个特质。

// Before 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–而不是添加一个新的常量,并且不得不重构所有我们可能要检查支持的条件代码。

解包数组

这个功能在我使用之前我不确定我会使用。以前我们总是必须复制东西或合并数组才能得到我们需要的东西。现在我们可以解包数组,行为将是相同的。我在我的代码中经常使用 DTO,它们都有一个名为 的方法toArray,这对我来说是一种将 DTO 转换为 Eloquent 将为我处理的东西的简单方法。让我们看一个例子。

// Before 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 之前,由于各种原因,有时您可能不会将类的新实例传递给构造函数,但有时确实如此。它造成了这种情况,您永远无法确定是否需要传递实例。有那个时刻——我会通过 null 看看会发生什么——对最好的时刻的希望。感谢 PHP 8.1 为我们提供了一些针对截止日期和仓促决策的保障。我对吗?让我们看一个例子。

// Before 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(),
    ) {}
}

在我看来,这里的主要优势是代码清洁度。使用构造函数中的 new 功能,我们可以不用担心可能会传递 null ——而只需让类来处理它。上面的例子有点简单。老实说,这可能是因为我以前没有真正遇到过这些问题。但是,我知道你们中的许多人会这样做,并希望看到使用这个新功能的好处。

只读属性

我特别特别喜欢的功能。这对我来说是一个巨大的游戏规则改变。它使我可以轻松地以不变性进行编程,而不必降低可见性。我不得不把我想要公开的属性改为保护或私有——这意味着我必须向类添加 getter——这感觉就像添加真正不需要的样板。让我们看一个例子。

// Before 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,
    }
}

看看这个代码例子,你可以发现因为这个新的语言特性而增加的改进是令人印象深刻的。很明显,我们可以看到只读属性给你带来的好处–你的代码不那么冗长了,你可以在保持不变性的同时放松可见性和访问性。