طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 132 · تقنيات تطوير الواجهات الخلفية
الدرس 15 · الوحدة 7: التعامل مع حالة التطبيق والبيانات

حالة التطبيق: ما يبقى وما يزول

الخادم ينسى كل شيء بعد كل رد؛ فأين نحفظ ما يجب أن يبقى؟ ⏱ نحو 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يحجز ويكتب 00
4يحجز ويكتب 00 ← حُجز مقعد واحد مرتين!

هذه مشكلة حالة السباق 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.

الخلاصة

تحدٍّ برمجي: لوحة الإعلانات

متوسط

اكتب board.php: لوحة إعلانات يراها كل المستخدمين، مع عدّاد خاص لكل مستخدم بعدد ما نشره هو.

المطلوب:
  • POST فيه msg: يُضاف الإعلان إلى ملف مشترك board.txt (سطر لكل إعلان، بـ FILE_APPEND و LOCK_EX)، ويزيد عدّاد المستخدم في الجلسة.
  • كل طلب (GET أو POST) يعرض: <p>نشرتَ: 2</p> ثم <ul> بكل الإعلانات من الملف بترتيب نشرها، مع htmlspecialchars.
  • إن لم يوجد الملف بعد فالقائمة فارغة ولا تحذيرات.
👀 اعرض الاستجابة المطلوبة
✓ التحقق الآلي: يرسل المختبر هذه الطلبات إلى خادمك بالترتيب ثم يفحص الاستجابات
  1. GET /board.php
  2. POST /board.php (msg=اختبار منتصف الفصل الأحد)
  3. POST /board.php (msg=<b>المعمل مغلق</b>)
  4. 👤 ب GET /board.php
  5. 👤 ب 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>";
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس