Zend_Form i wysyłanie plików - cz. 2

by Mateusz Tymek — on Zend Framework, PHP, JavaScript/AJAX

Head's up! This post was written back in 2009 and is very likely to contain outdated information.

W poprzednim artykule zaprezentowałem wysyłanie plików z formularza opartego o komponent Zend_Form. Teraz tamten przykład zostanie wzbogacony o możliwość wysyłania w tle przy pomocy JavaScript-u i biblioteki jQuery.

Plan

Ze względów bezpieczeństwa interpreter JavaScript nie ma dostępu do systemu plików, zatem nie może brać bezpośredniego udziału w przesyłaniu. Transfer plików w tle można zrealizować przy pomocy małej sztuczki: formularz z plikiem może być przesłany poprzez niewidoczną ramkę (IFRAME). Po zakończeniu procesu serwer zwróci kod JavaScript który poinformuje użytkownika o sukcesie operacji (lub o ewentualnym niepowodzeniu).

Formularz

Definicja formularza prawie niczym się nie różni od tej przedstawionej w poprzednim artykule:

<?php

class UploadForm extends Zend_Form
{
    public function init()
    {
        $this->addElement('file', 'uploadFile', array(
            'destination' => APPLICATION_PATH.'/uploads',
            'validators' => array(
                array('count', false, 1),
                array('size', false, 102400),
            ),
            'label' => 'Wyślij plik:'
        ));

        $this->addElement('submit', 'submit', array(
            'label' => 'Wyślij'
        ));

        $this->setEnctype('multipart/form-data');
        $this->setAttrib('id', 'uploadForm');
    }
}

Jedyna zmiana to ustawienie atrybutu id, który pomoże odwołać się do formularza z poziomu JavaScript.

Wysyłanie pliku od strony klienta

Trzeba mieć na uwadze że nie wszyscy użytkownicy mają włączoną obsługę JavaScriptu, a kod powinien zadziałać w każdej sytuacji. Dlatego wykorzystamy funkcję $(document).ready() dostępną w jQuery (więcej...) - bez JavaScriptu funkcja nie wykona się a plik zostanie wysłany zwykłym sposobem.
Kod umieścimy w pliku upload.js:

$(document).ready(function() {

    // 1.
    var iFrame = $('<iframe name="uploadFrame" id="uploadFrame" src=""></iframe>').hide();
    $('body').append(iFrame);

    // 2.
    $('#uploadForm').attr('target', 'uploadFrame');

    // 3.
    $('#uploadForm').attr('action', 'index.php?asyncUpload=1');

    // 4.
    $('#uploadForm').submit(function() {
        $('#uploadForm #submit').attr('disabled', 'disabled');
        $('#information').html('Wysyłanie pliku - proszę czekać');
        return true;
    });

    // 5.
    window.finishUpload = function(information) {
        $('#information').html(information);
        $('#uploadForm #submit').attr('disabled', '');
    }
});

Wyjaśnienie:

  1. Utworzenie i ukrycie (hide()) ramki, oraz dopisanie jej do dokumentu.
  2. Ustawienie atrybutu target formularza aby przesyłanie odbywało się poprzez ramkę.
  3. Będziemy wysłać dodatkowy parametr (asyncUpload) dzięki któremu serwer rozpozna że plik jest wysyłany asynchronicznie.
  4. Funkcja wywoływana w chwili naciśnięcia przycisku "wyślij". Jej zdaniem jest zablokowanie formularza i poinformowanie użytkownika o tym że coś się dzieje w tle. W tym miejscu można wyświetlić np. jakąś animację.
  5. Funkcja wywoływana przez ramkę w momencie zakończenia wysyłania. Jej parametr to łańcuch tekstowy z komunikatem dla użytkownika.

Odbieranie pliku po stronie serwera

Kod odbierający plik wymaga jedynie niewielkich zmian w stosunku do swej pierwotnej wersji. Przede wszystkim należy odpowiednio zareagować jeśli formularz został wysłany asynchronicznie:

if ($request->getParam('asyncUpload')) {
    echo <<<STOP
<script type="text/javascript">
    window.parent.finishUpload("$information");
</script>
STOP;
} else {
    $view->uploadForm = $uploadForm;
    $view->information = $information;
    echo $view->render('index.phtml');
}

Jeśli formularz jest wysłany poprzez ramkę to wystarczy wygenerować kod JS który przekaże użytkownikowi odpowiedni komunikat (czyli wywoła funkcję finishUpload()). W przeciwny wypadku (także gdy formularz nie został wcale wysłany a użytkownik po prostu wyświetla stronę) wygenerowana zostaje "normalna" treść.

Właściwa obsługa formularza i odbieranie pliku nie prawie ulegną zmianie. Sprawimy jedynie aby zmienna $information przekazywała więcej treści w wypadku niepowodzenia:

if ($request->isPost()) {
    if (!$uploadForm->isValid($request->getPost())) { // Próba walidacji formularza
        // sformatowanie komunikatu tak aby zawierał informacje o błędach
        $messages = implode('<br />', $uploadForm->uploadFile->getMessages());
        $information = 'Błąd podczas sprawdzania poprawności formularza. <br />'
                     . '<em>'.$messages.'</em>';
    } elseif (!$uploadForm->uploadFile->isUploaded()) { // Czy cokolwiek zostało wysłane?
        $information = 'Nie wybrano pliku do wysłania.';
    } elseif (!$uploadForm->uploadFile->receive()) { // Odbiór pliku
        $information = 'Błąd podczas odbierania pliku.';
    } else { // Sukces
        $information = 'Plik ' . $uploadForm->uploadFile->getFileName()
                     . ' został poprawnie wysłany.';
    }
}

Widok

Na koniec wystarczy dodać dwie linijki do sekcji <head> w skrypcie widoku:

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript" src="upload.js"></script>

Najpierw (korzystając z uprzejmości Google) dołączamy bibliotekę jQuery, a następnie - nasz kod JS.

Podsumowanie

W tym artykule postarałem się możliwie najprościej zaprezentować koncepcję wysyłania plików w tle poprzez ukrytą ramkę i połączyć ją z komponentem Zend_Form. Takie rozwiązanie ma jednak kilka wad o których należy pamiętać tworząc produkcyjną wersję aplikacji. Przede wszystkim jeśli wysyłanie pliku nie powiedzie się (a aplikacja wygeneruje wyjątek) to użytkownik będzie cały czas widział napis "wysyłanie pliku", zamiast odpowiedniego komunikatu. Inną wadą (istotną dla purystów, mniej ważną z praktycznego punktu widzenia) jest fakt iż ramki IFRAME oraz atrybut target nie są zgodne ze standardem XHTML Strict, a całe rozwiązanie opiera się na ich stosowaniu. Niestety standard nie opisuje żadnej alternatywnej metody wysyłania plików w tle.
Ponadto istnieje już wiele takich rozwiązań (działających po stronie klienta) zaimplementowanych jako wtyczki do bibliotek jQuery, Prototype, MooTools czy Dojo. Warto poszukać - być może trafi się na rozwiązanie które idalnie pasuje do naszego projektu.
Jeszcze inna alternatywa dla wysyłania plików w tle to zastosowanie obiektu Flash.

Przykład

Dołączony przykład to rozwinięta wersja aplikacji z poprzedniego artykułu. Tak samo jak poprzednio nie wykorzystuje całej infrastruktury MVC dostępnej w Zend Framework i do jej uruchomienia wystarczy podanie prawidłowej ścieżki do bibliotek w pliku index.php.

Pobierz przykład.

Przydatne linki


comments powered by Disqus