В OctoberCMS есть отличный виджет для загрузки файлов в формах. В данном уроке разберем, как его использовать, если вам нужно загрузить файл, разобрать его и сразу удалить, а красивый виджет использовать хочется, желательно без лишних таблиц в БД.
В качестве примера будем разбирать XML с курсами валют, который можно взять на сайте ЦБ.
Начало
Для начала создадим плагин, в котором будем работать. По старинке, попросим artisan подготовить всё за нас.
php artisan create:plugin sandbox.uploader
Вывод формы
Вся основная работа у нас будет в контроллере. Мы можем попросить artisan создать нам его, но в полученном таким образом контроллере будет много вещей, которые нам не понадобятся. Поэтому создадим его вручную (ну, или, через artisan, а потом почистим).
Итак, создаем наш контроллер RatesUploader
. Сразу добавим в него метод index
, чтобы отобразить страницу с кнопкой, по которой будет открываться popup с формой загрузки.
// /plugins/sandbox/uploader/controllers/RatesUploader.php
<?php
namespace Sandbox\Uploader\Controllers;
use Backend\Classes\Controller;
use Backend\Facades\BackendMenu;
class RatesUploader extends Controller
{
public function __construct()
{
parent::__construct();
BackendMenu::setContext('Sandbox.Uploader', 'uploader', '');
}
public function index()
{
}
}
В OctoberCMS адреса страниц админки строятся по схеме /backend/vendor/plugin/controller/method
. Если же метода нет, то по умолчанию вызывается index
. Если метод ничего не возвращает, то после его выполнения будет отображена страница, разметка которой лежит в файле с именем метода. Важно оставить метод в контроллере, даже если он пустой. Если же его убрать, то и открываться ничего не будет.
Далее создадим файл странцы, на которой разместим кнопку открытия popup.
<!-- /plugins/sandbox/uploader/controllers/ratesuploader/index.htm -->
<button class="btn btn-default oc-icon-upload"
data-control="popup"
data-handler="onOpenUploadPopup">
Загрузить файл
</button>
Теперь нам надо сделать так, чтобы по клику на эту кнопку открывался popup с формой, содержащей виджет загруки файла. Но для виджета формы нужна модель. Так что теперь создадим модель. И опять к помощи artisan мы прибегать не будем, так как, в таком случае, помимо класса модели мы получим yaml конфиги и миграцию, а нам это не нужно.
// /plugins/sandbox/uploader/models/RatesFormModel.php
<?php
namespace Sandbox\Uploader\Models;
use Model;
use October\Rain\Database\Relations\AttachOne;
use October\Rain\Database\Traits\Validation;
use System\Models\File;
/**
* @property File $rates_file
* @method AttachOne rates_file()
*/
class RatesFormModel extends Model
{
public $attachOne = [
'rates_file' => File::class,
];
use Validation;
public $rules = [
'rates_file' => 'required|file|mimes:xml'
];
public $customMessages = [
'rates_file.required' => 'Загрузите файл',
'rates_file.file' => 'Загрузите файл',
'rates_file.mimes' => 'Файл должен быть в формате XML',
];
}
Здесь мы создали модель для нашей формы. Как видите, в классе не прописано название таблицы БД, так как наша модель, на самом деле, никогда не будет сохраняться в БД. По хорошему, здесь нужно добавить переопределение родительских методов, которые предотвратят любую попытку сохранить экземпляры этой модели, но это уже материал для другой статьи.
Теперь нам нужно изменить контроллер. В первую очередь, нам нужно создать форму, которую мы отобразим в нашем popup. Для этого добавим следующий код в конструктор контроллера.
// /plugins/sandbox/uploader/controllers/RatesUploader.php
use Backend\Widgets\Form;
use Sandbox\Uploader\Models\RatesFormModel;
public function __construct()
{
// ...
if ($this->action === 'index') {
$this
->makeWidget(Form::class, [
'alias' => 'ratesUploadForm',
'model' => new RatesFormModel(),
'fields' => [
'rates_file' => [
'label' => 'Файл с курсами валют',
'type' => 'fileupload',
'fileTypes' => 'xml',
'mode' => 'file',
]
]
])
->bindToController();
}
}
Здесь мы создали виджет формы с нашей моделью RatesFormModel
и одним полем - rates_file
. Еще здесь необходимо вызвать метод bindToController()
, так как если этот метод не вызвать, то наша форма работать не будет. Также мы добавили здесь проверку if ($this->action === 'index')
. Это условие, которое проверяет, что открытая страница - index
, а на других страницах (если они в контроллере будут) нам этот виджет не нужен.
Далее в контроллер нужно добавить метод onOpenUploadPopup
и вернуть из него наш отрендеренный popup.
// /plugins/sandbox/uploader/controllers/RatesUploader.php
public function onOpenUploadPopup()
{
return $this->makePartial('upload_popup');
}
Ну и конечно, создать сам popup.
<!-- /plugins/sandbox/uploader/controllers/ratesuploader/_upload_popup.htm -->
<form>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title">Загрузите файл</h4>
</div>
<div class="modal-body">
<?= Form::sessionKey() ?>
<?= $this->widget->ratesUploadForm->render() ?>
</div>
<div class="modal-footer">
<div class="loading-indicator-container">
<button type="button"
class="btn btn-primary"
data-request="onImport"
data-request-validate=""
data-load-indicator="Импорт..."
data-request-success="$('.control-popup').popup('hide')">
Импортировать
</button>
<button type="button"
class="btn btn-default"
data-dismiss="modal">
Закрыть
</button>
</div>
</div>
</form>
Как видите, partial содержит сам popup, обернутый в тег form
в котором в качестве кнопки отправки использован вызов метода onImport
через встроенный AJAX фреймворк. А для вывода формы вызывается метод render()
виджета формы. Обратите внимание на ratesUploadForm
, это псевдоним виджета, который мы задали в параметре alias
когда создавали экземпляр виджета в конструкторе контроллера. Также нужно не забыть добавить ключ сессии, для этого используем Form::sessionKey()
, без него работать ничего не будет.
Пришло время посмотреть что из этого вышло. Открываем /backend/sandbox/uploader/ratesuploader
и смотрим.
Выглядит, как то, что нужно. Конечно, если сейчас выбрать файл и нажать на “Импортировать”, то мы получим сообщение об ошибке, в которой говорится, что метода onImport
в контроллере нет. А это значит, что пришло время написать и его.
Обработка формы
Добавим в наш контроллер метод onImport
с обработкой формы.
// /plugins/sandbox/uploader/controllers/RatesUploader.php
use Flash;
use October\Rain\Database\Models\DeferredBinding;
use System\Models\File;
public function onImport(): void
{
$sessionKey = post('_session_key');
$file = $this->getFile($sessionKey);
$this->importRates($file->getContents());
DeferredBinding::cancelDeferredActions(RatesFormModel::class, $sessionKey);
Flash::success('Курсы валют обновлены');
}
private function getFile(string $sessionKey): File
{
$formModel = new RatesFormModel();
$formModel->sessionKey = $sessionKey;
$formModel->validate();
return $formModel
->rates_file()
->withDeferred($sessionKey)
->orderByDesc('created_at')
->first();
}
private function importRates(string $rates)
{
// импортируем курсы валют из файла
// $rates = simplexml_load_string($rates);
// foreach ($rates as $rate) {
// ...
// }
}
В первую очередь мы получаем загруженный файл. Делам мы это в методе getFile()
.
В getFile()
создается экземпляр нашей модели, присваивается ключ сессии и вызывается валидация. Зачем тут это всё? Дело в том, что при сохранении модели валидация вызывается автоматически, если подключен трейт Validation
. В нашем же случае, поскольку мы модель не сохраняем, валидацию приходится вызывать вручную.
Ключ сессии же нужен чтобы нормально отработала отложенная привязка (DeferredBinding). Дело в том, что AttachOne использует DeferredBinding для связи с моделью до ее сохранения. И по этому без ключа не сработает ни валидация, ни получение файла в конце метода (обратите внимание на ->withDeferred($sessionKey)
).
После получения файла, вызываем importRates()
в котором импортируем курсы валют.
После успешного импорта подчищаем за собой с помощью вызова DeferredBinding::cancelDeferredActions(RatesFormModel::class, $sessionKey)
. Данный метод очистит отложенные привязки, а так же удалит не нужный более файл.
В конце радуем пользователя сообщением об успехе с помощью Flash::success()
.
Заключение
Конечно, для загрузки файла в админке сайта, можно использовать гораздо более простой вариант с обычной html-формой и <input type="file">
. Но наш вариант выглядит более красиво.
Код из статьи можно найти в репозитории.