Simple Zend_Form File Upload Example

Zend Framework 1.5's Zend_Form component is missing support for the file input element as it is waiting on a file upload component to build upon. We're busy people, so we'll fake it…

This is a super simple example showing how to do file uploads with Zend_Form in Zend Framework 1.5.

I'm building on the Simple Zend_Form Example, so make sure you have read that and that it works before you start this one.

This is what the form looks like:

SimpleFileUploadScreenshot.jpg

(Unstyled, as usual!)

This is what we need to do:

The form

The form is an extension of Zend_Form and is stored in app/forms/UploadForm.php and so the class name is forms_UploadForm:

<?php

class forms_UploadForm extends Zend_Form 
{ 
    public function __construct($options null) 
    { 
        $this->addElementPrefixPath('App''App/');
        parent::__construct($options);
        $this->setName('upload');
        $this->setAttrib('enctype''multipart/form-data');
        
        $description = new Zend_Form_Element_Text('description');
        $description->setLabel('Description')
                  ->setRequired(true)
                  ->addValidator('NotEmpty');

        $file = new App_Form_Element_File('file');
        $file->setLabel('File')
                 ->setRequired(true)
                 ->addValidator('NotEmpty');
             

        $submit = new Zend_Form_Element_Submit('submit');
        $submit->setLabel('Upload');
        
        $this->addElements(array($description$file$submit));
            

    } 
} 

We are going to create our own validator class within our own library namespace (lib/App) and so we need to tell the form about this using addElementPath(). We then set the name and enctype attribute of the form to allow for files to be uploaded.

The form has two fields: a text field called 'description' and the file upload field called 'file', along with a submit button. As Zend Framework doesn't have it's own file element, we will create our own called App_Form_Element_File.

The file form element

The file element, App_Form_Element_File, is stored in lib/App/Form/Element/File.php and looks like this:

<?php
require_once 'Zend/Form/Element/Xhtml.php';

class App_Form_Element_File extends Zend_Form_Element_Xhtml
{
    /**
     * Flag indicating whether or not to insert ValidFile validator when element is required
     * @var bool
     */
    protected $_autoInsertValidFileValidator true;

    /**
     * Default view helper to use
     * @var string
     */
    public $helper 'formFile';
    
    /**
     * Set flag indicating whether a ValidFile validator should be inserted when element is required
     * 
     * @param  bool $flag 
     * @return Zend_Form_Element
     */
    public function setAutoInsertValidFileValidator($flag)
    {
        $this->_autoInsertValidFileValidator = (bool) $flag;
        return $this;
    }

    /**
     * Get flag indicating whether a ValidFile validator should be inserted when element is required
     * 
     * @return bool
     */
    public function autoInsertValidFileValidator()
    {
        return $this->_autoInsertValidFileValidator;
    }
    
    
    public function isValid($value$context null)
    {
        // for a file upload, the value is not in the POST array, it's in $_FILES
        $key $this->getName();
        if(null === $value) {
            if(isset($_FILES[$key])) {
                $value $_FILES[$key];
            }
        }
        
        // auto insert ValidFile validator
        if ($this->isRequired()
            && $this->autoInsertValidFileValidator()
            && !$this->getValidator('ValidFile'))
        {
            $validators $this->getValidators();
            $validFile   = array('validator' => 'ValidFile''breakChainOnFailure' => true);
            array_unshift($validators$validFile);
            $this->setValidators($validators);
            
            // do not use the automatic NotEmpty Validator as ValidFile replaces it 
            $this->setAutoInsertNotEmptyValidator(false);
        }

        return parent::isValid($value$context);
    }

}

Zend Framework already provides a view helper for displaying a file element, formFile, so we set the $helper member variable to 'formFile' so that the correct element is rendered when the form is displayed. We then need to ensure that validation is handled correctly. For all other form elements the value of the field is returned in the POST array. For a file, this is not true as the data is with the $_FILES global array. We could handle this in the controller, but by putting it in the element class, we never have to think about it again. The isValid() member function is used to set the value for an element and also run the validator chain to determine if the value is valid or not.

We override isValid() for the file element to provide two functionalities:

* Set the value to the contents of correct sub-array of the $_FILES array .
* Automatically turn on a custom validator called ValidFile which will check if the upload succeeded.

The file element also has some helper functions (setAutoInsertValidFileValidator and getAutoInsertValidFileValidator) to control the auto-insertion of the ValidFile validator. Note that if we do automatically insert the ValidFile validator, then we turn off Zend_Form_Element's automatic NotEmpty validator as it is redundant.

Once we have set the $value variable correctly and inserted our validator, we call up to the parent's isValid() function which will run the validation chain for us.

The ValidFile validator

The ValidFile validator's full class name is App_Validate_ValidFile and so it's filename is lib/App/Validate/ValidFile.php. This class handles validating the 'error' field within the $value array (which as you'll recall came from $_FILES). This is the code:

<?php

class App_Validate_ValidFile extends Zend_Validate_Abstract
{

    const INI_SIZE 'iniSize';     // Value: 1; The uploaded file exceeds the upload_max_filesize directive in php.ini
    const FORM_SIZE 'formSize';   // Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form. 
    const PARTIAL 'partial';      // Value: 3; The uploaded file was only partially uploaded. 
    const NO_FILE 'noFile';       // Value: 4; No file was uploaded. 
    const NO_TMP_DIR 'noTmpDir';  // Value: 6; Missing a temporary folder.
    const CANT_WRITE 'cantWrite'// Value: 7; Failed to write file to disk.
    const EXTENSION 'extension';  // Value: 8; File upload stopped by extension. Introduced in PHP 5.2.0. 
    const ERROR 'error';          // General error for future proofing against new PHP versions

    /**
     * @var array
     */
    protected $_messageTemplates = array(
        self::INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini",
        self::FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form",
        self::PARTIAL => "The uploaded file was only partially uploaded",
        self::NO_FILE => "No file was uploaded",
        self::NO_TMP_DIR => "Missing a temporary folder",
        self::CANT_WRITE => "Failed to write file to disk",
        self::EXTENSION => "File upload stopped by extension",
        self::ERROR => "Unknown upload error"
    );

    /**
     * Defined by Zend_Validate_Interface
     *
     * Returns true if and only if $value[error] is equal to UPLOAD_ERR_OK.
     * 
     * @note: This validator expects $value to be the array from $_FILES
     *
     * @param array $value
     * @return boolean
     */
    public function isValid($value)
    {
        // default value and error is "no file uploaded"
        $valueString ";

        $error UPLOAD_ERR_NO_FILE;
        
        if(is_array($value) && array_key_exists('error'$value)) {
            // set the error to the correct value
            $error $value['error'];
            
            // set the %value% placeholder to the uplaoded filename
            $valueString $value['name'];
        }
        
        $this->_setValue($valueString);

        $result false;
        switch($error) {
            case UPLOAD_ERR_OK:
                $result true;
                break;
                
            case UPLOAD_ERR_INI_SIZE:
                $this->_error(self::INI_SIZE);
                break;
                
            case UPLOAD_ERR_FORM_SIZE:
                $this->_error(self::FORM_SIZE);
                break;
                
            case UPLOAD_ERR_PARTIAL:
                $this->_error(self::PARTIAL);
                break;
                
            case UPLOAD_ERR_NO_FILE:
                $this->_error(self::NO_FILE);
                break;
                
            case UPLOAD_ERR_NO_TMP_DIR:
                $this->_error(self::NO_TMP_DIR);
                break;
                
            case UPLOAD_ERR_CANT_WRITE:
                $this->_error(self::CANT_WRITE);
                break;
                
            case 8// UPLOAD_ERR_EXTENSION isn't defined in PHP 5.1.4, so use the value
                $this->_error(self::EXTENSION);
                break;
                
            default:
                $this->_error(self::ERROR);
                break;
        }

        return $result;
    }

}

Although this class is quite long, not much happens! All the work happens in isValid() where we set $error to the 'error' value originally from the $_FILES array and then set our error message based on which error PHP has reported to us. If PHP reported no error (UPLOAD_ERR_OK), then we return true, otherwise we return false after having set the error value.

The controller

Lastly, the controller is essentially identical to what we did in the Simple Zend_Form Example:

<?php

class IndexController extends Zend_Controller_Action
{
    function indexAction()
    {
        $this->view->pageTitle "Zend_Form File Upload Example";
        $this->view->bodyCopy "<p>Please fill out this form.</p>";

        $form = new forms_UploadForm();

        if ($this->_request->isPost()) {
            $formData $this->_request->getPost();
            if ($form->isValid($formData)) {
                
                // success - do something with the uploaded file
                $uploadedData $form->getValues();
                Zend_Debug::dump($uploadedData'$uploadedData');
                exit;
                
            } else {
                $form->populate($formData);
            }
        }

        $this->view->form $form;
    }
}

If it all validates correctly, then you need to process the uploaded data. For this example we just display it using Zend_Debug::dump(), so you can see what it looks like:

SimpleFileUploadScreenshot2.jpg

Conclusion

As you can see, although Zend Framework 1.5 doesn't have file upload element built in, adding one is not especially hard. In fact, we have more code used to validate that and provide an error message on failure than we do for the upload itself!

As usual, here's a zip file of this project: Zend_Form_FileUpload_Example.zip (It includes Zend Framework 1.5.1 which is why it's 3.9MB big).

Test it out and maybe use it as the basis of your file uploading needs with Zend_Form.

27 Responses to “Simple Zend_Form File Upload Example”

  1. 1 Dave Marshall

    Great post, I've been looking at this for a project one of our trainees is doing, we ended up with a simple Zend_Form_Element with the helper set, will go on to implement everything you've got here now.

  2. 2 Matthew Weier O'Phinney

    Very nice tutorial, Rob! Keep 'em coming!

  3. 3 Dominik Bulaj

    Great tutorial, thanks!
    I've found some missing things in
    App_Validate_ValidFile::isValid()
    There's one double-quote only:
    $valueString = ";

    Anyway, thank you for very helping post.

  4. 4 Rob...

    Dominik,

    That's two single quotes in the database. I wonder which plugin is "auto-converting" it…

    Regards,

    Rob…

  5. 5 RST

    I love your work Rob and I use ZF myself but when it takes over 200 lines of code to do a file upload something is not right. Now, I'm not trying to criticize you in any way (heck I will be buying your book when it comes out) and I think you have presented a very solid solution within the Zend Framework but damn that is a lot of code just to upload and validate a file.

    I went and checked with Code Igniter to see what it took to accomplish a similar upload/validation and found a solution that takes less than 1/3 as much code to accomplish. No it doesn't provide as fine grained control over errors as your example does but overall, as a web developer, that kind of strikes me as a pretty substantial difference in productivity. Don't you think?

  6. 6 Rob...

    Hi RST,

    Part of the issue is that there isn't a ZF solution at all at the moment. When there is, then the code you'd write will be just:

    
    $file = new App_Form_Element_File('file');
    $file->setLabel('File')
         ->setRequired(true)
         ->addValidator('NotEmpty');
    
    

    If minimising code code had been a goal, I could have reduced the App_Validate_ValidFile class down to probably 6 to 10 lines, but I chose to go to the last mile and cover everything, include future proofing for new error codes. Whilst I may not need that granularity on most projects, on the ones that I do, it's nice not to have to write any new code.

    Final thought: For code that is completely reusable like this, I don't particularly mind the size. I've written this once and have reused it on 3 projects so far, so the cost of writing trends to trivial.

    Regards,

    Rob…

  7. 7 JAB

    Rob,
    Thanks for the example. We are currently struggling with providing a progress bar during the upload. Our customers upload large (hundreds of megabytes) files to our site and they really want a progress bar to know it is working. Does the above do anything with that or is it the "quiet until done or error" type of upload.

    We haven't found a Php solution that works and are looking at a Perl one but that one is problematic. Any guidance or hope you can give us?
    Thanks!
    John

  8. 8 Dave Marshall

    @John

    Try having a look at APC (http://uk3.php.net/apc), the newer versions have hooks for monitoring file uploads, only problem being they're not thread safe.

  9. 9 Rob...

    JAB,

    Doesn't YUI have a progress bar thing? Might be worth a look.

    Regards,

    Rob…

  10. 10 jjshell

    Rob,
    Thank you for this tutorial. It's a great one.

    Have you scheduled a bit of Ajax in the tutorials to come? It is still quite unclear to me how to submit a form and process it the Ajax way with the ZF, and I'm afraid the doc doesn't cut it for the average php guy that I am.

    :)

  11. 11 Jonathan

    When I uploaded a file, I received this Warning:

    Warning: htmlspecialchars() expects parameter 1 to be string, array given in /var/www/site/library/Zend/View/Abstract.php on line 786

  12. 12 Yura Volovod

    Thanks! It's great.

  13. 13 Dave Marshall

    Why not propose this for inclusion in the Framework? There's a couple of proposals that mention it, but they don't fit with Zend_Form like this does.

  14. 14 Rob...

    Dave,

    When Zend_FileUpload or whatever is completed, I'm sure Matthew ensure that a kick-ass solution is implemented :)

    Regards,

    Rob…

  15. 15 Sam

    I get the same warning as Johnathan.

    Warning: htmlspecialchars() expects parameter 1 to be string, array given in C:\home\zendtest\library\Zend\View\Abstract.php on line 786

    It's an issue with the validation: if I fill out both fields properly the form submits; otherwise the warning.

    Sam

  16. 16 Mario

    I discovered the same htmlspecialchars error while developing my own file upload in Zend. I modified you isValid method a little bit:

    public function isValid($value, $context = null)
    {
    // for a file upload, the value is not in the POST array, it's in $_FILES
    $key = $this->getName();
    $fileValue = $value;
    if(null === $fileValue) {
    if(isset($_FILES[$key])) {
    if($_FILES[$key]['error'] != UPLOAD_ERR_NO_FILE ||
    !$this->getAllowEmpty() ||
    $this->isRequired()) {
    $fileValue = $_FILES[$key];
    }
    }
    }

    $return = parent::isValid($fileValue, $context);

    // parent called setValue so let us overwrite it
    $this->setValue($value);
    }

    You also need test for the required and allowEmpty values.

  17. 17 ed

    In response to the comments about the htmlspecialchars warning, the reason for that error is that the isValid() function is converting the string $value to an array from $FILES. The controller is expecting this array (during success) while the view is expecting a string to make a textbox (during error). The easiest way to fix it is to just revert to the original value whenever an error occurs.

    $key = $this->getName();
    $fileValue = $value;
    if(null === $value) {
    if(isset($_FILES[$key])) {
    $fileValue = $_FILES[$key];
    }
    }

    … no change …

    $return = parent::isValid($fileValue, $context);

    if(!$return){
    //its not valid so revert to the original value
    $fileValue = $value;
    }

    // parent called setValue so let us overwrite it
    $this->setValue($fileValue);

    Hopefully, this will be helpful to someone else.

  18. 18 Sam

    Thanks for the code and explanation ed, you were right. I added
    return $return;
    to the end to make it work.

    One issue: when you leave the description field blank and upload a valid form, the warning appears. I guess at that point the value is the FILES array and as such will trigger the warning.

    Sam

  19. 19 paul

    So, is it correct?

    public function isValid($value, $context = null)
    {
    // for a file upload, the value is not in the POST array, it's in $_FILES
    $key = $this->getName();
    $fileValue = $value;
    if(null === $value) {
    if(isset($_FILES[$key])) {
    $fileValue = $_FILES[$key];
    }
    }

    // auto insert ValidFile validator
    if ($this->isRequired()
    && $this->autoInsertValidFileValidator()
    && !$this->getValidator('ValidFile'))
    {
    $validators = $this->getValidators();
    $validFile = array('validator' => 'ValidFile', 'breakChainOnFailure' => true);
    array_unshift($validators, $validFile);
    $this->setValidators($validators);

    // do not use the automatic NotEmpty Validator as ValidFile replaces it
    $this->setAutoInsertNotEmptyValidator(false);
    }

    //return parent::isValid($value, $context);

    $return = parent::isValid($fileValue, $context);

    if(!$return){
    //its not valid so revert to the original value
    $fileValue = $value;
    }

    // parent called setValue so let us overwrite it
    $this->setValue($fileValue);

    return $return;

    }

  20. 20 Cully

    Just a simple spelling error (well, two of them):

    self::ERROR => "Uknown uplaod error"

    Other than that, great job! I'm glad I didn't try to do file upload stuff in ZF a couple weeks ago :)

  21. 21 Rob...

    Thanks Cully,

    I have updated.

    Regards,

    Rob..

  22. 22 Manu

    Hi…

    How to fix Warning: htmlspecialchars() with this use…
    I've 3 File form element in my form…
    When form is submit and validation don't pass form is populate with date that have been submit. But I think in the same case… ZF is trying to poupulate file element with the array… How can i fix it ???
    File Element should be populate with File name…
    Can you help me….
    Thanks…

  23. 23 Matthias

    I want to make a form with SubForms with FileUpload, other SubForms and Text between the forms.
    But how can I get Text between the forms? Till now I create a Hidden field with a legend (filled with the text), but now there's the problem, that I can't insert links or any HTML.
    Also I want to add a HTML-Table and other Things between the forms and I don't know how to do this. Is there no HTML-Element in the Zend_Form?
    And is it really impossible to add HTML-Code into a legend?

  24. 24 Trinath

    hi ,

    I have implemented this upload example … but I got these errors ..
    Warning: Zend_Loader::include_once(forms/UploadForm.php) [function.Zend-Loader-include-once]: failed to open stream: No such file or directory in /opt/lampp/htdocs/gen/library/Zend/Loader.php on line 83

    Warning: Zend_Loader::include_once() [function.include]: Failed opening 'forms/UploadForm.php' for inclusion (include_path='.:./library:../application/models/:../application/forms/:.:/opt/lampp/lib/php') in /opt/lampp/htdocs/gen/library/Zend/Loader.php on line 83

    Fatal error: Class 'forms_UploadForm' not found in /opt/lampp/htdocs/gen/application/controllers/IndexController.php on line 42

    please help me find where I have gone wrong …

    -
    Trinath

  25. 25 Kudose

    I changed parent::isValid($value, $context);

    to

    if(is_array($value)){
    // its not valid so revert to the original value
    // parent called setValue so let us overwrite it
    $this->setValue($value['name']);
    } else {
    parent::isValid($value, $context);
    }

    No more errors and everything works just fine.

  26. 26 Rob...

    Hi Kudose,

    That's a nice simple solution :)

    Regards,

    Rob….

  27. 27 Lgn

    Hi, i have a problem with the example. No file is uploaded to the tmp folder and i get no error message. Here is the result:
    $uploadedData array(3) {
    ["description"] => string(10) "NetAbalone"
    ["file"] => array(5) {
    ["name"] => string(24) "fred-droits-d'auteur.txt"
    ["type"] => string(10) "text/plain"
    ["tmp_name"] => string(42) "C:\Program Files\Zend\Core\temp\php692.tmp"
    ["error"] => int(0)
    ["size"] => int(2913)
    }
    ["submit"] => string(6) "Upload"
    }

    I was unable to find the problem.

    Greetings, Nicolas

The views expressed in these comments are not the views of the publisher. However, we believe in the rights of others to express their legitimate views and concerns. Any legitimate complaint emailed to rob@akrabat.com will be seriously considered and the post reviewed as desirable and necessary.

Leave a Reply

Pre order