Upload files using tabular form

Hi folks, I’m having some problems when I try upload multiples files in multiples instances of my model (tabular way).

I have a model called Files and a view that generate a form for multiple instances of that model. When I save the form without "multpart/form-data", everything works, but if I put this parameter on form and submit it, the validation shows the message that "File cannot be blank."

See my controller code bellow:




  public function actionRegistration() {

    $company  = new Company;

    $contacts = $this->getModelItens('Contact', 3);

    $banks    = $this->getModelItens('Bank'   , 2);

    $files    = $this->getModelItens('File'   , 2);

    

    $company->scenario = 'create';

    if($_POST['Company']) {

      $company->attributes = $_POST['Company'];

      $valid = $company->validate();

      $valid = $this->validateModels($_POST['Contact'], $contacts) && $valid;

      $valid = $this->validateModels($_POST['Bank'], $banks) && $valid;

      $valid = $this->validateModels($_POST['File'], $files) && $valid;

      

      if($valid) {

        if($company->save()) {

          $this->saveModels($contacts, $company->id);

          $this->saveModels($banks, $company->id);

          $this->saveModels($files, $company->id);

          

          $this->redirect('/obrigado');

        }

      }

    }

    

	  $this->render('registration', array('company' => $company, 'contacts' => $contacts, 'banks' => $banks, 'files' => $files));

	}




  private function getModelItens($model_name, $times, $scenario = 'create') {

    $models = array();

    for($i = 0; $i < $times; $i++) { 

      $models[$i]           = new $model_name; 

      $models[$i]->scenario = $scenario;

    }

    return $models;

  }


  private function validateModels($forms, $models) {

    $valid  = true;

    foreach($forms as $k => $form) { 

      $models[$k]->attributes = $form;

      $models[$k]->position   = $k;

      $valid = $models[$k]->validate() && $valid;

    }

    return $valid;

  }

  

  private function saveModels($models, $company_id) {

    foreach($models as $k => $model) {

      $model->company_id = $company_id;

      if($model instanceOf File) {

        if($model->save()) $this->upload_file($model, "file"); 

      } else $model->save();

    }

  }




  private function upload_file($model, $field, $k) {

    $path = Yii::app()->basePath . "/../assets/files/companies/{$model->company_id}/";


    $file = CUploadedFile::getInstance($model, $field);

    if($file instanceof CUploadedFile) $model->$field = $file;

    

    if($model->$field instanceof CUploadedFile) {

      if(!file_exists($path)) exec("mkdir -p {$path}");

      $model->$field->saveAs($path . $model->$field);

    }

  }



I’ve tried everything but I can’t fix it, any sugestion?

Thanks.

Hi,

I did some tests but without success :(

I have printed the output of $_FILES array and it seems that’s all right.

Anyone can help me?

The $_FILES output:




Array

(

    [File] => Array

        (

            [name] => Array

                (

                    [0] => Array

                        (

                            [file] => arquivos_alterados.txt

                        )


                    [1] => Array

                        (

                            [file] => app_monred.pdf

                        )


                )


            [type] => Array

                (

                    [0] => Array

                        (

                            [file] => text/plain

                        )


                    [1] => Array

                        (

                            [file] => application/pdf

                        )


                )


            [tmp_name] => Array

                (

                    [0] => Array

                        (

                            [file] => /private/var/tmp/phpHQ4IDb

                        )


                    [1] => Array

                        (

                            [file] => /private/var/tmp/php3tWwVs

                        )


                )


            [error] => Array

                (

                    [0] => Array

                        (

                            [file] => 0

                        )


                    [1] => Array

                        (

                            [file] => 0

                        )


                )


            [size] => Array

                (

                    [0] => Array

                        (

                            [file] => 412

                        )


                    [1] => Array

                        (

                            [file] => 284694

                        )


                )


        )


)



Do you have rule in model class for your file attribute?

It looks like it was posted, but it breaks on validation…

Obviously you must set enctype="multpart/form-data" or form will be submitted without posted file…

Yes, I have, see bellow:




	public function rules() {

		return array(

                             array('type', 'required', 'on'=>'create'),

			     array('company_id, type', 'numerical', 'integerOnly'=>true),

                             array('file', 'length', 'max'=>255),

                             array('file', 'file', 'allowEmpty' => false),

                             array('position', 'safe'),

			     array('id, company_id, type, file', 'safe', 'on'=>'search'),

		);

	}




it’s setted too.

Ok I think problem is here:




$valid = $this->validateModels($_POST['File'], $files) && $valid;



You are passing $_POST array instead $_FILES array.

Ok, this will break your generic validation method, but you must pass $_FILES for attribute which has rule type file along as $_POST for other attributes.

Hi ManInTheBox,

I did a test using your suggestion, but without success.

When I submit the form passing $_FILES array instead of passing $_POST the php thow a fatal error (see bellow).




Fatal error: Call to undefined method stdClass::validate() in /Users/san/src/neogama/fornecedores/svn/trunk/web/public/protected/controllers/SiteController.php on line 79



I also tried to assing the files instance before validations but without success too, the same error happens "File cannot be blank."

I think that yii upload feature not work with tabular input. Maybe I’ll have to implement my own upload method.

Let me know if you have another suggestion.

Thanks.

I just want to be clear and not misunderstood:

[i]Let’s say you have model with some attributes (type, category, title, …, picture)

[/i]

Attribute picture has file validator listed in rules.

When you do massive assign you will normally write something similar to:




$model->attributes = $_POST['MyModel'];



And that’s great. If posted attributes are listed in rules validation will be performed on them.

But, what about picture attribute? It is not just plain string from $_POST. It is a file from $_FILES.

So what I suggested to you is to do massive assign, and then manually set file from $_FILES.




$model->attributes = $_POST['MyModel'];

$model->picture = $_FILES['my_file'];



This way attribute picture will be validated, because it is not empty anymore.

Also you’ll need to rewrite your validation method, but for testing purposes I suggest simplest as possible.

I really hope you’ll make it somehow :)

Hi ManInTheBox,

I did the test, but it didn’t work too (see the error below), its becouse the format of $_POST array and $_FILES array are different.


mb_strlen() expects parameter 1 to be string, array given 

Hi,

I also tested this, and I have one correction to myself.

I used well known webapp skeleton in this example.

Here is working version:




// view file

...

<?php $form=$this->beginWidget('CActiveForm', array(

	'id'=>'login-form',

	'htmlOptions' => array('enctype' => 'multipart/form-data')

)); ?>

...

	<div class="row">

		<?php echo $form->labelEx($model,'image'); ?>

		<?php echo $form->fileField($model,'image'); ?>

		<?php echo $form->error($model,'image'); ?>

	</div>

...






// model file

class LoginForm extends CFormModel

{

	public $username;

	public $password;

	public $rememberMe;

	

	public $image;

...

	public function rules()

	{

		return array(

			array('username, password', 'required'),

			array('image', 'length', 'max'=>255),

			array('image', 'file', 'allowEmpty' => false),

                        /*

                          You may put image also in required validator

                          and error message will be displayed with all

                          required attributes. Otherwise it will be displayed

                          after all required fields are filled. You may remove

                          allowEmpty => false, because it is false by default.

                          Anyway it's just our old PHP constant UPLOAD_ERR_NO_FILE <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/smile.gif' class='bbc_emoticon' alt=':)' />

                        */

...






// and finally in controller action

...

	public function actionLogin()

	{

		$model=new LoginForm;

		if(isset($_POST['LoginForm']))

		{

			$model->attributes=$_POST['LoginForm'];

			$model->image = $_FILES['LoginForm']; // this was wrong in my previous post... sorry <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/smile.gif' class='bbc_emoticon' alt=':)' />

			if($model->validate())

			{

			    die(print_r($model)); // here you will see image file

...



Really hope you’ll succeed it now :)

$ProjectPhotos = array();

$photos_path = realpath(Yii::app()->basePath . ‘/../images/project_photos’);

foreach ($_POST[‘ProjectPhotos’] as $key => $value) {

    &#036;ProjectPhoto = new ProjectPhotos('batchSave');


    &#036;ProjectPhoto-&gt;attributes = &#036;value;

$photofile=CUploadedFile::getInstance($ProjectPhoto,’[’.$key.’]image’);

if($photofile){

&#036;ProjectPhoto-&gt;image=date('YmdHis').rand(100000,900000).'.'.&#036;photofile-&gt;extensionName;





&#036;photofile-&gt;saveAs(&#036;photos_path . '/' .&#036;ProjectPhoto-&gt;image);

}

$ProjectPhotos[] = $ProjectPhoto;

}

            foreach (&#036;ProjectPhotos as &#036;ProjectPhoto) {


                    &#036;ProjectPhoto-&gt;project_id = &#036;project_id;


                    &#036;ProjectPhoto-&gt;save();


            }

CUploadedFile::getInstance($model,’[’.$key.’]attribute_name’);

check out above statement

tested successfully