Yii Framework Forum

How to login correctly from different tables in Yii2.0


(Jamess) #1

Hi,
I am using Yii 2 basic application template on localhost. I have two tables

  1. admin
  2. employee

My need is how to login from different tables in Yii2. I referred the link https://www.yiiframework.com/wiki/864/how-to-login-from-different-tables-in-yii2.

So in my config I have foll:

'admin' => [
            'class'=>'yii\web\User',
            'identityClass' => 'app\models\Admin',
            'enableSession' => true,
        ],

		'employee' => [
            'class'=>'yii\web\User',
            'identityClass' => 'app\models\Employee',
            'enableSession' => true,
        ],

Is this configuration correct? I have correctly implemented Identity Interface methods in both Admin and Employee models as follows:

  1. Admin model
<?php

namespace app\models;

use Yii;
use app\models\Admin;
use yii\web\IdentityInterface;

class Admin extends \yii\db\ActiveRecord  implements IdentityInterface  
{
    
    public static function tableName()
    {
        return 'admin';
    }

   
    public function rules()
    {
        return [
            [['Name', 'username', 'password', 'Status'], 'required'],
            [['Name'], 'string', 'max' => 25],
            [['username', 'password', 'Status'], 'string', 'max' => 20],
            [['username'], 'unique'],
        ];
    }

    public function attributeLabels()
    {
        return [
            'AdminId' => 'Admin ID',
            'Name' => 'Name',
            'username' => 'Username',
            'password' => 'Password',
            'Status' => 'Status',
        ];
    }


	public static function findIdentity($id)
    {
        return static::findOne($id);
    }

	public static function findIdentityByAccessToken($token, $type = null)
    {
       throw new NotSupportedException();
    }

	public function getId()
    {
        return $this->AdminId;
    }

	 public function getAuthKey()
    {
        throw new NotSupportedException();
    }

	public function validateAuthKey($authKey)
    {
        throw new NotSupportedException();
    }

	public static function findByUsername($username){

		return self::findOne(['username'=>\yii\helpers\Html::encode($username)]);
	}

	public function validatePassword($password){

		return $this->password === \yii\helpers\Html::encode($password);
	}
}
  1. Employee model
<?php

namespace app\models;

use Yii;
use app\models\Employee;
use yii\web\IdentityInterface;

class Employee extends \yii\db\ActiveRecord implements IdentityInterface
{
    
    public static function tableName()
    {
        return 'employee';
    }

    
    public function rules()
    {
        return [
            [['FirstName', 'LastName', 'Age', 'username', 'password'], 'required'],
            [['Age'], 'integer'],
            [['FirstName', 'LastName'], 'string', 'max' => 20],
            [['username', 'password'], 'string', 'max' => 25],
            [['username'], 'unique'],
        ];
    }

    public function attributeLabels()
    {
        return [
            'EmpId' => 'Emp ID',
            'FirstName' => 'First Name',
            'LastName' => 'Last Name',
            'Age' => 'Age',
            'username' => 'Username',
            'password' => 'Password',
        ];
    }
	public static function findIdentity($id)
    {
        return static::findOne($id);
    }

	public static function findIdentityByAccessToken($token, $type = null)
    {
       throw new NotSupportedException();
    }

	public function getId()
    {
        return $this->EmpId;
    }

	 public function getAuthKey()
    {
        throw new NotSupportedException();
    }

	public function validateAuthKey($authKey)
    {
        throw new NotSupportedException();
    }

	public static function findByUsername($username){

		return self::findOne(['username'=>\yii\helpers\Html::encode($username)]);
	}

	public function validatePassword($password){

		return $this->password === \yii\helpers\Html::encode($password);
	}
}

Now I require that from one login form if the entered username and password are correct and from admin table, then message should be displayed as Logged in from Admin table.

If username and password are correct and from employee table, then message should be displayed as Logged In from Employee table.

What changes should I make in LoginForm model. I have the below LoginForm Model

<?php

namespace app\models;

use Yii;
use yii\base\Model;

class LoginForm extends Model
{
    public $username;
    public $password;
    public $rememberMe = true;

    private $_user = false;

    public function rules()
    {
        return [
            [['username', 'password'], 'required'],            
            ['rememberMe', 'boolean'],
            ['password', 'validatePassword'],
        ];
    }

    public function validatePassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            $user = $this->getAdmin();

            if (!$user || !$user->validatePassword($this->password)) {
                $this->addError($attribute, 'Incorrect username or password.');
            }
        }
    }

  
    public function login()
    {
        if ($this->validate()) {
            return Yii::$app->admin->login($this->getAdmin(), $this->rememberMe ? 3600*24*30 : 0);
        }
        return false;
    }

  
    public function getAdmin()
    {
        if ($this->_user === false) {
            $this->_user = Admin::findByUsername($this->username);
        }

        return $this->_user;
    }
}

After doing this all, I get the foll error as

# Invalid Configuration – [yii\base\InvalidConfigException](http://www.yiiframework.com/doc-2.0/yii-base-invalidconfigexception.html)

## User::identityClass must be set.

(Mehdi Achour) #2

Hi,

You need to adapt the LoginForm, so that instead of checking from Yii::$app->user and User, it checks both admin & employee and their respective AR classes.

getUser for example could be changed like this:

    public function getUser()
    {
        if ($this->_user === false) {
            $this->_user = Admin::findByUsername($this->username);
        }

        if (!$this->_user) {
            $this->_user = Employee::findByUsername($this->username);
        }

        return $this->_user;
    }

The rest of the class should be adapted in a similar manner. Try to do it, and if you’re stuck post back here.

PS: just saw your edit with the Error message. It’s because you’re using User instead of Admin/Employee I guess.


(Jamess) #3

Still the same error is displayed.


(Mehdi Achour) #4

It seems that you still need to configure the user component and point its identitiyClass to a valid class (\yii\web\User, or Admin, or Employee)


(Jamess) #5

What should I chage in config file?


(Mehdi Achour) #6

put back the user entry, at the same level as admin and employee


(Jamess) #7

If I have the foll in config, it works fine for employee table

'user' => [
		     
            'identityClass' => 'app\models\Admin',
            'enableSession' => true,
        ],

		'user' => [
			
            'identityClass' => 'app\models\Employee',
            'enableSession' => true,
        ],

But when I log in from admin table then, in Logout(john) employee username is displayed.(meaning it is getting logged out from employee table).


(Mehdi Achour) #8

You can’t have both entries under user key, the first one gets overriden by the second. This is why only employee works.

You should check the tutorial again and try to stick as closely possible to what it says, then adapt your LoginForm to target admin & employee instead of user.


(Jamess) #9

Ok, Thanks


(Jamess) #10

Not able to find out. If I change them again to admin and employee, still the same error is displayed. Is it possible in yii2 login from different tables?


(Schanz15) #11

While you could configure multiple user components, each using a different identity class, many parts of Yii simply assume that the application uses a single component with the name ‘user’ for authenticated users (e.g. logging, accessfilters, Url helpers, etc.). So I would strongly suggest to stick to the default user component.

That being said I would probably try to implement a separate IdentityClass and neither implement it in the User nor the Employee Model.

Here is some untested code (just to give you the idea):

config/web.php

// ...
'components' => [
        'user' => [
            'identityClass' => 'app\identity\User',
            'enableAutoLogin' => true,
        ],
]
// ...

identity/User.php:

<?php

namespace app\identity\User;

use yii\web\IdentityInterface;
use app\models\User as UserModel;
use app\models\Employee;

class User implements IdentityInterface
{
    private $_user;


    public static function findIdentity($id)
    {
        // $id will be something like "user-3" or "employee-2"
        list($type, $pk) = explode('-', $id);

        switch ($type) {
            case 'user': return UserModel::findOne($pk);
            case 'employee' return Employee::findOne($pk);
        }
    }

    public static function findIdentityByAccessToken($token, $type = null)
    {
        throw new \yii\base\NotSupportedException();
    }

    public function getId()
    {
        $prefix = $this->_user instanceof UserModel ? 'user' : 'employee';
        return $prefix . '-' . $this->_user->getPrimaryKey();
    }

    public function getAuthKey()
    {
        // Not required for stateless authentication -- otherwise you can implement that.
        throw new \yii\base\NotSupportedException();
    }

    public function validateAuthKey($authKey)
    {
        throw new \yii\base\NotSupportedException();
    }

    public function setUser(\yii\db\ActiveRecord $user)
    {
        $this->_user = $user;
    }
}

app\models\LoginForm.php

class LoginForm extends Model
{
    public $username;
    public $password;
    public $rememberMe = true;

    public function validatePassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            $user = $this->getUser();

            if (!$user || !$user->validatePassword($this->password)) {
                $this->addError($attribute, 'Incorrect username or password.');
            }
        }
    }

    public function login()
    {
        if ($this->validate()) {
            $userIdentity = new \app\identity\User(['user' => $this->getUser()]);
            return Yii::$app->user->login($userIdentity, $this->rememberMe ? 3600*24*30 : 0);
        }
        return false;
    }

    public function getUser()
    {
        if ($this->_user === false) {
            $this->_user = // Logic to create instance of User or Employee, based in $this->username or null if neither could be found.
        }

        return $this->_user;
    }
}

(Jamess) #12
  1. Should I crate a folder named identity in htdocs/basic/
  2. And in identity folder should I create User.php file.?
  3. I should remove the Identity Interface implemented methods from Admin and Employee model?
  4. And in Login form getUser() is the below code sufficient?
public function getUser()
    {
        if ($this->_user === false) {
            $this->_user = Admin::findByUsername($this->username);
        }

        if (!$this->_user) {
            $this->_user = Employee::findByUsername($this->username);
        }

        return $this->_user;
    }

(Schanz15) #13

To all of your questions: Yes :).

Regarding 1 & 2: You are free to put the file wherever you want and name it however you like – its just some name that i came up with.

Let me know whether it worked out :slight_smile:


(Jamess) #14

Hi,
It is giving me error as:

Error
Call to undefined method app\models\Admin::findByUsername()

public function getUser()
    {
        if ($this->_user === false) {
            $this->_user = Admin::findByUsername($this->username);
        }
 
        if (!$this->_user) {
            $this->_user = Employee::findByUsername($this->username);
        }
 
        return $this->_user;
    }

I dont have user table. I have only admin and employee table. So should I make changes as admin instead of user in User.php?

The scenario is as: I have admin and employee table,

In admin table AdminId is set to primary key and autoincremented. Similarly for employee table EmpId is primary key and auto incremented. In both tables I have username and password fields where username is set to Unique.

The Identity Interface methods that I implemented for Admin model was follows

public static function findIdentity($id)
    {
        return static::findOne($id);
    }

	public static function findIdentityByAccessToken($token, $type = null)
    {
       throw new NotSupportedException();
    }

	public function getId()
    {
        return $this->AdminId;
    }

	 public function getAuthKey()
    {
        throw new NotSupportedException();
    }

	public function validateAuthKey($authKey)
    {
        throw new NotSupportedException();
    }

	public static function findByUsername($username){

		return self::findOne(['username'=>\yii\helpers\Html::encode($username)]);
	}

	public function validatePassword($password){

		return $this->password === \yii\helpers\Html::encode($password);
	}

(Schanz15) #15

Yes.

Well of course you need to implement findByUsername in the Admin model:

public static function findByUsername(string $name)
{
    return static::find()->where(['username' => $name])->one();
}

(Jamess) #16

Hi, after making changes, foll error is displayed:

Call to a member function getPrimaryKey() on null

public function getId()
    {
        $prefix = $this->_user instanceof UserModel ? 'user' : 'employee';
        return $prefix . '-' . $this->_user->getPrimaryKey();
    }

(Schanz15) #17

Ohhh yeah – I forgot to extends from yii\base\BaseObject for app\identity\User.

class User extends \yii\base\BaseObject implements IdentityInterface

That should seal the deal.


(Jamess) #18

Hi, after making changes, the User.php is as follows:

<?php

namespace app\identity;

use yii\web\IdentityInterface;
use app\models\User as UserModel;
use app\models\Employee;

class User extends \yii\base\BaseObject implements IdentityInterface
{
    private $_user;


    public static function findIdentity($id)
    {
        // $id will be something like "user-3" or "employee-2"
        list($type, $pk) = explode('-', $id);

        switch ($type) {
            case 'admin': return UserModel::findOne($pk);
            case 'employee' : return Employee::findOne($pk);
        }
    }
	

    public static function findIdentityByAccessToken($token, $type = null)
    {
        throw new \yii\base\NotSupportedException();
    }

    public function getId()
    {
        $prefix = $this->_user instanceof UserModel ? 'admin' : 'employee';
        return $prefix . '-' . $this->_user->getPrimaryKey();
    }



    public function getAuthKey()
    {
        // Not required for stateless authentication -- otherwise you can implement that.
        throw new \yii\base\NotSupportedException();
    }

    public function validateAuthKey($authKey)
    {
        throw new \yii\base\NotSupportedException();
    } 
}

But there is error as :

The identity object must implement IdentityInterface.


    public function setIdentity($identity)
    {
        if ($identity instanceof IdentityInterface) {
            $this->_identity = $identity;
            $this->_access = [];
        } elseif ($identity === null) {
            $this->_identity = null;
        } else {
            throw new InvalidValueException('The identity object must implement IdentityInterface.');
        }
    }

(Schanz15) #19

I just created myself an empty application (composer create-project yii2-app-basic) and tried to implement a multi-table user login. This is what I came up with – its guaranteed to work :slight_smile:

https://pastebin.com/cuiQRvJn

The error that you are experiencing is most likely because \app\identity\User::findIdentity should return an instance of itself instead of the model class. Please see the patch file that I posted above for further details.

Dont hesitate to ask if you have any questions regarding the code.


(Jamess) #20

Hi,
After just adding use app\models\Admin while declaration at the top it is working fine.

Thanks.