Ручная обработка форм, работа с Filestorage, загрузка файлов

В этом примере мы рассмотрим вопрос ручного создания и обработки форм, работу с filestorage и загрузку файлов.

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

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

Шаги разработки:

1. Создание объекта

2. Создание интерфейса

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();
}