به‌روز‌رسانی PHP به نسخه‌ی 8 بدون شکستن اسکریپت

Update PHP to Version 8 without Breaking the Script

به روز رسانی PHP به نسخه ی 8 بدون شکستن اسکریپت

در نیمه ابتدایی سال ۲۰۲۰ حدس هایی زده می شد که نسخه جدید PHP چطور خواهد بود اما بسیاری از قابلیت ها هنوز در مرحله RFC و تایید نشده بودند. من در مورد برخی از آن ها در مقاله ای صحبت کردم اما در انتهای سال ۲۰۲۰ بسیاری از این قابلیت ها به صورت رسمی تایید شد (برخی از آن ها حذف شدند و برخی موارد جدید به آن ها اضافه شد). در این مقاله به این قابلیت های رسمی و تایید شده می پردازیم.

از آنجایی که این نسخه جدید PHP یک Major release است (نسخه ای کاملا جدید که دارای breaking changes است، یعنی کدهای قدیمی ممکن است در آن اجرا نشوند) باید قبل از به روز کردن سرور خودتان به PHP8 حتما این مقاله را مطالعه کنید. به طور مثال تمام deprecation ها در PHP7 (تمام دستوراتی که در نسخه هفتم منسوخ شده بودند) در نسخه ۸ به طور کامل حذف/نهایی شده اند. این مقاله به شما نشان خواهد داد که چطور می توانید قبل از به روز رسانی نسخه PHP سرور،‌کدهایتان را به روز رسانی کنید تا مشکلی پیش نیاید. علاوه بر آن چند قابلیت اصلی اضافه شده را نیز توضیح می دهم چرا که ممکن است بخواهید در پروژه هایتان از آن استفاده کنید.

در صورتی که می خواهید به همراه من به زبان PHP8 کدنویسی کنید باید ابتدا آن را نصب کنید. من روش انجام این کار را برای کاربران ویندوز و لینوکس توضیح می دهم.

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

ابتدا به وب سایت رسمی PHP رفته و نسخه ۸ را دانلود کنید. در زمان نوشتن این مقاله آخرین نسخه 8.0.7 بوده است و از این صفحه قابل دانلود می باشد. پس از اینکه فایل فشرده را دانلود کردید آن را به محل مورد نظر خود منتقل کنید (معمولا درایو C) و آن را در پوشه ای از حالت فشرده خارج کنید. در مرحله بعدی از منوی استارت عبارت Environment Variables را جست و جو کنید و زمانی که برایتان نمایش داده شد روی آن کلیک کنید. ما باید متغیرهای محیطی یا Environment Variables ها را به شکلی ویرایش کنیم که php به آن ها اضافه شود. برای این کار به بخش PATH رفته و مسیری که PHP را در آن از حالت فشرده خارج کردید به این بخش اضافه کنید:

اضافه کردن PHP8 به PATH سیستم
اضافه کردن PHP8 به PATH سیستم

حالا دوباره command prompt را در آن باز کنید و دستور php -v را اجرا کنید. اگر همه چیز را درست انجام داده باشید باید نسخه PHP8 به همراه اطلاعاتش برایتان نمایش داده شود.

کاربران لینوکس

فرآیند نصب برای کاربران لینوکس ساده تر است. آقای Ondřej Surý نسخه ۸ PHP را در یک PPA شخصی برایتان آماده کرده است بنابراین تنها کاری که باید انجام بدهید اضافه کردن این PPA به repo های شخصی تان است. برای انجام این کار دستور زیر را اجرا کنید:

sudo add-apt-repository ppa:ondrej/php

با این کار PPA به repo های شخصی ما اضافه شده است. در مرحله بعدی باید دستور زیر را اجرا کنید تا فرآیند نصب آغاز شود:

sudo apt install php8.0

پس از اینکه فرآیند نصب پایان یافت، دستور php -v را اجرا کنید. اگر همه چیز درست انجام شده باشد چنین نتیجه ای را می گیرید:

PHP 8.0.7 (cli) (built: Jun  4 2021 21:26:47) ( NTS )

Copyright (c) The PHP Group

Zend Engine v4.0.7, Copyright (c) Zend Technologies

    with Zend OPcache v8.0.7, Copyright (c), by Zend Technologies

همانطور که می بینید آخرین نسخه PHP برایمان نصب شده است.

سرویس های میزبانی

اگر از سرویس های میزبانی استفاده می کنید، طبیعتا خودتان به سرور دسترسی ندارید بنابراین شرکت میزبانی مورد نظر حتما باید PHP8 را روی آن سرور نصب کرده باشد تا شما بتوانید از بخش کنترل پنل خود آن نسخه را انتخاب کنید. در صورتی که چنین گزینه ای برایتان فعال نیست می توانید یک تیکت به پشتیبانی سرویس میزبانی خود ارسال کرده و بگویید که درخواست فعال سازی PHP8 را دارید.

۱. Union types

زبان های برنامه نویسی از نظر نوع داده (data type) به دو دسته تقسیم می شوند:

  • Dynamically Typed Languages: در این دسته از زبان ها، نوع داده ها در runtime (هنگام اجرا شدن اسکریپت) مشخص می شود و همچنین می تواند تغییر کند. مثلا ما می توانیم یک متغیر حاوی رشته را به یک عدد تبدیل کنیم. PHP در این دسته قرار دارد.
  • Statically Typed Languages: در این دسته از زبان ها نوع داده باید از قبل و در هنگام کدنویسی مشخص باشد (بررسی نوع داده در زمان کامپایل کد اتفاق می افتد) و نمی توانیم به سادگی هر چیزی را به هر چیزی تغییر بدهیم. زبان های C و Java از این دسته هستند.

قابلیت union types بیشتر در زبان های Statically Typed دیده می شود اما PHP آن را برای ما آورده است. Union به معنی «اجتماع» یا «گردهم‌آیی» است. احتمالا همین نام موضوع را برایتان توضیح داده است. Union Type یعنی اجتماع دو یا چند داده مختلف! ما قبل از این موضوع قابلیت type declaration را در زبان PHP داشتیم. مثلا اگر می خواستیم نوع آرگومان یک تابع را مشخص کنیم باید آن تایپ را قبل از آن آرگومان می آوردیم:

function test(bool $param) {}

اما حالا با union ها می توانیم چند نوع داده مختلف را مشخص کنیم:

public function foo(Foo|Bar $input): int|float;

همانطور که می بینید من گفته ام که آرگومان input$ یا باید از نوع Foo یا از نوع Bar باشد (Foo و Bar دو نوع داده خیالی هستند، شما می توانید نوع داده موردنظرتان را به جایشان بگذارید). علامت | به معنی «یا» است. من از همین قابلیت برای تعیین نوع داده برگردانده شده توسط این تابع نیز استفاده کرده ام و گفته ام مقدار برگردانده شده توسط این تابع حتما یا int یا float خواهد بود.

یک مثال ساده تر:

function stringToNumber(string|int $input): int|float

{

    return (int)$input;

}

در اینجا گفته ایم که این تابع یا رشته یا عدد می گیرد و حتما یا عدد صحیح یا عدد اعشاری برمی گرداند. درون بدنه این تابع نیز از int استفاده کرده ام. توجه داشته باشید که مثال های این مقاله فقط برای نمایش یک قابلیت خاص است و لزوما از نظر مدیریت خطا استاندارد نیست. مثلا اگر به تابع بالا رشته 2 را پاس بدهیم هیچ مشکلی نیست:

<?php




function stringToNumber(string|int $input): int|float

{

    return (int)$input;

}




var_dump(stringToNumber("2"));

اجرای این کد مقدار int(2) را برمی گرداند اما پاس دادن رشته ای مانند "A" باعث برگرداندن int(0) می شود چرا که حرف A نمی تواند به عدد تبدیل شود.

سوال بعدی اینجاست که اگر بخواهیم وجود یا عدم وجود یک مقدار را مشخص کنیم چطور؟ در این حالت از null به عنوان یکی از مقادیر استفاده می کنیم:

function func(string|null $input): string

{

    return "sample";

}

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

function func(?string $input): string

{

    return "sample";

}

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

نکته بسیار مهم در union type ها وجود دارد: void هیچگاه نمی تواند بخشی از یک union type باشد. چرا؟ void به معنی برنگشتن هیچ مقداری است و گفتن جمله ای مانند «برنگرداندن هیچ مقداری یا برگرداندن رشته» از نظر منطق PHP غلط است.

۲. JIT

JIT از قابلیت های جدید نسخه ۸ زبان PHP است اما در پروژه های وب هیچ تاثیری ندارد و بیشتر برای پروژه های پردازش گرافیکی و انیمیشن کاربرد دارد و بعید می دانم کسی از شما از آن استفاده کند بنابراین از آن عبور می کنیم.

۳. اپراتور nullsafe

احتمالا می دانید که از نسخه ۷ زبان PHP اپراتوری به نام Null coalescing معرفی شد که به شکل ؟؟ بود. این اپراتور به شما کمک می کرد از نوشتن یک شرط if و بررسی نتیجه isset خلاص شوید. مثال:

<?php




$input = [

    'key' => 'value',

    'nested' => [

        'key' => true

    ]

];




$result = $input['key'] ?? 'fallback';

var_dump($result);

یعنی اگر کلیدی به نام key در آرایه input قرار داشت همان مقدار را استفاده می کنیم و در غیر این صورت از رشته fallback استفاده خواهیم کرد. طبیعتا این کلید در این آرایه وجود دارد بنابراین با اجرای کد بالا مقدار زیر را می گیریم:

string(5) "value"

اگر چنین کلیدی وجود نداشت،‌ نتیجه همان رشته fallback می بود.

مشکل این اپراتور این است که برای متدها کار نمی کند. مثلا فرض کنید متدی به نام getStartDate داشته باشیم و بخواهیم نتیجه آن را بررسی کنیم. برای این کار نمی توانیم مستقیما از این اپراتور استفاده کنیم بلکه باید آن را در یک متغیر ذخیره کرده و سپس مقدار متغیر را در یک معادله جداگانه قرار بدهیم:

$startDate = $booking->getStartDate();




$dateAsString = $startDate ? $startDate->asDateTimeString() : null;

این مشکل با اپراتور nullsafe حل می شود. این اپراتور بدین شکل عمل می کند:

$dateAsString = $booking->getStartDate()?->asDateTimeString();

اضافه کردن علامت ؟ قبل از دسترسی به یک مقدار باعث می شود از بروز خطا جلوگیری کنیم. مثلا فرض کنید داده ای را از یک API گرفته ایم اما نمی دانیم آیا لزوما فیلدهای آن دارای مقدار است یا خیر. برای حل این مشکل می توانیم بدین شکل به فیلدها دسترسی داشته باشیم:

$foo?->bar?->baz()?->boo?->baa();

من در اینجا چهار بار از اپراتور nullsafe استفاده کرده ام. البته در نظر داشته باشید که این اپراتور فقط برای خواندن داده است و نمی توانید با استفاده از آن داده های جدیدی را بنویسید:

$offer?->invoice?->date = new DateTime();

این کد باعث بروز خطا می شود چرا که از اپراتور nullsafe برای نوشتن داده (ساخت یک نمونه جدید از DateTime) استفاده کرده ایم.

۴. آرگومان های اسمی (Named arguments)

همانطور که می دانید در حالت عادی زمانی که یک تابع را تعریف می کنید، ترتیب پاس دادن مقادیر مهم است چرا که مشخص می کند هر مقدار متعلق به کدام آرگومان است. مثال:

<?php




function printName($name, $last_name)

{

    var_dump($name . $last_name);

}




printName("Amir", "Zouerami");

printName("Zouerami", "Amir");

هر مقداری که اول پاس داده شود برابر name$ خواهد بود بنابراین با اجرای کد بالا چنین نتیجه ای را می گیریم:

string(12) "AmirZouerami"

string(12) "ZoueramiAmir"

همانطور که می بینید نتیجه کاملا متفاوت است. آرگومان های اسمی به شما کمک می کنند در هنگام پاس دادن آرگومان ها، آن ها را با نامشان صدا کنید تا دیگر ترتیب اهمیتی نداشته باشد:

<?php




function printName($name, $last_name)

{

    var_dump($name . $last_name);

}




printName(name: "Amir", last_name: "Zouerami");

printName(last_name: "Zouerami", name: "Amir");

همانطور که می بینید در هنگام پاس دادن مقادیر، آن ها را با نام متغیرهایشان صدا زده ایم.  با اجرای کد بالا چنین نتیجه ای را می گیریم:

string(12) "AmirZouerami"

string(12) "AmirZouerami"

بنابراین هر دو روش فراخوانی عینا یک نتیجه را به ما داده اند.

حالا که ویژگی های جدید و مهم را توضیح دادیم بهتر است به سراغ تغییر مهم برویم تا در هنگام به روز رسانی سرور با مشکل روبرو نشوید.

۵. گزارش خطا

در نسخه های قبلی PHP، هشدار های مربوط به دستورات منسوخ شده (deprecation) و خطاهای مربوط به حالت strict به صورت پیش فرض پنهان می شدند اما از PHP8 حالت گزارش خطا روی E_ALL است بنابراین همه چیز نمایش داده می شود. این مسئله بسیار مهم است چرا که اگر قبلا این دسته از هشدار ها و خطا ها را روی سرور خودتان تصحیح نکرده بودید، ممکن است با به روز رسانی ناگهانی نسخه PHP اطلاعات حساس شما نشت پیدا کند.

همچنین اپراتور error suppression که به شکل @ است دیگر باعث پنهان شدن خطاهای fatal نمی شود.

در نهایت باید بدانید که حالت پیش فرض گزارش خطا برای رابط PDO روی exceptions تنظیم شده است. ما برای گزارش خطای PDO سه حالت ERRMODE_SILENT و ERRMODE_WARNING و ERRMODE_EXCEPTION را داشتیم. از این به بعد حالت پیش فرض، حالت سوم است.

۶. صدا زدن متدهای غیر استاتیک به صورت استاتیک

در نسخه های قبلی PHP صدا زدن متدهای غیر استاتیک به صورت استاتیک مجاز بود اما از PHP8 به بعد این کار باعث پرتاب یک خطای fatal می شود. مثال:

class Foo {

    public function bar() {}

}

Foo::bar();

این کد یک خطا برایتان برمی گرداند.

۷. رابطه بین اپراتورهای + و - و .

از PHP8 به بعد زمانی که از اپراتور concatenation (علامت نقطه) استفاده می کنید، اپراتورهای + و - دارای اولویت (precedence) بالاتر خواهند بود. به مثال زیر توجه کنید:

echo 35 + 7 . '.' . 0 + 5;

به نظر شما نتیجه این کد چه مقداری است؟ در PHP7 و قبل تر نتیجه اجرای کد بالا 47 بود اما در PHP8 به بعد برابر با 42.5 می باشد.

۸. ارث بری در متدهای private

متدهای خصوصی (private) کلاس ها در PHP8 توسط کلاس های فرزند به ارث برده نمی شوند بنابراین کلاس های فرزند می توانند متدی با همان نام در خودشان داشته باشند.

۹. متد substr

متد substr به شما اجازه می دهد قسمتی از یک رشته را جدا کنید. در نسخه های قبل از PHP8 اگر offset پاس داده شده به این متد خارج از حدود کل رشته بود، مقدار false را دریافت می کردید. این مسئله برای متدهای mb_substr و iconv_substr و graphme_substr نیز صحیح بود مثال:

substr('FooBar', 42, 3); // false

mb_substr('FooBar', 42, 3); // ""

iconv_substr('FooBar', 42, 3); // false

grapheme_substr('FooBar', 42, 3); // false

مقادیر برگشتی توسط هر متد را به صورت کامنت روبروی آن نوشته ام. همانطور که می بینید تنها متدی که به جای برگرداندن false یک رشته خالی را برمی گرداند متد mb_substr است.  از PHP8 به بعد رفتار تمام این متدها به طور کل عوض شده و همگی مانند mb_substr یک رشته خالی را برمی گردانند. مثال:

substr('FooBar', 42); // ""

iconv_substr('FooBar', 42); // ""

grapheme_substr('FooBar', 42); // ""

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

۱۰. تغییر افزونه GD

اگر فایل php.ini را از PHP7 کپی کرده و برای یک سرور PHP8 قرار بدهید همه چیز بهم می ریزد چرا که افزونه GD از php_gd2.dll به php_gd.dll تغییر نام پیدا کرده است بنابراین باید آن را بدین شکل ویرایش کنید.

- extension=gd2

+ extension=gd

این نکته مخصوص کاربران ویندوز است. اگر از کاربران لینوکس هستید افزونه GD با همان نام قدیمی اش (gd.so) موجود است بنابراین مشکلی نخواهید داشت.

۱۱. تابع crypt

تابع crypt از این به بعد پارامتر salt را اجباری کرده است. در نسخه های قبلی اگر salt را پاس نمی دادید، یک هشدار از نوع notice به شما نمایش داده می شد اما از این به بعد خطا دریافت می کنید. همانطور که می دانید تابع crypt قدیمی است و به طور کلی استفاده از آن پیشنهاد نمی شود (از جایگزین بهتر آن ()password_hash استفاده کنید) اما در صورتی که بنا به دلیلی نیاز به crypt دارید باید این نکته را بدانید.

۱۲. رفتار جدید Assertion ها

اگر دوست دارید برای برنامه هایتان تست بنویسید حتما با assertion ها آشنا هستید. در نسخه های قبلی PHP اگر یک assertion صحیح نبود فقط یک هشدار (warning) را پرتاب می کرد. مثال:

assert(true === false);

// Warning: assert(): assert(true === false) failed in ... on line ...




اما از PHP8 به بعد هر assertion باعث پرتاب یک exception می شود:

assert(true === false);

// Fatal error: Uncaught AssertionError: assert(true === false) in ...:...

طبیعتا این تغییر رفتار ممکن است تست های برنامه شما را خراب کند، بنابراین حتما قبل از به روز رسانی سرور این موضوع را در ذهن خود داشته باشید.

نویسنده شوید

دیدگاه‌های شما

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