PHP 5.4 Datei-Upload mit Progressbar in laravel 4

27. März 2013 Web-Entwicklung von

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

Einen Kommentar schreiben

Kommentar von Sascha |

Vielen dank, hat mir fürs erste enorm weitergeholfen .