Пагиниране на резултати от база данни

При създаването на уеб приложения много често ще се сблъскате с изискването за ограничаване на броя на изведените елементи на страница.

Ще изходим от състояние, в което извеждаме всички данни без пагиниране. За избор на данни от базата данни имаме клас ArticleRepository, който освен конструктор съдържа метод findPublishedArticles, който връща всички публикувани статии, сортирани низходящо по дата на публикуване.

namespace App\Model;

use Nette;

class ArticleRepository
{
	public function __construct(
		private Nette\Database\Connection $database,
	) {
	}

	public function findPublishedArticles(): Nette\Database\ResultSet
	{
		return $this->database->query('
			SELECT * FROM articles
			WHERE created_at < ?
			ORDER BY created_at DESC',
			new \DateTime,
		);
	}
}

В презентера след това инжектираме моделния клас и в render метода изискваме публикуваните статии, които предаваме на шаблона:

namespace App\Presentation\Home;

use Nette;
use App\Model\ArticleRepository;

class HomePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articleRepository,
	) {
	}

	public function renderDefault(): void
	{
		$this->template->articles = $this->articleRepository->findPublishedArticles();
	}
}

В шаблона default.latte след това се грижим за извеждането на статиите:

{block content}
<h1>Статии</h1>

<div class="articles">
	{foreach $articles as $article}
		<h2>{$article->title}</h2>
		<p>{$article->content}</p>
	{/foreach}
</div>

По този начин можем да изведем всички статии, което обаче започва да създава проблеми в момента, когато броят на статиите нарасне. В този момент е подходящо да се внедри механизъм за пагиниране.

Той гарантира, че всички статии ще бъдат разделени на няколко страници и ние ще покажем само статиите от една текуща страница. Paginator сам ще изчисли общия брой страници и разпределението на статиите според това колко статии общо имаме и колко статии на страница искаме да покажем.

В първата стъпка ще променим метода за получаване на статии в класа на repository така, че да може да връща само статии за една страница. Също така ще добавим метод за установяване на общия брой статии в базата данни, който ще ни е необходим за настройка на Paginator:

namespace App\Model;

use Nette;


class ArticleRepository
{
	public function __construct(
		private Nette\Database\Connection $database,
	) {
	}

	public function findPublishedArticles(int $limit, int $offset): Nette\Database\ResultSet
	{
		return $this->database->query('
			SELECT * FROM articles
			WHERE created_at < ?
			ORDER BY created_at DESC
			LIMIT ?
			OFFSET ?',
			new \DateTime, $limit, $offset,
		);
	}

	/**
	 * Връща общия брой публикувани статии
	 */
	public function getPublishedArticlesCount(): int
	{
		return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
	}
}

След това ще се заемем с промените в презентера. В render метода ще предаваме номера на текущо показваната страница. За случая, когато този номер не е част от URL, ще зададем стойност по подразбиране за първата страница.

Освен това ще разширим render метода с получаване на инстанция на Paginator, неговата настройка и избор на правилните статии за показване в шаблона. HomePresenter след промените ще изглежда така:

namespace App\Presentation\Home;

use Nette;
use App\Model\ArticleRepository;

class HomePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articleRepository,
	) {
	}

	public function renderDefault(int $page = 1): void
	{
		// Ще установим общия брой публикувани статии
		$articlesCount = $this->articleRepository->getPublishedArticlesCount();

		// Ще създадем инстанция на Paginator и ще го настроим
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // общ брой статии
		$paginator->setItemsPerPage(10); // брой елементи на страница
		$paginator->setPage($page); // номер на текущата страница

		// От базата данни ще изтеглим ограничено множество статии според изчислението на Paginator
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// което ще предадем на шаблона
		$this->template->articles = $articles;
		// и също така самия Paginator за показване на възможностите за пагиниране
		$this->template->paginator = $paginator;
	}
}

Шаблонът ни вече итерира само върху статиите от една страница, достатъчно е да добавим връзките за пагиниране:

{block content}
<h1>Статии</h1>

<div class="articles">
	{foreach $articles as $article}
		<h2>{$article->title}</h2>
		<p>{$article->content}</p>
	{/foreach}
</div>

<div class="pagination">
	{if !$paginator->isFirst()}
		<a n:href="default, 1">Първа</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->page-1">Предишна</a>
		&nbsp;|&nbsp;
	{/if}

	Страница {$paginator->getPage()} от {$paginator->getPageCount()}

	{if !$paginator->isLast()}
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() + 1">Следваща</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPageCount()">Последна</a>
	{/if}
</div>

Така допълнихме страницата с възможност за пагиниране с помощта на Paginator. В случай, че вместо Nette Database Core като слой за база данни използваме Nette Database Explorer, можем да внедрим пагиниране и без използване на Paginator. Класът Nette\Database\Table\Selection съдържа метод page с логика за пагиниране, взета от Paginator.

Repository при този начин на внедряване ще изглежда така:

namespace App\Model;

use Nette;

class ArticleRepository
{
	public function __construct(
		private Nette\Database\Explorer $database,
	) {
	}

	public function findPublishedArticles(): Nette\Database\Table\Selection
	{
		return $this->database->table('articles')
			->where('created_at < ', new \DateTime)
			->order('created_at DESC');
	}
}

В презентера не е необходимо да създаваме Paginator, вместо него ще използваме метода на класа Selection, който ни връща repository:

namespace App\Presentation\Home;

use Nette;
use App\Model\ArticleRepository;

class HomePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articleRepository,
	) {
	}

	public function renderDefault(int $page = 1): void
	{
		// Ще изтеглим публикуваните статии
		$articles = $this->articleRepository->findPublishedArticles();

		// и в шаблона ще изпратим само тяхната част, ограничена според изчислението на метода page
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// и също така необходимите данни за показване на възможностите за пагиниране
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Тъй като в шаблона сега не изпращаме Paginator, ще променим частта, показваща връзките за пагиниране:

{block content}
<h1>Статии</h1>

<div class="articles">
	{foreach $articles as $article}
		<h2>{$article->title}</h2>
		<p>{$article->content}</p>
	{/foreach}
</div>

<div class="pagination">
	{if $page > 1}
		<a n:href="default, 1">Първа</a>
		&nbsp;|&nbsp;
		<a n:href="default, $page - 1">Предишна</a>
		&nbsp;|&nbsp;
	{/if}

	Страница {$page} от {$lastPage}

	{if $page < $lastPage}
		&nbsp;|&nbsp;
		<a n:href="default, $page + 1">Следваща</a>
		&nbsp;|&nbsp;
		<a n:href="default, $lastPage">Последна</a>
	{/if}
</div>

По този начин внедрихме механизъм за пагиниране без използване на Paginator.