Below I have implemented a class that will validate and upload files based on the specified rules.
class Uploader
{
protected $finfo;
// extensions corresponding mime types
protected $mimes = [
'jpg' => 'image/jpeg',
'pdf' => 'application/pdf',
'png' => 'image/png',
'gif' => 'image/gif'
];
// default settings
protected $defaults = [
'auto_rename' => true, // rename the uploaded file to a random ID
'new_name' => null, // rename the uploaded file to this name
'ext_whitelist' => null, // mime types corresponding to the types of files allowed to be uploaded
//'max_height' => null, // maximum height (in pixels) that the file can be; null for no limit
'max_size' => null, // maximum size (in megabytes) that the file can be; null for no limit
//'max_width' => null, // maximum width (in pixels) that the file can be; null for no limit
'path' => null, // path to the folder where the upload should be placed
'required' => false // require that file needs to be uploaded
];
// settings for fields to be uploaded
protected $rules = array();
// files that were processed properly
protected $processed = array();
// errors found during processing or uploading
protected $errors = array();
/**
* Initialize uploader.
*/
public function __construct()
{
$this->finfo = new finfo();
}
/**
* Set default settings.
* @param array $params
*/
public function setConfig(array $params)
{
foreach ($this->defaults as $name => $value) {
isset($params[$name]) && $this->defaults[$name] = $params[$name];
}
}
/**
* Set the settings for the fields to be uploaded.
* @param array $params
*/
public function setRules(array $params)
{
foreach ($params as $name => $value) {
// merge the default settings with field's settings
if (is_array($value)) {
$this->rules[$name] = $value + $this->defaults;
} else {
$this->rules[$value] = $this->defaults;
}
}
}
/**
* Process files uploaded based on the settings given.
* @param array $params
* @return true on success or false on failure
*/
public function process()
{
foreach ($this->rules as $name => $rules) {
// check file was given
if (empty($_FILES[$name]) || !$this->isUploadedFile($_FILES[$name])) {
if ($rules['required']) {
$this->errors[$name] = 'No file was provided.';
}
continue;
}
$file = $_FILES[$name];
// check file error
if ($file['error'] !== UPLOAD_ERR_OK) {
$this->errors[$name] = 'An error occurred while trying to upload file.';
continue;
}
$file['ext'] = $this->getExtension($file);
$file['size'] = $this->getSize($file);
// create directory if it does not exist
if (!$rules['path'] || !$this->mkdir($rules['path'])) {
$this->errors[$name] = 'An error occurred while trying to upload file.';
error_log('Unable to write to directory, ' . $rules['path'], 3, 'log.txt');
continue;
}
// check file extension
if ($rules['ext_whitelist'] && strpos($rules['ext_whitelist'], $file['ext']) === false) {
$this->errors[$name] = 'This file is not a valid format.' ;
continue;
}
// check file size
if ($rules['max_size'] && $file['size'] > $rules['max_size']) {
$this->errors[$name] = 'This file is too large to upload.';
continue;
}
// rename filename using given new name
if ($rules['new_name']) {
$file['name'] = $rules['new_name'];
// rename filename using random number
} elseif ($rules['auto_rename']) {
$file['name'] = uniqid() . '.' . $file['ext'];
}
// mark as processed
$this->processed[$name] = $file;
}
return !$this->errors;
}
/**
* Upload processed files.
* @return true on success or false on failure
*/
public function run()
{
foreach ($this->processed as $name => $file) {
$rules = $this->rules[$name];
$destination = $rules['path'] . '/' . $file['name'];
// save file to destination
if (!$this->move($file, $destination)) {
$this->errors[$name] = 'An unexpected error has occurred m.';
error_log('Unable to upload file, ' . $destination, 3, 'log.txt');
}
}
return !$this->errors;
}
/**
* Get processed file.
* @return array
*/
public function getFile($name)
{
return isset($this->processed[$name]) && !isset($this->errors[$name]) ? $this->processed[$name] : false;
}
/**
* Get errors found during processing or uploading.
* @return array
*/
public function getErrors()
{
return $this->errors;
}
/**
* Gets file extension.
* @param array $file
* @return extension of the file if it is found
*/
protected function getExtension(array $file)
{
return array_search($this->finfo->file($file['tmp_name'], FILEINFO_MIME_TYPE), $this->mimes, true);
}
/**
* Gets file size in MB.
* @param array $file
* @return size of the file
*/
protected function getSize(array $file)
{
return round((filesize($file['tmp_name']) / 1048576), 2);
}
/**
* Tells whether the file was uploaded via HTTP POST.
* @param array $file
* @return true on success, false on failure
*/
protected function isUploadedFile(array $file)
{
return file_exists($file['tmp_name']) && is_uploaded_file($file['tmp_name']);
}
/**
* Make directory if it does not already exist.
* @param array $file
* @return true on success, false on failure
*/
protected function mkdir($path)
{
return is_dir($path) && is_writable($path) || mkdir($path, 0755, true);
}
/**
* Delete file if it does exist.
* @param array $file
* @return true on success, false on failure
*/
protected function unlink($path)
{
return !is_file($path) || unlink($path);
}
/**
* Moves an uploaded file to a new location.
* @param array $file
* @return true on success, false on failure
*/
protected function move(array $file, $path)
{
// delete old file if one already exist and upload new file
return !$this->unlink($path) ?: move_uploaded_file($file['tmp_name'], $path);
}
}
This is how it would be used when a form is submitted:
require 'Uploader.php';
if (isset($_POST['submit'])) {
$uploader = new Uploader();
// set global settings
$uploader->setConfig(array(
'path' => dirname(__FILE__) . '/uploads' // where to upload files
));
// set settings for individual fields
$uploader->setRules(array(
'file1' => array(
'auto_rename' => false, // do not rename file
'ext_whitelist' => 'gif|jpg|png', // accept only GIF, JPEG, and PNG
'max_size' => 1 // accept only sizes up to 1 MB
),
'file2' => array(
'required' => true, // require file
'ext_whitelist' => 'pdf', // accept only PDF
'max_size' => 20 // accept only sizes up to 20 MB
)
));
if ($uploader->process() && $uploader->run()) {
if ($file1 = $uploader->getFile('file1')) {
echo $file1['name'] . ' was uploaded<br>';
}
if ($file2 = $uploader->getFile('file2')) {
echo $file2['name'] . ' was uploaded';
}
} else {
var_dump($uploader->getErrors());
}
}
I know there are many existing uploading classes, but I have yet to find one that is exactly to my liking. So I wanted to try and write my own. My goal with this class is to be able to validate files (for now, images and PDFs) and upload files. Though this class is probably not as strong as existing ones, it works as intended.
So, any suggestions on how my class can be improved, especially when it comes to error handling?