Search code examples
activerecordyii2many-to-manyactive-form

Yii2 model load does not work as expected


My question may look like the following question:

yii2 - model load function does not set some model attributes

However, the case here is different due to dealing with Many to Many relation through junction table.

For instance, I have three tables, Jobs, Eqtypes and the junction table Eqtype_jobs. I want to relate some Eqtypes to a current Job using simple activeform using multiple select dropDownList. The following is the code that I have in, the controller and the view:

//the controller code
public function actionEqtypeCreate($id)
{
  $eqtypes = \app\modules\crud\models\Eqtype::find()->all();
  $model = Job::findOne(['id' => $id]);
  if ($model->load(Yii::$app->request->post())){    
    var_dump($model->eqtypes);
    die(); // for debuging <<<<<<<<<<*>>>>>>>>
    foreach ($eqtypes as $eqt){
      $model->eqtypes->id = $eqt->id;
      $model->eqtypeJobs->eqtype_id = $eqt->eqtype_id;
      $model->save();
    }
    return $this->redirect(['index']);
  }
  return $this->render('eqtype-create', ['model' => $model, 'eqtypes' => $eqtypes]);  

}

Here the view:

//The view code i.e Form
<?php
//use Yii;
use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\helpers\ArrayHelper;

$form = ActiveForm::begin([
    'enableClientValidation' => true,
]);

echo $form->field($model, 'eqtypes')->dropDownList(ArrayHelper::map($eqtypes,'id','title'), ['multiple' => true]);

 echo Html::submitButton(
        '<span class="glyphicon glyphicon-check"></span> ' .
        ($model->isNewRecord ? Yii::t('cruds', 'Create') : Yii::t('cruds', 'Save')),
        [
        'id' => 'save-' . $model->formName(),
        'class' => 'btn btn-success'
        ]
        );      

$form->end();

Here I have a problem: the output of var_dump($model->eqtypes), just before the die() in the controller code, always returns empty array array(0) { }.

Indeed, what's making me try to debug using die(), without the debug, I have got the following error:

Indirect modification of overloaded property app\modules\crud\models\Job::$eqtypes has no effect at line 149

In my case line 149 is the first line after foreach statement in the controller code:

$model->eqtypes->id = $eqt->id;

Edit

All models are created using yii2-giiant. However the following is a partial copy of job model:

<?php
// This class was automatically generated by a giiant build task
// You should not change it manually as it will be overwritten on next build

namespace app\modules\crud\models\base;

use Yii;
abstract class Job extends \yii\db\ActiveRecord
{



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


    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['machine_id', 'product_id', 'title', 'qty'], 'required'],
            [['machine_id', 'product_id', 'qty', 'updated_by' ,'created_by'], 'integer'],
            [['created', 'started', 'completed'], 'safe'],
            [['desc'], 'string'],
            [['title', 'speed'], 'string', 'max' => 255],
            [['product_id'], 'exist', 'skipOnError' => true, 'targetClass' => \app\modules\crud\models\Product::className(), 'targetAttribute' => ['product_id' => 'id']],
            [['machine_id'], 'exist', 'skipOnError' => true, 'targetClass' => \app\modules\crud\models\Machine::className(), 'targetAttribute' => ['machine_id' => 'id']]
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => Yii::t('app', 'ID'),
            'machine_id' => Yii::t('app', 'Machine ID'),
            'created' => Yii::t('app', 'Created'),
            'started' => Yii::t('app', 'Started'),
            'completed' => Yii::t('app', 'Completed'),
            'product_id' => Yii::t('app', 'Product ID'),
            'title' => Yii::t('app', 'Title'),
            'qty' => Yii::t('app', 'Qty'),
            'speed' => Yii::t('app', 'Speed'),
            'created_by' => Yii::t('app', 'Created By'),
            'desc' => Yii::t('app', 'Desc'),
        ];
    }
 public function getEqtypeJobs()
    {
        return $this->hasMany(\app\modules\crud\models\EqtypeJob::className(), ['job_id' => 'id']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getEqtypes()
    {
        return $this->hasMany(\app\modules\crud\models\Eqtype::className(), ['id' => 'eqtype_id'])->viaTable('eqtype_jobs', ['job_id' => 'id']);
    }

I don't know, exactly, is the problem due to the miss loading of the POST data from the form due to the output of the var_dump or there is another thing missing in my code?!

So, I would like to know, why load does not works as expected? In other words var_dump($model->eqtypes) prints the stored values in the conjunction table not the submitted values from the form, and then if we could able to solve this, why the error message about the indirect modification?


Solution

  • The error you are having "Indirect modification of overloaded property" is due to the fact that you are trying to modify a property returned by "__get" magic method. Take a look at the "__get" of "yii\db\BaseActiveRecord", in short it first checks if your model has a property "eqtypes" and if not checks your model relations.

    As you understood, you cannot load relations in a model from a post and simply save it. I think in your solution, you are overshooting the problem and make your code too complex.

    I suggest you use this module: https://github.com/cornernote/yii2-linkall Install this and change your action to:

    $model = Job::findOne(['id' => $id]);
        if ($model->load(Yii::$app->request->post())){
            $postData = Yii::$app->request->post();
            $eqtypesIds = $postData['Job']['eqtypes'];
            $eqtypes = \app\models\Eqtype::findAll($eqtypesIds);
            $transaction = Yii::$app->db->beginTransaction();
            $extraColumns = []; // extra columns to be saved to the many to many table
            $unlink = true; // unlink tags not in the list
            $delete = true; // delete unlinked tags
            try {
                $model->linkAll('eqtypes', $eqtypes, $extraColumns, $unlink, $delete);
                $model->save();
    
                $transaction->commit();
            } catch (Exception $e) {
    
                $transaction->rollBack();
    
            }
            return $this->redirect(['index']);
        }
        $eqtypes = \app\models\Eqtype::find()->all();
        return $this->render('eqtype', ['model' => $model, 'eqtypes' => $eqtypes]);
    

    and add in your Job model:

    public function behaviors()
    {
        return [
            \cornernote\linkall\LinkAllBehavior::className(),
        ];
    }
    

    If you do not want to use this module, you should override the default load() and save() methods of your model to include loading and saving your relations.