إطار العمل Yii2: نظام المستخدمين

إنشاء نظام المستخدمين وتنفيذ عمليات تسجيل حساب جديد وتسجيل الدخول والخروج.

عدنان توتنيمنذ 4 سنوات

في غالبية الأنظمة البرمجية لا بد من وجود نظام للمستخدمين يسمح لهم بتسجيل حساب خاص بهم من أجل الاشتراك بخدمات معينة أو التواصل مع أشخاص آخرين أو الانضمام لمجموعات ضمن النّظام أو التعليق وإبداء الإعجاب أو التفاعل مع المقالات والمنشورات، وغير ذلك من الأمور الهامّة.

 يبدأ الأمر بآليات تسجيل حسابات جديدة وتسجيل الدخول والخروج، وليس انتهاءً بإسناد الأدوار (roles) والصلاحيات (permissions) للمستخدمين حتى يتم التحكّم الكامل بكل مستخدم وما هي الصفحات أو الخدمات التي يمكن له أن يقوم بها أو يصِل إليها ويتفاعل معها.

إنّ بناء نظام المستخدمين عن طريق إطار العمل Yii2 ليس بالأمر الصعب إطلاقاً، فما عليك إلا أن تقوم ببعض الخطوات البسيطة ليتم تحقيق ذلك كما سنرى في هذا المقال.

 

أولاً سنقوم بإعداد الـ model الذي سيمثّل المستخدمين، لذلك قم بإنشاء ملف جديد ضمن مجلد models، وليكن اسمه Users (انتبه إلى أنّه يوجد model معرّف مسبقاً اسمه User، فلن نقوم باستخدامه).

سيحتوي الـ model مبدأياً الكود التالي:

<?php

namespace app\models;
use Yii;

class Users extends \yii\db\ActiveRecord {
	const SCENARIO_LOGIN = 'login';
	const SCENARIO_REGISTER = 'register';
	public $confirm_password;

    public static function tableName(){
		return 'users';
	}
	
    public function rules(){
		return [
			[['username', 'email', 'password', 'confirm_password', 'age'], 'required'],
			[['email'], 'email'],
			[['age'], 'number'],
			[['confirm_password'], 'compare', 'compareAttribute' => 'password']
        ];
	}

	public function scenarios(){
		$scenarios = [];
		$scenarios[self::SCENARIO_REGISTER] = ['username', 'email', 'password', 'confirm_password', 'age'];
		$scenarios[self::SCENARIO_LOGIN] = ['email', 'password'];
		return $scenarios;
	}

}

سريعاً .. قمنا بتعريف الـ rules التي ينبغي تحققها عند استخدام أحد السّيناريوهَين اللذين قمنا بتعريفهما وهما سيناريو تسجيل حساب جديد (حيث سيتم التحقق من جميع الحقول)، والسّيناريو الآخر هو سيناريو تسجيل الدخول (وسيتم التحقق من الخاصّيتَين email و password فقط).

لاحظ أنّنا استخدمنا القاعدة compare مع حلق confirm_password، والتي ربطناها مع الخاصّية الأخرى password (من خلال compareAttribute)؛ أي أنّ الحقل confirm_password يجب أن تكون قيمته مطابقة لقيمة الحقل password.

ملاحظة: إنّ القيمة confirm_password لا نقوم بتخزينها في قاعدة البيانات، فهي غير مستخدمة إلا عند تسجيل حساب جديد، وقيتها يجب أن تساوي قيمة password، لذلك نقوم بتعريفها ضمن الـ model على أنّها متغيّر عادي فقط (كما وضعنا في بداية الـ model التعليمة التالية public $confirm_password)، وهذا يمكّننا من التعامل معها على أنّها attribute تنتمي للـ model، ولكن غير مخزّنة في قاعدة البيانات.

 

سنقوم أيضاً بإنشاء جدول في قاعدة البيانات ليتم ربطه مع الـ model:

CREATE TABLE users (
	id INT(11) NOT NULL AUTO_INCREMENT,
	username VARCHAR(150) NOT NULL,
	email VARCHAR(150) NOT NULL,
	password VARCHAR(150) NOT NULL,
	age int(5) NOT NULL,
	PRIMARY KEY(id)
);

الـ model الذي سيتم التعامل معه لبناء نظام المستخدمين يجب أن يقوم بتضمين interface معرّفة ضمن الـ Yii2 مسبقاً، فاستخدام التوابع الموجودة في هذه الـ interface يمكّننا بشكلٍ تلقائي من تخزين الـ session الخاصّة بالمستخدم؛ أي أنّ التطبيق سيتعرّف على المستخدم الذي قام بتسجيل دخوله، وسيتعامل معه على أنّه authenticated.

هذه الـ interface موجودة في المسار التالي:

vendor\yiisoft\yii2\web\IdentityInterface.php

لذلك قم بفتح هذا الملف وستجد فيه ترويسات التوابع التي ينبغي أن تكون موجودةً في ملف الـ model الذي يقوم بتضمين هذه الـ interface.

وسترى أيضاً في هذا الملف تعريفاً لهذه التوابع موضوعاً ضمن التعليقات، لذلك يمكنك نسخ هذه التوابع مع تعريفاتها ووضعها في ملف الـ model.

لا نحتاج لاستخدام هذه التوابع بأيدينا، فالـ Yii2 ستقوم باستدعاء هذه التوابع بشكل أوتوماتيكي عند الحاجة إليها.

افتح ملف Users واجعل هذا الـ model يُضمّن الـ interface المذكورة كالتالي:

class Users extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface {
    ...
}

ثمّ قم بنسخ التوابع مع تعريفاتها وضعها في الملف، فسيصبح الـ model بأكمله كالتالي:

<?php

namespace app\models;
use Yii;

class Users extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface {
	const SCENARIO_LOGIN = 'login';
	const SCENARIO_REGISTER = 'register';
	public $confirm_password;

    public static function tableName(){
		return 'users';
	}
	
    public function rules(){
		return [
			[['username', 'email', 'password', 'confirm_password', 'age'], 'required'],
			[['email'], 'email'],
			[['age'], 'number'],
			[['confirm_password'], 'compare', 'compareAttribute' => 'password']
        ];
	}

	public function scenarios(){
		$scenarios = [];
		$scenarios[self::SCENARIO_REGISTER] = ['username', 'email', 'password', 'confirm_password', 'age'];
		$scenarios[self::SCENARIO_LOGIN] = ['email', 'password'];
		return $scenarios;
	}
	
	public static function findIdentity($id)
	{
		return static::findOne($id);
	}
	
	public static function findIdentityByAccessToken($token, $type = null)
	{
		return static::findOne(['access_token' => $token]);
	}
	
	public function getId()
	{
		return $this->id;
	}
	
	public function getAuthKey()
	{
		return $this->authKey;
	}
	
	public function validateAuthKey($authKey)
	{
	    return $this->authKey === $authKey;
	}
}

(لسنا مضطرّين لكتابة الـ implementation لجميع توابع الـ interface، فيكفينا فقط تعريف التابع findIdentity و التابع getId، بينما بقية التوابع يمكننا كتابة ترويستها فقط دون كتابة جسم التابع، ولكن لا بأس من نسخ جميع التوابع ووضعها في الـ model).

 

الآن لنفترض أنّك أتممتَ قراءة المقال وتطبيق ما سيأتي فيه، وبعدها قمتَ بإجراء تجربةٍ لعملية تسجيل الدخول مثلاً، فستلاحظ أنّ الأمر لم ينجح، فلِمَ ذلك؟

قمنا بتعريف الـ model وأسميناه Users، ولنفرض أنّك قمتَ بتسميته Admins أو Persons أو Accounts أو أياً يكن الاسم، فكلّ ذلك لن ينجح، الاسم الوحيد الذي سيتم قبوله هو User (بدون الـ s في نهايته)، وذلك لأنّ الـ Yii2 تتعرّف على هذا الاسم وتعتمد عليه من أجل بناء نظام المستخدمين، ولكن نحن لا نريد هذا الاسم، لذلك علينا تعديل الإعدادات المتعلقة بهذه الحيثية كي يتم قبول الاسم الذي نريده، ونقوم بإجراء هذا التعديل من خلال الملف config/web.php، حيث نقوم بتغيير الاسم الموجود في إعدادات الـ component الذي اسمه user، حيث نجد فيه الخاصّية identityClass ونضع فيه القيمة التي نريد، عند ذلك سيتم التعرّف على الـ model الخاص بنا لبناء نظام المستخدمين.

نبدأ الآن بإنشاء الـ actions التي تسمح لنا بتسجيل حساب جديد، وتسجيل الدخول، وتسجيل الخروج، وعرض الصفحة الشخصية للمستخدم.

سنقوم بإنشاء controller جديد باسم UsersController، وسنكتب أولاً الـ action الخاص بتسجيل حساب جديد:

public function actionRegister(){
        $user = new Users();
        $user->scenario = Users::SCENARIO_REGISTER;

        if($user->load(Yii::$app->request->post())){
            if($user->save()){
                Yii::$app->user->login($user);
                return $this->redirect(['profile']);
            }
            else{
                return var_dump($user->getErrors());
            }
        }

        return $this->render('register', ['user' => $user]);
    }   

قمنا بتعريف object من الـ Users model، واخترنا السيناريو الخاص بتسجيل حساب جديد، وثمّ في حال كان الـ request من نمط get سيتم إعادة الصفحة register التي تحوي الـ form الخاص بتسجيل بيانات المستخدم الجديد (سنقوم بإنشائها لاحقاً)، أما في حال كان الـ request من النمط post فهنا يجب تخزين الـ object في قاعدة البيانات.

ولكن قبل تخزين الـ object ينبغي علينا أن نقوم بعمل hash لكلمة المرور، فبالطبع لا ينبغي أن يتم تخزينها كما هي، لذلك سنقوم بعمل hash لها من خلال التابع المعرّف سابقاً في Yii2 وهو generatePasswordHash والذي نمرّر له الـ string التي نريد عمل hash لها.

من أجل ذلك سنقوم بعمل override للتابع beforeSave ضمن الـ model، هذا التابع يتم استدعاؤه قبل استدعاء التابع save:

//In Users.php file

    public function beforeSave($insert){
		if($this->scenario == self::SCENARIO_REGISTER){
            $hash = \Yii::$app->getSecurity()->generatePasswordHash($this->password);
            $this->password = $hash;
        }
        return parent::beforeSave($insert);
	}

السبب في تعريفنا لهذا التابع ضمن الـ model لعمل hash لكلمة المرور هو أنّه سيتم التحقق من القاعدة التي وضعناها في التابع rules والخاصّة بمطابقة حقل كلمة المرور مع حقل تأكيد كلمة المرور، وعندما تتم المقارنة بينهما سيكون أحد الحقلَين قد تمّ عمل hash له بينما الآخر لا، فسيعيد رسالة خطأ لعدم التطابق، لذلك قمنا بتعريف هذا التابع وإجراء عملية الـ hashing فيه من أجل تجاوز الأمر، فتذكّر أنّ حقل تأكيد كلمة المرور ليس موجوداً في قاعدة البيانات، ولا يعنينا عمل hash له أو التعامل معه إلا من خلال الـ form لجعل المستخدم الذي يسجّل بياناته يتأكّد من أنّ كلمة المرور التي قام بإدخالها هي ذاتها.

ملاحظة: في التابع beforeSave تحققنا من أن يكون السيناريو هو SCENARIO_REGISTER حصراً، فلولا هذه التعليمة الشرطية فسيتم عمل hash لكلمة المرور عند استخدام السيناريو SCENARIO_LOGIN أيضاً، وبالتالي سيتم عمل hash لكلمة المرور مرّة أخرى (أي عملية hash فوق الـ hash الأساسي)، وستفشل عملية تسجيل الدخول، لذلك لا نريد تنفيذ هذا الأمر إلا عند تسجيل حساب جديد فقط.

 

الآن عند استدعاء التابع save وتخزين الـ record الجديد في قاعدة البيانات سنقوم بعمل login لهذا المستخدم من خلال استدعاء التابع login المُستدعى من Yii2 وتحديداً من الـ user component الذي قمنا بإعادة تعريفه عندما قمنا بتضمين الـ IdentityInterface.

بعد استدعاء هذا التابع سيتم اعتبار المستخدم الجديد أنّه authenticated، وبعدها سيتم الانتقال لتنفيذ الـ action التي اسمها profile والتي سيتم من خلالها عرض صفحة profile التي سنقوم بإنشائها:

//In UsersController.php

    public function actionProfile(){
        return $this->render('profile');
    }

سنقوم الآن بإنشاء صفحة register في مجلد views/users لوضع form تسجيل حساب جديد فيها:

<?php
use \yii\helpers\Html;
use \yii\widgets\ActiveForm;
?>

<?php $form = ActiveForm::begin(); ?>

<?= $form->field($user, 'username')->textInput() ?>

<?= $form->field($user, 'email')->textInput() ?>

<?= $form->field($user, 'password')->passwordInput() ?>

<?= $form->field($user, 'confirm_password')->passwordInput() ?>

<?= $form->field($user, 'age')->textInput(['type' => 'number']) ?>

<?= Html::submitButton('Save', ['class' => 'btn btn-success']) ?>

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

وسنقوم بإنشاء صفحة profile في المجلد users أيضاً والتي سيتم من خلالها عرض الملف الشخصي للمستخدم، ومن أجل تجريب نجاح الأمر سنكتب فيه الكود التالي:

<h1> <?= \Yii::$app->user->identity->username ?> </h1>

لاحظ أنّنا عندما قمنا بتعريف التابع profile لم نمرّر له أي متغيّر، بينما في الكود السابق قمنا بطباعة اسم المستخدم، فعندما نقوم باستخدام التابع login سيتم تخزين بيانات المستخدم في الخاصّية Yii::$app->user->identity ونستطيع من خلالها الوصول لبيانات المستخدم الـ authenticated في أي صفحة.

الآن قم بزيارة الرابط التالي:

localhost:8080/users/register

ثمّ أدخل بيانات مستخدم جديد وسترى أنّه قام بتوجيهك إلى صفحة profile وطباعة الحقل username فيها.

 

الآن سنقوم بإنشاء الـ action المسؤول عن تسجيل الخروج، وهو action بسيط لا نحتاج فيه إلا أن نستدعي التابع logout من الـ user component:

public function actionLogout(){
    Yii::$app->user->logout();
    return $this->redirect(['...']);  //Redirect to home page for example
}

بعد تسجيل الخروج إذا قمتَ بزيارة صفحة الـ profile سيظهر لك خطأ لأنّك قمتَ باستدعاء الخاصّية username من object غير موجود (قيمته null) لأنّه لا يوجد مستخدم authenticated حالياً، وهذا دليل على نجاح عملية تسجيل الخروج وحذف الـ session من التطبيق.

 

أخيراً سنقوم بعملية login حتى يستطيع المستخدم الذي قام بإنشاء حساب في التطبيق أن يعاود تسجيل دخوله متى شاء.

سننشئ صفحة login في مجلد users لعرض الـ form الخاص بإدخال الـ email وكلمة المرور:

<?php 
use \yii\helpers\Html;
use \yii\widgets\ActiveForm;
?>

<?php $form = ActiveForm::begin(); ?>

<?= $form->field($user, 'email')->textInput() ?>

<?= $form->field($user, 'password')->passwordInput() ?>

<?= Html::submitButton('Login', ['class' => 'btn btn-success']) ?>

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

<br>

<?php

    if($user->hasErrors('failed_login')){
        echo '<div class="alert alert-danger">';
        echo $user->getErrors()['failed_login'][0];
        echo '</div>';
    }

?>

إنّ عملية تسجيل الدخول تعتمد على إيجاد المستخدم الذي يكون بريده الإلكتروني مطابقاً للبريد الإلكتروني الذي قام المستخدم بإدخاله في صفحة login.

في حال تمّ العثور على هذا المستخدم فسيتم التحقق من تطابق كلمة المرور المخزّنة في قاعدة البيانات مع كلمة المرور التي قام المستخدم بإدخالها في الـ form، وذلك طبعاً بعد أن يتم عمل hash لكلمة المرور التي قام المستخدم بإدخالها حتى تكون عملية التطابق صحيحة (لأنّ كلمة المرور المخزّنة في قاعدة البيانات قد تمّ عمل hash لها من قبل).

إذا كانت كلمتا المرور متطابقتَين فإنّ عملية تسجيل الدخول قد تحققت وسيتم توجيه المستخدم إلى صفحة profile.

في حال عدم تطابق كلمتَي المرور فسيتم إعادة رسالة خطأ تبيّن للمستخدم أنّ البريد الإلكتروني خاطئ أو كلمة المرور ليست صحيحة.

public function actionLogin(){
        $user = new Users();
        $user->scenario = Users::SCENARIO_LOGIN;

        if($user->load(Yii::$app->request->post())){
            $identity = Users::find()->where(['email' => $user->email])->one();
            if($identity && Yii::$app->getSecurity()->validatePassword($user->password, $identity->password)){
                Yii::$app->user->login($identity);
                return $this->redirect(['profile']);
            }
            else{
                $user->addError('failed_login', 'Email or password is incorrect!');
            }
        }

        return $this->render('login', ['user' => $user]);
    }

قمنا بإنشاء object من الـ Users model واخترنا السيناريو المسؤول عن عملية تسجيل الدخول (لأنّنا لا نريد التحقق إلا من الحقلَين email و password).

في حال كان الـ request من النمط get سيتم إعادة صفحة login التي فيها الـ form.

في حال كان الـ request من النمط post ستتم محاولة إيجاد مستخدم له نفس الـ email المُدخل في الـ form.

في حال العثور على المستخدم (والذي سيتم تخزين بياناته في $identity) ستتم عملية مطابقة كلمة المرور من خلال التابع validatePassword المعرّف في Yii2 والذي يأخذ وسيطَين؛ الأول هو كلمة المرور التي قام المستخدم بإدخالها (المخزّنة في $user)، والثاني هو كلمة المرور الموجودة في قاعدة البيانات (المخزّنة في $identity).

في حال التطابق سيتم استدعاء التابع login من أجل اعتبار المستخدم authenticated وتوجيهه إلى صفحة profile.

في حال عدم التطابق سيتم إضافة خطأ جديد إلى الـ $user (عن طريق التابع addError الذي يأخذ وسيطَين؛ الأول الـ key الذي يمثّل الخطأ، والثاني رسالة الخطأ التي نريد عرضها).

لذلك في صفحة login كتبنا بعد نهاية الـ form الكود المسؤول عن التحقق من وجود الخطأ الذي قمنا بإضافته يدوياً في حال عدم تطابق البيانات، وفي حال وجود هذا الخطأ ستتم طباعة رسالة الخطأ لكي يقوم المستخدم بإعادة المحاولة مجدداً.

الآن قم بزيارة المسار التالي وقم بإخال بيانات المستخدم الذي أنشأته في عملية register السابقة:

localhost:8080/users/login

وفي حال أدخلتَ بيانات خاطئة فسيتم عرض رسالة الخطأ:

وهكذا نكون قد أتممنا بناء نظام للمستخدمين بشكلٍ مبسّط، وربما في مقالٍ قادمٍ سنعالج الأمور المتعلقة بالأدوار والصلاحيات.

0
إعجاب
1165
مشاهدات
0
مشاركة
1
متابع

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !