در آموزش قبل دیدیم که کانستراکتور کلاس 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
دقیقاً از همان تایپی باشد که مد نظرمان است به طوری که اِعمال این نوع تغییرات باعث میگردد تا در پروژههایی که به صورت گروهی توسط چندین و چند دولوپر مختلف توسعه مییابند احتمال وقوع باگهای سهوی کاهش یابد.