در معماری MVC مُدل این وظیفه را دارا است تا اصطلاحاً Business Logic وب اپلیکیشن را هندل کند و پیش از این هم دیدیم که کامپوننت Api در این پروژه حاوی فولدری تحت عنوان Models است که در حال حاضر خالی میباشد. در همین راستا، در این آموزش قصد داریم تا مُدلی تحت عنوان User بسازیم که این وظیفه را دارا است تا پروسهٔ CRUD مرتبط با جدول users را مدیریت کند. برای همین منظور، داخل فولدر Models فایلی میسازیم تحت عنوان User.php و آن را به صورت زیر تکمیل میکنیم:
<?php
namespace Api\Models;
class User
{
private $connection;
private $usersTable = "users";
public function __construct($database)
{
$this->connection = $database;
}
public function fetchUserById(int $id)
{
$query = "
SELECT
*
FROM
$this->usersTable
WHERE
id = ?
";
$statement = $this->connection->prepare($query);
$statement->execute([$id]);
return $statement;
}
private function emailAlreadyExists(string $email)
{
$query = "
SELECT
*
FROM
$this->usersTable
WHERE
email = ?
";
$statement = $this->connection->prepare($query);
$statement->execute([$email]);
return $statement->fetchAll(\PDO::FETCH_ASSOC);
}
public function create(array $data)
{
if ($this->emailAlreadyExists($data['mail'])) {
return false;
} else {
$salt = rand(1000000, 9999999);
$query = "
INSERT INTO $this->usersTable
(first_name, last_name, email, password, salt)
VALUES
(:first_name, :last_name, :email, :password, :salt);
";
$statement = $this->connection->prepare($query);
return $statement->execute([
'first_name' => $data['firstName'],
'last_name' => $data['lastName'],
'email' => $data['mail'],
'password' => md5(sha1($data['pass'] . $salt)),
'salt' => $salt,
]);
}
}
public function login($email, $password)
{
$sqlToGetSalt = $this->connection->prepare("SELECT * FROM $this->usersTable WHERE email = ?");
$sqlToGetSalt->execute([$email]);
$salt = $sqlToGetSalt->fetchAll(\PDO::FETCH_ASSOC)[0]['salt'];
$hashedPassword = md5(sha1($password . $salt));
$query = "
SELECT
*
FROM
$this->usersTable
WHERE
email = ? AND password = ?
";
$statement = $this->connection->prepare($query);
$statement->execute([$email, $hashedPassword]);
return $statement;
}
}در تفسیر این کلاس میتوان گفت که ابتدا به ساکن نِیماِسپیس این فایل را مشخص ساخته سپس داخل بدنهٔ کلاس User دو پراپرتی تحت عناوین connection$ و usersTable$ ساختهایم بدین صورت که پراپرتی اول به محض ساخت یک آبجکت از روی این کلاس در داخل کانستراکتور مقداردهی خواهد شد و پراپرتی دوم نیز دارای مقدار پیشفرض users است.
بررسی متد ()fetchUserById
همانطور که از نام این متد مشخص است، ()fetchUserById این وظیفه را دارد تا بر اساس آیدی یا شناسهٔ کاربران، کلیهٔ اطلاعات یک کاربر خاص را از جدول users فراخوانی کند. داخل بدنهٔ این متد ابتدا متغیری تحت عنوان query$ ساختهایم که حاوی کدهای اسکیوال است. به محض ساخت یک آبجکت از روی کلاس User، آبجکتی از کلاس Database به پراپرتی connection$ منتسب خواهد شد که پیش از این به معرفیاش پرداختیم؛ به عبارت دیگر، این پراپرتی دربرگیرندهٔ کلیهٔ متدهای کلاس PDO خواهد بود.
با در نظر گرفتن توضیحات فوق، متغیر statement$ حاوی متدی تحت عنوان ()prepare است که روی پراپرتی connection$ یا بهتر بگوییم آبجکتی از روی کلاس Database فراخوانی شده و به عنوان پارامتر ورودی این متد نیز متغیر query$ را پاس دادهایم و در ادامه متد ()execute را روی متغیر statemet$ فراخوانی کردهایم.
آنچه در ارتباط با این متد حائز اهمیت است اینکه در دستور WHERE مقدار ستون id را برابر با یک ? قرار دادهایم و متغیر id$ را نیز به عنوان یکی از اِلِمانهای یک آرایه به متد ()execute پاس دادهایم که در واقع این تضمین ایجاد میشود که به نوعی جلوی حملات SQL Injection گرفته شود که در این ارتباط میتوانید به مقالهٔ ارتباط با دیتابیس در PHP از طریق لایبرری PDO مراجعه نمایید.
بررسی متد ()emailAlreadyExists
سازوکار متد ()emailAlreadyExists نیز تا حد زیادی شبیه به متد ()fetchUserById است با این تفاوت که به جای ستون id، کوئری روی ستون email خواهد خورد. کاربرد این متد در مواردی است که بخواهیم بسنجیم ببینیم آیا کاربری با یک ایمیل خاص قبلاً در دیتابیس ثبت شده است یا خیر که اگر این گونه بود، باید جلوی ثبت کاربر جدید گرفته شود.
بررسی متد ()create
این متد یک پارامتر ورودی تحت عنوان data$ میگیرد که میباید از جنس آرایه باشد. داخل بدنهٔ این متد ابتدا به ساکن با استفاده از یک دستور شرطی چک کردهایم ببینیم که آیا قبلاً رکوردی با ایمیلی که در کلید ['data['mail$ وجود دارد ثبت شده است یا خیر که اگر این گونه بود، مقدار false ریترن خواهد شد و در غیر این صورت وارد دستور else شده و کدهای داخل این بلوک اجرا خواهند شد. قرار است تا با استفاده از متد ()rand عددی تصاوفی مابین 1000000 تا 9999999 در متغیر salt$ ذخیره گردد و در نهایت این عدد در فیلدِ salt در جدول users ذخیره خواهد شد.
| نکته |
واژهٔ Salt در لغت به معنی «نمک» است اما در مباحث امنیتی به عنوان یک لایهٔ محافظتی محسوب میگردد بدین صورت که تا حدی میتواند جلوی حدس زدن پسورد توسط برخی حملات همچون بروت فورس را بگیرد. |
در متغیر query$ کدهای اسکیوالی مبنی بر ثبت یک رکورد جدید در دیتابیس را نوشتهایم و پس از به اصطلاح Prepare (آماده) کردن این کوئری، با استفاده از متد ()execute مقادیر متناظر فیلدهای جدول users را در نظر گرفتهایم. همانطور که میبینیم، کلیدهای آرایهٔ data$ همنام با فیلدهای دیتابیس نیستند؛ به عبارتی، در دیتابیس فیلدی به نام first_name داریم اما کلید متناظر با این فیلد در آرایهٔ data$ برابر با firstName است و اتخاذ این تصمیم بدان دلیل بوده که یک کاربر با نیت سوء نتواند از روی پراپرتیهایی که قرار است به سمت سرور ارسال کند به نام فیلدهای جدول پی ببرد!
| هشدار |
| لازم به یادآوری است که تابع ()sha1 تحت هیچ عنوان امن نیست و استفاده از آن به تنهایی برای پروسهٔ هَش کردن پسورد اصلاً توصیه نمیشود. |
در ارتباط با مقدار در نظر گرفته شده برای فیلد password نیز لازم به توضیح است که ابتدا پسورد ارسالی توسط کاربر برای این وب سرویس را با مقدار متغیر salt$ کانکت نموده سپس به عنوان پارامتر ورودی تابع ()sha1 در نظر گرفتهایم و هَشی که این تابع بازمیگرداند را به عنوان پارامتر ورودی تابع ()md5 پاس دادهایم که این کار باعث میگردد تا حدس زدن پسورد در حملات بورت فورس به نوعی دشوارتر گردد.
بررسی متد ()login
این متد همانطور که از نامش پیدا است، جهت لاگین کردن به سیستم مورد استفاده قرار خواهد گرفت. در متد ()create دیدیم که از یک عدد تصادفی برای پُر کردن مقدار فیلد salt در جدول users استفاده کردیم که این عدد در پروسهٔ هَش کردن پسورد انتخابی کاربر مورد استفاده قرار گرفت. بالتبع در پروسهٔ لاگین کردن به سیستم مجدد به این عدد تصادفی نیاز خواهیم داشت تا آن را در کنار پسورد ارسالی گذاشته، آن را هَش نموده و نتیجه را با پسورد ثبتشده در دیتابیس مقایسه کرد به طوری که اگر هر دو یکسان بودند، این بدان معنا است که کاربر پسورد صحیحی را وارد کرده است.
در همین راستا، ابتدا نیاز داریم تا به این عدد دست یابیم که برای این منظور پیش از هر چیز یک کوئری به دیتابیس میزنیم تا این عدد را به دست آوریم. برای این منظور، متغیری تحت عنوان sqlToGetSalt$ ساخته و در آن گفتهایم که کاربری را با ایمیل موجود در پارامتر ورودی این متد تحت عنوان email$ بازگرداند و در نهایت مقدار فیلد salt چنین کاربری را در متغیر salt$ ذخیره کردهایم. سپس همان الگوریتمی که در متد ()create برای هَش کردن پسورد در نظر گرفتیم را به عنوان مقدار متغیر hashedPassword$ در نظر گرفتهایم؛ به عبارتی، ابتدا متغیر salt$ را با پارامتر ورودی دوم این متد تحت عنوان password$ کانکت نموده و به عنوان پارامتر ورودی متد ()sha1 در نظر گرفتهایم سپس مقداری که این متدی بازمیگرداند را برای متد ()md5 مد نظر قرار دادهایم.
تا این مرحله از کار توانستهایم با موفقیت پسورد هَششده را بسازیم و در ادامه نیاز داریم تا مجدد کوئری دیگری به دیتابیس بزنیم و ببینیم که آیا ایمیل ارسالی + پسورد با آنچه قبلاً در جدول users به ثبت رسیده یکسان هستند یا خیر که برای این منظور متغیری ساختهایم به نام query$ و کدهای اسکیوال مربوطه را در آن درج نموده سپس متغیرهای email$ و hashedPassword$ را به عنوان اِلِمانهای آرایهٔ ورودی تابع ()execute در نظر گرفته و در نهایت هم متغیر statement$ را ریترن کردهایم.
جمعبندی
در این آموزش با نحوهٔ ساخت مُدلی آشنا شدیم که این وظیفه را خواهد داشت تا کلیهٔ تَسکهای مرتبط با کاربران را هندل کند. در واقع، کارهایی همچون فراخوانی یک کاربر بر اساس شناسه، ثبت کاربر جدید و قابلیت لاگین کردن به سیستم با استفاده از ایمیل و رمزعبور را داخل این مُدل پیادهسازی نمودهایم به طوری که از این پس به سادگی قادر خواهیم بود تا در کنترلر مربوطه از این مُدل استفاده نماییم که این موضوع را در آموزش بعد مورد بررسی قرار خواهیم داد.
