Загрузка файлов в yii2

Загрузка файлов в yii2 осуществляется через класс yii\web\UploadedFile. Для безопасной загрузки файла необходимо использовать стандартный функционал ActiveForm и моделей с заготовленными правилами валидации.

Стандартная реализация

На официальном сайте Yii2 приводится статья, следуя которой можно реализовать стандартный механизм загрузки файла на сайт. Следует заметить, что для загрузки файла создается собственная модель с правилами валидации, в качестве ответа $model->upload() должен возвратить не только true или false, а, например, название файла.

Преимущества:

  • Нет сложных действий, все линейно

Недостатки:

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

Использование actions и виртуальной переменной

В данном примере понадобятся расширения yii2-file-kit и yii2-widget-fileinput.

Создадим виртуальную переменную $mainPicture в модели:

<?php

namespace app\models;

use Yii;

/**
 * This is the model class for table "articles".
 *
 * @property integer $id
 * @property string $title
 * @property string $picture
 */

class Articles extends \yii\db\ActiveRecord
{
    public $mainPicture;

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'articles';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['title', 'mainPicture'], 'required'],
            [['title', 'picture', 'mainPicture'], 'string', 'max' => 255],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'title' => 'Название',
            'picture' => 'Изображение'
        ];
    }
}

В web.config регистрируем компонент

'articlesStorage' => [
    'class' => 'trntv\filekit\Storage',
    'baseUrl' => '@web/files/articles',
    'filesystem'=> function() {
        $adapter = new \League\Flysystem\Adapter\Local('files/articles');
        return new League\Flysystem\Filesystem($adapter);
    }
],

В контроллере, который занимается созданием экземпляра articles (в бекенде) указываем actions:

public function actions()
{
    return [
        'upload'=>[
            'class'=>'trntv\filekit\actions\UploadAction',
            'fileStorage' => 'articlesStorage'
        ]
    ];
}

Функция create примет вид:

public function actionCreate()
{
    $model = new Articles();
    if ($model->load(Yii::$app->request->post())) {
         $model->attributes = [
                'picture' => $model->mainImage
         ];
         $model->save();
         return $this->redirect(['view', 'id' => $model->id]);
    } else {
         return $this->render('create', [
             'model' => $model,
         ]);
    }
}

Поле загрузки файла в форме создания статьи _form:

<?= $form->field($model, 'picture')->widget(\kartik\file\FileInput::classname(), [
            'options' => [
                'accept' => 'image/*',
                'multiple'=>false,
                'name' => 'file'
            ],
            'pluginOptions' =>  [
                'uploadUrl' => 'upload',
                'uploadAsync' => true,
                'initialPreviewAsData'=>true,
                'overwriteInitial'=>true,
                'showRemove' => false,
                'removeClass' => 'btn btn-danger',
                'removeIcon' => '<i class="glyphicon glyphicon-trash"></i> ',
                'showUpload' => false,
            ],
            'pluginEvents' => [
                'fileuploaded' => "function(e, params) {
                    var files_array = $.parseJSON(params.jqXHR.responseText);
                    $('#articles-mainpicture').val(files_array['files'][0]['path']);
                }",
                'filebatchselected' => "function(e, params) {
                    $(this).fileinput('upload');
                }",
            ]
        ]); ?>

<?= $form->field($model, 'mainPicture')->hiddenInput()->label(false) ?>

В данном случае, сохранение названия файла проходит через виртуальную переменную mainPicture в контроллере.

Обратите внимание на то, что используются события виджета FileInput.

Использование единого хранилища.

Данный способ отличается от предыдущего тем, что в таблице articles будем хранить не название файла, а его id. При такой реализации, в одной таблице базы данных будем хранить все загружаемые файлы, следовательно, для загрузки файлов можно будет использовать один и тот же action. Этот экшн при сохранении будет возвращать только id загруженного файла.

Добавим миграцию и создадим для нее модель через gii

<?php

use yii\db\Migration;

/**
 * Handles the creation of table `images`.
 */
class m170911_101145_create_images_table extends Migration
{
    /**
     * @inheritdoc
     */
    public function up()
    {
        $this->createTable('images', [
            'id' => $this->primaryKey(),
            'base_url' => $this->string(),
            'delete_url' => $this->string(),
            'name' => $this->string(),
            'path' => $this->string(),
            'size' => $this->string(),
            'type' => $this->string(),
            'url' => $this->string()
        ]);
    }

    /**
     * @inheritdoc
     */
    public function down()
    {
        $this->dropTable('images');
    }
}

Изменим action, для этого вынесем его в отдельный файл (необходимо скопировать экшн trntv\filekit\actions\UploadAction из папки vendor и внести правки в функцию run):

<?php
    ...
    public function run()
    {
        ...
            $output[$this->responsePathParam] = $path;
            $output[$this->responseUrlParam] = $this->getFileStorage()->baseUrl . '/' . $path;
            $output[$this->responseDeleteUrlParam] = Url::to([$this->deleteRoute, 'path' => $path]);

            $image = new Images();
            $image->attributes = [
                 'path' => $path,
                 'size' => $uploadedFile->size,
                 'type' => $uploadedFile->type,
                 'url' => $output[$this->responseUrlParam],
                 'base_url' => $this->getFileStorage()->baseUrl
            ];
            $image->save();

            $output['image_id'] = $image->id;

            $paths = \Yii::$app->session->get($this->sessionKey, []);

            ...

        $result['files'][] = $output;

        return $this->multiple ? $result : array_shift($result);
    }

    ...

}

Теперь в ответе $output будет содержаться id загруженного файла.

В контроллере указываем action:

public function actions(){
    return [
        'file_upload'=>[
             'class'=>'app\backend\actions\FileAction'
        ]
    ];
}

В файле \_form.php изменим события:

<?= $form->field($model, 'picture')->widget(\kartik\file\FileInput::classname(), [
            'options' => [
                'accept' => 'image/*',
                'multiple'=>false,
                'name' => 'file'
            ],
            'pluginOptions' =>  [
                'uploadUrl' => 'upload',
                'uploadAsync' => true,
                'initialPreviewAsData'=>true,
                'overwriteInitial'=>true,
                'showRemove' => false,
                'removeClass' => 'btn btn-danger',
                'removeIcon' => '<i class="glyphicon glyphicon-trash"></i> ',
                'showUpload' => false,
            ],
            'pluginEvents' => [
                'fileuploaded' => "function(e, params) {
                    var files_array = $.parseJSON(params.jqXHR.responseText);
                    $('#articles-mainpicture').val(files_array['files'][0]['image_id']);
                }",
                'filebatchselected' => "function(e, params) {
                    $(this).fileinput('upload');
                }",
            ]
        ]); ?>

<?= $form->field($model, 'mainPicture')->hiddenInput()->label(false) ?>

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

{{ message }}

{{ 'Comments are closed.' | trans }}