4. Разработка расширения для Joomla! 3.0 – больше функционала
Все статьи цикла:
- Разработка расширения для Joomla! 3.0 – подготовка
- Разработка расширения для Joomla! 3.0 – начинаем разработку
- Разработка расширения для Joomla! 3.0 – создаем ядро
- Текущая статья
- Разработка расширения для Joomla! 3.0 – интерфейс администратора и доработка кода
В этой статье мы рассмотрим некоторые другие возможности компонента Lendr. Вы, конечно, обратите внимание на то, что я добавил довольно много кода в файлы, которые раньше были пустыми. Но вместо того, чтобы тратить время на их рассмотрение, я сконцентрируюсь на тех областях, в которых был добавлен новый функционал или концепция. Вы без труда должны разобраться в коде, основанном на предыдущих статьях. Мы же продолжим наш цикл созданием различных модальных окон, необходимых для компонента Lendr.
Шаг 1: Создайте модальные окна
Обратите внимание на то, что я уже создал несколько модальных окон. Все они были сделаны, используя один и тот же способ (при помощи Bootstrap). Сначала я расскажу о двух способах создания таких окон, а потом более подробно опишу способ, который был выбран мной.
Первый способ – загрузить представление и шаблон через AJAX, используя при этом стандартный метод Joomla, который загружает внутренний файл представления в модальное окно при клике на кнопку. Исторически это был стандарт в Joomla для загрузки модальных окон (думайте о них как о iframes).
Второй метод – загрузить все модальное окно на страницу при загрузке страницы, но при этом скрывать его до тех пор, пока оно не будет активировано по клику на ссылку или кнопку.
Я выбрал второй метод по следующим причинам. Непосредственно загрузка модального окна во время загрузки страницы позволяет нам отображать его моментально при клике на ссылку или кнопку. Вы можете подумать, что такой способ замедляет загрузку страницы, но это очень незначительная потеря в скорости. Конечно, у каждого свои предпочтения, но мне больше нравится высокая скорость загрузки модального окна. Второй причиной выбора этого метода является то, что нам необходимо передать совсем небольшое количество данных в поля нашего окна. Поэтому не составит труда сделать это на лету, вместо того, чтобы запрашивать целое представление через AJAX. Опять же это личное предпочтение.
Давайте рассмотрим код, который необходим для включения модального окна в нашу страницу. Сначала разберем файл html.php, который в принципе можно считать родительским представлением для нашего модального окна.
views/book/html.php
<?php defined( '_JEXEC' ) or die( 'Restricted access' ); class LendrViewsBookHtml extends JViewHtml { function render() { $app = JFactory::getApplication(); //retrieve task list from model $model = new LendrModelsBook(); $this->book = $model->getItem(); $this->_addReviewView = LendrHelpersView::load('Review','_add','phtml'); $this->_addReviewView->book = $this->book; $this->_addReviewView->user = JFactory::getUser(); $this->_lendBookView = LendrHelpersView::load('Book', '_lend', 'phtml'); $this->_lendBookView->borrower = $this->book->waitlist_user; $this->_lendBookView->book = $this->book; $this->_returnBookView = LendrHelpersView::load('Book', '_return', 'phtml'); $this->_returnBookView->borrower = $this->book->waitlist_user; $this->_returnBookView->book = $this->book; $this->_reviewsView = LendrHelpersView::load('Review','list','phtml'); $this->_reviewsView->reviews = $this->book->reviews; $this->_modalMessage = LendrHelpersView::load('Profile','_message','phtml'); //display return parent::render(); } }
Здесь мы используем Lendr Helper
, который мы создали и использовали ранее. Обратите внимание, что мы используем подчеркивание (_) для выделения частичных шаблонов и указываем phtml
в качестве формата.
Файл частичного шаблона.
views/book/tmpl/_lend.php
<div id="lendBookModal" tabindex="-1" role="dialog" aria-labelledby="lendBookModalLabel" aria-hidden="true"> <div> <button type="button" data-dismiss="modal" aria-hidden="true">×</button> <h3 id="myModalLabel"><?php echo JText::_('COM_LENDR_LEND_BOOK'); ?></h3> </div> <div> <div> <form id="lendForm"> <div> <?php echo JText::_('COM_LENDR_LEND_BOOK_TO'); ?> <span id="borrower_name"></span> </div> <div id="book-modal-info"></div> <input type="hidden" name="book_id" id="bookid" value="<?php echo $this->book->book_id; ?>" /> <input type="hidden" name="user_id" value="<?php echo JFactory::getUser()->id; ?>" /> <input type="hidden" name="table" value="Book" /> <input type="hidden" name="waitlist_id" value="<?php echo $this->book->waitlist_id; ?>" /> <input type="hidden" name="borrower_id" id="borrower_id" value="<?php echo $this->book->borrower_id; ?>" /> <input type="hidden" name="lend" value="1" /> </form> </div> </div> <div> <button data-dismiss="modal" aria-hidden="true"><?php echo JText::_('COM_LENDR_CLOSE'); ?></button> <button on-click="lendBook();"><?php echo JText::_('COM_LENDR_LEND'); ?></button> </div> </div>
views/book/tmpl/book.php
<h2><?php echo $this->book->title; ?></h2> <div> <div> <img src="http://covers.openlibrary.org/b/isbn/<?php echo $this->book->isbn; ?>-L.jpg"> </div> <div> <h3><?php echo $this->book->title; ?></h3> <p><?php echo $this->book->summary; ?></p> <p> <?php if($this->book->user_id == JFactory::getUser()->id) { ?> <?php if ($this->book->lent) { ?> <a href="#returnBookModal"><?php echo JText::_('COM_LENDR_RETURN'); ?></a> <?php } elseif($this->book->waitlist_id > 0) { ?> <a href="#lendBookModal" data-toggle="modal" role="button" id="lendButton"><?php echo JText::_('COM_LENDR_LEND_BOOK'); ?></a> <?php } ?> <?php } else { if(($this->book->waitlist_id > 0) && $this->book->user_id == JFactory::getUser()->id) { ?> <a href="javascript:void(0);" on-click="cancelRequest(<?php echo $this->book->book_id; ?>);"><?php echo JText::_('COM_LENDR_CANCEL_REQUEST'); ?></a> <?php } else { ?> <div> <a href="javascript:void(0);" on-click="borrowBookModal(<?php echo $this->book->book_id; ?>);"><?php echo JText::_('COM_LENDR_BORROW'); ?></a> <button data-toggle="dropdown"> <span></span> </button> <ul> <li><a href="javascript:void(0);" on-click="addToWishlist('<?php echo $this->book->book_id; ?>');"><?php echo JText::_('COM_LENDR_ADD_WISHLIST'); ?></a></li> <li><a href="#newReviewModal" data-toggle="modal"><?php echo JText::_('COM_LENDR_WRITE_REVIEW'); ?></a></li> </ul> </div> <?php } } ?> </p> </div> </div> <br /> <div> <div> <ul> <li><a href="#detailsTab" data-toggle="tab"><?php echo JText::_('COM_LENDR_BOOK_DETAILS'); ?></a></li> <li><a href="#reviewsTab" data-toggle="tab"><?php echo JText::_('COM_LENDR_REVIEWS'); ?></a></li> </ul> <div> <div id="detailsTab"> <h2><?php echo JText::_('COM_LENDR_BOOK_DETAILS'); ?></h2> <p> <strong><?php echo JText::_('COM_LENDR_AUTHOR'); ?></strong><br /> <?php echo $this->book->author; ?> </p> <p> <strong><?php echo JText::_('COM_LENDR_PAGES'); ?></strong><br /> <?php echo $this->book->pages; ?> </p> <p> <strong><?php echo JText::_('COM_LENDR_PUBLISH_DATE'); ?></strong><br /> <?php echo $this->book->publish_date; ?> </p> <p> <strong><?php echo JText::_('COM_LENDR_ISBN'); ?></strong><br /> <?php echo $this->book->isbn; ?> </p> </div> <div id="reviewsTab"> <a href="#newReviewModal" role="button" data-toggle="modal"><i></i> <?php echo JText::_('COM_LENDR_ADD_REVIEW'); ?></a> <h2><?php echo JText::_('COM_LENDR_REVIEWS'); ?></h2> <?php echo $this->_reviewsView->render(); ?> </div> </div> </div> </div><?php echo $this->_addReviewView->render(); ?> <?php echo $this->_lendBookView->render(); ?> <?php echo $this->_returnBookView->render(); ?> <?php echo $this->_modalMessage->render(); ?>
Это отобразит внутреннее вторичное представление на странице, но так как мы использовали Bootstrap CSS, div
будет спрятан, пока не будет активирован.
Я буду продолжать использовать этот код для загрузки модальных окон в Lendr. Обратите внимание, что я использую два различных вызова для загрузки. Первый – это прямая загрузка окна (используется, когда в окне не требуется дополнительных данных). Второй – использование javascript (используется в случаях, когда в окно необходимо передать какие-то данные перед его отображением).
Шаг 2: Функционал одалживания и возврата
Теперь, когда наши модальные окна загружаются и отображают данные, мы начнем добавлять функциональность, связанную с этими окнами. Сначала, мы рассмотрим базовый процесс одалживания и возврата. В Lendr мы просто позволяем вам одолжить книгу и пометить её как возвращенную при ее возврате. С этим процессом связано несколько файлов. Давайте рассмотрим их поближе.
Контроллер
Сначала разберем файл контроллера lend.php, который служит главным контроллером как для одалживания так и для возврата.
controllers/lend.php
<?php defined( '_JEXEC' ) or die( 'Restricted access' ); class LendrControllersLend extends JControllerBase { public function execute() { $return = array("success"=>false); $model = new LendrModelsBook(); if ( $row = $model->lend() ) { $return['success'] = true; $return['msg'] = JText::_('COM_LENDR_BOOK_LEND_SUCCESS'); } else { $return['msg'] = JText::_('COM_LENDR_BOOK_LEND_FAILURE'); } echo json_encode($return); } }
Модель
В контроллере мы снова исполняем единственную задачу (execute) и в ней мы передаем данные и запрос напрямую в модель Book, которая обрабатывает вызов функции lend
.
models/book.php
public function lend($data = null) { $data = isset($data) ? $data : JRequest::get('post'); if (isset($data['lend']) && $data['lend']==1) { $date = date("Y-m-d H:i:s"); $data['lent'] = 1; $data['lent_date'] = $date; $data['lent_uid'] = $data['borrower_id']; $waitlistData = array('waitlist_id'=>$data['waitlist_id'], 'fulfilled' => 1, 'fulfilled_time' => $date, 'table' => 'Waitlist'); $waitlistModel = new LendrModelsWaitlist(); $waitlistModel->store($waitlistData); } else { $data['lent'] = 0; $data['lent_date'] = NULL; $data['lent_uid'] = NULL; } $row = parent::store($data); return $row; }
В этой функции вы можете заметить, что обрабатывается как одалживание, так и возврат. Обратите внимание, когда книга успешно одолжена, мы также помечаем связанный список ожиданий как исполненный.
Javascript
Ниже представлены javascript функции, связанные с одалживанием и возвратом. Первая функция занимается загрузкой окна (при этом добавляются необходимые переменные). Вторая функция использутеся для одалживания книги. Я использую вызов jQuery AJAX для передачи данных формы в контроллер lend (описан выше). Если контроллер/модель успешно выполняются, тогда модальное окно закрывается.
assets/js/lendr.js
function loadLendModal(book_id, borrower_id, borrower, waitlist_id) { jQuery("#lendBookModal").modal('show'); jQuery('#borrower_name').html(borrower); jQuery("#book_id").val(book_id); jQuery("#borrower_id").val(borrower_id); jQuery("#waitlist_id").val(waitlist_id); } function lendBook() { var lendForm = {}; jQuery("#lendForm :input").each(function(idx,ele){ lendForm[jQuery(ele).attr('name')] = jQuery(ele).val(); }); jQuery.ajax({ url:'index.php?option=com_lendr&controller=lend&format=raw&tmpl=component', type:'POST', data:lendForm, dataType:'JSON', success:function(data) { if ( data.success ) { jQuery("#lendBookModal").modal('hide'); } } }); }
Есть несколько мест, в которых пока что пропущены сообщения об ошибках. Мы добавим их в последующих статьях.
Шаг 3: Добавляем списки избранного, списки ожидания и обзоры
Мы продолжим работу над тремя областями, которые связаны с книгами. Функционал списка ожидания и списка избранного довольно прост. Оба будут просто загружать модальное окно, и добавлять определенную книгу либо в один, либо в другой список пользователя. И снова этот код во многом похож на код модальных окон, рассмотренных ранее. Более подробно вы сможете изучить код в GitHub репозитории. Я же остановлюсь на обзорах.
Изначально для обзоров я хотел использовать контроллер review, но потом решил, что технически новый обзор должен следовать такому же контроллеру добавления «add» как и другие части компонента. Это потребовало немного переписать контроллер, чтобы он мог правильно передавать данные в соответствующую модель.
Обзор создается из модального окна. Окно загружает форму, которая состоит из названия и текста обзора. Скрытые поля отслеживают книгу и пользователя, который оставляет обзор. Вот код этой формы:
views/review/tmpl/_add.php
<div id="newReviewModal" tabindex="-1" role="dialog" aria-labelledby="newReviewModal" aria-hidden="true"> <div> <button type="button" data-dismiss="modal" aria-hidden="true">×</button> <h3 id="myModalLabel"><?php echo JText::_('COM_LENDR_ADD_REVIEW'); ?></h3> </div> <div> <div> <form id="reviewForm"> <input type="text" name="title" placeholder="<?php echo JText::_('COM_LENDR_TITLE'); ?>" /> <textarea placeholder="<?php echo JText::_('COM_LENDR_SUMMARY'); ?>" name="review" rows="10"></textarea> <input type="hidden" name="user_id" value="<?php echo $this->user->id; ?>" /> <input type="hidden" name="view" value="review" /> <input type="hidden" name="book_id" value="<?php echo $this->book->book_id; ?>" /> <input type="hidden" name="model" value="review" /> <input type="hidden" name="item" value="review" /> <input type="hidden" name="table" value="review" /> </form> </div> </div> <div> <button data-dismiss="modal" aria-hidden="true"><?php echo JText::_('COM_LENDR_CLOSE'); ?></button> <button on-click="addReview()"><?php echo JText::_('COM_LENDR_ADD'); ?></button> </div> </div>
Мы передаем таблицу и другие поля, необходимые для соответствия с корректной моделью и функцией.
Ниже представлен javascript код, связанный с процессом обзора:
assets/js/lendr.js
//add a review function addReview() { var reviewInfo = {}; jQuery("#reviewForm :input").each(function(idx,ele){ reviewInfo[jQuery(ele).attr('name')] = jQuery(ele).val(); }); jQuery.ajax({ url:'index.php?option=com_lendr&controller=add&format=raw&tmpl=component', type:'POST', data:reviewInfo, dataType:'JSON', success:function(data) { if ( data.success ){ console.log(data.html); jQuery("#review-list").append(data.html); jQuery("#newReviewModal").modal('hide'); }else{ } } }); }
Возвращаемый HTML загружается из хелпера файла. Давайте посмотрим, что происходит в функции getHtml
этого хелпера:
helpers/view.php
function getHtml($view, $layout, $item, $data) { $objectView = LendrHelpersView::load($view, $layout, 'phtml'); $objectView->$item = $data; ob_start(); echo $objectView->render(); $html = ob_get_contents(); ob_clean(); return $html; }
Сначала метод загружает частичное представление и передает ему данные. Затем, используя буферизацию, мы назначаем полученный результат переменной и возвращаем её. Далее она будет передана в нашу javascript функцию и будет её значение будет добавлено на страницу.
Шаг 4: Поиск книг
Процесс поиска довольно интересен. И здесь мы может пойти разными путями. Мы можем внедрить свою собственную систему поиска или мы можем использовать возможности встроенного в Joomla компонента "Умный поиск" (Smart Search - com_finder). Несмотря на то, что не всегда стоит использовать Finder, мы все-таки воспользуемся такой возможностью.Таким образом, мы уменьшим количество кода и упростим структуру нашего компонента. Кроме того, это позволит продемонстрировать код, необходимый для создания Finder плагина для нового типа контента.
Плагин, который написан мной для Finder довольно простой и безусловно не демонстрирует всех возможностей. Ниже представлен установочный XML файл этого плагина.
plugins/finder/books/books.xml
<?xml version="1.0" encoding="utf-8"?> <extension version="3.1" type="plugin" group="finder" method="upgrade"> <name>Smart Search - Books</name> <author>Joomla! Project</author> <creationDate>March 2013</creationDate> <copyright>(C) 2005 - 2013 Open Source Matters. All rights reserved.</copyright> <license>GNU General Public License version 2 or later; see LICENSE.txt</license> <authorEmail>admin @ joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>3.0.0</version> <description></description> <files> <file plugin="books">books.php</file> <filename>index.html</filename> </files> </extension>
А вот код плагина.
plugins/finder/books/books.php
<?php /** * @package Joomla.Plugin * @subpackage Finder.Books * * @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE */ defined('JPATH_BASE') or die; require_once JPATH_ADMINISTRATOR . '/components/com_finder/helpers/indexer/adapter.php'; /** * Finder adapter for Lendr Books. * * @package Joomla.Plugin * @subpackage Finder.Books * @since 3.0 */ class PlgFinderBooks extends FinderIndexerAdapter { /** * The plugin identifier. * * @var string * @since 2.5 */ protected $context = 'Books'; /** * The extension name. * * @var string * @since 2.5 */ protected $extension = 'com_lendr'; /** * The sublayout to use when rendering the results. * * @var string * @since 2.5 */ protected $layout = 'book'; /** * The type of content that the adapter indexes. * * @var string * @since 2.5 */ protected $type_title = 'Book'; /** * The table name. * * @var string * @since 2.5 */ protected $table = '#__lendr_books'; /** * The field the published state is stored in. * * @var string * @since 2.5 */ protected $state_field = 'published'; /** * Load the language file on instantiation. * * @var boolean * @since 3.1 */ protected $autoloadLanguage = true; /** * Method to remove the link information for items that have been deleted. * * @param string $context The context of the action being performed. * @param JTable $table A JTable object containing the record to be deleted * * @return boolean True on success. * * @since 2.5 * @throws Exception on database error. */ public function onFinderDelete($context, $table) { if ($context == 'com_lendr.book') { $id = $table->id; } elseif ($context == 'com_finder.index') { $id = $table->link_id; } else { return true; } // Remove the items. return $this->remove($id); } /** * Method to determine if the access level of an item changed. * * @param string $context The context of the content passed to the plugin. * @param JTable $row A JTable object * @param boolean $isNew If the content has just been created * * @return boolean True on success. * * @since 2.5 * @throws Exception on database error. */ public function onFinderAfterSave($context, $row, $isNew) { // We only want to handle books here if ($context == 'com_lendr.book') { // Check if the access levels are different if (!$isNew && $this->old_access != $row->access) { // Process the change. $this->itemAccessChange($row); } // Reindex the item $this->reindex($row->id); } return true; } /** * Method to reindex the link information for an item that has been saved. * This event is fired before the data is actually saved so we are going * to queue the item to be indexed later. * * @param string $context The context of the content passed to the plugin. * @param JTable $row A JTable object * @param boolean $isNew If the content is just about to be created * * @return boolean True on success. * * @since 2.5 * @throws Exception on database error. */ public function onFinderBeforeSave($context, $row, $isNew) { // We only want to handle books here if ($context == 'com_lendr.book') { // Query the database for the old access level if the item isn't new if (!$isNew) { $this->checkItemAccess($row); } } return true; } /** * Method to update the link information for items that have been changed * from outside the edit screen. This is fired when the item is published, * unpublished, archived, or unarchived from the list view. * * @param string $context The context for the content passed to the plugin. * @param array $pks A list of primary key ids of the content that has changed state. * @param integer $value The value of the state that the content has been changed to. * * @return void * * @since 2.5 */ public function onFinderChangeState($context, $pks, $value) { // We only want to handle articles here if ($context == 'com_lendr.book') { $this->itemStateChange($pks, $value); } // Handle when the plugin is disabled if ($context == 'com_plugins.plugin' && $value === 0) { $this->pluginDisable($pks); } } /** * Method to index an item. The item must be a FinderIndexerResult object. * * @param FinderIndexerResult $item The item to index as an FinderIndexerResult object. * @param string $format The item format * * @return void * * @since 2.5 * @throws Exception on database error. */ protected function index(FinderIndexerResult $item, $format = 'html') { // Check if the extension is enabled if (JComponentHelper::isEnabled($this->extension) == false) { return; } $item->setLanguage(); $extension = ucfirst(substr($item->extension, 4)); $item->url = $this->getURL($item->id, $item->extension, $this->layout); $item->route = 'index.php?option='.$this->extension.'&view=book&layout='.$this->layout.'&id='.$item->book_id; // Add the type taxonomy data. $item->addTaxonomy('Type', 'Book'); // Add the language taxonomy data. $item->addTaxonomy('Language', $item->language); // Index the item. $this->indexer->index($item); } /** * Method to get the SQL query used to retrieve the list of books. * * @param mixed $sql A JDatabaseQuery object or null. * * @return JDatabaseQuery A database object. * * @since 2.5 */ protected function getListQuery($sql = null) { $db = JFactory::getDbo(); // Check if we can use the supplied SQL query. $sql = $sql instanceof JDatabaseQuery ? $sql : $db->getQuery(true); $sql->select('b.book_id as id, b.title, b.author, b.summary, b.pages, b.publish_date'); $sql->from('#__lendr_books AS b'); $sql->where($db->quoteName('b.book_id') . ' > 1'); return $sql; } /** * Method to get a SQL query to load the published state * * @return JDatabaseQuery A database object. * * @since 2.5 */ protected function getStateQuery() { $sql = $this->db->getQuery(true); $sql->select($this->db->quoteName('b.book_id')); $sql->select($this->db->quoteName('b.published') . ' AS book_state'); $sql->from($this->db->quoteName('#__lendr_books') . ' AS b'); return $sql; } }
Finder использует эти функции для индексации таблицы, загрузки данных и установки ссылок в результатах поиска.
Шаг 5: Подводим итоги
В этой статье мы охватили несколько функций, структуру и некоторые дополнительные идеи, которые могут быть применены к любому другому компоненту. Мы увидели, как можно использовать Bootstrap для модальных окон, мы углубились в javascript функциональность и использование, а также рассмотрели создание плагина. Я намеренно не включал каждый созданный файл или измененную функцию просто для того, чтобы сфокусировать эту статью на процессе размышления и концепций, окружающих создание компонента.
В следующей статье мы начнем подгонять код, и готовится к окончанию разработки. Мы рассмотрим интерфейс администратора, использование обновлений на странице по завершению AJAX вызова, начнем подчищать код, удаляя ненужные файлы и пытаясь найти пути упрощения и уменьшения количества кода.
Код компонента на GitHub