Script to Class

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.

https://github.com/dinocajic/php-youtube-tutorials

Final Upload Checks

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.

Script to Class

Transforming a script into a class enhances code modularity and reusability

PHP – P81: script to Class

“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.

MySQL Introduction

Transforming a script into a class enhances code modularity and reusability

PHP – P82: mysql introduction

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.

Leave a Reply