عند رفع الملفات في لارافل، قد تواجه مشاكل بسبب محدودية الذاكرة أو محدودية رفع الملفات في إعدادات السيرفر أو إعدادات php.

فهذا حل سهل لهذه المشكلة.

composer require pion/laravel-chunk-upload
  • ومن متطلباتها استخدام مكتبة رفع Front End وأنا جربت مكتبة Dropzone.

ويمكن تنزيلها عير هذا الأمر

npm install --save dropzone
  • ولا تنس تفعيل JavaScript لكي تعمل المكتبة المستخدمة.
npm run dev
  • في ملف ال blade
<form class="dropzone" id="my-form"></form>
  • في ملف الroutes وليكن web.php
Route::post('/file-upload', [FileController::class, 'upload']);
  • في ملف app.js
import Dropzone from 'dropzone'
import 'dropzone/dist/dropzone.css'

;(function () {
  // this to work after vite app.js been loaded
  const token = document
    .querySelector('meta[name="csrf-token"]')
    .getAttribute('content')
  let myDropzone = new Dropzone('#my-form', {
    url: '/file-upload', // هذا الرابط للرفع في web.php
    method: 'post',
    chunking: true,
    chunkSize: 2097152, // 2MB
    headers: {
      'X-CSRF-TOKEN': token,
    },
  })
})()

في FileController وهي مأخوذة من هذا المثال وفيه تفصيل كذلك في تسمية الملفات والمجلدات.. ولم أذكرها هنا اختصارا.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Events\ProcessUploadedFile;
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
use Illuminate\Http\UploadedFile;
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;

class FileController extends Controller
{
    public function upload(Request $request, FileReceiver $receiver)
    {
        // create the file receiver
        $receiver = new FileReceiver("file", $request, HandlerFactory::classFromRequest($request));

        // check if the upload is success, throw exception or return response you need
        if ($receiver->isUploaded() === false) {
            throw new UploadMissingFileException();
        }

        // receive the file
        $save = $receiver->receive();

        // check if the upload has finished (in chunk mode it will send smaller files)
        if ($save->isFinished()) {
            // save the file and return any response you need, current example uses `move` function. If you are
            // not using move, you need to manually delete the file by unlink($save->getFile()->getPathname())
            $filepath = $this->saveFile($save->getFile());
            ProcessUploadedFile::dispatch($filepath);
        }
    }

    protected function saveFile(UploadedFile $file)
    {
        $fileName = $this->createFilename($file);
        $finalPath = storage_path("app/upload/");
        $file->move($finalPath, $fileName);

        return $finalPath . $fileName;
    }

    protected function createFilename(UploadedFile $file)
    {
        return $file->getClientOriginalName();
    }
}

وهنا مثال افتراضي لما قد يحتويه الملف وليكن يحتوي email و username لنقم بتقسيم رفع المحتوى عن طريق jobs/batch queues، واستخدمنا كذلك php generators عن طريق الكلمة المفتاحية yield.

<?php

namespace App\Jobs;

use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Bus;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class ProcessUploadedFile implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public string $filepath)
    {
        //
    }

    public function handle(): void
    {
        $fileBath = $this->filepath;

        $generateRow = function ($row) {
            return [
                'email' => $row[1],
                'username' => $row[2],
            ];
        };

        $batch = [];
        foreach ($this->chunkFile($fileBath, $generateRow, 1000) as $chunk) {
            $batch[] = new ProcessUploadedChunk($chunk);
        }

        Bus::batch($batch)
            ->allowFailures()
            ->catch(function ($batch, $exception) {
                logger($batch->id . " failed! Exception: {$exception}");
            })
            ->then(function ($batch) {
                logger($batch->id . ' has completed successfully with no errors');
            })
            ->finally(function ($batch) {
                logger('finally ' . $batch->id . ' has completed');
            })
            ->dispatch();
    }

    public function chunkFile(string $path, callable $generator, int $chunkSize)
    {
        $file = fopen($path, 'r');

        $data = [];

        for ($i = 1; ($row = fgetcsv($file, null, ',')) !== false; $i++) {
            $data[] = $generator($row);

            if ($i % $chunkSize === 0) {
                yield $data;
                $data = [];
            }
        }

        if (!empty($data)) {
            yield $data;
        }

        fclose($file);
    }
}

وفي ملف ProcessUploadedChunk وليكن رفع البيانات إلى User Model بحسب مثالنا الافتراضي.

<?php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessUploadedChunk implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public array $chunk)
    {
        //
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        User::insert($this->chunk);
    }
}

وأود شكر الصديق العزيز مصطفى هموني على تعليمي لهذه المعلومات وإنما جمعتها في هذه التدوينة لإفادة الغير وكي تكون مرجعا للعودة إليها.