Третья статья из цикла "Создание MVC компонента для Joomla 1.6-2.5"

Сегодня мы рассмотрим создание формы редактирования для админки!

Для начала на нужно создать таблицу в которой мы будем хранить данные!
В XML файле манифеста уже прописана строка которая указывает файлы с SQL запросами.
Эти файлы лежат по адресу: admin/sql/install.mysql.utf8.sql

Далее нам нужно создать класс таблицы! Класс отображает таблицу базы данных, а экземпляр класса представляет строку!

Пример кода таблицы
<?php

// дапрет доступа
defined( '_JEXEC' ) or die;
jimport( 'joomla.database.table' );

class TableTest extends JTable
{

	/**
	 * Конструктор класса
	 * @param Object $db (database link object)
	 */
	function __construct( &$db )
	{
		parent::__construct( '#__test', 'id', $db );
	}

	/**
	 * имя для записи в таблице #__assets
	 */
	protected function _getAssetName()
	{
		$k = $this->_tbl_key;
		return 'com_test.testedit.' . ( int )$this->$k;
	}


	protected function _getAssetParentId( $table = null, $id = null )
	{
		$asset = JTable::getInstance( 'Asset' );
		$asset->loadByName( 'com_test' );
		return $asset->id;
	}

	/**
	 * Метод связывающий данные из $_REQUEST с полями таблицы
	 */
	public function bind( $array, $ignore = '' )
	{
		//Если в поле created_by не устрановллено то
		//идентификатор пользователя берем из текущего пользователя
		if ( empty( $array['created_by'] ) ) {
			$user = & JFactory::getUser();
			$array['created_by'] = $user->id;
		}
		//Если нет даты создания записи то устанавливаем дату создания
		if ( empty( $array['created'] ) ) {
			$array['created'] = date( 'Y-m-d H:i:s' );
		}
		//Разбиваем текст из редактора на introtext и fulltext
		if ( isset( $array['text'] ) ) {
			$allText = explode( '<hr id="system-readmores">', $array['text'] );
			$array['introtext'] = trim( $allText[0] );
			$array['fulltext'] = trim( $allText[1] );
		}
		//Устанавливаем ACL для компонента
		if ( isset( $array['rules'] ) && is_array( $array['rules'] ) ) {
			$rules = new JRules( $array['rules'] );
			$this->setRules( $rules );
		}
		//Создаем alias
		$array['alias'] = JApplication::stringURLSafe( $array['alias'] );
		if ( trim( str_replace( '-', '', $array['alias'] ) ) == '' ) {
			$array['alias'] = JApplication::stringURLSafe( $array['title'] );
		}
		return parent::bind( $array, $ignore );
	}

}

Файл с классом таблицы должне лежать в папке:
administrator/имя_компонента/tables/
Если мы называем имя файла с таблицей как test.php то класс таблицы должен называться: TableTest и должен наследоваться от класса JTable

В этом классе есть один обязательный метод это конструктор:

function __construct( &$db ){
	parent::__construct( '#__test', 'id', $db );
}

В конструктор передается ссылка на объект класса JDatabase, и вызывается родительский конструктор класса JTable: parent::__construct( '#__test', 'id', $db ); первый параметр это имя таблицы, второй название уникального поля c AUTO INCREMENT в таблице #__test.
Все остальные методы переопределены и не обязательны! в данном случае используются для установки данных (в комментарии к коду более подробно расписанно)!

Далее нам необходимо создать модель, контроллер и вид для работы с формой, а также XML файл который будет описывать форму!
Файл модели и контроллера должны называться как папка с видом. Файл с видом должен лежать в папке views/имя_вида/view.html.php.
Имена классов должны называться:

имя_компонентаControllerИмя_вида - контроллер для формы должен наследоваться от класса JControllerForm и должен лежать в папке controllers и называться: имя_вида.php

имя_компонентаModelИмя_вида - модель для формы должна унаследоваться от класса JModelAdmin и должна лежать в папке models и называться: имя_вида.php кроме того в папке models могут находиться ещу две папки forms - в которой будут XML файлы с формами, и папка fields - в которой мы можем рассполагать произвольные поля созданные нами!

имя_компонентаViewИмя_вида - любой вид должен наследоваться от класса JView и должен лежать в папке views/имя_вида и файл с видом должен называться view.html.php
В папке с нашим видом также есть папка tmpl в которой шаблон вывода вида!

Далее я приведу листинг всех файлов с подробными комментариями каждого участка кода:

Модель models/testedit.php
<?php

// запрещаем доступ извне
defined( '_JEXEC' ) or die;
// подключаем класс JModelAdmin
jimport( 'joomla.application.component.modeladmin' );

/**
 * Модель
 */
class TestModelTestedit extends JModelAdmin {

	/**
	 * Загрузка формы
	 */
	public function getForm( $data = array( ), $loadData = true ) {
		//загружаем форму их xml Файла который должен лежать в папке forms и доджен называться testedit.php
		$form = $this->loadForm( 'com_test.testedit', 'testedit', array( 'control' => 'jform', 'load_data' => $loadData ) );
		if ( empty( $form ) ) {
			return false;
		}
		//Получаем объект текущего пользователя
		$user = & JFactory::getUser();
		//Проверяем или пользователь может изменять состояние текущего материала
		if ( !$user->authorise( 'core.edit.state', $this->option . '.testedit.' . $this->getState( 'testedit.id' ) ) ) {
			//и если пользователь не может то блокирум поле изменение состония
			$form->setFieldAttribute( 'published', 'disabled', 'true' );
			$form->setFieldAttribute( 'published', 'filter', 'unset' );
		}
		//возвращаем форму
		return $form;
	}

	/**
	 * Получение значения текущей записи
	 */
	public function getItem( $id = null ) {
		//Если запись с текущим идентификатором загруженна
		if ( $item = parent::getItem( $id ) ) {
			//то объеденяем introtext и fulltext в поле text
			$item->text = trim( $item->fulltext ) != '' ? $item->introtext . '
<hr id="system-readmore" />
' . $item->fulltext : $item->introtext;
		}
		//возвращаем запись
		return $item;
	}

	/**
	 * Загрузка экземпляра таблицы TableTest
	 */
	public function getTable( $type = 'Test', $prefix = 'Table', $config = array( ) ) {
		return JTable::getInstance( $type, $prefix, $config );
	}

	/**
	 * Метод для установки данных в форму
	 */
	protected function loadFormData() {
		//получаем данные из пользовательского состояния
		//это необходимо в случае если форма отправленна и не все данные коректно установлены!
		//что бы данные не пропали то иы их таким способом получаем и возвращаем в форму
		$data = JFactory::getApplication()->getUserState( 'com_test.edit.testedit.data', array() );
		if ( empty( $data ) ) {
			$data = $this->getItem();
		}
		return $data;
	}

}
XML файл формы models/forms/testedit.xml
<?xml version="1.0"?>
<form>
    <fieldset>
        <field name="id" type="text" default="0" label="JGLOBAL_FIELD_ID_LABEL" readonly="true"/>
        <field name="title" type="text" label="JGLOBAL_TITLE" description="JFIELD_TITLE_DESC" required="true" size="30"
               class="inputbox"/>
        <field name="alias" type="text" label="JFIELD_ALIAS_LABEL" description="JFIELD_ALIAS_DESC" size="45"
               class="inputbox"/>
        <field name="published" type="list" label="JSTATUS" description="JFIELD_PUBLISHED_DESC" class="inputbox"
               filter="intval" size="1" default="1">
            <option value="1">JPUBLISHED</option>
            <option value="0">JUNPUBLISHED</option>
        </field>
        <field name="text" type="editor" label="JGLOBAL_FULL_TEXT" description="JGLOBAL_FULL_TEXT" required="true"
               buttons="true" filter="RAW"/>
        <field name="metakey" type="textarea" label="JFIELD_META_KEYWORDS_LABEL" description="JFIELD_META_KEYWORDS_DESC"
               class="inputbox" rows="3" cols="30"/>
        <field name="metadesc" type="textarea" label="JFIELD_META_DESCRIPTION_LABEL"
               description="JFIELD_META_DESCRIPTION_DESC" class="inputbox" rows="3" cols="30"/>
        <field name="created_by" type="user" label="JGLOBAL_FIELD_CREATED_BY_LABEL"
               description="JGLOBAL_FIELD_CREATED_BY_LABEL"/>
        <field name="created" type="calendar" label="JGLOBAL_CREATED_DATE" description="JGLOBAL_CREATED_DATE" size="22"
               class="inputbox" format="%Y-%m-%d %H:%M:%S" filter="user_utc"/>
    </fieldset>
    <fieldset name="accesscontrol">
        <field name="asset_id" type="hidden" filter="unset"/>
        <field
                name="rules"
                type="rules"
                label="JFIELD_RULES_LABEL"
                translate_label="false"
                filter="rules"
                validate="rules"
                class="inputbox"
                component="com_test"
                section="testedit"/>
    </fieldset>
</form>
Контроллер controllers/testedit.php
<?php

// запрещаем доступ извне
defined( '_JEXEC' ) or die;
//Подключаем класс JControllerForm
jimport( 'joomla.application.component.controllerform' );

/**
 * Класс контроллера Testedit
 */
class TestControllerTestedit extends JControllerForm
{

	/**
	 * Конструктор класса
	 */
	function __construct( $config = array() )
	{
		//устанавливаем вид на который будет переходить
		//после нажатия на кнопку "Сохранить и закрыть"
		$this->view_list = 'testlist';
		parent::__construct( $config );
	}

	/**
	 * Переопределения доступа для определения возможности
	 * редактирования записи для пользвателя
	 */
	protected function allowEdit( $data = array(), $key = 'id' )
	{
		// инициализируем переменные
		$recordId = ( int )isset( $data[$key] ) ? $data[$key] : 0;
		//получение объекта текущего пользователя
		$user = JFactory::getUser();
		//Получаем идентификатор пользователя
		$userId = $user->get( 'id' );
		// Сначала проверяем общий доступ на редактирование и если
		//пользователь может редактировать то возвращаем истину
		if ( $user->authorise( 'core.edit', 'com_test.testedit.' . $recordId ) ) {
			return true;
		}
		// Проверяем или материал создал пользлватель
		if ( $user->authorise( 'core.edit.own', 'com_test.testedit.' . $recordId ) ) {
			$ownerId = ( int )isset( $data['created_by'] ) ? $data['created_by'] : 0;
			if ( empty( $ownerId ) && $recordId ) {
				$record = $this->getModel()->getItem( $recordId );
				if ( empty( $record ) ) {
					return false;
				}
				$ownerId = $record->created_by;
			}
			if ( $ownerId == $userId ) {
				return true;
			}
		}
		return parent::allowEdit( $data, $key );
	}
}
Вид controllers/view/testedit/view.html.php
<?php
// запрещаем доступ извне
defined( '_JEXEC' ) or die;
//Подключаем класс JView
jimport( 'joomla.application.component.view' );

/**
 * Класс TestViewTestedit
 */
class TestViewTestedit extends JView
{
	//форма
	protected $form;
	//запись
	protected $item;
	//Пользователь
	protected $user;

	/**
	 * Отображение текущего вида
	 */
	public function display( $tpl = null )
	{
		//получаем форму из метода getForm модели testedit.php
		$this->form = $this->get( 'Form' );
		//получаем запись из метода getItem модели testedit.php
		$this->item = $this->get( 'Item' );
		//Получения объекта текущего пользователя
		$this->user = & JFactory::getUser();

		//Если есть каике то ощибки то не отображаем вид и выводим ошибки
		if ( count( $errors = $this->get( 'Errors' ) ) ) {
			JError::raiseError( 500, implode( '\n', $errors ) );
			return false;
		}
		//Подключаем помощник вида testHelper из папки helpers/testhelper.php
		$this->loadHelper( 'testHelper' );
		//Получаем список действий которые может выполнять пользователь
		$this->canDo = testHelper::getActions( 'testedit' );
		//выводим панель инструментов
		$this->_setToolBar();
		parent::display( $tpl );
	}

	/**
	 * Метод для вывода панели инструментов
	 */
	protected function _setToolBar()
	{
		//блокируем меню
		JRequest::setVar( 'hidemainmenu', true );
		//подключаем JS скрипты для подсказок
		JHTML::_( 'behavior.tooltip' );
		//подключаем JS скрипт для того что бы пользователь оставался на сайте
		JHTML::_( 'behavior.keepalive' );
		//Подключаем JS скрипты для валидации формы
		JHTML::_( 'behavior.formvalidation' );
		//Проверям или запись новая
		$isNew = ( $this->item->id == 0 );
		//Получаем списки доступов для текущей записи
		$canDo = testHelper::getActions( 'testedite', $this->item->id );

		//Выводим название компонента
		JToolBarHelper::title( JText::_( 'COM_TEST' ) . ': 
[ ' . ( $isNew ? JText::_( 'JTOOLBAR_NEW' ) : JText::_( 'JTOOLBAR_EDIT' ) ) . ']
' );
		// Проверяем или пользователь может создавать записи
		if ( $isNew && ( count( $this->user->getAuthorisedCategories( 'com_test', 'core.create' ) ) > 0 ) ) {
			//выводим кнопку применить
			JToolBarHelper::apply( 'testedit.apply' );
			//Ввыводим кнопке сохранить
			JToolBarHelper::save( 'testedit.save' );
			//Выводим кнопку сохранить и создать
			JToolBarHelper::save2new( 'testedit.save2new' );
			//Выводим кнопку закрыть
			JToolBarHelper::cancel( 'testedit.cancel' );
		} else {
			//Если нет доступа то проверяем отдельные дейстивя
			//Если пользователь может редактировать только свои или может редактировать
			if ( $canDo->get( 'core.edit' ) || ( $canDo->get( 'core.edit.own' ) && $this->item->created_by == $this->user->get( 'id' ) ) ) {
				//выводим кнопку применить
				JToolBarHelper::apply( 'testedit.apply' );
				//Ввыводим кнопке сохранить
				JToolBarHelper::save( 'testedit.save' );
				// если пользователь может создавать
				if ( $canDo->get( 'core.create' ) ) {
					//Выводим кнопку сохранить и создать
					JToolBarHelper::save2new( 'testedit.save2new' );
				}
			}
			// если пользователь может создавать
			if ( $canDo->get( 'core.create' ) ) {
				//Выводим кнопку сохранить и создать
				JToolBarHelper::save2copy( 'testedit.save2copy' );
			}
			//выводим кнопку закрыть
			JToolBarHelper::cancel( 'testedit.cancel', 'JTOOLBAR_CLOSE' );
		}
	}
}

Вы можете обратить внимание что в при создании некоторых кнопко есть параметр 'testedit.save' это означает что будет вызванна задача save в контроллере testedit

Шаблон вывода вида controllers/view/testedit/tmpl/default.php

В этом файле комментариев нет но и без них должно быть все понятно!

<?php
// No direct access
defined( '_JEXEC' ) or die;
$fields = array( 'title', 'alias', 'published' );
?>
<script type="text/javascript">
    Joomla.submitbutton = function(task) {
        if (task == 'testedit.cancel' || document.formvalidator.isValid(document.id('item-form'))) {
        <?php echo $this->form->getField( 'text' )->save(); ?>
            Joomla.submitform(task, document.getElementById('item-form'));
        } else {
            alert('<?php echo $this->escape( JText::_( 'JGLOBAL_VALIDATION_FORM_FAILED' ) ); ?>');
        }
    }
</script>
<form action="<?php echo JRoute::_( 'index.php?option=com_test&id=' . $this->form->getValue( 'id' ) ); ?>" method="post" name="adminForm" id="item-form" class="form-validate" enctype="multipart/form-data">
    <div class="width-60 fltlft">
        <fieldset class="adminform">
            <legend><?php echo JText::_( 'JGLOBAL_EDIT_ITEM' ); ?></legend>
            <ul class="adminformlist">
                <?php foreach ( $fields as $field ): ?>
                <li>
                    <?php echo $this->form->getLabel( $field ); ?>
                    <?php echo $this->form->getInput( $field ); ?>
                </li>
                <?php endforeach; ?>
                <?php if ( $this->canDo->get( 'core.admin' ) ): ?>
                <li><span class="faux-label"><?php echo JText::_( 'JGLOBAL_ACTION_PERMISSIONS_LABEL' ); ?></span>
                    <div class="button2-left"><div class="blank">
                        <button type="button" onclick="document.location.href='#access-rules';">
                            <?php echo JText::_( 'JGLOBAL_PERMISSIONS_ANCHOR' ); ?>
                        </button>
                    </div></div>
                </li>
                <?php endif; ?>
            </ul>
            <div class="clr"></div>
            <div><?php echo $this->form->getLabel( 'text' ); ?></div>
            <div class="clr"></div>
            <div><?php echo $this->form->getInput( 'text' ); ?></div>
        </fieldset>
    </div>
    <div class="width-40 fltrt">
        <?php echo JHtml::_( 'sliders.start', 'content-sliders', array( 'useCookie' => 1 ) ); ?>
        <?php echo JHtml::_( 'sliders.panel', JText::_( 'JGLOBAL_FIELDSET_PUBLISHING' ), 'publishing-details' ); ?>
        <fieldset class="panelform">
            <ul class="adminformlist">
                <li><?php echo $this->form->getLabel( 'created_by' ); ?><?php echo $this->form->getInput( 'created_by' ); ?></li>
                <li><?php echo $this->form->getLabel( 'created' ); ?><?php echo $this->form->getInput( 'created' ); ?></li>
            </ul>
        </fieldset>
        <?php echo JHtml::_( 'sliders.panel', JText::_( 'JGLOBAL_FIELDSET_METADATA_OPTIONS' ), 'publishing-details' ); ?>
        <fieldset class="panelform">
            <?php echo $this->form->getLabel( 'metadesc' ); ?>
            <?php echo $this->form->getInput( 'metadesc' ); ?>
            <?php echo $this->form->getLabel( 'metakey' ); ?>
            <?php echo $this->form->getInput( 'metakey' ); ?>
        </fieldset>
        <?php echo JHtml::_( 'sliders.end' ); ?>
    </div>
    <?php if ( $this->canDo->get( 'core.admin' ) ): ?>
    <div class="width-100 fltlft">
        <?php echo JHtml::_( 'sliders.start', 'permissions-sliders-' . $this->item->id, array( 'useCookie' => 1 ) ); ?>

        <?php echo JHtml::_( 'sliders.panel', JText::_( 'JGLOBAL_ACTION_PERMISSIONS_DESCRIPTION' ), 'access-rules' ); ?>
        <fieldset class="panelform">
            <?php echo $this->form->getLabel( 'rules' ); ?>
            <?php echo $this->form->getInput( 'rules' ); ?>
        </fieldset>

        <?php echo JHtml::_( 'sliders.end' ); ?>
    </div>
    <?php endif; ?>
    <input type="hidden" name="task" value="" />
    <input type="hidden" name="return" value="<?php echo JRequest::getCmd( 'return' ); ?>" />
    <?php echo JHtml::_( 'form.token' ); ?>
</form>
Помощник вида helpers/testhelper.php
<?php
// запрещаем доступ извне
defined('_JEXEC') or die;
//Класс помощника вида
class testHelper
{
	//метод получения доступов пользователя
	public static function getActions( $section, $recordId = 0 )
	{
		//получаем объект текущего пользователя
		$user = JFactory::getUser();
		//Создаем новый экземпляр класа JObject
		$result = new JObject;
		//Если поле текущая запись не пустое
		if ( empty( $recordId ) ) {
			$assetName = 'com_test';
		} else {
			$assetName = 'com_test.' . $section . '.' . (int)$recordId;
		}
		//Список возможныъ дейсвий в системе
		$actions = array(
			'core.admin', 'core.manage', 'core.create', 'core.edit',
			'core.edit.own', 'core.edit.state', 'core.delete'
		);
		//перебираем все действия и устанавливаем в объект result true если
		//действие доступно и false если недоступно
		foreach ( $actions as $action ) {
			$result->set( $action, $user->authorise( $action, $assetName ) );
		}
		//возвращаем результат
		return $result;
	}
}

Логика работы всего этого такая: если мы переходим по ссылке index.php?option=com_test&view=testedit то контроллер по умолчанию controller.php и вызываем метод display() который подключает наш вид views/testedit/view.html.php. Вид в свою очередь вызывает модель models/testedit.php и получает из неё данные которые потом отображаются в шаблоне вывода вида views/testedit/tmpl/default.php. Так же вид создает кнопки управления (Сохранить, Применить, Сохранить и закрыть и Закрыть)
Если же передать к ссылке добавить переменную id:
index.php?option=com_test&view=testedit&id=10 то в нашу форму будет загружена запись из таблицы

Если же мы нажимаем на одну из кнопок например testedit.save то вызывается контроллер controllers/testedit.php который выполнит задачу save и если нужно вызовет модель для выполнения каких то действий с БД

После всего этого наш компонент будет выглядеть так: Вид компонента testedit

Скачать пример компонента com_test можно тут!