PHP 5.4 Datei-Upload mit Progressbar in laravel 4

27. März 2013 Web-Entwicklung von Eric Kubenka

In der letzten Zeit habe ich einigen Freiraum, welcher es mir ermöglicht mich mit einigen Themen auseinanderzusetzen für welche ich normalerweise keine Zeit hätte. Momentan lerne ich laravel 4 immer noch kennen. Ich versuche neue Sachen aus und probiere mich an diversen Dingen. Nach Einrichtung, Authentifizierung und einigen Basics stand nun der File-Uplod auf meinem Zettel. Natürlich mit laravel, Twitter Bootstrap, ein wenig jQuery und einer hübschen Fortschrittsanzeige.

Mit PHP 5.4 ist es möglich den aktuellen Uploadstatus über die superglobale Variable $_SESSION abzurufen. Um Informationen über den aktuellen Uploadprozess abzurufen, muss diese Funktion in der “php".ini” aktiviert werden.

session.upload_progress.enabled = On
session.upload_progress.cleanup = On
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
session.upload_progress.freq =  "1%"
session.upload_progress.min_freq = "1"

Der Prozess ist einfach erklärt. Mittels jQuery wird das Submit-Event der Upload-Form abgefangen und fortlaufend mittels AJAX die aktuellen Werte zum Fortschritt des Uploads aus der Session abgefragt. Parallel dazu wird mittels jQuery-Forms ein AJAX-Submit der Form durchgeführt. Ich persönliche habe dazu eine Post-Route definiert.

// Upload route
Route::get('/upload', array('uses' => 'FileController@showUploadPage',
    'as' => 'showUploadPage'));

Route::post('/upload', array('uses' => 'FileController@processUpload',
    'as' => 'processUpload'));

Die Logik im dazugehörige FileController ist recht simpel. Überprüft wird einfach nur ob eine Datei vorhanden ist.

class FileController extends BaseController {

    protected $layout = 'master';

    public function showUploadPage() {
        $this->layout->content = View::make('files.newfile');
    }

    public function processUpload() {
        if (Input::hasFile('inputFileOne')) {
            $file = Input::file('inputFileOne');
            return Redirect::route('showUploadPage')->with('fileUploadSuccessfull', 'Upload successfull');
        } else {
            return Redirect::route('showUploadPage')->with('fileUploadError', 'Please choose a file before you start.');
        }
    }

Im dazugehörigen View ist folgendes Formular definiert. Das eingebundene Hidden-Input-Field dient später zum Abfragen des Upload-Prozesses. Dabei wird der festgelegte Wert des Value-Attributs an das in der php.ini definierte Präfix für den Dateiupload angehangen.

<form action="{'{ URL::route('processUpload') }'}" class="well form-horizontal" method="post" enctype="multipart/form-data" id="uploadform">
    <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="uploadform" />
    <div class="control-group">
        <label class="control-label" for="inputFileOne">Datei: </label>
        <div class="controls">
            <input type="file" class="input" id="inputFileOne" name="inputFileOne">
        </div>
    </div>
    <div class="control-group">
        <div class="controls">
            <button type="submit" class="btn">Upload starten</button>
        </div>
    </div>
</form>
<div id="progresslayer" class="well">
    <div class="progress progress-striped">
        <div id="progressbar" class="bar" ></div>
    </div>
</div>

Da es nur eine simple Testanwendung ist, habe ich den folgenden JavaScript-Code direkt mit in die View eingebunden. Der JavaScript-Part ist das Kernstück der Fortschrittsanzeige. Beim Seitenaufruf wird standardmäßig die Fortschrittsanzeige ausgeblendet und später beim Start des Uploads eingeblendet. Falls die Datei so klein ist, dass Code im Intervall-Abschnitt nicht einmal zur Ausführung kommt, wird der Fortschritt sofort auf 100% gesetzt.

   // Url to listen the Upload progress
    var url = "upl/uplstatus.php";
    var interval = 0;

    $(document).ready(function(e) {
        
        // Hide the progress-bar-div on startup.
        $('#progresslayer').hide();
        
        // catch the upload, prevent the default event and define the constantly triggered ajax-call
        $('#uploadform').submit(function(e) {
            e.preventDefault();
            
            // set interval action --> send post request to
            interval = setInterval(function()
            {
                $.ajax({
                    type: "POST",
                    url: url,
                    context: document.body,
                    dataType: 'json'
                }).done(function(data) {
                    if (data)
                    {
                        if (data.result !== false)
                        {
                            // uplaoad still in progress
                            $('#progresslayer').show();
                            var status = Math.round((data.bytes_processed / data.content_length) * 100);
                            $('#progressbar').width(status + '%');
                        }
                        else
                        {
                            // upload finished...waiting for answer of the ajaxSubmit-Event.
                            $('#progressbar').width('99%');
                        }
                    }
                });

            }, 1000);
            
            // submit the form via POST and catch the success
            $('#uploadform').ajaxSubmit({
                success: function()
                {
                    // Upload should be completed now: show progresbar, if not shown yet.
                    $('#progresslayer').show();
                    $('#progressbar').width('100%');
                    clearInterval(interval);
                },
                error: function()
                {
                    clearInterval(interval);
                }
            });

            
        });

    });

Wer laravel 4 bereits nutzt weiß, dass alle Session-Informationen mittels “Session::all()” abgerufen werden können. Alle Vorteile die dieses Verfahren bringt nützen zum Abrufen des Uploadstatus nichts, da die Informationen nicht in der laravel-Session gespeichert sind. Wahrscheinlich wird das in der Zukunft noch geändert. Um dennoch einen lauffähigen Fortschrittsbalken mit wenig Aufwand und PHP 5.4-Bordmitteln zu implementieren, habe ich mich schmutziger Weise an laravel vorbeigearbeitet. Dazu greife ich auf folgende PHP-Datei (/public/upl/uplstatus.php) zu, welche den aktuellen Uploadstatus mittels JSON überträgt. Wichtig hierbei ist, dass der übermittelte Wert des Hidden-Input-Felds an das Präfix angehangen wird. Da ich den Abruf der Session-Daten in einer anderen Datei verarbeitete, als den Upload-Prozess an sich, kann Wert nicht mittels $_POST[] abgegriffen werden.

session_start();

// progress name have to match the hidden input value
$progressName = ini_get("session.upload_progress.prefix")."uploadform";

if(isset($_SESSION[$progressName]))
{
    // if session upload value is set 
    echo json_encode($_SESSION[$progressName]);
}
else
{
    // return false, if no upload is in progress
    echo json_encode(array("result" => false));
}

Klar ist es eine recht unsaubere Methode, da das Framework übergangen wird, aber ich habe es nicht über laravel-Bordmittel hinbekommen, daher bin ich über jeden Tipp dankbar.

Mittels dieses kleinen Workarounds ist es möglich mit laravel 4 die PHP 5.4-Funktionen zum Abrufen des Uploadstatus abzugreifen und in einem Fortschrittsbalken anzuzeigen. Hat der Benutzer JavaScript deaktiviert, so wird der Upload-Prozess ohne Fortschrittsanzeige durchgeführt und das Ergebnis durch eine kleine Meldung dem mitgeteilt.

 

Wichtige Links:

  1. jQuery
  2. jQuery-Forms
  3. Twitter Bootstrap
  4. laravel 4

Zurück