php runtime and server environment

File Upload Configuration

File uploads are controlled by both your application code and PHP's runtime settings. A perfectly valid upload form can still fail before your controller runs if PHP rejects the request because the file is too large, uploads are disabled, or too many files were sent.

For junior PHP work, the important skill is not memorising every directive. It is knowing which settings affect uploads, how they relate to each other, and how to debug a failed upload without guessing.

The settings that matter

These are the main php.ini directives you will meet when working with uploads:

file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M
max_file_uploads = 20
max_input_time = 60
memory_limit = 128M

file_uploads must be enabled or PHP will not accept uploaded files at all.

upload_max_filesize is the maximum size of one uploaded file. If a user uploads one image, this is the limit that image must fit inside.

post_max_size is the maximum size of the whole request body. It must be larger than upload_max_filesize because the request contains the file plus form fields and multipart overhead. If post_max_size is too small, PHP may give you empty $_POST and $_FILES arrays, which can look like the form was not submitted.

max_file_uploads limits how many files PHP will accept in one request. This matters for multi-file forms such as gallery uploads, product image uploads, document packs, and admin imports.

max_input_time limits how long PHP spends reading request input. Very large uploads, slow connections, and overloaded environments can hit this before application code starts.

memory_limit is not usually the file-size limit because PHP stores uploaded files in temporary files, not as one big string in memory. It still matters if your application later reads the full file into memory, resizes images, parses CSV files, or builds previews.

Inspecting the active configuration

The configuration you edited is not always the configuration PHP is using. CLI PHP and web PHP can load different php.ini files, and PHP-FPM may need a reload before changes take effect.

This small script prints the active upload settings:

PHP example
<?php

declare(strict_types=1);

$settings = [
    'file_uploads',
    'upload_max_filesize',
    'post_max_size',
    'max_file_uploads',
    'max_input_time',
    'memory_limit',
];

foreach ($settings as $name) {
    echo $name . ': ' . ini_get($name) . PHP_EOL;
}

// Prints:
// file_uploads: 1
// upload_max_filesize: 10M
// post_max_size: 12M
// max_file_uploads: 20
// max_input_time: 60
// memory_limit: 128M

Run this through the same SAPI that receives the upload. If the bug happens in a browser through PHP-FPM, a CLI command may not prove the web runtime is configured correctly.

Size units in PHP settings

PHP settings often use shorthand units such as K, M, and G. When you compare limits in code, convert them into bytes first.

PHP example
<?php

declare(strict_types=1);

function iniSizeToBytes(string $value): int
{
    $value = trim($value);

    if ($value === '') {
        return 0;
    }

    $unit = strtolower($value[strlen($value) - 1]);
    $number = (float) $value;

    return match ($unit) {
        'g' => (int) ($number * 1024 * 1024 * 1024),
        'm' => (int) ($number * 1024 * 1024),
        'k' => (int) ($number * 1024),
        default => (int) $number,
    };
}

$uploadLimit = iniSizeToBytes('10M');
$postLimit = iniSizeToBytes('12M');

echo $uploadLimit . PHP_EOL;
echo $postLimit . PHP_EOL;

// Prints:
// 10485760
// 12582912

You do not need this helper in every project, but the idea matters. If a product owner asks for "10 MB uploads", you need to know whether the PHP runtime, web server, reverse proxy, and application validation all agree.

Configuration is only the first gate

PHP upload configuration decides whether the request can reach your code. It does not prove the file is safe, useful, or allowed.

A normal upload form must use method="post" and enctype="multipart/form-data":

<form method="post" enctype="multipart/form-data">
    <input type="file" name="avatar">
    <button type="submit">Upload</button>
</form>

Then application code should check the upload error, file size, MIME type, extension rules, destination path, and permissions before moving the uploaded file.

PHP example
<?php

declare(strict_types=1);

function iniSizeToBytes(string $value): int
{
    $value = trim($value);

    if ($value === '') {
        return 0;
    }

    $unit = strtolower($value[strlen($value) - 1]);
    $number = (float) $value;

    return match ($unit) {
        'g' => (int) ($number * 1024 * 1024 * 1024),
        'm' => (int) ($number * 1024 * 1024),
        'k' => (int) ($number * 1024),
        default => (int) $number,
    };
}

$file = [
    'error' => UPLOAD_ERR_OK,
    'size' => 512000,
];

if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
    throw new RuntimeException('The upload did not complete successfully.');
}

if (($file['size'] ?? 0) > iniSizeToBytes((string) ini_get('upload_max_filesize'))) {
    throw new RuntimeException('The uploaded file is too large.');
}

echo 'Upload passed the first runtime checks.' . PHP_EOL;

// Prints:
// Upload passed the first runtime checks.

In real code, $file would usually come from $_FILES['avatar']. This example uses a small array so the important runtime checks can be run and edited safely from the command line.

Understanding upload error codes

When PHP receives a file but cannot accept it cleanly, it records an upload error code in $_FILES['field']['error'].

PHP example
<?php

declare(strict_types=1);

function uploadErrorMessage(int $error): string
{
    return match ($error) {
        UPLOAD_ERR_OK => 'The upload completed successfully.',
        UPLOAD_ERR_INI_SIZE => 'The file exceeded upload_max_filesize.',
        UPLOAD_ERR_FORM_SIZE => 'The file exceeded the form MAX_FILE_SIZE value.',
        UPLOAD_ERR_PARTIAL => 'The file was only partially uploaded.',
        UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
        UPLOAD_ERR_NO_TMP_DIR => 'PHP has no temporary upload directory.',
        UPLOAD_ERR_CANT_WRITE => 'PHP could not write the uploaded file.',
        UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.',
        default => 'Unknown upload error.',
    };
}

echo uploadErrorMessage(UPLOAD_ERR_INI_SIZE) . PHP_EOL;
echo uploadErrorMessage(UPLOAD_ERR_NO_FILE) . PHP_EOL;

// Prints:
// The file exceeded upload_max_filesize.
// No file was uploaded.

These codes are useful because they separate configuration problems from validation problems. An oversized file that hits upload_max_filesize is not the same as an image that your application rejects because the MIME type is wrong.

Common debugging path

When an upload fails, work from the outside in:

  1. Confirm the form uses POST and multipart/form-data.
  2. Confirm the request is not blocked by the web server or reverse proxy before PHP sees it.
  3. Print the active PHP upload settings from the same SAPI handling the request.
  4. Check whether $_POST and $_FILES are both empty, which often points to post_max_size.
  5. Check the upload error code in $_FILES.
  6. Check temporary directory permissions if you see filesystem-related upload errors.
  7. Check application validation only after PHP has accepted the upload.

This order saves time. If PHP rejected the request at runtime level, changing controller validation will not fix it.

What you should be able to do

After this lesson, you should be able to explain the difference between upload_max_filesize and post_max_size, inspect the active upload settings, recognise the main upload error codes, and debug why a file upload never reaches normal application validation.

Practice

Task: Diagnose Upload Configuration

Create a small PHP diagnostic script for a project that accepts avatar uploads.

The project requirement is:

  • one uploaded avatar can be up to 5M
  • the full form request can be up to 8M
  • the form should only accept one uploaded file

Requirements

  • Print the active values for file_uploads, upload_max_filesize, post_max_size, and max_file_uploads.
  • Convert upload_max_filesize and post_max_size into bytes before comparing them.
  • Print a warning if uploads are disabled.
  • Print a warning if post_max_size is not larger than upload_max_filesize.
  • Print a warning if max_file_uploads allows more files than the feature needs.
  • Include a short note explaining whether the configuration is ready for the avatar form.

Check your work

Use clear output that another developer could paste into a support ticket or pull request comment. The script does not need to process a real uploaded file; it is checking whether PHP is ready to receive one.

Show solution

One possible solution is to inspect the active settings, normalise the size values, and print warnings that point to the likely configuration problem.

PHP example
<?php

declare(strict_types=1);

function iniSizeToBytes(string $value): int
{
    $value = trim($value);

    if ($value === '') {
        return 0;
    }

    $unit = strtolower($value[strlen($value) - 1]);
    $number = (float) $value;

    return match ($unit) {
        'g' => (int) ($number * 1024 * 1024 * 1024),
        'm' => (int) ($number * 1024 * 1024),
        'k' => (int) ($number * 1024),
        default => (int) $number,
    };
}

$settings = [
    'file_uploads' => (string) ini_get('file_uploads'),
    'upload_max_filesize' => (string) ini_get('upload_max_filesize'),
    'post_max_size' => (string) ini_get('post_max_size'),
    'max_file_uploads' => (string) ini_get('max_file_uploads'),
];

foreach ($settings as $name => $value) {
    echo $name . ': ' . $value . PHP_EOL;
}

$uploadMaxBytes = iniSizeToBytes($settings['upload_max_filesize']);
$postMaxBytes = iniSizeToBytes($settings['post_max_size']);
$maxFileUploads = (int) $settings['max_file_uploads'];

echo 'upload_max_filesize_bytes: ' . $uploadMaxBytes . PHP_EOL;
echo 'post_max_size_bytes: ' . $postMaxBytes . PHP_EOL;

if ($settings['file_uploads'] !== '1') {
    echo 'Warning: file uploads are disabled.' . PHP_EOL;
}

if ($postMaxBytes <= $uploadMaxBytes) {
    echo 'Warning: post_max_size should be larger than upload_max_filesize.' . PHP_EOL;
}

if ($maxFileUploads > 1) {
    echo 'Warning: this feature only needs one upload, but PHP allows more.' . PHP_EOL;
}

echo 'Review complete for the avatar upload form.' . PHP_EOL;

// Prints:
// file_uploads: 1
// upload_max_filesize: 5M
// post_max_size: 8M
// max_file_uploads: 20
// upload_max_filesize_bytes: 5242880
// post_max_size_bytes: 8388608
// Warning: this feature only needs one upload, but PHP allows more.
// Review complete for the avatar upload form.

The configuration is close to ready for the avatar form because uploads are enabled, upload_max_filesize allows a 5M file, and post_max_size is larger than the single-file limit. The remaining review point is max_file_uploads: allowing 20 files is PHP's default and may be acceptable globally, but the application should still enforce one avatar upload at the form and validation layers.