Генерация 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).

Таблица 5.1. Некоторые public-поля класса JUser
ПолеОписание
block Равно 1, если пользователь заблокирован
email 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).

05 01


Рис. 5.1. Ссылка "Задать вопрос"

При переходе по этой ссылке появляется форма для написания вопроса ( рис. 5.2). Обратите внимание, что в поле "Автор" подставилось имя текущего пользователя, если он залогинен.

05 02

Рис. 5.2. Форма для написания вопроса

После написания вопроса и нажатия кнопки "Отправить вопрос" происходит перенаправление на главную страницу компонента с сообщением об успешной отправке вопроса ( рис. 5.3).

05 03

Рис. 5.3. Сообщение об отправке вопроса

Зайдите в папку <путь к Денверу>/tmp/!sendmail и найдите в ней файл *.eml, содержащий письмо-уведомление администратора о новом вопросе.

SEF

Включите SEF в бэкенде. Для этого перейдите в меню "Сайт" - "Общие настройки" и убедитесь, что переключатель "Включить SEF (ЧПУ)" установлен в "Да". Если вы используете в качестве веб-сервера Apache со включенным mod_rewrite, то вы можете также установить переключатель "Перенаправление URL" в "Да"; тогда из ваших ссылок исчезнет строка "index.php". Вид раздела "Настройки SEO" при включенном mod_rewrite показан на рис. 5.4.

05 04

Рис. 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.

Вопросы

  1. Какой метод переводит внутреннюю ссылку, генерируемую Joomla, в SEF-ссылку?
  2. Каким образом компоненты работают с SEF-ссылками?
  3. Для чего служат функции генерации и декодирования SEF-ссылок?
  4. Что такое шаблон SEF-ссылок и как он задается?
  5. Какие классы используются для работы с документом и с данными пользователя?