مدیریت تایپ ها در PHP 7

محمد عسکری
منتشر شده در ۳ ماه پیش | خواندن در ۸ دقیقه

زمانی که PHP نسخه ۷ با تایپ های استاتیک معرفی شد، به یک چیز امیدوار شدم، آن هم این بود که دیگر با ناسازگاری ها هنگام نوشتن کد های PHP که تایپ متغیر آنها مشخص نیست، رو به رو نخواهم شد.

به خاطر دارم کد هایی را زمانی می‌خواندم که هیچ ایده‌ای درباره نوع متغیر هایی که می‌دیدم نداشتم. آیا پاسخ این تابع را به عنوان یک int می توانستم استفاده کنم یا یک Boolean؟ و آیا باعث ایجاد رفتار‌های خارج از انتظار خواهد شد یا خیر؟

مشخص کردن نوع متغیر بازگشتی بسیار مفید است. این مسئله نشان دهنده آن خواهد بود که دقیقا با چه مدل متغیری سروکار دارید و نیازی به حدس زدن درباره آن نخواهید داشت.

صبر کنید، دیگر نیازی به حدس زدن نوع متغیر نیست؟

مشخص نبودن تایپ متغیر ها در PHP باعث بسیاری از رفتار های عجیب و ابهاماتی بود که باید آنها را کشف می کردید. ابهام به معنی تردید است، و شرک و تردید نیز سردرد را به دنبال خواهد داشت.

مشخص کردن تایپ در PHP

PHP نسخه ۷ با امکان مشخص کردن دو تایپ معرفی شد. Scalar types و Return types

به نظر جذاب میاد؟ حال دیگر خواهیم دانست ورودی یک تابع چیست و خروجی آن چه خواهد بود. دیگر چه اشتباهی ممکن است رخ دهد؟

تایپ در PHP : سورپرایز

در تئوری، مشخص کردن تایپ ها در PHP نسخه ۷ به نظر جذاب می آید. زمانی که حوصله‌تان سر رفته و شروع به بازی کردن با آن می کنید، از خود می پرسید آیا واقعا تایپ مشخص شده؟ خروجی آن چیست اگر نوع دیگری برگردانم؟

نمونه کد زیر را ببینید

function mySuperFunction(): int
{
return "hello world";
}
mySuperFunction();

این کد مشکلی ندارد، تایپ مشخص شده برای متغیر بازگشتی تابع، یک Int می باشد و این در حالی است که تابع یک String را بازگردانده است. جای تعجبی ندارد اگر PHP خطایی برگرداند:

Fatal error: Uncaught TypeError: Return value of mySuperFunction() must be of the type integer, string returned.

حال بیایید مثال دیگری را بررسی کنیم:

function mySuperFunction():int
{
return "hello world1";
}
mySuperFunction();

نتایج همانند تست بالا خواهد بود، PHP خطا برمی‌گرداند. حال مشکل اصلی چیست؟ نمونه زیر را ببینید:

function mySuperFunction():int
{
return "1hello world";
}
mySuperFunction();

با توجه به آزمایش های بالا، این کد نیز باید خطا برگرداند، درست است؟ چرا که تایپ متغیر بازگشتی کماکان String می باشد درحالی که گفته ایم باید Int باشد.

این تابع 1 را بازمی‌گرداند. بدون خطا. عبارت "1hello world" برای PHP یک متغیر String می باشد. حال بیایید کمی جلوتر برویم:

function mySuperFunction():int
{
return 1.1456415;
}
mySuperFunction();

کاملا واضع و مشخص است که تایپ متغیر بازگشتی Int نمی باشد . با این حال هیچ گونه خطایی مشاهده نمی‌کنیم و تابع عدد ۱ را بازمیگرداند. حال کد زیر را ببینید:

function mySuperFunction():bool
{
return "1hello world";
}

PHP عبارت "1hello world" را به عنوان Boolean در نظر می گیرد و 1 برمیگرداند. حال نگاهی به کد زیر کنید:

function mySuperFunction():bool
{
return "abcde";
}
mySuperFunction();

در دنیای عجیب PHP، عبارت "abcd" یک Boolean است. و درست است، این تابع true باز می‌گرداند.

همانطور که مشاهده کردید، با این سبک از تعریف کردن تایپ متغیر، کماکان امکان دارد بسیاری از چیز ها را از دست بدهید. تنها به این دلیل که کد چیزی را عنوان می کند که امکان دارد درست نباشد.

به طور خلاصه این قوانین به طور جدی می‌تواند گمراه کننده باشد.

ورود به دنیای Strict Types

این واقعیت که می توانیم بدون هیچ سروصدایی متغیر String را به Boolean تبدیل کنیم، باعث می‌شود کد خود را اشتباه بگیریم. این مسئله ابهاماتی را اضافه می‌کند که انتظار داشتیم ساده و واضح باشد.

حال بیایید واقع بین باشیم. به عنوان توسعه دهنده، باید بتوانیم تایپ اطلاعات را در کد های خود کنترل کنیم.

اگر این کار را نکنیم، دری را برای مشکلات بی پایان، رفتار‌های غیر قابل انتظار، و سوتفاهم بین دیگر توسعه دهندگان باز کرده ایم. کد ها تغییر می کنند و باگ ها پدیدار می شوند. و رئیس شما، اخراجتان می کند. همسر شما از شما جدا شده و سقوط شما را نگاه می کند و شما در آتش خواهید سوخت.

اما صبر کنید، هر چیزی هنوز امکان دارد، و امیدی باقیست. PHP راه حلی برای این مسئله دارد.

مراقب حالت strict type باشید

function mySuperFunction(): bool{
return 'abcde';
}
echo mySuperFunction();

با اجرای کد بالا شما خطای زیر را خواهید گرفت:

Fatal error: Uncaught TypeError: Return value of mySuperFunction() must be of the type boolean, string returned

با مشاهده این خطا، یک احساس راحتی به من دست داد، البته که String یک Boolean نیست.

پیشنهاد من : حالت Strict type را در همه جای کد خود قرار دهید

یک snippet در ویرایشگر خود بسازید، که هربار فایل PHP ایجاد می شود، این حالت را در بالای آن فایل قرار دهد.

متاسفانه شما قادر نخواهید بود این حالت را به صورت Global تعریف کنید و نیاز دارید برای هر فایل PHP آن را تعریف کنید. علت آن ساده است، اجازه خواهد داد برنامه های خارجی با برنامه شما کار کنند، حتی اگر حالت strict type نباشند.

می دانم که برخی از شما ممکن است موافق نباشید. می‌دانم که برخی از شما حاضرید شفافیت و یکپارچگی را برای بدست آوردن انعطاف پذیری به آتش بکشید. و می‌دانم که استدلالتان این است که PHP جاوا نیست. خداروشکر که اینطور نیست.

همچنین می دانم برخی از شما خواهید گفت‌: "آره اما راحت تره که boolean من نمایش داده بشه و نیاز دارم که در یک جای خاص به صورت String با آن رفتار کنم"

پاسخ من این است :‌ معماری خود را اصلاح کنید یا سبک پیاده سازی خود را تغییر دهید. اگر نیاز دارید با حالت weak type کار کنید، یقینا چیزی در کد های شما ایراد دارد اگر واقعا به آن نیاز دارید، لطفا مسئله اصلی تری را حل کنید، و هرگز سمت استفاده از boolean به عنوان string یا استفاده از string به عنوان Int نروید.

شما توسعه دهنده هستید، هکر نیستید. شما مسائل را حل می کنید، اطراف آنها پرسه نمی زنید.

در ۵ کلمه :‌ درود بر strict type. حال PHP نیز یک زبان strong type است.

Nullable Type در PHP 7

چگونه اشتباه از این استفاده کنیم؟

declare(strict_types=1);
class User{
//Value object
}
class UserRepository{
private $database;
public function __construct(Database $database){
$this->database = $database;
}
public function getUserById(int $id):?User
{
//If user is not in the database, return null
$user = $this->database->fetchUser($id);
return $user;
}
}
class EmailSender
{
public function sendEmailToUser(int $userId)
{
$userRepository = new UserRepository(new Database());
$user = $userRepository->getUserById($userId);
//Can send email to... null!
$this->sendEmail($user);
}
}
$emailSender = new EmailSender();
$emailSender->sendEmailToUser(12345);

این کد هنگامی که بخواهید کاربری را که در دیتابیس نیست بگیرید خواهد ترکید. چگونه می توانیم به Null ایمیل بزنیم؟

واضح است که به صورت زیر می توانید کد را تصحیح کنید

...
class EmailSender
{
public function sendEmailToUser($userId)
{
$userRepository = new UserRepository(new Database());
$user = $userRepository->getUserById($userId);
if ($user !== null) {
$this->sendEmail($user);
}
}
}

هرچند این دیدگاه دو ایراد دارد:

۱. استفاده از عملگر Nullable باعث ایجاد یک شرط اضافی خواهد شد. بی استفاده و پر صداست.

۲. کد بالا بی صدا با خطا مواجه خواهد شد اگر کاربر وجود نداشته باشد. "چرا کاربر ایمیل نگرفت" کابوس شما خواهد شد . شما نیاز دارید که موارد زیر را در نظر بگیرید.

۱. عدم موجودیت کاربر

۲. null بودن کاربر

۳. کسی شرط null را قرارداده باشد و اجازه دهد اپلیکیشن هیچ کاری انجام ندهد.

قطعه کد زیر راه حل دیگریست بر این مشکل:

...
class UserRepository{
private $database;
public function __construct($database){
$this->database = $database;
}
public function getUserById($id):User
{
$user = $this->database->fetchUser($id);
//If user is not in the database, an error will be thrown!
return $user;
}
}
...

در این حالت دیگر نیازی به چک کردن حالت Null وجود نخواهد داشت، چرا که در صورت موجود نبودن کاربر، کد شما یک exception ایجاد می کند. که شما نیاز دارید آن خطا را مدیریت کنید. ساده و واضح اجازه نخواهید داد فضایی برای سردرگمی بوجود آید.


محمد عسکری

نظرات