حفظ البيانات في الملفات
أبسط مخزن دائم، وما يكفيه وما لا يكفيه. ⏱ نحو 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>
<?php// store.php: دوال تخزين بسيطة في ملف JSONconst DATA_FILE = __DIR__ . '/data/tasks.json'; function load_tasks(): array{ if (!file_exists(DATA_FILE)) { return []; } return json_decode(file_get_contents(DATA_FILE), true) ?? [];} function save_tasks(array $tasks): void{ if (!is_dir(dirname(DATA_FILE))) { mkdir(dirname(DATA_FILE), 0755, true); } file_put_contents(DATA_FILE, json_encode($tasks, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);}
كل POST يعدّل ثم يحفظ ثم يحوّل (PRG). والمهام باقية لأنها في data/tasks.json لا في متغير.
لاحظ foreach ($tasks as &$t): العلامة & تجعل $t مرجعًا للعنصر نفسه، فتعديله يعدّل المصفوفة. وبعد الحلقة unset($t) لفك الارتباط.
٤متى لا يكفي الملف؟
| الحاجة | ملف JSON | قاعدة بيانات (الوحدة 9) |
|---|---|---|
| بيانات قليلة لمستخدم أو مستخدمين | ✓ مناسب | أكثر مما يلزم |
| آلاف السجلات | ✗ يُحمَّل الملف كله مع كل طلب | ✓ تقرأ ما تحتاجه فقط |
| بحث وتصفية وترتيب | ✗ بكود PHP يدوي | ✓ WHERE و ORDER BY |
| مستخدمون متزامنون كثيرون | ✗ أقفال الملف تبطئ كل شيء | ✓ معاملات وأقفال ذكية |
| علاقات بين البيانات | ✗ صعبة | ✓ مفاتيح أجنبية و JOIN |
٥الصلاحيات والأمان
- يحتاج برنامج الخادم صلاحية الكتابة على المجلد الذي يحفظ فيه. على
Linuxتُضبط بـchmod(مثلًا 755 للمجلد)، وسنراها في درس النشر. - ملفات البيانات خارج
public، أو امنعها بـ .htaccess، وإلا يقرأ أي أحدdata/tasks.jsonمن الرابط. - لا تبنِ اسم ملف من مدخلات المستخدم مباشرة:
file_get_contents($_GET['f'])قد تقرأ../config.php! استخدمbasenameوقائمة مسموحة.
توقّع وتحقّق
لإضافة سطر في نهاية ملف دون مسحه:
FILE_APPEND.تعيد file($f) :
fopen($f, 'w') على ملف موجود:
w = كتابة من جديد.متى نترك الملف إلى قاعدة البيانات؟
الخلاصة
file_put_contentsللكتابة (وFILE_APPENDللإضافة)، وfile_get_contentsوfileللقراءة.fopenثمfgetsفي حلقة ثمfcloseللملفات الكبيرة.- حمّل ← عدّل ← احفظ:
loadوsaveفوق ملفJSON. - الملف للبيانات القليلة البسيطة، وقاعدة البيانات للكثيرة والمتزامنة والمترابطة.
- ملفات البيانات خارج
public، ولا أسماء ملفات من المستخدم.
تحدٍّ برمجي: سجل الزوار
متوسطاكتب 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 بلا تحذيرات.
👀 اعرض الاستجابة المطلوبة
GET /guestbook.phpPOST /guestbook.php(name=سعود&msg=موقع رائع)- 👤 ب
POST /guestbook.php(name=نورة&msg=<i>شكرًا</i>) POST /guestbook.php(name=&msg=اختبار)- 👤 ج
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>";