定制 muxtorov/yii2-cursor-pagination 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

muxtorov/yii2-cursor-pagination

Composer 安装命令:

composer require muxtorov/yii2-cursor-pagination

包简介

Cursor-based pagination helper for Yii2 APIs

README 文档

README

Cursor-based pagination helper for Yii2 APIs with PHP 7.4 and 8+ support.

Install

composer require muxtorov/yii2-cursor-pagination

Config

config/params.php

return [
    'pagination' => [
        'maxLimit' => 1000,
        'defaultPerPage' => 20,
        'cursorParam' => 'cursor',
        'perPageParam' => 'per-page',
        'directionParam' => 'direction',
    ],
];

Simple usage

use Muxtorov\Yii2CursorPagination\CursorDataProvider;

$provider = new CursorDataProvider();

$data = $provider->getData(
    OrderResource::find()->where(['userId' => $userId]),
    ['createdAt', 'id']
);

Response shape:

[
    'items' => [...],
    '_links' => [
        'self' => '...',
        'next' => '...',
        'prev' => '...',
    ],
    '_meta' => [
        'perPage' => 20,
        'direction' => 'next',
        'hasNext' => true,
        'hasPrev' => false,
        'nextCursor' => '...',
        'prevCursor' => '...',
    ],
]

Query params

GET /orders?per-page=20
GET /orders?cursor=eyJpZCI6MTAwfQ&direction=next
GET /orders?cursor=eyJpZCI6MTAwfQ&direction=prev

Defaults:

  • cursor: current page cursor
  • per-page: page size
  • direction: next or prev

Recommended architecture

This package should stay generic.

Keep these inside your project:

  • BaseFilter
  • QueryCollectionExtensionInterface
  • DefaultFilter
  • factory classes like api\modules\v1\factories\*
  • project-specific query extensions

Keep this package responsible only for:

  • reading request cursor params
  • applying cursor condition
  • building _links
  • building _meta

That separation is important because BaseFilter and your extensions are application-specific orchestration, not package-level generic logic.

Full integration with BaseFilter pattern

If your project already uses this style:

  • BaseFilter
  • QueryCollectionExtensionInterface
  • Factory
  • Extension
  • Controller

then the correct approach is:

  1. Keep package generic.
  2. Inject or instantiate CursorDataProvider inside your local BaseFilter.
  3. Let factories decide which cursor columns to use.

BaseFilter integration example

Example local project class:

<?php declare(strict_types=1);

namespace app\components\base;

use app\components\base\search\interfaces\FilterInterface;
use app\components\base\search\interfaces\QueryCollectionExtensionInterface;
use app\components\base\search\interfaces\SortInterface;
use Muxtorov\Yii2CursorPagination\CursorDataProvider;
use Yii;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;

class BaseFilter
{
    private string $modelClass;
    private ActiveQuery $query;
    private ?string $filterClass;
    private ?string $sortClass;
    private array $extensionClass;
    private CursorDataProvider $dataProvider;
    private array $cursorColumns;

    public function __construct(
        string $modelClass,
        ?string $filterClass = null,
        ?string $sortClass = null,
        ?array $extensionClass = [],
        array $cursorColumns = ['id']
    ) {
        $this->modelClass = $modelClass;
        $this->query = $modelClass::find();
        $this->filterClass = $filterClass;
        $this->sortClass = $sortClass;
        $this->extensionClass = $extensionClass ?? [];
        $this->cursorColumns = $cursorColumns;
        $this->dataProvider = new CursorDataProvider();
    }

    public function getData(?array $params = []): array
    {
        foreach ($this->extensionClass as $extensionClass) {
            /** @var QueryCollectionExtensionInterface $ext */
            $ext = new $extensionClass();
            $ext->applyToCollection($this->query, $this->modelClass, $params);
        }

        if ($this->filterClass !== null) {
            /** @var FilterInterface $filter */
            $filter = new $this->filterClass();
            $filter->apply($this->query, $this->modelClass);
        }

        if ($this->sortClass !== null) {
            /** @var SortInterface $sort */
            $sort = new $this->sortClass();
            $sort->apply($this->query, Yii::$app->request->get('sort', ''));
        }

        return $this->dataProvider->getData($this->query, $this->cursorColumns);
    }
}

Why cursorColumns matter

If your extension sorts by multiple columns, cursor pagination must use the same order.

Bad:

$query->orderBy(['order' => SORT_ASC, 'id' => SORT_ASC]);
// but provider uses only 'id'

Good:

$query->orderBy(['order' => SORT_ASC, 'id' => SORT_ASC]);
// provider uses ['order', 'id']

Rule:

  • one sort column: use ['id'] or your real sort column
  • multiple sort columns: always pass all of them in the same order
  • last column should usually be unique like id

QueryCollectionExtensionInterface example

Your app-level interface can stay exactly as it is:

<?php declare(strict_types=1);

namespace app\components\base\search\interfaces;

use yii\db\ActiveQuery;

interface QueryCollectionExtensionInterface
{
    public function applyToCollection(
        ActiveQuery $query,
        string $modelClass,
        ?array $params = []
    ): void;
}

Extension example

Example for status list:

<?php declare(strict_types=1);

namespace api\modules\v1\extensions;

use app\components\base\search\interfaces\QueryCollectionExtensionInterface;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;

class StatusExtension implements QueryCollectionExtensionInterface
{
    public function applyToCollection(ActiveQuery $query, string $modelClass, ?array $params = []): void
    {
        /** @var ActiveRecord $modelClass */
        $table = $modelClass::tableName();

        $query->andWhere(["$table.is_api_visible_column" => true]);
        $query->orderBy([
            "$table.order" => SORT_ASC,
            "$table.id" => SORT_ASC,
        ]);
    }
}

For this extension your cursor columns must be:

['order', 'id']

Factory example

Example factory for status endpoint:

<?php declare(strict_types=1);

namespace api\modules\v1\factories;

use api\modules\v1\extensions\StatusExtension;
use api\modules\v1\resources\StatusResource;
use app\components\base\BaseFilter;

class StatusFilterFactory
{
    public function create(): BaseFilter
    {
        return new BaseFilter(
            StatusResource::class,
            null,
            null,
            [StatusExtension::class],
            ['order', 'id']
        );
    }

    public function getStatus(): array
    {
        return $this->create()->getData();
    }
}

Example factory for package endpoint:

<?php declare(strict_types=1);

namespace api\modules\v1\factories;

use api\modules\v1\extensions\PackageExtension;
use api\modules\v1\resources\PackageResource;
use app\components\base\BaseFilter;

class PackageFilterFactory
{
    public function create(): BaseFilter
    {
        return new BaseFilter(
            PackageResource::class,
            null,
            null,
            [PackageExtension::class],
            ['id']
        );
    }

    public function getPackages(): array
    {
        return $this->create()->getData();
    }
}

Controller example

<?php declare(strict_types=1);

namespace api\modules\v1\controllers;

use api\controllers\AbstractController;
use api\constants\ApiMessages;
use api\modules\v1\factories\StatusFilterFactory;
use api\components\Yii;

class StatusController extends AbstractController
{
    private StatusFilterFactory $filterFactory;

    public function __construct($id, $module, StatusFilterFactory $filterFactory, $config = [])
    {
        $this->filterFactory = $filterFactory;
        parent::__construct($id, $module, $config);
    }

    public function actionIndex(): array
    {
        return $this->success(
            Yii::t('api', ApiMessages::SUCCESS_RETRIEVED),
            $this->filterFactory->getStatus()
        );
    }
}

Starting a new API endpoint from zero

When you create a new list API, use this checklist.

1. Create resource

class ProductResource extends \app\models\Product
{
}

2. Create extension

class ProductExtension implements QueryCollectionExtensionInterface
{
    public function applyToCollection(ActiveQuery $query, string $modelClass, ?array $params = []): void
    {
        $table = $modelClass::tableName();

        $query->andWhere(["$table.is_active" => 1]);
        $query->orderBy([
            "$table.created_at" => SORT_DESC,
            "$table.id" => SORT_DESC,
        ]);
    }
}

3. Create factory

class ProductFilterFactory
{
    public function create(): BaseFilter
    {
        return new BaseFilter(
            ProductResource::class,
            \app\components\base\search\DefaultFilter::class,
            null,
            [ProductExtension::class],
            ['created_at', 'id']
        );
    }

    public function getProducts(?array $params = []): array
    {
        return $this->create()->getData($params);
    }
}

4. Create controller action

class ProductController extends AbstractController
{
    private ProductFilterFactory $filterFactory;

    public function __construct($id, $module, ProductFilterFactory $filterFactory, $config = [])
    {
        $this->filterFactory = $filterFactory;
        parent::__construct($id, $module, $config);
    }

    public function actionIndex(): array
    {
        return $this->success(
            'OK',
            $this->filterFactory->getProducts()
        );
    }
}

5. Call the API

First request:

GET /v1/products?per-page=20

Next page:

GET /v1/products?per-page=20&cursor=PASTE_NEXT_CURSOR&direction=next

Previous page:

GET /v1/products?per-page=20&cursor=PASTE_PREV_CURSOR&direction=prev

Real response example

{
  "status": true,
  "message": "OK",
  "status_code": 200,
  "data": {
    "items": [
      {
        "id": 120,
        "name": "Example"
      }
    ],
    "_links": {
      "self": "http://api.test/v1/products?per-page=1",
      "next": "http://api.test/v1/products?per-page=1&cursor=WzEyMF0&direction=next",
      "prev": "http://api.test/v1/products?per-page=1&cursor=WzEyMF0&direction=prev"
    },
    "_meta": {
      "perPage": 1,
      "direction": "next",
      "hasNext": true,
      "hasPrev": false,
      "nextCursor": "WzEyMF0",
      "prevCursor": "WzEyMF0"
    }
  }
}

Common mistakes

1. Provider class name and file name mismatch

Wrong:

CustomerDataProvider.php
class CursorDataProvider

Use matching names:

CursorDataProvider.php
class CursorDataProvider

2. Query order does not match cursor columns

Wrong:

$query->orderBy(['created_at' => SORT_DESC, 'id' => SORT_DESC]);
$provider->getData($query, ['id']);

Correct:

$query->orderBy(['created_at' => SORT_DESC, 'id' => SORT_DESC]);
$provider->getData($query, ['created_at', 'id']);

3. Returning 404 for empty paginated list

For collection endpoints prefer:

  • 200 OK
  • empty items
  • valid _meta

Avoid 404 for an empty page.

4. Non-unique sort column

Wrong:

['created_at']

Better:

['created_at', 'id']

api recommendation

For api, the best structure is:

  • package: only CursorDataProvider
  • project: BaseFilter
  • project: QueryCollectionExtensionInterface
  • project: all factories
  • project: all extensions

That gives you:

  • reusable package
  • project-specific flexibility
  • clean separation
  • easy copy-paste onboarding for new APIs

Practical mapping for current api

  • StatusFilterFactory: use ['order', 'id']
  • PackageFilterFactory: use ['id']
  • OrderFilterFactory: use real query sort columns, usually ['id'] or ['created_at', 'id']
  • any list sorted by custom priority: include priority column first, id last

Summary

If you start a new API:

  1. create Resource
  2. create Extension
  3. define query order
  4. pass same order columns to BaseFilter
  5. return factory result from controller
  6. use nextCursor and prevCursor from response

This package is intentionally small so it can plug into your existing Yii2 architecture without forcing you to move project-specific abstractions into the package.

muxtorov/yii2-cursor-pagination 适用场景与选型建议

muxtorov/yii2-cursor-pagination 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1 次下载、GitHub Stars 达 1, 最近一次更新时间为 2026 年 04 月 06 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 muxtorov/yii2-cursor-pagination 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 muxtorov/yii2-cursor-pagination 我们能提供哪些服务?
定制开发 / 二次开发

基于 muxtorov/yii2-cursor-pagination 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 1
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 33
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-06