چرا سکان آکادمی؟
افزودن قابلیت ثبت‌نام کاربران در پروژهٔ RESTful API

افزودن قابلیت ثبت‌نام کاربران در پروژهٔ RESTful API

با توجه به اینکه به هر میزان کامپوننت‌های این پروژه بیشتر از یکدیگر مجزا باشند مدیریت آن‌ها راحت‌تر است، علاوه بر کنترلر پیش‌فرض این فریمورک به نام 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 برای آن ساخت.