分页

创建灵活且用户友好的 Web 应用程序的主要障碍之一是设计直观的用户界面。许多应用程序往往会迅速增长,变得越来越复杂,设计人员和程序员都发现他们无法应对显示数百条或数千条记录。重构需要时间,性能和用户满意度可能会下降。

显示每页合理的记录数量一直是每个应用程序的关键部分,并且曾经给开发人员带来很多麻烦。CakePHP 通过提供简洁的方式来分页数据,减轻了开发人员的负担。

CakePHP 控制器中的分页通过 paginate() 方法完成。然后,您在视图模板中使用 Cake\View\Helper\PaginatorHelper 来生成分页控件。

基本用法

您可以使用 ORM 表实例或 Query 对象调用 paginate()

public function index()
{
    // Paginate the ORM table.
    $this->set('articles', $this->paginate($this->Articles));

    // Paginate a select query
    $query = $this->Articles->find('published')->contain('Comments');
    $this->set('articles', $this->paginate($query));
}

高级用法

通过配置 $paginate 控制器属性或作为 paginate()$settings 参数来支持更复杂的用例。这些条件是您分页查询的基础。它们通过从 URL 传入的 sortdirectionlimitpage 参数进行增强。

class ArticlesController extends AppController
{
    protected array $paginate = [
        'limit' => 25,
        'order' => [
            'Articles.title' => 'asc',
        ],
    ];
}

提示

默认的 order 选项必须定义为数组。

您还可以使用 自定义查找方法 在分页中使用 finder 选项

class ArticlesController extends AppController
{
    protected array $paginate = [
        'finder' => 'published',
    ];
}

如果您的查找方法需要其他选项,您可以将这些选项作为查找器的值传递

class ArticlesController extends AppController
{
    // find articles by tag
    public function tags()
    {
        $tags = $this->request->getParam('pass');

        $customFinderOptions = [
            'tags' => $tags
        ];
        // We're using the $settings argument to paginate() here.
        // But the same structure could be used in $this->paginate
        //
        // Our custom finder is called findTagged inside ArticlesTable.php
        // which is why we're using `tagged` as the key.
        // Our finder should look like:
        // public function findTagged(Query $query, array $tagged = [])
        $settings = [
            'finder' => [
                'tagged' => $customFinderOptions
            ]
        ];
        $articles = $this->paginate($this->Articles, $settings);
        $this->set(compact('articles', 'tags'));
    }
}

除了定义一般分页值之外,您还可以在控制器中定义多个分页默认值集。每个模型的名称可以用作 $paginate 属性中的键

class ArticlesController extends AppController
{
    protected array $paginate = [
        'Articles' => [],
        'Authors' => [],
    ];
}

ArticlesAuthors 键的值可以包含基本 $paginate 数组中的所有键。

Controller::paginate() 返回一个 Cake\Datasource\Paging\PaginatedResultSet 的实例,它实现了 Cake\Datasource\Paging\PaginatedInterface

此对象包含分页记录和分页参数。

简单分页

默认情况下,Controller::paginate() 使用 Cake\Datasource\Paging\NumericPaginator 类,该类执行一个 COUNT() 查询来计算结果集的大小,以便可以呈现页码链接。在非常大的数据集上,此计数查询可能非常昂贵。如果您只想显示“下一页”和“上一页”链接,您可以使用“简单”分页器,它不会执行计数查询。

class ArticlesController extends AppController
{
    protected array $paginate = [
        'className' => 'Simple', // Or use Cake\Datasource\Paging\SimplePaginator::class FQCN
    ];
}

使用 SimplePaginator 时,您将无法生成页码、计数器数据、到最后一页的链接或总记录数控件。

分页多个查询

您可以在单个控制器操作中对多个模型进行分页,在控制器的 $paginate 属性和 paginate() 方法的调用中使用 scope 选项。

// Paginate property
protected array $paginate = [
    'Articles' => ['scope' => 'article'],
    'Tags' => ['scope' => 'tag']
];

// In a controller action
$articles = $this->paginate($this->Articles, ['scope' => 'article']);
$tags = $this->paginate($this->Tags, ['scope' => 'tag']);
$this->set(compact('articles', 'tags'));

scope 选项将导致分页器查找作用域的查询字符串参数。例如,以下 URL 可用于同时对标签和文章进行分页

/dashboard?article[page]=1&tag[page]=3

请参阅 分页多个结果 部分,了解如何生成分页的作用域 HTML 元素和 URL。

多次分页同一个模型

要在单个控制器操作中多次对同一个模型进行分页,您需要为该模型定义一个别名。

// In a controller action
$this->paginate = [
    'Articles' => [
        'scope' => 'published_articles',
        'limit' => 10,
        'order' => [
            'id' => 'desc',
        ],
    ],
    'UnpublishedArticles' => [
        'scope' => 'unpublished_articles',
        'limit' => 10,
        'order' => [
            'id' => 'desc',
        ],
    ],
];

$publishedArticles = $this->paginate(
    $this->Articles->find('all', scope: 'published_articles')
        ->where(['published' => true])
);

// Load an additional table object to allow differentiating in the paginator
$unpublishedArticlesTable = $this->fetchTable('UnpublishedArticles', [
    'className' => 'App\Model\Table\ArticlesTable',
    'table' => 'articles',
    'entityClass' => 'App\Model\Entity\Article',
]);

$unpublishedArticles = $this->paginate(
    $unpublishedArticlesTable->find('all', scope: 'unpublished_articles')
        ->where(['published' => false])
);

控制用于排序的字段

默认情况下,可以对表中任何非虚拟列进行排序。这在某些情况下是不可取的,因为它允许用户对未索引的列进行排序,而这些列的排序成本很高。您可以使用 sortableFields 选项设置允许排序的字段列表。当您希望对任何关联数据或可能是分页查询一部分的计算字段进行排序时,需要此选项。

protected array $paginate = [
    'sortableFields' => [
        'id', 'title', 'Users.username', 'created',
    ],
];

任何尝试对不在允许列表中的字段进行排序的请求都将被忽略。

限制每页的最大行数

每页获取的结果数量以 limit 参数的形式公开给用户。通常不希望允许用户获取分页集中所有行。 maxLimit 选项断言任何人都无法从外部将此限制设置得太高。默认情况下,CakePHP 将可以获取的最大行数限制为 100。如果此默认值不适合您的应用程序,您可以在分页选项中调整它,例如将其减少到 10

protected array $paginate = [
    // Other keys here.
    'maxLimit' => 10
];

如果请求的限制参数大于此值,它将被减少到 maxLimit 值。

超出范围的页面请求

Controller::paginate() 在尝试访问不存在的页面时会抛出一个 NotFoundException,即请求的页码大于总页数。

因此,您可以在捕获到 NotFoundException 时让正常的错误页面呈现,或者使用 try catch 块并采取适当的操作。

use Cake\Http\Exception\NotFoundException;

public function index()
{
    try {
        $this->paginate();
    } catch (NotFoundException $e) {
        // Do something here like redirecting to first or last page.
        // $e->getPrevious()->getAttributes('pagingParams') will give you required info.
    }
}

直接使用分页器类

您也可以直接使用分页器。

// Create a paginator
$paginator = new \Cake\Datasource\Paginator\NumericPaginator();

// Paginate the model
$results = $paginator->paginate(
    // Query or table instance which you need to paginate
    $this->fetchTable('Articles'),
    // Request params
    $this->request->getQueryParams(),
    // Config array having the same structure as options as Controller::$paginate
    [
        'finder' => 'latest',
    ]
);

在视图中进行分页

查看 Cake\View\Helper\PaginatorHelper 文档以了解如何创建用于分页导航的链接。