Veritabanı Sonuçlarını Sayfalama

Web uygulamaları geliştirirken, sayfa başına listelenen öğe sayısını sınırlama gereksinimiyle sık karşılaşırsınız; buna sayfalama denir.

Tüm verileri sayfalamasız listelediğimiz bir durumdan başlayalım. Veritabanından veri seçmek için bir ArticleRepository sınıfımız var. Yapıcının yanı sıra, yayımlanmış tüm makaleleri yayın tarihine göre azalan sırada döndüren bir findPublishedArticles metodu içerir.

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,
		);
	}
}

Bu model sınıfını presenter'a enjekte ederiz. Render metodunda yayımlanmış makaleleri alır ve şablona aktarırız:

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 şablonu da makaleleri listelemeyi üstlenir:

{block content}
<h1>Makaleler</h1>

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

Böylece tüm makaleleri listeleyebiliriz, ama makale sayısı arttıkça bu sorun olmaya başlar. İşte o noktada bir sayfalama düzeneği kurmak işe yarar.

Bu düzenek tüm makaleleri birkaç sayfaya böler ve yalnızca o an seçili sayfaya ait makaleleri gösteririz. Toplam sayfa sayısını ve makalelerin bölünmesini, toplam makale sayısı ile sayfa başına istenen makale sayısına dayanarak Paginator aracı hesaplar.

İlk adımda, repository sınıfındaki makale getirme metodunu, yalnızca tek bir sayfaya ait makaleleri döndürebilecek şekilde değiştireceğiz. Ayrıca, Paginator'ı yapılandırmak için gereken, veritabanındaki toplam makale sayısını veren bir metot ekleyeceğiz:

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,
		);
	}

	/**
	 * Yayımlanmış makalelerin toplam sayısını döndürür
	 */
	public function getPublishedArticlesCount(): int
	{
		return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime);
	}
}

Sonra presenter'ı değiştirelim. Geçerli sayfa numarasını renderDefault metoduna aktaracağız. Bu numara URL'nin bir parçası değilse varsayılan değer olarak 1 (ilk sayfa) alacağız.

Ayrıca render metodunu, bir Paginator örneği oluşturup yapılandıracak ve şablonda gösterilecek uygun makaleleri seçecek şekilde genişleteceğiz. Değiştirilmiş HomePresenter şöyle görünür:

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
	{
		// Yayımlanmış makalelerin toplam sayısını al
		$articlesCount = $this->articleRepository->getPublishedArticlesCount();

		// Paginator örneğini oluştur ve yapılandır
		$paginator = new Nette\Utils\Paginator;
		$paginator->setItemCount($articlesCount); // toplam öğe sayısı
		$paginator->setItemsPerPage(10); // sayfa başına öğe
		$paginator->setPage($page); // geçerli sayfa numarası

		// Paginator'ın hesabına göre veritabanından sınırlı bir makale kümesi getir
		$articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset());

		// onları şablona aktar
		$this->template->articles = $articles;
		// ve sayfalama denetimlerini göstermek için Paginator'ın kendisini de
		$this->template->paginator = $paginator;
	}
}

Şablon artık yalnızca geçerli sayfanın makaleleri üzerinde dönüyor. Yalnızca sayfalama bağlantılarını eklememiz gerekiyor:

{block content}
<h1>Makaleler</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">İlk</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() - 1">Önceki</a>
		&nbsp;|&nbsp;
	{/if}

	Sayfa {$paginator->getPage()} / {$paginator->getPageCount()}

	{if !$paginator->isLast()}
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPage() + 1">Sonraki</a>
		&nbsp;|&nbsp;
		<a n:href="default, $paginator->getPageCount()">Son</a>
	{/if}
</div>

Böylece Paginator kullanan sayfalama tamamlandı. Veritabanı katmanı olarak Nette Database Explorer kullanıyorsanız (Nette Database Core yerine), sayfalamayı doğrudan Paginator aracını kullanmadan da gerçekleştirebilirsiniz. Nette\Database\Table\Selection sınıfı, sayfalama mantığını kendi içinde barındıran bir page() metodu içerir.

Bu yaklaşımda repository şöyle görünür:

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');
	}
}

Presenter'da bir Paginator örneği oluşturmamız gerekmez. Bunun yerine, repository'den dönen Selection nesnesinin sunduğu page() metodunu kullanırız:

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
	{
		// Yayımlanmış makaleleri getir
		$articles = $this->articleRepository->findPublishedArticles();

		// ve şablona yalnızca page metodunun hesabıyla sınırlanan bölümünü aktar
		$lastPage = 0;
		$this->template->articles = $articles->page($page, 10, $lastPage);

		// ve sayfalama seçeneklerini göstermek için gereken verileri de
		$this->template->page = $page;
		$this->template->lastPage = $lastPage;
	}
}

Şablona artık Paginator nesnesini aktarmadığımız için, sayfalama bağlantılarını gösteren bölümü uyarlamamız gerekiyor:

{block content}
<h1>Makaleler</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">İlk</a>
		&nbsp;|&nbsp;
		<a n:href="default, $page - 1">Önceki</a>
		&nbsp;|&nbsp;
	{/if}

	Sayfa {$page} / {$lastPage}

	{if $page < $lastPage}
		&nbsp;|&nbsp;
		<a n:href="default, $page + 1">Sonraki</a>
		&nbsp;|&nbsp;
		<a n:href="default, $lastPage">Son</a>
	{/if}
</div>

Böylece sayfalama düzeneğini, Paginator aracını açıkça kullanmadan gerçekleştirmiş olduk.