In-Portal Developers Guide

This is a wiki-based Developers Guide for In-Portal Open Source CMS. The purpose of this guide is to provide advanced users, web developers and programmers with documentation on how to expand, customize and improve the functionality and the code the In-Portal software. Please consider contributing to our documentation writing effort.

K4:Написание импорт скриптов

From In-Portal Developers Guide

Jump to: navigation, search
Дополнения Дополнения
Статьи в этой категории

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

Бывает, что специфический импорт данных надо сделать только один раз. Например, когда в новый проект предстоит "залить" накопившиеся у заказчика данные. В таком случае не стоит тратить время на разработку интерфейса.

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

Contents

Стандартный интерфейс на базе трёх шаблонов

Как правило, достаточно трёх шаблонов для построения полноценного интерфейса. Это:

Шаблон входных параметров

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

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

  • Элемент для указания источника данных, например
    • Элемент input для закачки файла с данными
    • Элемент select для выбора файла с данными из определённого каталога
  • Элементы, описывающие структуру данных, например
    • Список импортируемых полей
    • Символы-разделители полей
    • Символы-разделители строк
  • Предупреждение о том, как импорт может повлиять на систему и элемент, позволяющий пользователю подтвердить что он ознакомился с этим предупреждением

Как правило, на шаблоне входных параметров достаточно двух кнопок

  • Начать импорт - эта кнопка вызывает событие типа OnImportStart
  • Отказаться от импорта - эта кнопка может возвращать пользователя на шаблон, с которого он попал на Шаблон входных параметров, либо, если на Шаблон входных параметров попадают из главного меню - на родительскую по отношению к вышеупомянутому Шаблону секцию главного меню.

Обычно шаблон входных параметров делается также как и форма редактирования тех данных, которые будут импортироваться (т.е. с тем же префиксом). Одним из отличий является то, что будет использоваться special, напр. import:

<inp2:m_RenderElement name="inp_edit_box" prefix="prefix.import" field="SampleImportParameter" title="la_fld_SampleImportParameter"/>

В данном случае все входные параметры будут виртуальными необязательными полями объекта. Также нужно переписать метод обработчика событий getPassedID таким образом, чтобы для special "import" не делалась автозагрузка записи:

function getPassedID(&$event) {
	if ($event->Special == 'import') {
		return 0;
	}
 
	return parent::getPassedID($event);
}

Шаблон прогресса

Сюда пользователь может попасть, если корректно задаст параметры импорта на Шаблоне входных параметров.

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

Вот типичный код, обеспечивающий помещение компонента AjaxProgressBar в шаблон.

<inp2:m_RenderElement name="ajax_progress_bar" cancel_action="cancel_action();"/>
<script type="text/javascript">
	function cancel_action() {
		submit_event('import', 'OnImportCancel');
	}
 
	$Import = new AjaxProgressBar('<inp2:m_t t="dummy" import_event="OnImportProgress" pass="m,import" finish_template="custom/import/finish" js_escape="1"/>');
</script>

Тэг RenderElement обеспечивает прорисовку элемента AjaxProgressBar. На случай, если импорт затянулся, и пользователь больше не желает ждать пока импорт закончится, в AjaxProgressBar есть кнопка Cancel. Параметр тэга cancel_action назначает JavaScript-команду, выполняемую по нажатию этой кнопки. Как правило, достаточно вызвать событие OnImportCancel для соответствующего префикса. В примере указан префикс 'import', но префикс может быть и другим, в частности, можно использовать префикс таблицы в которую делается импорт.

Также, в примере создаётся JavaScript-объект класса AjaxProgressBar. При этом задаётся параметр - URL, на который будут последовательно производиться Ajax-запросы пока импорт не завершится. Этот URL обеспечивает что при Ajax-запросе для выбранного префикса будет вызвано событие OnImportProgress.

Когда импорт завершается, происходит перенаправление на шаблон окончания импорта.

Шаблон окончания импорта

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

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

Ещё на этом шаблоне можно поместить кнопку, возвращающую пользователя на Шаблон входных параметров

Набор событий для скриптов со стандартным интерфейсом

Как правило, достаточно трёх событий для обеспечения функциональности импорта. Это:

OnImportStart

В этом событии может происходить проверка введённых пользователем параметров. Если введены недопустимые значения, пользователь остаётся на шаблоне параметров, и видит сообщения об ошибке.

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

$import_params = Array (
	'file_pos' => 0,
	'file_name' => $csv_file,
	'file_size' => filesize($csv_file),
);
 
$this->Application->StoreVar('import_status', serialize($import_params));

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

OnImportProgress

В этом событии считывается из сессии информация о стадии процесса и параметры импорта.

$import_status = $this->Application->RecallVar('import_status');

Получаем из источника данных очередную порцию информации и добавляем эти данные в базу данных проекта.

Если источник данных - это CSV-файл, то получение данных выглядит примерно так:

$user =& $this->Application->recallObject('u.import', null, Array ('skip_autoload' => true)); // создаём объект для импорта
/* @var $user kDBItem */
 
$file_pointer = fopen($import_status['file_name'], 'r'); // второй параметр означает что вайл открывается для чтения
fseek($file_pointer, $import_status['file_pos']);
$i = 0;
while (($i < RECORDS_PER_PAGE) && !feof($file_pointer)) {
	$data = fgetcsv($file_pointer, 10000); // считываем одну строку CSV-файла в массив
	$user->Clear(); // выставляем всем полям значения по умолчанию и таким образом гарантируюем, что в объекте будут только его собственные данные.
	$user->SetDBField('FirstName', $data[0]);
	$user->SetDBField('LastName', $data[1]);
	$user->SetDBField('Login', $data[2]);
	$user->SetDBField('Street', $data[3]);
	$user->SetDBField('Password', md5($data[4]));
	// также можно воспользоваться методом "SetDBFieldsFromHash" для массовой записи данных в объект
 
	$status = $user->Create();
 
	if (!$status) {
		// обработка ошибки о том, что запись не создалась
		break; // если нужно, то можно прекратить весь импорт при возникновении первой ошибки
	}
	$i++;
}
$import_status['file_pos'] = ftell($file_pointer);
fclose($file_pointer);

Вставлять записи в базу данных можно и просто SQL-запросами. Однако, желательно это делать, используя соответствующие объекты, поскольку объект обеспечит автоматически такие полезные функции как проверка данных, установление необходимых связей с другими таблицами базы данных.

Далее, если ещё есть данные для импорта, то обновляется в сессии информация о стадии импорта, подсчитывается и возвращается Ajax-скрипту процентное выражение стадии импорта.

if ($import_status['file_pos'] < $import_status['file_size']) {
	$this->Application->StoreVar('import_status', serialize($import_status));
	echo ($import_status['file_pos'] / $import_status['file_size']) * 100;
	$event->status = erSTOP; // Этот статус для того чтобы скрипт не пытался обрабатывать шаблон
	return;
}
else {
	$this->Application->RemoveVar('import_status');
	$this->Application->Redirect( $this->Application->GetVar('finish_template') );
}

Если данных больше нет - делается перенаправление на Шаблон окончания импорта.

В процессе импорта могут возникать ошибки. Если это происходит, то информацию об ошибках желательно предоставить пользователю. Тут есть такие варианты.

if (!$user->Create()) {
	$this->Application->StoreVar('error_info', serialize($user->FieldErrors));
	$this->Application->StoreVar('error_fields', serialize($user->FieldValues));
}
  • Можно не прерывать импорт, а информацию о всех произошедших ошибках записывать в лог-файл. Тогда на шаблоне окончания импорта можно показать ссылку на этот лог-файл.
$log_file_pointer = fopen(FULL_PATH . '/user_import.log', 'a'); // второй параметр означает что файл открывается для добавления информации
$status = $user->Create();
fwrite($log_file_pointer, 'User [' . $user->GetDBField('Login') . '] ' . ($status ? 'was' : 'was not') . ' created');
fclose($log_file_pointer);

OnImportCancel

В этом событии часто бывает достаточно обеспечить перенаправление на Шаблон входных параметров.

$event->redirect = 'import/cancel';

Также, если необходимо, здесь можно запрограммировать возвращение системы в состояние, которое было до импорта - стереть созданные записи, очистить переменные в сессии и.т.п..

Отладка скрипта

Отладку скрипта, как обычно, удобно производить с помощью расширения браузера Zend Studio Toolbar и встроенного отладчика Zend. Для отладки Ajax-запросов в Zend Studio Toolbar можно выбрать режим "Debug all pages on this site" - тогда вначале через отладчик будет пропущен скрипт Шаблона прогресса, а затем - Ajax-запросы, вызываемые с этого шаблона.

Если нет возможности использовать отладчик Zend, как это может быть в случае ошибок, воспроизводимых только на LIVE-сайте, можно воспользоваться браузером FireFox с расширением FireBug. В консоли FireBug можно видеть параметры и результаты Ajax-запросов, а если этого недостаточно - можно из консоли FireBug скопировать URL Ajax-запроса, открыть его в отдельном окне браузера и отлаживать там.

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

Бывает полезно сравнить количество записей в источнике данных и количество импортированных записей. Эти значения, как правило, должны совпадать. Если они не совпадают, надо выявить причину расхождений - это может быть ошибка.

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

При работе с объектами желательно использовать метод Clear() перед формированием каждого нового набора данных - иначе значения полей, сообщения об ошибках и другие элементы данных могут перенестись из предшестующего в цикле набора данных. Например, после ошибки создания записи по причине неправильных данных, все последующие попытки создать запись будут тоже неудачными - если не очищен массив ошибок при переходе к следующему набору данных.

При работе с объектами необходимо учитывать особенности форматирования и проверки данных в объекте. Часто бывает недостаточно поместить данные в поле объекта, соответствующее по названию полю в таблице. Например, для корректного сохранения поля типа "дата", надо одно и то же значение даты-времени вписать в два виртуальных поля.

// не так
$user->SetDBField('dob', $data[5]);
// а так
$user->SetDBField('dob_date', $data[5]);
$user->SetDBField('dob_time', $data[5]);