Pragmatism in the real world

Simple Zend_Form File Upload Example Revisited

I’ve been thinking about the Simple Zend_Form File Upload Example that I discussed last month.

To recap, if you haven’t read the comments, if the form fails to validate for some reason then you get a nasty error:

Warning: htmlspecialchars() expects parameter 1 to be string, object given in /Users/rob/Sites/akrabat/Zend_Form_FileUpload_Example/lib/Zend/View/Abstract.php on line 786

Essentially, what is happening is that the App_Form_Element_File class that we wrote assigns the $_FILES array to the $value parameter for the form element. On redisplay of the form, the formFile view helper then calls the escape() view helper passing in the $value when rendering the <input> element. The escape() view helper calls htmlspecialchars() which throws the warning about $value not being a string.

*whew!*

What we need is something that’s an array when the data is valid, but can also look like a string to htmlspecialchars(). This got me thinking about the SPL and creating an object for the data from the $_FILES array.

Let’s call this object App_Form_Element_FileValue and store it in lib/App/Form/Element/FileValue.php:

<?php

class App_Form_Element_FileValue extends ArrayObject
{
public function __toString()
{
$result = '';
if(isset($this->name)) {
$result = $this->name;
}
return $result;
}
}

The ArrayObject class is part of the SPL and handily provides a set of functions that enables the object to work with most functions that we like to use with an array including the ability to access the data using array notation. We implement the PHP5 magic function __toString() so that htmlspecialchars() will get a string from the object when it asks for one which nicely knocks that problem on the head.

To integrate it into the code, we need to modify App_Form_Element_File::isValid() from:

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];
}
}
// continues...

to

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 = new App_Form_Element_FileValue($_FILES[$key]);
}
}
// continues...

We also need to modify the validator App_Validate_ValidFile::isValid() function as it’s rather too rigourous in its checking. We currently check that $value is an array using is_array():

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'];
}
// continues...

As $value is now an object of type App_Form_Element_FileValue, we need to change the test in the if statement to:

public function isValid($value)
{
// default value and error is "no file uploaded"
$valueString = '';
$error = UPLOAD_ERR_NO_FILE;

if((is_array($value) || $value instanceof ArrayObject)
&& 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'];
}
// continues...

Note that we test for an instance of ArrayObject as that is where the functionality of array behaviour is implemented and is more generic in case we need to reuse this code with another object that behaves like an array.

Those are the only changes needed to elegantly remove the error message.

Here’s a zip file of this project with the above changes: Zend_Form_FileUpload_Example_Revisited.zip (It includes Zend Framework 1.5.2 which is why it’s 3.9MB big).

Test it out and see if it works for you as well as it works for me !

34 thoughts on “Simple Zend_Form File Upload Example Revisited

  1. Akro, there is problem with your example ;-)

    first of all :
    in: App_Form_Element_FileValue

    i think it should be $this['name'] instead of $this-> name , that doestnt work for me at all.

    One more thing i had to change is to modify 1 line in isValid() in
    Zend_Form_Element.

    When it tries to do $value = $this->getValue();

    the value will be populated with string filename – that will break our validator later which expect an array.

    had to change it to:
    if(!($value instanceof ArrayObject)){
    $value = $this->getValue();
    }

    now that finally made everything function as expected.

    Cheers

    Marcin

    if(!($value instanceof ArrayObject)){
    $value = $this->getValue();
    }

  2. I don't know what Ergo's issue was – this worked perfectly fine for me from the start.

    However, there's a small issue with using this component with subforms caused by PHP's weird behavior.

    When you would normally expect files to be like this:

    $_FILES = array(
     'file1' => array(
      'name' => ..., 
      'tmp_name' => ..., 
      etc.
     ),
     'file2' => array(
      'name' => ..., 
      'tmp_name' => ..., 
      etc.
     )
    )
    

    in the case of a subform the file array looks like this:

    $_FILES = array(
     'form_name' => array(
      'name' => array(
       'file1' => ...,
       'file2' => ...
      ),
      'tmp_name' => array(
       'file1' => ...,
       'file2' => ...
      ),
      etc.
     )
    )
    

    because of this, the isValid method and rest are pretty much useless. I worked around it by manually mucking with $_FILES if the rest of the form was valid, and am currently also trying to figure out how to fix the problem in your class but haven't yet found a good solution.

  3. Jani,

    Thanks for pointing this out. I'll have to play with sub forms.

    For the record, when discussing this in #zftalk, you suggested this solution in App_Form_Element_File::isValid():

    $key = $this->getName();
    $form = $this->getBelongsTo();
    if(null === $value) {
    if(isset($_FILES[$key])) {
    $value = new App_Form_Element_FileValue($_FILES[$key]);
    }
    else if(isset($_FILES[$form]) && isset($_FILES[$form]['name']) &&
    isset($_FILES[$form]['name'][$key])) {
    $value = new App_Form_Element_FileValue(array(
    'name' => $_FILES[$form]['name'][$key],
    'tmp_name' => $_FILES[$form]['tmp_name'][$key],
    'type' => $_FILES[$form]['type'][$key],
    'error' => $_FILES[$form]['error'][$key],
    'size' => $_FILES[$form]['size'][$key]
    ));
    }
    }

    Rob…

  4. Hi and thanks again for your great work… The example you provide today works fine but the problem for me is to define more than one file upload field and to simply identify them (ie. first for jpeg file, second for pdf, third for csv…all file type having different path desitnation…)
    As my poor english help me in understanding, it seems there is a way of doing that with subforms but i can't figure out how it works… does anyone can light me up ?

  5. Hi,
    big thx for the tutorial. Please check this question:

    I try to use some parts of your tut in an existing app but i get: "Plugin by name ValidFile was not found in the registry." in File.php.
    If I comment the line $this->setValidators($validators) in line 63 it works of course and $validators seems to be good.
    ValidFile is not extended by Zend_Controller_Plugin_Abstract. So I can't use registerPlugin("ValidFile.php")
    How do i bring it to work?
    Thx for any hints :)

  6. unless you make the file required it won't be validated, even if it was submitted.

    i think isValid() in ..Element_File needs some tweaks, not sure what kind yet though

  7. this doesn't work as expected either, try uploading a file that exceeds the ini set limit, it should throw the error "The uploaded file exceeds the upload_max_filesize directive in php.ini" but doesn't, why?

    Though i am having a tough time trying to understand what this code does

    // 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);
    }
    You also set an NotEmpty validator, why?

    "By default, if an element is required, but does not contain a 'NotEmpty' validator, isValid() will add one to the top of the stack, with the breakChainOnFailure flag set. This makes the required flag have semantic meaning: if no value is passed, we immediately invalidate the submission and notify the user, and prevent other validators from running on what we already know is invalid data." Framework site, http://framework.zend.com/manual/en/zend.form.elements.html#zend.form.elements.filters

  8. ok,

    First, as far as i can see you don't need to add a 'NotEmpty' validator when you add a ->isRequired(true) validator because the 'NotEmpty' flag will be set automatically.

    Now, to allow the creating file uploads which are NOT required you need to make a slight tweak, maybe you guys can tell me if i got it right:

    we need to modify App_Form_Element_File::isValid() from:

    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 = new App_Form_Element_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);
    }

    to…

    public function isValid($value, $context = null)
    {
    $key = $this->getName();
    if (null === $value) {
    if(isset($_FILES[$key])) {
    $value = new App_Form_Element_FileValue($_FILES[$key]);
    }
    }

    if ((($value['error'] == UPLOAD_ERR_NO_FILE) || (null === $value)) && !$this->isRequired() && $this->getAllowEmpty()) {
    return true;
    }

    // auto insert ValidFile validator
    if ($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);
    }

  9. hai rob, thanks for your tutorial.

    Sory for my bad english.
    I want to ask you something about your tutorial, i want to upload image to server with your aplication, how to check that file is image ? thx.

  10. Hi! Rob,Sory for my bad english too. nice job with the tutorials, i am learning a lot, but i have a question, i had read that in the Zf 1.5 RC1 we don't need to use the incubator view helper, that`s right? because i`m trying to change the code but this message appeared:

    "Fatal error: Class 'forms_ContactForm' not found in homeuseraplicationscontrollersIndexController.php on line 16

    i had removed the path and the request of the view.. please tell me if i am wrong..

  11. I've some troubles with the App_Form_Element_FileValue::toString();

    I think that you should do that :

    class BandBCS_Form_Element_FileValue extends ArrayObject
    {
        public function __toString()
        {
            $result = '';
            if(isset($this['name'])) {
                $result = $this['name'];
            }
            return $result;
        }
    }
    

    Do i'm wrong ?

  12. Hi Apsy,

    I'm assuming that Matthew will sort out a file upload form component when Zend_Upload is sorted out.

    Regards,

    Rob…

  13. Does someone have good results with subforms ?
    Here,
    $form = $this->getBelongsTo(); return null value

  14. Yann => You have to do a setBelongsTo.

    Rob => In order to allow multiple Validators and a "non value" for the FileUpload, I think you should do
    [code]
    if(null === $value) {
    if(isset($_FILES[$key]) && !empty($_FILES[$key]['name'])) {
    $value = new App_Form_Element_FileValue($_FILES[$key]);
    }
    else if(isset($_FILES[$form]) && !empty($_FILES[$form]['name']) &&
    isset($_FILES[$form]['name'][$key])) {
    $value = new App_Form_Element_FileValue(array(
    'name' => $_FILES[$form]['name'][$key],
    'tmp_name' => $_FILES[$form]['tmp_name'][$key],
    'type' => $_FILES[$form]['type'][$key],
    'error' => $_FILES[$form]['error'][$key],
    'size' => $_FILES[$form]['size'][$key]
    ));
    }
    }
    [/code]

    instead of
    [code]
    if(null === $value) {
    if(isset($_FILES[$key])) {
    $value = new App_Form_Element_FileValue($_FILES[$key]);
    }
    else if(isset($_FILES[$form]) && isset($_FILES[$form]['name']) &&
    isset($_FILES[$form]['name'][$key])) {
    $value = new App_Form_Element_FileValue(array(
    'name' => $_FILES[$form]['name'][$key],
    'tmp_name' => $_FILES[$form]['tmp_name'][$key],
    'type' => $_FILES[$form]['type'][$key],
    'error' => $_FILES[$form]['error'][$key],
    'size' => $_FILES[$form]['size'][$key]
    ));
    }
    }
    [/code]

    And example of Validate filename extensions :
    [code] "this file is not allowed"
    );

    /**
    * @var array
    */
    protected $_extenstions_allowed = array();

    protected $_pattern;

    public function __construct(array $extenstions_allowed)
    {
    $this->_extenstions_allowed = $extenstions_allowed;
    $extensions = implode('|', $this->_extenstions_allowed);
    $this->_pattern = '`.(' . $extensions . ')$`i';
    }

    public function isValid($value)
    {
    $valueString = (string) $value;

    $this->_setValue($valueString);

    $validate = new Zend_Validate_Regex($this->_pattern);

    if (!$validate->isValid($valueString)) {
    $this->_error();
    return false;
    }
    return true;
    }
    }
    [/code]

    Use case :
    [code]
    $formElement = new App_Form_Element_File();
    $formElement->addValidator(new App_Validate_FilenameExtension(array('bmp', 'jpg', 'png'));
    [/code]

  15. Damn… That sucks…
    The code for the App_Validate_FilenameExtensions (sorry for the spam) :
    "this file is not allowed"
    );

    /**
    * @var array
    */
    protected $_extenstions_allowed = array();

    protected $_pattern;

    public function __construct(array $extenstions_allowed)
    {
    $this->_extenstions_allowed = $extenstions_allowed;
    $extensions = implode('|', $this->_extenstions_allowed);
    $this->_pattern = '`.(' . $extensions . ')$`i';
    }

    public function isValid($value)
    {
    $valueString = (string) $value;

    $this->_setValue($valueString);

    $validate = new Zend_Validate_Regex($this->_pattern);

    if (!$validate->isValid($valueString)) {
    $this->_error();
    return false;
    }
    return true;
    }
    }
    ?>

    (Rob, you should grow up your comment input :p)

  16. Annoying, for PHP 5.1.*, __toString() isn't as magic as we'd like to be. htmlspecialchars() doesn't bother checking for the function, so it dies in the same fashion.

    Still trying to come up with something that doesn't involve hacking the Zend Library with a version check and a manual __toString() call. :(

  17. hello Rob,

    Thanks for you great post. I have some problem in validator.

    'Plugin by name ValidFile was not found in the registry.'

    Can you pass me hint how to resolve this.

    Thanks

  18. Hi,

    Its very good link. We are developing J2ME Canvas library and components with web interface. This component help us a lot.

    Error we faced :
    Warning: htmlspecialchars() expects parameter 1 to be string, object given in /Users/rob/Sites/akrabat/Zend_Form_FileUpload_Example/lib/Zend/View/Abstract.php on line 786

    Solution which eliminate warning and also do the validation perfectly,

    replace the line :
    return parent::isValid($value, $context);

    with following code :
    if(is_array($value)){
    // its not valid so revert to the original value
    // parent called setValue so let us overwrite it

    $result = parent::isValid($value, $context);
    $this->setValue($value['name']);
    } else {
    $result = parent::isValid($value, $context);
    }
    return $result;

  19. In regard to the fix posted by Mobbingo, when form validation succeeds, it does not return the array with the pertinent information for the file input element. I.e., $form->getValues() will return the filename submitted, but not the size, temp filename, etc.

  20. Hey Rob,

    Great resource. Thanks man! You saved precious time for me ;) Have a beer from me! :)

    Regards,
    Aki.

  21. Hi guys, I got this example working, and the debug shows the fields correct.

    But how can I echo out the contents of the uploaded file or get a handle on it? The tmp_name file doesn't exist when I view it in explorer…

    Maybe I just don't understand exactly what's happening here :)

    Thanks for the article. It's great!

  22. I have Zend framework in version 1.5 and have a problemFatal error:

    Fatal error: Uncaught exception 'Zend_Loader_PluginLoader_Exception' with message 'Plugin by name ValidFile was not found in the registry.' in c:projectlibraryZendLoaderPluginLoader.php:335 Stack trace: #0 c:projectlibraryZendFormElement.php(984): Zend_Loader_PluginLoader->load('ValidFile') #1 c:projectlibraryZendFormElement.php(1048): Zend_Form_Element->addValidator('ValidFile', true, Array) #2 c:projectlibraryZendFormElement.php(1082): Zend_Form_Element->addValidators(Array) #3 c:projectlibraryAppFormElementFile.php(61): Zend_Form_Element->setValidators(Array) #4 c:projectlibraryZendForm.php(1870): App_Form_Element_File->isValid(NULL, Array) #5 c:projectapplicationAppcontrollersArtController.php(62): Zend_Form->isValid(Array) #6 c:projectlibraryZendControllerAction.php(502): App_CmsarticleController->addAction() #7 c:projectlib in c:projectlibraryZendLoaderPluginLoader.php on line 335

    Help me, please

  23. Hi. I have the same problem as Zeton. Should I do something to the $this->getPluginLoader to add a path to the /App/Validate/App_Vialidate_ValidFile.php ?
    Or is this a new error because we and Zeton use ZF 1.5?

  24. Adding the

    $this->addElementPrefixPath('App', 'App/');

    Within the form's constructor should allow it to find the plugin. You must make sure you get spelling and case of the class name and file correct so that the loader can find it.

    Regards,

    Rob…

  25. i am using zend_form for inserting form element value i am using
    $formData = $this->_request->getPost();
    if ($register->isValid($formData))
    { $users->addAlbum($formData); }

    in form i am also using zend captcha
    when run this project error will be sended

    Message: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'captcha' in 'field list'
    while i am using in form 'ignore'=>true
    please help me …
    thaks

Comments are closed.