Цель лекции: Изучить, как происходит работа с базой данных средствами Joomla.

Префикс таблиц базы данных

Префикс таблиц базы данных - это строка, которая присоединяется к названию каждой таблицы Joomla в базе данных. Префикс задается при установке Joomla. В старых версиях по умолчанию использовался префикс "jos_", однако это создавало потенциальную уязвимость сайта, т.к. хакеры знали название таблицы с паролями пользователей - "jos_users". Теперь префикс, предлагаемый при установке, генерируется случайным образом.

Использование префикса позволяет разместить в одной базе данных несколько установок Joomla.

Различают реальный и символический префиксы. Реальный префикс - это то конкретное сочетание символов, которое используется в названиях таблиц базы данных. Символический префикс - это сочетание "#__" (решетка и два знака подчеркивания), которое используется в запросах вместо реального префикса. При обработке запроса вместо символического префикса будет автоматически подставлен реальный. Например, при реальном префиксе "jos_" строка "#__mycomponent_mytable" превратится в "jos_mycomponent_mytable".

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

Выполнение запроса к базе данных

Чтобы выполнить запрос к базе данных Joomla, необходимо осуществить пять операций:

  1. Получение ссылки на объект JDatabase.
  2. Формирование запроса.
  3. Задание запроса.
  4. Выполнение запроса.
  5. При необходимости - загрузка результата.

Получение ссылки на объект JDatabase

JDatabase - абстрактный класс, предоставляющий доступ к соединению с базой данных. Это соединение создается при инициализации приложения Joomla, а в коде своего расширения мы можем получить ссылку на него с помощью метода getDbo() статического класса JFactory:

$db = JFactory::getDbo();

Формирование SQL-запроса

В старых версиях Joomla запросы формулировались в виде строки:

$query = 'SELECT * FROM #__categories';

В Joomla 1.6 появился объект JDatabaseQuery, методы которого позволяют упростить создание сложных SQL-запросов. Названия этих методов практически совпадают с ключевыми словами языка SQL: select(), from(), where(), having(), join() и т.д. Использование объекта JDatabaseQuery иллюстрирует следующий пример:

$db = JFactory::getDbo();
$query = $db->getQuery(true);
$query->select('id, name');
$query->from('#__users');
$query->order('name');
$query->where('username LIKE \'a%\'');
$db->setQuery($query);
echo $query->__toString();

В данном примере мы получаем из таблицы #__users отсортированный по алфавиту список id и имен пользователей, чьи логины начинаются на букву "a". Данный код выведет на экран следующий SQL-запрос:

SELECT id, name FROM #__users WHERE username LIKE 'a%' ORDER BY name

Как известно, употребляющиеся в запросе названия полей и таблиц рекомендуется заключать в ограничители, чтобы избежать совпадений с зарезервированными словами. Кроме того, строковые значения в запросах также берутся в кавычки. Методы nameQuote() и Quote() заключают, соответственно, названия и значения в правильные ограничители. Для MySQL это обратные апострофы (``) для названий и обычные апострофы ('') для значений.

Рассмотрим пример использования этих методов:

$query = 'SELECT * FROM '.$db->nameQuote('#__users').' WHERE '.$db->nameQuote('username').'='.$db->Quote('admin');

Для базы данных MySQL с префиксом таблиц jos_ переменная $query примет следующее значение:

SELECT * FROM `jos_users` WHERE `username`='admin';

Задание запроса

Чтобы задать SQL-запрос для последующего выполнения, используется метод:

JDatabase setQuery(string $query, string $offset=0, string $limit=0)

где $query - это запрос, а $offset и $limit - соответственно смещение для начала выборки и количество выбираемых строк.

Например:

$db->setQuery($query, 0, 10);

Обратите внимание, что метод setQuery() не выполняет запрос, а только задает его.

Выполнение запроса

Без выборки данных

Для выполнения запроса, не требующего выборки данных (например, UPDATE или INSERT), используется метод mixed query().

При успешном выполнении запроса метод возвращает указатель на его результат, в противном случае - false. Например:

$db = JFactory::getDBO();
$query = "UPDATE #__users SET block = 0 WHERE username LIKE 'a%'";
$db->setQuery($query);
$result = $db->query();

С выборкой данных

В классе JDatabase существуют методы для получения форматированного результата. Их можно разделить на следующие группы:

  1. Получение одного значения: loadResult().
  2. Получение одной строки таблицы: loadRow(), loadAssoc(), loadObject().
  3. Получение одного столбца таблицы: loadResultArray().
  4. Получение нескольких строк и нескольких столбцов: loadRowList(), loadAssocList(), loadObjectList().

Рассмотрим каждый из этих методов на примере таблицы #__categories, использующейся Joomla (таблица 2.1).

Таблица 2.1. Таблица #__categories
idasset_idparent_idlftrgtlevelpathextensionlanguage
1 0 0 0 11 0   system *
2 27 1 1 2 1 uncategorised com_content *
3 28 1 3 4 1 uncategorised com_banners *
4 29 1 5 6 1 uncategorised com_contact *
5 30 1 7 8 1 uncategorised com_newsfeeds *
6 31 1 9 10 1 uncategorised com_weblinks *

mixed loadResult()

Метод загружает значение первого столбца первой строки результирующей выборки. Используется для получения из базы данных какого-либо одного значения. При ошибке выполнения запроса метод вернет null, как и все рассмотренные ниже методы выборки данных.

Например, получим значение поля extension в записи под номером 2:

$db = JFactory::getDbo();
$query = 'SELECT '.$db->nameQuote('extension').
      ' FROM '.$db->nameQuote('#__categories').
      ' WHERE '.$db->nameQuote('id').'='.$db->Quote('2');
$db->setQuery($query);
echo $db->loadResult();

Результатом выполнения данного запроса будет значение "com_content".

array loadRow()

Загружает первую строку результирующей выборки в виде массива. Если запрос возвращает больше одной строки, то метод вернет первую из них.

$db = JFactory::getDbo();
$query = 'SELECT * FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
print_r($db->loadRow());

Результатом запроса будет следующий список (будем называть списком массив, индексами которого являются числа 0, 1, 2 и т.д.):

Array([0]=>1 [1]=>0 [2]=>0 [3]=>0 [4]=>11 [5]=>0 [6]=> [7]=>system [8]=>ROOT
 [9]=>root [10]=> [11]=> [12]=>1 [13]=>0 [14]=>0000-00-00 00:00:00 
[15]=>1 [16]=>{} [17]=> [18]=> [19]=> [20]=>0 [21]=>2009-10-18 16:07:09
 [22]=>0 [23]=>0000-00-00 00:00:00 [24]=>0 [25]=>*)

array loadAssoc()

Метод загружает первую строку результирующей выборки в виде ассоциативного массива, ключами которого становятся названия полей таблицы. Если запрос возвращает больше одной строки, то метод вернет первую из них.

$db = JFactory::getDbo();
$query = 'SELECT * FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
print_r($db->loadAssoc());

Результат запроса:

Array([id]=>1 [asset_id]=>0 [parent_id]=>0 [lft]=>0 [rgt]=>11 [level]=>0 [path]=> [extension]=
>system [title]=>ROOT [alias]=>root [note]=> [description]=> 
[published]=>1 [checked_out]=>0 [checked_out_time]=>0000-00-00 00:00:00 [access]=
>1 [params]=>{} [metadesc]=> [metakey]=> [metadata]=> [created_user_id]=>0 
[created_time]=>2009-10-18 16:07:09 [modified_user_id]=
>0 [modified_time]=>0000-00-00 00:00:00 [hits]=>0 [language]=>*)

object loadObject()

Метод загружает первую строку результирующей выборки в виде объекта класса stdClass, причем его полями становятся названия полей таблицы. Если запрос возвращает больше одной строки, то метод вернет первую из них.

$db = JFactory::getDbo();
$query = 'SELECT * FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
print_r($db->loadObject());

Результат запроса:

stdClass Object([id]=>1 [asset_id]=>0 [parent_id]=>0 [lft]=>0 [rgt]=>11 [level]=
>0 [path]=> [extension]=>system [title]=>ROOT [alias]=>root [note]=> 
[description]=> [published]=>1 [checked_out]=>0 [checked_out_time]=>0000-00-00 00:00:00 [access]=
>1 [params]=>{} [metadesc]=> [metakey]=> [metadata]=> 
[created_user_id]=>0 [created_time]=>2009-10-18 16:07:09 [modified_user_id]=>0 [modified_time]=
>0000-00-00 00:00:00 [hits]=>0 [language]=>*)

array loadResultArray(int numinarray=0)

Метод загружает массив значений из результирующей выборки, полученных из одного столбца. Параметр numinarray используется для указания того, какой столбец нужно вернуть.

$db = JFactory::getDbo();
$query = 'SELECT '.$db->nameQuote('extension').
      ' FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
print_r($db->loadResultArray()); 

Результатом будет следующий список:

Array([0]=>com_banners [1]=>com_contact [2]=>com_content [3]=>com_newsfeeds [4]=>com_weblinks [5]=>system)

Данный метод позволяет перебирать в цикле столбцы таблицы:

$db = JFactory::getDbo();
$query = 'SELECT '.$db->nameQuote('path').','.$db->nameQuote('extension').','.$db->nameQuote('language').
' FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
for ($i = 0; $i <= 2; $i++)
{
  $column = $db->loadResultArray($i);
  print_r($column);
  echo "<br>";
}

В результате на экран будет выведено:

Array([0]=> [1]=>uncategorised [2]=>uncategorised [3]=
>uncategorised [4]=>uncategorised [5]=>uncategorised)
Array([0]=>system [1]=>com_content [2]=>com_banners [3]=
>com_contact [4]=>com_newsfeeds [5]=>com_weblinks)
Array([0]=>* [1]=>* [2]=>* [3]=>* [4]=>* [5]=>*)

array loadRowList(int key)

Метод загружает список массивов или ассоциативный массив массивов. Если задан параметр key, то ключами возвращаемого массива будут значения поля, идущего в таблице под номером key, начиная с нуля.

$db = JFactory::getDbo();
$query = 'SELECT * FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
print_r($db->loadRowList(7));

В данном примере из таблицы #__categories ядра Joomla извлекаются все записи, причем ключами полученного массива будут значения столбца №7, т.е. поля extension:

Array
(
  [system]=>Array([0]=>1   … [25]=>*)
  [com_content]=>Array([0]=>2   … [25]=>*) 
  [com_banners]=>Array([0]=>3   … [25]=>*) 
  [com_contact]=>Array([0]=>4   … [25]=>*) 
  [com_newsfeeds]=>Array([0]=>5 … [25]=>*) 
  [com_weblinks]=>Array([0]=>6 … [25]=>*)
)

Если не указать параметр key, то вместо ассоциативного массива мы получим список:

Array
(
  [0]=>Array([0]=>1 … [25]=>*)
  [1]=>Array([0]=>2 … [25]=>*) 
  [2]=>Array([0]=>3 … [25]=>*) 
  [3]=>Array([0]=>4 … [25]=>*) 
  [4]=>Array([0]=>5 … [25]=>*) 
  [5]=>Array([0]=>6 … [25]=>*)
)
  

array loadAssocList(string key='', string column='')

Метод загружает список ассоциативных массивов или ассоциативный массив ассоциативных массивов. Если задан параметр key, то ключами полученного массива будут значения столбца под названием key. Если задан параметр column, то в полученном массиве будет всего один столбец column.

$db = JFactory::getDbo();
$query = 'SELECT * FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
print_r($db->loadAssocList('extension'));

Результат запроса:

Array
(
  [system]=>Array([id]=>1 [asset_id]=>0 ... [language]=>*)
  [com_content]=>Array([id]=>2 [asset_id]=>27 ... [language]=>*) 
  [com_banners]=>Array([id]=>3 [asset_id]=>28 ... [language]=>*) 
  [com_contact]=>Array([id]=>4 [asset_id]=>29 ... [language]=>*) 
  [com_newsfeeds]=>Array([id]=>5 [asset_id]=>30 ... [language]=>*) 
  [com_weblinks]=>Array([id]=>6 [asset_id]=>31 ... [language]=>*)
)

Как видим, ключи полученного массива - это значения поля extension, заданного параметром в метод loadAssocList().

Зададим значение второго параметра, чтобы получить только значение id для каждой строки таблицы:

$db = JFactory::getDbo();
$query = 'SELECT * FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
print_r($db->loadAssocList('extension','id'));

Результат запроса:

Array([system]=>1 [com_content]=>2 [com_banners]=>3 [com_contact]=>4 [com_newsfeeds]=>5 [com_weblinks]=>6)

array loadObjectList(string key='')

Метод загружает список объектов stdClass или ассоциативный массив объектов stdClass. Если задан параметр key, то ключами полученного массива будут значения поля под названием key:

$db = JFactory::getDbo();
$query = 'SELECT * FROM '.$db->nameQuote('#__categories');
$db->setQuery($query);
print_r($db->loadObjectList('extension'));

Результат запроса:

Array
(
  [system]=>stdClass Object([id]=>1 [asset_id]=>0 ... [language]=>*)
  [com_content]=>stdClass Object([id]=>2 [asset_id]=>27 ... [language]=>*)
  [com_banners]=>stdClass Object([id]=>3 [asset_id]=>28 ... [language]=>*)
  [com_contact]=>stdClass Object([id]=>4 [asset_id]=>29 ... [language]=>*)
  [com_newsfeeds]=>stdClass Object([id]=>5 [asset_id]=>30 ... [language]=>*)
  [com_weblinks]=>stdClass Object([id]=>6 [asset_id]=>31 .. [language]=>*)
)


Таблицы базы данных (класс JTable)

Класс JTable реализует паттерн Active Record и используется для управления таблицами базы данных.

Для каждой таблицы, которую вы будете создавать для своего компонента, необходимо создать класс, производный от JTable. Каждый такой класс помещается в отдельном файле в папке /administrator/components/com_<имя компонента>/tables. Имя класса строится по схеме Table<название таблицы>, а файла - <название таблицы>.php.

Для каждого поля таблицы необходимо создать одноименное поле класса.

Кроме того, создается конструктор класса, принимающий ссылку на объект JDatabase. Конструктор вызывает родительский конструктор, передавая ему название таблицы, название поля, являющегося первичным ключом таблицы, и объект JDatabase.

Например, пусть имеется таблица #__mycomponent_mytable из двух столбцов: id и name. Тогда производный от JTable класс должен выглядеть так:

class TableMytable extends JTable
{
  var $id = null;
  var $name = null;
  function __construct(&$db)
  {
    parent::__construct('#__mycomponent_mytable', 'id', $db);
  }
}

Чтобы использовать в коде вашего компонента файл, содержащий этот класс, нужно добавить папку tables в список директорий, в которых JTable может искать классы таблиц:

JTable::addIncludePath(JPATH_ADMINISTRATOR.DS.'components'.DS.'com_mycomponent'.DS.'tables');

 

Теперь при создании экземпляра класса TableMytable Joomla будет искать файл mytable.php.

Для получения экземпляра данного класса используется метод getInstance():

object getInstance(string $type, string $prefix= 'JTable', array $config=array())

где

$type - вторая часть имени класса;
$prefix - первая часть имени класса;
$config - массив, содержащий настройки конфигурации.

В их числе может находиться объект-представитель базы данных, и тогда он будет использован вместо глобального объекта JDatabase. Например:

$row =& JTable::getInstance('mytable', 'Table');

Производный от JTable класс наследует в числе прочих методы bind(), store(), load() и delete(), позволяющие управлять записями таблицы без единой строки SQL-кода.

Создание/редактирование записи таблицы

Как правило, для создания или редактирования записи таблицы во фронтенде или бэкенде создается HTML-форма. Значения, введенные в нее пользователем, можно получить с помощью класса JRequest в виде массива. Полученный массив необходимо связать с объектом JTable. Связывание заключается в том, что каждому полю класса присваивается значение элемента массива, ключ которого совпадает с названием этого поля. Для этого используется метод

bool bind(mixed $src, mixed $ignore=array())

где:

$src - ассоциативный массив или объект для связывания с экземпляром класса;
$ignore - массив или разделенный пробелами список полей, которые нужно игнорировать при связывании.

Например:

if (!$row->bind(JRequest::get('post')))
  return JError::raiseWarning(500, $row->getError());

Обычно ошибка связывания возникает, если поля класса не соответствуют ключам массива, - например, когда в HTML-форме в название элемента input вкралась опечатка.

Для сохранения записи используется метод

bool store(bool $updateNulls=false)


Если параметр $updateNulls равен false, то те поля объекта JTable, которые имеют значение null, будут игнорироваться при связывании.

Пример использования метода store():

if (!$row->store())
  JError::raiseError(500, $row->getError());

Метод store() на основе хранящихся в $row значений генерирует запрос UPDATE или INSERT, в зависимости от значения id. Если запись создается впервые, то она не имеет значения id и будет сконструирован запрос INSERT, в противном случае - UPDATE. Это позволяет использовать данный метод как для создания новых записей, так и для обновления существующих.

Получение записи из таблицы

Для получения записи используется метод

bool load(mixed $keys = NULL, bool $reset = true)

где

$keys - первичный ключ записи, которую необходимо получить, или массив полей для поиска соответствий;
$reset - задает, будут ли перед получением новой записи заново присвоены полям значения по умолчанию.

Например:

$row->load($id);


Удаление записи

Для удаления записи используется метод

bool delete(mixed $id=null)

где $id - первичный ключ записи, которую требуется удалить. Если он не задан, то используется значение соответствующего поля объекта.

Пример:

$row->delete($id);


Управление полями ordering, checked_out/checked_out_time, published и hits

Существуют методы класса JTable для управления часто используемыми полями ordering, checked_out/checked_out_time, published и hits.

ordering

Поле ordering позволяет пользователю упорядочить список объектов по своему желанию. Чтобы пересчитать значения в поле ordering, используется метод

 void reorder([string $where = ''])


При этом записи сортируются по значению ordering, а затем в это поле записываются натуральные числа, начиная с 1. Параметр $where позволяет задать условие ограничения выборки, к которой будет применена эта операция.

Пример использования метода:

$table->reorder();

Изменить значение ordering для одной записи, передвинув ее выше или ниже в списке, можно с помощью метода

void move(int $dirn, [string $where = ''])

где $dirn - величина, которая будет прибавлена к текущему значению ordering (отрицательное значение приведет к смещению записи вверх, а положительное - вниз).

Например, поднимем запись на одну строку вверх:

$table->load($id);
$table->move(-1);


checked_out/checked_out_time

Поля checked_out и checked_out_time используются для блокировки записей во избежание редактирования их несколькими пользователями одновременно. checked_out хранит id пользователя, работающего с записью в данный момент, а checked_out_time - время начала редактирования. Прежде чем заблокировать запись, необходимо проверить, не заблокирована ли она уже другим пользователем, с помощью метода

bool isCheckedOut(int $with=0, int $against=null)

где:

$with - id пользователя, с которым нужно сравнить значение поля checked_out. Если запись заблокирована как раз этим пользователем, то функция вернет false, как и в том случае, если она не заблокирована вообще. В обоих этих случаях текущий пользователь имеет право работать с ней;
$against - id пользователя, использующийся, если функция вызвана как статическая.


Для блокировки записей используется метод

bool checkOut(int $userId, mixed $pk=null)

где:

$userId - id пользователя, блокирующего запись;
$pk - первичный ключ записи, которую необходимо заблокировать. Если он не задан, используется значения соответствующего поля класса.


При этом в поле checked_out_time будет записано текущее время.

Для разблокировки записей используется метод

bool checkIn(mixed $pk=null)

Рассмотрим пример использования этих методов:

$table->load($id);
$user = JFactory::getUser();
if ($table->isCheckedOut($user->get('id')))
  die('Запись уже заблокирована другим пользователем');
echo 'Запись не заблокирована';
if (!$table->checkout($user->get('id')))
  die('Не удалось заблокировать запись с id текущего пользователя');
echo 'Заблокировали запись';
// работа с записью...
if (!$table->checkin($user->get('id')))
  die('Не удалось разблокировать запись');
echo 'Разблокировали запись';


published

Значение поля published показывает, опубликована ли запись. Чтобы изменить значение этого поля для одной или нескольких записей, используется метод

bool publish(mixed $pks=null, int $state=1, int $userId=0)

где:

$pks - массив ключей записей, к которым необходимо применить операцию;
$state - новое значение поля published (0 или 1);
$userId - используется только когда в таблице существует также поле checked_out. При наличии в таблице этого поля метод publish() может быть применен только к тем записям, для которых checked_out равно 0 или заданному $userId.

Метод вернет true и в том случае, если какие-либо из записей были заблокированы и для них не удалось изменить значение published.

Пример использования этого метода:

$id_list = array($id);
$user = JFactory::getUser();
if (!$table->publish($id_list, 1, $user->get('id')))
  die($table->getError());

hits

В поле hits хранится количество просмотров записи. Для увеличения этого значения на 1 используется метод

bool hit(mixed $pk=null)

где $pk - первичный ключ записи.

Например:

$table->hit();

Практика

Создание таблицы базы данных

Создайте таблицу для хранения вопросов, выполнив следующий SQL-запрос:

CREATE TABLE `jos_myquestions`
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`date` DATETIME NOT NULL,
`question` TEXT NOT NULL,
`city` VARCHAR(50) NULL,
`email` VARCHAR(50) NOT NULL,
`IP` VARCHAR(15) NOT NULL,
`id_cat` INT NOT NULL,
`published` TINYINT(1) NULL DEFAULT '1',
`expiration_date` DATETIME NULL DEFAULT '0000-00-00 00:00:00',
`senttoexpert` TINYINT(1) NULL DEFAULT '0',
`answer` TEXT NULL DEFAULT '',
`senttoauthor` TINYINT(1) NULL DEFAULT '0'
);

По умолчанию дата снятия вопроса с публикации имеет значение '0000-00-00 00:00:00'. Будем считать, что такая дата означает, что вопрос опубликован навсегда.

Для тестирования системы добавьте запись в таблицу, выполнив SQL-запрос:

INSERT INTO `jos_myquestions`(`id`, `name`, `date`, `question`, `city`, `email`, `IP`, `id_cat`)
 VALUES(NULL, 'Аноним', '2012-01-01 09:00:00', 
'Есть ли жизнь на Марсе', 'Москва', 'somebody@mail.ru', '12.345.67.890', '1')

Создание класса таблицы

Создайте в папке /administrator/components/com_myquestions папку tables. В этой папке создайте файл question.php:

<?php
defined('_JEXEC') or die('Restricted access');
class TableQuestion extends JTable
{
  var $id = null;
  var $name = null;
var $date = null;
  var $question = null;
  var $city = null;
  var $email = null;
  var $IP = null;
  var $id_cat = null;
  var $published = null;
  var $expiration_date = null;
  var $senttoexpert = null;
  var $answer = null;
  var $senttoauthor = null;
  function __construct(&$db)
  {
    parent::__construct('#__myquestions', 'id', $db);
  }
}
?>

Как видите, класс TableQuestion расширяет класс JTable. Каждому полю таблицы #__myquestions соответствует поле этого класса. Также перегружен конструктор __construct(), принимающий в качестве параметра объект-представитель базы данных и вызывающий родительский конструктор, используя название таблицы базы данных, первичный ключ и объект-представитель базы данных.

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

Как и ранее, отделим HTML-вывод от логики обработки. PHP-код, необходимый для загрузки значений элементов формы, будет храниться в файле admin.myquestions.php, а код формы - в файле admin.myquestions.html.php. Откройте admin.myquestions.php и замените его содержимое следующим кодом:

<?php defined('_JEXEC') or die('Restricted access'); 
 $option = JRequest::getVar('option'); 
$task = JRequest::getVar('task');
require_once (JApplicationHelper::getPath('admin_html')); 
JTable::addIncludePath(JPATH_COMPONENT.DS.'tables') ;  
switch($task) {   case 'reply':     replyToQuestion($option);     break;   default:     break; }  
function replyToQuestion($option) 
{   $row =& JTable::getInstance('Question','Table');   $cid = JRequest::getVar('cid', array(0), '',
 'array');   $id = $cid[0];   $row->load($id);   
HTML_questions::replyToQuestion($row, $option); } ?>

Проверив, что код вызван из Joomla, мы используем выражение require_once(JApplicationHelper::getPath('admin_html')) для подключения файла admin.myquestions.html.php.

Затем с помощью JTable::addIncludePath() папка tables добавляется к списку директорий, в которых следует искать классы таблиц.

Переключатель switch() вызывает функцию, соответствующую значению переменной $task.

В функции replyToQuestion() создается экземпляр класса TableQuestion для управления записью таблицы. С помощью JRequest::getVar() из переменных запроса извлекается массив cid, хранящий идентификаторы записей. Так как эксперт будет отвечать только на один вопрос за раз, то мы выбираем первый идентификатор и загружаем соответствующую запись. Затем она передается в функцию вывода HTML_questions::replyToQuestion().

Теперь создайте файл /administrator/components/com_myquestions/admin.myquestions.html.php:

<?php
defined ('_JEXEC') or die ('Restricted access');
class HTML_questions
{
  function replyToQuestion($row, $option)
  {
    $editor = JFactory::getEditor();
    ?>
    <form action = "index.php" method="post" 
    name="adminForm" id="adminForm">
      <fieldset class="adminform">
        <table class="admintable" width=100%>
          <tr>
            <td width="100" class="key">
               <?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 $row->name;?>"/>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_DATE');?>:
            </td>
            <td>
              <span class="text_area" type="text" name="date"
               id="date"><?php echo JHTML::_('date', $row->date,JText::_('DATE_FORMAT_LC3'));?></span>
            </td>
          </tr>          
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_QUESTION');?>:
            </td>
            <td>
              <?php
                echo $editor->display('question',  $row->question, '100%', '250', '40', '10');?>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_CITY');?>:
            </td>
            <td>
              <input class="text_area" type="text" name="city" id="city"
               size="50" maxlength="50" value="<?php echo $row->city;?>"/>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_EMAIL');?>:
            </td>
            <td>
              <input class="text_area" type="text" name="email" id="email"
               size="50" maxlength="50" value="<?php echo $row->email;?>"/>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_IP');?>:
            </td>
            <td>
              <span class="text_area" type="text" name="IP"
               id="IP"><?php echo $row->IP;?></span>
            </td>
          </tr>
          <tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_CATEGORY');?>:
            </td>
            <td>
              <input class="text_area" type="text" name="id_cat" id="id_cat" 
              size="50" maxlength="250" value="<?php echo $row->id_cat;?>"/>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_PUBLISHED');?>:
            </td>
            <td valign="top">
              <?php
                if ($row->published == '1')
                  echo JText::_('JYES');
                else
                  echo JText::_('JNO');?>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_EXPIRATION_DATE');?>:
            </td>
            <td>
              <?php echo JHTML::_('calendar', $row->expiration_date, 'expiration_date', 'expiration_date', '%Y-%m-%d');?>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_SENTTOEXPERT');?>:
            </td>
            <td valign="top">
              <?php
                if ($row->senttoexpert == '1')
                  echo JText::_('JYES');
                else
                  echo JText::_('JNO');?>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_ANSWER');?>:
            </td>
            <td>
              <?php
                echo $editor->display('answer',  $row->answer,'100%', '250', '40', '10');?>
            </td>
          </tr>
          <tr>
            <td width="100" class="key">
              <?php echo JText::_('COM_MYQUESTIONS_SENTTOAUTHOR');?>:
            </td>
            <td valign="top">
                <?php
                if ($row->senttoauthor == '1')
                  echo JText::_('JYES');
                else
                  echo JText::_('JNO');?>
            </td>
          </tr>
        </table>
      </fieldset>
      <input type="hidden" name="id" value="<?php echo $row->id;?>"/>
      <input type="hidden" name="option" value="<?php echo $option;?>"/>
      <input type="hidden" name="task" value=""/>
    </form>
  <?php
  }
}
?>
 Листинг .

Функция HTML_questions::replyToQuestion() выводит на экран уже заполненную форму, значения элементов которой берутся из объекта $row. Форме присвоено название adminForm, чтобы к ней можно было обращаться из JavaScript.

Классы JHTML и JEditor будут рассмотрены позже. Сейчас поясним только те выражения, в которых используются методы этих классов:

echo JHTML::_('date', $row->date,JText::_('DATE_FORMAT_LC3'));
выводит дату $row->date в формате DATE_FORMAT_LC3 (один из стандартных форматов, заданных в Joomla).
$editor = JFactory::getEditor(); echo $editor->display('question', $row->question, '100%', '250', '40', '10');
отображает выбранный администратором HTML-редактор. Если не выбран ни один редактор, то будет отображено поле <textarea>. В редакторе или поле <textarea> будет выведено значение $row->question.
echo JHTML::_('calendar', $row->expiration_date, 'expiration_date', 'expiration_date', '%Y-%m-%d');
выводит текстовое поле со значением $row->expiration_date и пиктограмму календаря, при нажатии на которую появляется календарь для выбора даты.

Перед закрывающим тегом </form> выводятся три скрытые элемента. Первый из них хранит значение id записи, т.к. оно необходимо для дальнейшего сохранения отредактированного вопроса. Элемент option хранит название текущего компонента для правильного редиректа в дальнейшем. Третьему скрытому элементу, task, не присвоено значения, чтобы JavaScript-код панели инструментов мог изменять его до отправки формы.

Осталось добавить перевод ключей COM_MYQUESTIONS_AUTHOR, COM_MYQUESTIONS_DATE и др. Откройте файл /administrator/language/ru-RU/ru-RU.com_myquestions.ini и добавьте к его содержимому следующий код:

COM_MYQUESTIONS_AUTHOR="Автор"
COM_MYQUESTIONS_DATE="Дата вопроса"
COM_MYQUESTIONS_QUESTION="Текст вопроса"
COM_MYQUESTIONS_CITY="Город"
COM_MYQUESTIONS_EMAIL="e-mail"
COM_MYQUESTIONS_IP="IP-адрес"
COM_MYQUESTIONS_CATEGORY="Категория"
COM_MYQUESTIONS_PUBLISHED="Отображать ли вопрос на сайте"
COM_MYQUESTIONS_EXPIRATION_DATE="Дата снятия вопроса с публикации"
COM_MYQUESTIONS_SENTTOEXPERT="Отправлен ли вопрос эксперту"
COM_MYQUESTIONS_ANSWER="Ответ"
COM_MYQUESTIONS_SENTTOAUTHOR="Отправлен ли ответ автору вопроса"

Обратите внимание, что мы не задали перевод для слов "Да" и "Нет", а использовали ключи JYES и JNO, т.к. подобные распространенные слова уже переведены в файле /administrator/language/ru-RU/ru-RU.ini.

Наберите в адресной строке браузера ссылку http://localhost/joomla/administrator/index.php?option=com_myquestions&task=reply&cid[]=1. Должна появиться следующая страница (рис. 2.1).



02 01sm


увеличить изображение
Рис. 2.1.  Фрагмент формы для ответа на вопрос

Сохранение введенных данных

После того, как эксперт напечатал ответ на заданный вопрос и нажал кнопку "Сохранить", необходимо сохранить информацию в базе данных. Прежде всего, создайте две функции - save() и saveQuestion() - в файле admin.myquestions.php:

 function save() 
{   
    $row =& JTable::getInstance('question', 'Table');
    if (!$row->bind(JRequest::get('post')))
    {
        echo "<script> alert('".$row->getError()."'); 
        window.history.go(-1); </script>\n";
        exit();
    }
    $row->question = JRequest::getVar('question', '', 'post', 'string', JREQUEST_ALLOWRAW);
    $row->answer = JRequest::getVar('answer', '', 'post', 'string', JREQUEST_ALLOWRAW);
    
    if (!$row->store())
    {
        echo "<script> alert('".$row->getError()."'); 
        window.history.go(-1); </script>\n";
        exit();
    }
    return $row;
}
function saveQuestion($option, $task)
{
    $row = save();
    global $app;
    if ($task == 'save')
        $app->redirect('index.php?option='.$option, JText::_('COM_MYQUESTIONS_REPLY_SAVED'));
    else
        if ($task == 'apply')
            $app->redirect('index.php?option='.$option.'&task=
            reply&cid[]='.$row->id, JText::_('COM_MYQUESTIONS_REPLY_SAVED'));
}
        

Переменной $row присваивается значение экземпляра класса TableQuestion и вызывается функция bind() для связывания переменных, полученных из формы, с полями этого класса.

Для тех значений, которые вводились с помощью редактора Joomla, стандартный способ получения значений из массива JRequest::get('post') не подходит, т.к. функция bind() автоматически удаляет HTML-код, что приведет, в частности, к потере разрывов строк и тегов абзаца. Поэтому для получения значений question и answer в том виде, в котором они были введены в редакторе, используется функция getVar() класса JRequest. Данной функции передается имя переменной формы, значение по умолчанию, метод запроса, с помощью которого мы хотим получить данные (get/post), ожидаемый формат и флаг JREQUEST_ALLOWRAW, означающий, что данные не должны быть отфильтрованы.

Наконец, вызывается функция store() для сохранения вопроса в базе данных.

В функции saveQuestion() происходит вызов функции save(), а затем в зависимости от задачи, т.е. от того, какая кнопка была нажата, - "Сохранить" или "Сохранить и закрыть", - мы перенаправляем пользователя либо к той же странице редактирования вопроса, на которой он находится, но уже с сохраненными данными, либо к главной странице нашего компонента. В обоих случаях выводится сообщение о том, что данные были сохранены. Для перенаправления и вывода сообщения используется функция redirect() глобального объекта JApplication.

Добавьте задачу сохранения записи в переключатель switch() в файле admin.myquestions.php (выделенный код):

switch ($task)
{
	case 'reply' :
		replyToQuestion ($option);
		break;
	case 'save' :
	case 'apply' :
		saveQuestion($option,$task);
		break;
	default:
		break;
}

Добавьте перевод для ключа COM_MYQUESTIONS_REPLY_SAVED в файл /administrator/language/ru-RU/ru-RU.com_myquestions.ini:

COM_MYQUESTIONS_REPLY_SAVED="Данные сохранены"

Сохраните все ваши файлы и перейдите по ссылке http://localhost/joomla/administrator/index.php?option=com_myquestions&task=reply&cid[]=1. Напишите что-нибудь в поле для ответа и нажмите кнопку "Сохранить и закрыть". Вы должны увидеть следующую страницу (рис. 2.2).



02 02sm


увеличить изображение
Рис. 2.2.  Результат сохранения ответа на вопросС помощью phpMyAdmin вы можете проверить, что данные были сохранены в таблице базы данных jos_myquestions (рис. 2.3).



02 03sm


увеличить изображение
Рис. 2.3.  Ответ на вопрос сохранен в базе данных

Вывод списка записей

Прежде всего, добавьте в файл admin.myquestions.php следующую функцию:

function showQuestions($option)
{
    $db = JFactory::getDbo();
    $query = "SELECT * FROM #__myquestions";
    $db->setQuery($query);
    $rows = $db->loadObjectList();
    if ($db->getErrorNum())
    {
        echo $db->stderr();
        return false;
    }
    HTML_questions::showQuestions($option, $rows);
}

Эта функция загружает все записи из таблицы #__myquestions и передает их в виде массива $rows в следующую функцию, которую необходимо добавить в файл admin.myquestions.html.php в класс HTML_questions:

function showQuestions($option, &$rows) 
  { 
    $maxlen = 100;
  ?>
    <form action="index.php" method="post" name="adminForm">
      <table class="adminlist">
        <thead>
          <tr>
            <th width="20">
              <input type="checkbox" name="toggle" value="" 
              onclick="checkAll(<?php echo count($rows);?>);"/>
            </th>         
            <th class="title"><?php echo JText::_('COM_MYQUESTIONS_AUTHOR');?></th>
            <th><?php echo JText::_('COM_MYQUESTIONS_DATE');?></th>
            <th><?php echo JText::_('COM_MYQUESTIONS_QUESTION');?></th>
            <th><?php echo JText::_('COM_MYQUESTIONS_EMAIL');?></th>
            <th><?php echo JText::_('COM_MYQUESTIONS_PUBLISHED');?></th>
            <th><?php echo JText::_('COM_MYQUESTIONS_EXPIRATION_DATE');?></th>
            <th><?php echo JText::_('COM_MYQUESTIONS_SENTTOEXPERT');?></th>
            <th><?php echo JText::_('COM_MYQUESTIONS_ANSWER');?></th>
            <th><?php echo JText::_('COM_MYQUESTIONS_SENTTOAUTHOR');?></th>      
          </tr>
        </thead> 
        <?php
          jimport('joomla.filter.output');
          $k = 0;       
          for  ($i = 0,  $n = count($rows); $i < $n;  $i ++)       
          {
            $row = &$rows[$i];
            $checked = JHTML::_('grid.id', $i, $row->id);
            $link = JFilterOutput::ampReplace('index.php?option=' .$option . '&task=reply&cid[]='. $row->id);
        ?>
        <tr class="<?php echo "row$k";?>">
          <td><?=$checked?></td>
          <td><?=$row->name?></td>
          <td><?=JHTML::_('date', $row->date, JText::_('DATE_FORMAT_LC3'))?></td>
          <td><?='<a href="'.$link.'">'.substr(strip_tags($row->question),
          0,$maxlen-1).'</a>'?></td>
          <td><?=$row->email?></td>
          <td align="center">
            <?php
              if ($row->published == '1')
                echo JText::_('JYES');
              else
                echo JText::_('JNO');?>
          </td>
          <td>
            <?php
              if ($row->expiration_date == '0000-00-00 00:00:00')
                echo JText::_('COM_MYQUESTIONS_DATE_NOT_DEFINED');
              else
                echo JHTML::_('date', $row->expiration_date, JText::_('DATE_FORMAT_LC3'));?>
          </td>
          <td align="center">
            <?php
              if ($row->senttoexpert == '1')
                echo JText::_('JYES');
              else
                echo JText::_('JNO');?>
          </td>
          <td><?=substr(strip_tags($row->answer),0,$maxlen-1)?></td>
          <td align="center">
            <?php
              if ($row->senttoauthor == '1')
                echo JText::_('JYES');
              else
                echo JText::_('JNO');
            ?>
          </td>
        </tr>
        <?php
          $k = 1 - $k;
        }
        ?>
      </table>
      <input type="hidden" name="option" 
      value="<?php echo $option;?>"/>
      <input type="hidden" name="task" 
      value=""/>
      <input type= "hidden" name="boxchecked"
       value="0"/>
    </form>
  <?php
  }
        
 Листинг .

Записи выводятся в таблице, для которой задан CSS-класс adminlist. Все заголовки таблицы, кроме первого, - это обычный текст. Первый заголовок является чекбоксом и используется для одновременного выделения всех отображенных записей.

Затем начинается цикл для вывода самих записей. Значение переменной $k меняется с 0 на 1 и обратно для того, чтобы переключаться между различными классами CSS для четных и нечетных строк, имеющими немного различающиеся свойства фона. С помощью вызова функции JHTML::_('grid.id') мы получаем HTML-код для чекбокса, который будет обрабатываться с помощью JavaScript.

Для каждого вопроса и ответа выводятся первые maxlen символов вместо его текста целиком. При этом с помощью функции strip_tags() отбрасываются теги, чтобы предотвратить ситуацию, когда граница обрезки текста может оказаться внутри тега.

Для перехода к форме ответа на вопрос для каждой записи выводится гиперссылка, которая пропускается через функцию JFilterOutput::ampReplace(), заменяющую амперсанды "&" на коды "&" в соответствии со спецификацией XHTML. Для подключения класса JFilterOutput в код вставлена строка jimport('joomla.filter.output').

Перед закрывающим тегом </form> расположены три скрытых элемента. Option и task были рассмотрены при анализе формы для ответа на вопрос. Значение boxchecked заключается в следующем. Когда пользователь ставит флажок в каком-либо из чекбоксов, значение boxchecked меняется на 1. Значение boxchecked, равное 0, возвращается, когда ни один из чекбоксов не отмечен. С помощью этого значения JavaScript обрабатывает список.

Для обработки случая, когда не выбрано никакой задачи, измените код переключателя switch в файле admin.myquestions.php следующим образом:

switch($task)
{
    case 'reply':
        replyToQuestion($option);
        break;
    case 'save':
    case 'apply':
        saveQuestion($option, $task);
        break;
    default:
        showQuestions($option);
        break;
}

Добавьте в файл /administrator/language/ru-RU/ru-RU.com_myquestions.ini строку:

COM_MYQUESTIONS_DATE_NOT_DEFINED="Дата не задана"

Теперь при загрузке http://localhost/joomla/administrator/index.php?option=com_myquestions должна появиться страница, как на рис. 2.4.



02 04sm


увеличить изображение
Рис. 2.4.  Список вопросов

Удаление записей

Добавьте следующий оператор case в переключатель switch() в файле admin.myquestions.php:

case 'remove':
    removeQuestions($option);
    break;

Также добавьте функцию removeQuestions():

function removeQuestions($option)
{
    global $app;
    $cid = JRequest::getVar('cid', array(), '', 'array');
    $db = JFactory::getDbo();
    if(count($cid))
    {
        $cids = implode(',', $cid);
        $query = "DELETE FROM #__myquestions WHERE id IN ($cids)";
        $db->setQuery($query);
        if (!$db->query())
        {
            echo "<script> alert('".$db->getErrorMsg()."'); 
            window.history.go(-1); </script>\n";
        }
    }
    $app->redirect('index.php?option=' . $option, JText::_('COM_MYQUESTIONS_QUESTION_DELETED'));
}

Если в массиве cid есть элементы, то составляется строка из идентификаторов, разделенных запятыми, которая затем используется для построения запроса удаления соответствующих записей. В данном случае нельзя использовать метод JTable::delete(), т.к. он предназначен для удаления одной записи, а не нескольких.

Добавьте в файл /administrator/language/ru-RU/ru-RU.com_myquestions.ini строку:

COM_MYQUESTIONS_QUESTION_DELETED="Вопрос(ы) успешно удален(ы)"

 

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

JDatabase - абстрактный класс, предоставляющий доступ к соединению с базой данных, создающемуся при инициализации приложения Joomla.
JDatabaseQuery - класс, методы которого совпадают с ключевыми словами языка SQL и позволяют упростить создание сложных SQL-запросов.
JTable - класс, реализующий паттерн Active Record и использующийся для управления таблицами базы данных.
Префикс таблиц базы данных - строка, которая присоединяется к названию каждой таблицы Joomla в базе данных.
Реальный префикс - то конкретное сочетание символов, которое используется в названиях таблиц базы данных.
Связывание - процесс присвоения каждому полю производного от JTable класса значения элемента массива переменных запроса, так что ключ элемента совпадает с названием поля.
Символический префикс - сочетание "#__" (решетка и два знака подчеркивания), которое используется в запросах вместо реального префикса.

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

При работе с базой данных различают реальный и символический префиксы. Реальный префикс используется в названиях таблиц базы данных, а символический префикс ("#__") используется в запросах вместо реального префикса. При обработке запроса вместо символического префикса будет автоматически подставлен реальный.

Чтобы выполнить запрос к базе данных Joomla, необходимо осуществить пять операций: получение ссылки на объект JDatabase (абстрактный класс, предоставляющий доступ к соединению с базой данных), формирование запроса, задание запроса, выполнение запроса, загрузка результата.

Запрос может быть сформулирован в виде строки либо разбит на составляющие и построен с помощью методов класса JDatabaseQuery.

Запрос задается для последующего выполнения методом setQuery(), а выполняется либо методом query(), либо, если нам необходимо получить результат, одним из методов для получения форматированного результата: loadResult(), loadRow() и т.д.

Для каждой таблицы, использующейся расширением, необходимо создать класс, производный от JTable. Для каждого поля таблицы необходимо создать одноименное поле этого класса. Производный от JTable класс наследует в числе прочих методы bind(), store(), load() и delete(), позволяющие управлять записями таблицы без единой строки SQL-кода. Когда компонент получает массив переменных запроса, он осуществляет связывание, то есть присваивает каждому полю этого класса значение элемента массива, ключ которого совпадает с названием данного поля.

Существуют методы класса JTable для управления часто используемыми полями ordering, checked_out/checked_out_time, published и hits.

Вопросы

  1. Что такое реальный и символический префиксы?
  2. Какие операции необходимо осуществить для выполнения запроса к базе данных Joomla?
  3. Каким образом может быть сформулирован SQL-запрос?
  4. Какие методы задают и выполняют запрос?
  5. Для чего создается производный от JTable класс?
  6. В чем заключается связывание?
  7. Каким образом осуществляется управления часто используемыми полями?