三种分页策略各有适用场景,没有单一"最好的库"——用 league/fractal 做 API                                     ────────
  响应层,底层分页逻辑自己实现最灵活。下面给出三种策略的完整对比和代码。

  ---                                                                                                                     三种策略对比
                                                                                                                          ┌────────┬──────────────────────┬────────────────────────┬──────────────────────────┐
  │  策略  │       适用场景       │          优点          │           缺点           │
  ├────────┼──────────────────────┼────────────────────────┼──────────────────────────┤
  │ Offset │ 小数据集、需跳页     │ 实现简单、支持随机跳页 │ 大数据集性能差、数据漂移 │
  ├────────┼──────────────────────┼────────────────────────┼──────────────────────────┤
  │ Cursor │ 无限滚动、实时数据   │ 性能稳定、无漂移       │ 不能跳页                 │
  ├────────┼──────────────────────┼────────────────────────┼──────────────────────────┤
  │ Keyset │ 超大数据集、排序稳定 │ 最快、数据库友好       │ 只能按索引列排序         │
  └────────┴──────────────────────┴────────────────────────┴──────────────────────────┘

  ---
  安装

  composer require league/fractal

  ---
  1. Offset 分页(传统)

  <?php
  // offset_pagination.php

  use League\Fractal\Manager;
  use League\Fractal\Resource\Collection;
  use League\Fractal\Pagination\IlluminatePaginatorAdapter;

  class UserTransformer extends \League\Fractal\TransformerAbstract
  {
      public function transform(array $user): array
      {
          return [
              'id'    => $user['id'],
              'name'  => $user['name'],
              'email' => $user['email'],
          ];
      }
  }

  function offsetPaginate(PDO $pdo, int $page, int $limit): array
  {
      $offset = ($page - 1) * $limit;

      // 数据查询
      $stmt = $pdo->prepare('SELECT * FROM users ORDER BY id LIMIT :limit OFFSET :offset');
      $stmt->bindValue(':limit',  $limit,  PDO::PARAM_INT);
      $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
      $stmt->execute();
      $users = $stmt->fetchAll(PDO::FETCH_ASSOC);

      // 总数
      $total = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
      $lastPage = (int)ceil($total / $limit);

      $fractal = new Manager();
      $resource = new Collection($users, new UserTransformer());

      // 手动构建分页元数据
      $resource->setMetaValue('pagination', [
          'total'        => (int)$total,
          'per_page'     => $limit,
          'current_page' => $page,
          'last_page'    => $lastPage,
          'from'         => $offset + 1,
          'to'           => min($offset + $limit, $total),
          'links' => [
              'prev' => $page > 1        ? "/api/users?page=" . ($page - 1) . "&limit=$limit" : null,
              'next' => $page < $lastPage ? "/api/users?page=" . ($page + 1) . "&limit=$limit" : null,
          ],
      ]);

      return $fractal->createData($resource)->toArray();
  }

  // 使用
  $result = offsetPaginate($pdo, page: 2, limit: 10);
  echo json_encode($result, JSON_PRETTY_PRINT);

  输出:
  {
      "data": [
          { "id": 11, "name": "Alice", "email": "alice@example.com" }
      ],
      "meta": {
          "pagination": {
              "total": 95,
              "per_page": 10,
              "current_page": 2,
              "last_page": 10,
              "links": {
                  "prev": "/api/users?page=1&limit=10",
                  "next": "/api/users?page=3&limit=10"
              }
          }
      }
  }

  ---
  2. Cursor 分页(推荐用于无限滚动)

  Cursor 是对某列值的编码指针,不依赖 OFFSET。

  <?php
  // cursor_pagination.php

  function cursorPaginate(PDO $pdo, ?string $cursor, int $limit): array
  {
      // cursor 是 base64 编码的 id
      $afterId = $cursor ? (int)base64_decode($cursor) : 0;

      $stmt = $pdo->prepare(
          'SELECT * FROM users WHERE id > :after_id ORDER BY id ASC LIMIT :limit'
      );
      $stmt->bindValue(':after_id', $afterId, PDO::PARAM_INT);
      $stmt->bindValue(':limit',    $limit,   PDO::PARAM_INT);
      $stmt->execute();
      $users = $stmt->fetchAll(PDO::FETCH_ASSOC);

      // 判断是否还有下一页(多取一条)
      $hasMore = count($users) === $limit;
      $nextCursor = $hasMore ? base64_encode((string)end($users)['id']) : null;

      $fractal  = new Manager();
      $resource = new Collection($users, new UserTransformer());

      $resource->setMetaValue('pagination', [
          'per_page'    => $limit,
          'has_more'    => $hasMore,
          'next_cursor' => $nextCursor,
          'links' => [
              'next' => $nextCursor
                  ? "/api/users?cursor=$nextCursor&limit=$limit"
                  : null,
          ],
      ]);

      return $fractal->createData($resource)->toArray();
  }

  // 第一页
  $result = cursorPaginate($pdo, cursor: null, limit: 10);

  // 下一页(用上一次返回的 next_cursor)
  $nextCursor = $result['meta']['pagination']['next_cursor'];
  $result2 = cursorPaginate($pdo, cursor: $nextCursor, limit: 10);

  输出:
  {
      "data": [...],
      "meta": {
          "pagination": {
              "per_page": 10,
              "has_more": true,
              "next_cursor": "MTU=",
              "links": {
                  "next": "/api/users?cursor=MTU=&limit=10"
              }
          }
      }
  }

  ---
  3. Keyset 分页(最快,适合超大表)

  基于索引列的范围查询,完全避免 OFFSET 扫描。

  <?php
  // keyset_pagination.php

  // 支持多列排序的 Keyset 分页
  function keysetPaginate(
      PDO    $pdo,
      ?int   $lastId,
      ?string $lastCreatedAt,
      int    $limit
  ): array {
      if ($lastId && $lastCreatedAt) {
          // 使用复合 keyset:(created_at, id) 保证唯一排序
          $stmt = $pdo->prepare(
              'SELECT * FROM users
               WHERE (created_at, id) > (:last_created_at, :last_id)
               ORDER BY created_at ASC, id ASC
               LIMIT :limit'
          );
          $stmt->bindValue(':last_created_at', $lastCreatedAt);
          $stmt->bindValue(':last_id',         $lastId, PDO::PARAM_INT);
      } else {
          $stmt = $pdo->prepare(
              'SELECT * FROM users ORDER BY created_at ASC, id ASC LIMIT :limit'
          );
      }
      $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
      $stmt->execute();
      $users = $stmt->fetchAll(PDO::FETCH_ASSOC);

      $hasMore  = count($users) === $limit;
      $lastRow  = $hasMore ? end($users) : null;

      $fractal  = new Manager();
      $resource = new Collection($users, new UserTransformer());

      $resource->setMetaValue('pagination', [
          'per_page' => $limit,
          'has_more' => $hasMore,
          'next_keyset' => $lastRow ? [
              'last_id'         => $lastRow['id'],
              'last_created_at' => $lastRow['created_at'],
          ] : null,
      ]);

      return $fractal->createData($resource)->toArray();
  }

  // 第一页
  $result = keysetPaginate($pdo, null, null, limit: 10);

  // 下一页
  $keyset = $result['meta']['pagination']['next_keyset'];
  $result2 = keysetPaginate($pdo, $keyset['last_id'], $keyset['last_created_at'], 10);

  ---
  4. 统一封装(策略模式)

  <?php
  // PaginationService.php

  class PaginationService
  {
      public function __construct(private PDO $pdo) {}

      public function paginate(string $strategy, array $params): array
      {
          return match($strategy) {
              'offset' => offsetPaginate($this->pdo, $params['page'], $params['limit']),
              'cursor' => cursorPaginate($this->pdo, $params['cursor'] ?? null, $params['limit']),
              'keyset' => keysetPaginate(
                  $this->pdo,
                  $params['last_id']         ?? null,
                  $params['last_created_at'] ?? null,
                  $params['limit']
              ),
              default => throw new \InvalidArgumentException("Unknown strategy: $strategy"),
          };
      }
  }

  // API 入口
  $service = new PaginationService($pdo);

  // 根据请求参数自动选择策略
  $strategy = $_GET['strategy'] ?? 'cursor';
  $result   = $service->paginate($strategy, $_GET);

  header('Content-Type: application/json');
  echo json_encode($result);

  ---
  选择建议

  数据量 < 10万,需要跳页    → Offset
  无限滚动 / 实时 feed       → Cursor
  数据量 > 100万,高性能要求  → Keyset
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐