Upload files in Yii2 with MongoDb and GridFs

  1. The scenario
  2. The structure
  3. Rendering the file

The scenario

Uploading files in a webapp can be extremely tricky and sometimes the quickest way to do it is to store the file directly in the webserver or into a DNS (like Amazon S3) and then to save the link and the metadata inside a table in the DB. The thing is that you'll have to deal with file permissions, server storage, file management and so on (which is perfectly fine, by the way).

Another approach is to save the file as a blob inside a database: this is extremely easy and convenient with MongoDb because you can then leverage the built-in sharding feature in case you scale-up your app. With yii2 it's also surprisingly easy. Let's begin.

The structure

We will work with the GridFs framework tutorial found here: Metadata and asset management.

Step 1: the model

Depending on your type of application (advanced or basic), create a new model Asset in app\models or, backend\models / frontend\models common\models.

namespace common\models;

use yii\mongodb\file\ActiveRecord;

/**
 * Class Asset
 * @package common\models
 * @property string $_id MongoId
 * @property array $filename
 * @property string $uploadDate
 * @property string $length
 * @property string $chunkSize
 * @property string $md5
 * @property array $file
 * @property string $newFileContent
 * Must be application/pdf, image/png, image/gif etc...
 * @property string $contentType
 * @property string $description
 */
class Asset extends ActiveRecord
{

    public static function collectionName()
    {
        return 'asset';
    }

    public function rules()
    {
        return[
            [['description', 'contentType'], 'required'],
        ];
    }

    public function attributes()
	{
	      return array_merge(
	          parent::attributes(),
	          ['contentType', 'description']
	      );
	}
} 

Now, from the documentation, you must set all the fields but 2: "contentType" and "description" that we will add for 2 reasons. The type of content is necessary to render the blob and the description is a nice addition for the "alt" tag in case of images. Create the AssetSearch model class accordingly to your needs.

Step 2: the view

In _form.php (format as you like, in this case there's no html because I put this form in a modal with custom html and tags that can be confusing for the sake of the tutorial)

<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;

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

                <?= $form->field($model, 'description')->textInput(['class'=>'form-control inline-input', 'placeholder'=>Yii::t('app', 'Description')])->label('')?>

                <?= $form->field($model, 'file')->fileInput()->label('')?>
                
                <?= Html::submitButton($model->isNewRecord ? 'Upload' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?>
            
<?php ActiveForm::end(); ?>
Step 3: the controller
Action Create

Now, the interesting part:

use yii\web\UploadedFile;
use common\models\Asset;

public function actionCreate()
    {
        $model = new Asset;
        if ($model->load($_POST)) {
            $file = UploadedFile::getInstance($model,'file');
            $model->filename=$file->name;
            $model->contentType=$file->type;
            $model->file=$file;
            if($model->save()){
                return $this->redirect(['view', 'id' => (string)$model->_id]);
            }
        }else
            return $this->render('create', [
                'model' => $model,
            ]);
    }

Under the hood GridFs will create 2 collections: asset.files and asset.chunks ; the first one stores the metadata and the second one the file itself, but you don't need to worry about any of that because our friends at 10gen and yii made all this quite "invisible" and easy.

Ok, now you've uploaded a file with GriFs in MongoDb. Now you have to retrieve it.

Action get

Name this action as you like. Basically this is the action that will render the file; now you'll see the reason of that "contentType" attribute.

/**
     * Displays the asset as a blob
     * @param integer $_id
     * @return mixed
     */
    public function actionGet($id)
    {
        $model=$this->findModel($id);
        header('Content-type: '.$model->contentType);
        echo $model->file->getBytes();
    }

Rendering the file

In order to render the file - let's say, an image - , apply this code in any view:

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

<?= Html::img(Url::to(['asset/get', 'id'=>(string)$model->_id]), ['alt'=>$model->description]);?>

If this was an asset gallery à la Wordpress where you put pdf, images and so on, then you might want something like this:

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

<?php if(strpos($model->contentType,'image')===false): //Not an image?>
        <iframe src="<?=Url::to(['asset/get', 'id'=>(string)$model->_id]);?>" width="100%" height="600px"></iframe>
    <?php else: ?>
        <?= Html::img(Url::to(['asset/get', 'id'=>(string)$model->_id]), ['alt'=>$model->description]);?>
    <?php endif; ?>

Quick note: this method is recommended for all of you guys who have to store sensible user data files (like card IDs, passports and so on) because you can leverage the standard AccessControl rules to display a single file so that you can put granular permissions to CRUD operations regarding files too (especially to read operations). If you are thinking of storing your user files in the server and you are not sure of how an .htaccess or apache2.conf file works, then I recommend this method (search engines crawlers can be very nasty when you publish /your/folder/image.png and you forget to remove the "List" permissions).

Hope this will help someone: thanks for your feedback.

6 0
6 followers
Viewed: 26 439 times
Version: 2.0
Category: How-tos
Written by: edoardo849
Last updated by: pceuropa
Created on: Feb 9, 2014
Last updated: 11 months ago
Update Article

Revisions

View all history

Related Articles