بعد شهر من العمل على متجر القهوة والتمور بـ PHP، صار الكود الذي تكتبه يعمل، وتمر اختباراته، ويقبله PHPStan في أغلب الأحيان. ولكن في مراجعة الكود Code Review يكتب لك الزميل الذي يعمل بـ PHP منذ سنوات تعليقات صغيرة لا تفهم سببها أول مرة: «لا تستخدم empty هنا»، «أضف true في آخر in_array»، «احذف & من هذه الحلقة»، «استخدم DateTimeImmutable». وكل تعليق منها يبدو تفضيلاً شخصياً، إلى أن يصلك تقرير من الدعم أن سعر الهيل في الفاتورة صار مثل سعر التمر، أو أن عميلاً أدخل كود الخصم «0» فتجاهله المتجر. وهذه هي الأخطاء التي يقع فيها المطور القادم من لغة أخرى: الكود صحيح في Python أو JavaScript أو Java، ويعمل في PHP دون أي رسالة، ولكنه يعطي نتيجة أخرى.
وهذا المقال الأخير في مرحلة لغة PHP للمطورين، وفيه ثلاثة عشر خطأ من هذا النوع، كل خطأ في قسم مستقل من ملف واحد mistakes.php يشغل النسخة الخاطئة ثم النسخة الصحيحة، ونقرأ المخرج الحقيقي من PHP 8.5.12. وبعدها نرى أي هذه الأخطاء أشار إليها PHPStan وأيها مر دون أي تنبيه، ثم نضع خطة قراءة من المصادر الرسمية لمن يريد أن يكمل. وبعض هذه الأخطاء لمسناها في المقالات السابقة، مثل المقارنة == في المقال الأول، والمصفوفة التي تصبح كائناً في JSON، فهنا نجمعها في مكان واحد كمرجع ترجع إليه.
وسوف نناقش في هذا المقال ما يلي:
- كيف شغلنا التجربة، ولماذا ثلاثة عشر خطأ بالذات.
- أخطاء المصفوفات Arrays: المرجع في
foreach، وarray_mergeوالمفاتيح، وunsetقبل JSON، وsortالتي تعيدtrue. - أخطاء النصوص Strings:
strlenبالبايت مع العربية، وstrposالتي تعيد صفراً. - أخطاء المقارنة Comparison:
array_searchوin_array، وemptyمع «0»، وswitch. - الكائنات والتواريخ:
DateTimeالقابل للتعديل والمنطقة الزمنية، وcloneالسطحي. - الأمان: حقن SQL، وتخزين الباسورد بـ md5.
- ما الذي كشفه PHPStan وما الذي فاته، ثم خطة قراءة من المصادر الرسمية.
التجربة Lab: ملف واحد وثلاثة عشر قسماً
اخترنا الأخطاء من معيار واحد: أن يكون الكود الخاطئ طبيعياً لمن جاء من لغة أخرى، وأن يعمل في PHP دون أي خطأ أو تحذير، وأن يعطي نتيجة مختلفة عما يتوقعه. فالأخطاء التي يرفضها PHP بنفسه، مثل نوع خاطئ مع strict_types، لا تحتاج مقالاً، لأن PHP يخبرك بها. وكل قسم في الملف يطبع سطر WRONG بالنسخة الخاطئة وسطر FIXED بالنسخة الصحيحة، ويمكن تشغيل قسم واحد برقمه:
docker run --rm -v "$PWD:/app" -w /app slp-php-cli:8.5 php mistakes.php
docker run --rm -v "$PWD:/app" -w /app slp-php-cli:8.5 php mistakes.php 9وفي أول الملف سطر date_default_timezone_set('UTC') يحاكي ما يحدث في صورة Docker الرسمية عندما لا يحدد php.ini أي منطقة زمنية، وسوف ترى أثره في الخطأ التاسع. والشكل التالي يلخص الأخطاء الثلاثة عشر في خمس مجموعات، ومعها ما إذا كان PHPStan قد أشار إليها في التجربة:

ويمكنك أن تستخدم الملف نفسه بطريقة أخرى إذا كنت تقود فريقاً يستقبل مطورين جدداً من لغات أخرى: اطلب من المطور الجديد أن يقرأ كل قسم ويتوقع النتيجة قبل أن يشغله، ثم يقارن توقعه بالمخرج. والفكرة أن القسم الذي يتوقع المطور نتيجته بشكل خاطئ هو الخطأ المرشح أن يكتبه في أول شهر، لأن التوقع الخاطئ يعني أن عادته من لغته السابقة ما زالت تقود يده، وهذه العادات تختلف حسب اللغة التي جاء منها: القادم من JavaScript يتوقع أن sort تعيد المصفوفة، والقادم من Python يتوقع أن strlen تعد الحروف، والقادم من Java يتوقع أن switch صارم. وبعد ساعة واحدة مع الملف يصبح عنده قائمة شخصية قصيرة يراجع بها كوده قبل أن يرسله للمراجعة، وهذه القائمة الشخصية أنفع له من أي قائمة عامة، لأنها مبنية على عاداته هو وليس على عادات غيره. وأضف إلى الملف كل خطأ جديد يظهر في مراجعات فريقك، بنسخة خاطئة ونسخة صحيحة ومخرج حقيقي، فيصبح مع الوقت دليل الفريق نفسه المبني على أخطائه الحقيقية، بدل وثيقة طويلة لا يقرؤها أحد بعد أسبوعها الأول.
أخطاء المصفوفات Arrays
المصفوفة في PHP نوع واحد للقائمة والقاموس، وتنسخ كقيمة، كما رأينا في المقال الأول، وأغلب أخطاء هذه المجموعة تأتي من نسيان واحدة من هاتين الحقيقتين.
الخطأ 1: المرجع Reference الذي يبقى بعد foreach
القادم من Python أو JavaScript يريد أن يعدل عناصر المصفوفة داخل الحلقة، فيتعلم أن PHP يحتاج & لذلك، ويكتب:
$prices = [990, 4500, 1250];
foreach ($prices as &$p) {
$p = intdiv($p * 115 + 50, 100);
}
foreach ($prices as $p) { // $p is still a reference to $prices[2]
}
echo 'WRONG ', json_encode($prices), "\n";WRONG [1139,5175,5175]
FIXED [1139,5175,1438]
والسعر الأخير، الهيل بعد الضريبة 1438، صار 5175 وهو سعر التمر. والسبب أن المتغير $p لا يختفي بعد الحلقة الأولى في PHP، لأن نطاق المتغيرات في PHP هو الدالة وليس الحلقة، فيبقى مرجعاً إلى العنصر الأخير. ثم تأتي الحلقة الثانية، وكل دورة فيها تكتب قيمة العنصر الحالي في $p، أي في العنصر الأخير، حتى تصل إلى العنصر الأخير نفسه فتكتب فيه قيمته الجديدة. وهذا السلوك موثق بتحذير صريح في توثيق foreach. والإصلاح الذي في الملف هو array_map(fn (int $p) => intdiv($p * 115 + 50, 100), $prices)، والذي يعيد مصفوفة جديدة ولا يعدل الأصل، وهذا أوضح عند القراءة. وإذا كنت تحتاج التعديل في المكان لأن المصفوفة كبيرة جداً، فاكتب unset($p) في السطر التالي للحلقة مباشرة، دائماً.
الخطأ 5: array_merge تعيد ترقيم المفاتيح Keys
في المتجر نحفظ المخزون في مصفوفة مفاتيحها أرقام المنتجات، ونريد أن ندمج مخزون مستودعين. والقادم من JavaScript يبحث عن ما يقابل {...a, ...b} فيجد array_merge:
$stockA = [101 => 5, 205 => 2]; // product id => qty
$stockB = [307 => 9];
echo 'WRONG ', json_encode(array_merge($stockA, $stockB)), "\n";
echo 'FIXED ', json_encode($stockA + $stockB), ' | array_replace: ', json_encode(array_replace($stockA, [205 => 7])), "\n";WRONG [5,2,9]
FIXED {"101":5,"205":2,"307":9} | array_replace: {"101":5,"205":7}اختفت أرقام المنتجات كلها، لأن array_merge تعامل المفاتيح الرقمية كمواقع في قائمة، فتعيد ترقيمها من الصفر، وتحفظ المفاتيح النصية فقط. أما العامل + فيحفظ كل المفاتيح، وعند التكرار يأخذ قيمة المصفوفة الأولى، وarray_replace يحفظها أيضاً ويأخذ قيمة الثانية. والقاعدة: إذا كانت مفاتيح المصفوفة معرفات IDs فهي قاموس، ولا تستخدم معها array_merge أبداً. ونفس الخطر في array_merge(...$lists) عندما تجمع قوائم، فهي صحيحة هناك لأن المفاتيح مواقع.
الخطأ 6: unset في قائمة يحول JSON إلى كائن
رأينا هذا في المقال الأول باختصار، وهنا بمخرجه:
$items = ['قهوة', 'تمر', 'هيل'];
unset($items[1]);
echo 'WRONG ', json_encode($items, JSON_UNESCAPED_UNICODE), "\n";WRONG {"0":"قهوة","2":"هيل"}
FIXED ["قهوة","هيل"] | array_filter+values: ["قهوة","هيل"]
بعد unset صارت المفاتيح 0 و2، وهذه ليست قائمة متصلة، فكتبها json_encode ككائن JSON، والتطبيق الذي ينتظر قائمة ويستدعي .map() عليها سوف يتوقف. ونفس الشيء يحدث مع array_filter لأنها تحفظ المفاتيح الأصلية، وهذا أكثر شيوعاً لأن من يكتبها لا يرى أي unset. والإصلاح أن تمرر النتيجة على array_values قبل أي json_encode، أو أن تتحقق بـ array_is_list في الاختبار. وفي المقال الرابع رأيت أن قاعدة التحقق list في Laravel تمنع العكس، أي أن يرسل العميل كائناً بدل القائمة.
الخطأ 8: sort ترتب في المكان وتعيد true
في Python تعيد sorted(prices) قائمة جديدة، وفي JavaScript تعيد prices.sort() نفس المصفوفة بعد ترتيبها، فيكتب القادم منهما:
$prices = [4500, 990, 1250];
$sorted = sort($prices);
echo 'WRONG $sorted = ', var_export($sorted, true), "\n";WRONG $sorted = true
FIXED [990,1250,4500] | asort keeps keys: {"قهوة":990,"هيل":1250}دوال الترتيب في PHP تأخذ المصفوفة بالمرجع، وترتبها في مكانها، وتعيد true دائماً منذ PHP 8.2، كما في توثيق sort. ولاحظ أن sort تحذف المفاتيح وتعيد ترقيمها، فإذا كانت المفاتيح أسماء منتجات أو معرفات فاستخدم asort للترتيب حسب القيمة مع حفظ المفاتيح، وksort للترتيب حسب المفتاح، وusort مع العامل <=> للترتيب بقاعدتك. والأسماء نفسها تخبرك: الحرف a يعني Associative أي يحفظ المفاتيح، وk يعني حسب المفتاح، وu يعني بدالة منك.
أخطاء النصوص Strings
الخطأ 2: strlen وsubstr تعدان البايتات Bytes
في Python 3 وJava وJavaScript النص سلسلة من الحروف، أما في PHP فهو سلسلة من البايتات، وكل حرف عربي في UTF-8 بايتان. والمتجر يعرض أول أربعة حروف من اسم المنتج في إشعار قصير:
$name = 'قهوة خولانية';
echo 'WRONG strlen=', strlen($name), ' substr=', substr($name, 0, 5), ' valid utf-8? ', var_export(mb_check_encoding(substr($name, 0, 5), 'UTF-8'), true), "\n";
echo 'FIXED mb_strlen=', mb_strlen($name), ' mb_substr=', mb_substr($name, 0, 4), ' strtoupper(é)=', strtoupper('é'), ' mb_strtoupper(é)=', mb_strtoupper('é'), "\n";WRONG strlen=23 substr=قه� valid utf-8? false
FIXED mb_strlen=12 mb_substr=قهوة strtoupper(é)=é mb_strtoupper(é)=É
الاسم «قهوة خولانية» فيه 12 حرفاً بالمسافة، وstrlen أعطت 23 لأنها تعد 11 حرفاً عربياً بايتين لكل منها مع مسافة ببايت واحد. وsubstr($name, 0, 5) أخذت خمسة بايتات، أي حرفين ونصف، فخرج نص غير صالح بترميز UTF-8، وهذا النص المكسور إذا وصل إلى json_encode يجعلها تفشل وتعيد false. والدوال الصحيحة من إضافة mbstring تبدأ بـ mb_ وتعد الحروف. ولاحظ أيضاً أن strtoupper('é') لم تغير شيئاً، لأن الدوال الأساسية منذ PHP 8.2 لا تعرف إلا حروف ASCII مهما كانت إعدادات اللغة Locale. والقاعدة لأي متجر أو نظام عربي: كل عملية على طول النص أو جزء منه أو حالة حروفه تكون بـ mb_، وحدود الطول في التحقق من المدخلات بالحروف وليس بالبايتات.
الخطأ 3: strpos تعيد صفراً عندما يكون النص في البداية
رموز المنتجات السعودية في المتجر تبدأ بـ SA، ونريد أن نعرضها بعلامة خاصة:
$sku = 'SA-COFFEE-01';
echo 'WRONG ', strpos($sku, 'SA') ? 'Saudi product' : 'not Saudi', "\n";
echo 'FIXED ', str_starts_with($sku, 'SA') ? 'Saudi product' : 'not Saudi', ' | str_contains: ', var_export(str_contains($sku, 'COFFEE'), true), "\n";WRONG not Saudi
FIXED Saudi product | str_contains: trueالدالة strpos تعيد موقع النص، وهو هنا صفر لأن SA في البداية، وتعيد false إذا لم تجده. والصفر في الشرط يعامل كـ false، فخرج المنتج السعودي «غير سعودي». وهذا خطأ قديم جداً لدرجة أن PHP 8.0 أضاف ثلاث دوال لتجنبه: str_contains وstr_starts_with وstr_ends_with، وكلها تعيد bool فقط. وإذا احتجت الموقع نفسه فقارن بـ !== false، ولا تستخدم strpos في شرط مباشرة أبداً.
أخطاء المقارنة Comparison
الخطأ 4: array_search وin_array
نفس مشكلة الصفر تتكرر مع array_search، ومعها مشكلة المقارنة المتساهلة التي رأيناها في المقال الأول:
$codes = ['10', '20', '30'];
$i = array_search('10', $codes);
echo 'WRONG ', $i ? "found at {$i}" : 'not found', ' | in_array("1e1"): ', var_export(in_array('1e1', $codes), true), "\n";
$i = array_search('10', $codes, true);
echo 'FIXED ', $i !== false ? "found at {$i}" : 'not found', ' | in_array("1e1", strict): ', var_export(in_array('1e1', $codes, true), true), "\n";WRONG not found | in_array("1e1"): true
FIXED found at 0 | in_array("1e1", strict): falseالكود «10» موجود في الموقع صفر، فخرج «غير موجود». وفي نفس السطر in_array('1e1', $codes) أعطت true، أي أن PHP وجد «1e1» في قائمة ليس فيها، لأن النصين الرقميين يقارنان كرقمين، و1e1 عشرة. والدالتان array_search وin_array تقبلان معاملاً ثالثاً strict، وهو false افتراضياً لأسباب التوافق مع الكود القديم. والقاعدة: اكتب true في المعامل الثالث دائماً، وقارن نتيجة array_search بـ !== false. وتخيل أن القائمة هي قائمة أكواد خصم صالحة، وأن المستخدم يرسل «1e1».
الخطأ 7: empty تعتبر «0» فارغاً
عميل أدخل كود خصم «0»، وهو كود حقيقي في حملة سابقة، فتجاهله المتجر:
$discountCode = '0'; // a real code typed by the customer
echo 'WRONG ', empty($discountCode) ? 'no code' : "code {$discountCode}", "\n";WRONG no code
FIXED code 0 | isset+!== "": trueempty تعيد true لكل قيمة تساوي false عند التحويل، وهذه القائمة تشمل النص "0" مع "" و0 و0.0 وnull والمصفوفة الفارغة. والنص «0» حالة خاصة في PHP لا توجد في Python ولا JavaScript، فهناك النص غير الفارغ دائماً صحيح. والإصلاح أن تقول ما تقصده بالضبط: إذا كنت تقصد «نص فارغ» فاكتب $code === ''، وإذا كنت تقصد «غير موجود» فاكتب isset أو === null. ولا تستخدم empty مع مدخلات المستخدم أبداً.
الخطأ 11: switch يقارن بـ == وmatch يقارن بـ ===
$code = '1e1';
switch ($code) {
case '10':
$r = 'matched 10';
break;
default:
$r = 'no match';
}
echo "WRONG switch('1e1'): {$r}\n";
echo "FIXED match('1e1'): ", match ($code) { '10' => 'matched 10', default => 'no match' }, "\n";WRONG switch('1e1'): matched 10
FIXED match('1e1'): no matchالقادم من Java أو C يتوقع أن switch يقارن بدقة، ولكنه في PHP يستخدم ==، فطابق «1e1» الحالة «10». وmatch الذي أضيف في PHP 8.0 يستخدم ===، ويعيد قيمة، ويرمي UnhandledMatchError إذا لم تطابق أي حالة ولم يكن هناك default، ولا يحتاج break فلا يمكن أن تنسى واحدة فيمر التنفيذ إلى الحالة التالية. والقاعدة: في أي كود جديد استخدم match، واترك switch للحالات التي تحتاج فيها عدة أسطر في كل حالة، مع الانتباه إلى أنواع القيم.
أخطاء الكائنات والتواريخ Objects and Dates
الخطأ 9: DateTime قابل للتعديل والمنطقة الزمنية UTC
طلب أنشئ الساعة 10:30 مساءً بتوقيت السعودية، ونريد تاريخ التسليم بعد يومين:
$created = new DateTime('2026-10-31 22:30:00');
$due = $created->modify('+2 days');
echo 'WRONG created=', $created->format('Y-m-d H:i'), ' due=', $due->format('Y-m-d H:i'), ' tz=', date_default_timezone_get(), "\n";
$created = new DateTimeImmutable('2026-10-31 22:30:00', new DateTimeZone('Asia/Riyadh'));
$due = $created->modify('+2 days');WRONG created=2026-11-02 22:30 due=2026-11-02 22:30 tz=UTC
FIXED created=2026-10-31 22:30 +03 due=2026-11-02 22:30 +03 in UTC=2026-10-31 19:30في المخرج الخاطئ مشكلتان. الأولى أن تاريخ الإنشاء نفسه تغير إلى 2 نوفمبر، لأن modify في DateTime تعدل الكائن نفسه وتعيده، فـ $created و$due هما نفس الكائن، وهذا ما رأيناه في المقال الأول عن الكائنات التي تمرر كمقبض. وDateTimeImmutable تعيد كائناً جديداً في كل تعديل، فيبقى الأصل كما هو، وهي ما تستخدمه أطر العمل الحديثة. والمشكلة الثانية أن المنطقة الزمنية الافتراضية UTC، فالساعة 22:30 فهمت بتوقيت UTC، وهي في الحقيقة 1:30 فجراً من اليوم التالي بتوقيت السعودية، والطلب قد يظهر في تقرير اليوم الخطأ. والإصلاح أن تحدد المنطقة الزمنية صراحة عند إنشاء التاريخ، وأن تحفظ في قاعدة البيانات بنوع يحمل المنطقة مثل timestamptz كما في المقال الرابع، وأن تحول إلى توقيت العرض عند العرض فقط. والسطر الأخير يبين ذلك: 22:30 بتوقيت السعودية هي 19:30 بتوقيت UTC.
الخطأ 10: clone نسخ سطحي Shallow Copy
القادم من Java يعرف هذا الخطأ، والقادم من JavaScript يعرفه من {...obj}، ولكنه في PHP يفاجئ لأن المصفوفات تنسخ بعمق تلقائياً، فيظن المطور أن الكائنات كذلك:
$cart = new ArrayObject(['items' => new ArrayObject(['قهوة'])]);
$copy = clone $cart;
$copy['items'][] = 'تمر';
echo 'WRONG original has ', count($cart['items']), " items\n";
final class Cart
{
public function __construct(public ArrayObject $items) {}
public function __clone()
{
$this->items = clone $this->items; // deep copy what the object owns
}
}WRONG original has 2 items
FIXED original has 1 item, copy has 2clone ينسخ الكائن نفسه، ولكن الخصائص التي هي كائنات تنسخ كمقابض، فالسلة الأصلية ونسختها تشتركان في نفس قائمة الأسطر، وإضافة التمر إلى النسخة أضافته إلى الأصل. والإصلاح الدالة السحرية __clone التي يستدعيها PHP على النسخة بعد نسخها، وفيها تنسخ كل كائن يملكه الكائن، كما في توثيق النسخ. والحل الأفضل في كثير من الحالات هو ما رأيناه في المقال الأول: كائنات readonly لا تتغير، والتعديل ينتج كائناً جديداً بـ clone مع التعديل، فلا يوجد ما يتغير من خلفك أصلاً.
أخطاء الأمان Security
الخطأ 12: بناء SQL بدمج النصوص
هذا الخطأ ليس خاصاً بـ PHP، ولكن PHP تحمله أكثر من غيرها لأن جزءاً كبيراً من الكود القديم على الإنترنت كتب بهذه الطريقة، وما زال يظهر في نتائج البحث وفي الإجابات المنسوخة. وفي التجربة قاعدة SQLite في الذاكرة فيها ثلاثة طلبات لثلاثة عملاء، والبحث بطلبات العميل «a»:
$input = "a' OR '1'='1";
$rows = $db->query("SELECT id, total FROM orders WHERE customer = '{$input}'")->fetchAll(PDO::FETCH_ASSOC);
echo 'WRONG rows returned: ', count($rows), "\n";
$stmt = $db->prepare('SELECT id, total FROM orders WHERE customer = ?');
$stmt->execute([$input]);
echo 'FIXED rows returned: ', count($stmt->fetchAll()), "\n";WRONG rows returned: 3
FIXED rows returned: 0
المدخل أغلق علامة التنصيص وأضاف شرطاً صحيحاً دائماً، فأعاد الاستعلام طلبات كل العملاء. أما الاستعلام المعد Prepared Statement فيرسل نص الاستعلام والقيم منفصلين إلى قاعدة البيانات، فتعامل القيمة كنص كامل يبحث عن عميل اسمه حرفياً a' OR '1'='1 ولا يجده. والتفاصيل في توثيق الاستعلامات المعدة في PDO، وفي Laravel تفعل Eloquent والـ Query Builder ذلك تلقائياً، إلا عندما تستخدم DB::raw أو whereRaw وتدمج فيها قيمة من المستخدم، وهذا هو المكان الذي يجب أن تبحث عنه في المراجعة.
الخطأ 13: md5 أو sha1 لتخزين الباسورد
والخطأ الأخير يأتي غالباً من كود قديم أو من دروس قديمة: تخزين md5($password) في جدول المستخدمين. وفي التجربة قسنا لماذا هذا خطير:
$a = md5('qahwa2026');
$b = md5('qahwa2026');
$t = hrtime(true);
for ($i = 0; $i < 100_000; $i++) {
md5("guess{$i}");
}
$h1 = password_hash('qahwa2026', PASSWORD_DEFAULT);
$h2 = password_hash('qahwa2026', PASSWORD_DEFAULT);WRONG md5 same hash twice: true, 100,000 guesses in 44 ms
FIXED password_hash: $2y$12$... different each time: true, verify: true, one hash 231 msفي المخرج أعلاه لاحظ التالي:
- md5 تعطي نفس الهاش لنفس الباسورد دائماً، فمن سرق الجدول يرى فوراً كل المستخدمين الذين يستخدمون نفس الباسورد، ويستطيع أن يستخدم جداول جاهزة لأشهر الباسوردات.
- مئة ألف محاولة أخذت 44 مللي ثانية في PHP على جهاز مشغول، أي أكثر من مليوني محاولة في الثانية، وأدوات الكسر المتخصصة على بطاقات الرسوميات أسرع بكثير.
- password_hash مع
PASSWORD_DEFAULTاستخدمت bcrypt بتكلفة 12، وهذا ما يعنيه$2y$12$في بداية النتيجة، وأضافت ملحاً Salt عشوائياً، فخرج هاش مختلف لنفس الباسورد في كل مرة، وأخذ الهاش الواحد 231 مللي ثانية، أي أن نفس المئة ألف محاولة تحتاج أكثر من ست ساعات. password_verifyتقرأ الخوارزمية والتكلفة والملح من النص نفسه، فلا تحتاج أن تحفظها في أعمدة منفصلة، وpassword_needs_rehashتخبرك متى ترفع التكلفة لمستخدم قديم عند دخوله التالي.
والقاعدة: لا تكتب أي كود لتخزين الباسورد بنفسك، واستخدم password_hash وpassword_verify فقط، أو ما يبنيه إطارك فوقهما، والأسئلة الشائعة في صفحة الباسوردات في دليل PHP تشرح لماذا لا تصلح دوال الهاش السريعة لهذا الغرض.
ماذا يحدث إذا رفعت تكلفة bcrypt؟ What If
رأيت أن الهاش الواحد بتكلفة 12 أخذ 231 مللي ثانية، والسؤال الطبيعي: ما الرقم الصحيح للتكلفة؟ وماذا يحدث للمستخدمين القدامى عندما ترفعها؟ كتبنا الملف whatif-cost.php الذي يحسب ثلاثة هاشات بكل تكلفة من 10 و12 و14، ثم يتحقق من باسورد قديم حفظ بتكلفة 10:
foreach ([10, 12, 14] as $cost) {
$times = [];
for ($i = 0; $i < 3; $i++) {
$t = hrtime(true);
$h = password_hash('qahwa2026', PASSWORD_BCRYPT, ['cost' => $cost]);
$times[] = (hrtime(true) - $t) / 1e6;
}
printf("cost %d: %s ms prefix %s\n", $cost, implode(' / ', array_map(fn ($x) => sprintf('%.0f', $x), $times)), substr($h, 0, 7));
}
$old = password_hash('qahwa2026', PASSWORD_BCRYPT, ['cost' => 10]);
var_dump(password_verify('qahwa2026', $old), password_needs_rehash($old, PASSWORD_DEFAULT));cost 10: 70 / 73 / 86 ms prefix $2y$10$
cost 12: 363 / 405 / 344 ms prefix $2y$12$
cost 14: 1036 / 1048 / 1244 ms prefix $2y$14$
bool(true)
bool(true)في المخرج أعلاه لاحظ التالي:
- التكلفة في bcrypt لوغاريتمية، فكل زيادة بواحد تضاعف عدد الدورات، ومن 10 إلى 12 يجب أن يتضاعف الزمن أربع مرات، وهذا قريب مما قسنا مع تذبذب الجهاز: من حوالي 70 إلى حوالي 370 مللي ثانية.
- زمن التكلفة 12 هنا بين 344 و405 مللي ثانية، بينما كان 231 في تشغيل الملف الرئيسي قبل دقائق، وهذا هو الجهاز المشغول نفسه الذي ذكرناه في المقالات السابقة، فلا تأخذ الرقم المطلق، وإنما النسبة بين التكاليف.
- الباسورد القديم بتكلفة 10 ما زال يتحقق بنجاح، لأن التكلفة محفوظة داخل النص
$2y$10$، وpassword_needs_rehashأعادتtrueلأن التكلفة الافتراضية الحالية أعلى. والنمط الصحيح أن تستدعيها بعد كل دخول ناجح، فإذا أعادتtrueتحسب هاشاً جديداً بالباسورد الذي أدخله المستخدم للتو وتحفظه، فيترقى كل المستخدمين تدريجياً دون أن يشعروا. - تكلفة 14 تعني أكثر من ثانية لكل دخول على هذا الجهاز، وهذا يجعل صفحة الدخول نفسها هدفاً سهلاً لإغراق السيرفر بطلبات دخول خاطئة. والقاعدة العملية أن تختار أعلى تكلفة تبقي الهاش الواحد في حدود بضع مئات من المللي ثواني على سيرفرك الحقيقي، وأن تضع حداً لعدد محاولات الدخول.
أين تظهر هذه الأخطاء في مشروع حقيقي؟
الأمثلة في التجربة صغيرة حتى ترى السبب بوضوح، ولكنها في المشاريع الحقيقية تختبئ في أماكن محددة، ومعرفة هذه الأماكن تجعل المراجعة أسرع. فالمرجع بعد foreach يظهر عادة في دوال طويلة تعالج سلة أو تقريراً، حيث تكون الحلقتان متباعدتين بعشرات الأسطر، فلا يرى من يضيف الحلقة الثانية أن الأولى استخدمت &. وarray_merge مع المفاتيح الرقمية يظهر عندما تجمع نتائج استعلامين مفهرسين بالمعرف، مثل منتجات من مستودعين أو صلاحيات من دورين، والنتيجة لا تفشل وإنما تفقد ارتباط القيمة بصاحبها. وarray_filter قبل json_encode يظهر في كل API تحذف عناصر غير مسموحة من قائمة قبل إرسالها، فتعمل الشاشة في المتصفح لأن JavaScript يقبل الكائن في بعض الأماكن، ثم يتعطل تطبيق الجوال الذي يتوقع قائمة.
وأخطاء النصوص تظهر في كل مكان يقص النص أو يقيس طوله: ملخص المنتج في صفحة القائمة، ورسالة SMS بحد أقصى للحروف، وحقل في قاعدة البيانات بطول محدد، والتحقق من طول الاسم في النموذج. وفي نظام عربي كل هذه الأماكن تخطئ مع strlen وsubstr، وأحياناً تكتب حرفاً مكسوراً في قاعدة البيانات يظهر لاحقاً كعلامة استفهام. وأخطاء المقارنة تظهر في قوائم السماح Allow Lists، مثل أكواد الخصم والدول المسموحة وأدوار المستخدمين، وفي التحقق من التوكن، وهذه بالضبط الأماكن التي يبحث فيها المخترق عن مقارنة متساهلة. وأخطاء التواريخ تظهر في التقارير اليومية، والمهام المجدولة التي تعمل منتصف الليل، ومواعيد انتهاء العروض، حيث يغلق العرض قبل وقته بثلاث ساعات لأن السيرفر بتوقيت UTC. وأخطاء الأمان تظهر في الكود الذي كتب بسرعة خارج الإطار، مثل سكربت تصدير أو لوحة إدارة داخلية، لأنه المكان الذي لا يمر عليه Eloquent ولا المراجعة المعتادة.
ما الذي كشفه PHPStan؟ وما الذي فاته؟
بعد أن جمعنا الأخطاء شغلنا PHPStan بالمستوى max على نفس الملف، لنعرف أيها كان سيكشفه لنا قبل التشغيل:
/app/mistakes.php:52:Ternary operator condition is always false.
/app/mistakes.php:54:Call to function in_array() with arguments '1e1', array{'10', '20', '30'} and true will always evaluate to false.
/app/mistakes.php:73:Variable $discountCode in empty() always exists and is always falsy.
/app/mistakes.php:101:Parameter #1 $value of function count expects array|Countable, ArrayObject<int, string>|null given.
/app/mistakes.php:128:Match arm comparison between '1e1' and '10' is always false.
/app/mistakes.php:136:Cannot call method fetchAll() on PDOStatement|false.
/app/mistakes.php:148:Call to function md5() on a separate line has no effect.وهذا مختصر من 15 رسالة في الملف phpstan-output.txt. والسطر 52 هو شرط الخطأ 4 الذي استخدم نتيجة array_search مباشرة، والسطر 73 هو empty في الخطأ 7، وفي الحالتين عرف PHPStan المشكلة لأن القيم في التجربة ثابتة، فحسب أن الشرط دائماً خاطئ. أما الرسائل الأخرى فهي عن النسخ الصحيحة أو عن تفاصيل في كود التجربة نفسه، مثل أن query قد تعيد false. والمهم ما لم يظهر: لا شيء عن المرجع بعد foreach، ولا عن strlen مع العربية، ولا عن array_merge والمفاتيح، ولا عن sort التي أسندنا نتيجتها، ولا عن switch، ولا عن DateTime، ولا عن حقن SQL، ولا عن md5. والسبب أن هذا الكود صحيح من ناحية الأنواع، وكل دالة استدعيت بما تقبله، والخطأ في المعنى. وهناك حزم إضافية تغطي جزءاً من ذلك، مثل phpstan-strict-rules التي تمنع المقارنة المتساهلة واستخدام empty، ولكن أخطاء مثل array_merge والتواريخ والأمان لا يكشفها إلا اختبار يفحص النتيجة، ومراجع يعرف PHP.
أخطاء لم تعد أخطاء: ما الذي أصلحته PHP 8 بنفسها؟
من المفيد أن تعرف أيضاً الأخطاء الشهيرة التي لم نضعها في القائمة، لأن PHP الحديثة صارت ترفضها بنفسها، فإذا قرأت عنها في مقال قديم فلا تقلق منها في PHP 8.5، ولكن احذر منها إذا كان مشروعك على إصدار أقدم. أشهرها المقارنة 0 == 'a' التي كانت تعطي true قبل PHP 8.0 وسببت ثغرات في مقارنة التوكن، وصارت false كما رأينا في المقال الأول. ومنها النص الذي يبدأ برقم ثم فيه كلام، مثل «5 ريال»، والذي كان يقبل كرقم مع تنبيه في PHP 7، وصار يرمي TypeError عند تمريره لمعامل int حتى بدون الوضع الصارم، كما رأينا في تجربة «ماذا يحدث إذا» في المقال الأول. ومنها استدعاء دالة على null، والذي كان خطأ قاتلاً لا يلتقط، وصار Error يمكن التقاطه وتسجيله منذ PHP 7، وقراءة متغير غير معرف التي ارتفعت من تنبيه Notice إلى تحذير Warning في PHP 8.0، والقسمة على صفر التي صارت ترمي DivisionByZeroError كما رأينا في المقال الثاني. وكل هذه التغييرات موثقة في دليل الترقية إلى 8.0، وهو أهم دليل ترقية في تاريخ اللغة الحديث، لأنه الإصدار الذي حول كثيراً من السلوكيات الصامتة إلى أخطاء واضحة. والنتيجة أن قائمة الأخطاء التي تحتاج أن تحفظها في PHP 8.5 أقصر بكثير مما كانت في PHP 5، والأخطاء الثلاثة عشر في هذا المقال هي تقريباً ما بقي منها.
لماذا بقيت هذه السلوكيات؟ التوافق مع الكود القديم Backward Compatibility
قد يتساءل البعض بعد هذه القائمة: إذا كان فريق PHP يعرف أن in_array بدون strict خطر، وأن switch يقارن بتساهل، فلماذا لا يغيرونها؟ والإجابة في الرقم الذي رأيناه في المقال الأول: PHP تعمل خلف حوالي سبعين بالمئة من المواقع التي تعرف لغتها، وأغلب هذه المواقع لا يحدث كودها أحد، وإنما يحدث فقط إصدار PHP الذي تعمل عليه من مزود الاستضافة. فلو تغير معنى in_array في إصدار جديد، لتغير سلوك ملايين المواقع بصمت في يوم الترقية، دون أي رسالة خطأ، وهذا أسوأ من الخطأ نفسه. لذلك تتبع PHP طريقاً آخر في تطويرها: تضيف البديل الصحيح بجانب القديم، مثل match بجانب switch، وstr_contains بجانب strpos، وDateTimeImmutable بجانب DateTime، ثم تطبع تحذير E_DEPRECATED للسلوك القديم لعدة إصدارات، ثم تزيله في إصدار رئيسي مع توثيقه في دليل الترقية.
وهذا الطريق نجح في حالات كثيرة رأيناها في هذه المرحلة: مقارنة الرقم بالنص تغيرت في PHP 8.0 بعد نقاش طويل، والتحويل الضمني من float إلى int مع فقدان الكسر صار يعطي Deprecated، و$http_response_header صار مهملاً في 8.5. ولكنه يعني أن عبء الاختيار عليك: اللغة تعطيك الأداتين، القديمة والحديثة، وأنت الذي تختار. ولهذا يقال إن PHP الحديثة لغة جيدة مختبئة داخل لغة قديمة، والفرق بينهما ليس في الإصدار الذي تثبته، وإنما في الدوال والإعدادات التي تختارها، وهذا بالضبط ما يحدده أسلوب PHP الحديث.
ما هو أسلوب PHP الحديث Idiomatic PHP؟
إذا جمعت ما رأيته في المقالات الخمسة، فأسلوب PHP الحديث له صفات واضحة تستطيع أن تراها في أي ملف خلال ثوان، وهي ما يبحث عنه المراجع الخبير عندما يقرأ كودك:
- الوضع الصارم Strict Types: كل ملف يبدأ بـ
declare(strict_types=1)، كما شرحنا في المقال الأول، فلا يتحول النص إلى رقم بصمت عند استدعاء دالة. - الأنواع في كل مكان Types: على كل معامل وكل قيمة إرجاع وكل خاصية، و
mixedفقط عندما تقصدها، وأشكال المصفوفات في PHPDoc عندما لا تستطيع استبدالها بكلاس. - كائنات القيمة Value Objects: كلاسات
final readonlyصغيرة بخصائص في الـ Constructor، مثلProductفي المقال الأول، بدل مصفوفات تمر بين الدوال بمفاتيح نصية لا يفحصها أحد. - التعداد Enum بدل الثوابت النصية، و
matchبدلswitch، و?->و??بدل سلاسلissetالمتداخلة. - المعاملات المسماة Named Arguments عندما تكون القيم من نفس النوع، مثل
new Product(name: 'تمر', priceHalalas: 4500)، حتى لا تمرر رقمين بترتيب معكوس، كما في توثيق المعاملات. - المقارنة الصارمة في كل مكان:
===و!==، وtrueفيin_arrayوarray_search، وبلاemptyمع المدخلات. - الاستثناءات بدل قيم الفشل:
JSON_THROW_ON_ERROR، ومعالج يحول التحذيرات إلىErrorException، واستثناءات خاصة بالمنطق تحمل بياناتها، كما في المقال الثاني. - Composer للتحميل التلقائي PSR-4، وPint أو PHP-CS-Fixer للتنسيق، وPHPStan بمستوى عال، وPHPUnit، وكلها في CI.
ولاحظ أن كل نقطة في هذه القائمة تمنع واحداً أو أكثر من الأخطاء الثلاثة عشر: الأنواع والكائنات تمنع خلط المفاتيح، وmatch يمنع الخطأ 11، والمقارنة الصارمة تمنع الأخطاء 4 و7، والكائنات الثابتة تمنع الخطأين 9 و10. أي أن الأسلوب الحديث ليس ذوقاً في الكتابة، وإنما طريقة لتجنب سلوكيات اللغة القديمة دون أن تحفظها كلها.
كيف تمنع هذه الأخطاء في فريق؟ الأدوات Tooling والمراجعة
رأينا أن PHPStan بإعداده الافتراضي أشار إلى خطأين فقط، والسؤال العملي: كيف تمنع البقية في فريق فيه مطورون قادمون من لغات مختلفة؟ والإجابة طبقات، وكل طبقة تمسك جزءاً:
- قواعد التنسيق: PHP-CS-Fixer فيه قاعدتان من نوع المخاطرة Risky تحولان الكود تلقائياً، الأولى strict_comparison التي تحول
==إلى===، والثانية strict_param التي تضيفtrueإلىin_arrayوأخواتها. وسميت Risky لأنها قد تغير سلوك كود يعتمد على المقارنة المتساهلة، فشغلها مرة واحدة على المشروع مع الاختبارات، ثم اجعلها جزءاً من CI. - قواعد التحليل:
phpstan-strict-rulesتمنعemptyوالمقارنة المتساهلة والشروط على غيرbool، أي أنها كانت ستشير إلى الأخطاء 3 و4 و7 حتى مع قيم غير ثابتة. - إعادة الكتابة الآلية: Rector يحول الصيغ القديمة إلى الحديثة على مستوى المشروع كله، مثل
strpos(...) !== falseإلىstr_contains، وswitchالبسيط إلىmatch. - الاختبارات: الأخطاء التي في المعنى، مثل
array_mergeوالمراجع والتواريخ والمناطق الزمنية، لا يمسكها إلا اختبار يتحقق من النتيجة، مثل اختبارات المقال الرابع التي تتحقق من المخزون بعد كل طلب. - المراجعة: قائمة التحقق في هذا المقال مصممة لتكون قائمة مراجعة، ومع الوقت تتحول إلى عادة لا تحتاج قائمة.
قد يتساءل البعض: هل PHP لغة سيئة إذن؟
بعد ثلاثة عشر خطأ في مقال واحد قد تخرج بانطباع أن PHP لغة مليئة بالفخاخ، وهذا الانطباع منتشر بين المطورين لأسباب تاريخية. ولكن انظر إلى ما رأيته في المرحلة كلها: لغة بأنواع تفحص وقت التشغيل، وتعداد وكائنات ثابتة وخطافات خصائص، ومحلل ثابت يفهم الأنواع العامة، ومدير حزم من أفضل مديري الحزم في كل اللغات، ونموذج تشغيل يجعل تسرب الذاكرة وحالات السباق داخل الكود نادرة في تطبيقات الويب، وإطار مثل Laravel بنى فيه خدمة كاملة مع معاملة وأقفال واختبارات بخمسة ملفات. ولكل لغة فخاخها التي يحفظها أهلها: في JavaScript المقارنة == وthis، وفي Python المعاملات الافتراضية القابلة للتعديل والنسخ السطحي، وفي Java المقارنة == بين النصوص، وفي Go الحلقة التي تلتقط متغيراً مشتركاً في الإصدارات القديمة. والفرق في PHP أن فخاخها قديمة ومشهورة ومكتوبة في الدليل، وأن اللغة أضافت لكل واحد منها بديلاً صحيحاً.
والسؤال الأهم ليس «هل PHP جيدة؟»، وإنما «هل تكتب PHP الحديثة؟». فالمطور الذي يكتب بالوضع الصارم وبالأنواع وبالمقارنة الصارمة وبالأدوات في CI لن يقابل أغلب هذه الأخطاء، والمطور الذي ينسخ كوداً من إجابة كتبت قبل عشر سنوات سوف يقابلها كلها. ولهذا كانت خطة القراءة في هذا المقال من المصادر الرسمية فقط: لأن الدليل الرسمي وأدلة الترقية تعكس اللغة كما هي اليوم، بينما أغلب ما تجده في البحث يعكسها كما كانت.
قائمة تحقق Checklist لمراجعة كود PHP
- هل بعد كل
foreach (... as &$x)سطرunset($x)، أو هل يمكن استبدالها بـarray_map؟ - هل كل عملية على طول نص عربي أو جزء منه بدوال
mb_؟ - هل تستخدم
str_containsوstr_starts_withبدلstrposفي الشروط؟ - هل في كل
in_arrayوarray_searchالمعامل الثالثtrue، وهل نتيجةarray_searchتقارن بـ!== false؟ - هل المصفوفات التي مفاتيحها معرفات تدمج بـ
+أوarray_replaceوليسarray_merge؟ - هل تمر كل قائمة على
array_valuesقبلjson_encodeإذا مرت علىunsetأوarray_filter؟ - هل اختفت
emptyمن التعامل مع المدخلات؟ - هل التواريخ
DateTimeImmutableبمنطقة زمنية صريحة؟ - هل الكائنات التي تملك كائنات أخرى فيها
__clone، أو هل هيreadonly؟ - هل
matchبدلswitchفي الكود الجديد؟ - هل كل استعلام SQL معد، وهل كل
whereRawوDB::rawبلا قيم من المستخدم؟ - هل الباسوردات بـ
password_hashفقط؟
خطة القراءة Reading Plan من المصادر الرسمية
هذه المرحلة أعطتك ما تحتاجه لتبدأ، والباقي في المصادر الرسمية، وهي في PHP غنية جداً ومحدثة مع كل إصدار. والترتيب التالي على أربعة أسابيع، ساعة في اليوم تقريباً:

- الأسبوع الأول، اللغة: ابدأ بـ البداية في دليل PHP، ثم اقرأ المرجع اللغوي Language Reference كاملاً، وركز على الأنواع والمقارنة وتحويل الأنواع والمصفوفات والمراجع، فهي مصدر أغلب أخطاء هذا المقال. والتعليقات أسفل صفحات الدليل قديمة أحياناً، فاعتمد على نص الصفحة نفسه.
- الأسبوع الثاني، ما تغير: اقرأ أدلة الترقية من الترقية إلى 8.0 حتى الترقية إلى 8.5، فهي أقصر طريق لتعرف الفرق بين PHP التي في الدروس القديمة وPHP الحالية. وعندما تستخدم ميزة جديدة اقرأ مقترحها في قائمة RFC، لأن المقترح يشرح لماذا صممت الميزة بهذا الشكل وما الذي رفض. وراجع جدول الإصدارات المدعومة كل سنة.
- الأسبوع الثالث، الأدوات: توثيق Composer، ثم معايير PHP-FIG وأهمها PSR-4 للتحميل التلقائي وPER لأسلوب الكود وPSR-7 وPSR-15 لطلبات HTTP، ثم دليل PHPStan وتوثيق PHPUnit.
- الأسبوع الرابع، الإطار والأمان: اقرأ توثيق Laravel أو توثيق Symfony مرة كاملة من أوله إلى آخره، وليس فقط الصفحة التي تحتاجها اليوم، لأن الإطار يحل كثيراً من مشاكلك بطرق لن تعرفها إلا إذا قرأت عنها. ثم اقرأ فصل الأمان في دليل PHP، ومعه صفحة OWASP عن حقن SQL.
وإذا أردت أن تتابع اللغة نفسها، فـ مؤسسة PHP تمول جزءاً كبيراً من تطوير PHP اليوم وتنشر تقارير عنه، والقرارات نفسها تتخذ بالتصويت على مقترحات RFC التي ذكرناها.
كيف تقرأ صفحة دالة في دليل PHP؟
أغلب الأخطاء الثلاثة عشر مكتوبة في صفحة الدالة نفسها في الدليل، ولكن في قسم لا يقرؤه من يبحث عن مثال سريع. فكل صفحة دالة في دليل PHP لها نفس الأقسام، وكل قسم يجيب على سؤال محدد. القسم الأول هو التوقيع Description، مثل sort(array &$array, int $flags = SORT_REGULAR): true، وفيه وحده ثلاث معلومات من الخطأ 8: العلامة & تعني أن الدالة تعدل المصفوفة نفسها، ونوع الإرجاع true يعني أنها لا تعيد شيئاً مفيداً، والمعامل الثاني يحدد طريقة المقارنة. ثم قسم القيم المعادة Return Values، وهو الذي يقول إن strpos قد تعيد 0 أو false، ومعه غالباً صندوق تحذير Warning يقول صراحة استخدم ===، وهذا الصندوق هو الخطأ 3 مكتوباً بلغة الدليل. ثم قسم سجل التغييرات Changelog، وفيه في أي إصدار تغير سلوك الدالة، مثل تغير نوع الإرجاع أو رمي ValueError بدل التحذير، وهذا القسم هو ما تحتاجه عندما يعمل كود على سيرفر ويختلف على آخر. ثم الأمثلة Examples والملاحظات Notes. والعادة التي تستحق أن تبنيها: قبل أن تستخدم أي دالة لأول مرة اقرأ التوقيع وقسم القيم المعادة وصناديق التحذير، فهي لا تأخذ أكثر من دقيقة، وتوفر عليك أغلب ما في هذا المقال.
الخلاصة
وصلنا لنهاية الموضوع ولنهاية المرحلة، وأهم ما في هذا المقال:
- ثلاثة عشر خطأ تعمل كلها في PHP 8.5 دون أي رسالة، وتعطي نتيجة غير التي يتوقعها القادم من لغة أخرى.
- المصفوفات: المرجع يبقى بعد
foreach، وarray_mergeتعيد ترقيم المفاتيح، وunsetيحول JSON إلى كائن، وsortتعيدtrue. - النصوص:
strlenأعطت 23 لاسم من 12 حرفاً، وstrposأعطت صفراً جعل المنتج السعودي غير سعودي. - المقارنة:
in_arrayوجدت «1e1» في قائمة ليس فيها، وemptyتجاهلت الكود «0»، وswitchطابق «1e1» مع «10». - الكائنات:
DateTimeعدل تاريخ الإنشاء، والمنطقة الافتراضية UTC، وcloneشارك قائمة الأسطر. - الأمان: الدمج أعاد طلبات كل العملاء، وmd5 جربت مئة ألف باسورد في 44 مللي ثانية مقابل 231 مللي ثانية لهاش واحد بـ bcrypt.
- PHPStan أشار إلى خطأين فقط من الثلاثة عشر، والباقي تكشفه الاختبارات والمراجعة.
وبهذا نكون قد أكملنا مرحلة PHP: الأدوات والصيغة، والأنواع والذاكرة والأخطاء، والتزامن، وخدمة حقيقية، والأخطاء الشائعة. والمرحلة نفسها مكتوبة بنفس الشكل لكل لغات المسار، فإذا أردت أن تقارن PHP بلغة أخرى فاقرأ نفس المقال في مرحلتها.
دليلPHP لمن يعرف البرمجة: الصيغة والأدوات Toolchain في يومدليل عملي للمطور القادم من Python أو JavaScript يثبت PHP 8.5 ويقارن صيغتها بما يعرفه ويشغل Composer وPint وPHPStan وPHPUnit على مشروع صغير بمخرجات حقيقية.
دليلالأنواع Types والذاكرة والأخطاء في PHP 8 بتجارب مقيسةنحسب ضريبة مئة ألف سعر بطريقتين ونقيس جامع الدورات وذاكرة Worker طويل ونشغل كل نوع من أنواع الفشل في PHP 8.5، لترى أين يخطئ من يكتب PHP بعقل لغة أخرى.
دليلالتزامن Concurrency في PHP: عمليات FPM والألياف Fibersقسنا مئة طلب HTTP وأربعة ملايين عملية SHA-256 بعمليات PHP-FPM وFibers وAMPHP والتفرع والخيوط، ثم صنعنا حالة سباق على المخزون بين عمليات FPM وأصلحناها بالقفل.
دليلخدمة حقيقية بـ PHP: Laravel مع PostgreSQL والاختباراتبنينا خدمة طلبات لمتجر صغير بـ Laravel 13 وPostgreSQL 17 بمعاملة تقفل المخزون، مع اختبار يرسل 20 طلباً متزامناً على 10 علب، ثم Docker Image بمرحلتين وقياس للأداء كشف ثمن الاتصال بقاعدة البيانات.
دليلأخطاء شائعة وأسلوب Python الصحيح Pythonicخمسة عشر خطأً يقع فيها من يأتي إلى Python من Java وJavaScript وC#، كل واحد بتشغيل خاطئ حقيقي وإصلاحه وما يمسكه ruff منه، ثم خطة قراءة لأربعة أسابيع من التوثيق الرسمي.
دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتشغيل الأخطاء الثلاثة عشر بـ PHP 8.5.12 وتحليلها بـ PHPStan 2.3.4 داخل Docker.