Multiple files upload in Yii - tabular input fields

By Ronald van Belzen | May 31, 2013

There are several ways to tackle the challenge of uploading multiple files in Yii. The differences are based upon the form field(s) being used. Here I will show an example of a tabular file input form. Other methods include the use of the CMultiFileUpload widget and the multiple attribute for the file input field introduced by HTML5.

When you have not read my previous blog post about single file upload, you should consider it when you run into problems. Also you should take into account the php.ini settings for max_file_uploads, post_max_size and upload_max_filesize.

To the model that will store the information about the image the field $images is added for the file upload:

class GalleryImage extends CActiveRecord { 
  public $images;

And to this class the validation rule is added for the new field:

  public function rules() { 
    return array( 
    // other rules 
    array('images', 'file', 'allowEmpty' => true, 'types' => 'gif,jpg,jpeg,png', 'maxSize' => 5242880, 'maxFiles' => 5, ), 
    // other rules ); 
  }

Next we create a form for the input of the files and their descriptions. To add a bit of complexity the images can be added to different galleries:

<?php 
/* @var $this GalleryImageController */ 
/* @var $models GalleryImage */ 

$this->breadcrumbs=array( 
  'Gallery Images'=>array('index'), 
  'Create', 
); 

$this->menu=array( 
  array('label'=>'List GalleryImage', 'url'=>array('index')), 
  array('label'=>'Manage GalleryImage', 'url'=>array('admin')), 
); 
?> 

<h1>Upload Gallery Images</h1> 

<div class="form"> 

<?php $form=$this->beginWidget('CActiveForm', array( 
  'id'=>'gallery-image-form', 
  'enableAjaxValidation'=>false, 
  'htmlOptions'=>array('enctype'=>'multipart/form-data'), 
)); ?> 

  <p class="note">Fields with <span class="required">*</span> are required.</p> 

  <?php echo $form->errorSummary($models); ?> 

  <div class="row"> 
    <?php echo $form->labelEx($models[0],'gallery_id'); ?> 
    <?php echo $form->dropDownList($models[0],'gallery_id',CHtml::listData(Gallery::model()->findAll(), 'id', 'name')); ?> 
    <?php echo $form->error($models[0],'gallery_id'); ?> 
  </div> 

  <?php foreach($models as $key => $model): ?> 
  <div class="row"> 
    <?php echo $form->labelEx($model,'description'); ?> 
    <?php echo $form->textField($model,"[$key]description", array('size'=>100,'maxlength'=>255)); ?><strong> [1] </strong> 
    <?php echo $form->fileField($model, "[$key]images", array('size'=>90)); ?> 
    <?php echo $form->error($model,"[$key]images"); ?> 
  </div> 
  <?php endforeach; ?> 

  <div class="row buttons"> 
    <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save'); ?> 
  </div> 

<?php $this->endWidget(); ?> 

</div><!-- form -->

Personally, I find the use of the field name format of "[]attributeName" not intuitive, but when you realize that the widget prefixes the field names you will undertand why it works.

The variable $models is an array, as you will also observe in the controller that determines the length of the array. Multiple files upload in this example is restricted to the creation of images in the image gallery. Image updates will be done by single file upload.

public function actionCreate() { 
  $models[] = new GalleryImage; 
  $models[] = new GalleryImage; 
  $models[] = new GalleryImage; 
  $models[] = new GalleryImage; 
  $models[] = new GalleryImage; 

  if(isset($_POST['GalleryImage'])) { 
    $valid = true; 
    $destination = Yii::getPathOfAlias('webroot.gallery'); 
    foreach($models as $key => $model) { 
      $model->gallery_id = $_POST['GalleryImage']['gallery_id']; 
      $model->description = $_POST['GalleryImage'][$key]['description']; 
      $model->images = CUploadedFile::getInstances($model, "[$key]images"); 

      if($model->images) { 
        $uid = uniqid(); 
        $model->fullsize = 'img' . $uid . '.'; 
        $model->mediumsize = 'med' . $uid . '.png'; 
        $model->thumbnail = 'min' . $uid . '.png'; 
        if($model->validate()) { 
          switch( exif_imagetype($model->images[0]->tempName) ) { 
            case IMAGETYPE_GIF: 
              $model->fullsize .= 'gif'; $model->images[0]->saveAs($destination . '/' . $model->fullsize); 
              if($model->save()) { 
                $this->resizeImage($model->fullsize, $model->mediumsize, $model->thumbnail); 
              } else { 
                $valid = false; 
              } 
              break; 
            case IMAGETYPE_JPEG: 
              $model->fullsize .= 'jpg'; 
              $model->images[0]->saveAs($destination . '/' . $model->fullsize); 
              if($model->save()) { 
                $this->resizeImage($model->fullsize, $model->mediumsize, $model->thumbnail); 
              } else { 
                $valid = false; 
              } 
              break; 
            case IMAGETYPE_PNG: 
              $model->fullsize .= 'png'; 
              $model->images[0]->saveAs($destination . '/' . $model->fullsize); 
              if($model->save()) { 
                $this->resizeImage($model->fullsize, $model->mediumsize, $model->thumbnail); 
              } else { 
                $valid = false; 
              } 
              break; 
            default: 
               $model->addError('images', 'The file ' . $model->images[0]->name . ' cannot be uploaded. Only files with the image formats gif, jpg or png can be uploaded.'); 
               $valid = false; 
               break; 
            } 
          } else { 
            $valid = false; 
          } 
        } 
      } 
    if($valid) { 
      $this->redirect(array('index')); 
    } 
  } 
  $this->render('create',array( 
    'models'=>$models, 
  )); 
}

The form passes 3 different variables for the controller function to handle. The request variable gallery_id is passed only once, but is copied to all model variables $model->gallery_id. The variable description is passed in an array and each element of the array is moved to the appropiate model instance attribute.

The files (images) are also passed in an array and each element of the array is passed to $model->images with the help of the function CUploadedFile::getInstances(). When the presence of a files can be confirmed the remainder of the model attributes are set and before validating the input the function determines the real content type and determines from the content type what file-extension should be used in the variable saved in $model->fullsize.

The most important thing to take into account is that the function getInstances() should be used and not getInstance(). The other thing is that the file attributes are part of an array with a single element. Therefore these attributes should be retrieve as  $model->images[0]->tempName and $model->images[0]->name and the files needs to be save with $model->images[0]->saveAs(...).

The function resizeImage() resizes the image to a medium size and a thumbnail size to be used in the image gallery.

Further reading

Add new comment