Взаимодействие элементов архитектуры MVC в Joomla

MVC ("Model - View - Controller") - это набор паттернов проектирования, который предполагает разделение программного кода на три группы:

  • модели (model) используются для хранения данных. В Joomla модели реализуются с помощью абстрактного класса JModel;
  • представления (view) генерируют вывод для заданной информации с помощью шаблона. В Joomla реализуются с помощью абстрактного класса JView;
  • контроллеры (controller) получают команды от пользователя и управляют моделями и представлениями для выполнения этих команд. В Joomla реализуются с помощью абстрактного класса JController.

Приблизительно схема взаимодействия этих групп в коде Joomla представлена на следующей диаграмме последовательности ( рис. 6.1 на основе иллюстрации из книги [4, p.246]).

06 01

Рис. 6.1. Взаимодействие контроллера, модели и представления

В файле /components/com_<имя компонента>/<имя компонента>.php находится код для создания контроллера, например:

$controller = new MyComponentController();
$controller->execute(JRequest::getVar('task'));
$controller->redirect();




В HTTP-запросе задается задача, представление и, при необходимости, другие данные. Метод execute() вызывает метод вашего контроллера, который называется так же, как и заданная задача. Если задача не указана, то ей будет присвоено значение "display", следовательно, будет выполнен метод display(). Для этой и всех остальных задач, которые должен выполнять ваш компонент, необходимо создать в классе контроллера одноименные методы. Наконец, метод redirect() перенаправляет пользователя к другому URL, если такой URL был задан в каком-либо методе при выполнении контроллера.

В простейшем случае класс контроллера описан в файле /components/com_<имя компонента>/controller.php, в более сложных случаях этих классов может быть несколько. Каждый из них должен быть производным от JController:

class MyComponentController extends JController
{
  …
  function display()
  {
    …
    parent::display();
        …
  }
…
}




Вы можете переопределить метод JController::display() в своем классе контроллера. Метод display() базового класса вызывает методы getView(), getModel(), а также метод display() заданного представления. getView() возвращает объект-представитель заданного представления, getModel() - заданной модели. По умолчанию используются те представление и модель, название которых совпадает с именем контроллера.

Далее нас будет интересовать работа метода display() заданного представления.

Каждый класс представления описан в файле /components/com_<имя компонента>/views/<имя представления>/view.html.php и является производным от JView. В этом классе может быть перегружен метод display(), чтобы вызвать метод класса модели для загрузки данных:

class MyComponentViewMyView extends JView
{
  function display($tpl=null)
  {
    $model=&$this->getModel();
    $list=$model->getList();
    $this->assignRef('list', $list);
    parent::display($tpl);
  }
}




Каждый класс модели описан в файле /components/com_<имя компонента>/models/<имя модели>.php и является производным от JModel. В этом классе может находиться метод для загрузки данных из базы данных или другого источника:

class ModelMyComponentMyModel extends JModel
{
  var $_somelist = null;
  function getList()
  {
    if (!$this->_somelist)
    {  
      $query = "SELECT * FROM #__mycomponent";
      $this->_somelist = $this->_getList($query, 0, 0);
    }
    return $this->_somelist;
  }
}




Итак, метод класса представления display() вызывает метод класса модели для загрузки данных и сохраняет результат в какой-либо переменной, которая затем с помощью метода JView::assignRef() связывается с текущим представлением. Наконец, вызывается метод базового класса JView::display(), который загружает файл заданного шаблона при помощи перехвата выходного потока.

Шаблон находится в папке /components/com_<имя компонента>/views/<имя представления>/tmpl. В его коде осуществляется вывод на экран переменных текущего представления. Например:

<table width="100%">
  <?php
    foreach($this->list as $l)
      echo '<tr><td>'.$l->data.'</td></tr>';
  ?>
</table>




Так выглядит простейший вариант взаимодействия моделей, представлений и контроллеров.

Классы Joomla для реализации MVC

JModel

Одно из полей класса JModel - объект-представитель базы данных $_db. Таким образом, для выполнения запросов к базе данных в методах производных от JModel классов нужно обращаться непосредственно к этому полю, не получая новой ссылки на глобальный объект JDatabase:

$this->_db->setQuery($query);
$this->_db->query();




Получение списка каких-либо объектов и количества записей
array _getList(string $query, int $limitstart=0, int $limit=0)
int _getListCount(string $query)




где

$query - запрос к базе данных;
$limitstart - смещение;
$limit - количество записей.



Например:

$query = "SELECT * FROM #__mycomponent";
$list = $this->_getList($query, 0, 0);  
$count = $this->_getListCount($query);




JView

Joomla поддерживает возможность добавления нескольких моделей к одному представлению. В таком случае ссылки на объекты-представители моделей будут храниться в поле _models объекта JView. Для добавления модели используется метод

JModel setModel(object &$model, bool $default = false)




где

$model - имя модели (т.е. имя соответствующего класса);
$default - назначить ли ее моделью по умолчанию.



Метод возвращает добавленную модель.

Получение ссылки на объект-представитель одной из добавленных к представлению моделей
JModel getModel(string $name = null)




где $name - имя модели.

Например, добавим в коде контроллера к представлению SomeView модели Model1 и Model2:

$view = &$this->getView('SomeView', 'html');
$view->setModel($this->getModel('Model1'), true);
$view->setModel($this->getModel('Model2'));




Получение данных из зарегистрированной модели или поля представления
mixed get(string $property, string $default = null) 




где

$property - название метода модели, который требуется вызвать, или поля представления. В первом случае будет вызван метод get<Название метода>() - обратите внимание на заглавную букву;
$default - если данные должны быть получены из модели, то $default - имя модели. Если требуется получить значение поля, то $default - значение, которое будет возвращено, если такое поле отсутствует.



Например, если в модели, заданной для текущего представления по умолчанию, есть метод getValue(), то получить в классе представления возвращаемое им значение можно так:

$temp = &$this->get('value');




Связывание переменной с представлением
bool assignRef(string $key, mixed &$val) 




где

$key - имя поля объекта-представителя представления. Не может начинаться со знака подчеркивания;
$val - значение поля.



Пример:

$view->assignRef('somevar', $someval); 




Выполнение и отображение скрипта шаблона
void display(string $tpl = null)
string loadTemplate(string $tpl = null)




где $tpl - имя файла шаблона. Конкретное имя файла зависит от значений имени и расширения макета, заданных в классе, по умолчанию это соответственно default и php. Если вы хотите изменить эти значения, используйте методы setLayout() и setLayoutExt(). Будет произведен поиск файла <имя макета>_<$tpl>.<расширение макета> или при $tpl=null <имя макета>.<расширение макета>.

display() выводит на экран результат работы скрипта шаблона, а loadTemplate() только возвращает этот результат. При ошибке display() возвращает объект Exception.

Например:

echo $view->loadTemplate('mytpl'); 




отобразит результат выполнения скрипта /components/com_<имя компонента>/views/<имя представления>/tmpl/default_mytpl.php.

JController

Выполнение задачи путем вызова одноименного метода производного класса
mixed execute(string $task) 




где $task - имя задачи. Если такой задачи не найдется, будет выполнена задача "__default".

Метод возвращает значение, возвращаемое вызванным методом, или false в случае ошибки.

Например, код

$controller->execute('addItem'); 




приведет к вызову метода addItem() контроллера $controller.

Регистрация задачи

Регистрация задачи - это ее сопоставление какому-либо методу класса, производного от JController.

JController registerTask(string $task, string $method) 




где

$task - задача;
$method - имя метода.



Пример в коде контроллера:

$this->registerTask('save', 'saveItem'); 




Стандартная реализация метода display()

О методе display() говорилось выше.

JController display(bool $cachable = false, array $urlparams = false) 




где

$cachable - задает, кэшировать ли вывод представления;
$urlparams - массив пар "имя-значение" для URL, использующихся при кэшировании.



Получение ссылки на текущее представление
JView getView(string $name = '', string $type = '', string $prefix = '', array $config = array())




где

$name - имя представления. По умолчанию совпадает с именем контроллера;
$type - тип представления, который можно определить как $document->getType();
$prefix - префикс класса представления. По умолчанию <имя контроллера>View;
$config - массив параметров, которые будут переданы в конструктор представления, - имя представления, кодировка, путь к директории шаблонов и т.д.



Если класс <префикс><имя представления> не найдется в директориях, заданных по умолчанию, то будет произведен его поиск в файле <имя представления>/view.<тип представления>.php. Например, если в коде класса MyComponentController есть строка

$view = &$this->getView('Item', 'html');




то будет произведен поиск класса MyComponentViewItem в файле /components/com_<имя компонента>/views/Item/view.html.php.

Получение объекта-представителя модели
JModel getModel(string $name = '', string $prefix = '', array $config = array()) 




где

$name - имя модели. По умолчанию совпадает с именем контроллера;
$prefix - префикс класса модели. По умолчанию <имя контроллера>Model;
$config - массив параметров, которые будут переданы в конструктор модели.



Задание параметров для будущего перенаправления
JController setRedirect(string $url, string $msg=null, string $type=null) 




где

$url - URL для перенаправления;
$msg - сообщение для пользователя;
$type - тип сообщения. По умолчанию - "message".



Например, в коде контроллера можно написать:

$this->setRedirect('index.php?option=com_mycomponent', 'Текст сообщения', 'notice'); 




Перенаправление браузера
bool redirect() 




Метод возвращает false, если URL для перенаправления не был задан заранее.

Практика

Модели

Модель для списка всех категорий

В папке /components/com_myquestions создайте папку models, а в ней - файл all.php:

<?php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.application.component.model');
class ModelMyQuestionsAll extends JModel
{
  var $_categories = null;
  function getList()
  {
    if (!$this->_categories)
    {  
      $query = "SELECT id, name, `desc` FROM #__myquestions_categories";
      $this->_categories = $this->_getList($query, 0, 0);
    }
    return $this->_categories;
  }
}
?>




Мы подключаем библиотеку моделей Joomla и объявляем класс ModelMyQuestionsAll как производный от класса JModel. В классе хранится список категорий _categories. Метод getList() проверяет, загружен ли список категорий. Если нет, то мы создаем запрос, чтобы выбрать из базы данных все категории вопросов, и получаем их с помощью метода _getList() класса JModel.

Модель для списка вопросов из какой-либо категории или из всех категорий

Создайте файл /components/com_myquestions/models/category.php:

<?php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.application.component.model');
class ModelMyQuestionsCategory extends JModel
{
  var $_questions = null;
  var $_id = null;
  var $_name = null;
  function __construct()
  {
    parent::__construct();
    $id = JRequest::getVar('id','all');
    $this->_id = $id;
  }
  function getList()
  {
    if (!$this->_questions)
    {
      if ($this->isAllCat())
        $id_text = "";
      else
        $id_text = " id_cat={$this->_id} AND ";
      $query = "SELECT q.id, q.question, q.name, q.date, q.email, q.city, q.answer, c.id AS id_cat, 
      c.name AS name_cat FROM #__myquestions q, #__myquestions_categories c WHERE $id_text answer <> 
      '' AND (published = 1 OR (expiration_date <> '0000-00-00 00:00:00' AND expiration_date > NOW())) 
      AND q.id_cat=c.id";
      $this->_questions = $this->_getList($query, 0, 0);  
    }
    return $this->_questions;
  }
  function getCatName()
  {
    if (!$this->_name)
    {
      if (!$this->isAllCat())
      {
        $query = "SELECT name FROM #__myquestions_categories WHERE id = '" . $this->_id . "'";
        $this->_db->setQuery($query);
        $this->_name = $this->_db->loadResult();
      }
    }
    if (!$this->isAllCat())
      return $this->_name;
    else
      return JText::_('COM_MYQUESTIONS_ALL_QUESTIONS');
  }
  Function isAllCat()
  {
    if ($this->_id=='all')
      return true;
    return false;
  }
}
?>


 Листинг .

В данном классе хранятся список вопросов _questions, id категории _id и название категории _name.

Конструктор класса вызывает конструктор родительского класса, затем получает из HTTP-запроса id категории и сохраняет его в поле _id. Если id категории не задан, то вместо него сохраняется значение all, т.к. в таком случае будут выводиться вопросы сразу из всех категорий.

Метод getCatName() возвращает либо название текущей категории, либо строку "Все вопросы", если категория не задана.

Метод isAllCat() возвращает true, если категория не задана, и false в противном случае.

Модель для одного вопроса

Создайте файл /components/com_myquestions/models/question.php:

<?php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.application.component.model');
class ModelMyQuestionsQuestion extends JModel
{
  var $_question = null;
  var $_id = null;
  function __construct()
  {
    parent::__construct();
    $id = JRequest::getVar('id',0);
    $this->_id = $id;
  }
  function getQuestion()
  {
    if (!$this->_question)
    {
      $query = "SELECT q.id, q.question, q.name, q.date, q.email, q.city, q.answer, q.published, q.expiration_date, 
      c.id AS id_cat, c.name AS name_cat FROM #__myquestions q, #__myquestions_categories c WHERE q.id_cat=c.id AND 
      q.id = {$this->_id}";
      $this->_db->setQuery($query);
      $this->_question = $this->_db->loadObject();
      if ($this->_question->answer == '' || ($this->_question->published == 0 && ($this->
      _question->expiration_date == '0000-00-00 00:00:00' || strtotime($this->_question->expiration_date) <= time())))
      {
        JError::raiseError(404, JText::_(' COM_MYQUESTIONS_ERROR404'));
      }
    }
    return $this->_question;
  }
}
?>




Функция getQuestion() загружает одну запись с заданным id. Если после загрузки вопроса оказывается, что он является неопубликованным, то генерируется сообщение об ошибке 404.

Представления

Создайте в папке /components/com_myquestions подпапку views, а в ней - папки all, category и question. В каждой из них создайте по папке для шаблонов под названием tmpl.

Получившееся дерево папок показано на рис. 6.2.

06 02

Рис. 6.2. Дерево папок MVC-компонента

Просмотр списка всех категорий

Создайте файл /components/com_myquestions/views/all/view.html.php:

<?php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.application.component.view');
class QuestionViewAll extends JView
{
  function display($tpl=null)
  {
    global $option;
    $model=&$this->getModel();
    $list=$model->getList();
    for ($i=0; $i<count($list); $i++)
    {
      $row=&$list[$i];
      $row->link=JRoute::_('index.php?option='.$option.'&id='.$row->id.'&view=category&task=show');
    }
    $this->assignRef('list', $list);
    parent::display($tpl);
  }
}
?>




Класс QuestionViewAll объявляется как производный от класса JView.

В методе display() мы получаем ссылку на ассоциированную с данным представлением модель и используем ее метод getList(), чтобы получить список категорий. К каждому элементу этого списка добавляем ссылку для просмотра данной категории. Связываем этот список с переменной list шаблона и вызываем метод JView::display() для отображения шаблона.

По умолчанию будет отображен шаблон default. Напишем его. Создайте файл /components/com_myquestions/views/all/tmpl/default.php:

<?php
defined('_JEXEC') or die('Restricted access');
global $option;
echo "<a href=\"".JRoute::_('index.php?option='.$option.'&view=question&task=showform')."\">".
JText::_('COM_MYQUESTIONS_ADD_QUESTION')."</a>";
?>
<br/>
<table width="100%">
  <?php
    foreach($this->list as $l)
      echo '<tr><td><p><a href="'.$l->link.'">'.$l->name.'</a></td><td>'.$l->desc.'</td></tr>';
  ?>
</table>
<br/>




Доступ к переменной list осуществляется через $this, т.к. это поле текущего класса QuestionViewAll.

Просмотр списка вопросов из какой-либо категории или из всех категорий

Создайте файл /com_myquestions/views/category/view.html.php:

<?php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.application.component.view');
class QuestionViewCategory extends JView
{
  function display($tpl=null)
  {
    global $option;
    $model=&$this->getModel();
    $list=$model->getList();
    $name_cat=$model->getCatName();
    $is_all_cat=$model->isAllCat();
    for ($i=0; $i<count($list); $i++)
    {
      $row=&$list[$i];
      $row->link=JRoute::_('index.php?option='.$option.'&id='.$row->id.'&view=question&task=show');
      if ($is_all_cat)
        $row->link_cat=JRoute::_('index.php?option='.$option.'&id='.$row->id_cat.'&view=category&task=show');
    }
    $this->assignRef('list', $list);
    $this->assignRef('name_cat', $name_cat);
    $this->assignRef('is_all_cat', $is_all_cat);
    parent::display($tpl);
  }
}
?>




Данный код в целом аналогичен коду метода QuestionViewAll::display(). Если выводится список вопросов сразу из всех категорий, то в name_cat будет храниться текст "Все вопросы", а к объекту-представителю каждого вопроса добавится ссылка на его категорию. Если же выводится содержимое одной категории, то в name_cat будет храниться ее название, а ссылок на категорию каждого вопроса выводиться не будет, т.к. все эти ссылки будут одинаковы и вести на страницу с текущим же списком.

Для создания шаблона по умолчанию создайте файл /components/com_myquestions/views/category/tmpl/default.php:

<?php
defined('_JEXEC') or die('Restricted access');
global $option;
echo "<a href=\"".JRoute::_('index.php?option='.$option.'&view=question&task=showform')."\">".JText::_('COM_MYQUESTIONS_ADD_QUESTION')."</a>";
?>
<H1><?=$this->name_cat?></H1>
<?php foreach($this->list as $l): ?>
  <table width="100%">
    <tr>
      <td width="25%"><i><?=$l->name?></i></td>
      <td width="25%"><i><u><?=$l->email?></u></i></td>
      <td width="25%"><i><?=JHTML::_('date', $l->date,
      JText::_('DATE_FORMAT_LC3'))?></i></td>
      <td width="25%"><i><?=$l->city?></i></td>
    </tr>
    <?php
      if ($this->is_all_cat == true)
      {
    ?>
    <tr>
      <td colspan="4"><a href="/<?=$l->link_cat?>"><?=$l->name_cat?></a></td>      
    </tr>
    <?php
      }
    ?>
    <tr>
      <td colspan="4"><b><?=$l->question?></b></td>
    </tr>
    <tr>
      <td colspan="4"><?=$l->answer?></td>
    </tr>
    <tr>
      <td colspan="4"><a style="text-decoration: none;" title="<?=JText::_('COM_MYQUESTIONS_READMORE')?>" 
      alt="<?=JText::_('COM_MYQUESTIONS_READMORE')?>" href="/<?=$l->link?>">---></a></td>
    </tr>
  </table>
  <br/>
<?php endforeach;?>




Данный шаблон аналогичен шаблону по умолчанию для представления all.

Просмотр одного вопроса

Код для отображения одного вопроса аналогичен коду для отображения списка вопросов. Создайте файл /components/com_myquestions/views/question/view.html.php:

<?php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.application.component.view');
class QuestionViewQuestion extends JView
{
  function display($tpl=null)
  {
if ($tpl !== 'form')
    {
      global $option;
      $model=&$this->getModel();
      $question=$model->getQuestion();
      $question->date=JHTML::Date($question->date);
     
      $this->assignRef('question', $question);
       $this->assignRef('option', $option);

      $this->assignRef('link_cat',JRoute::_('index.php?option='.$option.'&
      id='.$question->id_cat.'&view=category&task=show'));
    }  
     parent::display($tpl);
  }
}
?>




Представление question будет соответствовать двум шаблонам - один для отображения вопроса, второй для вывода формы для отправки вопроса. Для первого шаблона необходимы данные о вопросе, которые мы получаем из модели. Для второго шаблона не требуется никаких данных кроме имени пользователя, которое мы определим в контроллере.

Напишем шаблон для отображения одного вопроса. Создайте файл /components/com_myquestions/views/question/tmpl/default.php:

<?php
  defined('_JEXEC') or die('Restricted access');
  global $option;
  echo "<a href=\"".JRoute::_('index.php?option='.$option.'&view=question&task=showform')."\">"
.JText::_('COM_MYQUESTIONS_ADD_QUESTION')."</a>";
?>
<table width="100%">
  <tr>
    <td><i><?=$this->question->name?></i></td>
    <td><i><u><?=$this->question->email?></u></i></td>
    <td><i><?=JHTML::_('date', $this->question->date,
JText::_('DATE_FORMAT_LC3'))?></i></td>
    <td><i><?=$this->question->city?></i></td>
  </tr>
  <tr>
    <td colspan="4"><a href="/<?=$this->
link_cat?>"><?=$this->question->name_cat?></a></td>
  </tr>
  <tr>
    <td colspan="4"><b><?=$this-
>question->question?></b></td>
  </tr>
  <tr>
    <td colspan="4"><?=$this->question->answer?></td>
  </tr>
</table>




Добавим другой шаблон, отображающий форму для написания вопроса. Создайте файл /components/com_myquestions/views/question/tmpl/default_form.php:

<?php
  defined('_JEXEC') or die('Restricted access');
?>
<form action="<?=JRoute::_('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 $this->user_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="<?=JRequest::getVar("option","")?>"/>
  <input type="submit" class="button" id="button"
 value="<?php echo JText::_('COM_MYQUESTIONS_SENDBUTTON');?>"/>
</form>


 Листинг .

Создание контроллера

Создайте файл /components/com_myquestions/controller.php (метод addQuestion() скопируйте из файла /components/com_myquestions/myquestions.php, убрав параметр $option):

<?php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.application.component.controller');
class QuestionController extends JController
{
  function display()
  {
    $document =& JFactory::getDocument();
    $viewName = JRequest::getVar('view', 'all');
    $viewType = $document->getType();
    $view = &$this->getView($viewName, $viewType);
    $model =& $this->getModel($viewName, 'ModelMyQuestions');
    if (!JError::isError($model))
    {
      $view->setModel($model, true);
    }
    $view->setLayout('default');
    $view->display();
  }
  function showForm()
  {
    $document =& JFactory::getDocument();
    $viewName = JRequest::getVar('view', 'question');
    $viewType = $document->getType();
    $view = &$this->getView($viewName, $viewType);
    $user =&JFactory::getUser();
    if($user->name)
      $view->user_name = $user->name;
    else
      $view->user_name = '';
    $view->display('form'); 
  }
  function addQuestion()
  {
    …
  }
}
?>




В методе display() мы получаем название запрашиваемого представления и тип текущего документа, который одновременно является и типом представления. Затем получаем ссылку на соответствующее представление и ссылку на одноименную модель. Добавляем модель к представлению, назначив ее по умолчанию. Задаем имя макета - default и вызываем метод JView::display(), который выполнит скрипт /components/com_myquestions/views/all/tmpl/default.php.

В методе showForm() мы также получаем объект-представитель текущего пользователя JFactory::getUser(), чтобы подставить его имя в форму для написания вопроса. Выражение $view->display('form') отображает шаблон из файла default_form.php (т.е. имя файла в данном случае строится по схеме "default"+"_"+tpl, где tpl - параметр функции display()).

Метод addQuestion() добавляет новый вопрос в базу данных точно так же, как это делалось ранее. Обратите внимание на то, что название этого метода совпадает со значением, которое хранилось в скрытом элементе task формы для добавления вопроса:

<input type="hidden" name="task" value="addquestion"/>




Напишем код для создания объекта контроллера. Откройте файл /components/com_myquestions/myquestions.php и замените существующий код следующим:

<?php
defined('_JEXEC') or die('Restricted access');
require_once(JPATH_COMPONENT.DS.'controller.php');
JTable::addIncludePath(JPATH_ADMINISTRATOR.
DS.'components'.DS.'com_myquestions'.DS.'tables');
$controller = new QuestionController();
$controller->execute(JRequest::getVar('task'));
$controller->redirect();
?>




С помощью строки require_once(JPATH_COMPONENT.DS.'controller.php') подключается содержимое файла, содержащего код класса контроллера.

Изменение шаблона SEF-ссылок

Шаблон SEF-ссылок, использовавшийся нами до сих пор, не годится для применения в компоненте MVC, т.к. включает только переменные task и id. Для компонента MVC в URL должно быть задано еще по меньшей мере значение view.

Возможно, вы заметили, что в коде фронтенда, переделанном с учетом модели MVC, мы строили URL по шаблону option/view/task/id при включенных SEF и option=com_myquestions&view=value1&task=value2&id=value3 в противном случае. Для наглядности ниже приведено несколько примеров таких ссылок ( таблица 6.1).

Таблица 6.1. Примеры ссылок, использующихся в MVC-версии компонента myquestions
СсылкаviewtaskidЗначение
/myquestions/category/show/1 category show 1 Просмотр категории #1
/myquestions/question/show/1 question show 1 Просмотр вопроса #1
/myquestions/category/show/all category show all Просмотр вопросов из всех категорий
/myquestions/all/show all show - Просмотр списка всех категорий
/myquestions/question/showform question showform - Вывод формы для написания вопроса



Изменим функции генерации и декодирования SEF-ссылок. Откройте файл /components/com_myquestions/router.php и измените код функции MyQuestionsBuildRoute() следующим образом:

function MyQuestionsBuildRoute(&$query)
{
  $segments = array();
    if (isset($query['view']))
  {
    $segments[] = $query['view'];
    unset($query['view']);
  }
  if (isset($query['task']))
  {
    $segments[] = $query['task'];
    unset($query['task']);
  }
  if (isset($query['id']))
  {
    $segments[] = $query['id'];
    unset($query['id']);
  }
  return $segments;
}
        

В том же файле замените функцию MyQuestionsParseRoute() следующей:

function MyQuestionsParseRoute ($segments)
{
  $vars = array();
  $vars['view'] = $segments[0];
  if (count($segments) > 1)
  {
         $vars['task'] = $segments[1];
    if (count($segments) > 2)
      $vars['id'] = $segments[2];
  }
  return $vars;
}
        




В том же файле замените функцию MyQuestionsParseRoute() следующей:

function MyQuestionsParseRoute ($segments) { $vars = array(); $vars['view'] = $segments[0]; if (count($segments) > 1) { $vars['task'] = $segments[1]; if (count($segments) > 2) $vars['id'] = $segments[2]; } return $vars; }

Как видите, теперь мы предполагаем, что первый элемент в массиве segments - это view, второй - task, а третий - id.

Добавление контроллера к коду бэкенда

Бэкенд не нуждается в большом контроле над форматом вывода, поэтому его можно не переводить на архитектуру MVC. Добавим только контроллер, чтобы исключить выражение switch().

Создайте файл /administrator/components/com_myquestions/controller.php. В нем мы объявим класс QuestionController. В конструкторе этого контроллера регистрируются задачи, взятые из старого кода переключателя switch из файла admin.myquestions.php.

<?php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.application.component.controller');

class QuestionController extends JController
{
  function __construct($default = array())
  {
    parent::__construct($default);
    
    $this->registerTask('reply', 'replyToQuestion');
    $this->registerTask('save', 'saveQuestion');
    $this->registerTask('apply', 'saveQuestion');
    $this->registerTask('remove', 'removeQuestions');
    $this->registerTask('sendToExpert', 'send');
    $this->registerTask('sendAnswer', 'send');
    
    $this->registerTask('showCat', 'showCategories');
    $this->registerTask('addCat', 'editCategory');
    $this->registerTask('editCat', 'editCategory');
    $this->registerTask('saveCat', 'saveCategory');
    $this->registerTask('applyCat', 'saveCategory');
    $this->registerTask('removeCat', 'removeCategories');
  }
}
?>




Все функции из файла admin.myquestions.php перейдут в класс QuestionController в качестве методов практически без изменений, за исключением одного аспекта. Отказ от выражения switch ведет к невозможности передавать переменные непосредственно в методы класса контроллера. Поэтому необходимо либо добавлять в класс контроллера новые поля, либо получить переменные из переменных HTTP-запроса или других источников непосредственно в коде каждого метода. В нашем примере почти все методы используют значения переменных option и task. Теперь эти значения будут не передаваться как параметры, а извлекаться из HTTP-запроса с помощью функции JRequest(). Например, первые строки функции saveQuestion() примут вид:

function saveQuestion()
{
  $option = JRequest::getVar('option');
  $task = JRequest::getVar('task');
  $row = $this->save();
  ...
}




Итак, перенесите в класс QuestionController функции replyToQuestion(), save(), saveQuestion() и др. Затем замените содержимое файла admin.myquestions.php следующим кодом:

<?php defined('_JEXEC') or die('Restricted access'); require_once(JApplicationHelper::getPath('admin_html')); 
require_once(JPATH_COMPONENT.DS.'controller.php'); JTable::addIncludePath(JPATH_COMPONENT.DS.'tables'); 
$controller = new QuestionController(array('default_task' => 'showQuestions')); $controller->execute(JRequest::getVar('task')); 
$controller->redirect(); ?>




Как вы уже заметили, конструктор нашего контроллера в бэкенде имеет параметр default. При вызове конструктора мы передаем в него массив, который хранит значение default_task, равное showQuestions. Таким путем задано название задачи, которая будет выполнена по умолчанию.

Ключевые термины

JController - абстрактный класс для реализации контроллеров.
JModel - абстрактный класс для реализации моделей.
JView - абстрактный класс для реализации представлений.
Регистрация задачи - сопоставление ее какому-либо методу класса, производного от JController.



Краткие итоги

Joomla поддерживает архитектуру MVC для компонентов. Модели, представления и контроллеры реализуются соответственно с помощью абстрактных классов JModel, JView и JController. В компоненте могут быть созданы классы, производные от всех или некоторых из этих классов.

Вот простейшая схема взаимодействия модели, представления и контроллера.

В файле, который находится в корневой папке компонента и называется так же, как компонент, находится код для создания контроллера и вызова его методов execute() и redirect(). Метод execute() вызывает метод контроллера, который называется так же, как и заданная задача.

Класс контроллера, производный от JController, содержит методы для каждой задачи, которую должен выполнять компонент. Метод JController::display(), который вызывается по умолчанию, вызывает методы getView(), getModel(), а также метод display() заданного представления.

В классе представления, производном от JView, может быть перегружен метод display() для вызова метода класса модели для загрузки данных и сохранения результата в какой-либо переменной. Затем с помощью метода JView::assignRef() эта переменная связывается с текущим представлением и вызывается метод базового класса JView::display(), который загружает файл заданного шаблона при помощи перехвата выходного потока.

В коде шаблона осуществляется вывод на экран переменных текущего представления.

Вопросы

  1. Какие классы Joomla позволяют реализовать элементы архитектуры MVC?
  2. Опишите схему взаимодействия модели, представления и контроллера.
  3. Что такое регистрация задачи?