Ручная обработка форм, работа с Filestorage, загрузка файлов
В этом примере мы рассмотрим вопрос ручного создания и обработки форм, работу с filestorage и загрузку файлов.
Генератор интерфейсов значительно упрощает разработку автоматическим созданием форм ввода информации, но иногда возникает необходимость создания собственной специфической формы.
Задача - дать возможность администраторам загружать свои файлы и некое описание.
Шаги разработки:
3. Доработка стандартного контроллера
Создание объекта
Зайдем в панель управления ORM и создадим объект userfiles (более подробно работа с ORM разбиралась в предыдущих уроках):
Таким образом, имеем объект userfiles c двумя полями:
- title - название файлов;
- file_links - ссылка на список объектов filestorage.
Создание интерфейса
Сгенерируем для этого объекта модуль административной панели (как это делать так же разбиралось ранее):
Откроем проект дизайнера этого интерфейса.
Удалим из проекта editWindow, оно нам не понадобится, добавим в проект компонент window назовем его addWindow.
Установим свойства:
- layout: ‘fit’;
- isExtended: true (расширенный компонент, который может иметь свои события и методы).
Сразу добавим в него пользовательское событие dataSaved (будем его инициировать, когда файл будет загружен).
Вкладка Events -> Add Event в панели свойств окна addWindow, содержание события оставим пустым.
Добавим в окно форму form и включим в нее:
- текстовое поле title;
- поля загрузки файлов file_a, file_b;
- тулбар (Toolbar->Panel) footerbar (в dockedItems), который включает:
- (toolbar->fill) fill;
- две кнопки saveBtn, cancelBtn.
Получим следующую структуру:
(Menu Items это новая опция, которая появляется в DVelum 0.9.4 и выше).
Выставим следующие свойства компонентам:
addWindow
- layout: ‘fit’ - тип layout (заполнение всего пространства);
- title: [js:] appLang.UPLOAD - токен локализации текста, можно написать просто «Загрузка».
Мышкой растягиваем окно до нужной ширины и высоты.
footerbar
- dock: bottom - перемещаем тулбар вниз;
- ui: footer - спец стиль ExtJs для оформления кнопок окон.
saveBtn
- width: 90 - ширина кнопки;
- text: [js:] appLang.SAVE - токен локализации текста, можно написать просто «Cохранить».
cancelBtn
- width: 90 - ширина кнопки;
- text: [js:] appLang.CANCEL - токен локализации текста, можно написать просто «Отмена».
form
- bodyPadding: 5 - делаем отступ для формы, чтобы поля небыли впритык к кромкам окна;
- bodyCls : ‘formBody’ - добавляем стиль формы (DVelum) и закрашиваем ее серым цветом, иначе она будет белой:
fieldDefailts:{
anchor:”100%”,
labelWidth: 100
}
Устанавливаем настройки полей по умолчанию:
- растягиваться на всю ширину формы;
- ширина лейбла 100.
title
- fieldLabel: [js:] appLang.TITLE - токен локализации текста, можно написать просто «Название»;
- name: title - имя поля для отправки формы;
- allowBlank: 0 – поле обязательно к заполнению.
file_a
- fieldLabel: [js:] appLang.FILE + " 1" - токен локализации текста, можно написать просто «Файл 1»;
- allowBlank: 0 – поле обязательно к заполнению;
- name: file[] - имя поля для отправки формы, [] означает что файлов будет несколько (массив).
file_b
- fieldLabel: [js:] appLang.FILE + " 2" - токен локализации текста, можно написать просто «Файл 2»;
- name: file[] - имя поля для отправки формы ([] означает, что файлов будет несколько (массив)).
В результате получаем окно следующего вида:
* Чтобы посмотреть окно необходимо нажать кнопку Show Window на панели свойств окна (панель свойств вызывается кликом по нужному компоненту в списке компонент проекта).
Приступим к описанию поведения:
cancelBtn
Во вкладке events открываем событие click и описываем поведение закрытия окна:
this.close();
Поскольку мы выставили окну свойство isExtended: 1, все события будут вызываться в контексте addWindow, соответственно мы можем обратиться к методу закрытия окна напрямую:
saveBtn
Во вкладке events открываем событие click и описываем поведение. Клик по кнопке будет вызывать метод sendData у окна addWindow (метод мы опишем дальше):
this.sendData();
Опишем метод sendData отправки формы, для этого создадим его во вкладке Methods компонента addWindow:
var me = this;
this.childObjects.form.getForm().submit({
clientValidation: true, // валидация на стороне клиента
waitMsg:appLang.SAVING, // сообщение во время сохранения
method:'post', // метод отправки данных
// внутренний метод dvelum, формирует URL, можно написать
// строку адрес контроллера? например ‘/adminarea/userfiles/upload’
url:app.createUrl([app.admin , 'userfiles' , 'upload']),
success: function(form , action) {
if(!action.result.success){
// при ошибке выводим сообщение
Ext.Msg.alert(appLang.MESSAGE, action.result.msg);
}else{
// при успехе инициируем событие dataSaved и закрываем окно
me.fireEvent('dataSaved');
me.close();
}
},
// внутренний обработчик ошибок (DVelum) подключения к серверу
// можно описать свой
failure: app.formFailure
});
Удаляем событие dataGrid itemdblclick, которое нам создал генератор, в этом примере оно нам не понадобится.
Переписываем функцию добавления записи в таблицу таким образом, чтобы она отображала addWindow и при успешном выполнении перезагружала таблицу:
function showUserfilesEditWindow(id){
Ext.create("appUserfilesClasses.addWindow",{
listeners:{
'dataSaved' :{
fn:function(){
appUserfilesRun.dataGrid.getStore().load();
}
}
}
}).show();
}
Теперь научим контроллер принимать данные, отправленные из интерфейса.
Доработка контроллера
Откроем system/app/Backend/Userfiles/Controller.php (файл был сгенерирован при создании модуля) и допишем метод uploadAction:
/**
* Принимаем данные из интерфейса
*/
function uploadAction()
{
// проверяем наличие прав на редактирование
$this->_checkCanEdit();
// получаем поле title из интерфейса, фильтруем его
$title = Request::post('title', FILTER::FILTER_STRING, '');
// массив ошибок
$errors = array();
// заголовок не может быть пустым
if(!strlen($title))
$errors['title'] = $this->_lang->get('CANT_BE_EMPTY');
// получаем настройки хранилища
$storageConfig = Config::factory(Config::File_Array, $this->_configMain->get('configs').'/filestorage.php');
// передаем в него идентификатор текущего пользователя
$storageConfig->set('user_id', $this->_user->id);
// инициализируем хранилище
$fileStorage = Filestorage::factory($storageConfig->get('adapter'), $storageConfig);
// загружаем файлы
$files = $fileStorage->upload();
if(empty($files)){
// ничего не загрузилось
$errors['file[]'] = 'Необходимо загрузить хотябы 1 файл';
}
// если были ошибки, останавливаем обработку
if(!empty($errors))
Response::jsonError($this->_lang->get('FILL_FORM') , $errors);
// пытаемся сохранить данные
$object = Db_Object::factory('Userfiles');
try{
// устанавливаем свойства объекта, неверные данные могут вызвать Exception,
// поля можно останавливать по одному $object->set(key , value), тогда будет понятно к какому полю
// относится ошибка, но в данном примере мы упрощаем код
$object->setValues(array(
'title' => $title,
'file_links'=>Utils::fetchCol('id', $files)
));
// если не удалось сохранить объект, бросаем исключение (будет отловлено блоком try)
if(!$object->save())
throw new Exception($this->_lang->get('CANT_EXEC'));
// сообщаем, что сохранение прошло успешно
Response::jsonSuccess();
}catch(Exception $e){
// сообщаем об ошибке
Response::jsonError($e->getMessage());
}
}
Перейдем в интерфейс, файлы удачно загружаются:
Зайдем в ORM и посмотрим содержимое объектов, убедимся, что ссылки на файлы созданы:
Теперь отобразим ссылки на файлы в интерфейсе. В данном случае упростим задачу: дополним поле «Заголовок» ссылками без создания дополнительных полей в таблице.
Для этого переработаем метод listAction контроллера:
/**
* Get list of items. Returns JSON reply with
* ORM object field data;
* Filtering, pagination and search are available
* Sends JSON reply in the result
* and closes the application.
*/
public function listAction()
{
$pager = Request::post('pager' , 'array' , array());
$filter = Request::post('filter' , 'array' , array());
$query = Request::post('search' , 'string' , false);
$filter = array_merge($filter , Request::extFilters());
$dataModel = Model::factory($this->_objectName);
// добавим в выборку поле file_links
$fields = $this->_listFields;
$fields[] = 'file_links';
$data = $dataModel->getListVc($pager , $filter , $query , $fields);
if(empty($data))
Response::jsonSuccess(array() , array('count' => 0 ));
// получаем идентификаторы извлеченных записей
$recordIds = Utils::fetchCol('id', $data);
// получим URL адрес текущего модуля
$url = $this->_router->findUrl($this->getModule());
// Получаем список ссылок хранящихся в объектах и добавим их к заголовку в виде тега
// правильнее ссылку рендерить в интерфейсе, мы упрощаем задачу в рамках текущего примера
// Есть несколько вариантов получения списка ссылок:
// a) Извлечение из таблицы линковки при помощи объекта Links (самый гибкий, но сложный метод)
// a) десериализация свойства базового объекта
// б) инициализация объекта Db_Object и обращение к его свойству
// В данном примере мы разберем самый простой вариант (б)
foreach($data as $k=>&$v)
{
if(!empty($v['file_links']))
{
$links = unserialize($v['file_links']);
if(!empty($links) && is_array($links))
{
foreach($links as $index => $linkData)
{
//сгенерируем ссылку на загрузку файла downloadAction текущего
//модуля с параметром id файла в url
$fileUrl = $url . Request::url(array('download' , $linkData['id']));
$v['title'] .= '<br><a href="'.$fileUrl.'">' . $linkData['title'] . '</a>';
}
}
}
}unset($v);
Response::jsonSuccess($data , array('count' => $dataModel->getCount($filter , $query)));
}
Ссылки появились, осталось описать метод отдачи файла.
Правильнее было бы отдавать статику через NGINX при помощи заголовка X-Accel-Redirect или аналогов на других веб серверах. С учетом того, что нужные модули могут быть установлены не у всех, отдадим при помощи PHP:
/**
* Скачать файл
*/
public function downloadAction()
{
// Получаем идентификатор файла из URI
$id = Request::getInstance()->getPart(3);
$id = intval($id);
try{
$file = Db_Object::factory('Filestorage' , $id);
}catch(Exception $e){
//запрошен несуществующий файл
Response::redirect($this->_router->findUrl($this->getModule()));
}
// тут можно проверить права на файл
// ....
// получаем настройки хранилища
$storageConfig = Config::factory(Config::File_Array, $this->_configMain->get('configs') . '/filestorage.php');
// определяем путь к файлу
$path = $storageConfig->get('filepath') . $file->get('path');
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $file->get('name'));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
readfile($path);
exit();
}