Yii2: Загрузка изображений и удаление через AJAX

Давайте рассмотрим как в Yii2 прикреплять изображения к статье, обновлять их и удалять через AJAX. Я предполагаю что у вас уже сгенерированн CRUD для ваших задач.

Для начала создадим новую ячейку в БД в моём случае это бует таблица posts и ячейку я назову "logo". Тип Varchar длина 512, allow null.

Модель

В модели необходимо создать публичную переменную в которую мы будем передавать загружаемое изображение. Добвааляем её в класс модели models/Posts.php:

class Posts extends \yii\db\ActiveRecord // Ваш класс модели
{

    public $file; // Добавляем переменную

Далее в правилах валидации добавим правило для нашего файла: [['file'], 'file'], В моём случае набор правил будет выглядеть вот так:

public function rules()
{
    return [
        [['title', 'slug', 'content'], 'required'],
        [['content'], 'string'],
        [['title', 'slug'], 'string', 'max' => 255],
        [['logo', 'img'], 'string', 'max' => 512],
        [['slug'], 'unique'],
        [['file'], 'file'], // Добавленное правило валидации 
    ];
}

Представление

Теперь давайте перейдём в наше представление views/posts/_form.php и в самом верху подключим хелперы:

use yii\helpers\Html;
use yii\helpers\Url;

Будьте внимательны, если вы создавали ваш CRUD не в ручную а например при помощи Gii то скорей всего хелпер yii\helpers\Html; у вас уже подключен и подключать его повторно не следует.

В параметрах формы необходимо указать что мы будем передавать файлы:

<?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]); ?>

Теперь давайте создадим поле для загрузки изображения. Обратите внимание поле будет передавать данные в ранее созданую нами публичную переменную $file в модели Posts что бы затем контроллер мог сохранить отдельно файл в папку на сервере и записать имя файла в объект, а затем сохранить его в БД.

<?= $form->field($model, 'file')->fileInput() ?>

Далее прямо над полем для загрузки размещаем небольшой виджет который будет отображать загруженное изображение при просмотре статьи:

<?php if(!empty($model->logo)){
    echo Html::img($model->logo, $options = ['class' => 'postImg', 'style' => ['width' => '180px']]);
} ?>

Давайте разберёмся что тут у нас происходит? Данный виджет будет виден только при редактировании уже ранее созданой статьи и только в том случае, если к статье уже было ранее добавленно изображение: if(!empty($model->logo)). Далее мы вызываем html хелпер Html::img который будет выдавать код типа <img src> и с качестве первого параметра принимает путь к файлу: Html::img($model->logo как раз наше изображение. Второй параметр у нас отвечает за размер изображения.

Контроллер

Для начала давайте подключим недостающий хелпер который занимается загрузкой файлов в Yii2:

use yii\web\UploadedFile;

Теперь необходимо в двух экшенах контроллера (созжание и обновление) необходимо добавить обработку передаваемого файла изображения. Для этого в экшенах public function actionCreate и public function actionUpdate сразу после строчки: if ($model->load(Yii::$app->request->post()) && $model->save()) { добавляем вот такой код:

$imageName = time();
$model->file = UploadedFile::getInstance($model, 'file');
if(!empty($model->file))
{
    $model->file->saveAs('uploads/blog_'.$imageName.'.'.$model->file->extension);
    $model->logo = 'uploads/blog_'.$imageName.'.'.$model->file->extension;
    $model->save();
}

Давайте разберём его. В данном методе $imageName = time(); я получаю текущий тайм штам типа 1486298642 я буду использовать его как часть имени файла, что бы избежать конфликта одинаковых имён на сервере. Далее мы передаём в наш объект модели наш файл полученный из формы: $model->file = UploadedFile::getInstance($model, 'file'); через метод хелпер UploadedFile. Далее задаём условие, если файл загружен if(!empty($model->file)) сохраняем его в папку uploads в публичной директории сервера: $model->file->saveAs('uploads/blog_'.$imageName.'.'.$model->file->extension); затем к ранее созданному нами штампу добавляем префикс blog_ в результате загруженные файлы будут иметь имена вида blog_486298642 и затем добавляем точку и расширение оригинального файла '.'.$model->file->extension.

Теперь обратите внимание мы наконец отходим от работы с самим файлом который ранее хранился в переменной $file и передаём его имя и путь в объект модели в виде строки: $model->logo = 'uploads/blog_'.$imageName.'.'.$model->file->extension; Собственно осталось только сохранить данное состояние объекта $model->save(); что бы данные записались в БД. Это акупльно как в случае создания, так и обновления материала. Давайте ещё раз вернёмся на три строки выше к условию if(!empty($model->file)) мы его создали на тот случай, когда при обновлении материала мы не меняем текущее изображение или его вовсе нет. Без данного условия мы-бы получили ошибку о попытке передать пустой объект.

Удаление изображения через AJAX

Теперь давайте реализуем возможность удаления прикреплённого изображения без перезагрузки страницы, что весма полезно при обновлении материалов. Для начала, здесь же в контроллере создадим новый метод, который мы в дальнейшем и будем вызывать через AJAX запрос. Привожу полный код экшена:

public function actionDeleteimage($id)
{
    $model = $this->findModel($id);
    $imgName = $model->logo;
    unlink(Yii::getAlias('').$imgName);
    $model->logo = null;
    $model->update();
    if (Yii::$app->request->isAjax)
    {
        return 'Deleted';
    } else {
        return $this->redirect(['update', 'id' => $model->id]);
    }
}

Как видно из кода в качестве агрумента мы передаём ID объекта, далее создаём переменные с индификатором $model = $this->findModel($id); и путь до файла $imgName = $model->logo; Которые получаем из экземпляра объекта. Затем вызываем стандартный метод php - unlink() с параметром пути, который удаляет файл на сервере. Далее присваиваем экземпляру объекта значение null в для изображения и обновляем экземпляр модели $model->update(); в случае ели всё прошло успешно возвращяем строку Deleted которая будет отображена в форме.

Терерь давайте вернёмся в представление и в нашей форме внутри виджета  <?php if(!empty($model->logo)){ сразу после хелпера с изображением, что мы ранее создали вставляем AJAX ссылку которая будут обращатся к нашему экшену удаления в контроллере, который мы только что создали. Итак довайте добавим наконец наш код:

echo Html::a('<span class="glyphicon glyphicon-trash"></span>', ['posts/deleteimage', 'id' => $model->id], [
    'onclick'=>
         "$.ajax({
             type:'POST',
             cache: false,
             url: '".Url::to(['posts/deleteimage', 'id' => $model->id])."',
             success  : function(response) {
                 $('.link-del').html(response);
                 $('.postImg').remove();
             }
          });
     return false;
     $(".postImg").remove(); // Удалить превью картинки
     ",
     'class' => 'link-del'
]);

Как видно из кода мы снова прибегли к помощи html хелпера для создания ссылки с AJAX запросом. Далее мы вызываем роутер которвый обращается к экшену удаления ['posts/deleteimage' и передаём переменную с id нашего объекта 'id' => $model->id]. Теперь после клика по ссылке отработает наш экшен на удаление и вернёт там ответ. В случае успеха сработает вот эта интересная часть AJAX:

success : function(response) {
    $('.link-del').html(response);
    $('.postImg').remove();
}

Первая строка получит return из экшена где мы написали Deleted и отобразит его на экране, а вторая строка уберёт элемент картинки из DOM страницы.

Вот собственно и всё, теперь вы можете не засорять ваш сервер уже несуществующими изображениями.

P.S.

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