cerbero/lazy-json-pages

一个框架无关的包,可以通过异步HTTP请求将任何分页的JSON API中的项目加载到Laravel懒集合中。

2.0.1 2024-09-13 00:53 UTC

This package is auto-updated.

Last update: 2024-09-13 00:53:26 UTC


README

Author PHP Version Build Status Coverage Status Quality Score PHPStan Level Latest Version Software License PER Total Downloads

use Illuminate\Support\LazyCollection;

LazyCollection::fromJsonPages($source)
    ->totalPages('pagination.total_pages')
    ->async(requests: 3)
    ->throttle(requests: 100, perMinutes: 1)
    ->collect('data.*');

一个框架无关的API抓取器,可以通过异步HTTP请求将任何分页的JSON API中的项目加载到Laravel懒集合中。

提示

需要以内存高效的方式读取无分页的大JSON文件?

请考虑使用🐼 Lazy JSON🧩 JSON Parser

📦 安装

通过Composer

composer require cerbero/lazy-json-pages

🔮 使用方法

👣 基础知识

根据我们的编码风格,我们可以以4种不同的方式实例化Lazy JSON Pages

use Cerbero\LazyJsonPages\LazyJsonPages;
use Illuminate\Support\LazyCollection;

use function Cerbero\LazyJsonPages\lazyJsonPages;

// lazy collection macro
LazyCollection::fromJsonPages($source);

// classic instantiation
new LazyJsonPages($source);

// static method
LazyJsonPages::from($source);

// namespaced helper
lazyJsonPages($source);

在示例中,变量$source代表任何指向分页JSON API的。一旦我们定义了源,我们就可以链式调用方法来定义API如何分页

$lazyCollection = LazyJsonPages::from($source)
    ->totalItems('pagination.total_items')
    ->offset()
    ->collect('results.*');

在调用collect()时,我们表示分页结构已定义,并且我们准备在Laravel懒集合中收集分页项,在那里我们可以逐个遍历项并应用过滤和转换,以内存高效的方式。

💧 源

源是任何可以指向分页JSON API的手段。默认支持多种源

  • 端点URI,例如https://example.com/api/v1/users或任何Psr\Http\Message\UriInterface的实例
  • PSR-7请求,即任何Psr\Http\Message\RequestInterface的实例
  • Laravel HTTP客户端请求,即任何Illuminate\Http\Client\Request的实例
  • Laravel HTTP客户端响应,即任何Illuminate\Http\Client\Response的实例
  • Laravel HTTP请求,即任何Illuminate\Http\Request的实例
  • Symfony请求,即任何Symfony\Component\HttpFoundation\Request的实例
  • 用户定义的源,即任何Cerbero\LazyJsonPages\Sources\Source的实例

以下是一些源的示例

// a simple URI string
$source = 'https://example.com/api/v1/users';

// any PSR-7 compatible request is supported, including Guzzle requests
$source = new GuzzleHttp\Psr7\Request('GET', 'https://example.com/api/v1/users');

// while being framework-agnostic, Lazy JSON Pages integrates well with Laravel
$source = Http::withToken($bearer)->get('https://example.com/api/v1/users');

如果上述任何源都无法满足我们的使用情况,我们可以实现自己的源。

点击此处查看如何实现自定义源。

要实现自定义源,我们需要扩展Source并实现2个方法

use Cerbero\LazyJsonPages\Sources\Source;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class CustomSource extends Source
{
    public function request(): RequestInterface
    {
        // return a PSR-7 request
    }

    public function response(): ResponseInterface
    {
        // return a PSR-7 response
    }
}

父类Source为我们提供了对2个属性的访问权限

  • $source:用于我们用例的自定义源
  • $client:Guzzle HTTP客户端

要实现的方法将我们的自定义源转换为PSR-7请求和PSR-7响应。请参考现有的源以查看一些实现。

一旦实现了自定义源,我们就可以指示Lazy JSON Pages使用它

LazyJsonPages::from(new CustomSource($source));

如果您在多个项目中实现了相同的自定义源,请随时发送PR,我们将考虑默认支持您的自定义源。提前感谢您的任何贡献!

🏛️ 分页结构

在定义了之后,我们需要让Lazy JSON Pages知道分页API的样式。

如果API使用与page不同的查询参数来指定当前页,例如?current_page=1,我们可以链式调用pageName()方法。

LazyJsonPages::from($source)->pageName('current_page');

否则,如果当前页码的数字出现在URI路径中,例如https://example.com/users/page/1,我们可以链式调用pageInPath()方法。

LazyJsonPages::from($source)->pageInPath();

默认情况下,URI路径中的最后一个整数被认为是页码。但如果我们需要,可以自定义用于捕获页码的正则表达式。

LazyJsonPages::from($source)->pageInPath('~/page/(\d+)$~');

一些API的分页可能从不同于1的页码开始。如果是这样,我们可以通过链式调用firstPage()方法来定义第一页。

LazyJsonPages::from($source)->firstPage(0);

现在我们已经自定义了API的基本结构,我们可以描述根据分页是长度感知还是基于游标的分页方式来分页项目。

📏 长度感知分页

术语“长度感知”表示包含以下至少一种长度信息的分页:

  • 总页数
  • 项目总数
  • 最后一页的页码

Lazy JSON Pages只需要这些细节中的任何一个来正常工作。

LazyJsonPages::from($source)->totalPages('pagination.total_pages');

LazyJsonPages::from($source)->totalItems('pagination.total_items');

LazyJsonPages::from($source)->lastPage('pagination.last_page');

如果长度信息嵌套在JSON体中,我们可以使用点符号来指示嵌套级别。例如,pagination.total_pages表示总页数位于对象pagination下,键为total_pages

否则,如果长度信息显示在头部,我们可以通过简单地定义头部的名称来使用相同的方法收集它。

LazyJsonPages::from($source)->totalPages('X-Total-Pages');

LazyJsonPages::from($source)->totalItems('X-Total-Items');

LazyJsonPages::from($source)->lastPage('X-Last-Page');

API可以以数字(total_pages: 10)或URI(last_page: "https://example.com?page=10")的形式公开它们的长度信息,Lazy JSON Pages都支持。

如果分页使用偏移量,我们可以使用offset()方法来配置它。偏移量的值将基于第一页上项目数量来计算。

// indicate that the offset is defined by the `offset` query parameter, e.g. ?offset=50
LazyJsonPages::from($source)
    ->totalItems('pagination.total_items')
    ->offset();

// indicate that the offset is defined by the `skip` query parameter, e.g. ?skip=50
LazyJsonPages::from($source)
    ->totalItems('pagination.total_items')
    ->offset('skip');

↪️ 游标感知分页

并非所有分页都是长度感知的,一些可能以这种方式构建,其中每一页都有一个指向下一页的游标。

我们可以通过指出包含游标的键或头部来处理这种分页。

LazyJsonPages::from($source)->cursor('pagination.cursor');

LazyJsonPages::from($source)->cursor('X-Cursor');

游标可能是一个数字、一个字符串或一个URI:Lazy JSON Pages支持它们全部。

🔗 链接头分页

一些分页API响应包括一个名为Link的头部。例如,GitHub(GitHub):如果我们检查响应头部,我们可以看到像这样的Link头部。

<https://api.github.com/repositories/1296269/issues?state=open&page=2>; rel="next",
<https://api.github.com/repositories/1296269/issues?state=open&page=43>; rel="last"

要懒加载来自Link头部分页的项目,我们可以链式调用linkHeader()方法。

LazyJsonPages::from($source)->linkHeader();

👽 自定义分页

Lazy JSON Pages提供了一些方法来从最流行的分页机制中提取项目。但是,如果我们需要一个定制解决方案,我们可以实现自己的分页。

点击这里查看如何实现自定义分页。

要实现自定义分页,我们需要扩展Pagination类并实现1个方法。

use Cerbero\LazyJsonPages\Paginations\Pagination;
use Traversable;

class CustomPagination extends Pagination
{
    public function getIterator(): Traversable
    {
        // return a Traversable yielding the paginated items
    }
}

父类Pagination给我们提供了访问3个属性:

  • $source:指向分页JSON API的
  • $client:Guzzle HTTP客户端
  • $config:通过链式调用如totalPages()方法生成的配置。

方法 getIterator() 定义了以内存高效的方式提取分页项的逻辑。请参阅现有分页实现以查看一些示例。

一旦实现了自定义分页,我们可以指示 Lazy JSON Pages 使用它

LazyJsonPages::from($source)->pagination(CustomPagination::class);

如果您在不同的项目中实现了相同的自定义分页,请随时发送 PR,我们将考虑默认支持您的自定义分页。提前感谢您做出的任何贡献!

🚀 请求优化

分页 API 之间存在差异,因此 Lazy JSON Pages 允许我们针对我们的特定用例调整 HTTP 请求。

默认情况下,HTTP 请求是同步发送的。如果我们想要发送多个请求而不等待响应,我们可以调用 async() 方法并设置并发请求的数量

LazyJsonPages::from($source)->async(requests: 5);

注意

请注意,异步请求虽然可以加快速度,但会以内存为代价,因为会一次性加载更多响应。

一些 API 设置了速率限制,以减少在一定时间内的允许请求数量。我们可以指示 Lazy JSON Pages 通过限制请求来遵守这些限制

// we send a maximum of 3 requests per second, 60 per minute and 3,000 per hour
LazyJsonPages::from($source)
    ->throttle(requests: 3, perSeconds: 1)
    ->throttle(requests: 60, perMinutes: 1)
    ->throttle(requests: 3000, perHours: 1);

内部,Lazy JSON Pages 使用 Guzzle 作为其 HTTP 客户端。我们可以通过添加任意数量的 中间件 来自定义客户端行为

LazyJsonPages::from($source)
    ->middleware('log_requests', $logRequests)
    ->middleware('cache_responses', $cacheResponses);

如果我们需要每次调用 Lazy JSON Pages 时都添加一个中间件,我们可以添加一个全局中间件

LazyJsonPages::globalMiddleware('fire_events', $fireEvents);

有时编写 Guzzle 中间件 可能很麻烦。作为替代,Lazy JSON Pages 提供方便的方法在发送请求或接收响应时触发回调

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

LazyJsonPages::from($source)
    ->onRequest(fn(RequestInterface $request) => ...)
    ->onResponse(fn(ResponseInterface $response, RequestInterface $request) => ...);

我们还可以调整在 API 连接超时之前允许的秒数,或整个 HTTP 请求允许的持续时间(默认情况下,它们都设置为 5 秒)

LazyJsonPages::from($source)
    ->connectionTimeout(7)
    ->requestTimeout(10);

如果第三方 API 存在故障或易于出错,我们可以指定我们希望重复失败的 HTTP 请求的次数以及计算在重试之前等待毫秒数的回退策略(默认情况下,失败请求会在 100、400 和 900 毫秒的指数回退后重复 3 次)

// repeat failing requests 5 times after a backoff of 1, 2, 3, 4 and 5 seconds
LazyJsonPages::from($source)
    ->attempts(5)
    ->backoff(fn(int $attempt) => $attempt * 1000);

💢 错误处理

如果在抓取过程中出现错误,我们可以截获错误并执行自定义逻辑来处理它

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

LazyJsonPages::from($source)
    ->onError(fn(Throwable $e, RequestInterface $request, ?ResponseInterface $response) => ...);

此包抛出的任何异常都扩展了 LazyJsonPagesException 类。这使得在单个捕获块中处理所有异常变得容易

use Cerbero\LazyJsonPages\Exceptions\LazyJsonPagesException;

try {
    LazyJsonPages::from($source)->linkHeader()->collect()->each(...);
} catch (LazyJsonPagesException $e) {
    // handle any exception thrown by Lazy JSON Pages
}

作为参考,以下是此包抛出的所有异常的完整表

🤝 Laravel集成

如果在 Laravel 项目中使用,Lazy JSON Pages 在以下情况下自动触发事件:

  • 即将发送 HTTP 请求,通过触发 Illuminate\Http\Client\Events\RequestSending
  • 接收 HTTP 响应,通过触发 Illuminate\Http\Client\Events\ResponseReceived
  • 连接失败,通过触发 Illuminate\Http\Client\Events\ConnectionFailed

这对于调试工具(如 Laravel TelescopeSpatie Ray)或触发相关事件监听器特别有用。

📆 更新日志

有关最近更改的更多信息,请参阅 CHANGELOG

🧪 测试

composer test

💞 贡献

有关详细信息,请参阅 CONTRIBUTINGCODE_OF_CONDUCT

🧯 安全性

如果您发现任何与安全相关的问题,请通过电子邮件发送至 andrea.marco.sartori@gmail.com,而不是使用问题跟踪器。

🏅 致谢

⚖️ 许可证

MIT 许可证(MIT)。更多信息请参阅许可证文件