探索 PHP yield 关键词
想象一个场景,我们要编写一个命令,将文件迭代存入 S3 bucket 并对其逐个进行处理。我们可以使用 listObjectsV2
方法调用 S3 API 罗列 bucket 中的内容:
AwsFacade::createClient('s3', $config)
->listObjectsV2([
'Bucket' => 'top-secret-bucket'
]);
这个 API 调用将会返回多达 1000 个结果,鉴于我们需要扫描多达上百上千个文件,我们准备进行上百次调用。在对数据库记录进行分页时,可以使用 LIMIT 和 OFFSET 轻松完成,但在进行像 S3 这样的 HTTP 调用时呢?
public function handle(): void
{
$s3Client = AwsFacade::createClient('s3', $config);
$continuationToken = null;
do {
$response = $s3Client->listObjectsV2([
'Bucket' => 'top-secret-bucket',
'ContinuationToken' => $continuationToken,
]);
foreach ($response->get('Contents') as $result) {
$this->processResult($result);
}
$continuationToken = $response->get('NextContinuationToken');
$isTruncated = $response->get('IsTruncated');
} while ($isTruncated);
}
IsTruncated 指明是否还有更多的结果,NextContinuationToken 为我们提供了一个指针,我们可以使用它对 bucket 中的所有文件进行分页。
不过,从 S3 获取文件列表只是其中一部分,所以我真的不想让所有的分页功能弄乱我命令的主要方法。我更喜欢这样的东西;
public function handle(): void
{
foreach ($this->listS3Files() as $s3File) {
$this->processFile($s3File);
}
}
一种方法是将 do-while 循环移动到一个单独的方法中,并构建一个返回的结果数组,但当我们处理如此多的记录时,这变得不切实际。我们需要某种方法将 HTTP 调用提取到一个单独的方法中,同时轻松地对结果进行分页,以便我们可以根据需要进行尽可能多的调用。yield 来了:
public function handle(): void
{
$this->s3Client = AwsFacade::createClient('s3', $config);
foreach ($this->listS3Files() as $s3File) {
$this->processFile($s3File);
}
}
// The return type of the method is now iterable
private function listS3Files(): iterable
{
$continuationToken = null;
do {
$response = $this->s3Client->listObjectsV2([
'Bucket' => 'top-secret-bucket',
'ContinuationToken' => $continuationToken,
]);
foreach ($response->get('Contents') as $result) {
// Using yield rather than return allows us to paginate over results
yield $result;
}
$continuationToken = $response->get('NextContinuationToken');
$isTruncated = $response->get('IsTruncated');
} while ($isTruncated);
}
将 return
替换为 yield
会将此方法更改为所谓的生成器。当一个方法返回结果时,该方法范围内的所有其他内容都将被丢弃。如果我们得到结果,生成器的状态将被保存,$continuationToken
将被保留,它会在每次调用时从停止的地方继续,对所有结果进行懒惰的分页。这意味着我们可以在 foreach 循环中使用该方法,迭代 S3 bucket 中的每个文件,根据需要处理每个文件,同时在需要时进行额外的 HTTP 调用以获取更多结果!