حالة التطبيق: ما يبقى وما يزول
الخادم ينسى كل شيء بعد كل رد؛ فأين نحفظ ما يجب أن يبقى؟ ⏱ نحو 30 دقيقة.
- تشرح أن
PHPيبدأ كل طلب من الصفر وتمحى متغيراته بعده. - تختار مكان حفظ الحالة حسب صاحبها ومدة بقائها.
- تحفظ حالة مشتركة في ملف مع قفل، وحالة خاصة في الجلسة.
- تشرح حالة السباق وكيف يمنعها القفل والمعاملات.
١كل طلب يبدأ من الصفر
في برنامج بايثون الذي كتبته في 111 كان المتغير يبقى في الذاكرة ما دام البرنامج يعمل. أما PHP فيعمل بطريقة مختلفة جذريًّا: كل طلب يشغّل الملف من أوله، ثم تُمحى كل متغيراته عند انتهاء الرد. لاحظ هذا العدّاد الذي لا يعدّ:
<?php$count = 0; // يبدأ من الصفر مع كل طلب!$count++;echo "العدد: $count";
ثلاثة طلبات، والجواب 1 كل مرة. المتغير $count عاش أجزاء من الثانية فقط.
فأين نحفظ ما يجب أن يبقى؟ هذا سؤال حالة التطبيق State: البيانات التي تتغير مع الاستخدام وتلزم بعد الطلب الحالي.
٢خريطة أماكن الحالة
| المكان | من يراه؟ | كم يبقى؟ | مثال |
|---|---|---|---|
| متغيرات PHP | هذا الطلب فقط | حتى نهاية الرد | نتيجة حساب مؤقتة |
الطلب نفسه (GET / POST) | هذا الطلب | حتى نهاية الرد | كلمة البحث، رقم الصفحة |
| الكوكي | مستخدم واحد (في متصفحه) | حتى تنتهي | اللغة المفضلة |
| الجلسة | مستخدم واحد (على الخادم) | حتى يغلق المتصفح أو تنتهي مدتها | السلة، تسجيل الدخول |
| ملف على الخادم | كل المستخدمين | دائم | عدّاد الزيارات العام، إعدادات |
| قاعدة البيانات | كل المستخدمين | دائم | الحسابات، الطلبات، المنتجات |
السؤالان اللذان يحددان المكان: لمن هذه البيانات (مستخدم واحد أم الجميع)؟ وكم يجب أن تبقى؟
٣العدّاد ثلاث مرات
المثال نفسه بثلاثة أماكن مختلفة للحالة. الطلبات الثلاثة الأولى من «مستخدم أ»، ثم يطلب «مستخدم ب» من متصفح آخر:
<?phpsession_start(); // 1) لكل مستخدم: الجلسة$_SESSION['mine'] = ($_SESSION['mine'] ?? 0) + 1; // 2) للجميع: ملف على الخادم$file = __DIR__ . '/total.txt';$total = (int) @file_get_contents($file) + 1;file_put_contents($file, $total, LOCK_EX); echo "زياراتك أنت: {$_SESSION['mine']}\n";echo "كل الزيارات: $total";
الطلب الرابع من متصفح آخر بلا كوكي جلسة، فهو مستخدم جديد: عدّاده الخاص بدأ من 1، أما العدّاد العام في الملف فواصل: 4.
والخيار LOCK_EX يقفل الملف أثناء الكتابة، حتى لا يكتب طلبان في اللحظة نفسها فيضيع أحدهما. فالخادم الحقيقي يخدم مستخدمين كثيرين في وقت واحد.
٤تزامن الطلبات: مشكلة الحالة المشتركة
حين يتشارك الجميع حالة واحدة (ملف أو صف في قاعدة البيانات)، قد يقرأ طلبان القيمة نفسها قبل أن يكتب أحدهما:
| الزمن | الطلب أ | الطلب ب | المقاعد في الملف |
|---|---|---|---|
| 1 | يقرأ: 1 مقعد متبقٍ | 1 | |
| 2 | يقرأ: 1 مقعد متبقٍ | 1 | |
| 3 | يحجز ويكتب 0 | 0 | |
| 4 | يحجز ويكتب 0 | 0 ← حُجز مقعد واحد مرتين! |
هذه مشكلة حالة السباق Race condition. وحلولها: قفل الملف طوال «اقرأ ثم عدّل ثم اكتب» بـ flock، أو (الأفضل) قاعدة البيانات بمعاملاتها Transactions التي عرفتها في 121، وهو ما سنفعله في الوحدة 9.
<?php$fp = fopen(__DIR__ . '/seats.txt', 'c+');flock($fp, LOCK_EX); // انتظر حتى تحصل على القفل وحدك$seats = (int) stream_get_contents($fp);if ($seats > 0) { $seats--; ftruncate($fp, 0); rewind($fp); fwrite($fp, (string) $seats);}flock($fp, LOCK_UN); // حرّر القفلfclose($fp);
٥حالة التطبيق في الواجهات الحديثة
في تطبيقات React (مقرر 242) والجوال (251) تعيش «حالة الواجهة» في المتصفح أو الجهاز: أي تبويب مفتوح، وما كُتب في الحقل. أما حالة البيانات الحقيقية (الطلبات والحسابات) فمرجعها الوحيد الخادم وقاعدة بياناته، والواجهة تطلبها عبر API. وقاعدة ذهبية: الخادم هو مصدر الحقيقة؛ ما تقوله الواجهة عن الرصيد أو السعر للعرض فقط، والخادم يحسبه من جديد.
توقّع وتحقّق
متغير عادي في PHP يبقى:
عدد زيارات الموقع كله يُحفظ في:
السلة الخاصة بكل مستخدم أنسب مكان لها:
حجز المقعد نفسه مرتين لطلبين متزامنين يسمى:
Race condition.الخلاصة
- متغيرات
PHPتعيش طلبًا واحدًا فقط. - لمستخدم واحد مؤقتًا ← الجلسة (أو الكوكي لغير الحساس). للجميع ودائمًا ← ملف أو قاعدة بيانات.
LOCK_EXوflockيمنعان تداخل الكتابة من طلبات متزامنة.- حالة السباق: طلبان يقرآن القيمة نفسها قبل أن يكتب أحدهما.
- الخادم مصدر الحقيقة؛ الواجهة تعرض فقط.
تحدٍّ برمجي: لوحة الإعلانات
متوسطاكتب board.php: لوحة إعلانات يراها كل المستخدمين، مع عدّاد خاص لكل مستخدم بعدد ما نشره هو.
- POST فيه
msg: يُضاف الإعلان إلى ملف مشتركboard.txt(سطر لكل إعلان، بـ FILE_APPEND و LOCK_EX)، ويزيد عدّاد المستخدم في الجلسة. - كل طلب (GET أو POST) يعرض:
<p>نشرتَ: 2</p>ثم<ul>بكل الإعلانات من الملف بترتيب نشرها، مع htmlspecialchars. - إن لم يوجد الملف بعد فالقائمة فارغة ولا تحذيرات.
👀 اعرض الاستجابة المطلوبة
GET /board.phpPOST /board.php(msg=اختبار منتصف الفصل الأحد)POST /board.php(msg=<b>المعمل مغلق</b>)- 👤 ب
GET /board.php - 👤 ب
POST /board.php(msg=شكرًا)
- الكود يعمل بلا أخطاء أو تحذيرات
- أول زيارة: نشرتَ: 0 بلا تحذيرات
- بعد إعلانين: نشرتَ: 2
- الإعلان الثاني يُعرض نصًّا لا وسمًا
- مستخدم آخر (بلا كوكي): نشرتَ: 0 لكنه يرى الإعلانين
- المستخدم الآخر ينشر: يرى الإعلانات الثلاثة بالترتيب
- تكتب في الملف مع FILE_APPEND
💡 تلميح 1
💡 تلميح 2
file($file, FILE_IGNORE_NEW_LINES) تعيد مصفوفة بأسطر الملف.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
<?phpsession_start();$file = __DIR__ . '/board.txt'; if ($_SERVER['REQUEST_METHOD'] === 'POST' && trim($_POST['msg'] ?? '') !== '') { file_put_contents($file, trim($_POST['msg']) . "\n", FILE_APPEND | LOCK_EX); $_SESSION['posted'] = ($_SESSION['posted'] ?? 0) + 1;} $lines = file_exists($file) ? file($file, FILE_IGNORE_NEW_LINES) : [];echo "<p>نشرتَ: " . ($_SESSION['posted'] ?? 0) . "</p>";echo "<ul>";foreach ($lines as $l) { echo "<li>" . htmlspecialchars($l) . "</li>";}echo "</ul>";