2

I am working on yii2. In one of my view, I am trying to upload an image. But I am unable to upload it.

Model

class MeterAcceptanceHeader extends \yii\db\ActiveRecord
{

public static $status_titles =[
    0 => 'Prepared',
    1 => 'Created',
    2 => 'Printed',
    3 => 'Canceled',
];

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

/**
 * @inheritdoc
 */
public function rules()
{
    return [
        [['sub_div', 'prepared_by'], 'required'],
        [['prepared_by', 'updated_by'], 'integer'],
        [['prepared_at', 'updated_at'], 'safe'],
        [['sub_div', 'meter_type', 'status'], 'string', 'max' => 100],
        [['images'], 'string', 'max' => 255],
        [['images'], 'file', 'skipOnEmpty' => true, 'extensions' => 'png,jpg,pdf', 'maxFiles' => 4],
        [['sub_div'], 'exist', 'skipOnError' => true, 'targetClass' => SurveyHescoSubdivision::className(), 'targetAttribute' => ['sub_div' => 'sub_div_code']],
        [['prepared_by'], 'exist', 'skipOnError' => true, 'targetClass' => User::className(), 'targetAttribute' => ['prepared_by' => 'id']],
    ];
}

/**
 * @inheritdoc
 */
public function attributeLabels()
{
    return [
        'id' => 'ID',
        'sub_div' => 'Sub Div',
        'meter_type' => 'Meter Type',
        'prepared_by' => 'Prepared By',
        'prepared_at' => 'Prepared At',
        'updated_at' => 'Updated At',
        'status' => 'Status',
        'updated_by' => 'Updated By',
        'images' => 'Document Snap',
    ];
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getMeterAcceptanceDetails()
{
    return $this->hasMany(MeterAcceptanceDetails::className(), ['accpt_id' => 'id']);
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getSubDiv()
{
    return $this->hasOne(SurveyHescoSubdivision::className(), ['sub_div_code' => 'sub_div']);
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getPrepared()
{
    return $this->hasOne(User::className(), ['id' => 'prepared_by']);
}
}

MeterAcceptanceHeader Table

enter image description here

MeterAcceptanceImages Table

enter image description here

There is a form1 in which user is prompt to select from the dropdown.

Form1 View

<div class="meter-acceptance-header-form">
<?php $model->status = common\models\MeterAcceptanceHeader::$status_titles[0]; ?>

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

<?= $form->field($model, 'sub_div')->dropDownList([''=>'Please Select'] + \common\models\SurveyHescoSubdivision::toArrayList()) ?>

<?= $form->field($model, 'meter_type')->dropDownList([''=>'Please Select','Single-Phase' => 'Single-Phase', '3-Phase' => '3-Phase', 'L.T.TOU' => 'L.T.TOU']) ?>

<?= $form->field($model, 'status')->textInput(['maxlength' => true,'readonly' => true]) ?>

<div class="form-group">
    <a class="btn btn-default" onclick="window.history.back()" href="javascript:;"><i
                class="fa fa-close"></i>
        Cancel</a>
    <a class="<?= $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary' ?>" onclick="
      $('#acceptance-form').submit();" href="javascript:">
        <?= $model->isNewRecord ? 'Create' : 'Update' ?></a>
</div>

<?php ActiveForm::end(); ?>

After clicking on Create button the user is prompt to the second form in which a user will upload an image.

Below is my code for the controller from which I am trying to upload it.

public function actionSetpdf($id)
{
    $model = $this->findModel($id);
    $m = 0;
    $accpt_id = $model->id;
    $meter_type = $model->meter_type;
    $ogp_sub_div = $model->sub_div;
    $images=[];
    $ic=0;
    $files_uploaded = false;
    if(Yii::$app->request->isAjax && Yii::$app->request->post())
    {
        $data = explode(',',$_POST['data']);

        foreach($data as $value)
        {

            $m = new MeterAcceptanceDetails;
            $m -> load(Yii::$app->request->post());

            $m->accpt_id = $accpt_id;
            $m->meter_type = $meter_type;
            $m->created_at = date('Y-m-d H:i:s');
            $m->created_by = Yii::$app->user->id;
            $m->meter_id = $value;
            $m->meter_msn = \common\models\Meters::idTomsn($value);
            $m->flag = 1;// 1 means created
            $m->ogp_sub_div = $ogp_sub_div;

            if($m->save())
            { 
                // Here the upload image code starts
                if($ic==0)
                {

                    $model->images = UploadedFile::getInstances($model, 'images');
                    foreach ($model->images as $file)
                    {
                        if (file_exists($file->tempName))
                        {
                            $img_s = new MeterAcceptanceImages;
                            $file_name = rand(0, 1000) . time().date('his') . '.' . $file->extension;
                            $file->saveAs('uploads/meter_acceptance/' . $file_name);
                            $img_s->file_path = $file_name;
                            $img_s->accpt_id = $accpt_id;

                            if ($img_s->save()) {
                                $images[] = $img_s;

                            } else {
                                print_r($img_s->getErrors());
                            }
                        }
                    }
                }else{
                    foreach($images as $image){
                        $img_s = new MeterAcceptanceImages;
                        $img_s->file_path = $image->file_path;
                        $img_s->accpt_id = $accpt_id;
                        $img_s->save();
                    }
                }

                $model->status = MeterAcceptanceHeader::$status_titles[1];
                $model->update();

            }
            else{

                $this->renderAjax('viewcreated');
            }
        }

    }
    else{
        $this->renderAjax('viewcreated');
    }


    return $this->redirect(Url::toRoute(['meteracceptanceheader/viewsetpdf','id' => $model->id,'model' => $this->findModel($id)]));

}

Form2 View

<div class="map-meters-form" id="doc">
<?php $form = ActiveForm::begin(['id' => 'map-form', 'enableClientValidation' => true, 'enableAjaxValidation' => false,
'options' => ['enctype' => 'multipart/form-data']]) ?>
<section class="content">
<div class="box">
    <div id="chk" class="box-body">
        <?php Pjax::begin(); ?>
        <?= DetailView::widget([
            'model' => $model,
            'attributes' => [

                [
                        'label'=>'Serial #',
                         'value' => function($d)
                         {
                             return $d->id;
                         }
                ],
                [
                    'label' => 'Meter Type',
                    'value' => function ($d) {
                        if(is_object($d))
                            return $d->meter_type;
                        return ' - ';
                    },


                ],
                'sub_div',
                [
                    'label' => 'Sub Division Name',
                    'value' => function ($d) {
                        if(is_object($d))
                            return $d->subDiv->name;
                        return '-';
                    },


                ],
                [
                    'label' => 'Prepared By',
                    'value' => function ($d) {
                        if(is_object($d))
                            return $d->prepared->name;
                    },


                ],
                'prepared_at',

                'status',


            ],
        ]) ?>
        <br>
        <div class="pre-scrollable">
        <?= GridView::widget([
            'dataProvider' => $dataProvider,
            //'ajaxUpdate'       => true,
            'filterModel' => false,
            //'id'=>'gv',

            'columns' => [
                ['class' => 'yii\grid\SerialColumn'],
                ['class' => 'yii\grid\CheckboxColumn', 'checkboxOptions' => function($d) {
                    return ['value' => $d['meter_id']];
                }],
                'Meter_Serial_Number',
                'Meter_Type',
                'Sub_Division_Code',
                'Sub_Division_Name',
            ],
        ]); ?>
        </div>
        <?php Pjax::end(); ?>

        <?= $form->field($model, 'images[]')->fileInput(['multiple' => true, 'accept' => 'image/*'])?>
        <br>
        <form>
            <p>
                <a href="<?= URL::toRoute(['meteracceptanceheader/setpdf', 'id'=>$model->id])?>" name="redirect" class="btn btn-primary" id="myid">Submit</a>

                <br/>

            </p>
        </form>
    </div>
  </div>
  </section>
  <?php ActiveForm::end(); ?>
  </div>
  <?php
  $url = Url::toRoute(['/meteracceptanceheader/setpdf','id'=>$model->id]);
  $script = <<< JS
  $(document).ready(function () {  

  $(document).on('pjax:end', function() {
  $("#chk").find("input:checkbox").prop("checked", true);
  });
  $("#chk").find("input:checkbox").prop("checked", true);

  $('#myid').on('click',function(e) {


   e.preventDefault();    
   var strValue = "";        
    $('input[name="selection[]"]:checked').each(function() {

    if(strValue!=="")
        {
        strValue = strValue + " , " + this.value;

        }
    else 
       strValue = this.value;     

  });

  $.ajax({
     url: '$url',
     type: 'POST',
     dataType: 'json',
     data: {data:strValue},         
     success: function(data) {
        alert(data);
     }
  });
  }) 
  });
  JS;
  $this->registerJs($script, \yii\web\View::POS_END);
  ?>

This will allow selecting an image. Now when I try to click submit button I am getting below error

PHP Notice 'yii\base\ErrorException' with message 'Undefined offset: 0'

in E:\xampp\htdocs\inventory-web\vendor\yiisoft\yii2\db\Command.php:330

By debugging the controller code I found that the error comes at foreach ($model->images as $file) as print_r($model->images) return Array() empty.

By doing print_r($model) I got

common\models\MeterAcceptanceHeader Object ( [_attributes:yii\db\BaseActiveRecord:private] => Array ( [id] => 1 [sub_div] => 37111 [meter_type] => L.T.TOU [prepared_by] => 12 [prepared_at] => 2018-08-20 12:41:27 [updated_at] => [status] => Prepared [updated_by] => [images] => Array ( ) ) [_oldAttributes:yii\db\BaseActiveRecord:private] => Array ( [id] => 1 [sub_div] => 37111 [meter_type] => L.T.TOU [prepared_by] => 12 [prepared_at] => 2018-08-20 12:41:27 [updated_at] => [status] => Prepared [updated_by] => [images] => ) [_related:yii\db\BaseActiveRecord:private] => Array ( ) [_errors:yii\base\Model:private] => [_validators:yii\base\Model:private] => [_scenario:yii\base\Model:private] => default [_events:yii\base\Component:private] => Array ( ) [_behaviors:yii\base\Component:private] => Array ( ) )

I have used the same process in other modules as well and it works properly.

How can I get rid of this problem?

Update 1

<?php

use yii\helpers\Html;
use yii\grid\GridView;
use yii\helpers\Url;
use app\models\User;
use yii\widgets\DetailView;
use yii\widgets\ActiveForm;
use yii\widgets\Pjax;
use kartik\select2\Select2;
use kartik\file\FileInput;


/* @var $this yii\web\View */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = $model->id;
$this->title = 'Meter Acceptance Form';
$this->params['breadcrumbs'][] = $this->title;
?>
<section class="content-header">
   <h1>Meter Acceptance</h1>
</section>
<div class="map-meters-form" id="doc">

<section class="content">
    <div class="box">
        <div id="chk" class="box-body">
            <?php Pjax::begin(); ?>
            <?=
            DetailView::widget([
                'model' => $model,
                'attributes' => [
                    [
                        'label' => 'Serial #',
                        'value' => function($d){
                            return $d->id;
                        }
                    ],
                    [
                        'label' => 'Meter Type',
                        'value' => function ($d){
                            if( is_object($d) )
                                return $d->meter_type;
                            return ' - ';
                        },
                    ],
                    'sub_div',
                    [
                        'label' => 'Sub Division Name',
                        'value' => function ($d){
                            if( is_object($d) )
                                return $d->subDiv->name;
                            return '-';
                        },
                    ],
                    [
                        'label' => 'Prepared By',
                        'value' => function ($d){
                            if( is_object($d) )
                                return $d->prepared->name;
                        },
                    ],
                    'prepared_at',
                    'status',
                ],
            ])
            ?>
            <br>
            <div class="pre-scrollable">
                <?=
                GridView::widget([
                    'dataProvider' => $dataProvider,
                    //'ajaxUpdate'       => true,
                    'filterModel' => false,
                    //'id'=>'gv',
                    'columns' => [
                        ['class' => 'yii\grid\SerialColumn'],
                        ['class' => 'yii\grid\CheckboxColumn', 'checkboxOptions' => function($d){
                            return ['value' => $d['meter_id']];
                        }],
                        'Meter_Serial_Number',
                        'Meter_Type',
                        'Sub_Division_Code',
                        'Sub_Division_Name',
                    ],
                ]);
                ?>
            </div>
            <?php Pjax::end(); ?>
            <?php
            $form = ActiveForm::begin(['id' => 'map-form', 'enableClientValidation' => true, 'enableAjaxValidation' => false,
                'options' => ['enctype' => 'multipart/form-data']])
            ?>
            <?=$form->field($model, 'images[]')->fileInput(['multiple' => true, 'accept' => 'image/*']) ?>
            <br>
            <p>
                <a href="<?=URL::toRoute(['meteracceptanceheader/setpdf', 'id' => $model->id]) ?>" name="redirect" class="btn btn-primary" id="myid">Submit</a>

                <br/>

            </p>
            <?php ActiveForm::end(); ?>
        </div>
    </div>
</section>
</div>
<?php
$url = Url::toRoute(['/meteracceptanceheader/setpdf','id'=>$model->id]);
$script = <<< JS
$(document).ready(function () {  

$(document).on('pjax:end', function() {
       $("#chk").find("input:checkbox").prop("checked", true);
});
      $("#chk").find("input:checkbox").prop("checked", true);

   $('#myid').on('click',function(e) {

  e.preventDefault();    

 //START Append form data
  var data = new FormData();

  var files= $('input[name="MeterAcceptanceHeader[images][]"]')[0].files;

  //append files
  $.each(files,function(index,file){
      data.append("MeterAcceptanceHeader[images][]",file,file.name);
  });


 var strValue = "";        
    $('input[name="selection[]"]:checked').each(function() {

    if(strValue!=="")
        {
        strValue = strValue + " , " + this.value;

        }
    else 
       strValue = this.value;     

});
    //alert(strValue);

    //append your query string to the form data too
  data.append('data',strValue);

  //END append form data
  $.ajax({
     url: '$url',
     type: 'POST',
     dataType: 'json',
      contentType: false,
      processData: false,
     data: {data:strValue},         
     success: function(data) {
        alert(data);
     }
     });

     }) 
     });
     JS;
     $this->registerJs($script, \yii\web\View::POS_END);
     ?>

Any help would be highly appreciated.

Muhammad Omer Aslam
  • 22,976
  • 9
  • 42
  • 68
Moeez
  • 494
  • 9
  • 55
  • 147

1 Answers1

9

What looks like that you are trying to submit an image via Ajax call when you click on submit button in the step 2 for image upload as you are binding the click to the #myid which is the anchor button

<a href="<?= URL::toRoute(['meteracceptanceheader/setpdf', 'id'=>$model->id])?>" name="redirect" class="btn btn-primary" id="myid">Submit</a>

And if you are trying to send the image via ajax you need to use the FormData interface.

The FormData interface provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data".

But before I address how to do it, you need to look into some other issues related to Form2 view.

  • You are nesting the 2 forms which is technically wrong and you can't do that see Why.

  • Above all why are you creating a separate form for the submit button?

    see this line on top

    <?php $form = ActiveForm::begin(['id' => 'map-form', 'enableClientValidation' => true, 'enableAjaxValidation' => false,
    'options' => ['enctype' => 'multipart/form-data']]) ?>
    

    This is where your first form starts and the file input field

    <?= $form->field($model, 'images[]')->fileInput(['multiple' => true, 'accept' => 'image/*'])?>

    is inside this form, and on the very next line you have the anchor button that i mentioned above in the start but you are wrapping it inside a separate form and that too before you close the ActiveForm

       <form>
            <p>
                <a href="<?= URL::toRoute(['meteracceptanceheader/setpdf', 'id'=>$model->id])?>" name="redirect" class="btn btn-primary" id="myid">Submit</a>
    
                <br/>
    
            </p>
        </form>
    

    instead you are closing the ActiveForm after this form by calling <?php ActiveForm::end(); ?>. As you have seen in the previous question that you faced weird behavior with the filter inputs for the GridVew as you were wrapping the GridView inside the form, and you are repeating that same mistake here too and also nesting 2 forms within.

What i will advise you to do first

  • Remove the form that you are creating just for the anchor button, as you wont be needing it if you want to submit the image via ajax by clicking on the anchor just keep the anchor inside the main ActiveForm. And Move the ActiveForm::begin() just before the fileInput() and after the Pjax::end().

With that said you should now user FormData to upload the image via ajax and to do so you have to add these options contentType: false and processData: false inside your ajax call and use FormData.append() to append the input files to the FormData.

So your javascript for the click function will look like this, i assume the model used for the image upload is MeterAcceptanceImages

$('#myid').on('click',function(e) {
      event.preventDefault();

      //START Append form data
      let data = new FormData();

      let files= $("input[name='MeterAcceptanceImages[images][]']")[0].files;

      //append files
      $.each(files,function(index,file){
          data.append('MeterAcceptanceImages[images][]',file,file.name);
      });

      var strValue = "";
      $('input[name="selection[]"]:checked').each(function() {
           if(strValue!==""){
               strValue = strValue + " , " + this.value;
           }else{
              strValue = this.value;     
           }
      });

      //append your query string to the form data too
      data.append('data',strValue);

      //END append form data

      $.ajax({
          url: '$url',
          type: 'POST',
          dataType: 'json',
          contentType: false,
          processData: false,
          data: data,
          success: function(data) {
             alert(data);
          }
      });
});

So overall your view Form2.php should look like below

<div class="map-meters-form" id="doc">

    <section class="content">
        <div class="box">
            <div id="chk" class="box-body">
                <?php Pjax::begin(); ?>
                <?=
                DetailView::widget([
                    'model' => $model,
                    'attributes' => [
                        [
                            'label' => 'Serial #',
                            'value' => function($d){
                                return $d->id;
                            }
                        ],
                        [
                            'label' => 'Meter Type',
                            'value' => function ($d){
                                if( is_object($d) )
                                    return $d->meter_type;
                                return ' - ';
                            },
                        ],
                        'sub_div',
                        [
                            'label' => 'Sub Division Name',
                            'value' => function ($d){
                                if( is_object($d) )
                                    return $d->subDiv->name;
                                return '-';
                            },
                        ],
                        [
                            'label' => 'Prepared By',
                            'value' => function ($d){
                                if( is_object($d) )
                                    return $d->prepared->name;
                            },
                        ],
                        'prepared_at',
                        'status',
                    ],
                ])
                ?>
                <br>
                <div class="pre-scrollable">
                    <?=
                    GridView::widget([
                        'dataProvider' => $dataProvider,
                        //'ajaxUpdate'       => true,
                        'filterModel' => false,
                        //'id'=>'gv',
                        'columns' => [
                            ['class' => 'yii\grid\SerialColumn'],
                            ['class' => 'yii\grid\CheckboxColumn', 'checkboxOptions' => function($d){
                                    return ['value' => $d['meter_id']];
                                }],
                            'Meter_Serial_Number',
                            'Meter_Type',
                            'Sub_Division_Code',
                            'Sub_Division_Name',
                        ],
                    ]);
                    ?>
                </div>
                <?php Pjax::end(); ?>
                <?php
                $form = ActiveForm::begin(['id' => 'map-form', 'enableClientValidation' => true, 'enableAjaxValidation' => false,
                            'options' => ['enctype' => 'multipart/form-data']])
                ?>
                <?=$form->field($model, 'images[]')->fileInput(['multiple' => true, 'accept' => 'image/*']) ?>
                <br>
                <p>
                    <a href="<?=URL::toRoute(['meteracceptanceheader/setpdf', 'id' => $model->id]) ?>" name="redirect" class="btn btn-primary" id="myid">Submit</a>

                    <br/>

                </p>
                <?php ActiveForm::end(); ?>
            </div>
        </div>
    </section>
</div>
<?php
$url = Url::toRoute(['/meteracceptanceheader/setpdf', 'id' => $model->id]);
$script = <<< JS
  $(document).ready(function () {  

  $(document).on('pjax:end', function() {
    $("#chk").find("input:checkbox").prop("checked", true);
  });
  $("#chk").find("input:checkbox").prop("checked", true);

$('#myid').on('click',function(e) {
      event.preventDefault();

      //START Append form data
      let data = new FormData();

      let files= $("input[name='MeterAcceptanceImages[images][]']")[0].files;

      //append files
      $.each(files,function(index,file){
          data.append('MeterAcceptanceImages[images][]',file,file.name);
      });

      var strValue = "";
      $('input[name="selection[]"]:checked').each(function() {
           if(strValue!==""){
               strValue = strValue + " , " + this.value;
           }else{
              strValue = this.value;     
           }
      });

      //append your query string to the form data too
      data.append('data',strValue);

      //END append form data

      $.ajax({
          url: '$url',
          type: 'POST',
          dataType: 'json',
          contentType: false,
          processData: false,
          data: data,
          success: function(data) {
             alert(data);
          }
      });
});

  });
JS;
$this->registerJs($script, \yii\web\View::POS_END);
?>

Now if you try to print_r(UploadedFile::getInstances('images')) should show you all the images you selected and submitted to upload. To troubleshoot in case of errors while uploading of the ajax call you can see my answer i posted previously related to ajax file uploads.

Muhammad Omer Aslam
  • 22,976
  • 9
  • 42
  • 68
  • `let` is not supported – Moeez Aug 27 '18 at 04:19
  • I have used `var data = new FormData(); var files= $('input[name="MeterAcceptanceHeader[images][]"]')[0].files;` – Moeez Aug 27 '18 at 04:55
  • And applied your suggested changes. But while submitting I am getting `PHP Notice 'yii\base\ErrorException' with message 'Undefined variable: model' in E:\xampp\htdocs\inventory-web\backend\views\meteracceptanceheader\viewcreated.php:18` – Moeez Aug 27 '18 at 04:56
  • Line # 18 is `$this->title = $model->id;` – Moeez Aug 27 '18 at 05:11
  • is the above line `$this->title = $model->id;` inside the view i dont think so? and what do you mean `let` is not supported ? i have tested the solution and it works well , can you please confirm the things asked – Muhammad Omer Aslam Aug 27 '18 at 07:04
  • Let definations are not supported by current JavaScript version. I am getting this error – Moeez Aug 27 '18 at 07:19
  • @MrFaisal which browser are you using which throws the exception for `let` statement, or it is the editor that shows the error. and the error for the line `$this->title = $model->id;` does not make sense as you are overriding the title in the very next line `$this->title = 'Meter Acceptance Form';` so you can remove the line `$this->title = $model->id;` from your view. – Muhammad Omer Aslam Aug 27 '18 at 09:39
  • I have updated my `JS` now `let` is working. Furthermore, if I remove `$this->title = $model->id` then I again get the same error inside the `gridview =>` that is at line `'model' => $model,` – Moeez Aug 27 '18 at 09:47
  • @MrFaisal that is because you need to pass the `$model` from the `controller/action` to the view, that is just basic thing as you are passiong the `$model` to the `DetailView` widget – Muhammad Omer Aslam Aug 27 '18 at 10:12
  • I have already passed it and it was working previously. But after adding `contentType: false, processData: false,` it's giving me this error – Moeez Aug 27 '18 at 10:46
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/178845/discussion-between-muhammad-omer-aslam-and-mr-faisal). – Muhammad Omer Aslam Aug 27 '18 at 10:58