Пагиниране на резултати от база данни
При създаването на уеб приложения много често ще се сблъскате с изискването за ограничаване на броя на изведените елементи на страница.
Ще изходим от състояние, в което извеждаме всички данни без
пагиниране. За избор на данни от базата данни имаме клас 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>
|
<a n:href="default, $paginator->page-1">Предишна</a>
|
{/if}
Страница {$paginator->getPage()} от {$paginator->getPageCount()}
{if !$paginator->isLast()}
|
<a n:href="default, $paginator->getPage() + 1">Следваща</a>
|
<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>
|
<a n:href="default, $page - 1">Предишна</a>
|
{/if}
Страница {$page} от {$lastPage}
{if $page < $lastPage}
|
<a n:href="default, $page + 1">Следваща</a>
|
<a n:href="default, $lastPage">Последна</a>
{/if}
</div>
По този начин внедрихме механизъм за пагиниране без използване на Paginator.