Лекция 5. SEF-ссылки. Классы ядра JDocument, JUser
Генерация SEF-ссылок (класс JRoute)
В Joomla существует возможность создавать SEF-ссылки (Search-Engine Friendly) вида www.mysite.ru/one/two/three, удобные и для посетителей, и для поисковых систем. Для этого используется единственный метод класса JRoute, переводящий внутреннюю ссылку, генерируемую Joomla, в SEF-ссылку:
string _(string $url, bool $xhtml=true, int $ssl=null)
где
$url | - абсолютный или относительный URI; |
$xhtml | - заменять ли амперсанды на "&"; |
$ssl | - при $ssl=1 полученный URI будет начинаться с протокола https://, в противном случае - http://. |
Данный метод разбирает $url на пары "ключ-значение" и сохраняет результаты разбора в массиве. Из этого массива удаляется элемент option. Его значение добавляется к новому URL в качестве первого сегмента. Затем производится поиск файла /components/com_<option>/router.php. В этом файле должны находиться две функции: функция для генерации SEF-ссылок (<имя компонента>BuildRoute()) и функция для декодирования элементов SEF-ссылок (<имя компонента>ParseRoute()). Метод JRoute::_() вызовет функцию <имя компонента>BuildRoute() и передаст ей массив, полученный при разборе $url. Функция вернет массив $segments. Метод JRoute::_() добавит к новому URL все элементы этого массива, разделив их слэшами. Если в запросе останутся какие-либо необработанные переменные, они будут добавлены в конец URL.
Допустим, метод JRoute::_() получит на вход ссылку вида
index.php?option=com_mycomponent&var1=value1&var2=value2&…&varN=valueN
Тогда он передаст в функцию <имя компонента>BuildRoute() ассоциативный массив
Array([option]=>com_mycomponent [var1]=>value1 [var2]=>value2... [varN]=>valueN)
причем пары "ключ-значение" будут расположены в том порядке, в котором они были в исходной ссылке. Задача функции <имя компонента>BuildRoute() - выбрать необходимые пары и сохранить в результирующем массиве только значения, но так, чтобы впоследствии можно было восстановить соответствующие им ключи. Это достигается использованием какого-либо фиксированного порядка. Функция вернет массив вида
Array([0]=>value1 [1]=>value2 … [N-1]=>valueN)
из которого затем будет создана SEF-ссылка
component/mycomponent/value1/value2/…/valueN
Впоследствии при щелчке на какой-либо SEF-ссылке произойдет обратный процесс. Будет вызван метод <имя компонента>ParseRoute(), который получит на вход массив
Array([0]=>value1 [1]=>value2 ... [N-1]=>valueN)
и вернет ассоциативный массив:
Array([var1]=>value1 [var2]=>value2 … [varN]=>valueN)
который Joomla установит в качестве переменных HTTP-запроса. Таким образом, посетители сайта и поисковые системы увидят SEF-ссылки, а компонент будет работать с обычным набором пар "ключ-значение".
Обратите внимание, что SEF-ссылка не содержит никакой информации о том, как называются ключи массива. Чтобы их можно было восстановить, функция генерации ссылок и функция их декодирования должны неявно задавать шаблон SEF-ссылок для конкретного компонента. В данном примере подразумевается шаблон, который можно сформулировать вербально так: "переменные записываются в следующем порядке: var1, var2, … varN".
Шаблон может быть более сложным. Например, его структура может меняться в зависимости от задачи, т.е. от значения входящей в него переменной task.
В простейшем случае содержимое файла router.php выглядит так:
<?php defined('_JEXEC') or die ('Restricted access'); function <имя компонента>BuildRoute(&$query) { $segments = array(); if (isset($query['var1'])) { $segments[] = $query['var1']; unset($query['var1' ]) ; } ... if(isset($query['varN' ])) { $segments[] = $query['varN']; unset($query['varN']); } return $segments; } function <имя компонента>ParseRoute($segments) { $vars = array(); $vars['var1'] = @$segments[0]; ... $vars['varN'] = @$segments[1]; return $vars; } ?>
Обратите внимание, что массив $query должен быть передан в функцию <имя компонента>BuildRoute() по ссылке. По мере заполнения массива $segments обработанные элементы удаляются из массива $query с помощью unset(). Любые элементы, которые останутся в массиве $query после работы функции <имя компонента>BuildRoute(), останутся и в URL. Если мы передадим $query по значению, вызовы функции unset() будут действовать только на локальную копию этого массива и все элементы старого URL будут появляться после SEF-сегментов.
Документ (класс JDocument)
Документ - это буфер, использующийся для хранения содержимого веб-страницы, которая будет показана пользователю после выполнения запроса.
Получение ссылки на глобальный объект JDocument:
$document = JFactory::getDocument();
Этот объект хранит название, описание, язык, направление текста, дату модификации, кодировку и некоторые другие значения. Класс JDocument содержит несколько методов для получения этих значений: getTitle(), getDescription(), getLanguage(), getDirection(), getModifiedDate(), getCharset() и др. Соответственно, методы для задания этих значений называются setTitle(), setDescription() и т.д. и принимают в качестве аргумента новое значение.
Получение значения мета-тега
string getMetaData(string $name, bool $http_equiv = false)
где
$name | - название тега; |
$http_equiv | - относится ли этот мета-тег к группе http-equiv (например, Content-Type, Refresh и др.). |
Примеры:
echo $document->getMetaData('content-type', true); echo $document->getMetaData('keywords', false);
Изменение значения мета-тега
void setMetaData(string $name, string $content, bool $http_equiv = false, bool $sync = true)
где
$content | - значение атрибута content; |
$sync | - синхронизировать ли тег content-type с MIME-типом документа. |
Пример:
$document->setMetaData('content-type','text/html',true,true);
Добавление скриптов и каскадных таблиц стилей
Перечисленные ниже методы добавляют в секцию <head> соответствующие теги.
Добавление ссылки на скрипт:
void addScript(string $url, string $type = "text/javascript", bool $defer = false, bool $async = false)
где
$url | - URL скрипта; |
$type | - тип скрипта (text/javascript, text/vbscript и т.д.); |
$defer | - добавлять ли к тегу <script> атрибут defer="defer"; |
$async | - добавлять ли к тегу <script> атрибут async="async". |
Пример:
$document->addScript('/components/com_mycomponent/js/script.js');
Добавление непосредственно текста скрипта
void addScriptDeclaration(string $content, string $type = 'text/javascript')
где
$content | - текст скрипта; |
$type | - тип скрипта. |
Пример:
$document->addScriptDeclaration('alert("Hello World")');
Добавление внешней таблицы стилей
void addStyleSheet(string $url, string $type = 'text/css', string $media = null, array $attribs = array())
где
$url | - URL файла CSS; |
$type | - MIME-тип файла; |
$media | - значение атрибута media (screen, print, projection и др.); |
$attribs | - массив других атрибутов тега <link>. |
Пример:
$document->addStyleSheet('/components/com_mycomponent/css/style.css');
Добавление внутренней таблицы стилей
void addStyleDeclaration(string $content, string $type = 'text/css')
где
$content | - код CSS; |
$type | - значение атрибута type будущего тега <style>. |
Пример:
$document->addStyleDeclaration('.myclass { color: red; }');
Пользователь (класс JUser)
Пользователь, просматривающий сайт, представлен объектом класса JUser, доступ к которому можно получить через метод getUser() класса JFactory:
$user = JFactory::getUser();
Чтобы получить доступ к объекту, представляющему какого-либо другого пользователя, необходимо передать в метод getUser() id или логин этого пользователя:
$user = JFactory::getUser(42); $user = JFactory::getUser('admin');
Поля класса JUser
JUser имеет ряд полей, для которых определен уровень доступа public и к которым поэтому можно обращаться непосредственно. Наиболее важные из них перечислены ниже ( таблица 5.1).
Поле | Описание |
---|---|
block | Равно 1, если пользователь заблокирован |
E-mail пользователя | |
guest | Равно 1, если пользователь является гостем, т.е. не залогинен |
id | ID пользователя |
lastvisitDate | Дата и время последнего входа пользователя в систему |
name | Имя пользователя |
params | Настройки пользователя |
registerDate | Дата и время регистрации аккаунта пользователя |
sendEmail | Равно 1, если пользователь согласен получать сообщения с сайта по электронной почте |
username | Логин пользователя |
Например, выведем приветствие для залогиненного пользователя:
if ($user->guest) echo "Пожалуйста, войдите в систему или зарегистрируйтесь"; else echo "Здравствуйте, {$user->name}! Последний раз вы были на сайте ".JHTML::_('date',$user->lastvisitDate);
Получение и изменение настроек пользователя
mixed getParam(string $key, mixed $default = null) mixed setParam(string $key, mixed $value)
где
$key | - ключ параметра; |
$default | - значение параметра по умолчанию; |
$value | - устанавливаемое значение параметра. |
setParam() | - возвращает предыдущее значение параметра. |
Пример:
echo $user->getParam('language','ru-RU'); $user->setParam('language','en-GB');
Практика
Форма для написания вопроса
Измените код конструкции switch в файле myquestions.php, добавив обработку задачи showform:
case 'showform': showForm($option); break;
Добавьте в этот же файл функцию showForm():
function showForm($option) { $user = JFactory::getUser(); if($user->name) $name = $user->name; else $name = ''; HTML_questions::showForm($option, $name); }
Перед вызовом функции вывода HTML-кода мы получаем имя залогиненного в настоящий момент пользователя, если таковой имеется. Код $user = JFactory::getUser() присваивает переменной $user ссылку на объект-представитель залогиненного пользователя. Если удалось получить имя пользователя, то мы сохраняем это значение в переменной $name, а в противном случае этой переменной присваивается пустая строка. Таким образом поле "Автор" в форме для написания вопроса будет уже заполнено, так что залогиненным пользователям не придется его заполнять.
Перейдите в файл myquestions.html.php и добавьте в класс HTML_questions метод showForm():
function showForm($option, $name) { ?> <form action="index.php" method="post"> <table> <tr> <td width="100"> <?php echo JText::_('COM_MYQUESTIONS_AUTHOR');?>: </td> <td> <input class="text_area" type="text" name="name" id="name" size="50" maxlength="255" value="<?php echo $name;?>"/> </td> </tr> <tr> <td width="100"> <?php echo JText::_('COM_MYQUESTIONS_CITY');?>: </td> <td> <input class="text_area" type="text" name="city" id="city" size="50" maxlength="50"/> </td> </tr> <tr> <td width="100"> <?php echo JText::_('COM_MYQUESTIONS_EMAIL');?>: </td> <td> <input class="text_area" type="text" name="email" id="email" size="50" maxlength="50"/> </td> </tr> <tr> <td width="100"> <?php echo JText::_('COM_MYQUESTIONS_QUESTION');?>: </td> <td> <textarea name='question' id='question' class='inputbox' rows='15' cols='38'></textarea> </td> </tr> <tr> <td width="100"> <?php echo JText::_('COM_MYQUESTIONS_PUBLISHED');?>: </td> <td> <input type="hidden" name="published" value="0"/> <input type="checkbox" name="published" id="published" value="1"/> </td> </tr> </table> <input type="hidden" name="task" value="addquestion"/> <input type="hidden" name="option" value="<?php echo $option;?>"/> <input type="submit" class="button" id="button" value="<?php echo JText::_('COM_MYQUESTIONS_SENDBUTTON');?>"/> </form> <?php }
Листинг .
Как видите, в данной форме сразу два элемента с именем published. Дело в том, что, когда флажок установлен в выбранное состояние, то сценарию-обработчику формы в числе других параметров приходит пара "имя_флажка=значение". Однако когда флажок не установлен, эта пара не посылается. Поэтому используется следующий прием: перед флажком в форме помещается одноименное скрытое поле со значением, равным нулю. Тогда если флажок не установлен, то сценарий получит пару published=0. Если же он установлен, то сценарий тоже получит эту пару, но сразу же последует пара published=1, которая перекроет значение скрытого поля.
Так как мы поместили на форму скрытый элемент task со значением addquestion, то она будет обработана при обработке задачи addquestion. Поэтому добавьте в конструкцию switch в файле myquestions.php следующий код:
case 'addquestion': addQuestion($option); break;
Добавьте в этот же файл функцию addQuestion():
function addQuestion($option) { $row =& JTable::getInstance('question', 'Table'); if (!$row->bind(JRequest::get('post'))) { echo "<script> alert('".$row->getError()."'); window.history.go(-1); </script>\n"; exit(); } $row->question = nl2br(htmlspecialchars(JRequest::getVar('question', '', 'post', 'string',JREQUEST_ALLOWRAW), ENT_QUOTES)); $row->IP = getenv('REMOTE_ADDR'); $row->date = JFactory::getDate()->toFormat(); $row->id_cat = 1; if (!$row->store()) { echo "<script> alert('".$row->getError()."'); window.history.go(-1); </script>\n"; exit(); } $mailer = JFactory::getMailer(); $mailer->setSender('test@mysite.ru'); $mailer->addRecipient('admin@mysite.ru'); $mailer->setSubject(JText::_('COM_MYQUESTIONS_ADMIN_LETTER_SUBJECT')); $mailer->setBody(JText::sprintf('COM_MYQUESTIONS_ADMIN_LETTER_NEW_QUESTION',$row->question)); $mailer->IsHTML(true); if ($mailer->Send() !== true) { echo "<script> alert('".JText::_('COM_MYQUESTIONS_ADMIN_LETTER_ERROR')."'); window.history.go(-1); </script>\n"; exit(); } global $app; $app-> redirect(JRoute::_('index.php?option='.$option.'&task=view&view=all'), JText::sprintf('COM_MYQUESTIONS_QUESTION_SENT',$row->name)); }
Текст вопроса, введенный пользователем, пропускается через функцию htmlspecialchars(), преобразующую специальные символы в HTML-сущности. Таким путем предотвращается ввод нежелательных HTML-тегов. Затем результат пропускается через функцию nl2br(), вставляющую код разрыва строки <br/> перед каждым переводом строки, чтобы текст вопроса при выводе на веб-странице не слился в одну строку.
IP-адрес пользователя определяется с помощью функции getenv(), которая возвращает значение переменной окружения, в данном случае - REMOTE_ADDR.
По умолчанию вопросу присваивается категория с id, равным 1, то есть "Без категории".
Добавьте в файл language/ru-RU/ru-RU.com_myquestions.ini код:
COM_MYQUESTIONS_ADD_QUESTION="Задать вопрос" COM_MYQUESTIONS_AUTHOR="Автор" COM_MYQUESTIONS_DATE="Дата вопроса" COM_MYQUESTIONS_QUESTION="Текст вопроса" COM_MYQUESTIONS_CITY="Город" COM_MYQUESTIONS_EMAIL="e-mail" COM_MYQUESTIONS_CATEGORY="Категория" COM_MYQUESTIONS_PUBLISHED="Отображать ли вопрос на сайте" COM_MYQUESTIONS_SENDBUTTON="Отправить вопрос" COM_MYQUESTIONS_QUESTION_SENT="Спасибо, %s! Ваш вопрос отправлен. Он будет опубликован на сайте после получения ответа" COM_MYQUESTIONS_ADMIN_LETTER_SUBJECT="Новый вопрос на сайте" COM_MYQUESTIONS_ADMIN_LETTER_NEW_QUESTION="<p>Добрый день!</p><p>На сайте появился новый вопрос:</p><p><i>%s</i></p>" COM_MYQUESTIONS_ADMIN_LETTER_ERROR="Ошибка отправки письма"
Осталось добавить ссылку для написания вопроса. Измените начало функции showCategories() так:
function showCategories($rows, $option) { ?> <p><a href='index.php?option=<?=$option?>&task=showlist'><?=JText::_('COM_MYQUESTIONS_ALL_QUESTIONS') ?></a></p> <p><a href='index.php?option=<?=$option?>&task=showform'><?=JText::_('COM_MYQUESTIONS_ADD_QUESTION') ?></a></p>
Теперь на главной странице компонента во фронтенде появилась ссылка "Задать вопрос" ( рис. 5.1).
Рис. 5.1. Ссылка "Задать вопрос"
При переходе по этой ссылке появляется форма для написания вопроса ( рис. 5.2). Обратите внимание, что в поле "Автор" подставилось имя текущего пользователя, если он залогинен.
Рис. 5.2. Форма для написания вопроса
После написания вопроса и нажатия кнопки "Отправить вопрос" происходит перенаправление на главную страницу компонента с сообщением об успешной отправке вопроса ( рис. 5.3).
Рис. 5.3. Сообщение об отправке вопроса
Зайдите в папку <путь к Денверу>/tmp/!sendmail и найдите в ней файл *.eml, содержащий письмо-уведомление администратора о новом вопросе.
SEF
Включите SEF в бэкенде. Для этого перейдите в меню "Сайт" - "Общие настройки" и убедитесь, что переключатель "Включить SEF (ЧПУ)" установлен в "Да". Если вы используете в качестве веб-сервера Apache со включенным mod_rewrite, то вы можете также установить переключатель "Перенаправление URL" в "Да"; тогда из ваших ссылок исчезнет строка "index.php". Вид раздела "Настройки SEO" при включенном mod_rewrite показан на рис. 5.4.
Рис. 5.4. Раздел панели управления "Настройки SEO"
Если ваша конфигурация не позволяет использовать mod_rewrite, SEF-ссылки все равно могут быть построены, но они будут включать строку "index.php", например: http://www.mysite.ru/index.php/one/two/three.
Нажмите кнопку "Сохранить и закрыть" для сохранения конфигурации. Если вы используете mod_rewrite, убедитесь, что вы переименовали находящийся в корневой папке Joomla файл htaccess.txt в .htaccess (если переименовать файл в проводнике Windows не удается, воспользуйтесь интерфейсом командной строки или каким-либо файловым менеджером, например, Total Commander).
Если вы получили сообщение о том, что ваша конфигурация не может быть перезаписана, задайте те же два значения вручную. Для этого откройте файл configuration.php в корневой папке Joomla, найдите строки:
public $sef = '0'; public $sef_rewrite = '0';
и измените оба значения на "1" вместо "0".
Генерация SEF-ссылок
Напишем функцию для генерации SEF-ссылок. Создайте файл /components/com_myquestions/router.php:
<?php defined('_JEXEC') or die ('Restricted access'); function MyQuestionsBuildRoute(&$query) { $segments = array(); if (isset($query['task'])) { $segments[] = $query['task']; unset($query['task' ]) ; } if(isset($query['id' ])) { $segments[] = $query['id']; unset($query['id']); } return $segments; } ?>
Мы создаем пустой массив $segments. Затем проверяем, есть ли в массиве запроса элемент "task", и в этом случае добавляем значение задачи в массив $segments в качестве первого элемента и затем удаляем task из запроса. Далее мы повторяем тот же процесс для id. Наконец, возвращаем массив $segments, чтобы JRoute::_() могла закончить построение URL.
Исправим функции вывода нашего компонента так, чтобы они выводили SEF-ссылки вместо обычных. Откройте файл /components/com_myquestions/myquestions.html.php и измените код функции showCategories() класса HTML_questions следующим образом:
function showCategories($rows, $option) { ?> <p><a href='<?=JRoute::_('index.php?option='.$option.'&task=showlist')?>'> <?=JText::_('COM_MYQUESTIONS_ALL_QUESTIONS')?></a></p> <p><a href='<?=JRoute::_('index.php?option='.$option.'&task=showform')?>'> <?=JText::_('COM_MYQUESTIONS_ADD_QUESTION')?></a></p> <table> <?php foreach($rows as $row) { $link = JRoute::_('index.php?option='.$option.'&id='.$row->id.'&task=showlist'); echo '<tr><td><p><a href="' . $link . '">'.$row->name. '</a></td><td>'.$row->desc.'</td></tr>'; } ?> </table> <?php }
Измените выделенный код в функции HTML_questions::showQuestions():
foreach($rows as $row) { $link = JRoute::_('index.php?option='.$option.'&id='.$row->id.'&task=showquestion'); $link_cat = JRoute::_('index.php?option='.$option.'&id_cat='.$row->id_cat.'&task=showlist'); ?>
Измените также выделенный код в функции HTML_questions::showQuestion():
function showQuestion($row, $option, $row_cat) { $link_cat = JRoute::_('index.php?option='.$option.'&id_cat='.$row->id_cat.'&task=showlist');
Теперь компонент будет генерировать SEF-ссылки по шаблону, установленному в функции MyQuestionsBuildRoute().
Декодирование SEF-ссылок
Если вы сейчас попытаетесь щелкнуть на одной из SEF-ссылок, то получите сообщение:
"Fatal error: Call to undefined function myquestionsParseRoute() in Y:\home\localhost\www\joomla\includes\router.php on line …".
Напишем функцию для декодирования SEF-ссылок.
Откройте файл /components/com_myquestions/router.php и добавьте следующую функцию:
function MyQuestionsParseRoute ($segments) { $vars = array(); $vars['task'] = @$segments[0]; $vars['id'] = @$segments[1]; return $vars; }
Как видите, в функции MyQuestionsParseRoute() мы считали переменные task и id из массива $segments в том же порядке, в котором мы их записывали в одноименный массив в функции MyQuestionsBuildRoute().
Знаки "@" при получении элементов массива $segments используются для подавления вывода сообщений об обращении к несуществующим элементам массива, т.к. не все наши SEF-ссылки будут содержать id.
Теперь щелкните по какой-либо ссылке во фронтенде и обратите внимание на строку статуса в браузере. Вы должны увидеть URL вида: http://localhost/joomla/component/myquestions/showlist или http://localhost/joomla/component/myquestions/showquestion/1
Ключевые термины
JDocument | - класс для работы с документом. |
JRoute | - класс для создания SEF-ссылок. |
JUser | - класс для работы с данными о пользователе.Документ |
Документ | - буфер, использующийся для хранения содержимого веб-страницы, которая будет показана пользователю после выполнения запроса. |
Функция генерации SEF-ссылок | - функция, которая принимает массив элементов HTTP-запроса и возвращает массив сегментов SEF-ссылки. |
Функция декодирования SEF-ссылок | - функция, которая из массива сегментов SEF-ссылки создает массив переменных HTTP-запроса. |
Шаблон SEF-ссылок | - последовательность сегментов. |
Краткие итоги
SEF-ссылки в Joomla создаются с помощью метода JRoute::_(), который переводит внутреннюю ссылку, генерируемую Joomla, в SEF-ссылку. Чтобы компонент работал с SEF-ссылками, сгенерированными по собственному шаблону, необходимо создать в корневой папке его фронтенда файл router.php, в котором должны находиться функция для генерации SEF-ссылок и функция для их декодирования. Эти функции осуществляют взаимно обратные операции: первая из них из массива элементов HTTP-запроса создает массив сегментов SEF-ссылки, а вторая из массива сегментов SEF-ссылки создает массив переменных HTTP-запроса.
Так как SEF-ссылки не позволяют задать названия переменных запроса, то единственный способ определить, к какой переменной относится то или иное значение сегмента, - это использовать шаблон, который задает последовательность сегментов. Шаблон неявно задается в коде каждой из функций в файле router.php.
Для работы с документом и с данными пользователя в Joomla существуют соответственно классы JDocument и JUser.
Вопросы
- Какой метод переводит внутреннюю ссылку, генерируемую Joomla, в SEF-ссылку?
- Каким образом компоненты работают с SEF-ссылками?
- Для чего служат функции генерации и декодирования SEF-ссылок?
- Что такое шаблон SEF-ссылок и как он задается?
- Какие классы используются для работы с документом и с данными пользователя?