Transforming a script into a class enhances code modularity and reusability
We covered file uploads in the last few articles, and purposefully strayed away from object oriented programming. Why? Because most of the time that you find a solution online to a problem like this, it won’t be laid out nicely for you. It’s going to be just a blob of code and not going to fit your architecture perfectly. I remember when I was a junior developer, that used to give me some anxiety. “Couldn’t they just have created this as a class instead of this script,” I found myself asking frequently. It’s really not that far of a stretch to do this yourself, and you will do it frequently.
If you’re interested in seeing how we built up the entire upload script, read the articles below.
https://blog.devgenius.io/php-p77-basics-of-file-uploading-ba377da3d072
https://blog.devgenius.io/php-p78-checking-file-types-ffb2762d2d19
https://blog.devgenius.io/php-p79-file-size-checks-e419bc6483bf
https://dinocajic.medium.com/php-p80-final-upload-checks-8b24427fd928
Recap
We have a basic HTML form and a simple process upload script.
<form action="./upload.php" method="post" enctype="multipart/form-data">
<div>
Select Image to upload:
</div>
<div>
<input type="file" name="file_name" id="file_name">
</div>
<div>
<input type="submit" name="submit" value="Upload">
</div>
</form>
<?php
$file_name = $_FILES["file_name"]["name"];
$target_file = "uploads/" . $file_name;
$temp_file = $_FILES["file_name"]["tmp_name"];
if ( file_exists($target_file) )
{
die("The file already exists");
}
$allowed = ['jpg', 'png'];
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
if ( ! in_array($extension, $allowed) )
{
die("The format is not correct. You may only upload: " . implode(", ", $allowed));
}
if ( getimagesize($temp_file) === false )
{
die("You must upload an image");
}
$max_size_mb = 0.5;
$max_size_kb = $max_size_mb * 1024 * 1024;
if ( $_FILES["file_name"]["size"] > $max_size_kb )
{
die("The file size is too large. You may only upload up to: " . $max_size_mb . "MB");
}
move_uploaded_file($temp_file, $target_file);
echo "Your image was successfully uploaded.";
What our upload script does is:
- After the user selects the file and clicks the
Upload
button, it first grabs the file name and temporary file location. - It next checks to see if the file with the same file name exists in the final destination.
- The file extension is checked against an allowed list of items.
- It continues to check for the image size since a user could potentially add a valid extension and it still upload a file that’s not an image.
- The maximum file size is checked to prevent the user from uploading extremely large files.
- If everything passes, the file is moved from its temporary location to its permanent location.
The Object Oriented Approach
Looking at our existing file, we see that chunks of code can be grouped together. These will become our functions. Let’s start grouping these.
file_exists()
if ( file_exists($target_file) )
{
die("The file already exists");
}
file_extension_allowed()
$allowed = ['jpg', 'png'];
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
if ( ! in_array($extension, $allowed) )
{
die("The format is not correct. You may only upload: " .
implode(", ", $allowed)
);
}
check_image_size()
if ( getimagesize($temp_file) === false )
{
die("You must upload an image");
}
check_max_size()
$max_size_mb = 0.5;
$max_size_bytes = $max_size_mb * 1024 * 1024;
if ( $_FILES["file_name"]["size"] > $max_size_bytes )
{
die("The file size is too large. You may only upload up to: " .
$max_size_mb . "MB"
);
}
It’s easy to separate these out since that’s how we built them. For our purpose, I’m thinking that we keep it simple. You can be as creative as you want.
index.html
file holds the form.upload.php
is the file that grabs the data and instantiates our class. It will also pass data to our object.ImageUpload.php
is going to be our class that does the heavy lifting.
app/
uploads/
index.html
upload.php
ImageUpload.php
We know what index.html
is going to look like. No changes necessary there.
<form action="./upload.php" method="post" enctype="multipart/form-data">
<div>
Select Image to upload:
</div>
<div>
<input type="file" name="file_name" id="file_name">
</div>
<div>
<input type="submit" name="submit" value="Upload">
</div>
</form>
For the time being, let’s create a class skeleton for the ImageUpload.php
file. We’ll look at that last even though realistically I would look at that at the same time that I’m working on my upload.php
file.
<?php
namespace Files\Uploads;
class ImageUpload
{
public function __construct()
{
}
}
I added a namespace to it just for fun. Remember that namespaces are just virtual directories. If you need a refresher on namespaces, you can read about them here.
https://blog.devgenius.io/php-p69-defining-namespaces-5a8e0a4bfdc0
Great, let’s continue working on our upload.php
file next. All we need to do is instantiate our ImageUpload
object and pass it the $_FILES
array. We’re not going to pass it the entire $_FILES
array, since it consists of an array of files. We’re just going to pass it the file that we uploaded. It’s going to be stored in $_FILES["file_name"]
.
<?php
require_once("ImageUpload.php");
$image_upload = new Files\Uploads\ImageUpload( $_FILES["file_name"] );
Remember the namespace? We’ll need to append that to the beginning of the ImageUpload
. We’re done with upload.php
. Everything else will be handled in the ImageUpload
class.
ImageUpload
We know that upload.php is instantiating the ImageUpload object and sending us the $_FILES["file_name"]
array. Let’s grab that array in the constructor and assign it to a class property.
<?php
namespace Files\Uploads;
class ImageUpload
{
private array $_file;
public function __construct( array $file )
{
$this->_file = $file;
}
}
The file comes in through the constructor and gets assigned to the $_file
property. The next portion of the code is up to you. Should we make all of the checks private
or public
? Should the only functionality of this class be to upload an image or should we allow for the user to use the functions for sporadic checks throughout? This is something that you need to think about when you’re creating your class.
Why am I even asking this? Well, we’re going to create a process_upload
function next. The question is do we call it automatically during instantiation (from the constructor) or do we call it from upload.php
file? I like to be able to call it from upload.php
since it seems cleaner for error handling if we wanted to extend that in the future.
<?php
namespace Files\Uploads;
class ImageUpload
{
private array $_file;
public function __construct( array $file )
{
$this->_file = $file;
}
public function process_upload()
{
var_dump($this->_file);
}
}
<?php
require_once("ImageUpload.php");
$image_upload = new Files\Uploads\ImageUpload( $_FILES['file_name'] );
$image_upload->process_upload();
The output that we receive shows us that it works so far.
array (size=5)
'name' => string '2 Comments.jpg' (length=14)
'type' => string 'image/jpeg' (length=10)
'tmp_name' => string '/tmp/phpDh5748' (length=14)
'error' => int 0
'size' => int 468430
We’re free to continue to move our script into our new ImageUpload
class.
$file_name = $_FILES["file_name"]["name"];
$target_file = "uploads/" . $file_name;
$temp_file = $_FILES["file_name"]["tmp_name"];
We’ll need to modify this slightly since we’re not going to be relying on our $_FILES
array any more.
<?php
namespace Files\Uploads;
class ImageUpload
{
private array $_file;
public function __construct( array $file )
{
$this->_file = $file;
}
public function process_upload()
{
$file_name = $this->_file["name"];
$target_file = "uploads/" . $file_name;
$temp_file = $this->_file["tmp_name"];
}
}
Next, let’s add our move_upload_file
method and make sure this works.
<?php
namespace Files\Uploads;
class ImageUpload
{
private array $_file;
public function __construct( array $file )
{
$this->_file = $file;
}
public function process_upload()
{
$file_name = $this->_file["name"];
$target_file = "uploads/" . $file_name;
$temp_file = $this->_file["tmp_name"];
// This is where the checks will go
move_uploaded_file($temp_file, $target_file);
echo "Your image was successfully uploaded.";
}
}
And it works! Time to start adding our check functions. First on the list, checking if the file exists. This is such a simple function that it makes no sense to create a separate method for it. We’ll just keep it as is.
<?php namespace Files\Uploads; class ImageUpload { private array $_file; public function __construct( array $file ) { $this->_file = $file; } public function process_upload() { $file_name = $this->_file["name"]; $target_file = "uploads/" . $file_name; $temp_file = $this->_file["tmp_name"]; if ( file_exists( $target_file ) ) { die("The file already exists"); } move_uploaded_file($temp_file, $target_file); echo "Your image was successfully uploaded."; } }
Next is the list of allowed file extensions. Since this is an ImageUpload
class, it makes sense to have a property with allowed file extensions. So, we can move that array into a property.
<?php
namespace Files\Uploads;
class ImageUpload
{
private array $_file;
private array $_allowed_file_extensions = ['jpg', 'png'];
public function __construct( array $file )
{
$this->_file = $file;
}
public function process_upload()
{
$file_name = $this->_file["name"];
$target_file = "uploads/" . $file_name;
$temp_file = $this->_file["tmp_name"];
if ( file_exists( $target_file ) )
{
die("The file already exists");
}
if ( ! $this->file_extension_allowed( $file_name ))
{
die("The format is not correct. You may only upload: " . implode(", ", $this->_allowed_file_extensions));
}
move_uploaded_file($temp_file, $target_file);
echo "Your image was successfully uploaded.";
}
public function file_extension_allowed( string $file_name ): bool
{
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
return in_array($extension, $this->_allowed_file_extensions);
}
}
Our process_upload
method calls the file_extension_allowed
. If the file extension matches an extension in our $_allowed_file_extensions array
, it returns true
, otherwise false
. If the value is false
, the process stops.
Similar to our file_exists
function, we really don’t have to create a separate method for getimagesize
.
<?php namespace Files\Uploads; class ImageUpload { private array $_file; private array $_allowed_file_extensions = ['jpg', 'png']; public function __construct( array $file ) { $this->_file = $file; } public function process_upload() { $file_name = $this->_file["name"]; $target_file = "uploads/" . $file_name; $temp_file = $this->_file["tmp_name"]; //... if ( getimagesize($temp_file) === false ) { die("You must upload an image"); } move_uploaded_file($temp_file, $target_file); echo "Your image was successfully uploaded."; } //... }
Finally, we need to check for the max file size.
<?php
namespace Files\Uploads;
class ImageUpload
{
private array $_file;
private array $_allowed_file_extensions = ['jpg', 'png'];
private float $_max_size_mb = 0.5;
public function __construct( array $file )
{
$this->_file = $file;
}
public function process_upload()
{
$file_name = $this->_file["name"];
$target_file = "uploads/" . $file_name;
$temp_file = $this->_file["tmp_name"];
//...
if ( $this->check_max_file_size( $this->_file["size"] ))
{
die("The file size is too large. You may only upload up to: " . $this->_max_size_mb . "MB");
}
move_uploaded_file($temp_file, $target_file);
echo "Your image was successfully uploaded.";
}
//...
public function check_max_file_size( $file_size ): bool
{
$max_size_bytes = $this->_max_size_mb * 1024 * 1024;
return $file_size > $max_size_bytes;
}
}
We set the $_max_size_mb
as a class property. The process_upload method calls the check_max_file_size
method. If it returns true
, the size exceeds the max file size.
Let’s see what the full script looks like now.
With a little creativity, we were able to transform our PHP script to a class that fits our existing architecture. This didn’t have to look like this. You could write this code to be as unique as you want it to be.
PHP’S UPLOAD CHECKS ARE THE LAST LINE OF DEFENSE AGAINST MALICIOUS CONTENT
PHP – P80: FINAL UPLOAD CHECKS
You can never be too careful especially when allowing others to upload files to your server. I recommend using a tried and tested PHP package, but we’re learning how stuff works here so we’ll do a few more tests ourselves.
Transforming a script into a class enhances code modularity and reusability
“Couldn’t they just have created this as a class instead of this script,” I found myself asking frequently. It’s really not that far of a stretch to do this yourself, and you will do it frequently.
Transforming a script into a class enhances code modularity and reusability
It is an Oracle distributed database system that runs on a server. It uses SQL syntax and paired with PHP to the point that it’s not common to learn one without the other.