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

حفظ البيانات في الملفات

أبسط مخزن دائم، وما يكفيه وما لا يكفيه. ⏱ نحو 35 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تقرأ الملفات وتكتبها وتضيف إليها بدوال PHP.
  • تقرأ الملفات الكبيرة سطرًا سطرًا بـ fopen و fgets.
  • تبني وحدة تخزين في ملف JSON وتستخدمها في تطبيق.
  • تحدد متى يكفي الملف ومتى تلزم قاعدة البيانات، وتطبّق أمان الملفات.

١الملف أبسط مخزن دائم

عرفت في الدرس 15 أن البيانات المشتركة الدائمة تُحفظ خارج PHP. قبل قاعدة البيانات، أبسط مكان: ملف على الخادم. مناسب للبيانات القليلة: إعدادات، وسجلات Logs، وتخزين مؤقت، ونماذج أولية. ولكل ملف عمليات أساسية:

<?php$f = __DIR__ . '/notes.txt'; file_put_contents($f, "السطر الأول\n");                 // اكتب (يمسح القديم)file_put_contents($f, "السطر الثاني\n", FILE_APPEND);   // أضف في النهايةfile_put_contents($f, "السطر الثالث\n", FILE_APPEND); echo file_get_contents($f);                 // اقرأ الملف كله نصًّاecho "---\n";$lines = file($f, FILE_IGNORE_NEW_LINES);    // اقرأه مصفوفة أسطرecho count($lines) . " أسطر، آخرها: " . end($lines) . "\n";echo filesize($f) . " بايت\n";var_dump(file_exists(__DIR__ . '/nothing.txt'));

٢القراءة سطرًا سطرًا: fopen

file_get_contents تقرأ الملف كله في الذاكرة دفعة واحدة. لملف سجلات حجمه مئات الميغابايت هذا خطر؛ فنفتحه ونقرؤه سطرًا سطرًا:

<?phpfile_put_contents(__DIR__ . '/access.log', "GET /index.php 200\nGET /x.php 404\nPOST /login.php 401\nGET /about.php 404\n"); $counts = [];$fp = fopen(__DIR__ . '/access.log', 'r');     // r للقراءةwhile (($line = fgets($fp)) !== false) {    $status = trim(explode(' ', $line)[2]);    $counts[$status] = ($counts[$status] ?? 0) + 1;}fclose($fp);                                   // أغلق ما فتحت ksort($counts);foreach ($counts as $s => $n) echo "$s: $n\n";
الوضعمعناه
'r'قراءة فقط.
'w'كتابة: ينشئ الملف أو يمسح محتواه.
'a'إضافة في النهاية.
'c+'قراءة وكتابة دون مسح (مع flock كما في الدرس 15).

٣ملف JSON: قاعدة بيانات مصغّرة

بدمج ما تعلمته في درسي JSON والملفات نحصل على مخزن عملي: نحمّل المصفوفة كلها من ملف JSON، نعدّلها، ثم نحفظها. نضع ذلك في دالتين داخل وحدة store.php، فلا يعرف بقية التطبيق كيف تُخزَّن البيانات. وهذا تطبيق مهام كامل (جرّب إضافة مهمة وتأشيرها):

<?phprequire __DIR__ . '/store.php';$tasks = load_tasks(); if ($_SERVER['REQUEST_METHOD'] === 'POST') {    $action = $_POST['action'] ?? '';    if ($action === 'add' && trim($_POST['title'] ?? '') !== '') {        $tasks[] = ['id' => count($tasks) ? max(array_column($tasks, 'id')) + 1 : 1,                    'title' => trim($_POST['title']), 'done' => false];    } elseif ($action === 'toggle') {        foreach ($tasks as &$t) {            if ($t['id'] === (int) $_POST['id']) $t['done'] = !$t['done'];        }        unset($t);    }    save_tasks($tasks);    header('Location: tasks.php', true, 303);    exit;}?><h3>مهامي (<?= count($tasks) ?>)</h3><ul><?php foreach ($tasks as $t): ?>  <li>    <form method="post" style="display:inline">      <input type="hidden" name="action" value="toggle">      <input type="hidden" name="id" value="<?= $t['id'] ?>">      <button><?= $t['done'] ? '✅' : '⬜' ?></button>    </form>    <?= $t['done'] ? '<s>' . htmlspecialchars($t['title']) . '</s>' : htmlspecialchars($t['title']) ?>  </li><?php endforeach; ?></ul><form method="post">  <input type="hidden" name="action" value="add">  <input name="title" placeholder="مهمة جديدة"> <button>أضف</button></form>

كل POST يعدّل ثم يحفظ ثم يحوّل (PRG). والمهام باقية لأنها في data/tasks.json لا في متغير.

🧪 مختبر الخادمأضف مهام وأشّرها، ثم اضغط «🔄 خادم جديد»: ستختفي لأن الخادم الجديد بلا ملفات. وعلى خادم حقيقي تبقى.

لاحظ foreach ($tasks as &$t): العلامة & تجعل $t مرجعًا للعنصر نفسه، فتعديله يعدّل المصفوفة. وبعد الحلقة unset($t) لفك الارتباط.

٤متى لا يكفي الملف؟

الحاجةملف JSONقاعدة بيانات (الوحدة 9)
بيانات قليلة لمستخدم أو مستخدمين✓ مناسبأكثر مما يلزم
آلاف السجلات✗ يُحمَّل الملف كله مع كل طلب✓ تقرأ ما تحتاجه فقط
بحث وتصفية وترتيب✗ بكود PHP يدوي✓ WHERE و ORDER BY
مستخدمون متزامنون كثيرون✗ أقفال الملف تبطئ كل شيء✓ معاملات وأقفال ذكية
علاقات بين البيانات✗ صعبة✓ مفاتيح أجنبية و JOIN

٥الصلاحيات والأمان

توقّع وتحقّق

لإضافة سطر في نهاية ملف دون مسحه:

FILE_APPEND.

تعيد file($f) :

كل سطر عنصر.

fopen($f, 'w') على ملف موجود:

w = كتابة من جديد.

متى نترك الملف إلى قاعدة البيانات؟

الملف للصغير والبسيط.

الخلاصة

تحدٍّ برمجي: سجل الزوار

متوسط

اكتب guestbook.php مع وحدة تخزين في JSON. الزائر يكتب اسمه ورسالته، والكل يرى الرسائل.

المطلوب:
  • احفظ الرسائل في data/guestbook.json مصفوفة عناصر فيها name و msg.
  • POST فيه name و msg (كلاهما غير فارغ بعد trim): أضف الرسالة واحفظ ثم حوّل 303 إلى guestbook.php. وإن نقص أحدهما: 422 والنص الاسم والرسالة مطلوبان.
  • GET يعرض <p>عدد الرسائل: 2</p> ثم كل رسالة <li><b>الاسم</b>: الرسالة</li> بالأحدث أولًا، مع htmlspecialchars.
  • إن لم يوجد الملف: عدد الرسائل 0 بلا تحذيرات.
👀 اعرض الاستجابة المطلوبة
✓ التحقق الآلي: يرسل المختبر هذه الطلبات إلى خادمك بالترتيب ثم يفحص الاستجابات
  1. GET /guestbook.php
  2. POST /guestbook.php (name=سعود&msg=موقع رائع)
  3. 👤 ب POST /guestbook.php (name=نورة&msg=<i>شكرًا</i>)
  4. POST /guestbook.php (name=&msg=اختبار)
  5. 👤 ج GET /guestbook.php
  • الكود يعمل بلا أخطاء أو تحذيرات
  • أول زيارة: عدد الرسائل: 0
  • إضافة ← تحويل 303
  • بلا اسم ← 422
  • زائر جديد يرى الرسالتين
  • الأحدث أولًا، والوسوم معروضة نصًّا
  • تستخدم json_encode و file_put_contents

💡 تلميح 1
حمّل الكل أولًا: $all = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
💡 تلميح 2
للأحدث أولًا: array_reverse($all). وأنشئ مجلد data إن لم يوجد بـ mkdir.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

<?php$file = __DIR__ . '/data/guestbook.json';$all = file_exists($file) ? (json_decode(file_get_contents($file), true) ?? []) : []; if ($_SERVER['REQUEST_METHOD'] === 'POST') {    $name = trim($_POST['name'] ?? '');    $msg = trim($_POST['msg'] ?? '');    if ($name === '' || $msg === '') {        http_response_code(422);        exit('الاسم والرسالة مطلوبان');    }    $all[] = ['name' => $name, 'msg' => $msg];    if (!is_dir(dirname($file))) mkdir(dirname($file), 0755, true);    file_put_contents($file, json_encode($all, JSON_UNESCAPED_UNICODE), LOCK_EX);    header('Location: guestbook.php', true, 303);    exit;} echo "<p>عدد الرسائل: " . count($all) . "</p><ul>";foreach (array_reverse($all) as $m) {    echo "<li><b>" . htmlspecialchars($m['name']) . "</b>: " . htmlspecialchars($m['msg']) . "</li>";}echo "</ul>";
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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