PHP 8: ثورة في اللغة
يجلب PHP 8 تحسينات كبرى تُحدِّث اللغة وترفع الأداء بفضل مُصرِّف JIT.
صدر PHP 8.0 في نوفمبر 2020، وافتتح سلسلة إصدارات سنوية غيّرت بعمق طريقة كتابة PHP: أنواع أكثر تعبيراً، وصياغة أكثر إيجازاً، وكائنات غير قابلة للتعديل، وتعدادات (enums) أصلية. تعتمد شيفرة Symfony أو Laravel الحديثة كثيراً على هذه الميزات، وكثيراً ما تشترط المكتبات الحديثة PHP 8.2 أو أحدث. يستعرض هذا المقال الميزات الأكثر تأثيراً في العمل اليومي، إصداراً بعد إصدار، مع فائدة كل منها ومزالقها.
ملاحظة قبل البدء: يحصل كل إصدار فرعي من PHP على سنتين من الدعم النشط ثم سنتين من إصلاحات الأمان. البقاء على إصدار منتهي الدعم لا يعني فقط الحرمان من الميزات، بل يعني قبل كل شيء التعرّض لثغرات لن تُصلَح. راجع صفحة الإصدارات المدعومة لتخطيط الترقيات.
الوسائط المسمّاة (Named Arguments)
تتيح الوسائط المسمّاة تمرير معامل باسمه بدلاً من موضعه. لم يعد ضرورياً تكرار كل القيم الافتراضية لتغيير المعامل الأخير فقط، ويصبح الاستدعاء موثَّقاً بذاته:
// Before
htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);
// PHP 8
htmlspecialchars($string, double_encode: false);
الوجه الآخر: أصبحت أسماء المعاملات جزءاً من الواجهة العامة للدالة. إعادة تسمية معامل في مكتبة قد تكسر شيفرة مستخدميها. إن كنت تصون حزمة، فاعتبر هذه التسميات تغييرات غير متوافقة.
ترقية الخصائص، وأنواع الاتحاد، والمعامل nullsafe
قدّم PHP 8.0 أيضاً ثلاث ميزات نستخدمها في كل ملف تقريباً. ترقية الخصائص في المُنشئ تحذف شيفرة التصريح والإسناد المتكررة. أنواع الاتحاد (int|string) تصف بدقة القيم المقبولة. والمعامل nullsafe ?-> يوقف سلسلة الاستدعاءات بمجرد أن تكون إحدى حلقاتها null:
final class InvoiceService
{
public function __construct(
private InvoiceRepository $invoices,
private ?LoggerInterface $logger = null,
) {}
public function findReference(int|string $id): ?string
{
$invoice = $this->invoices->find($id);
// null if the invoice, customer or address does not exist
return $invoice?->getCustomer()?->getAddress()?->getCountryCode();
}
}
يُضاف إلى ذلك دوال طال انتظارها (str_contains() وstr_starts_with() وstr_ends_with())، والنوع mixed، وإمكانية استخدام throw كتعبير، مثل $value ?? throw new InvalidArgumentException().
تعبير Match
match نسخة أكثر أماناً من switch: فهو تعبير يُرجع قيمة، والمقارنة فيه صارمة (===)، ولا يوجد break قد يُنسى، وإن لم يطابق أي فرع ولم يوجد default يرمي PHP الخطأ UnhandledMatchError بدلاً من المتابعة بصمت.
$status = match($code) {
200 => 'OK',
301 => 'Redirect',
404 => 'Not Found',
500 => 'Server Error',
default => 'Unknown',
};
انتبه إلى المقارنة الصارمة: إن كان $code قادماً من سلسلة نصية ('200')، فلن يطابق أي فرع رقمي. حوِّل القيمة مسبقاً. يمكن لعدة قيم أن تتشارك فرعاً واحداً بفصلها بفواصل (301, 302 => 'Redirect')، ويسمح match(true) بتقييم شروط عشوائية.
التعدادات Enums (PHP 8.1)
تحلّ التعدادات محلّ ثوابت الأصناف والسلاسل «السحرية». التعداد المدعوم (backed) يربط كل حالة بقيمة عددية أو نصية، ما يسهّل تخزينه في قاعدة البيانات أو تحويله إلى JSON. ويمكنه أن يحمل دوالاً وأن ينفّذ واجهات:
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
public function label(): string {
return match($this) {
self::Active => 'Active',
self::Inactive => 'Inactive',
self::Pending => 'Pending',
};
}
}
الدوال المولَّدة تلقائياً تغطي الاستخدامات الشائعة:
$status = Status::from('active'); // Status::Active
$maybe = Status::tryFrom('archived'); // null instead of an exception
$all = Status::cases(); // list of all cases
function activate(User $user): void
{
// the type guarantees a valid value, no manual validation
$user->setStatus(Status::Active);
}
يتعامل Doctrine ORM (عبر enumType على العمود) والـ Serializer ونماذج Symfony (EnumType) مع التعدادات أصلاً. استخدم from() حين تكون القيمة غير الصالحة خطأً برمجياً، وtryFrom() لمدخلات المستخدم.
الألياف Fibers (PHP 8.1)
الـ Fibers دوال قابلة للمقاطعة: يمكنها تعليق تنفيذها واستئنافه لاحقاً مع الاحتفاظ بمكدّس الاستدعاءات. هي لا تُنشئ توازياً (كل شيء يعمل في خيط واحد)، لكنها تتيح كتابة شيفرة غير متزامنة تبدو كأنها متزامنة.
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('first pause');
echo "Received: $value\n";
});
$result = $fiber->start(); // 'first pause'
$fiber->resume('data');
عملياً، نادراً ما نتعامل مع الـ Fibers مباشرة. إنها لبنات منخفضة المستوى تستخدمها مكتبات مثل AMPHP أو حلقة الأحداث Revolt، التي تنسّق التعليق والاستئناف أثناء عمليات الإدخال والإخراج الشبكية.
إضافات أخرى في PHP 8.1
- صياغة الاستدعاء من الدرجة الأولى:
$fn = strlen(...);تُنشئClosureمن أي دالة أو طريقة. - نوع الإرجاع
neverللدوال التي ترمي استثناءً دائماً أو تُنهي السكربت. newفي المُهيِّئات: يمكن استخدام كائن كقيمة افتراضية لمعامل، كما في مثالUserأدناه.array_is_list()للتحقق من أن مفاتيح المصفوفة متتالية 0، 1، 2…
الخصائص Readonly (PHP 8.1) والأصناف readonly (PHP 8.2)
الخاصية readonly لا تُهيَّأ إلا مرة واحدة، ومن داخل نطاق الصنف. إنها مثالية لكائنات القيمة (value objects) وكائنات DTO والأحداث، التي يجب ألا تتغير بعد إنشائها:
class User {
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly DateTimeImmutable $createdAt = new DateTimeImmutable(),
) {}
}
يسمح PHP 8.2 بالتصريح عن الصنف كله بأنه readonly، ما يغني عن تكرار الكلمة المفتاحية على كل خاصية. و«لتعديل» مثل هذا الكائن، نُنشئ نسخة منه بالقيمة الجديدة (دوال with…()):
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
public function withAmount(int $amount): self
{
return new self($amount, $this->currency);
}
}
حدود يجب معرفتها: يجب أن تكون خاصية readonly محدَّدة النوع، ولا تقبل قيمة افتراضية في تصريحها، وعدم قابلية التعديل سطحي. فإن احتوت الخاصية كائناً قابلاً للتعديل، فيمكن دائماً تعديل ذلك الكائن. ومن هنا فائدة DateTimeImmutable بدلاً من DateTime. جلب PHP 8.2 أيضاً أنواع DNF ((Countable&ArrayAccess)|null) وأهمل (deprecated) إنشاء الخصائص الديناميكية.
PHP 8.3 و8.4: أحدث الإضافات
قدّم PHP 8.3 ثوابت الأصناف محدَّدة النوع، والسمة #[\Override] (يرمي PHP خطأً إن كانت الطريقة لا تعيد تعريف أي شيء فعلياً، مثلاً بعد إعادة تسمية في الصنف الأب)، والدالة json_validate(). وأضاف PHP 8.4 ميزتين مهمتين للنمذجة: خطافات الخصائص (property hooks) التي تربط منطق القراءة أو الكتابة بالخاصية مباشرة، والرؤية غير المتماثلة التي تسمح مثلاً بقراءة عامة وكتابة خاصة:
final class Product
{
public const string DEFAULT_CURRENCY = 'EUR'; // PHP 8.3
public private(set) string $status = 'draft'; // PHP 8.4
public int $priceInCents { // PHP 8.4
set(int $value) {
if ($value < 0) {
throw new InvalidArgumentException('The price must be positive.');
}
$this->priceInCents = $value;
}
}
public string $formattedPrice {
get => number_format($this->priceInCents / 100, 2).' '.self::DEFAULT_CURRENCY;
}
public function __construct(int $priceInCents)
{
$this->priceInCents = $priceInCents;
}
public function publish(): void
{
$this->status = 'published';
}
}
يسمح PHP 8.4 أيضاً بسَلسلة استدعاء مباشرة بعد new دون أقواس إضافية (new Foo()->bar())، ويضيف دوالاً مثل array_find() وarray_any(). ويواصل PHP 8.5، الصادر في نوفمبر 2025، في الاتجاه نفسه، ولا سيما بمعامل الأنبوب |>.
أداء JIT
يحسّن مُصرِّف JIT الأداء بمقدار 2 إلى 3 أضعاف في العمليات كثيفة الاستخدام للمعالج. فعِّله في php.ini:
opcache.jit=1255
opcache.jit_buffer_size=100M
لكن يجب التدقيق: يُصرِّف JIT الـ bytecode إلى شيفرة آلة، وهذا يفيد أساساً الحسابات البحتة (معالجة الصور، الخوارزميات الرياضية، المحلّلات). أما تطبيق الويب المعتاد فيقضي معظم وقته في انتظار قاعدة البيانات أو الشبكة أو نظام الملفات، والمكسب فيه ضئيل عادةً. في المقابل، يبقى OPcache ضرورياً في كل الحالات، ولا يعمل JIT إلا إن كان مفعَّلاً. القيمتان tracing وfunction اسمان مستعاران أوضح لـ opcache.jit، ومنذ PHP 8.4 أصبح JIT معطَّلاً افتراضياً (opcache.jit=disable). قِس بحِمل تطبيقك الفعلي قبل تفعيله في الإنتاج.
قائمة تحقق للانتقال إلى PHP 8.x
- اقرأ أدلة الترحيل الرسمية لكل إصدار على php.net، وخاصة أقسام «التغييرات غير المتوافقة» و«الميزات المهمَلة».
- فعِّل عرض رسائل الإهمال في بيئة الاختبار (
error_reporting=E_ALL) وأصلحها قبل تغيير الإصدار. - شغّل تحليلاً ساكناً (PHPStan أو Psalm) وأتمِت إعادة الكتابة المتكررة باستخدام Rector.
- حدِّث اعتماديات Composer وتحقق من القيد
"php"فيcomposer.json. - شغّل مجموعة الاختبارات كاملة على الإصدار الجديد في CI قبل تحويل بيئة الإنتاج.
اعتماد هذه الميزات تدريجياً، ملفاً بعد ملف، يكفي لجعل قاعدة الشيفرة أكثر أماناً ووضوحاً: الأنواع تكشف الأخطاء مبكراً، والتعدادات والكائنات readonly تقضي على فئات كاملة من الأخطاء، وmatch يجعل التفرعات الشاملة صريحة.