پیش از تکمیل کنترلر DefaultController
، مجدد نگاهی به ساختار کلاس Routing
میاندازیم که در آن اِندپوینتهای معتبر در این RESTful API را مشخص کردهایم:
<?php
namespace Core;
class Routing
{
public $routes = [
[
'route' => 'api/v1/articles',
'module' => 'Api',
'controller' => 'DefaultController',
'action' => 'index',
],
[
'route' => 'api/v1/signup',
'module' => 'Api',
'controller' => 'UserController',
'action' => 'signup',
],
[
'route' => 'api/v1/auth',
'module' => 'Api',
'controller' => 'UserController',
'action' => 'auth',
],
];
public function __construct()
{
return $this->routes;
}
}
همانطور که میبینیم، اِندپوینت اول این وظیفه را دارا است تا هر زمانی که کلاینت وارد یوآرالِ api/v1/articles
شد، اَکشن index
قرار گرفته داخل DefaultController
اجرا گردد. حال ممکن است این پرسش ایجاد گردد که «چگونه سیستم متوجه خواهد شد که متد انتخابی مثلاً POST
،GET
یا غیره است؟» که در پاسخ به این پرسش باید گفت که داخل اَکشن index
با استفاده از ساختار switch
این قضیه را هندل کردهایم که بر اساس نوع متد، تَسک مربوطه را عملی سازد.
ابتدا متدی تحت عنوان ()index
داخل کلاس DefaultController
به صورت زیر مینویسیم:
public function index($id = null)
{
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
header("Access-Control-Allow-Headers: Authorization");
switch ($this->requestMethod) {
case 'GET':
if (isset($id) && is_numeric($id)) {
$this->getArticleById($id);
} else {
$this->getAllArticles();
};
break;
case 'POST':
$this->createNewArticle();
break;
case 'PUT':
if (isset($id) && is_numeric($id)) {
$this->updateArticleById($id);
} else {
$this->throwError(406, 'The ID parameter is required.');
}
break;
case 'DELETE':
if (isset($id) && is_numeric($id)) {
$this->deleteArticleById($id);
} else {
$this->throwError(406, 'The ID parameter is required.');
}
break;
default:
break;
}
}
همانطور که میبینیم، اَکشن ()index
یک پارامتر ورودی میگیرد که به صورت پیشفرض null
است سپس داخل بدنهٔ این متد چهار هِدِر سِت کردهایم که عبارتند از:
- هِدِر
Access-Control-Allow-Origin
که مشخص میسازد چه نوع کلاینتهایی میتوانند به ریسپانس ارسالی از سمت سرور دسترسی یابند و مقدار*
نیز حاکی از آن است که هر سیستمی میتواند به ریسورسها دسترسی داشته باشد. - هِدِر
Content-Type
در ریسپانس تایپ (نوع) ریسورس ارسالی از سمت سرور را برای کلاینت مشخص میسازد که در این مثالapplication/json
نشانگر آن است که نوع محتوا جیسون است (دستورcharset=UTF-8
نیز اِنکودینگ محتوا را مشخص میسازد.) - هِدِر
Access-Control-Allow-Methods
مشخص میکند که کدام متدهای اچتیتیپی میتوانند به یک ریسورس خاص دست یابند و در این اَکشن گفتهایم که چنین امکانی برای متدهایPUT
،POST
،GET
وDELETE
فراهم است. - هِدِر
Access-Control-Allow-Headers
در ریسپانس تعیین میکند در تبادل دیتا مابین کلاینت و سرور کدام دسته از هِدِرها میتوانند مورد استفاده قرار گیرند و این در حالی است که برخی هِدِرهای رایج مانندContent-Type
یاAccept
به صورت پیشفرض فعال هستند. در مثال فوق دستور دادهایم تا امکان استفاده از هِدِر تحت عنوانAuthorization
ایجاد گردد.
داخل اَکشن ()index
از یک دستور switch
استفاده کردهایم که بر اساس پارامتر ورودیاش که پراپرتی this->requestMethod$
بوده و حاوی نوع متد ارسالی از سمت کلاینت است، مشخص میسازد برای هر متد خاص چه تَسکی میباید اجرا گردد.
نکته |
کلیهٔ متدهایی که داخل اَکشن ()index فراخوانی خواهند شد از جنس private هستند چرا که نیاز داریم تا فقط و فقط از داخل همین کلاس به آنها دسترسی داشته باشیم. |
GET
بود، با استفاده از یک دستور شرطی چک شود ببینیم که آیا پارامتر ورودی id$
سِت شده و مقدار آن یک عدد است یا خیر که اگر این گونه بود، متد ()getArticleById
فراخوانی خواهد شد و در غیر این صورت متد ()getAllArticles
فراخوانی میگردد. در کیس دوم تست کردهایم ببینیم که آیا متد ارسالی POST
است یا خیر که اگر این گونه بود، متد ()createNewArticle
فراخوانی خواهد. در کیس سوم متد PUT
را چک کردهایم که در صورت مَچ بودن این کیس، متد ()updateArticleById
کال (فراخوانی) میگردد و در نهایت به کیس آخر میرسیم که مقدار DELETE
را میسنجد که اگر این گونه بود، متد ()deleteArticleById
کال خواهد شد و چنانچه هیچ کدام از کیسهای فوق اجرا نشوند، وارد بلوک default
شده که اساساً هیچ اتفاقی داخل آن رخ نخواهد داد.بررسی متد ()getArticleById
این متد یک پارامتر ورودی تحت عنوان id$
از جنس عدد صحیح میگیرد تا بر اساس آن به جدول articles
کوئری زده و دیتای مرتبط با مقالهای با آن شناسه را بازگرداند به طوری که داریم:
private function getArticleById(int $id)
{
$articleModel = new Article((new Database())->connect());
$result = $articleModel->readById($id);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if ($result) {
$this->returnResponse(200, $result);
} else {
$this->returnResponse(200, "No article found with id of $id");
}
}
همانطور که ملاحظه میشود، ابتدا به ساکن آبجکتی تحت عنوان articleModel$
از روی کلاس Article
ساخته و به عنوان پارامتر ورودی کانستراکتور این کلاس، آبجکتی از روی کلاس Database
ساخته و متد ()connect
آن را فراخوانی نمودهایم تا از این پس مُدل Article
برای کوئری زدن از یک کانکشن با دیتابیس برخوردار باشد. سپس متغیری ساختهایم تحت عنوان result$
و مقدار آن را برابر با فراخوانی متد ()readById
روی آبجکت articleModel$
قرار دادهایم و همانطور که ملاحظه میشود، پارامتر ورودی متد ()getArticleById
که id$
است را مجدد به متد ()readById
پاس دادهایم و در ادامه متد ()fetchAll
از کلاس PDO
را روی متغیر result$
فراخوانی کرده و نتیجه را مجدد به همین متغیر اختصاص دادهایم (پارامتر ورودی PDO::FETCH_ASSOC\
این تضمین را ایجاد میکند که آرایهای از جنس Associative بازگردانده شود.) در ادامه، با استفاده از یک دستور شرطی سنجیدهایم ببینیم که آیا متغیر result$
حاوی مقداری میباشد یا خیر که اگر این گونه بود، متد ()returnResponse
را فراخوانی کرده و کد وضعیت 200 را به عنوان آرگومان اول و متغیر result$
که حاوی دیتای مقالهای با شناسهٔ مرتبط با id$
است را به عنوان آرگومان دوم آن در نظر میگیریم. برای مثال داریم:
همانطور که ملاحظه میشود، اِندپوینتی همچون api/v1/articles/1
را در نظر گرفته و متد انتخابی نیز GET
است و با کلیک روی دکمهٔ ارسال و یا فشردن دکمهٔ اینتر، خروجی فوق در معرض دیدمان قرار خواهد گرفت. حال اگر شناسهای همچون 100 که در دیتابیس وجود خارجی ندارد را وارد کنیم، وارد دستور else
شده و خروجی جیسون زیر ریترن خواهد شد:
{
"response": {
"code": "200 OK",
"message": "No article found with id of 100"
}
}
میبینیم پیامی حاکی از آن که «مقالهای با شناسهٔ 100 یافت نشد.» برای کلاینت ارسال شده است.
هشدار |
نکتهٔ مهم در ارتباط با خروجی جیسونی که در تصویر مشاهده میشود آن است که نتیجه در قالب یک Associative Array در اختیار کلاینت قرار گرفته است که کلیدهای این آرایه همان فیلدهای جدول هستند و این مسئله اِسکمای دیتابیس را در دسترس کاربرانی با نیت سوء قرار میدهد! |
با مد نظر قرار دادن هشداری که در بالا دادیم، نیاز است تا خروجی به شکلی در دسترس کلاینت قرار گیرد که کمترین اطلاعات موجود از ساختار دیتابیس فاش گردد. برای همین منظور، متد ()readById
را به صورت زیر ریفتکور میکنیم:
public function readById(int $id)
{
if (is_numeric($id) && $id > 0) {
$query = "
SELECT
$this->articlesTable.id as articleId,
title as articleTitle,
content as articleBody,
created_at as time,
category_name as catName,
first_name as authorFirstName,
last_name as authorLastName
FROM
$this->articlesTable
LEFT JOIN categories ON
$this->articlesTable.category_id = categories.id
LEFT JOIN users ON
$this->articlesTable.user_id = users.id
WHERE
$this->articlesTable.id = ?
";
$statement = $this->connection->prepare($query);
$statement->execute([$id]);
return $statement;
}
}
از کیورد as
که پیش از این با کاربردش آشنا شدیم برای تکتک ستونهای جدول استفاده کرده تا نامی دلخواه و در عین حال غیریکسان با فیلدهای جدول مربوطه انتخاب کردهایم. اگر مجدد اِندپوینت فوق را اجرا کنیم، در خروجی خواهیم داشت:
{
"response": {
"code": "200 OK",
"message": [
{
"articleId": "1",
"articleTitle": "What Is Linux?",
"articleBody": "DescriptionLinux is a family of open source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991 by Linus Torvalds. Linux is typically packaged in a Linux distribution.",
"time": "2019-05-18 09:51:41",
"catName": "Linux",
"authorFirstName": "Behzad",
"authorLastName": "Moradi"
}
]
}
}
در حقیقت، این کار یک لایهٔ امنیتی به وب سرویس اضافه میکند تا بدین طریق حدس زدن نام دقیق فیلدهای جداول دیتابیس دشوارتر گردد.
بررسی متد ()getAllArticles
سازوکار این متد نیز مشابه ()getArticleById
است با این تفاوت که در این متد قصد داریم تا کلیهٔ مقالات را از دیتابیس فراخوانی کنیم که برای همین منظور، از متدی تحت عنوان ()read
استفاده نمودهایم به طوری که داریم:
private function getAllArticles()
{
$articleModel = new Article((new Database())->connect());
$result = $articleModel->read();
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if ($result) {
$this->returnResponse(200, $result);
} else {
$this->returnResponse(200, 'No articles found.');
}
}
جهت تست، اِندپوینت api/v1/articles
را داخل نرمافزار Postman وارد کرده و روی دکمهٔ ارسال کلیک میکنیم:
در کل دو مقاله تاکنون به ثبت رسیده است که میبینیم هر دوی آنها در دسترس کلاینت قرار گرفتهاند. میبینیم که کماکان این متد اِسکمای جدول را فاش ساخته است که برای رفع این مشکل، متد ()read
موجود در مُدل Article
را نیز به صورت زیر تغییر میدهیم:
public function read()
{
$query = "
SELECT
$this->articlesTable.id as articleId,
title as articleTitle,
content as articleBody,
created_at as time,
category_name as catName,
first_name as authorFirstName,
last_name as authorLastName
FROM
$this->articlesTable
LEFT JOIN categories ON
$this->articlesTable.category_id = categories.id
LEFT JOIN users ON
$this->articlesTable.user_id = users.id;
";
$statement = $this->connection->prepare($query);
$statement->execute();
return $statement;
}
اکنون اگر مجدد کوئری فوق را اجرا کنیم، در خروجی خواهیم داشت:
{
"response": {
"code": "200 OK",
"message": [
{
"articleId": "1",
"articleTitle": "What Is Linux?",
"articleBody": "DescriptionLinux is a family of open source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991 by Linus Torvalds. Linux is typically packaged in a Linux distribution.",
"time": "2019-05-18 09:51:41",
"catName": "Linux",
"authorFirstName": "Behzad",
"authorLastName": "Moradi"
},
{
"articleId": "2",
"articleTitle": "What Is PHP?",
"articleBody": "DescriptionPHP: Hypertext Preprocessor is a general-purpose programming language originally designed for web development. It was originally created by Rasmus Lerdorf in 1994; the PHP reference implementation is now produced by The PHP Group.",
"time": "2019-05-18 09:51:41",
"catName": "PHP",
"authorFirstName": "Behzad",
"authorLastName": "Moradi"
}
]
}
}
حال فرض کنیم که صدها مقاله در جدول articles
ثبت شده باشد و این در حالی است که با رفتن به این اِندپوینت یک کوئری سنگین به دیتابیس خواهد خورد تا کلیهٔ مقالات را در دسترس کلاینت قرار دهد و اینجا است که میباید با مفهومی تحت عنوان Pagination آشنا بود که در همین راستا توصیه میکنیم به مقالهٔ آشنایی با اصول نامگذاری صحیح ریسورسها در معماری RESTful API مراجعه نمایید.
فرض کنیم که کلیهٔ تَسکهای مرتبط با متد GET
یا به عبارتی فِچ کردن مقالات از دیتابیس نیاز به لاگین بودن در سیستم ندارند و از همین روی هر کلاینتی میتواند به هر شکلی که بخواهد با استفاده از اِندپوینتهایی که برای این منظور در نظر گرفته شدهاند به مقالات دسترسی داشته باشد.
هشدار |
در عین حال توجه داشته باشیم که در حال حاضر این وب سرویس میتواند در معرض حملاتی از جنس DoS قرار گیرد بدین شکل که بیش از حد توان سرور کوئریهای سنگین برای این سرویس از جانب کلاینتهای مختلف ارسال گردد تا جایی که سرور با کمبود منابع مواجه شده و از دسترس خارج گردد. |
()auth
که در UserController
نوشتیم برای این منظور استفاده نماییم که در ادامهٔ این آموزش خواهیم دید که به چه شکل میتوان این کار را انجام داد. بررسی متد ()createNewArticle
همانطور که داخل این متد میبینیم، از دستور try
و catch
استفاده نمودهایم چرا که قصد فراخوانی کلاس JWT
را داریم و پیش از این هم گفتیم با توجه به اینکه ممکن است در حین فراخوانی این کلاس با اِسکپشنی مواجه شویم، میباید کدهای اصلی را داخل بلوک try
قرار داده و چنانچه هیچ مشکلی رخ ندهد، دستورات داخل این بلوک اجرا میشوند و در غیر این صورت وارد بلوک catch
شده و متدی تحت عنوان ()getMessage
که مرتبط با کلاس Exception
بوده و حاوی ماهیت ارور است را چاپ میکنیم:
private function createNewArticle()
{
try {
$this->validateRequest();
$this->validateParams('articleTitle', $this->request['request_params']['articleTitle'], true);
$this->validateParams('articleContent', $this->request['request_params']['articleContent'], true);
$this->validateParams('catId', $this->request['request_params']['catId'], true);
$token = $this->getBearerToken();
$payload = JWT::decode($token, parent::JWT_SECRET_KEY, ['HS256']);
$articleModel = new Article((new Database())->connect());
$this->request['request_params']['usrId'] = $payload->userId;
$isInserted = $articleModel->create($this->request['request_params']);
if ($isInserted) {
$this->returnResponse(201, 'New article added.');
} else {
$this->returnResponse(501, 'Unable to add a new article.');
}
} catch (\Exception $e) {
echo $e->getMessage();
}
}
داخل بلوک try
متدی را فراخوانی نمودهایم تحت عنوان ()getBearerToken
که هنوز داخل BaseController
ساخته نشده است که در ابتدا میباید این متد را نوشته، آن را تفسیر کنیم و در ادامه مجدد به بررسی متد ()createNewArticle
برمیگردیم. برای این منظور، در انتهای کلاس BaseController
متدی تحت عنوان ()getBearerToken
به صورت زیر مینویسیم:
public function getBearerToken()
{
$headers = apache_request_headers();
if (isset($headers['Authorization'])) {
if (preg_match('/Bearer\s(\S+)/', $headers['Authorization'], $matches)) {
return $matches[1];
}
}
$this->throwError(404, 'Token is not found');
}
گفتیم که به منظور ایجاد یک مقالهٔ جدید و یا آپدیت و حذف مقالات قدیمی نیاز به این داریم تا در وب سرویس لاگین باشیم و یکی از راهکارهایی که از آن طریق میتوان در معماری رِست هویت کاربر را مابین سرور و کلاینت ردوبدل کرد، هِدِری است تحت عنوان Authorization
و اگر توجه کرده باشیم، داخل متد ()index
نیز از دستور زیر استفاده کردیم:
header("Access-Control-Allow-Headers: Authorization");
گفتیم که Access-Control-Allow-Headers
هِدِرهایی که مجاز به استفاده از آنها خواهیم بود را مشخص میسازد و از آنجا که نیاز به استفاده از Authorization
داریم، به وب سرور دستور دادهایم که استفاده از این هِدِر مجاز است.
در زبان برنامهنویسی پیاچپی متدی تعبیه شده تحت عنوان ()apache_request_headers
که دربرگیرندهٔ کلیهٔ هِدِرهایی است که سِت شدهاند. برای همین منظور، متغیری ساختهایم تحت عنوان headers$
و این متد را به آن اختصاص داده سپس با اولین دستور شرطی چک کردهایم ببینیم که آیا داخل متغیر headers$
کلید Authorization
سِت شده است یا خیر؛ اگر این گونه بود، وارد دومین دستور شرطی میشویم و در غیر این صورت با فراخوانی متد ()throwError
و انتخاب کد وضعیتی همچون 404، پیامی با این مضمون که «توکن یافت نشد.» در دسترس کلاینت قرار میدهیم.
گفتیم که اگر متغیر headers$
داری کلید Authorization
باشد، وارد دومین دستور شرطی میشویم و همانطور که میبینیم، با استفاده از رِگولار اِسکپرشن دستور دادهایم که تابع ()preg_match
داخل ['headers['Authorization$
بگردد و هر استرینگی که پس از Bearer
قرار داشت را داخل آرایهای به نامی دلخواه همچون matches$
بریزد سپس اِلِمان یکم این آرایه را ریترن کردهایم. در واقع، آنچه قاعداً در این خانه از آرایهٔ matches$
قرار میگیرد توکنی است که در حین ارسال ریکوئست در نظر خواهیم گرفت.
در این مرحله از آموزش نیاز است تا با مفهومی تحت عنوان Bearer Authentication آشنا شویم که یکی از ویژگیهای پروتکل HTTP است که از آن طریق میتوان با استفاده از ارسال توکن در بخش هِدِر، به سرور این پیام را داد که کلاینت مذکور معتبر بوده و اجازهٔ دسترسی به برخی اِندپوینتهای خاص را دارا است به طوری که ساختار کلی و نحوهٔ استفاده از این هِدِر به صورت زیر است:
Authorization: Bearer <token>
به عبارتی، نام هِدِر Authorization
است و مقدار در نظر گرفته شده برای آن استرینگ Bearer
به علاوه یک توکن است. واژهٔ انگلیسی «Bear» به عنوان اسم به معنی «خرس» است اما به عنوان فعل به معنی «حمل کردن» است و بالتبع برای اسم فاعلِ «Bearer» میتوان معادلی همچون «حامل» یا «حملکننده» در نظر گرفت. با این توضیحات، <Bearer <token
را میتوان این گونه تفسر نمود که «به حامل این توکن اجازهٔ دسترسی داده شود.» و لازم به یادآوری است که به دلیل مسائل امنیتی، از این هِدِر صرفاً میباید در پروتکل HTTPS استفاده نمود مضاف بر اینکه درج کیورد Bearer
الزامی بوده و مشخصکنندهٔ نوع Authorization
است.
نقطهٔ مقابل Bearer Authentication، ساختاری تحت عنوان Basic Authentication است که از آن طریق کلاینت هِدِر Authorization
را به صورت زیر ارسال میکند:
Authorization: Basic ZGVtbzpwQDU1dzByZA
میبینیم که ابتدا کیورد Basic
نوشته شده تا نوع درخواست مشخص گردد سپس یک اِسپیس قرار گرفته و استرینگی به صورت username:password
که اِنکود شده باشد پس از آن قرار میگیرد (این استرینگ میباید با فرمت Base64 اِنکود گردد.) همچنین لازم به یادآوری است که Basic Authentication نیز میباید صرفاً با استفاده از HTTPS ارسال گردد تا امنیت آن افزایش یابد.
با ذکر این توضیحات، حال مجدد میتوانیم به تفسیر کدهای داخل متد ()createNewArticle
بپردازیم. داخل بلوک try
ابتدا دستور ()validateRequest
را فراخوانی کردهایم تا این اطمینان حاصل گردد که Content-Type
برابر با application/json
است سپس با فراخوانی متد ()validateParams
تضمین کردهایم که پارامترهایی همچون articleContent
،articleTitle
و catId
الزامی بوده و حتماً باید در ریکوئست ارسالی موجود باشند.
در ادامه، متغیری ساختهایم تحت عنوان token$
و مقدار آن را برابر با متد ()getBearerToken
قرار دادهایم که این وظیفه را دارا است تا از داخل هِدِرهای ارسالی، توکنی که در هِدِر Authorization
قرار گرفته است را ریترن میکند. سپس متغیری دیگری ساختهایم به نام payload$
که مقدار آن برابر با فراخوانی متد ()decode
از کلاس JWT
است که سه پارامتر ورودی میگیرد؛ پارامتر اول توکنی است که از سمت کلاینت ارسال شده، پارامتر دوم کلید سری است که پیش از این دیدیم چگونه آن را در قالب کانستنت JWT_SECRET_KEY
ذخیره سازیم و پارامتر سوم هم الگوریتم هَشینگ مورد استفاده است به طوری که یک خروجی فرضی از محتویات متغیر payload$
به صورت زیر خواهد بود:
object(stdClass)#6 (4) {
["iat"]=> int(1558166641)
["iss"]=> string(9) "localhost"
["exp"]=> int(1558170241)
["userId"]=> int(1)
}
میبینیم کلیهٔ مقادیری که داخل متد ()auth
برای ساخت یک توکن در نظر گرفته بودیم در اختیارمان قرار دارند. در ادامه، یک آبجکت از روی مُدل Article
ساخته و آن را در متغیری به نام articleModel$
ذخیره کردهایم و نیاز به توضیح نیست که کلیهٔ مُدلها برای کارکرد صحیح خود نیاز به آبجکتی از روی کلاس Database
دارند و از همین روی متد ()connect
این کلاس را فراخوانی کرده و به عنوان پارامتر ورودی کلاس Article
در نظر گرفتهایم.
در حین ساخت توکن داخل متد ()auth
دیدیم که شناسهٔ کاربر را به عنوان یکی از مقادیر Payload در نظر گرفتیم و نیاز به توضیح نیست که برای ثبت یک مقالهٔ جدید، به شناسهٔ کاربر نیاز داریم. از همین روی، کلید جدیدی تحت عنوان usrId
برای ['this->request['request_params$
ساخته و مقدار آن را برابر با payload->userId$
قرار میدهیم و در نهایت ['this->request['request_params$
را به عنوان پارامتر ورودی تابع ()create
در نظر میگیریم و آن را در متغیری تحت عنوان isAdded$
ذخیره میسازیم. اگر مقدار این متغیر برابر با true
بود، وارد بلوک if
شده و با استفاده از متد ()returnResponse
کد وضعیت 201 و پیامی با این مضمون که «مقالهٔ جدید افزوده شد.» برای کلاینت ارسال میکنیم و در غیر این صورت، وارد بلوک else
شده و کد وضعیت + پیامی مرتبط مبنی عدم موفقیت در ثبت مقالهای جدید در نظر میگیریم.
حال نیاز است تا قابلیت افزودن مقالهٔ جدید را تست کنیم که برای همین منظور، وارد نرمافزار Postman شده و تنظیمات تصویر زیر را مد نظر قرار میدهیم:
همانطور که میبینیم، ابتدا وارد اِندپوینت api/v1/auth
شده تا در وب سرویس لاگین نماییم که پس از کلیک بر روی دکمهٔ ارسال، میبینیم که توکنی در اختیارمان قرار میگیرد. حال میباید این توکن را کپی کرده و همانطور که در ادامه مشاهده میکنیم، به عنوان مقدار هِدِر Authorization
در نظر بگیریم:
در توضیح پیرامون تصویر فوق میتوان گفت که پس از انتخاب متد POST
و وارد کردن اِندپوینت api/v1/articles
، در تَب مربوط به Headers ابتدا Content-Type
را با مقدار application/json
در نظر گرفته سپس Authorization
را با مقدار Bearer
به همراه یک اِسپیس سپس توکنی که کپی کرده بودیم را وارد مینماییم. حال نیاز است تا در تَب مربوط به Body مقادیر مورد نیاز را وارد نماییم به طوری که داریم:
همانطور که میبینیم در تَب مربوط به Body جیسون زیر را وارد کردهایم:
{
"request_params": {
"articleTitle": "New article title",
"articleContent": "New article content",
"catId": "2"
}
}
که با کلیک بر روی دکمهٔ ارسال، در بخش خروجی جیسیون زیر در معرض دیدمان قرار خواهد گرفت:
{
"response": {
"code": "201 Created",
"message": "New article added."
}
}
اکنون اگر به دیتابیس مراجعه کنیم، خواهیم دید که رکورد جدیدی با شناسهٔ ۳ با مقادیر فوقالذکر ثبت شده است:
+----+---------+-------------+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+
| id | user_id | category_id | title | content | created_at |
+----+---------+-------------+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+
| 1 | 1 | 1 | What Is Linux? | DescriptionLinux is a family of open source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991 by Linus Torvalds. Linux is typically packaged in a Linux distribution. | 2019-05-18 09:51:41 |
| 2 | 1 | 2 | What Is PHP? | DescriptionPHP: Hypertext Preprocessor is a general-purpose programming language originally designed for web development. It was originally created by Rasmus Lerdorf in 1994; the PHP reference implementation is now produced by The PHP Group. | 2019-05-18 09:51:41 |
| 3 | 1 | 2 | New article title | New article content | 2019-05-18 12:42:52 |
+----+---------+-------------+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+
آنچه در ارتباط با متد ()createNewArticle
حائز اهمیت است اینکه توکن ارسالی از طرف کلاینت این تضمین را ایجاد میکند که کلاینت معتبر است اما در عین حال میتوان یک لایهٔ امنیتی بیشتر نیز به این متد به صورت زیر اضافه نمود:
private function createNewArticle()
{
try {
$this->validateRequest();
$this->validateParams('articleTitle', $this->request['request_params']['articleTitle'], true);
$this->validateParams('articleContent', $this->request['request_params']['articleContent'], true);
$this->validateParams('catId', $this->request['request_params']['catId'], true);
$token = $this->getBearerToken();
$payload = JWT::decode($token, parent::JWT_SECRET_KEY, ['HS256']);
$user = new User((new Database())->connect());
$result = $user->fetchUserById($payload->userId);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if (!$result) {
$this->throwError(401, 'No user found');
}
$articleModel = new Article((new Database())->connect());
$this->request['request_params']['usrId'] = $payload->userId;
$isInserted = $articleModel->create($this->request['request_params']);
if ($isInserted) {
$this->returnResponse(201, 'New article added.');
} else {
$this->returnResponse(501, 'Unable to add a new article.');
}
} catch (\Exception $e) {
echo $e->getMessage();
}
}
همانطور که میبینیم، آبجکتی از روی کلاس User
ساخته تا در نهایت چک کنیم ببینیم که آیا payload->userId$
موجود در توکن دستکاری شده است یا خیر به طوری که نتیجه را در متغیری تحت عنوان result$
ریخته و چنانچه چنین کاربری وجود خارجی نداشت، با استفاده از متد ()throwError
با کد وضعیتی همچون 401 و پیامی با این مضمون که «کاربر یافت نشد.» کلاینت را از غیرمعتبر بودن درخواست مطلع میسازیم و در غیر این صورت، طبق روال گذشته ادامهٔ دستورات اجرا خواهند شد.
بررسی متد ()updateArticleById
ساختار این متد تا حد بسیاری شبیه به ()createNewArticle
است با این تفاوت که در آن متد ()update
از مُدل Article
فراخوانی شده است به طوری که داریم:
private function updateArticleById(int $id)
{
try {
$this->validateRequest();
$this->validateParams('articleTitle', $this->request['request_params']['articleTitle'], true);
$this->validateParams('articleContent', $this->request['request_params']['articleContent'], true);
$this->validateParams('catId', $this->request['request_params']['catId'], true);
$token = $this->getBearerToken();
$payload = JWT::decode($token, parent::JWT_SECRET_KEY, ['HS256']);
$user = new User((new Database())->connect());
$result = $user->fetchUserById($payload->userId);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if (!$result) {
$this->throwError(401, 'No user found');
}
$articleModel = new Article((new Database())->connect());
$this->request['request_params']['usrId'] = $payload->userId;
$isUpdated = $articleModel->update($id, $this->request['request_params']);
if ($isUpdated) {
$this->returnResponse(200, 'The article updated.');
} else {
$this->returnResponse(501, 'Unable to update the article.');
}
} catch (\Exception $e) {
echo $e->getMessage();
}
}
این متد یک پارامتر ورودی تحت عنوان id$
از جنس عدد صحیح میگیرد که به نوعی شناسهٔ مقالهای است که قصد بهروزرسانی آن را داریم و همچنین این شناسه را به عنوان پارامتر اول متد ()update
در نظر گرفته و پارامتر دوم این متد نیز ['this->request['request_params$
میباشد که حاوی کلیهٔ دادههای جدیدی است که میباید جایگزین مقادیر قبلی در دیتابیس شوند.
جهت تست، اِندپوینت api/v1/articles/1
را مد نظر قرار داده، متد PUT
را در نرمافزار Postman انتخاب کرده، در تَب مربوط به Headers هِدِر Content-Type
را برابر با application/json
قرار داده و هِدِر Authorization
را برابر با استرینگ Bearer
به علاوهٔ یک اسپیس و توکنی که قبلاً به دست آوردیم کرده و در تَب مربوط به Body نیز همانطور که در تصویر زیر میبینیم، جیسونی حاوی کلیدهای مورد نیاز میسازیم:
میبینیم که در خروجی کد وضعیت و پیامی مرتبط با بهروزرسانی مقاله ریترن شده است و چنانچه به جدول articles
رجوع کنیم، میبینیم مقالهای با شناسهٔ ۱ به صورت زیر آپدیت شده است:
+----+---------+-------------+---------------------------+-----------------------------+---------------------+
| id | user_id | category_id | title | content | created_at |
+----+---------+-------------+---------------------------+-----------------------------+---------------------+
| 1 | 1 | 1 | The article title updated | The article content updated | 2019-05-18 09:51:41 |
+----+---------+-------------+---------------------------+-----------------------------+---------------------+
بررسی متد ()deleteArticleById
متدی که برای حذف یک مقاله نوشتهایم نیز تا حد بسیاری مشابه متد ()updateArticleById
است با این تفاوت که متد ()delete
مُدل Article
در آن فراخوانی شده است:
private function deleteArticleById(int $id)
{
try {
$token = $this->getBearerToken();
$payload = JWT::decode($token, parent::JWT_SECRET_KEY, ['HS256']);
$user = new User((new Database())->connect());
$result = $user->fetchUserById($payload->userId);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if (!$result) {
$this->throwError(401, 'No user found');
}
$articleModel = new Article((new Database())->connect());
$isDeleted = $articleModel->delete($id);
if ($isDeleted) {
$this->returnResponse(200, 'The article deleted.');
} else {
$this->returnResponse(501, 'Unable to delete the article.');
}
} catch (\Exception $e) {
echo $e->getMessage();
}
}
فرض کنیم قصد داریم مقالهای با شناسهٔ ۱ را از دیتابیس حذف کنیم که برای این منظور خواهیم داشت:
همانطور که میبینیم، ابتدا متد DELETE
را انتخاب نموده سپس اِندپوینت api/v1/articles/1
را در نظر گرفته و طبق روال قبل، هِدِر Authorization
را نیز با مقادیر درست سِت کردهایم به طوری که با کلیک بر روی دکمهٔ ارسال، مقالهٔ مذکور با موفقیت از دیتابیس حذف خواهد شد.
به طور کلی، در این مرحله از آموزش، کلاس DefaultController ساختاری به صورت زیر خواهد داشت:
<?php
namespace Api\Controllers;
use Api\Config\Database;
use Api\Models\Article;
use Api\Models\User;
use Core\BaseController;
use Firebase\JWT\JWT;
class DefaultController extends BaseController
{
public function index($id = null)
{
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
header("Access-Control-Allow-Headers: Authorization");
switch ($this->requestMethod) {
case 'GET':
if (isset($id) && is_numeric($id)) {
$this->getArticleById($id);
} else {
$this->getAllArticles();
};
break;
case 'POST':
$this->createNewArticle();
break;
case 'PUT':
if (isset($id) && is_numeric($id)) {
$this->updateArticleById($id);
} else {
$this->throwError(406, 'The ID parameter is required.');
}
break;
case 'DELETE':
if (isset($id) && is_numeric($id)) {
$this->deleteArticleById($id);
} else {
$this->throwError(406, 'The ID parameter is required.');
}
break;
default:
break;
}
}
private function getArticleById(int $id)
{
$articleModel = new Article((new Database())->connect());
$result = $articleModel->readById($id);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if ($result) {
$this->returnResponse(200, $result);
} else {
$this->returnResponse(200, "No article found with id of $id");
}
}
private function getAllArticles()
{
$articleModel = new Article((new Database())->connect());
$result = $articleModel->read();
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if ($result) {
$this->returnResponse(200, $result);
} else {
$this->returnResponse(200, 'No articles found.');
}
}
private function createNewArticle()
{
try {
$this->validateRequest();
$this->validateParams('articleTitle', $this->request['request_params']['articleTitle'], true);
$this->validateParams('articleContent', $this->request['request_params']['articleContent'], true);
$this->validateParams('catId', $this->request['request_params']['catId'], true);
$token = $this->getBearerToken();
$payload = JWT::decode($token, parent::JWT_SECRET_KEY, ['HS256']);
$user = new User((new Database())->connect());
$result = $user->fetchUserById($payload->userId);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if (!$result) {
$this->throwError(401, 'No user found');
}
$articleModel = new Article((new Database())->connect());
$this->request['request_params']['usrId'] = $payload->userId;
$isInserted = $articleModel->create($this->request['request_params']);
if ($isInserted) {
$this->returnResponse(201, 'New article added.');
} else {
$this->returnResponse(501, 'Unable to add a new article.');
}
} catch (\Exception $e) {
echo $e->getMessage();
}
}
private function updateArticleById(int $id)
{
try {
$this->validateRequest();
$this->validateParams('articleTitle', $this->request['request_params']['articleTitle'], true);
$this->validateParams('articleContent', $this->request['request_params']['articleContent'], true);
$this->validateParams('catId', $this->request['request_params']['catId'], true);
$token = $this->getBearerToken();
$payload = JWT::decode($token, parent::JWT_SECRET_KEY, ['HS256']);
$user = new User((new Database())->connect());
$result = $user->fetchUserById($payload->userId);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if (!$result) {
$this->throwError(401, 'No user found');
}
$articleModel = new Article((new Database())->connect());
$this->request['request_params']['usrId'] = $payload->userId;
$isUpdated = $articleModel->update($id, $this->request['request_params']);
if ($isUpdated) {
$this->returnResponse(200, 'The article updated.');
} else {
$this->returnResponse(501, 'Unable to update the article.');
}
} catch (\Exception $e) {
echo $e->getMessage();
}
}
private function deleteArticleById(int $id)
{
try {
$token = $this->getBearerToken();
$payload = JWT::decode($token, parent::JWT_SECRET_KEY, ['HS256']);
$user = new User((new Database())->connect());
$result = $user->fetchUserById($payload->userId);
$result = $result->fetchAll(\PDO::FETCH_ASSOC);
if (!$result) {
$this->throwError(401, 'No user found');
}
$articleModel = new Article((new Database())->connect());
$isDeleted = $articleModel->delete($id);
if ($isDeleted) {
$this->returnResponse(200, 'The article deleted.');
} else {
$this->returnResponse(501, 'Unable to delete the article.');
}
} catch (\Exception $e) {
echo $e->getMessage();
}
}
}
همچنین لازم به یادآوی است که از خط چهارم تا هشتم کلاسهای مورد استفاده در این کنترلر به اصطلاح use
شدهاند.
جمعبندی
در این آموزش دیدیم که به چه شکل میتوانیم کلیهٔ تَسکهای مرتبط با مُدل Article
را با استفاده از کنترلرِ DefaultController
هندل نماییم. به عبارتی، با استفاده از اِندپوینتی به آدرس api/v1/articles
با استفاده از یک دستور switch
نوع متد را مشخص نموده سپس بر آن اساس اَکشن مربوطه را اجرا نمودیم.