Текст книги "Разрботка расширений для CMS Joomla"
Автор книги: Яна Седова
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 7 (всего у книги 10 страниц)
Архитектура MVC в компонентах Joomla
Рассмотрены принципы реализации архитектуры MVC в компоненте и классы Joomla, использующиеся для этого.
Цель лекции:Ознакомиться с основами применения архитектуры MVC при разработке компонентов.
Взаимодействие элементов архитектуры MVC в Joomla
MVC(«Model – View – Controller») – это набор паттернов проектирования, который предполагает разделение программного кода на три группы:
модели(model) используются для хранения данных. В Joomla модели реализуются с помощью абстрактного класса JModel;
представления(view) генерируют вывод для заданной информации с помощью шаблона. В Joomla реализуются с помощью абстрактного класса JView;
контроллеры(controller) получают команды от пользователя и управляют моделями и представлениями для выполнения этих команд. В Joomla реализуются с помощью абстрактного класса JController.
Приблизительно схема взаимодействия этих групп в коде Joomla представлена на следующей диаграмме последовательности (рис. 6.1 на основе иллюстрации из книги [4, p.246]).
Рис. 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. В его коде осуществляется вывод на экран переменных текущего представления. Например:
'.$l->data.' |
Так выглядит простейший вариант взаимодействия моделей, представлений и контроллеров.
Классы 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:
_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:
_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:
_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.
Рис. 6.2. Дерево папок MVC-компонента
Просмотр списка всех категорий
Создайте файл /components/com_myquestions/views/all/view.html.php:
getModel(); $list=$model->getList(); for ($i=0; $i
Класс QuestionViewAll объявляется как производный от класса JView.
В методе display() мы получаем ссылку на ассоциированную с данным представлением модель и используем ее метод getList(), чтобы получить список категорий. К каждому элементу этого списка добавляем ссылку для просмотра данной категории. Связываем этот список с переменной list шаблона и вызываем метод JView::display() для отображения шаблона.
По умолчанию будет отображен шаблон default. Напишем его. Создайте файл /components/com_myquestions/views/all/tmpl/default.php:
link.'">'.$l->name.' | '.$l->desc.' |
Доступ к переменной list осуществляется через $this, т.к. это поле текущего класса QuestionViewAll.
Просмотр списка вопросов из какой-либо категории или из всех категорий
Создайте файл /com_myquestions/views/category/view.html.php:
getModel(); $list=$model->getList(); $name_cat=$model->getCatName(); $is_all_cat=$model->isAllCat(); for ($i=0; $i
Данный код в целом аналогичен коду метода QuestionViewAll::display(). Если выводится список вопросов сразу из всех категорий, то в name_cat будет храниться текст "Все вопросы", а к объекту-представителю каждого вопроса добавится ссылка на его категорию. Если же выводится содержимое одной категории, то в name_cat будет храниться ее название, а ссылок на категорию каждого вопроса выводиться не будет, т.к. все эти ссылки будут одинаковы и вести на страницу с текущим же списком.
Для создания шаблона по умолчанию создайте файл /components/com_myquestions/views/category/tmpl/default.php:
=$this->name_cat?> list as $l): ?>
=$l->name?> | =$l->email?> | =JHTML::_('date', $l->date, JText::_('DATE_FORMAT_LC3'))?> | =$l->city?> |
link_cat?>">=$l->name_cat?> | |||
=$l->question?> | |||
=$l->answer?> | |||
" alt="=JText::_('COM_MYQUESTIONS_READMORE')?>" href="=$l->link?>">–> |
Данный шаблон аналогичен шаблону по умолчанию для представления all.
Просмотр одного вопроса
Код для отображения одного вопроса аналогичен коду для отображения списка вопросов. Создайте файл /components/com_myquestions/views/question/view.html.php:
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:
=$this->question->name?> | =$this->question->email?> | =JHTML::_('date', $this->question->date, JText::_('DATE_FORMAT_LC3'))?> | =$this->question->city?> |
link_cat?>">=$this->question->name_cat?> | |||
=$this– >question->question?> | |||
=$this->question->answer?> |
Добавим другой шаблон, отображающий форму для написания вопроса. Создайте файл /components/com_myquestions/views/question/tmpl/default_form.php:
Листинг .
Создание контроллера
Создайте файл /components/com_myquestions/controller.php(метод addQuestion() скопируйте из файла /components/com_myquestions/myquestions.php, убрав параметр $option):
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 формы для добавления вопроса:
Напишем код для создания объекта контроллера. Откройте файл /components/com_myquestions/myquestions.phpи замените существующий код следующим:
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 Ссылка view task id Значение /myquestions/category/show/1category show 1 Просмотр категории #1 /myquestions/question/show/1question show 1 Просмотр вопроса #1 /myquestions/category/show/allcategory show all Просмотр вопросов из всех категорий /myquestions/all/showall show – Просмотр списка всех категорий /myquestions/question/showformquestion 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; }
Как видите, теперь мы предполагаем, что первый элемент в массиве segments – это view, второй – task, а третий – id.
Добавление контроллера к коду бэкенда
Бэкенд не нуждается в большом контроле над форматом вывода, поэтому его можно не переводить на архитектуру MVC. Добавим только контроллер, чтобы исключить выражение switch().
Создайте файл /administrator/components/com_myquestions/controller.php. В нем мы объявим класс QuestionController. В конструкторе этого контроллера регистрируются задачи, взятые из старого кода переключателя switch из файла admin.myquestions.php.
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следующим кодом:
'showQuestions')); $controller->execute(JRequest::getVar('task')); $controller->redirect(); ?>
Как вы уже заметили, конструктор нашего контроллера в бэкенде имеет параметр default. При вызове конструктора мы передаем в него массив, который хранит значение default_task, равное showQuestions. Таким путем задано название задачи, которая будет выполнена по умолчанию.
Ключевые термины
JController – абстрактный класс для реализации контроллеров. JModel – абстрактный класс для реализации моделей. JView – абстрактный класс для реализации представлений. Регистрация задачи – сопоставление ее какому-либо методу класса, производного от JController.
Краткие итоги
Joomla поддерживает архитектуру MVC для компонентов. Модели, представления и контроллеры реализуются соответственно с помощью абстрактных классов JModel, JView и JController. В компоненте могут быть созданы классы, производные от всех или некоторых из этих классов.