سرفصل‌های آموزشی
آموزش RESTful API
تکمیل کلاس BaseController

تکمیل کلاس BaseController

همان‌طور که از نام این پروژه (rest-api-blog) مشخص است، با استفاده از این وب سرویس قصد داریم تا یک سیستم وبلاگ ساده را پیاده‌سازی نماییم به طوری که عمده‌ترین قابلیت‌های این RESTful API عبارتند از:

  • ثبت‌نام کاربر
  • لاگین
  • فراخوانی کلیهٔ‌ مقالات
  • فراخوانی مقاله بر اساس شناسه
  • ثبت مقالهٔ جدید
  • ویرایش مقالات
  • حذف مقالات

برای این منظور، نیاز به یکسری کدهای زیرساختی داریم که در همین راستا در این آموزش خواهیم دید که به چه شکل کلاس BaseController را تکمیل خواهیم نمود به طوری که تا این لحظه کلاس مذکور به صورت زیر است:

<?php
namespace Core;

class BaseController
{

}

ابتدا کدهای تکمیلی را مد نظر قرار داده سپس به تفسیر کدهای جدید که داخل این کلاس نوشته‌ شده‌اند خواهیم پرداخت:

<?php
namespace Core;

class BaseController
{
    public $requestMethod;
    public $request;
    public $requestParams;

    public function __construct()
    {
        $this->requestMethod = $_SERVER["REQUEST_METHOD"];
        $this->request = (array) json_decode(file_get_contents('php://input'), true);
    }

    public function throwError(int $statusCode, string $message)
    {
        header('Content-Type: application/json');
        echo json_encode([
            'error' => [
                'status' => $statusCode,
                'message' => $message,
            ],
        ]);
        die;
    }

    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'];
        }
    }

    public function validateParams($fieldName, $value, $isRequired = false)
    {
        if ($isRequired == true && $value == "") {
            $this->throwError(400, "The $fieldName field is required.");
        }
        return $value;
    }

    public function returnResponse($statusCode, $message)
    {
        header('Content-Type: application/json');
        header("Status: $statusCode");
        $http = [
            100 => '100 Continue',
            101 => '101 Switching Protocols',
            200 => '200 OK',
            201 => '201 Created',
            202 => '202 Accepted',
            203 => '203 Non-Authoritative Information',
            204 => '204 No Content',
            205 => '205 Reset Content',
            206 => '206 Partial Content',
            300 => '300 Multiple Choices',
            301 => '301 Moved Permanently',
            302 => '302 Found',
            303 => '303 See Other',
            304 => '304 Not Modified',
            305 => '305 Use Proxy',
            307 => '307 Temporary Redirect',
            400 => '400 Bad Request',
            401 => '401 Unauthorized',
            402 => '402 Payment Required',
            403 => '403 Forbidden',
            404 => '404 Not Found',
            405 => '405 Method Not Allowed',
            406 => '406 Not Acceptable',
            407 => '407 Proxy Authentication Required',
            408 => '408 Request Time-out',
            409 => '409 Conflict',
            410 => '410 Gone',
            411 => '411 Length Required',
            412 => '412 Precondition Failed',
            413 => '413 Request Entity Too Large',
            414 => '414 Request-URI Too Large',
            415 => '415 Unsupported Media Type',
            416 => '416 Requested Range Not Satisfiable',
            417 => '417 Expectation Failed',
            500 => '500 Internal Server Error',
            501 => '501 Not Implemented',
            502 => '502 Bad Gateway',
            503 => '503 Service Unavailable',
            504 => '504 Gateway Time-out',
            505 => '505 HTTP Version Not Supported',
        ];
        echo json_encode([
            'response' => [
                'code' => $http[$statusCode],
                'message' => $message,
            ],
        ]);
        die;
    }

    public function notfound()
    {
        $this->returnResponse(404, 'The endpoint you are looking for is not found.');
    }
}

همان‌طور که می‌بینیم، در بدنهٔ این کلاس سه پراپرتی تعریف کرده‌ایم تحت عناوین requestMethod$ و request$ و همچنین requestParams$ و داخل کانستراکتور، که به محض استفاده از این کلاس فراخوانی خواهد شد، نیز دستور داده‌ایم تا مقدار پراپرتی requestMethod$ برابر با ["SERVER["REQUEST_METHOD_$ قرار گیرد که این وظیفه را دارا است تا نوع متد اچ‌تی‌تی‌پی مورد استفاده در حین ارسال ریکوئست را برگرداند. به طور مثال، اگر درخواستی فرضی همچون مورد زیر را مد نظر قرار دهیم:

GET https://example.com/api/v1/users

با فراخوانی آرایهٔ سوپرگلوبال SERVER_$ به همراه کلید REQUEST_METHOD به استرینگ GET دست خواهیم یافت به طوری که داریم:

<?php 
echo $_SERVER["REQUEST_METHOD"]; // return GET

در ادامه و داخل کانستراکتور این کلاس مقدار پراپرتی request$ را مشخص کرده‌ایم بدین صورت که با استفاده از قابلیت Type Hinting زبان پی‌اچ‌پی و درج دستور (array) پس از علامت مساوی گفته‌ایم که نوع دیتایی که در این پراپرتی ذخیره می‌گردد حتماً می‌باید آرایه باشد (برای کسب اطلاعات بیشتر در این رابطه، به مقالهٔ‌ آشنایی با مفهوم Type Hinting در زبان PHP مراجعه نمایید.)

با توجه به اینکه کلیهٔ درخواست‌هایی که برای این وب سرویس ارسال می‌شوند می‌باید از جنس جیسون باشند، مسلماً نیاز داریم تا برای پردازش آن‌ها محتوای دریافتی را از حالت جیسون خارج کنیم و به یک آرایه مبدل سازیم که برای این منظور از متدی در زبان پی‌اچ‌پی تحت عنوان ()json_decode استفاده کرده‌ایم بدین صورت که پارامتر دوم آن را برابر با true قرار داده‌ایم که این تضمین را ایجاد می‌کند که آبجکت‌ها را تبدیل به یک اصطلاحاً Associative Array می‌کند.

    نکته

گاهی آرایه‌ای که کلیدهای آن از جنس عدد باشد نیاز ما را مرتفع نمی‌سازد که در چنین مواقعی نیاز به استفاده از آرایه‌های اصطلاحاً Associative داریم بدین شکل که کلید‌ها در این نوع آرایه‌ها استرینگ هستند. به طور مثال، آرایهٔ  ['names = ['first_name' => 'ali$ یک Associative Array است.

گفتیم که در نظر گرفتن مقدار true به عنوان پارامتر دوم ()json_decode، این تضمین را ایجاد می‌کند که آبجکت‌ها تبدیل به آرایه شوند که برای درک بهتر این موضوع، مثال زیر را مد نظر قرار می‌دهیم:

$this->request = (array) json_decode(file_get_contents('php://input'));

حال فرض کنیم درخواستی از جنس POST به صورت زیر دریافت می‌کنیم:

array(1) {
    ["request_params"]=>
    object(stdClass)#7 (4) {
      ["firstName"]=>
      string(6) "Behzad"
      ["lastName"]=>
      string(6) "Moradi"
      ["mail"]=>
      string(14) "hi@example.com"
      ["pass"]=>
      string(6) "123456"
    }

همان‌طور که می‌بینیم، کلید request_params حاوی یک آبجکت است اما این در حالی است که اگر مقدار true را به عنوان پارامتر دوم به صورت زیر در نظر بگیریم:

$this->request = (array) json_decode(file_get_contents('php://input'), true);

به عنوان خروجی خواهیم داشت:

array(1) {
    ["request_params"] =>
    array(4) {
        ["firstName"] =>
        string(6)
        "Behzad" ["lastName"] =>
        string(6)
        "Moradi" ["mail"] =>
        string(14)
        "hi@example.com" ["pass"] =>
        string(6)
        "123456"
    }
}

می‌بینیم که آبجکت مذکور به یک Associative Array تبدیل شده است. در ادامه می‌رسیم به بررسی عملکرد فانکشن ()file_get_contents که اساساً این وظیفه را دارا است تا محتوایات یک فایل را در قالب استرینگ بازگرداند. زمانی که پارامتر ورودی php://input را برای این فانکشن در نظر می‌گیریم، این امکان در اختیارمان قرار می‌گیرد تا دیتایی که در قالب متد POST ارسال می‌شود و به اصطلاح Raw (خام) است را به صورت یک استرینگ به دست آوریم و همان‌طور که پیش از این توضیح دادیم، با استفاده از فانکشن ()json_decode استرینگ مذکور را می‌توان به یک آرایه یا آبجکت مبدل ساخت. برای درک بهتر این موضوع، کد زیر را مد نظر قرار می‌دهیم:

$this->request = file_get_contents('php://input');

حال می‌توانیم یک خروجی فرضی به صورت زیر داشته باشیم:

string(123) "{
    "request_params": {
        "firstName": "Behzad",
        "lastName": "Moradi",
        "mail": "hi@example.com",
        "pass": "123456"
    }
}"

اما اگر همان کد اولیه به صورت زیر را مد نظر قرار دهیم:

$this->request = (array) json_decode(file_get_contents('php://input'), true);

خروجی ما به صورت زیر خواهد بود:

array(1) {
    ["request_params"] =>
    array(4) {
        ["firstName"] =>
        string(6)
        "Behzad" ["lastName"] =>
        string(6)
        "Moradi" ["mail"] =>
        string(14)
        "hi@example.com" ["pass"] =>
        string(6)
        "123456"
    }
}

می‌بینیم دیتای جیسونی که پیش از این به صورت استرینگ خام بود، حال به صورت یک آرایه درآمده است.

بررسی متد ()throwError

در ادامه قصد داریم به تفسیر متدی تحت عنوان ()throwError بپردازیم که ساختار کلی آن به صورت زیر است:

public function throwError(int $statusCode, string $message)
{
    header('Content-Type: application/json');
    echo json_encode([
        'error' => [
            'status' => $statusCode,
            'message' => $message,
        ],
    ]);
    die;
}

وظیفه‌ای که این متد دارد این است که در صورت فراخوانی و پاس دادن پارامترهای ورودی مناسب،‌ پیامی در قالب فرمت جیسون در معرض دید کاربر بگذارد. این متد دو پارامتر ورودی می‌گیرد بدین شکل که پارامتر اول تحت عنوان statusCode$ که از جنس عدد صحیح باید باشد مرتبط با کد وضعیت پروتکل اچ‌تی‌تی‌پی است و پارامتر دوم که از جنس استرینگ است نیز مرتبط با پیامی است که قصد داریم در معرض دید کاربران قرار گیرد (برای آشنایی بیشتر با کدهای وضعیت، می‌توانید به آموزش آشنایی با کدهای وضعیت در پروتکل HTTP مراجعه نمایید.)

پیش از هر چیز با استفاده از تابع ()header این دستور را به وب سرور می‌دهیم که تایپ یا نوع محتوایی که عرضه خواهد شد فقط و فقط جسیون خواهد بود؛ سپس با استفاده از تابع ()json_encode آرایه‌ای حاوی کلید error را چاپ خواهیم کرد که این کلید خود حاوی آرایه‌ای دیگر شامل کلیدهای status و message است که به ترتیب کد وضعیت و پیام مربوطه را در خود نگهداری خواهند کرد.

بررسی متد ()validateRequest

متد ()validateRequest این وظیفه را دارا است تا پراپرتی requestParams$ را مقداردهی کند به طوری که داریم:

public function validateRequest()
{
    if ($_SERVER['CONTENT_TYPE'] != 'application/json') {
        $this->throwError(403, 'Forbidden. The only acceptable content type is application/json.');
    }
    if (!isset($this->request['request_params']) || !is_array($this->request['request_params'])) {
        $this->throwError(400, 'Bad Request. The parameters of the request are not defined.');
    } else {
        $this->requestParams = $this->request['request_params'];
    }
}

ابتدا با یک دستور شرطی چک کرده‌ایم ببینیم که آیا مقدار ["SERVER["REQUEST_METHOD_$ برابر با استرینگ application/json می‌باشد یا خیر که اگر این گونه نبود، با استفاده از متدی که پیش از این نوشتیم تحت عنوان ()throwError یک کد وضعیت مرتبط همچون 403 و پیامی که کاملاً واضح و گویا باشد را ریترن خواهیم کرد.

    نکته

در ارتباط با توسعهٔ ای‌پی‌آی همواره می‌باید این نکته را مد نظر داشت که نیاز است تا پیام‌هایی که مرتبط با ارورها هستند کاملاً واضح و شفاف باشند تا دولوپری که اقدام به استفاده از وب سرویس ما می‌کند به خوبی بداند که در صورت بروز ارور مشکل از کجا است.

در ادامه و با استفاده از یک دستور if دیگر چک کرده‌ایم ببینیم که آیا کلیدی تحت عنوان request_params داخل آرایهٔ request$ وجود دارد یا خیر و آیا اساساً این کلید یک آرایه می‌باشد یا خیر که اگر این گونه نبود، با استفاده از متد ()throwError اروری با کد وضعیت 400 در معرض دید کاربر قرار خواهیم داد اما چنانچه چنین کلیدی در قالب یک آرایه وجود داشت، وارد دستور else شده و مقدار پراپرتی requestParams$ را برابر با ['this->request['request_params$ قرار می‌دهیم.

بررسی متد ()validateParams

متدی داریم تحت عنوان ()validateParams که این وظیفه را دارا است تا چک کند پارامترهایی که برای وب سرویس ارسال می‌کنیم خالی نباشند به طوری که داریم:

public function validateParams($fieldName, $value, $isRequired = false)
{
    if ($isRequired == true && $value == "") {
        $this->throwError(400, "The $fieldName field is required.");
    }
    return $value;
}

این متد سه پارامتر ورودی می‌گیرد که به ترتیب مرتبط با نام فیلد، مقدار در نظر گرفته شده برای آن فیلد و همچنین اجباری/غیراجباری بودن آن فیلد هستند. همچنین با استفاده از یک دستور if چک کرده‌ایم ببینیم که اگر مقدار پارامتر isRequired$ برابر با true بود و همچنین مقدار پارامتر value$ تهی بود، اروری با کد وضعیت 400 در معرض دید کاربر قرار گیرد و در غیر این صورت نیز مقدار پارامتر value$ ریترن گردد.

بررسی متد ()returnResponse

یکی دیگر از متدهای پرکاربرد این وب سرویس ()returnResponse است که زمان‌هایی مورد استفاده قرار می‌گیرد تا بخواهیم نتیجه‌ای را برگردانیم به طوری که ساختار این متد به شرح زیر است:

public function returnResponse($statusCode, $message) {
    header('Content-Type: application/json');
    header("Status: $statusCode");
    $http = [
        100 => '100 Continue',
        101 => '101 Switching Protocols',
        200 => '200 OK',
        201 => '201 Created',
        202 => '202 Accepted',
        203 => '203 Non-Authoritative Information',
        204 => '204 No Content',
        205 => '205 Reset Content',
        206 => '206 Partial Content',
        300 => '300 Multiple Choices',
        301 => '301 Moved Permanently',
        302 => '302 Found',
        303 => '303 See Other',
        304 => '304 Not Modified',
        305 => '305 Use Proxy',
        307 => '307 Temporary Redirect',
        400 => '400 Bad Request',
        401 => '401 Unauthorized',
        402 => '402 Payment Required',
        403 => '403 Forbidden',
        404 => '404 Not Found',
        405 => '405 Method Not Allowed',
        406 => '406 Not Acceptable',
        407 => '407 Proxy Authentication Required',
        408 => '408 Request Time-out',
        409 => '409 Conflict',
        410 => '410 Gone',
        411 => '411 Length Required',
        412 => '412 Precondition Failed',
        413 => '413 Request Entity Too Large',
        414 => '414 Request-URI Too Large',
        415 => '415 Unsupported Media Type',
        416 => '416 Requested Range Not Satisfiable',
        417 => '417 Expectation Failed',
        500 => '500 Internal Server Error',
        501 => '501 Not Implemented',
        502 => '502 Bad Gateway',
        503 => '503 Service Unavailable',
        504 => '504 Gateway Time-out',
        505 => '505 HTTP Version Not Supported',
    ];
    echo json_encode([
        'response' => [
            'code' => $http[$statusCode],
            'message' => $message,
        ],
    ]);
    die;
}

همان‌طور که ملاحظه می‌شود، این متد دو پارامتر ورودی می‌گیرد تحت عناوین statusCode$ و message$ به طوری که پارامتر اول این وظیفه را دارا است تا یک کد وضعیت همچون 200 یا 404 را به این متد پاس دهد و پارامتر دوم هم پیامی است که ما به عنوان توسعه‌دهندهٔ این وب سرویس برای درک بهتر نتیجه در اختیار سایر دولوپرها می‌گذاریم.

داخل این متد و پیش از هر کاری دو هِدِر سِت کرده‌ایم با این توضیح که هِدِر اول این تضمین را ایجاد می‌کند که نوع محتوایی که در دسترس دیگر سرویس‌ها قرار خواهد گرفت حتماً جیسون است و هِدِر دوم نیز کد وضعیت ارسالی را در بخش هِدِر درج می‌کند. سپس آرایه‌ای درست کرده‌ایم تحت عنوان http$ که حاوی اصلی‌ترین کدهای وضعیت پروتکل اچ‌تی‌تی‌پی است و در ادامه نیز آرایه‌ای به فانکشن ()json_encode پاس داده‌ایم که کلیدی تحت عنوان response دارا است که حاوی کلیدهای code و message است که این دو کلید نیز به ترتیب دربرگیرندهٔ پارامترهای ورودی statusCode$ و message$ خواهند بود و ()json_encode هم این تضمین را ایجاد می‌کند که این آرایه در قالب فرمت جیسون چاپ شود و در نهایت هم از یک دستور die استفاده کرده‌ایم (لازم به یادآوری است که به جای دستور die از exit هم می‌توان استفاده نمود.)

بررسی متد ()notfound

متد ()notfound که پیش از این داخل کنترلر DefaultController قرار داشت را نیز به داخل BaseController منتقل کرده‌ایم چرا که ممکن است بخواهیم تا از این متد در چندین و چند کنترلر مختلف استفاده نماییم. کاری که داخل این متد انجام داده‌ایم این است که با استفاده از متد ()returnResponse که پیش از این نوشتیم کد وضعیتی همچون 404 را ریترن کرده‌ایم مبنی بر اینکه ریسورس مد نظر کاربر یافت نشده است.

جمع‌بندی

در این پروژه قصد داریم تا علاوه بر کنترلر اصلی که DefaultController نام دارد، کنترلر دیگری تحت عنوان UserController نیز داشته باشیم که این وظیفه را خواهد داشت تا کلیهٔ‌ تَسک‌های مرتبط با کاربران مثل ثبت‌نام و لاگین در آن جای گیرد. از همین روی، برخی متدهایی که مابین کنترلرهای مختلف یکسان هستند را داخل BaseController تعریف نمودیم تا از داخل همهٔ کنترلرهای پروژه به آن‌ها دسترسی داشته باشیم.

online-support-icon