با توجه به اینکه به هر میزان کامپوننتهای این پروژه بیشتر از یکدیگر مجزا باشند مدیریت آنها راحتتر است، علاوه بر کنترلر پیشفرض این فریمورک به نام DefaultController
، کنترلر دیگری به نام UserController
خواهیم ساخت که این وظیفه را دارد تا ارتباط مابین کلاینت و مُدل User
را برقرار سازد که در همین راستا داخل پوشهٔ Controllers
فایلی تحت عنوان UserController.php
ساخته و آن را به صورت زیر تکمیل میکنیم:
<?php
namespace Api\Controllers;
class UserController extends \Core\BaseController
{
public function signup()
{
if ($this->requestMethod === 'POST') {
$this->validateRequest();
$user = new \Api\Models\User((new \Api\Config\Database())->connect());
$isAdded = $user->create($this->request['request_params']);
if ($isAdded) {
$this->returnResponse(201, 'New user added.');
} else {
$this->returnResponse(406, 'This email has already been taken.');
}
} else {
$this->throwError(405, 'Method Not Allowed. The only method that is acceptable is POST.');
}
}
}
پس از ثبت نِیماِسپیس، با استفاده از کیورد extends
دستور دادهایم تا کلاس UserController
کلیهٔ خصوصیات خود را از کلاس BaseController
به ارث ببرد. سپس داخل این کلاس متدی تحت عنوان ()signup
ایجاد کردهایم و همانطور که در توضیح کلاس Routing
دیدیم، به محض آنکه کاربر یوآرال api/v1/signup
را وارد سازد، سیستم بر اساس کنترلر و اَکشن در نظر گرفته شده وارد کلاس UserController
شده سپس متد ()signup
را فراخوانی میکند. در عین حال، به منظور کوتاهتر شدن نام کلاسهای مورد استفاده، میتوان کلاس فوق را به صورت زیر ریفکتور کرد:
<?php
namespace Api\Controllers;
use Api\Config\Database;
use Api\Models\User;
use Core\BaseController;
class UserController extends BaseController
{
public function signup()
{
if ($this->requestMethod === 'POST') {
$this->validateRequest();
$user = new User((new Database())->connect());
$isAdded = $user->create($this->request['request_params']);
if ($isAdded) {
$this->returnResponse(201, 'New user added.');
} else {
$this->returnResponse(406, 'This email has already been taken.');
}
} else {
$this->throwError(405, 'Method Not Allowed. The only method that is acceptable is POST.');
}
}
}
همانطور که میبینیم، به جای درج نام کامل کلاسها در حین ساخت آبجکت، ابتدا به ساکن کلاسهای مذکور را خارج از بدنهٔ کلاس UserController
به اصطلاح use
کردهایم سپس در حین ساخت یک آبجکت جدید از روی هر کدام از کلاسها، صرفاً نام کلاس را درج کردهایم.
داخل متد ()signup
ابتدا با استفاده از یک دستور شرطی چک کردهایم ببینیم که آیا مقدار پراپرتی requestMethod$
که از کلاس BaseController
به ارث بردهایم برابر با متد POST
است یا خیر که اگر این گونه بود، وارد بلوک if
میشویم و در غیر این صورت وارد else
شده و با استفاده از متد موجود در BaseController
تحت عنوان ()throwError
کد وضعیتی همچون 405 به همراه پیامی واضح و گویا مبنی بر اینکه «تنها متد قابلقبول POST
است.» در معرض دید کاربر قرار میدهیم.
چنانچه فرض کنیم که جواب این شرط true
باشد، داخل بلوک if
ابتدا متد ()validateRequest
را فراخوانی میکنیم با این توضیح که این متد وظیفه دارد تا بر اساس پارامترهای ارسالی از طرف کلاینت، پراپرتی موجود در کلاس BaseController
تحت عنوان requestParams$
را مقداردهی کند.
سپس یک آبجکت جدید از روی کلاس User
ساخته و آن را داخل متغیری تحت عنوان user$
ذخیره کردهایم و همانطور که در حین ساخت کلاس User
دیدیم، کانستراکتور این کلاس نیاز به آبجکتی از روی کلاس Database
خواهد داشت که برای همین منظور، به عنوان پارامتر ورودی کلاس User
آبجکتی از روی کلاس Database
ساخته و متد ()connect
آن را فراخوانی کردهایم به طوری که از این پس کلاس User
به منظور انجام عملیات CRUD، از یک کانکشن کارا با دیتابیس برخوردار است.
با فراخوانی متد ()create
روی آبجکت user$
و پاس دادن ['this->request['request_params$
به آن، نتیجهٔ ثبت یک کاربر جدید در متغیری تحت عنوان isAdded$
ذخیره میگردد و در ادامه با استفاده از یک دستور شرطی چک کردهایم ببینیم که آیا مقدار این متغیر برابر با true
میباشد یا خیر که اگر این گونه بود وارد بلوک if
شده و رسپانس 201 مبنی بر ساخت یک ریسورس جدید + پیامی مرتبط را ریترن میکنیم و در غیر این صورت نیز وارد بلوک else
شده و کد وضعیت 406 مبنی بر عملی نشدن درخواست کلاینت را بازمیگردانیم.
آموزش نحوهٔ تست کردن UserController
در این مرحله از آموزش قصد داریم تا با ساخت یک کاربر جدید از طریق این وب سرویس، عملکرد پروژهٔ خود را تست نماییم. در آموزش ابزارهای مورد استفاده جهت تست API گفتیم که میتوانیم از پلاگینی تحت عنوان Postman برای مرورگر گوگل کروم جهت تست ایپیآی خود استفاده نماییم که در ادامه با نحوهٔ بهکارگیری آن آشنا خواهیم شد. پس از نصب پلاگین Postman از این لینک، محیط این نرمافزار به صورت زیر میباشد:
حال قصد داریم تا اِندپوینتی همچون api/v1/signup
که به منظور ساخت یک کاربر جدید مورد استفاده قرار میگیرد را وارد کرده، پراپرتیهای مورد نیاز را در نظر گرفته و در نهایت با ارسال یک درخواست کامل به ایپیآی خود، یک کاربر جدید در دیتابیس ثبت نماییم:
با کلیک بر روی دکمهٔ Send میبینیم که اروری با کد وضعیت 405 و پیامی با این مضمون که «فقط متد POST
قابلقبول است.» مواجه شدهایم. در واقع، همانطور که در بلوک کد زیر میبینیم:
public function signup()
{
if ($this->requestMethod === 'POST') {
$this->validateRequest();
$user = new User((new Database())->connect());
$isAdded = $user->create($this->request['request_params']);
if ($isAdded) {
$this->returnResponse(201, 'New user added.');
} else {
$this->returnResponse(406, 'This email has already been taken.');
}
} else {
$this->throwError(405, 'Method Not Allowed. The only method that is acceptable is POST.');
}
}
پیش از هر چیز با استفاده از یک دستور شرطی گفتهایم که اگر متد POST
نبود، چنین اروری نمایش داده شود. برای رفع این ارور، روی لیست متدها کلیک کرده و از میان آنها گزینهٔ POST را انتخاب کرده و روی دکمهٔ ارسال کلیک میکنیم به طوری که خواهیم داشت:
میبینیم که مجدد با اروری به همراه کد وضعیت 403 مواجه شدیم و متن پیام نیز حاکی از آن است که «فقط فرمت جیسون قابلقبول میباشد.» و علت این بروز چنین اروری آن است که داخل متد ()validateRequest
شرطی به صورت زیر در نظر گرفتهایم:
public function validateRequest()
{
if ($_SERVER['CONTENT_TYPE'] != 'application/json') {
$this->throwError(403, 'The only acceptable content type is application/json.');
}
if (!isset($this->request['request_params']) || !is_array($this->request['request_params'])) {
$this->throwError(400, 'The parameters of the request are not defined.');
} else {
$this->requestParams = $this->request['request_params'];
}
}
همانطور که میبینیم، در اولین دستور شرطی گفتهایم که اگر فرمت ریکوئست ارسالی برابر با application/json
نبود، اروری با کد وضعیت 403 در معرض دید کاربر قرار گیرد که برای رفع این مشکل، تنظیمات زیر را مد نظر قرار میدهیم:
همانطور که ملاحظه میگردد، روی تَب Headers کلیک کرده و در بخش Key نام هِدِر Content-Type
را نوشته و در بخش Value نیز مقدار application/json
را مینویسیم.
به خاطر داشته باشید |
در نظر داشته باشیم که به محض نوشتن ابتدای نام هِدِر مذکور و مقدار آن، نرمافزار Postman به صورت خودکار فیلدها را پُر میکند. |
در واقع، با انجام تنظیمات فوق به ایپیآی این پیام را میرسانیم که فرمت ریکوئست (درخواست) ارسالی جیسون است و با کلیک بر روی دکمهٔ ارسال، میبینیم که اروری با کد وضعیت 400 و پیامی با این مضمون که «پارامترهای ارسالی درخواست مشخص نشدهاند.» در معرض دیدمان قرار میگیرد و این ارور از آنجا ناشی میشود که داخل اولین دستور شرطی قرار گرفته داخل متد ()signup
متد ()validateRequest
که پیش از این مورد بررسی قرار گرفت را فراخوانی کردهایم که حاوی کدهای زیر است:
public function validateRequest()
{
if ($_SERVER['CONTENT_TYPE'] != 'application/json') {
$this->throwError(403, 'The only acceptable content type is application/json.');
}
if (!isset($this->request['request_params']) || !is_array($this->request['request_params'])) {
$this->throwError(400, 'The parameters of the request are not defined.');
} else {
$this->requestParams = $this->request['request_params'];
}
}
در دومین دستور شرطی قرار گرفته داخل این متد گفتهایم که اگر کلیدی تحت عنوان request_params
داخل پراپرتی request$
سِت نشده بود و یا اگر سِت شده بود مقدار آن خالی بود و هیچ آرایهای داخل آن یافت نشد، اروی با کد وضعیت 400 در معرض دید کاربر قرار گیرد که برای رفع این مشکل نیاز است تا پارامترهای ارسالی مورد نیاز را به صورت زیر در نرمافزار Postman درج نماییم:
پس از کلیک بر روی تَب Body، میبینیم با توجه به اینکه قبلاً هِدِری تحت عنوان Content-Type
را با مقدار application/json
سِت کردهایم، در اینجا گزینهٔ JSON به صورت خودکار انتخاب شده است. سپس روی گزینهٔ raw کلیک کرده و جیسون زیر را در بخش مربوطه کپی میکنیم:
{
"request_params": {
"firstName": "Behzad",
"lastName": "Moradi",
"mail": "hi@example.com",
"pass": "123456"
}
}
پس از کلیک کردن روی دکمهٔ ارسال، میبینیم که پیامی به صورت زیر معرض دیدمان قرار میگیرد:
{
"response": {
"code": "201 Created",
"message": "New user added."
}
}
در حقیقت، هم کد وضعیت 201 و هم پیام مربوطه حاکی از آنند که دیتای ارسالی با موفقیت در جدول users
ثبت گردیده است به طوری که اگر به این جدول رجوع کنیم، خواهیم داشت:
+----+------------+-----------+----------------+----------------------------------+---------+
| id | first_name | last_name | email | password | salt |
+----+------------+-----------+----------------+----------------------------------+---------+
| 1 | Behzad | Moradi | hi@example.com | ef4a19aa7e2cfda1258c169017b23fde | 5625433 |
+----+------------+-----------+----------------+----------------------------------+---------+
اکنون قصد داریم ببینیم که اگر با همان پارامترهای ارسالی قبلی مجدد روی دکمهٔ ارسال کلیک کنیم چه اتفاقی خواهد افتاد:
میبینم که کد وضعیت 406 به همراه پیامی با این مضمون که «ایمیل انتخابی قبلاً توسط کاربر دیگری به ثبت رسیده است.» مواجه میشویم. در واقع، دلیل بروز چنین اروری کارکرد صحیح فانکشنی تحت عنوان ()emailAlreadyExists
است که گفتیم چک میکند ببیند آیا ایمیل انتخابی قبلاً در دیتابیس به ثبت رسیده است یا خیر. حال نوبت به توضیح پیرامون پارامترهای ارسالی میرسد که در بخش Body نرمافزار درج نمودیم:
{
"request_params": {
"firstName": "Behzad",
"lastName": "Moradi",
"mail": "hi@example.com",
"pass": "123456"
}
}
همانطور که میبینیم، ابتدا به ساکن کلیدی تحت عنوان request_params
ساختهایم که خود حاوی آرایهای از یکسری مقادیر است که عبارتند از mail
،lastName
،firstName
و pass
که پیش از این توضیح دادیم برای آن که کاربری با نیت سوء نتواند به اِسکمای جداول دیتابیس ما پی ببرد، هرگز از نام فیلدهای جداول برای پارامترهای ارسالی استفاده نمیکنیم. حال قصد داریم ببینیم که اگر یکی از پارامترهای فوق که برای کارکرد صحیح تابع ()create
همگی اجباری هستند را حذف کنیم، چه اتفاقی خواهد افتاد:
با حذف کلید firstName
و انتخاب ایمیلی جدید سپس کلیک بر روی دکمهٔ ارسال، با اروری به صورت زیر مواجه خواهیم شد:
Notice: Undefined index: firstName in /var/www/rest-api-blog/app/Api/Models/User.php on line 58
Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'first_name' cannot be null in /var/www/rest-api-blog/app/Api/Models/User.php:62 Stack trace: #0 /var/www/rest-api-blog/app/Api/Models/User.php(62): PDOStatement->execute(Array) #1 /var/www/rest-api-blog/app/Api/Controllers/UserController.php(15): Api\Models\User->create(Array) #2 [internal function]: Api\Controllers\UserController->signup() #3 /var/www/rest-api-blog/app/Core/App.php(47): call_user_func_array(Array, Array) #4 /var/www/rest-api-blog/public/index.php(6): Core\App->__construct() #5 {main} thrown in /var/www/rest-api-blog/app/Api/Models/User.php on line 62
با توجه به اینکه داخل متد ()create
و زمانی که متد ()execute
را اجرا میکنیم، ['data['firstName$
را به عنوان مقدار فیلد first_name
در نظر گرفته اما زمانی که این متد فراخوانی میشود هرگز چنین کلیدی در آرایهٔ data$
وجود ندارد، با ارور فوق مواجه خواهیم شد و نیاز به توضیح نیست که از هم دید یوایکسی و هم از دید امنیتی، نمایش ارورها بدین شکل اصلاً کار صحیحی نیست بلکه ارورهایی از این دست نیز میباید همچون سایر ارورها با یک کد وضعیت مناسب + متنی واضح در اختیار کاربر این وب سرویس قرار گیرد.
اساساً متد ()validateParams
این وظیفه را دارا است تا پارامترهای ارسالی به سمت وب سرویسمان را تأیید کند که برای رفع ارور فوق، متد ()signup
را به صورت زیر تکمیل میکنیم:
public function signup()
{
if ($this->requestMethod === 'POST') {
$this->validateRequest();
$this->validateParams('firstName', $this->request['request_params']['firstName'], true);
$this->validateParams('lastName', $this->request['request_params']['lastName'], true);
$this->validateParams('mail', $this->request['request_params']['mail'], true);
$this->validateParams('pass', $this->request['request_params']['pass'], true);
$user = new User((new Database())->connect());
$isAdded = $user->create($this->request['request_params']);
if ($isAdded) {
$this->returnResponse(201, 'New user added.');
} else {
$this->returnResponse(406, 'This email has already been taken.');
}
} else {
$this->throwError(405, 'Method Not Allowed. The only method that is acceptable is POST.');
}
}
همانطور که میبینیم، در چهار خط پشت سر هم متد ()validateParams
را فراخوانی کرده در هر بار نام یکی از پارامترهای ارسالی اجباری را برایش در نظر گرفتهایم به طوری که پارامتر سوم این متد که true
است، این تضمین را ایجاد میکند که فیلد مذکور اجباری است. حال مجدد به نرمافزار Postman بازگشته و مجدد روی دکمهٔ ارسال کلیک میکنیم به طوری که خواهیم داشت:
میبینیم که با کلیک بر روی دکمهٔ Preview با یک به اصطلاح Notice از طرف مفسر پیاچپی مواجه شدهایم که هدفش اطلاعرسانی این مورد است که اندیسی تحت عنوان firstName
وجود ندارد که برای رفع این هشدار، تابع فوق را به صورت زیر ریفتکور میکنیم:
public function signup()
{
if ($this->requestMethod === 'POST') {
$this->validateParams('firstName', @$this->request['request_params']['firstName'], true);
$this->validateParams('lastName', @$this->request['request_params']['lastName'], true);
$this->validateParams('mail', @$this->request['request_params']['mail'], true);
$this->validateParams('pass', @$this->request['request_params']['pass'], true);
$this->validateRequest();
$user = new User((new Database())->connect());
$isAdded = $user->create($this->request['request_params']);
if ($isAdded) {
$this->returnResponse(201, 'New user added.');
} else {
$this->returnResponse(406, 'This email has already been taken.');
}
} else {
$this->throwError(405, 'Method Not Allowed. The only method that is acceptable is POST.');
}
}
در حقیقت، پیش از نام پارامتر دوم متد ()validateParams
از علامت @
استفاده کردهایم که در زبان پیاچپی این امکان را در اختیارمان میگذارد تا دیگر هشدارهایی از این دست نشان داده نشوند به طوری که اگر مجدد روی دکمهٔ ارسال کلیک کنیم، با خروجی زیر مواجه خواهیم شد:
میبینیم که از این پس فیلدهای الزامی هم به خوبی هندل شدهاند و چنانچه کاربر این وب سرویس حداقل فیلدهای اجباری را در حین استفاده برای سرور ارسال نکند، پیامی گویا و شفاف در معرض دیدش قرار خواهد گرفت.
جمعبندی
پس از ساخت UserController
، در این آموزش اقدام به پیادهسازی اَکشنی تحت عنوان ()signup
نمودیم که پس از مراجعه به اِندپوینت api/v1/signup
فراخوانی خواهد شد. همچنین دیدیم که به چه شکل میتوان اطمینان حاصل کرد که کلاینت حداقل فیلدهای ضروری برای عملی کردن پروسهٔ ثبتنام را در درخواست خود بگنجاند. در ادامهٔ تکمیل این کنترلر، در آموزش بعد قصد داریم ببینیم که به چه شکل میتوان کاربر را به سیستم لاگین کرده و یک JWT برای آن ساخت.