سرفصل‌های آموزشی
آموزش OOP در PHP
آشنایی با مفهوم Type Hinting در زبان PHP

آشنایی با مفهوم Type Hinting در زبان PHP

در آموزش قبل دیدیم که کانستراکتور کلاس User یک پارامتر الزامی داشت تحت عنوان database$ با این توضیح که حین ساخت یک آبجکت جدید از روی این کلاس، می‌بایست آبجکتی از روی کلاس Database به آن پاس می‌‌دادیم. با مد نظر قرار دادن این نکته، در این آموزش قصد داریم Type Hinting را در پروژه‌ای که در آموزش گذشته ساختیم اِعمال نماییم. برای شروع، یک کپی از روی فایل Database.php گرفته و آن را داخل همان پوشهٔ classes تحت عنوانی دلخواه همچون Database2.php ذخیره می‌سازیم و محتوای داخل آن را به صورت زیر ریفکتور می‌کنیم:

<?php
namespace SokanAcademy;

final class Database2
{
    public $connection;

    public function __construct()
    {
        $this->connection = new \PDO("mysql:host=localhost;dbname=php-oop", 'root', '');
        $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    }
}

در واقع،‌ تنها تغییری که صورت داده‌ایم آن است که نام کلاس Database را به Database2 تغییر داده‌ایم. حال وارد فایل index.php شده و آن را به صورت زیر آپدیت می‌کنیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

// $dbObj = new SokanAcademy\Database();
$tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($tmpDbObj);
$allUsers = $user->fetch();
if ($allUsers) {
    foreach ($allUsers as $user) {
        echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
    }
}

آبجکت قبلی تحت عنوان dbObj$ که از روی کلاس Database ساخته بودیم را کامنت کرده در عوض آبجکتی جدید به نام tmpDbObj$ از روی کلاس Database2 ساخته‌ایم و آن را به عنوان آرگومان کلاس User در نظر گرفته‌ایم و این در حالی است که اگر این فایل را اجرا کنیم، به درستی خروجی زیر در معرض دیدمان قرار خواهد گرفت:

Behzad Moradi

حال کلاس User را به صورت زیر ریفکتور می‌کنیم:

<?php
namespace SokanAcademy;

class User
{
    private $db;
    private $tableName = 'users';

    public function __construct(Database $database)
    {
        $this->db = $database->connection;
    }

    public function fetch()
    {
        $statement = $this->db->query("SELECT * FROM $this->tableName");
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
    }
}

تنها کاری که داخل این کلاس انجام داده‌ایم آن است که قبل از پارامتری تحت عنوان database$ که برای کانستراکتور این کلاس در نظر گرفته بودیم نام کلاس Database را نوشته‌ایم که به این کار اصطلاحاً Type Hinting گفته می‌شود. به عبارت دیگر، به کانستراکتور این کلاس دستور داده‌ایم که آرگومان ورودی به این کلاس در حین ساخت یک آبجکت جدید فقط و فقط می‌باید از جنس کلاس Database باشد. مجدد نگاهی به محتویات فایل index.php می‌اندازیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

// $dbObj = new SokanAcademy\Database();
$tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($tmpDbObj);
$allUsers = $user->fetch();
if ($allUsers) {
    foreach ($allUsers as $user) {
        echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
    }
}

می‌بینیم که به جای آبجکت ساخته‌شده از روی کلاس Database، آبجکتی را به عنوان آرگومان پاس داده‌ایم که از روی کلاس Database2 ساخته شده است و با این توضیحات در خروجی خواهیم داشت:

/var/www/oop/type-hinting$ php index.php 
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to SokanAcademy\User::__construct() must be an instance of SokanAcademy\Database, instance of SokanAcademy\Database2 given, called in /var/www/oop/type-hinting/index.php on line 7 and defined in /var/www/oop/type-hinting/classes/User.php:9

متن ارور حاکی از آن است که آرگومان ورودی به کلاس User می‌باید از جنس Database باشد اما این در حالی است که آبجکتی از روی کلاس Database2 پاس داده شده است. در حقیقت،‌ Type Hinting باعث شده تا دولوپر اجازه نداشته باشد تا هر نوع آبجکتی را به عنوان آرگومان به کلاس User دهد. مجدد کدهای فوق را به صورت زیر تغییر داده و این فایل را اجرا خواهیم کرد:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$dbObj = new SokanAcademy\Database();
// $tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($dbObj);
$allUsers = $user->fetch();
if ($allUsers) {
    foreach ($allUsers as $user) {
        echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
    }
}

با پاس دادن آبجکت ساخته‌شده از روی کلاس Database، می‌بینیم که این فایل بدون هیچ گونه مشکلی کار خواهد کرد. حال فرض کنیم که قصد داریم کدی بنویسیم که تایپ هینتینگ کلاس User به گونه‌ای باشد که هم کلاس Database را قبول کند و هم کلاس Database2 که در این صورت می‌باید از اینترفیس‌ها کمک بگیریم.

آشنایی با مفهوم Interface Type Hinting

در آموزش آشنایی با مفهوم Interface در متودولوژی OOP با مفهوم و نحوهٔ کارکرد اینترفیس‌ها در زبان برنامه‌نویسی پی‌اچ‌پی آشنا شدیم. حال در این قسمت از آموزش قصد داریم ببینیم که به چه شکل می‌توان از اینترفیس‌ها در پروسهٔ تایپ هینتینگ استفاده نماییم که برای همین منظور داخل پوشهٔ classes فایلی تحت عنوان DatabaseInterface.php ایجاد کرده و کدهای زیر را داخل آن می‌نویسیم:

<?php
namespace SokanAcademy;

interface DatabaseInterface
{
    
}

همان‌طور که می‌بینیم، اینترفیسی ایجاد کرده‌ایم تحت عنوان DatabaseInterface که هیچ متدی داخل آن تعریف نشده است. در ادامه، هر دو کلاس Database و Database2 را از این اینترفیس ایمپلیمنت می‌کنیم به طوری که مثلاً برای کلاس Database داریم:

<?php
namespace SokanAcademy;

final class Database implements DatabaseInterface
{
    public $connection;

    public function __construct()
    {
        $this->connection = new \PDO("mysql:host=localhost;dbname=php-oop", 'root', '');
        $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    }
}

به همین شکل کلاس Database2 را نیز ریفکتور می‌کنیم و در ادامه به کلاس User رفته و کانستراکتور آن را به شکل زیر آپدیت می‌کنیم:

<?php
namespace SokanAcademy;

class User
{
    private $db;
    private $tableName = 'users';

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database->connection;
    }

    public function fetch()
    {
        $statement = $this->db->query("SELECT * FROM $this->tableName");
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
    }
}

می‌بینیم که بر خلاف گذشته که کلاس Database را تایپ هینتینگ کرده بودیم، این بار اینترفیس DatabaseInterface را مورد استفاده قرار داده‌ایم. حال به فایل index.php رفته و آن را به صورت زیر تغییر می‌دهیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$dbObj = new SokanAcademy\Database();
$tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($dbObj); // or $tmpDbObj
$allUsers = $user->fetch();
if ($allUsers) {
    foreach ($allUsers as $user) {
        echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
    }
}

به عنوان پارامتر ورودی کلاس User خواه آبجکت dbObj$ و خواه tmpDbObj$ را مورد استفاده قرار دهیم، کلاس User عملکرد صحیحی خواهد داشت چرا که داخل کانستراکتور آن دستور داده‌ایم که آبجکت ورودی می‌باید از جنس اینترفیس DatabaseInterface باشد و از آنجا که هر دو کلاس Database و Database2 از روی چنین اینترفیسی ایمپلیمنت شده‌اند، این کلاس به درستی کار خواهد کرد.

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

در تکمیل این مبحث،‌ کلاس User را به صورت زیر تکمیل می‌کنیم تا قابلیت ثبت یک رکورد جدید نیز در اختیارمان قرار گیرد:

<?php
namespace SokanAcademy;

class User
{
    private $db;
    private $tableName = 'users';

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database->connection;
    }

    public function fetch()
    {
        $statement = $this->db->query("SELECT * FROM $this->tableName");
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
    }

    public function insert(array $data)
    {
        $values = "'{$data['firstName']}', '{$data['lastName']}'";
        $this->db->query("INSERT INTO $this->tableName (`first_name`, `last_name`) VALUES ($values)");
    }
}

متدی نوشته‌ایم تحت عنوان ()insert که یک پارامتر ورودی می‌گیرد به نام data$ که قبل از نام آن کیورد array نوشته‌ شده است بدان معنا که دستور داده‌ایم تا هر گونه آرگومانی که برای این متد در نظر گرفته می‌شود می‌باید از جنس آرایه باشد. داخل متد ()query از دستور INSERT INTO اس‌کیوال استفاده کرده‌ایم تا از آن طریق یک رکورد جدید داخل جدول users ایجاد کنیم به طوری که اِلِمان‌های ['data['firstName$ و ['data['lastName$ به ترتیب با ستون‌های first_name و last_name مَچ خواهند شد. در ادامه،‌ وارد فایل index.php شده و به شکلی که در ادامه مشاهده می‌کنیم،‌ متد ()insert را فراخوانی می‌کنیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$dbObj = new SokanAcademy\Database();
$tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($dbObj); // or $tmpDbObj
// $allUsers = $user->fetch();
// if ($allUsers) {
//     foreach ($allUsers as $user) {
//         echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
//     }
// }
$user->insert(['firstName' => 'Sara', 'lastName' => 'Ahmari']);

پس از کامنت‌ کردن کدهای قبلی، متد ()insert را به آبجکت user$ منتسب کرده‌ و یک آرایه را به عنوان آرگومان در نظر گرفته‌ایم. با اجرای این فایل، یک رکورد جدید در دیتابیس ثبت خواهد شد به طوری که از این پس دو رکورد به صورت زیر خواهیم داشت:

+----+------------+-----------+
| id | first_name | last_name |
+----+------------+-----------+
|  1 | Behzad     | Moradi    |
|  2 | Sara       | Ahmari    |
+----+------------+-----------+

حال جهت تست،‌ کدهای داخل فایل index.php را به صورت زیر تغییر می‌دهیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$dbObj = new SokanAcademy\Database();
$tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($dbObj); // or $tmpDbObj
// $allUsers = $user->fetch();
// if ($allUsers) {
//     foreach ($allUsers as $user) {
//         echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
//     }
// }
$user->insert('This is a string');

در صورت اجرای مجدد این فایل،‌ در خروجی خواهیم داشت:

/var/www/oop/type-hinting$ php index.php 
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to SokanAcademy\User::insert() must be of the type array, string given, called in /var/www/oop/type-hinting/index
.php on line 14 and defined in /var/www/oop/type-hinting/classes/User.php:20

متن ارور حاکی از آن است که آرگومان پاس داده شده به متد ()insert می‌باید از جنس آرایه باشد اما یک استرینگ در نظر گرفته شده است.

درآمدی بر Type Hinting سایر دیتا تایپ‌ها در PHP 7.0

نسخهٔ PHP 7.0 بر خلاف نسخه PHP 5.0 از دیتا تایپ‌های عدد صحیح، عدد اعشاری، استرینگ و بولین پشتیبانی می‌کند که برای درک بهتر کاربرد آن‌ها، کلاس User را به صورت زیر تکمیل می‌کنیم:

<?php
namespace SokanAcademy;

class User
{
    private $db;
    private $tableName = 'users';

    protected $name;
    protected $isDeveloper;
    protected $age;
    protected $height;

    public function __construct(DatabaseInterface $database)
    {
        $this->db = $database->connection;
    }

    public function fetch()
    {
        $statement = $this->db->query("SELECT * FROM $this->tableName");
        return $statement->fetchAll(\PDO::FETCH_ASSOC);
    }

    public function insert(array $data)
    {
        $values = "'{$data['firstName']}', '{$data['lastName']}'";
        $this->db->query("INSERT INTO $this->tableName (`first_name`, `last_name`) VALUES ($values)");
    }

    // Setter methods
    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function setIsDeveloper(bool $val)
    {
        $this->isDeveloper = $val;
    }

    public function setAge(int $age)
    {
        $this->age = $age;
    }

    public function setHeight(float $height)
    {
        $this->height = $height;
    }
}

از خطوط نهم تا دوازدهم چهار پراپرتی از جنس protected تعریف کرده‌ایم که به ترتیب جهت نگهداری مقادیر نام، حرفه، سن و قد مورد استفاده قرار می‌گیرند سپس چهار متد به اصطلاح Setter ساخته‌ایم تا پس از ساخت یک آبجکت از روی این کلاس،‌ بتوان از آن طریق این پراپرتی‌ها را مقداردهی نمود (جهت آشنایی بیشتر با این دست متدها، به آموزش آشنایی با مفاهیم Setter و Getter در متودولوژی OOP مراجعه نمایید.)

متد ()setName پارامتری تحت عنوان name$ می‌گیرد که می‌باید دیتا تایپ آن string باشد؛ به عبارتی، فقط و فقط دیتایی می‌توان به این متد پاس دارد که از جنس استرینگ باشد سپس آن داده را به پراپرتی name$ منتسب خواهد کرد. متد ()setIsDeveloper پارامتری به نام val$ می‌گیرد که تایپ در نظر گرفته شده برای آن bool است بدان معنا که دیتای ورودی می‌باید true یا false باشد. متد ()setAge پارامتری تحت عنوان age$ می‌گیرد و از آنجا که سن همواره یک مقدار عددی است، تایپ int یا به عبارتی عدد صحیح برایش در نظر گرفته شده است و در نهایت هم به متد ()setHeight می‌رسیم که جنس پارامتر ورودی آن می‌باید float یا عدد اعشاری باشد. حال به عنوان مثال،‌ همین متد آخر را داخل فایل index.php مورد استفاده قرار می‌دهیم به طوری که خواهیم داشت:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$dbObj = new SokanAcademy\Database();
$tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($dbObj); // or $tmpDbObj
// $allUsers = $user->fetch();
// if ($allUsers) {
//     foreach ($allUsers as $user) {
//         echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
//     }
// }
// $user->insert(['firstName' => 'Sara', 'lastName' => 'Ahmari']);
$user->setHeight('this is to test');

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

/var/www/oop/type-hinting$ php index.php 
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to SokanAcademy\User::setHeight() must be of the type float, string given, called in /var/www/oop/type-hinting/in
dex.php on line 15 and defined in /var/www/oop/type-hinting/classes/User.php:47

متن این ارور حاکی از آن است که آرگومان پاس داده شده به متد ()setHeight یک استرینگ است در حالی که باید عدد اعشاری باشد. به عنوان مثالی دیگر، فایل index.php را به صورت زیر آپدیت می‌کنیم:

<?php
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$dbObj = new SokanAcademy\Database();
$tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($dbObj); // or $tmpDbObj
// $allUsers = $user->fetch();
// if ($allUsers) {
//     foreach ($allUsers as $user) {
//         echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
//     }
// }
// $user->insert(['firstName' => 'Sara', 'lastName' => 'Ahmari']);
// $user->setHeight('this is to test');
$user->setIsDeveloper('hi');

پس از اجرای مجدد این فایل، می‌بینیم علیرغم این که دستور داده‌ایم تا آرگومان ورودی تابع ()setIsDeveloper می‌باید بولین باشد، اما یک استرینگ پاس داده‌ایم و مفسر پی‌اچ‌پی هیچ گونه اروری از ما نمی‌گیرد!

اصطلاحاً گفته‌ می‌شود که bool به طور پیش‌فرض از Weak Type Checking برخوردار است به طوری که اگر بخواهیم این مشکل را مرتفع سازیم، می‌باید دستور (declare (strict_types = 1 را به خط اول این فایل اضافه کنیم به طوری که این دستور قابلیت به اصطلاح Strict Type Checking را در این فایل فعال می‌سازد به طوری که از این پس خواهیم داشت:

<?php
declare (strict_types = 1);
ini_set('display_errors', '1');
require_once 'vendor/autoload.php';

$dbObj = new SokanAcademy\Database();
$tmpDbObj = new SokanAcademy\Database2();
$user = new SokanAcademy\User($dbObj); // or $tmpDbObj
// $allUsers = $user->fetch();
// if ($allUsers) {
//     foreach ($allUsers as $user) {
//         echo $user['first_name'] . ' ' . $user['last_name'] . "\n";
//     }
// }
// $user->insert(['firstName' => 'Sara', 'lastName' => 'Ahmari']);
// $user->setHeight('this is to test');
$user->setIsDeveloper('hi');

حال اگر این فایل را مجدد اجرا کنیم در خروجی داریم:

/var/www/oop/type-hinting$ php index.php 
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to SokanAcademy\User::setIsDeveloper() must be of the type bool, string given, called in /var/www/oop/type-hinting/index.php on line 17 and defined in /var/www/oop/type-hinting/classes/User.php:37

متن ارور حاکی از آن است که متد ()setIsDeveloper آرگومانی از جنس بولین باید بگیرد اما ما یک استرینگ پاس داده‌ایم و این کار مجاز نیست.

جمع‌بندی
در این آموزش پروژه‌ای که در آموزش آشنایی با مفهوم Dependency Injection پیاده‌سازی کردیم را تا حدودی اصولی‌تر نموده و این امکان را فراهم آوردیم تا آبجکت دیتابیسی پاس داده شده به کلاس User دقیقاً از همان تایپی باشد که مد نظرمان است به طوری که اِعمال این نوع تغییرات باعث می‌گردد تا در پروژه‌هایی که به صورت گروهی توسط چندین و چند دولوپر مختلف توسعه می‌یابند احتمال وقوع باگ‌های سهوی کاهش یابد. 

online-support-icon