Develop Simply

Ivan K's development musings

Setting Up Bad Ass File Uploading

So you just want to upload files, maybe resize some images, maybe even do some validation - well you’re in luck. This quick tutorial will show you how to do this the easy way with Jam.

First off, lets create a table for our model. We’ll be using timestamped-migrations for that:

minion db:generate --name=create_table_paintings
> 1351501477_create_table_paintings.php Migration File Generated

Now we can modify the migration file to add the nesessary fields:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php defined('SYSPATH') OR die('No direct script access.');

class Create_Table_Paintings extends Migration
{
  public function up()
  {
      $this->create_table('paintings', array(
          'name' => 'string',
          'file' => 'string',
      ));
  }
  
  public function down()
  {
      $this->drop_table('paintings');
  }
}
?>

The id column will be added automatically. You can disable that behavior but its generally useful and in this case saves us some bit of writing. And always having an ID column is a good DB practice anyway.

Now we run the migration to create our paintings table in the database.

minion db:migrate

We got the table now we can create the model corresponding to the table. Put this file in your APPPATH/classes/model/painting.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php defined('SYSPATH') OR die('No direct script access.');

class Model_Painting extends Jam_Model {
  
  public static function initialize(Jam_Meta $meta)
  {
      $meta->fields(array(
          'id' => Jam::field('primary'),
          'name' => Jam::field('string'),
          'file'=> Jam::field('upload', array('server' => 'local')),
      ));

      $this->validator('file', 'name', array('present' => TRUE));
  }
}
?>

Here we add the 3 columns from the table to map them to model fields. Primary field is a special one, used for fining by index later on. As for the ‘file’ field - it’s an “upload” field which requires you to set a server for the upload. You will generally keep your files on your local machine - and to do that we set the server to ‘local’, but you can set this to various other server types, for example rackspace cloudfiles or ftp server - and it will upload the files there and display them with the appropriate public URL - but don’t worry about that for now.

The default upload locations for local are DOCROOT/upload/temp, and DOCROOT/upload/{model}/{id}/ but you can change them in the config. And we also add validation to require the presense of both name and file fields so the model will not be valid if the user does not input any of those two.

So anyway, we now proceed to the controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php defined('SYSPATH') OR die('No direct script access.');

class Controller_Paintings extends Controller
{
  public function action_show()
  {
      $painting = Jam::factory('painting', $this->request->param('id'));
      $this->response->body(View::factory('paintings/show', array('painting' => $painting)));
  }

  public function action_new()
  {
      $painting = Jam::factory('painting');

      if ($this->request->method() === Request::POST)
      {
          $data = $this->request->post();

          if (Upload::not_empty($_FILES['file']))
          {
              $data['file'] = $_FILES['file'];
          }

          if ($painting->set($data)->check())
          {
              $painting->save();
              $this->request->redirect('paintings/show/'.$painting->id());
          }
      }

      $this->response->body(View::factory('paintings/form', array('painting' => $painting)));
  }
}
?>

With the show action we just display the painting, While the secound - “new” action does all the work of uploading, validating and saving the model. The uploading magic happens in “check” method - we upload the file to the temporary folder and serve it from there, should the validation fail - we will still have the file uploaded so the user will not have to upload it a second time, and we can also display a thumbnail. If everything is OK then ->check() returns TRUE, and in the ->save() method the file gets moved to its final destination.

The corresponding views to make this all work are:

APPPATH/views/paintings/form.php

1
2
3
4
5
6
<?php $form = Jam::form($painting) ?>
<?php echo Form::open('paintings/new', array('enctype' => 'multipart/form-data')) ?>
  <?php echo $form->row('input', 'name') ?>
  <?php echo $form->row('file', 'file', array('temp_source' => TRUE)) ?>
  <?php echo Form::submit('submit', "Create Painting") ?>
<?php echo Form::close(); ?>

APPPATH/views/paintings/show.php

1
<img src="<?php echo $painting->file->url() ?>" alt="<?php echo $painting->name() ?>"/>

Well that’s out of the way and we can start palying with this in our browser. But there are still some things we can modify to make this even better. Say you want to limit to uploading to only images, by using the built in uploaded validator, and we’ll add a thumbnail while we’re at it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php defined('SYSPATH') OR die('No direct script access.');

class Model_Painting extends Jam_Model {
  
  public static function initialize(Jam_Meta $meta)
  {
      $meta->fields(array(
          'id' => Jam::field('primary'),
          'name' => Jam::field('string'),
          'file'=> Jam::field('upload', array(
              'server' => 'local'
              'thumbnails' => array(
                  'thumb' => array(
                      'resize' => array(150, 150)
                  )
              )
          )),
      ));

      $this
          ->validator('file', 'name', array('present' => TRUE))
          ->validator('file', array('uploaded' => array('only' => 'images', 'minimum_width' => 200, 'minimum_height' => 200)))
  }
}
?>

Now we can add a thumbnail to form field, so that when the validation fails, we can see the image that’s already uploaded:

1
2
3
4
5
6
7
8
9
<?php $form = Jam::form($painting) ?>
<?php echo Form::open('paintings/new', array('enctype' => 'multipart/form-data')) ?>
  <?php echo $form->row('input', 'name') ?>
  <?php if ( ! $form->object()->file->is_empty()): ?>
      <img src="<?php echo $form->object()->file->url('thumb') ?>" alt="<?php echo $painting->name() ?>"/>
  <?php endif ?>
  <?php echo $form->row('file', 'file', array('temp_source' => TRUE)) ?>
  <?php echo Form::submit('submit', "Create Painting") ?>
<?php echo Form::close(); ?>

The upload Field is very flexible - it can accept different sources for the images, for example you can pass a URL to the field and it will download it. You can use php://input for ajax uploads, or if you upload it manually to the temp dir by any other method (Flash uploader for example) it will work appropriately.

As a last touch we will add 2 columns to the table - file_width and file_height - and set it up so they get automatically populated with the width and height of the image.

minion db:generate --name=add_file_width_and_file_height_to_paintings
minion db:migrate

Jam Fields only know and can operate on themselves, and can’t change other fields. So this must be done through a behavior. Luckily there is just such a bihavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php defined('SYSPATH') OR die('No direct script access.');

class Model_Painting extends Jam_Model {
  
  public static function initialize(Jam_Meta $meta)
  {
      $meta->behaviors(array(
          'file' => Jam::behavior('uploadable', array(
              'save_size' => TRUE,
              'server' => 'local'
              'thumbnails' => array(
                  'thumb' => array(
                      'resize' => array(150, 150)
                  )
              )
          ))
      ));

      $meta->fields(array(
          'id' => Jam::field('primary'),
          'name' => Jam::field('string'),
      ));

      $this
          ->validator('file', 'name', array('present' => TRUE))
          ->validator('file', array('uploaded' => array('only' => 'images', 'minimum_width' => 200, 'minimum_height' => 200)))
  }
}
?>

And we’re done. Everytime a new image is uploaded we will get a proper file_width and file_height.