File Uploads with PHP Doctrine

It’s time to take a look at how file uploads can be integrated into the Doctrine validation and CRUD process. We will have a product in the form of a digital download as an example, it will have a screenshot image that can be maximum 250 pixels wide and high. The download itself will be a zipped file.

public function setTableDefinition(){
...
  $this->hasColumn('image_path', 	'string', 	100);
  $this->hasColumn('file_path', 	'string', 	100);
...
}
	
public $files = array(
  'image_path' 	=> array(
    'update' 	=> false, 
    'insert' 	=> true, 
    'folder' 	=> 'screenshots', 
    'upload' 	=> array('uploadImage', 250, 250),
    'new_name' 	=> array('body' => array('func' => 'uniqid', 'params' => array()), 'extension' => 'jpg')),
  'file_path' 	=> array(
    'update' 	=> false, 
    'insert' 	=> true,
    'folder'	=> 'zipfiles',
    'upload'	=> array('upload')));

The above $files configuration array will be located in the model, in this case Product. The image handling for instance is located in the sub-array with the image_path key.

On update we will not check for empty, if the field is empty we do nothing, the old file will stay the same and the path to it that we have in the db will also stay of course, if it has a value we will replace the current file.

On insert we will check for a value and issue a validation error if the field is empty.

The folder is of course referring to the folder we will upload to.

Next we set the name of the upload function to use, in this case it is uploadImage, and it will take two extra arguments, 250 and 250 to constrain the image width and height.

Finally we set the configuration data needed to determine the name of the uploaded file, if omitted we will use the name of the original. If not we need an array specifying first how to create the file name body. The function to use, in this case uniqid() and the parameters to send it. Repeat for extension.

public function validateOnUpdate(){ return $this->validateFiles('update'); }
public function validateOnInsert(){ return $this->validateFiles('insert'); }

These two are empty hooks (also located in the Product model) that Doctrine allows us to use in order to jack into the validation process. We use them to run through the validateFiles method in Mdl:

function validateFiles($ui){
  $errorStack = $this->getErrorStack();
  foreach($this->files as $file => $info){
    if($info[$ui]){
      if(empty($_FILES[$file]['name']))
        $errorStack->add($file, $file);
    }
  }
  return $errorStack;
}

We begin by fetching the current errorStack, next we loop through the $files array described above and check for true or false for the given field and update/insert key.

In our case the first time we enter the loop and $ui is ‘insert’ we get first true and then we check if $_FILES[‘image_path’][‘name’] is empty, if it is empty we simply add the field name to the errorStack. In our Smarty config file the error in the [error] section is then having the same key as the field name.

In this way when we try to save with the model’s save() method we will throw an error if any of the fields – having for instance ‘insert’ => true in the config array – are empty in the $_FILES array. All of the below is in the Ctrl class.

function getBodyExt($key, $info){
  $inf = $info['new_name'][$key]; 
  if(is_array($inf))
    return call_user_func_array($inf['func'], $inf['params']);
  return $inf;
}

function getFilePath($key, $folder, $file_name = ''){
  if(empty($_FILES[$key]['name']))
    return '';
  $file_name = empty($file_name) ? $_FILES[$key]['name'] : $file_name;
  return $folder."/".$file_name;
}

function afterSave(&$obj){
  if(!empty($obj->files)){
    foreach($obj->files as $file => $info){
      if(!empty($info['new_name'])){
        $body 		= $this->getBodyExt('body', $info);
        $ext 		= $this->getBodyExt('extension', $info);
        $new_name 	= empty($ext) ? $body : $body.'.'.$ext;
      }else
        $new_name 	= '';
        $path 			= $this->getFilePath($file, $info['folder'], $new_name);
        $upload 		= Arr::fluent($info['upload']);
        $func 			= $upload->shift();
        $params 		= $upload->unshift($path, $file)->c;
        if(call_user_func_array(array($this, $func), $params)){
          if(is_file($obj->$file))
            unlink($obj->$file);
          $obj->$file = $path;
        }
      }
      $obj->save();
    }
}

To make a long story short, here we use our configuration data to create proper file paths, names, extension names and calling the designated upload methods with the correct arguments.

Note the use of the fluent interface for arrays, a little bit pointless usage here since we don’t chain at all. At first I thought I would need to chain but in the end I didn’t and I was too lazy to revert to array_shift and array_unshift

All of the above is happening after the other product data has been saved to the database.

Note the use of is_file to check for a pre-existing file, if we have one we delete it since it will be replaced with the currently uploading file.

Finally we save the new information (file paths) to the database, in the Ctrl::save() method they were set to NULL since the $_POST array doesn’t contain them, that’s why we need this second save to update the information with the file paths.

function upload($file_path, $key){
  $move_result 		= move_uploaded_file($_FILES[$key]['tmp_name'], $file_path);
  if($move_result)	chmod($file_path, 0777);
  else				return false;
  return true;
}

function uploadImage($file_path, $key, $max_width, $max_height){
  $move_result = move_uploaded_file($_FILES[$key]['tmp_name'], $file_path);
  if($move_result){
    $result = Common::reScaleImage($file_path, $max_width, $max_height);
    if(!$result){
      unlink($file_path);
      return false;
    }else
      chmod($file_path, 0777);
  }else
    return false;
  return true;
}

The above two functions are slimmed down versions of what has already been explained in an earlier tutorial. They are responsible for actually saving our files in the correct locations.

And there it is, a somewhat complicated way of handling file uploads since we try to account for as many different scenarios as possible for both update and create.


Source download.

Update: I just changed the delete method in Ctrl, it could look something like this:

function delete(){
  $obj = $this->find();
  if(!empty($obj->files)){
    foreach($obj->files as $file => $info){
      if(is_file($obj->$file))
        unlink($obj->$file);
    }
  }
  $obj->delete();
  return $this->onDelete();
}

Haven’t tested yet though…


Related Posts

Tags: , , ,