Делаем пагинацию на PHP

07.02.2023 в 19:02
26107
+11

Зачастую в тестовых заданиях на джунов присутствует пункт о реализации пагинации в какой-нибудь системе. И мне довольно часто прилетают просьбы показать как это делается, так как зачастую это задание ставит в тупик начинающих разработчиков. Что-ж, давайте разбираться.

Делать пагинацию я хочу предложить на базе написанного нами в курсе ООП в PHP движка для блога. Готовый проект можно взять по ссылке. Накатите дамп базы и настройте веб-сервер так, чтобы корневой директорией для домена myproject.loc была папка www.

Теперь давайте добавим побольше статей в базу, чтобы было по чему пагинацию делать.

INSERT INTO `articles` (`author_id`, `name`, `text`) VALUES
(1, 'Название статьи 1', 'Текст статьи 1'),
(1, 'Название статьи 2', 'Текст статьи 2'),
(1, 'Название статьи 3', 'Текст статьи 3'),
(1, 'Название статьи 4', 'Текст статьи 4'),
(1, 'Название статьи 5', 'Текст статьи 5'),
(1, 'Название статьи 6', 'Текст статьи 6'),
(1, 'Название статьи 7', 'Текст статьи 7'),
(1, 'Название статьи 8', 'Текст статьи 8'),
(1, 'Название статьи 9', 'Текст статьи 9'),
(1, 'Название статьи 10', 'Текст статьи 10'),
(1, 'Название статьи 11', 'Текст статьи 11'),
(1, 'Название статьи 12', 'Текст статьи 12'),
(1, 'Название статьи 13', 'Текст статьи 13'),
(1, 'Название статьи 14', 'Текст статьи 14'),
(1, 'Название статьи 15', 'Текст статьи 15'),
(1, 'Название статьи 16', 'Текст статьи 16'),
(1, 'Название статьи 17', 'Текст статьи 17'),
(1, 'Название статьи 18', 'Текст статьи 18'),
(1, 'Название статьи 19', 'Текст статьи 19'),
(1, 'Название статьи 20', 'Текст статьи 20');

Посмотрим на имеющиеся у нас статьи. Отсортируем их в обратном порядке.

SELECT * FROM articles ORDER BY id DESC;

Все записи без пагинации

Теперь, если вы еще не в курсе, нужно познакомиться с двумя конструкциями языка SQL: LIMIT и OFFSET. Они позволяют получить только часть строк из тех, что были получены запросом. LIMIT задаёт лимит записей, OFFSET задает количество строк, которые нужно пропустить.

Например, мы хотим пропустить первые 5 строк, и вывести следующие 10 строк:

SELECT * FROM articles ORDER BY id DESC LIMIT 10 OFFSET 5;

OFFSET и LIMIT

Как видим, у нас пропустились первые 5 строк с id: 22,21,20,19,18, которые присутствовали в результате запроса без LIMIT и OFFSET. Также мы видим, что строк у нас здесь 10.

Как нетрудно догадаться, для реализации пагинации нам можно воспользоваться этими конструкциями. Если мы хотим выводить на каждой странице нашего блога по 5 записей, то для получения записей на первой странице стоит использовать запрос:

SELECT * FROM articles ORDER BY id DESC LIMIT 5 OFFSET 0;

Первые 5 записей

Для второй страницы запрос будет следующим:

SELECT * FROM articles ORDER BY id DESC LIMIT 5 OFFSET 5;

Следующие 5 записей

То есть здесь мы пропустили первые 5 записей и вывели следующие 5.

Аналогично строим запрос для третьей страницы:

SELECT * FROM articles ORDER BY id DESC LIMIT 5 OFFSET 10;

И так далее, пока записи не закончатся.

Получаем формулу для получения запроса, которые выводит записи на n-ой странице блога, где k-число записей на одной странице:

SELECT * FROM articles ORDER BY id DESC LIMIT k OFFSET (n-1)*k;

Чтобы понять, сколько у нас будет страниц, нам нужно число записей разделить на число записей, выводимых на одной странице. Полученное число округляем в большую сторону. К примеру, если у нас 22 записи, и на одной странице выводим 5, то число страниц будет 22/5 = 4,4. Округляем в большую сторону и получаем 5. На первых четырех страницах у нас будет по 5 записей, а на последней будет 2 записи.
Получить общее число записей можно с помощью запроса:

SELECT COUNT(*) FROM articles;

Число записей

Осталось всё это дело закодить. Начнём с самого простого – выведем ссылки на странички со статьями.
В классе ActiveRecordEntity добавляем метод для получения количества страниц. Метод будет принимать на вход количество записей на одной странице.

src/MyProject/Models/ActiveRecordEntity.php

public static function getPagesCount(int $itemsPerPage): int
{
    $db = Db::getInstance();
    $result = $db->query('SELECT COUNT(*) AS cnt FROM ' . static::getTableName() . ';');
    return ceil($result[0]->cnt / $itemsPerPage);
}

В MainController вызовем этот метод и передадим в шаблон число страниц.

src/MyProject/Controllers/MainController.php

public function main()
{
    $articles = Article::findAll();
    $this->view->renderHtml('main/main.php', [
        'articles' => $articles,
        'pagesCount' => Article::getPagesCount(5),
    ]);
}

Добавим в шаблоне внизу странички номера страниц.

templates/main/main.php

<?php include __DIR__ . '/../header.php'; ?>
<?php foreach ($articles as $article): ?>
    <h2><a href="/articles/<?= $article->getId() ?>"><?= $article->getName() ?></a></h2>
    <p><?= $article->getText() ?></p>
    <hr>
<?php endforeach; ?>

<div style="text-align: center">
<?php for ($pageNum = 1; $pageNum <= $pagesCount; $pageNum++): ?>
    <a href="/<?= $pageNum === 1 ? '' : $pageNum ?>"><?= $pageNum ?></a>
<?php endfor; ?>
</div>

<?php include __DIR__ . '/../footer.php'; ?>

Обратите внимание: при формировании ссылки на страницу мы проверяем, является ли страничка первой, и если это так, то формируем ссылку на главную страницу, иначе формируем ссылку вида: /n, где n – номер страницы.
Посмотрим на результат:
Номера страничек

Теперь давайте напишем метод для получения записей на n-ой страничке. Для этого в классе ActiveRecordEntity добавим метод getPage(), принимающий на вход количество записей на одной странице и номер страницы.

src/MyProject/Models/ActiveRecordEntity.php

/**
 * @return static[]
 */
public static function getPage(int $pageNum, int $itemsPerPage): array
{
    $db = Db::getInstance();
    return $db->query(
        sprintf(
            'SELECT * FROM `%s` ORDER BY id DESC LIMIT %d OFFSET %d;',
            static::getTableName(),
            $itemsPerPage,
            ($pageNum - 1) * $itemsPerPage
        ),
        [],
        static::class
    );
}

Добавим в MainController экшен для странички.

src/MyProject/Controllers/MainController.php

public function page(int $pageNum)
{
    $this->view->renderHtml('main/main.php', [
        'articles' => Article::getPage($pageNum, 5),
        'pagesCount' => Article::getPagesCount(5),
    ]);
}

Переписываем экшен main.

src/MyProject/Controllers/MainController.php

public function main()
{
    $this->page(1);
}

Проверяем главную страницу:
Первая страница

Добавляем роут для страничек с номерами.

src/routes.php

'~^(\d+)$~' => [\MyProject\Controllers\MainController::class, 'page'],

Всё. Пагинация готова. Можно переходить по страничкам. Так выглядит пятая страница.

Из улучшений можно сделать текущий номер страницы не ссылкой, а просто текстом. Для этого передадим номер текущей странички в шаблон.

src/MyProject/Controllers/MainController.php

public function page(int $pageNum)
{
    $this->view->renderHtml('main/main.php', [
        'articles' => Article::getPage($pageNum, 5),
        'pagesCount' => Article::getPagesCount(5),
        'currentPageNum' => $pageNum,
    ]);
}

В шаблоне будем сравнивать текущий номер страницы с тем, который в текущей итерации.

templates/main/main.php

<?php include __DIR__ . '/../header.php'; ?>
<?php foreach ($articles as $article): ?>
    <h2><a href="/articles/<?= $article->getId() ?>"><?= $article->getName() ?></a></h2>
    <p><?= $article->getText() ?></p>
    <hr>
<?php endforeach; ?>

<div style="text-align: center">
<?php for ($pageNum = 1; $pageNum <= $pagesCount; $pageNum++): ?>
    <?php if ($currentPageNum === $pageNum): ?>
        <b><?= $pageNum ?></b>
    <?php else: ?>
        <a href="/<?= $pageNum === 1 ? '' : $pageNum ?>"><?= $pageNum ?></a>
    <?php endif; ?>
<?php endfor; ?>
</div>

<?php include __DIR__ . '/../footer.php'; ?>

Результат:
Последняя страница

Вот такая простая пагинация у нас получилась. Она подойдет для большинства проектов без высокой нагрузки и с небольшим количеством записей. А для крупных проектов потребуется более специфичное решение, которое я рассмотрю в одном из ближайших уроков.

Изменения, сделанные в ходе урока, вы можете посмотреть на гитхабе.

loader
07.02.2023 в 19:02
26107
+11
Логические задачи с собеседований