Uploading multiple and single files in a single form

I am trying to write a file that will handle the uploading of files in PHP, but I’m currently stuck in the logic part.

The issue I am having, and I searched around for this for days and cant find an answer, is that I want to be able to have a form that will allow single file uploads, and also multi-file uploads.

For example, the following form:

<form action="upload" method="post" enctype="multipart/form-data">
     <input type="file" name="image_1[]" multiple="multiple">
      <input type="file" name="image_2">
</form>

In the example above, I want to allow image_1 to have multiple files, and image_2 to have only one.
What I cant figure out is how to check the files, since uploading them will give to different arrays, like this:

Array
(
    [image_1] => Array
        (
            [name] => Array
                (
                    [0] => file_1.pdf
                    [1] => file_2.pdf
                )

            [type] => Array
                (
                    [0] => application/pdf
                    [1] => application/pdf
                )

            [tmp_name] => Array
                (
                    [0] => /tmp/phpxrR9jO
                    [1] => /tmp/phpTLIOX5
                )

            [error] => Array
                (
                    [0] => 0
                    [1] => 0
                )

            [size] => Array
                (
                    [0] => 164086
                    [1] => 151993
                )

        )

    [image_2] => Array
        (
            [name] => file.pdf
            [type] => application/pdf
            [tmp_name] => /tmp/php9sLiBn
            [error] => 0
            [size] => 500065
        )

)

I want to check the files for errors, size, type, etc. but I’m not sure how I can do this in an OOP way.

What i started writing but could actually write (because I don’t understand how this should work), is a main function that will loop through all of the files, and inside that loop, will call on other functions that will validate the file.
These other functions will check the files size, type, errors, etc.

What I cant figure out is how to do this when you have multi-file fields also.
Should I check if the file is a multi-file and then loop through it in each validator function, or should I loop through them before, and then call each validator function for that specific file value?

Or maybe I should convert the image_1 array into smaller arrays, or somehow group all names, types, tmp_names, etc.?

So… what’s the difference between putting a file in image_2, and just putting 1 file in image_1?

Just because a field accepts multiple files, doesnt mean it has to have more than 1…

Also multiple is boolean (like selected). You don’t give it a value.

foreach($_FILES['image_1']['name'] AS $index => $filename) {
at which point, you can access $_FILES['image_1']['type'][$index] to get the current file’s type, etc. etc. etc.

Yes, it wont always have more than 1 file, but it can.
image_1 and image_2 were just examples to make things clearer, in reality, there could be any number of fields, some could be single file uploads and some could be multi-file uploads.

Yes, that is currently what i basically have, but, this is only for fields that are multi-file.
My question is how to combine the file validation for both the multi-file fields and the single file fields.

At the moment, i am using the following function to loop through all of the upload files:

private function validateFiles()
    {
        //Loop through all of the file
        foreach ($this->_files AS $this->field => $this->fileData) {

            //Check file for errors
            $this->checkFileForErrors();

            //Check file sizes
            $this->checkMaxFileSize();

            //Check file types
            $this->checkFileType();
        }
    }

Then, in each of the functions i have an if statement checking if the file is a multi-file, and if so i loop through them and do the check, if not, i just do the check in the single file.

Similar to this:

private function checkFileForErrors()
    {
        //If there are multiple files
        if (is_array($this->fileData['size'])) {

            //Loop through each file error
            foreach($this->fileData['error'] AS $fileError) {
                switch ($fileError) {
                    case UPLOAD_ERR_OK:
                        break;
                    case UPLOAD_ERR_NO_FILE:
                        throw new RuntimeException('No file sent.');
                    case UPLOAD_ERR_INI_SIZE:
                    case UPLOAD_ERR_FORM_SIZE:
                        throw new RuntimeException('Exceeded filesize limit.');
                    default:
                        throw new RuntimeException('Unknown errors.');
                }
            }


        //If there is a single file
        } else {
            switch ($this->fileData['error']) {
                case UPLOAD_ERR_OK:
                    break;
                case UPLOAD_ERR_NO_FILE:
                    throw new RuntimeException('No file sent.');
                case UPLOAD_ERR_INI_SIZE:
                case UPLOAD_ERR_FORM_SIZE:
                    throw new RuntimeException('Exceeded filesize limit.');
                default:
                    throw new RuntimeException('Unknown errors.');
            }
        }
    }

I do this for each validation function. The question is if there is a better way to do this.

(This is just sample code for explaining purposes, not the actual final code).

If it were me, i’d probably take a single file upload field and form it into a ‘multi’ file upload field with a single file before you send the field for checking.

You can easily make a single-upload look like a multi-upload, the other way not so much.

You need to reverse the logic and test for an array in the outer code, looping as necessary for a multi-file field, than call the validation steps for each single file. By doing this, you can also have specific valuation steps for a multi-file field, such as a minimum or maximum number of files per field. This will also eliminate the repetition of code in the validation steps.

and what about converting the multi-file array into a regular single file array, or the opposite?

In looking at the arrangement of your class code, this is not how OOP works. OOP is not about taking your main code, wrapping class/method definitions around it and adding $this-> in front of everything. All that is doing is adding an unnecessary layer of syntax, that doesn’t add any value to what you are doing. Specifically, user written methods/functions should accept any input data that they operate on as call-time parameters, so that they are general-purpose and reusable.

I originally thought you were using a data driven design, where you have defined a data structure (array, database table) that controls what general-purpose code does, but it actually looks like you are unconditionally looping over the raw submitted form data. This opens your code up to DOS attacks.

Your processing logic should look like this -

// use a data driven design
// definition of expected form data - the main index is the field name, which is unique
$fields = [];
$fields['image_1'] = ['label'=>'Select image(s)', 'type'=>'file', 'multiple'=>true]; // note: you will always get a [0]th element in the array of form data
$fields['image_2'] = ['label'=>'Select image', 'type'=>'file', 'multiple'=>false];

// post method form processing
if($_SERVER['REQUEST_METHOD'] == 'POST')
{
	// loop over the definition of expected form data
	foreach($fields as $field=>$arr)
	{
		// determine which validation/processing to use
		switch($arr['type'])
		{
			// file upload handling
			case 'file':
				// determine if the expected data is for multiple or single files
				if($arr['multiple'] ?? false)
				{
					// multiple
					// do any minimum/maximum number of files check here...
					// loop over the actual data
					$file_elements = ['name','type','tmp_name','error','size'];
					foreach(array_keys($_FILES[$field]['name']) as $key)
					{
						// get elements for one file
						foreach($file_elements as $element)
						{
							$file[$element] = $_FILES[$field][$element][$key];
						}
						// at this point, $file contains the values for a single file
						// call the error and validation methods, which should accept their inputs as call time parameters
						$validation->checkFileForErrors($file);
					}
				}
				else
				{
					// single
					$file = $_FILES[$field];
					// at this point, $file contains the values for a single file
					// call the error and validation methods, which should accept their inputs as call time parameters
					$validation->checkFileForErrors($file);
				}
			
			break;
			
			// code for other type handling...
		}
	}
}
?>