Basic picture file uploads are handled on the client-side by a form and on the server-side by a script moving a cached temporary file to a permanent location while recording the appropriate path information to database. In this post, we will elaborate on each of these portions.
Contents
The HTML Form
For forms to upload files correctly, 2 requirements must be met: a named file input must be included in the form and the encoding type must be appropriately. We show below an example proper form:
<form action='' method='post' enctype='multipart/form-data'>
<div class='form-group'>
<label>Picture:
<input type='file' name='newPicture' class='form-control' />
</label>
</div>
<div class='form-group'>
<label>Description:
<textarea name='description' class='form-control'></textarea>
</label>
</div>
<input type='submit' name='action' value='Add picture' class='btn btn-primary' />
<a href='/product/index' class='btn btn-secondary'>Cancel</a>
</form>
First, we see on line 1 that the enctype
attribute for the form is set to multipart/form-data
. This encodes the submitted form data with different parts for different data, much like an email with attachments included. If the form enctype
attribute to this value, the file is simply not received by the server application.
Second, we see on line 4 an input with the type
attribute set to the value file
. This input also has the attribute name
set to newPicture
so that we may refer to it by this name in the server script.
The submit URL for this form will be the same as its calling URL.
The PHP Script
Now, a server-side script must receive the form data, check the data and save the file as appropriate:
public function addPicture($product_id){
if(isset($_FILES['newPicture'])
&& $_FILES['newPicture']['error'] == UPLOAD_ERR_OK){
$info = getimagesize($_FILES['newPicture']['tmp_name']);
$allowedTypes = [IMAGETYPE_JPEG=>'.jpg',
IMAGETYPE_PNG=>'.png',
IMAGETYPE_GIF=>'.gif'];//accept jpg, png, gif
if($info === false){ // no go
$this->view('product/addPicture', ['error'=>'Bad file format']);
}else if(!array_key_exists($info[2], $allowedTypes)){ // no go
$this->view('product/addPicture',
['error'=>'Not an accepted file type']);
}else{
//save the picture in the images folder
$path = getcwd().DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR;
$filename = uniqid().$allowedTypes[$info[2]];
move_uploaded_file($_FILES['newPicture']['tmp_name'], $path.$filename);
$newPicture = $this->model('Picture');
$newPicture->product_id = $product_id;
$newPicture->filename = $filename;
$newPicture->description = $_POST['description'];
$newPicture->create();
header('location:/product/index');
}
}else{
$this->view('product/addPicture');
}
}
This method is meant to accept files associated to products in a product catalog. Therefore, on line 1, we need to pass in the product_id foreign key value for the picture record to refer to the appropriate product record.
On lines 2 and 3, we verify that the picture is indeed sent (line 2) to the receiving script without any error (line 3). If not, the script branches to line 27 and presents the submission form.
On lines 4 and 8, we extract image file information (line 4) that will allow us to validate (on line 8) that the received file is indeed an image. If not, an error message is sent to the user on line 9.
At line 10, the image information (from line 4) is used again against an image type whitelist (produced on lines 5-7). If the file is not of the proper type, an error message is sent to the user, on lines 11-12.
Finally, if all checks are good, the script continues on to lines 15-24. Lines 15, 16, 17 handle saving the file to the filesystem as follows:
- Define the saving path in the OS, based on the current working directory. The file will be saved in the images folder under the server document root.
- Define the file name to a random string with the extension from the whitelist defined on lines 5-7.
- Move (save) the file from its temporary location to the images folder.
Lines 19-24 record the data as follows:
- Get an instance of the Picture class.
- Populate the product_id foreign key from the method parameter.
- Populate the filename, since other computers don’t have access to the full path structure. We will use this filename later to display these pictures.
- Populate the description from the form data.
- Invoke Picture::create() to save the Picture record to the database.
- Redirect the client side to the Product index.
Viewing the Pictures
In such an application, we simply call up all the pictures in a product detail user story with a controller method as follows:
public function detail($product_id){
$theProduct = $this->model('Product')->find($product_id);
$thePictures = $this->model('Picture')->getForProduct($product_id);
$theProduct->pictures = $thePictures;
$this->view('product/detail', $theProduct);
}
By setting the pictures to be part of the product, we are able to pass all the data as a single object to then output the picture URLs as in the following view code:
<?php
foreach($data->pictures as $picture){
echo "<img src='/images/$picture->filename' style='max-width:100px;' />";
echo "<a href='/product/deletePicture/$picture->picture_id' class='btn btn-danger'>Delete picture</a>";
}
?>
On line 3, we output an image element that will refer to the faved file in the images folder under the Web server document root. On line 4, we also include a delete link.
Deleting the Picture
Mistakes happen and things change, so it makes sense to be able to delete pictures from our Web application. The hyperlink form the above view calls the deletePicture method from our product controller, defined as follows:
public function deletePicture($picture_id){
$thePicture = $this->model('Picture')->find($picture_id);
unlink(getcwd().DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR.$thePicture->filename);
$thePicture->delete();
header('location:/product/detail/'.$thePicture->product_id);
}
The process is simply to find the Picture record using its primary key value, delete the associated file with the unlink instruction, and delete the picture record before redirecting back the appropriate page (here we go back to the product details.
The Picture Model and Database
To support all these operations, we need a Picture model similar to the following:
class Picture extends Model{
var $product_id;
var $filename;
var $description;
public function getForProduct($product_id){
$SQL = 'SELECT * FROM Picture where product_id = :product_id';
$stmt = self::$_connection->prepare($SQL);
$stmt->execute(['product_id'=>$product_id]);
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Picture');
return $stmt->fetchAll();
}
public function create(){
$SQL = 'INSERT INTO Picture(product_id,filename,description) VALUE(:product_id,:filename,:description)';
$stmt = self::$_connection->prepare($SQL);
$stmt->execute(['product_id'=>$this->product_id,'filename'=>$this->filename,'description'=>$this->description]);
return $stmt->rowCount();
}
public function find($picture_id){
$SQL = 'SELECT * FROM Picture WHERE picture_id = :picture_id';
$stmt = self::$_connection->prepare($SQL);
$stmt->execute(['picture_id'=>$picture_id]);
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Picture');
return $stmt->fetch();
}
public function update(){
$SQL = 'UPDATE Picture SET filename = :filename,description = :description WHERE picture_id = :picture_id';
$stmt = self::$_connection->prepare($SQL);
$stmt->execute(['filename'=>$this->filename,'description'=>$this->description,'picture_id'=>$this->picture_id]);
return $stmt->rowCount();
}
public function delete(){
$SQL = 'DELETE FROM Picture WHERE picture_id = :picture_id';
$stmt = self::$_connection->prepare($SQL);
$stmt->execute(['picture_id'=>$this->picture_id]);
return $stmt->rowCount();
}
}
This model performs operations on a Picture table with the following fields:
- picture_id: the automatically incremented integer primary key
- product_id: the foreign key to the product table
- filename: a string type to hold the short filename
- description: a text type field to hold long descriptions.
Conclusion
These general guidelines are also applicable to records that are child records of a master-detail relationship. You could change the upload types by modifying the upload whitelist and replacing getimagesize
with other validation mechanisms.