Advanced upload using Yii2 FileInput widget

The web tip on uploading file in Yii 2 using FileInput widget has seen a lot of web hits. As a result, this in turn has generated more feedback and queries by many users (both offline and online). As a request by many folks, I am creating a follow-up web tip to the earlier web tip. In the former web tip, you learnt how to create a new record and upload a file using the FileInput widget by Krajee. This web tip expands the scenario for advanced usage and include the following additional functionalities:

  • optimize and enhance the model
  • update the record and replace the saved image.
  • delete the saved image

The following parameters are useful to setup for global access:

  • Yii::$app->params['uploadPath'] as suggested in the earlier web tip. This will store the server path for all your file uploads. You can copy and store a default placeholder image in this folder, to display an image, whenever no file is uploaded e.g. default_user.jpg.
  • Yii::$app->params['uploadUrl']. This will store the Url pointing to server location for your file uploads. Note your web server must be configured to allow the uploadPath access.

For example, your configuration can be:

Yii::$app->params['uploadPath'] = Yii::$app->basePath . '/uploads/';
Yii::$app->params['uploadUrl'] = Yii::$app->urlManager->baseUrl . '/uploads/';

About FileInput

The FileInput widget is based on the bootstrap-fileinput plugin by Krajee. Here are the advanced steps for you to configure the widget for upload:

Model

It is recommended to optimize code for your model and controller. Instead of overloading the controller, we will now enhance our model code from the previous web tip. We will now include these new additional methods:

  • getImageFile: Will return the uploaded/stored image file name with complete path
  • getImageUrl: Will return the uploaded/stored image url accessible via browser front end.
  • uploadImage: Will process upload of the image and return a yii\web\UploadedFile instance. If no image was uploaded this method will return false.
  • deleteImage: Will process deletion of already existing image in database and return true if image file was found – else return false.

Accordingly set any additional model scenarios or validation rules for your attributes as needed.

namespace common\models;

use yii\db\ActiveRecord;

/**
* Class Person
* @package common\models
* @property int $id unique person identifier
* @property string $name person / user name
* @property array $avatar generated filename on server
* @property string $filename source filename from client
*/
class Person extends ActiveRecord
{
    /**
    * @var mixed image the attribute for rendering the file input
    * widget for upload on the form
    */
    use yii\web\UploadedFile;
    public $image;

    public function rules()
    {
        return [
            [['name', 'avatar', 'filename', 'image'], 'safe'],
            [['image'], 'file', 'extensions'=>'jpg, gif, png'],
        ];
    }

    /**
     * fetch stored image file name with complete path 
     * @return string
     */
    public function getImageFile() 
    {
        return isset($this->avatar) ? Yii::$app->params['uploadPath'] . $this->avatar : null;
    }

    /**
     * fetch stored image url
     * @return string
     */
    public function getImageUrl() 
    {
        // return a default image placeholder if your source avatar is not found
        $avatar = isset($this->avatar) ? $this->avatar : 'default_user.jpg';
        return Yii::$app->params['uploadUrl'] . $avatar;
    }

    /**
    * Process upload of image
    *
    * @return mixed the uploaded image instance
    */
    public function uploadImage() {
        // get the uploaded file instance. for multiple file uploads
        // the following data will return an array (you may need to use
        // getInstances method)
        $image = UploadedFile::getInstance($this, 'image');

        // if no image was uploaded abort the upload
        if (empty($image)) {
            return false;
        }

        // store the source file name
        $this->filename = $image->name;
        $ext = end((explode(".", $image->name)));

        // generate a unique file name
        $this->avatar = Yii::$app->security->generateRandomString().".{$ext}";

        // the uploaded image instance
        return $image;
    }

    /**
    * Process deletion of image
    *
    * @return boolean the status of deletion
    */
    public function deleteImage() {
        $file = $this->getImageFile();

        // check if file exists on server
        if (empty($file) || !file_exists($file)) {
            return false;
        }

        // check if uploaded file can be deleted on server
        if (!unlink($file)) {
            return false;
        }

        // if deletion successful, reset your file attributes
        $this->avatar = null;
        $this->filename = null;

        return true;
    }
}

Controller

Great. With the model now ready, your controller code can be leaner. Setup your controller action for create, update, and delete.

use Yii;
use common\models\Person;
use yii\web\UploadedFile;

class PersonController extends \yii\web\Controller
{
    /**
     * Creates a new Person model.
     * If creation is successful, the browser will be redirected to the 'view' page.
     * @return mixed
     */
    public function actionCreate()
    {
        $model = new Person;

        if ($model->load(Yii::$app->request->post())) {
            // process uploaded image file instance
            $image = $model->uploadImage();

            if ($model->save()) {
                // upload only if valid uploaded file instance found
                if ($image !== false) {
                    $path = $model->getImageFile();
                    $image->saveAs($path);
                }
                return $this->redirect(['view', 'id'=>$model->id]);
            } else {
                // error in saving model
            }
        }
        return $this->render('create', [
            'model'=>$model,
        ]);
    }

    /**
     * Updates an existing Person model.
     * If update is successful, the browser will be redirected to the 'view' page.
     * @param integer $id
     * @return mixed
     */
    public function actionUpdate($id)
    {
        $model = $this->findModel($id);
        $oldFile = $model->getImageFile();
        $oldAvatar = $model->avatar;
        $oldFileName = $model->filename;

        if ($model->load(Yii::$app->request->post())) {
            // process uploaded image file instance
            $image = $model->uploadImage();

            // revert back if no valid file instance uploaded
            if ($image === false) {
                $model->avatar = $oldAvatar;
                $model->filename = $oldFileName;
            }

            if ($model->save()) {
                // upload only if valid uploaded file instance found
                if ($image !== false && unlink($oldFile)) { // delete old and overwrite
                    $path = $model->getImageFile();
                    $image->saveAs($path);
                }
                return $this->redirect(['view', 'id'=>$model->_id]);
            } else {
                // error in saving model
            }
        }
        return $this->render('update', [
            'model'=>$model,
        ]);
    }    

    /**
     * Deletes an existing Person model.
     * If deletion is successful, the browser will be redirected to the 'index' page.
     * @param integer $id
     * @return mixed
     */
    public function actionDelete($id)
    {
        $model = $this->findModel($id);

        // validate deletion and on failure process any exception 
        // e.g. display an error message 
        if ($model->delete()) {
            if (!$model->deleteImage()) {
                Yii::$app->session->setFlash('error', 'Error deleting image');
            }
        }
        return $this->redirect(['index']);
    }

    /**
     * Finds the Person model based on its primary key value.
     * If the model is not found, a 404 HTTP exception will be thrown.
     * @param integer $id
     * @return Person the loaded model
     * @throws NotFoundHttpException if the model cannot be found
     */
    protected function findModel($id)
    {
        if (($model = Person::findOne($id)) !== null) {
            return $model;
        } else {
            throw new NotFoundHttpException('The requested page does not exist.');
        }
    }  
}

View

Your view pretty much remains the same as earlier except that you now add functionality for preview, update, and delete. You may also wish to add any error block to display any error messages.

Edit your _form.php or your other view file where you are rendering the create and image upload form.

use kartik\helpers\Html; // or yii\helpers\Html
use kartik\widgets\ActiveForm; // or yii\widgets\ActiveForm
use kartik\widgets\FileInput;

$form = ActiveForm::begin([
    'options'=>['enctype'=>'multipart/form-data'] // important
]);
echo $form->field($model, 'filename');

// display the image uploaded or show a placeholder
// you can also use this code below in your `view.php` file
$title = isset($model->filename) && !empty($model->filename) ? $model->filename : 'Avatar';
echo Html::img($model->getImageUrl(), [
    'class'=>'img-thumbnail', 
    'alt'=>$title, 
    'title'=>$title
]);

// your fileinput widget for single file upload
echo $form->field($model, 'image')->widget(FileInput::classname(), [
    'options'=>['accept'=>'image/*'],
    'pluginOptions'=>['allowedFileExtensions'=>['jpg','gif','png']
]);

/**
* uncomment for multiple file upload
echo $form->field($model, 'image[]')->widget(FileInput::classname(), [
    'options'=>['accept'=>'image/*', 'multiple'=>true],
    'pluginOptions'=>['allowedFileExtensions'=>['jpg','gif','png']
]);
*/

// render the submit button
echo Html::submitButton($model->isNewRecord ? 'Upload' : 'Update', [
    'class'=>$model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']
);

// render a delete image button 
if (!$model->isNewRecord) { 
    echo Html::a('Delete', ['/person/delete', 'id'=>$model->id], ['class'=>'btn btn-danger']);
}

ActiveForm::end();

Thus, you should be able to use the FileInput for advanced cases of upload. Basically, completely manage your files for insert, update, and delete. As mentioned earlier, in case you are using multiple file input, the $image variable in controller will return an array of files, which you must loop through. Its important for multiple file uploads to follow an array naming convention as mentioned in this web tip.

32 thoughts on “Advanced upload using Yii2 FileInput widget

  1. I’m very confused by your use of “avatar” and “filename.” I can completely understand the use of “filename” to store the name of the source file, but what is “avatar.” From your code it looks like it’s just a repeat of the filename. Or is is something else?

    1. If you read again the wiki’s example (and code) closely, the avatar stores the auto generated file name (for uniqueness) – while filename is what the user picked on his/her client for upload.

      Generating a unique file name is important before storing on server – because it obviously will conflict with other records.

      1. That’s kind of what I thought, but then, it looked like that was a lot of confusing information for an edge case, so I thought you must have meant something else. Now, I know I can just sift through and delete all that extraneous stuff about generating and storing two separate separate filenames. Thanks for your response.

        1. Its just an example to show how you can capture source file attributes (e.g. file name as considered here) if needed. You need to adapt it to your use case – there could be other requirements as well.

          1. Hi Karik,

            We have used your plugin and it is awesome.

            But here I am facing one issue, First I have upload 2 images and them again I have selected 1 image after submitting button then I am getting only last image not all three. Will you please guide me on it.

            Same thing is also arise when we deleting am image.

            I hope you understand my question.

            Below is my sample code:

            use kartik\file\FileInput;

            field($model, ‘filedata[]’)->widget(FileInput::classname(), [
            ‘options’=>[
            ‘multiple’=>true,
            ‘accept’ => ‘image/*’
            ],
            ‘pluginOptions’ => [
            //’initialPreview’=>[
            // Html::img(Yii::$app->homeUrl.”images/logo.png”, [‘class’=>’file-preview-image’, ‘alt’=>’The Moon’, ‘title’=>’The Moon’]),
            //],
            //’overwriteInitial’=>false,
            ‘overwriteInitial’ => true,
            ‘uploadUrl’ => Url::to([”]),
            ‘uploadExtraData’ => [
            ‘album_id’ => 20,
            ‘cat_id’ => ‘Nature’
            ],
            ‘allowedFileExtensions’ => [‘jpg’, ‘png’],
            ‘maxFileCount’ => 3,
            ‘showRemove’ => false,
            ‘showUpload’ => false,
            ]
            ]);
            ?>

            Thanks in advance,

  2. Hi Kartik,

    Your plugin is great and has been suiting my needs. However I have a use-case where which I think is a valid one and is not covered by the plugin. It goes like this:
    1- I need to allow users to upload a max of 3 files, so it’s a multiple file upload so I have overwriteInitial false;
    2- If a user uploads one file and later comes back and wants to upload another, this is excelent and the plugin works;
    3- However, if a user has uploaded one file, and later comes back and wishes to clear the uploaded file and upload a different one (or more than one), then here the plugin in does not work as expected. Clicking on the “Remove” button does nothing, so there is no way of clearing/removing the images if overwriteInitial is set to false;
    4- Also, there is no way of deleting 1 file while leaving the others untouched, even with overwriteInitial set to true, because all files are cleared.

    I am aware that I can use the fileclear/filecleared events (they work with both true and false on the overwriteInitial) but this would imply making a post to the server to clear the images and then reload the page, which defeats the plugin idea.

    Maybe I missed something, and if so, I would be more than grateful if you correct me.

    Thanks for all the hard work.
    Let me know your thoughts,
    Rui

    1. Rui – the plugin gives you just an alternative to the built in HTML File Input to upload files from client. It by default does not have any server side code or validation enabled (so reading previous uploads or data from server is entirely out of scope of this plugin). So its upto each developer to use it as per their own use case. For this purpose, the plugin offers you several ways to style, extend, and add features to expand upon.

      For example, for your use case, you may use the following features of the plugin to achieve various use cases. These are suggestions but in no means ONE way to solve what you need:

      • You can change the remove button label to name it to something meaningful for your users – e.g. Clear (Preview). You may call an additional javascript code on click of this if needed e.g. to hide preview elements if needed OR set a deleted flag within an hidden input for you to process a server response.
      • You may want to alternatively entirely hide the above remove button – if you want to totally create your own code/logic.
      • Extending on the above, you can add CUSTOM buttons. You can add another button if needed by editing the layoutTemplates.main1. Markup for this can be created like any bootstrap input group addon button. You can call this new button say Delete File – and call a client code (in addition to server code if needed).

      Etcetra – there could be more such ways – but building the code that talks to the server where needed lies with each developer. The plugin gives you some basic ideas for quick starting to use a file input plugin. For example the sample buttons like Remove or Upload are just initial ideas that could be useful for basic use cases. Advanced use cases need to extend and build upon this.

      Let me know if what I tell makes sense.

  3. Hi Kartik,

    Thanks for your thorough answer.

    I am aware that I need to build the backend code and I already did. It works like a charm for points #1 and #2 on my previous comment.

    Also, I did try and extend the Remove functionality by using the filecleared event with something like this:

    'pluginOptions' => [
    'showUpload' => false,
    'initialPreview'=> $myFiles,
    'overwriteInitial'=>false,
    ]
    'pluginEvents' => [
    'filecleared' => 'function() { $("#hiddenInputFilesCleared").val("true"); '
    . '$(".field-view-field_name .file-preview-thumbnails").empty();'
    . '$(".field-view-field_name .file-input").addClass("file-input-new");'
    . '$(".field-view-field_name .file-caption-name").empty();'
    . '}',
    ]

    but this didn’t work. The hidden input gets it’s value changed to true (which is great) but the fields don’t hide – actually they “blink”, hiding and showing again. I guess that the plugin “resets” itself to its initial state.

    Maybe I’m doing this wrong, but I read all documentation and examples and I didn’t see a clean way of clearing the preview. Using the JS above in the filecleared event seems like a hack, but even that doesn’t work.

    Let me know what you think.
    Thanks,
    Rui

  4. Hi,
    Kartik! thanks for your extension but i need to implement functionality to select multiple files one by one not in one time as your code works, and also shows corresponding close icon.
    please reply soon as possible.

    Thanks,
    Sprybhanu

    1. Its not in scope of the extension currently – because the extension is been deliberately made simple without any overheads and optimally just use the HTML 5 native file input features with enhanced styles and events. The native file input does not support editing or selectively removing selected files. You would need to build your own code for uploading it or removing it from server as needed.

  5. Hello, I would like to help me, the line $image = UploadedFile::getInstance($model, ‘image’); returns null, this is my code

    model

    class Trabajador extends \yii\db\ActiveRecord
    {
    
    <pre><code>  public $image;
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'trabajador';
    }
    
    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['image'], 'safe'],
            [['image'], 'file'],
        ];
    }
    </code></pre>
    
    }
    

    view

    echo $form->field($model, 'image')->widget(FileInput::classname(), [
               'options' => ['accept' => 'image/*'],
               //'name' => '',
               //'disabled' => true,
               'pluginOptions' => [
                               //'showCaption' => false,
                               //'showPreview' => false,
                               //'autoFitCaption' => false,
                               'showRemove' => false,
                               //'showUpload' => false,
                               'browseClass' => 'btn btn-default',//btn-block
                               'browseIcon' => '<i></i> ',
                               'browseLabel' => 'Seleccione la Foto',
                               //'previewFileType' => 'any',
                               'allowedFileExtensions'=>['jpg','gif','png']
                               ],
          ]);
    

    controller

    if($model->load(Yii::$app->request->post()) && $model->validate()){
    
        $image = UploadedFile::getInstance($model, 'image');
        $model-&gt;foto = $image-&gt;name;
        $ext = end((explode(".", $image-&gt;name)));
        $foto = Yii::$app-&gt;security-&gt;generateRandomString().".{$ext}";
        $path = Yii::$app-&gt;params['uploadPath'] . $foto;
        $image-&gt;saveAs($path);
    }
    
    1. Not looked in detail on your code. But would recommend you to debug and just look at your setup, your yii model rules, and the way you are submitting and receiving file data, and reading via UploadedFile. Your attribute image must be set to safe in your model validation rules. It seems so ok from your code, but that could be other reason for not getting data on your server.

  6. Hi I tried myself on your script for the profile edit of a user.
    I fit it to my needs but the problem is that my [code]$image[/code] is always empty but with no error message. I tried a lot but have no clue after all.

    this is in my form(update.php) the last part after some inputs:
    [code]
    echo $form->field($model, ‘image’)->widget(FileInput::classname(), [
    ‘pluginOptions’ => [
    ‘options’=>[‘accept’=>’image/*’],
    ‘allowedFileExtensions’=>[‘jpg’,’gif’,’png’],
    ‘initialPreview’=>[
    Html::img($model->getImageUrl(), [‘class’=>’file-preview-image’, ‘alt’=>$title, ‘title’=>$title]),
    ],
    ‘showCaption’ => false,
    ‘showRemove’ => false,
    ‘showUpload’ => false,
    ‘browseClass’ => ‘btn btn-grey’,
    ‘browseIcon’ => ‘ ‘,
    ‘browseLabel’ => ‘Profilbild’
    ],
    ])->label(false);
    [/code]

    Than $model->image should be recognized in here or:
    [code]
    $image = $model->uploadImage();
    [/code]

    but due to it’s empty the model returns:
    [code]
    $image = UploadedFile::getInstance($this, ‘image’);
    if (empty($image)) {
    return false;
    }
    [/code]

    I really don’t know how to get an error for it or actually have a clue to localize the problem…

  7. I have a problem with Advanced upload using Yii2 Fileinput widget. Please help me!
    In my View:
    field($model, ‘thumb’)->widget(FileInput::className(),[
    ‘options’=>[‘accept’=>’image/*’],
    ‘pluginOptions’ => [
    ‘uploadUrl’ => Url::to(‘files/thumbs’),
    ‘maxFileCount’ => 10,
    ‘uploadExtraData’ => [
    ‘album_id’ => 20,
    ‘cat_id’ => ‘Nature’
    ],
    ]
    ]); ?>

    And this is error:
    logo_insert.png: SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

    Thank all!

  8. Hi Kartik
    Great tutorial!
    Can you note, that class yii\web\UploadedFile has 2 methods with similar names: getInstance – for single file upload and getInstances – for multiple.
    I know you note that in comment to model listing, but, for me, it easy for overlook.
    Thanks

  9. I truly admire all the work you’ve done to make Yii2 an even better development platform. After a little trouble understanding it (entirely my doing), this article has been phenomenal in getting file uploads done. Is there any chance that you;l write a similar article on how to do multiple file uploads. I assume that such would require a separate model for the file uploads with a one -> many relationship, but I’m having trouble figuring out how to accomplish that within what you’ve give us here. Even if you could post the schema, models, views, and controllers for that type of set up, it would help greatly. I just can figure out how to make it all happen.

  10. Hello! First of all, wounderfull plugin! It’s help me and do it in a easy way.
    I hope you can help me on a configuration problem. I would like to use the plugin with the advanced template but i would like to keep all the upload file in the common folder.
    So i create the following structure: common/web/uploads
    Then i set my uploadPath in the params as “Yii::getAlias(‘@common/web/uploads/’)”
    This work fine for the upload of the file, but i can’t get the uploadUrl and print the image.
    Can you help me?
    Thanks!!!

  11. Have a look at my code below When i try to load an image from a model. The model has the image, which is already uploaded using kartik-v widget.
    The image is mandatory, but if you look closely the hidden input for icon has the value set as 0. Because of this a validation error is thrown as the image field is a required field. How to handle this.?

    <

    div class="form-group field-level-icon required">
    Icon

    ×

    </div>
    </div>
    <div class="clearfix"></div>    <div class="file-preview-status text-center text-success"></div>
    <div class="kv-fileinput-error file-error-message" style="display: none;"></div>
    </div>
    </div>
    <div class="kv-upload-progress hide"></div>
    <div class="input-group ">
    


    1 files selected

    <

    div class="input-group-btn">

    1. the code above is corrupted so here is the update

      <

      div class=”form-group field-level-icon required”>
      Icon

      ×

      </div>
      </div>
      <div class="clearfix"></div>    <div class="file-preview-status text-center text-success"></div>
      <div class="kv-fileinput-error file-error-message" style="display: none;"></div>
      </div>
      </div>
      <div class="kv-upload-progress hide"></div>
      <div class="input-group ">
      


      1 files selected

      <

      div class=”input-group-btn”>

Leave a Reply