تحميل الملفات في PHP

صور الملف الشخصي، ومرفقات السيرة الذاتية، ومشاركة الصور — كل تطبيق ويب حقيقي يتعامل مع عمليات تحميل الملفات. وتجعل لغة PHP هذه العملية بسيطة، لكن أي تفصيل أمني واحد يتم تجاهله قد يؤدي إلى ظهور ثغرة خطيرة.

1. المتطلبات الأساسية

(1) إعداد نموذج HTML المطلوب

HTML
<!-- The three requirements for a file upload form -->
<form method="POST" enctype="multipart/form-data">
    <input type="file" name="avatar">
    <button type="submit">Upload</button>
</form>
المتطلب السبب
method="POST" يجب إرسال الملفات عبر طريقة POST
enctype="multipart/form-data" مطلوب! بدونه، يتم إرسال اسم الملف فقط
type="file" يطلب من المتصفح عرض أداة اختيار الملفات
🔥 خطأ شائع: نسيان enctype="multipart/form-data" هو الخطأ الأكثر شيوعًا عند تحميل الملفات — حيث يتلقى PHP اسم الملف دون أن يتلقى محتوياته أبدًا.

(2) إنشاء مجلد التحميل

داخل myphp/، أنشئ مجلدًا باسم uploads/ وتأكد من أن PHP يمكنه الكتابة فيه:

PHP
<?php
// Automatically create the upload directory from your script
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}
?>

2. نظرة متعمقة على $_FILES

PHP
<?php
// After uploading a file, $_FILES is structured like this:
print_r($_FILES['avatar']);
/*
Array (
    [name]     => photo.jpg          // Original filename
    [type]     => image/jpeg         // MIME type reported by the browser
    [tmp_name] => C:\xampp\tmp\php123.tmp  // Temporary path on the server
    [error]    => 0                  // Error code (0 = success)
    [size]     => 85469              // File size in bytes
)
*/
?>
المفتاح المعنى ملاحظة
name اسم الملف الأصلي غير موثوق — يمكن للمستخدمين تزويره
type نوع MIME وفقًا للمتصفح، غير موثوق
tmp_name ملف مؤقت للخادم يتم حذفه تلقائيًا عند انتهاء البرنامج النصي
error رمز الخطأ 0 = نجاح، أي قيمة أخرى = مشكلة
size الحجم بالبايت مفيد لفرض حدود الحجم

(1) رموز الأخطاء

الرمز الثابت المعنى
0 UPLOAD_ERR_OK ✅ تم التحميل بنجاح
1 UPLOAD_ERR_INI_SIZE يتجاوز القيمة المحددة في ملف php.ini upload_max_filesize
2 UPLOAD_ERR_FORM_SIZE يتجاوز MAX_FILE_SIZE الخاص بالنموذج
3 UPLOAD_ERR_PARTIAL تم تحميل الملف جزئيًا فقط
4 UPLOAD_ERR_NO_FILE لم يتم تحديد أي ملف
6 UPLOAD_ERR_NO_TMP_DIR لم يتم تكوين مجلد مؤقت
7 UPLOAD_ERR_CANT_WRITE فشل في الكتابة على القرص

3. move_uploaded_file() — الدالة الأساسية

يخزن PHP الملفات التي تم تحميلها في دليل مؤقت. يجب عليك نقلها صراحةً إلى موقع دائم، وإلا فستُفقد عند انتهاء البرنامج النصي:

PHP
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['avatar'];
    $uploadDir = __DIR__ . '/uploads/';

    if ($file['error'] !== UPLOAD_ERR_OK) {
        echo "Upload failed. Error code: " . $file['error'];
    } else {
        $targetPath = $uploadDir . $file['name'];
        
        if (move_uploaded_file($file['tmp_name'], $targetPath)) {
            echo "Upload successful! File saved at: {$targetPath}";
        } else {
            echo "Failed to move the uploaded file";
        }
    }
}
?>

▶ مثال: التحميل الأساسي للملفات مع معالجة الأخطاء

PHP
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $file = $_FILES['avatar'];
    $uploadDir = __DIR__ . '/uploads/';

    if ($file['error'] === UPLOAD_ERR_OK) {
        $targetPath = $uploadDir . $file['name'];
        if (move_uploaded_file($file['tmp_name'], $targetPath)) {
            echo "<p style='color:green'>Upload successful!</p>";
        }
    } elseif ($file['error'] === UPLOAD_ERR_NO_FILE) {
        echo "<p style='color:red'>Please select a file</p>";
    } else {
        echo "<p style='color:red'>Upload failed. Error code: {$file['error']}</p>";
    }
}
?>

<form method="POST" enctype="multipart/form-data">
    <input type="file" name="avatar">
    <button type="submit">Upload</button>
</form>
▶ جرّب الكود

4. التحقق من الأمان (أمر بالغ الأهمية!)

(1) 4.1 الحد الأقصى لحجم الملف

PHP
<?php
$maxSize = 2 * 1024 * 1024;  // 2MB

if ($file['size'] > $maxSize) {
    die("File too large — maximum 2MB allowed");
}
?>

(2) 4.2 القائمة البيضاء للموسعات

PHP
<?php
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

if (!in_array($ext, $allowed)) {
    die("File type not allowed. Accepted: " . implode(', ', $allowed));
}
?>
💡 نصيحة: استخدم دائمًا قائمة بيضاء (لا تسمح إلا بالأنواع المعروفة بأنها آمنة)، ولا تستخدم أبدًا قائمة سوداء (تحظر ملفات .php و.exe وما إلى ذلك). فالمهاجمون سيجدون دائمًا امتدادًا لم تتضمنه قائمتك السوداء.

(3) 4.3 إنشاء اسم ملف آمن

PHP
<?php
// ❌ Using the user-supplied filename directly is dangerous
// $targetPath = $uploadDir . $file['name'];

// ✅ Generate a unique, safe filename
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$safeName = uniqid('img_') . '.' . $ext;
$targetPath = $uploadDir . $safeName;
?>

▶ مثال: وظيفة التحميل الآمن الكاملة

PHP
<?php
function handleUpload(array $file, string $uploadDir): array {
    $maxSize = 2 * 1024 * 1024;  // 2MB
    $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
    
    // 1. Check for upload errors
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return ['success' => false, 'message' => 'Upload failed'];
    }
    
    // 2. Check file size
    if ($file['size'] > $maxSize) {
        return ['success' => false, 'message' => 'File must not exceed 2MB'];
    }
    
    // 3. Check extension (whitelist)
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    if (!in_array($ext, $allowed)) {
        return ['success' => false, 'message' => 'Only image files are allowed'];
    }
    
    // 4. Verify the real file type (Magic Bytes — far more reliable than extension)
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    
    $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    if (!in_array($mimeType, $allowedMimes)) {
        return ['success' => false, 'message' => 'File type mismatch'];
    }
    
    // 5. Generate a safe filename
    $safeName = uniqid('upload_') . '.' . $ext;
    $targetPath = $uploadDir . $safeName;
    
    // 6. Move the file
    if (move_uploaded_file($file['tmp_name'], $targetPath)) {
        return [
            'success' => true,
            'message' => 'Upload successful',
            'path'    => $targetPath,
            'name'    => $safeName,
        ];
    }
    
    return ['success' => false, 'message' => 'Failed to save the file'];
}

// Usage
$result = handleUpload($_FILES['avatar'], __DIR__ . '/uploads/');
echo $result['message'];
?>
▶ جرّب الكود

5. تحميل ملفات متعددة

PHP
<form method="POST" enctype="multipart/form-data">
    <input type="file" name="photos[]" multiple>
    <button type="submit">Upload Multiple</button>
</form>
PHP
<?php
$uploadDir = __DIR__ . '/uploads/';
$results = [];

foreach ($_FILES['photos']['name'] as $i => $name) {
    // Reassemble into a single-file array
    $singleFile = [
        'name'     => $_FILES['photos']['name'][$i],
        'type'     => $_FILES['photos']['type'][$i],
        'tmp_name' => $_FILES['photos']['tmp_name'][$i],
        'error'    => $_FILES['photos']['error'][$i],
        'size'     => $_FILES['photos']['size'][$i],
    ];
    
    $results[] = handleUpload($singleFile, $uploadDir);
}

foreach ($results as $r) {
    echo $r['message'] . "<br>";
}
?>
💡 نصيحة: يخزن لغة PHP الملفات المتعددة في بنية «عمودية» — حيث تُخزَّن جميع الأسماء في مصفوفة واحدة، وجميع الأحجام في مصفوفة أخرى، وهكذا. ويجب عليك إعادة تجميعها يدويًّا في مصفوفات منفصلة لكل ملف على حدة من أجل معالجتها.


6. عرض الصور التي تم تحميلها

PHP
<?php
// List images in the uploads directory
$images = glob(__DIR__ . '/uploads/*.{jpg,jpeg,png,gif,webp}', GLOB_BRACE);
?>

<h3>Uploaded Images</h3>
<div style="display:flex;flex-wrap:wrap;gap:10px">
    <?php foreach ($images as $img): ?>
        <img src="uploads/<?= basename($img) ?>" width="150">
    <?php endforeach; ?>
</div>

<form method="POST" enctype="multipart/form-data">
    <input type="file" name="avatar">
    <button type="submit">Upload</button>
</form>

❓ أسئلة شائعة

س ما الفرق بين move_uploaded_file وcopy؟
ج move_uploaded_file يتحقق من أن الملف قد وصل فعليًّا عبر HTTP POST (آمن). أما copy() العادي فيمكن استغلاله لنسخ ملفات عشوائية على الخادم. استخدم دائمًا move_uploaded_file() لعمليات التحميل.
س تستمر الملفات الكبيرة في الفشل. ما المشكلة؟
ج تحقق من ثلاثة حدود: (1) upload_max_filesize في ملف php.ini (القيمة الافتراضية 2M)؛ (2) post_max_size (يجب أن تكون أكبر من upload_max_filesize)؛ (3) الحقل المخفي الاختياري MAX_FILE_SIZE في النموذج. بعد تغيير ملف php.ini، أعد تشغيل Apache/Nginx.
س هل $_FILES['avatar']['type'] موثوق به؟
ج لا. إنه نوع MIME الذي يبلغ عنه المتصفح، وهو أمر يسهل تزويره. استخدم finfo_file() لفحص «بايتات السحر» الفعلية للملف من أجل الكشف الدقيق.

📖 ملخص

📝 تمارين

  1. إنشاء ميزة لتحميل الصورة الرمزية: تحديد الحجم بـ 1 ميغابايت، والسماح بتنسيقات jpg/png/gif فقط، وعرض معاينة بعد التحميل.
  2. إضافة رسائل خطأ شاملة إلى ميزة التحميل (حجم الملف كبير جدًّا، نوع الملف غير صحيح، لم يتم تحديد ملف) والحفاظ على حالة النموذج في حالة الفشل.
  3. تنفيذ أداة تحميل جماعي: تحميل ما يصل إلى 5 صور وعرضها في شبكة من الصور المصغرة بعد الانتهاء من التحميل.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%