Trying To Get Property Of Non-Object

I’m trying to do scenario 2 on Kartik’s wiki for eager load relations on gridview widget. I followed it exactly and I’m getting ‘Trying to get property of non-object’. I was able to get it working on another model very quickly, but this one is being stubborn. Can’t seem to get it.

So, in order to debug, I tried doing a simpler implementation for lazyload on grid:




['label'=>'Role', 'value'=>function ($model, $index, $widget) { return $model->role->role_name; }],

That returned the same error. The relation is on the user model, which since I’m using the advanced template, is located in /common/models/User. The role relation should point to the role model at /backend/models/Role. On user, I have the following 3 methods:




    public function getRole()

    {

        return $this->hasMany(Role::className(), ['id' => 'role']);

    }

       /**

       * get role name

       * 

       */

    public function getRoleName() 

    {

    return $this->role->role_name;

    }


    

    /**

    * get list of roles for dropdown

    */

    public static function getRoleList()

    {      

        $droptions = Role::find()->asArray()->all();

        return Arrayhelper::map($droptions, 'id', 'role_name');  

    }



The getRoleList method works perfectly in /backend/views/user/update, this is what makes me think the problem is related to namespace because I can see the relation is working. I have the controller, views, and search model for user in the backend folder.

I put this at the top of the Role.php file:


use common\models\User;

and this on top of User.php:


use backend\models\Role;

I think if I can get the lazy load version to work, I will probably figure what I need for the eager load version from the wiki. Anyway, at the moment, I’m stuck…

what file and what line does the error message refer to?

Thanks I appreciate your help:

in C:\var\www\yiitry\backend\views\user\index.php at line 36

refers to:




['label'=>'Role', 'value'=>function ($model, $index, $widget) { return $model->role->role_name; }],


PHP Notice – yii\base\ErrorException


Trying to get property of non-object

I set up a relationship between User and Role as I outlined in my post. I did something similar with Profile and Gender and it worked perfectly. The only differences that I can see are that User resides in Common/models/User and Role is in backend/models/Role. On the Profile/Gender relationship, they are in the same frontend/models folder. The other thing that is different is obviously the Yii template User model has code specific to it and does not include the attribute labels. I tried putting them in to see if it made a difference, but it didn’t.

There was a constant for role in the original template, but I removed it so I could use the table instead and it works perfectly until I try to use the relationship. The dropdowns for update user work perfectly, so it is referencing the relationship correctly from backend/views/user/_form. Here is the common/models/User user model:




<?php

namespace common\models;


use yii\db\ActiveRecord;

use yii\helpers\Security;

use yii\web\IdentityInterface;

use yii\db\Expression;

use backend\models\Role;

use backend\models\Status;

use yii\helpers\ArrayHelper;


/**

 * User model

 *

 * @property integer $id

 * @property string $username

 * @property string $password_hash

 * @property string $password_reset_token

 * @property string $email

 * @property string $auth_key

 * @property integer $role

 * @property integer $status

 * @property integer $created_at

 * @property integer $updated_at

 * @property string $password write-only password

 */

class User extends ActiveRecord implements IdentityInterface

{

	

	const STATUS_ACTIVE = 10;


	


    

    

    public static function tableName()

        {

                return 'user';

        }

        

      

    

	/**

	 * Creates a new user

	 *

	 * @param array $attributes the attributes given by field => value

	 * @return static|null the newly created model, or null on failure

	 */

	public static function create($attributes)

	{

		/** @var User $user */

		$user = new static();

		$user->setAttributes($attributes);

		$user->setPassword($attributes['password']);

		$user->generateAuthKey();

		if ($user->save()) {

			return $user;

		} else {

			return null;

		}

	}


	/**

	 * @inheritdoc

	 */

	public function behaviors()

	{

		return [

			'timestamp' => [

				'class' => 'yii\behaviors\TimestampBehavior',

				'attributes' => [

					ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],

					ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],

				],

                'value' => new Expression('NOW()'),

			],

		];

	}


	/**

	 * @inheritdoc

	 */

	public static function findIdentity($id)

	{

		return static::find($id);

	}


	/**

	 * Finds user by username

	 *

	 * @param string $username

	 * @return static|null

	 */

	public static function findByUsername($username)

	{

		return static::find(['username' => $username, 'status' => self::STATUS_ACTIVE]);

	}


	/**

	 * Finds user by password reset token

	 *

	 * @param string $token password reset token

	 * @return static|null

	 */

	public static function findByPasswordResetToken($token)

	{

		$expire = \Yii::$app->params['user.passwordResetTokenExpire'];

		$parts = explode('_', $token);

		$timestamp = (int)end($parts);

		if ($timestamp + $expire < time()) {

			// token expired

			return null;

		}


		return static::find([

			'password_reset_token' => $token,

			'status' => self::STATUS_ACTIVE,

		]);

	}


	/**

	 * @inheritdoc

	 */

	public function getId()

	{

		return $this->getPrimaryKey();

	}


	/**

	 * @inheritdoc

	 */

	public function getAuthKey()

	{

		return $this->auth_key;

	}


	/**

	 * @inheritdoc

	 */

	public function validateAuthKey($authKey)

	{

		return $this->getAuthKey() === $authKey;

	}


	/**

	 * Validates password

	 *

	 * @param string $password password to validate

	 * @return bool if password provided is valid for current user

	 */

	public function validatePassword($password)

	{

		return Security::validatePassword($password, $this->password_hash);

	}


	/**

	 * Generates password hash from password and sets it to the model

	 *

	 * @param string $password

	 */

	public function setPassword($password)

	{

		$this->password_hash = Security::generatePasswordHash($password);

	}


	/**

	 * Generates "remember me" authentication key

	 */

	public function generateAuthKey()

	{

		$this->auth_key = Security::generateRandomKey();

	}


	/**

	 * Generates new password reset token

	 */

	public function generatePasswordResetToken()

	{

		$this->password_reset_token = Security::generateRandomKey() . '_' . time();

	}


	/**

	 * Removes password reset token

	 */

	public function removePasswordResetToken()

	{

		$this->password_reset_token = null;

	}


	/**

	 * @inheritdoc

	 */

	public function rules()

	{

		return [

			['status', 'default', 'value' => self::STATUS_ACTIVE],

			[['status'],'in', 'range'=>array_keys($this->getStatusList())],


			['role', 'default', 'value' => 10],

			[['role'],'in', 'range'=>array_keys($this->getRoleList())],


			['username', 'filter', 'filter' => 'trim'],

			['username', 'required'],

			['username', 'string', 'min' => 2, 'max' => 255],


			['email', 'filter', 'filter' => 'trim'],

			['email', 'required'],

			['email', 'email'],

			['email', 'unique'],

		];

	}

    

    /**

    * get role relation

    * 

    */

    

    public function getRole()

    {

        return $this->hasOne(Role::className(), ['id' => 'role']);

    }

       /**

       * get role name

       * 

       */

    public function getRoleName() 

    {

    return $this->role->role_name;

    }


    

    /**

    * get list of roles for dropdown

    */

    public static function getRoleList()

    {      

        $droptions = Role::find()->asArray()->all();

        return Arrayhelper::map($droptions, 'id', 'role_name');  

    }

    

     /**

    * get status relation

    * 

    */

    

    public function getStatus()

    {

        return $this->hasMany(Status::className(), ['id' => 'status']);

    }

       /**

       * get role name

       * 

       */

    public function getStatusName() 

    {

    return $this->status->status_name;

    }


    

    /**

    * get list of roles for dropdown

    */

    public static function getStatusList()

    {      

        $droptions = Status::find()->asArray()->all();

        return Arrayhelper::map($droptions, 'id', 'status_name');  

    }

   

    

}



No need to paste all of your code here :)

looks like the problem is in the following statement: $model->role->role_name

You access role_name of the role relation. But this relation might be null when no related record is in the database.

You have to check that first:


return $model->role ? $model->role->role_name : '- no role -';

Thanks I tried it, but it’s returning the same error. In the user controller, index calls an instance of UserSearch, not User, so I’m not sure how the user model is handed in to UserSearch.

I just double-checked and the same exact code works on my other index page for frontend/views/profile/index.

So it looks to me like something is breaking because of the file structure or I’m making a crazy basic typo somewhere, but I’ve been at this for hours and don’t see the problem…

I was thinking that UserSearch expects the user model to be handed in certain way and maybe that’s not happening because UserSearch is in backend/models/search/UserSearch and User is in common/models/User. I’ve tried namespacing everything and it doesn’t complain that it can’t find the class…

Same error in same file and line?

How did you setup your gridview then that it contains null values?

btw. same problem exists in getRoleName() method in your model.

When I made your change, it returned the same exact error. I double-checked the DB and there are no null values in the records. Also, the relationship works from update and calls the correct value. In that case, it calls the getRoleList method.

I tried modifying the getRoleName method on the model as you suggested and that didn’t work either.

I only made the one line change in the widget:




<?= GridView::widget([

		'dataProvider' => $dataProvider,

		'filterModel' => $searchModel,

		'columns' => [

			['class' => 'yii\grid\SerialColumn'],


			'id',

			'username',

			//'auth_key',

			//'password_hash',

			//'password_reset_token',

			// 'email:email',

		

  ['label'=>'Role', 'value'=>function ($model, $index, $widget) { return $model->role ? $model->role->role_name : '- no role -'; }],


            //'role',

			 'status',

			// 'created_at',

			// 'updated_at',


			['class' => 'yii\grid\ActionColumn'],

		],

	]); ?>

I added some debug to the UserController:

$basicModel = User::find()->joinWith(‘role’)->asArray()->all();

and sent that along to the index view, then did:

var_dump(Arrayhelper::map($basicModel, ‘id’, ‘role_name’));

output:




array(10) { [2]=> NULL [3]=> NULL [4]=> NULL [5]=> NULL [6]=> NULL [7]=> NULL [9]=> NULL [10]=> NULL [11]=> NULL [12]=> NULL }

But all 10 users have roles that correspond to records in the DB. The MySql DB table is named role. The getRoleList method on the User model works perfectly and returns the values from the DB. I’m very new to all of this, so not sure that is a proper test.

you should better dump role and see what comes out:


var_dump(Arrayhelper::map($basicModel, 'id', 'role'));

Also please use [code] tags to wrap your code here to make it more readable.

I made your suggested change and output is as follows:


array(10) { [2]=> array(2) { ["id"]=> string(2) "20" ["role_name"]=> string(5) "Admin" } [3]=> array(2) { ["id"]=> string(2) "10" ["role_name"]=> string(4) "User" } [4]=> array(2) { ["id"]=> string(2) "10" ["role_name"]=> string(4) "User" } [5]=> array(2) { ["id"]=> string(2) "10" ["role_name"]=> string(4) "User" } [6]=> array(2) { ["id"]=> string(2) "10" ["role_name"]=> string(4) "User" } [7]=> array(2) { ["id"]=> string(2) "20" ["role_name"]=> string(5) "Admin" } [9]=> array(2) { ["id"]=> string(2) "10" ["role_name"]=> string(4) "User" } [10]=> array(2) { ["id"]=> string(2) "10" ["role_name"]=> string(4) "User" } [11]=> array(2) { ["id"]=> string(2) "10" ["role_name"]=> string(4) "User" } [12]=> array(2) { ["id"]=> string(2) "10" ["role_name"]=> string(4) "User" } }

So in this case we see that the relationship is working, but I have no idea what is wrong with the widget at this point.

You are looking on the wrong code then. error must be somewhere else. try to track it down using the stacktrace and check the affected code files and lines.

Hard to go somewhere else with it, it errors on that change and points that line:




PHP Notice – yii\base\ErrorException


Trying to get property of non-object


1. in C:\var\www\yiitry\backend\views\user\index.php at line 40

3536373839404142434445            //'auth_key',

            //'password_hash',

            //'password_reset_token',

            // 'email:email',

 

  ['label'=>'Role', 'value'=>function ($model, $index, $widget) { return $model->role->role_name; }], <--this is line 40.

 

            //'role',

             'status',

            // 'created_at',

            // 'updated_at',

2. in C:\var\www\yiitry\backend\views\user\index.php –	 yii\base\Application::handleError()

I don’t know where else to look.

When I commented out line 40, and ran my debug array from var dump, I checked the

performance profiling and can see the join to role is being made properly.





Performance Profiling

Total processing time: 1,199 ms; Peak memory: 5.0 MB.


Total 8 items.

#	Time	Duration	Category	Info

 	 	 		

1	06:42:00.353	1022.1 ms	yii\db\Connection::open	Opening DB connection: 

2	06:42:01.377	1.0 ms	yii\db\Command::query	SELECT `user`.* FROM `user` LEFT JOIN `role` ON `user`.`role` = `role`.`id`

3	06:42:01.379	16.0 ms	yii\db\Command::query	SHOW FULL COLUMNS FROM `user`

4	06:42:01.403	0.0 ms	yii\db\Command::query	SHOW CREATE TABLE `user`

5	06:42:01.404	1.0 ms	yii\db\Command::query	SELECT * FROM `role` WHERE `id` IN ('20', '10')

6	06:42:01.422	1.0 ms	yii\db\Command::query	SELECT COUNT(*) FROM `user`

7	06:42:01.425	0.0 ms	yii\db\Command::query	SELECT * FROM `user` LIMIT 20

8	06:42:01.453	1.0 ms	yii\db\Command::query	SELECT * FROM `user` WHERE `id`=2

Also did some debug on view.php since its only a single record. If I var_dump ($model->role) into the view, I get the correct answer, but when I add the role_name attribute, it fails, so that is precisely where it is breaking. The problem is that I can’t see what is wrong with the getRoleName method.




   public function getRoleName() 

    {

    return $this->role ? $this->role->role_name : '- no role -';

    }



Could it be a problem with getRole?




  public function getRole()

    {

        return $this->hasOne(Role::className(), ['id' => 'role']);

    }



The name of the attribute on User is role, which has an int value, the model it’s joining to is Role, and it is joining on id, which is also an int and id is lowercase in the db. This all looks right to me.

And here is the Role model:





namespace backend\models;

use common\models\User;

/**

 * This is the model class for table "role".

 *

 * @property integer $id

 * @property string $role_name

 *

 * @property User[] $users

 */

class Role extends \yii\db\ActiveRecord

{

	/**

	 * @inheritdoc

	 */

	public static function tableName()

	{

		return 'role';

	}


	/**

	 * @inheritdoc

	 */

	public function rules()

	{

		return [

			[['role_name'], 'string', 'max' => 45]

		];

	}


	/**

	 * @inheritdoc

	 */

	public function attributeLabels()

	{

		return [

			'id' => 'ID',

			'role_name' => 'Role Name',

		];

	}


	/**

	 * @return \yii\db\ActiveQuery

	 */

	public function getUsers()

	{

		return $this->hasOne(User::className(), ['role' => 'id']);

	}

}

  1. Do you by any chance have another model named Role inside the common folder?

  2. Can you update what happens if you change this line




 ['label'=>'Role', 'value'=>function ($model, $index, $widget) { $model->role->role_name; }],



TO JUST THIS:




'roleName',



I can confirm that I do not have a duplicate model of Role inside Common. When I changed to ‘roleName’, I got the same error. I also noticed that I have the same exact problem with the status relation.

Do you think it can be related to the fact that UserSearch model is in backend/models/search/UserSearch and User is in common/models/User. I have it namespaced and it is not complaining about not finding the class. But it sees like something could be breaking in the magic get method, this is just a guess…

Also, I created a table and model called debug and used the same exact code on the Role model to relate the two models. Role has an attribute of debug_id and Debug has an attribute of debug_name. Then I added:




['label'=>'Debug Id', 'value'=>function ($model, $index, $widget) { return $model->debug ? $model->debug->debug_name : '- no debug -'; }],




It works perfectly. I don’t think I have a typo at this point in my User model. There was a typo on Role, where I had a getUsers method that should have been getUser, but I corrected that and it made no difference.

The Role and Debug models exist in the same backend/models folder. Since there is no DebugSearch model, I can pretty much rule out the search model as the problem.

So the main differences between the working code and the not-working code are:

  1. The user model doesn’t have attribute labels

  2. The User model resides in common/models and the relations reside in a different folder, backend/models.

  3. The user model has methods unique to the user model not on the other models (which I can’t see how they would cause a conflict).

The relations on User for Role and Status exhibit the same broken behavior. I hope it’s not too obnoxious to ask, but has anyone successfully created a relation to user with the same directory structure that I am using? At this point, I’m thinking this is a bug and would like to rule that out if possible.

Problem solved. This was a tough one. I decided to test the user model by adding a new relationship to the Debug model that I set up. I simply copied and pasted the code I was using for:




 public function getDebug()

    {

        return $this->hasOne(Debug::className(), ['id' => 'debug_id']);

    }

       /**

       * get role name

       * 

       */

    public function getDebugName() 

    {

    return $this->debug ? $this->debug->debug_name : '- no role -';

    }


    

    /**

    * get list of roles for dropdown

    */

    public static function getDebugList()

    {      

        $droptions = Debug::find()->asArray()->all();

        return Arrayhelper::map($droptions, 'id', 'debug_name');  

    }



This is exactly the same as the methods for the relation to role, so imagine my surprise when it actually worked. Why would the 2 relations act differently? Didn’t make sense.

Yesterday, I had a chance to show a programmer at work the problem and the comment that stuck out when we were looking at relations was when he said that calling the User model attribute role instead of role_id was confusing.

I thought about this today and realized that the column name was the same name as the related model name and this might cause a problem. So I started by adding a column to user called role_id for testing, but it still didn’t work. It wasn’t until I renamed the existing role column to something_else that it finally worked. I took these baby steps so I could easily step backwards if I had to.

The upshot is there may be a bug in the getRelation() method if the column name of the model is the same as the related model, as it was in this case, column name role, model name role.

You might want to consider changing the default build on the advanced template to use status_id and role_id on the User model instead of status and role.

I do appreciate the assistance I received here, it was helpful, so thanks again to CEBE and Kartik. This is a great framework and a great community.

The problem is clearly in uniqueness in naming your relation and your model attribute. In your User model, you have an attribute named [font="Lucida Console"]role[/font] and you also have a relation getter named [font="Lucida Console"]getRole[/font]




/*

 * User Model

 * @property integer $role

*/

...

public function getRole()

{

    return $this->hasOne(Role::className(), ['id' => 'role']);

}



So [font="Lucida Console"]$user->role->name[/font] is bound to give an error. It is recommended using an unique relation name, that does not conflict with your attribute name. For example:




public function getRoleInfo()

{

    return $this->hasOne(Role::className(), ['id' => 'role']);

}



Now [font="Lucida Console"]$user->roleinfo->name[/font] would not give the error.

Thanks @CeBe it worked for me :)

I thinks is shorter:


return yii\helpers\ArrayHelper::getValue($model, 'role.role_name', '- no role -');

(reference)