· Admin

Laravel'de Dosya Yükleme ve İlerleme Çubuğu

Kullanıcı bir dosya yüklüyor, sayfa donuyor, 30 saniye bekliyor, sonuç belirsiz... Kötü deneyim. İlerleme çubuğu eklemek kullanıcıya "dosyanız yükleniyor, %45 tamamlandı" mesajı verir. JavaScript'in XMLHttpRequest progress event'i ile bunu kolayca yapabilirsiniz.

Backend: Laravel Controller

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileUploadController extends Controller
{
    public function upload(Request $request)
    {
        $request->validate([
            'file' => ['required', 'file', 'max:51200'], // 50MB max
        ]);

        $file = $request->file('file');

        $filename = time() . '_' . $file->getClientOriginalName();
        $path = $file->storeAs('uploads', $filename, 'public');

        return response()->json([
            'success' => true,
            'path' => $path,
            'url' => Storage::url($path),
            'size' => $file->getSize(),
            'original_name' => $file->getClientOriginalName(),
        ]);
    }
}

Dosya adının başına time() ekliyoruz — aynı isimde dosya yüklenince üzerine yazmasını önler. Production'da Str::uuid() daha güvenli.

Route

Route::post('upload', [FileUploadController::class, 'upload'])->name('file.upload');

Frontend: HTML + JavaScript

<div class="upload-container">
    <form id="uploadForm" enctype="multipart/form-data">
        @csrf
        <div class="file-input-wrapper">
            <input type="file" id="fileInput" name="file" class="form-control">
        </div>

        <div id="progressWrapper" style="display: none; margin-top: 15px;">
            <div class="progress" style="height: 25px; background: #e9ecef; border-radius: 5px; overflow: hidden;">
                <div id="progressBar"
                     style="width: 0%; height: 100%; background: linear-gradient(90deg, #3b82f6, #2563eb); transition: width 0.3s; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 13px;">
                    0%
                </div>
            </div>
            <small id="progressText" class="text-muted mt-1 d-block"></small>
        </div>

        <div id="resultMessage" style="display: none; margin-top: 15px;"></div>

        <button type="submit" id="uploadBtn" class="btn btn-primary mt-3">
            Dosya Yükle
        </button>
    </form>
</div>

JavaScript: XMLHttpRequest ile Progress

document.getElementById('uploadForm').addEventListener('submit', function(e) {
    e.preventDefault();

    const fileInput = document.getElementById('fileInput');
    const progressWrapper = document.getElementById('progressWrapper');
    const progressBar = document.getElementById('progressBar');
    const progressText = document.getElementById('progressText');
    const resultMessage = document.getElementById('resultMessage');
    const uploadBtn = document.getElementById('uploadBtn');

    if (!fileInput.files.length) {
        alert('Lütfen bir dosya seçin.');
        return;
    }

    const file = fileInput.files[0];
    const formData = new FormData();
    formData.append('file', file);
    formData.append('_token', document.querySelector('input[name="_token"]').value);

    // UI hazırlığı
    progressWrapper.style.display = 'block';
    resultMessage.style.display = 'none';
    uploadBtn.disabled = true;
    uploadBtn.textContent = 'Yükleniyor...';

    const xhr = new XMLHttpRequest();

    // İlerleme takibi
    xhr.upload.addEventListener('progress', function(e) {
        if (e.lengthComputable) {
            const percent = Math.round((e.loaded / e.total) * 100);
            progressBar.style.width = percent + '%';
            progressBar.textContent = percent + '%';

            const loaded = formatSize(e.loaded);
            const total = formatSize(e.total);
            progressText.textContent = `${loaded} / ${total} yüklendi`;
        }
    });

    // Tamamlandı
    xhr.addEventListener('load', function() {
        uploadBtn.disabled = false;
        uploadBtn.textContent = 'Dosya Yükle';

        if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            progressBar.style.background = 'linear-gradient(90deg, #22c55e, #16a34a)';
            resultMessage.style.display = 'block';
            resultMessage.className = 'alert alert-success';
            resultMessage.innerHTML = `
                <strong>Yüklendi!</strong> ${response.original_name}
                <br><a href="${response.url}" target="_blank">Dosyayı Görüntüle</a>
            `;
        } else {
            showError('Yükleme başarısız. Lütfen tekrar deneyin.');
        }
    });

    // Hata
    xhr.addEventListener('error', function() {
        uploadBtn.disabled = false;
        uploadBtn.textContent = 'Dosya Yükle';
        showError('Bağlantı hatası. İnternet bağlantınızı kontrol edin.');
    });

    // Zaman aşımı
    xhr.addEventListener('timeout', function() {
        uploadBtn.disabled = false;
        uploadBtn.textContent = 'Dosya Yükle';
        showError('Yükleme zaman aşımına uğradı.');
    });

    xhr.timeout = 300000; // 5 dakika
    xhr.open('POST', '{{ route("file.upload") }}');
    xhr.send(formData);

    function showError(message) {
        progressBar.style.background = 'linear-gradient(90deg, #ef4444, #dc2626)';
        resultMessage.style.display = 'block';
        resultMessage.className = 'alert alert-danger';
        resultMessage.textContent = message;
    }
});

function formatSize(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
    return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}

Neden XMLHttpRequest kullanıyoruz, fetch değil? Çünkü fetch API'sinde upload progress event'i yok. xhr.upload.addEventListener('progress') upload ilerlemesini takip eden tek standart yol. Fetch ile ancak ReadableStream ile download progress takip edebilirsiniz, upload için değil.

Dosya Tipi Kısıtlaması

Backend'de:

$request->validate([
    'file' => ['required', 'file', 'max:51200', 'mimes:jpg,jpeg,png,gif,pdf,doc,docx,xlsx,zip'],
]);

Frontend'de (opsiyonel ama iyi UX):

<input type="file" id="fileInput" name="file"
       accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xlsx,.zip">

Frontend kısıtlaması sadece UX için — kullanıcı dosya seçerken filtreleme yapar. Güvenlik backend'de. Accept attribute'ü atlatılabilir, asıl kontrol her zaman server tarafında olmalı.

Çoklu Dosya Yükleme

<input type="file" id="fileInput" name="files[]" multiple>
public function uploadMultiple(Request $request)
{
    $request->validate([
        'files' => ['required', 'array', 'max:10'],
        'files.*' => ['file', 'max:10240', 'mimes:jpg,png,pdf'],
    ]);

    $uploaded = [];

    foreach ($request->file('files') as $file) {
        $filename = time() . '_' . $file->getClientOriginalName();
        $path = $file->storeAs('uploads', $filename, 'public');

        $uploaded[] = [
            'path' => $path,
            'url' => Storage::url($path),
            'name' => $file->getClientOriginalName(),
        ];
    }

    return response()->json([
        'success' => true,
        'files' => $uploaded,
    ]);
}

Sürükle Bırak (Drag & Drop)

const dropZone = document.getElementById('dropZone');

['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
    dropZone.addEventListener(event, e => {
        e.preventDefault();
        e.stopPropagation();
    });
});

dropZone.addEventListener('dragover', () => {
    dropZone.classList.add('drag-over');
});

dropZone.addEventListener('dragleave', () => {
    dropZone.classList.remove('drag-over');
});

dropZone.addEventListener('drop', (e) => {
    dropZone.classList.remove('drag-over');
    const files = e.dataTransfer.files;
    // files'ı FormData'ya ekleyip yükleme fonksiyonunu çağır
});
.drop-zone {
    border: 2px dashed #cbd5e1;
    border-radius: 10px;
    padding: 40px;
    text-align: center;
    transition: all 0.3s;
    cursor: pointer;
}

.drop-zone.drag-over {
    border-color: #3b82f6;
    background-color: #eff6ff;
}

PHP Upload Limitleri

php.ini'de (veya .user.ini'de):

upload_max_filesize = 50M
post_max_size = 55M
max_execution_time = 300

post_max_size her zaman upload_max_filesize'dan büyük olmalı. Nginx kullanıyorsanız client_max_body_size ayarını da güncellemeyi unutmayın:

client_max_body_size 55M;

Bu ayarları güncellemezseniz Laravel validation'a bile ulaşamadan PHP veya Nginx seviyesinde hata alırsınız. Hata mesajı da genelde açıklayıcı olmaz — sadece "413 Request Entity Too Large" veya boş response.

İlerleme çubuğu basit bir UX iyileştirmesi gibi görünüyor ama kullanıcı memnuniyetinde büyük fark yaratıyor. Özellikle büyük dosya yüklemelerinde kullanıcının "çalışıyor mu acaba?" diye düşünmesini engelliyor.