How to add image uploading to your forms with Symfony 1.2 and Doctrine, from beginning to end

I’m still very new to Symfony 1.2 and its form framework. Reading through documentation, discussion forums and blog posts, I couldn’t find a tutorial which had everything I needed in an image upload. Having finished an image upload for the first time, I decided to write such a tutorial.

This tutorial requires Symfony 1.2, the Doctrine ORM plugin and the PHP GD library. You should already have all of these elements installed, and be familiar with them.

We’ll start at the beginning. I’m going to make an uploader for a background image to attach to a Website model. Make sure you have a field in your model to store the image file name:

Website:
  columns:
    background_image:  string(255)

And rebuild your model:

symfony doctrine:build-all-reload

Now open the form class generated from your schema.

You’re going to add a widget and a validator for the image upload. The widget will also allow the user to view the image after it has been uploaded, and delete it if necessary. The validator will make sure the image upload is not required (it is required by default). There is also a second validator, sfValidatorPass, because the widget we are using adds a checkbox widget giving the user the option to delete the file.

You’ll also override the doSave() method and updateObject() methods. The doSave() method will handle the upload (if there is one), create a new file name for the image, and save the image both to the file system and the background_image field automatically. Since the background_image field is automatically set with the full path of the file, you will use updateObject() to remove the path and just store the file name. If you move your project to another system, the file path will change and so we want to store just the file name.

Also in doSave(), you will handle the case where the user has checked off the “delete image” checkbox, which is automatically named background_image_delete.

Here is the code so far under lib/form/doctrine/WebsiteForm.class.php:

class WebsiteForm extends BaseWebsiteForm
{
  public function configure()
  {
    $this->widgetSchema['background_image'] = new sfWidgetFormInputFileEditable(array(
      'file_src' => '/'.basename(sfConfig::get('sf_upload_dir')).'/'.$this->getObject()->getBackgroundImage(),
      'is_image' => true,
      'edit_mode' => strlen($this->getObject()->getBackgroundImage()) > 0,
      'template'  => '
<div>%file%
%input%
%delete% %delete_label%</div>
'
    ));

    $this->validatorSchema['background_image'] = new sfValidatorFile(array(
      'required' => false,
      'mime_types' => 'web_images'
    ));
    $this->validatorSchema['background_image_delete'] = new sfValidatorPass();
  }

  protected function doSave ( $con = null )
  {
    $upload = $this->getValue('background_image');

    if ( $upload )
    {
      $filename = sha1($upload->getOriginalName().microtime().rand()).$upload->getExtension($upload->getOriginalExtension());
      $filepath = sfConfig::get('sf_upload_dir').'/'.$filename;

      $oldfilepath = sfConfig::get('sf_upload_dir').'/'.$this->getObject()->getBackgroundImage();
      if ( file_exists($oldfilepath) )
      {
        unlink($oldfilepath);
      }

      $upload->save($filepath);
    }

    $delete = $this->getValue('background_image_delete');
    if ( $delete )
    {
      $filename = $this->getObject()->getBackgroundImage();
      $filepath = sfConfig::get('sf_upload_dir').'/'.$filename;
      @unlink($filepath);
      $this->getObject()->setBackgroundImage(null);
    }

    return parent::doSave($con);
  }

  public function updateObject($values = null)
  {
    $object = parent::updateObject($values);
    $object->setBackgroundImage(str_replace(sfConfig::get('sf_upload_dir').'/', '', $object->getBackgroundImage()));
    return $object;
  }

}

This should work well for the most part. But what if the image is 2000 pixels in width? It will be very disruptive to display such a large image.

That’s where sfThumbnailPlugin comes in.

Install the plugin if you haven’t already done so:

$ symfony plugin:install sfThumbnailPlugin
$ php symfony cc

Now create a new directory to store your thumbnails:

$ mkdir /web/uploads/thumbnails

You’ll probably want to use this directory in other modules or applications in your project. Add a new setting called sf_thumbnail_dir in your project configuration:

config/ProjectConfiguration.class.php

...
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    ...
    sfConfig::set('sf_thumbnail_dir', sfConfig::get('sf_upload_dir').'/thumbnails');
    ...
  }
}

Now all that’s left to do is create and store the thumbnail from the doSave() method of your form class, and make sure that your form displays the thumbnail and not the source image in your configure() method.

Modify the widget settings so that it displays the thumbnail. Note that I’ve …’d most of the parts that don’t change:

lib/form/doctrine/WebsiteForm.class.php

class WebsiteForm extends BaseWebsiteForm
{
  public function configure()
  {
    ...
    $this->widgetSchema['background_image'] = new sfWidgetFormInputFileEditable(array(
      'file_src' => '/'.basename(sfConfig::get('sf_upload_dir')).'/'.basename(sfConfig::get('sf_thumbnail_dir')).'/'.$this->getObject()->getBackgroundImage(),
      'is_image' => true,
      'edit_mode' => strlen($this->getObject()->getBackgroundImage()) > 0,
      'template'  => '
<div>%file%
%input%
%delete% %delete_label%</div>
'
    ));
    ...
  }

  protected function doSave ( $con = null )
  {
    $upload = $this->getValue('background_image');

    if ( $upload )
    {
      ...
      $thumbnailpath = sfConfig::get('sf_thumbnail_dir').'/'.$filename;
      ...
      $oldthumbnailpath = sfConfig::get('sf_thumbnail_dir').'/'.$this->getObject()->getBackgroundImage();
      if ( file_exists($oldthumbnailpath) )
      {
        unlink($oldthumbnailpath);
      }

      ...
      $thumbnail = new sfThumbnail(150, 150, true, true, 75, 'sfGDAdapter');
      $thumbnail->loadFile($filepath);
      $thumbnail->save($thumbnailpath);
    }

    $delete = $this->getValue('background_image_delete');

    if ( $delete )
    {
      ...
      $thumbnailpath = sfConfig::get('sf_thumbnail_dir').'/'.$filename;
      @unlink($thumbnailpath);
      ...
    }
  }

  ...

}

And that’s all there is to it. Note that there is no template file here because the form framework handles the view of the form inputs.

Questions? Complaints? Leave a comment.

Here are some related resources:

Here are some other posts you might be interested in:

14 Comments

  1. santini
    Posted August 10, 2009 at 10:55 am | Permalink

    Thanks for the tutorial, it saved my time.

    To avoid a conflicts in file names I would change a function that generates file names to something similar to:

    sha1($upload->getOriginalName() . microtime() . rand())

    • jjmontgo
      Posted August 10, 2009 at 10:30 pm | Permalink

      Thanks santini. This is definitely a good idea and something I hadn’t considered. Which is strange because I get people uploading images with the same file name _all_ the _time_!

      • santini
        Posted August 11, 2009 at 3:07 pm | Permalink

        They will sooner or later figure out that something is wrong.

        Probably sooner than you think ;-) .

        Try to check in the database identical sha1 hashes and you’ll see the scale of the problem. Maybe you got luck so far…

        Wish you luck :-)

  2. M
    Posted August 10, 2009 at 11:01 am | Permalink

    hello, i have a question:

    what if the form is embedded in another one?

    form A embeds an arbitrary number of forms containing images, but in this way the object is saved by the A form method saveembeddedforms,with the call of ->getObject()->save, so bypassing the form doSave Method.

    how could you manage this?
    editing saveembeddedforms?

    • jjmontgo
      Posted August 10, 2009 at 10:53 pm | Permalink

      Not sure how to answer this one. I posted this tutorial right after figuring out how to do the full image upload, so I’m just getting started myself. I also haven’t had any need to embed or merge forms that have file upload.

      Can anyone else answer this question?

  3. annis
    Posted August 23, 2009 at 10:34 pm | Permalink

    Hey there,
    I’ve been working with Symfony since version 1.1 and immediately jumped ship to 1.2 when it came out. It’s now much easier to handle forms that upload files. You don’t actually have to code all that you’ve done so far. Do have a look at sfFormDoctrine.class.php in your Symfony installation. Generating your filename and storing the file; as well as deleting when the user activates the checkbox!

    Also, try to store your default values in app.yml where it’s easy to modify, you can use PHP code in your yaml files (have a look at the Jobeet fixture files for how to handle this).

    And thirdly: for thumbnail management, use a method in your form that’s automatically called when it updates the respective field: updateBackgroundColumn(), don’t use updateObject() or the like, please.

    Cheers, Annis

    • Posted September 1, 2009 at 4:16 pm | Permalink

      Hey Annis, I’m starting with symfony 1.2 , I’ve been working with symfony since version 0.9 I think. I find new symfony forms very confusing when you want to customize it., right now I want to use the thumbnail plugin to resize some images, it was very easy back in symfony 1.0. You say we only need to create a an update method for the respective field, I wonder if you have some example code it would come in really handy for me.

      Thanks in advance

      Carlos Mafla

    • Posted September 2, 2009 at 7:28 pm | Permalink

      I found a nice solution here for image resizing using a custom validator and sfThumbnailPlugin http://forum.symfony-project.org/index.php/m/84735/

      Hope it helps someone else

      Cheers

  4. lukis
    Posted September 24, 2009 at 12:21 pm | Permalink

    I have problem i am struggling with for some time now:
    Is there a way to save file with filename same as primary key of inserted record?
    For example such method works only for updates couse primary id already exists:
    public function generateImageFilename(sfValidatedFile $file)
    {
    return $this->getId().$file->getExtension($file->getOriginalExtension());
    }
    Do u have any solution for situation when new record is being saved?

    • jjmontgo
      Posted September 24, 2009 at 3:09 pm | Permalink

      I haven’t tried this out myself, but here are my first thoughts.

      In order to use the primary key in your file name, you have to save the object before executing the file upload so that the primary key is generated.

      In my post above, for the function doSave(), take the parent call “parent::doSave($con)” and put it at the top of the function:


      $returnVal = parent::doSave($con);

      Then at the bottom of the function:


      return $returnVal;

      This should work in both cases whether you are updating or saving a new data object.

      • lukis
        Posted September 25, 2009 at 11:59 am | Permalink

        Your solution indeed works but creates new issues:

        First thing is how to prevent from saving file during $returnVal = parent::doSave($con);?
        Becouse i save file after it doing [...] $upload->save($filepath); [...] and after that i have 2 files created in directory.

        Second matter is that i still need to save filename in database couse i need to know extension of file even when i know that name is same as primary id. Do do that i need to make something like update on newly created record…

        thats how i see it

  5. Posted September 29, 2009 at 8:54 am | Permalink

    Thank you for this tutorial!

  6. simis
    Posted November 18, 2009 at 10:14 am | Permalink

    i got this error , any idea ?
    Fatal error: Call to a member function beginTransaction() on a non-object in F:\lavoro\WEB\hotel\lib\vendor\symfony\lib\plugins\sfDoctrinePlugin\lib\form\sfFormDoctrine.class.php on line 177

    • simis
      Posted November 18, 2009 at 10:35 am | Permalink

      solved , that was my mistake, this script is very good tnx.
      The problem now is the file name, it’s the coded one…


Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*