На этом шаге мы с вами научимся создавать постраничную навигацию по статьям в MODx CMS.

Лирическое отступление

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

Ну что, двигаемся дальше?!

Постраничное разбиение

Как вы помните (смею надеяться :), на главной странице сейчас у нас выводится список статей с кратким описанием и ссылкой на полные версии. Этот список ранее мы уже отсортировали по дате добавления, начиная с самых новых статей.

Однако сейчас выводятся абсолютно все статьи, находящиеся в папке "Блог", а это значит, добавляя новые статьи, в итоге мы получим длиннющий лист на главной странице. Это некрасиво и очень неудобно. Следовательно, необходимо ограничивать вывод статей на одну страницу, например, последними пятью статьями, а остальные переносить на следующие страницы.

Вывод статей на главной странице обеспечивает сниппет "Articles". Нам придется модифицировать его, чтобы создать автоматическое разделение на страницы. Перед тем, как начнем работать непосредственно с программным кодом, прикинем в теории, что именно потребуется сделать:

  • Поскольку сейчас выводятся все статьи на одной странице, нужно сделать ограничение на количество статей. Пусть это будет 5 (пять) статей на страницу; в дальнейшем добавим дополнительную переменную – параметр, содержащий нужное количество статей на одну страницу.
  • Сниппет будет автоматически генерировать навигацию "Назад" – "Вперед"; содержимое ссылок в навигации будет зависеть от выбранной страницы, т.е. перейдя на одну страницу "Назад", ссылка должна соответственно измениться; кроме того, нужно учесть, что будут существовать два момента, когда либо ссылка "Назад", либо ссылка "Вперед" не будет показана (почему? - задание на дом :).
  • Необходимо иметь ввиду, что сниппет будет получать некоторые параметры извне (поговорим об этом ниже), а это всегда сигнал о критическом внимании к безопасности программного кода. Кроме того, нужно учитывать момент оптимизации нагрузки базы данных, поскольку количество статей может быть неограниченным.

Ну что ж, от теории плавно переходим к практическим упражнениям :).

$results = $modx->getDocumentChildren(
 $id = 1, // ID родительского документа, а именно документа "Блог"
   $active = 1, // Выбираем только опубликованные документы
   $deleted = 0, // Выбираем только неудаленные документы 
    'id, pagetitle, published, introtext, content, menuindex, createdby, createdon, deleted,  menutitle', // Выбираем поля из БД 
    $where = '', // Дополнительные условия не требуются
    $sort='createdon', // Сортируем документы по полю createdon, т.е. по дате создания
 $dir='DESC', // Сортируем документы по убыванию
    $limit = '' // Ограничения не устанавливаем (параметр LIMIT в SQL запросе)
);

Представленный выше код – это уже известная функция getDocumentChildren из API MODx, которую мы использовали в сниппете "Articles". Обратим внимание на один из ее параметров, а именно параметр $limit. Как понятно из комментария, этот параметр является значением LIMIT в SQL запросе.

Модифицируем слегка наш сниппет, чтобы "прочувствовать" смысл этого параметра в действии:

$results = $modx->getDocumentChildren(
 $id = 1, // ID родительского документа, а именно документа "Блог"
   $active = 1, // Выбираем только опубликованные документы
   $deleted = 0, // Выбираем только неудаленные документы 
    'id, pagetitle, published, introtext, content, menuindex, createdby, createdon, deleted,  menutitle', // Выбираем поля из БД 
    $where = '', // Дополнительные условия не требуются
    $sort='createdon', // Сортируем документы по полю createdon, т.е. по дате создания
 $dir='DESC', // Сортируем документы по убыванию
    $limit = '0,5' // Вывод пяти документов, начиная с первого
);

Сохраните сниппет и обновите главную страницу: отобразятся только пять последних добавленных статей, начиная с самой новой.

Необходимо отметить, что первая цифра (нуль, по логике базы данных) здесь обозначает первый документ, с которого надо начинать выборку, а вторая цифра (пять) обозначает, сколько всего требуется выбрать документов.

Итак, теперь мы можем ввести дополнительный параметр $num, обзначающий количество статей, а также $start, обозначающий номер документа в выборке, с которого будет вестись отсчет.

$num = 5; // Количество статей на одну страницу
 
$start = 0; // Номер начального документа в выборке
 
$results = $modx->getDocumentChildren(
  $id = 1, // ID родительского документа, а именно документа "Блог"
   $active = 1, // Выбираем только опубликованные документы
   deleted = 0, // Выбираем только неудаленные документы 
  'id, pagetitle, published, introtext, content, menuindex, createdby, createdon, deleted,  menutitle', // Выбираем поля из БД
 $where = '', // Дополнительные условия не требуются
    $sort='createdon', // Сортируем документы по полю createdon, т.е. по дате создания
 $dir='DESC', // Сортируем документы по убыванию
    $limit = $start.",".$num  // Вывод $num документов, начиная с $start
);

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

$limit = $start.",".$num // Вывод $num документов, начиная с $start

Задумаемся теперь о первом значении $start параметра LIMIT, т.е. в данном случае нуле. Еще раз – он обозначает стартовый документ, с которого начинается выборка из базы данных.

Стартовый документ на главной странице равен нулю. Пусть наша текущая страница тоже будет иметь номер 0 (нуль).

Хорошо, а что если мысленно представить, что мы нажали ссылку "Назад" в навигации? Это значит, что теперь текущая страница получила порядковый номер 1 (один), а отсчет документов в выборке должен начаться уже с 5-го (пятого) документа. При этом второе значение 5 (пять), т.е. параметр $num, у нас неизменно, т.к. общее количество выводимых статей на одну страницу всегда одинаковое.

Теперь также мысленно шагаем еще на одну страницу назад. Текущая страница получает порядковый номер 2, отсчет документов начинается с 10-го (десятого) документа.

Еще назад! Текущая страница – порядковый номер 3, отсчет документов – с 15-го. И так далее.

Теперь нетрудно заметить зависимость в этих последовательностях: первое значение ($start) в LIMIT есть произведение номера текущей страницы ($p) на количество выводимых статей одной страницы ($num), т.е. $start = $p * $num:

$p = 0; // Номер текущей страницы
 
$num = 5; // Количество статей на одну страницу
 
$start = $p * $num; // Номер документа в выборке, с которого будет вестись отсчет
 
$results = $modx->getDocumentChildren(
    $id = 1, // ID родительского документа, а именно документа "Блог"
   $active = 1, // Выбираем только опубликованные документы
   deleted = 0, // Выбираем только неудаленные документы 
  'id, pagetitle, published, introtext, content, menuindex, createdby, createdon, deleted,  menutitle', // Выбираем поля из БД
 $where = '', // Дополнительные условия не требуются
    $sort='createdon', // Сортируем документы по полю createdon, т.е. по дате создания
 $dir='DESC', // Сортируем документы по убыванию
    $limit = $start.",".$num  // Вывод $num документов, начиная с $start
);

Попробуйте поменять значение параметра $p = 1, $p = 2 и т.д. Сохраняя сниппет и обновляя затем главную страницу, вы будете видеть, что будут выводиться разные статьи, как если бы вы переходили по ссылкам "Назад" и "Вперед".

Вручную менять эти значения как-то некрасиво, не правда ли? :) Значит, нам нужно передавать сниппету эти значения $p извне, чтобы можно было эмулировать переход по ссылкам постраничной навигации. Этого легко добиться, используя суперглобальный массив $_GET, т.е. значения, передаваемые в URL, могут быть доступны в любом сниппете, в том числе и нашем, конечно.

Попробуем это реализовать:

$p = $_GET["p"]; // Номер текущей страницы
 
$num = 5; // Количество статей на одну страницу
 
$start = $p * $num; // Номер документа в выборке, с которого будет вестись отсчет
 
$results = $modx->getDocumentChildren(
 $id = 1, // ID родительского документа, а именно документа "Блог"
   $active = 1, // Выбираем только опубликованные документы
   deleted = 0, // Выбираем только неудаленные документы 
  'id, pagetitle, published, introtext, content, menuindex, createdby, createdon, deleted,  menutitle', // Выбираем поля из БД
 $where = '', // Дополнительные условия не требуются
    $sort='createdon', // Сортируем документы по полю createdon, т.е. по дате создания
 $dir='DESC', // Сортируем документы по убыванию
    $limit = $start.",".$num  // Вывод $num документов, начиная с $start
);

После обновления и сохранения сниппета, откроем главную страницу и добавим к адресу параметр &p=1, например, так: http://localhost/modx/&p=1. Перейдем по этому адресу и, меняя значение p=0, p=1, p=2,.. и т.д., в итоге получим то же самое, как при экспериментах с ручным изменением значения $p напрямую в сниппете.

Кстати, очень важно обратить внимание на то, что значением $p может стать любой символ. Это к вопросу о безопасности работы с внешними данными. Попробуйте сейчас ввести p=-1… Ой, ошибка. Почему она здесь появилась? Да все потому, что по логичному мнению базы данных, отрицательного стартового значения не может быть в принципе. А он у нас получается именно отрицательным, смотрите сами:

Если $_GET["p"] передает значение, равное -1 (минус один), то произведение $start = $p * $num даст нам значение -5 (минус пять), т.к. $num в нашем случае соответствует 5 (пяти). При этом в SQL запросе получается бессмыслица: LIMIT -5,5, что в результате и приводит к критической ошибке.

Каков вывод из этого может следовать? Очень простой – нужно всегда жестко контролировать все внешние параметры, т.е. те параметры, которые имеют значение для программного кода и могут быть изменены пользователями случайно или специально. Приведенный выше пример ошибки – самое мягкое, что может случиться. Используя подобные "дыры", злоумышленники могут внедрить вредоносный код, что часто приводит к неприятным последствиям.

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

Мы же ограничимся в данном случае тем, что разрешим вводить пользователю только неотрицательные целые числа, создав специальную функцию numeric для проверки этих данных:

// Проверяет, что переданное значение - неотрицательное целое число
// Возвращает TRUE/FALSE
function numeric($str) {
   return (!ereg("^[0-9]+$", $str)) ? false : true;
}
// Проверяем, что $_GET["p"] содержит только цифры от 0 до 9
// Иначе присваиваем переменной $p = 0
if (numeric($_GET["p"])) {
 $p = $_GET["p"];
}
else {
   $p = 0;
}

Можем снова поэкспериментировать. Как видно, &p=-1 уже не вызывает ошибок. В коде сниппета все "неправильные" значения автоматически заменяются на 0 (нуль).

Теперь попробуйте ввести какое-нибудь большое число, например, &p=1000. Такое значение вполне допустимо в нашем коде. Что же мы видим? Правильно – ничего. Пустая страница. Это логично, поскольку у нас еще нет 5000 статей на сайте.

Однако это некрасиво – выдавать пустую страницу – и, строго говоря, неправильно с точки зрения хорошего программного кода. Такие ситуации тоже должны учитываться и исправляться. Чтобы достичь этого, нам нужно заранее, еще до выполнения запроса в БД, знать общее количество страниц. Запросы, в которых будут значения $_GET["p"], превышающие возможное количество страниц, будут просто игнорироваться.

С помощью следующего SQL кода можно легко получить общее количество статей:

SELECT COUNT( * ) AS cnt
FROM `modx_site_content`
WHERE `parent` =1
AND `published` =1
AND `deleted` =0

Этот код почти равнозначен SQL запросу, формируемому функцией getDocumentChildren, за исключением того, что нам важно получить только общее количество статей. В данном случае сортировка по какому-то полю и/или значение каких-либо полей нас не интересуют вообще, т.к. эти данные никак не влияют на возвращаемое количество.

Чтобы сделать запрос в БД и затем его обработать, воспользуемся еще двумя функциями API: query иgetRow. Строго говоря, эти функции относятся не к самому API MODx, а к API базы данных MODx, поскольку позволяют работать напрямую с базой данных. Это так называемое DB API, хотя для простоты можно считать их как одно целое.

// Просто добавляем статический блок будущей навигации
$output = "
<div id=\"pagination\">
<a href=\"#\">< Назад</a>
<a href=\"#\">Вперед ></a>
</div>
"; 

И замените его следующим кодом:

// Добавляем динамически формируемую навигацию
$output = " <div id=\"pagination\">";
 
// Если $p = 0, значит мы находимся на первой странице и ссылку "Вперед" не нужно показывать
if ($p == 0) {
  $output .= "
   <a href=\"&amp;p=".($p+1)."\">&lt; Назад</a>
   ";
}
// Если $p = $totalPages, значит мы находимся на последней странице и ссылку "Назад" не нужно показывать
else if ($p == $totalPages) {
 $output .= "
   <a href=\"&amp;p=".($p-1)."\">Вперед &gt;</a>
 ";
}
// Если оба варианта не подошли, значит мы где-то посередине между первой и последней страницей
// Следовательно, показываем обе ссылки в навигации
else {
    $output .= "
               <a href=\"blog/modx-pagination/&amp;p=".($p+1)."\">&lt; Назад</a>
              <a href=\"blog/modx-pagination/&amp;p=".($p-1)."\">Вперед &gt;</a>
            ";
}
 
// Просто закрываем блок навигации
$output .= "
         </div>
";

Обновлено: благодаря замечанию посетителя Gazpasser, внесено небольшое, но крайне полезное добавление - в навигационных ссылках добавлена конструкция [~[*id*]~], с помощью которой MODx создает ссылку на текущую страницу, последовательно обрабатывая следующий код:

  • [*id*] MODx преобразует в номер текущей страницы (ID), например, 111
  • а затем [~111~] - в корректный URL, причем MODx автоматически создаст полностью корректный дружественный URL, если таковой используется на вашем сайте

К сожалению, не получилось обмануть парсер MODx при подсветке кода =) В общем, поверьте на слово, там, где в последнем примере кода выводится текущий адрес страницы "blog/modx-pagination/", на самом деле должно быть [~[*id*]~]. Просто MODx обрабатывает эти конструкции и в примерах кода. Но в приложенном коде сниппета можно посмотреть правильный вариант.

Кстати говоря, используя конструкцию типа [~111~], Вы можете создавать прямые ссылки на документы внутри системы управления. Такая возможность часто бывает удобной при работе с сайтом. Кроме того, если в будущем адрес страницы, на которую была поставлена ссылка с помощью этой конструкции, изменится, то MODx автоматически преобразует старый адрес в новый.

Вы не поверите, но это все :)! Остается только выложить обновленный полный код сниппета Articles.

Как всегда, с нетерпением жду комментариев,
Игорь a.k.a Fuzzy.