Production-Safe Excel Upload and Import
Build one secure workflow that validates an HTTP upload, stores it privately, inspects the workbook, checks headers, previews SQL changes, and imports through PDO.
Updated
Full packagePDOUpload validationProduction
MnbExcel::validateUpload()MnbExcel::read()ReadSession::inspect()ReadSession::dryRunImportToSql()ReadSession::importToSql()What you will learn
- Validate an untrusted HTTP upload before opening it.
- Keep uploaded files outside the public web root under generated names.
- Verify worksheet and headers before a dry-run and transactional import.
Before you start
- Install
mnb/mnb-phpexcel:^2.0. - Enable the PDO driver for the target database.
- Create the target table and a unique index for the business key.
Define the import contract#
$requiredHeaders = ['SKU', 'Product Name', 'Price'];
$columnMap = [
'SKU' => 'sku',
'Product Name' => 'name',
'Price' => 'price',
];The spreadsheet contract, validation rules, SQL schema, and unique indexes must describe the same business data.
Validate and store the uploaded file#
use Mnb\PHPExcel\MnbExcel;
use Mnb\PHPExcel\Support\MnbExcelException;
$validation = MnbExcel::validateUpload($_FILES['spreadsheet'], [
'allowed_extensions' => ['xlsx'],
'max_size_mb' => 100,
'validate_package' => true,
'max_zip_entries' => 5000,
'max_uncompressed_size_mb' => 500,
]);
if (($validation['valid'] ?? false) !== true) {
throw new RuntimeException('The uploaded workbook failed validation.');
}
$storageDirectory = dirname(__DIR__) . '/storage/uploads';
if (!is_dir($storageDirectory) && !mkdir($storageDirectory, 0770, true) && !is_dir($storageDirectory)) {
throw new RuntimeException('Upload storage could not be created.');
}
$storedPath = $storageDirectory . '/' . bin2hex(random_bytes(16)) . '.xlsx';
if (!move_uploaded_file($_FILES['spreadsheet']['tmp_name'], $storedPath)) {
throw new RuntimeException('The uploaded workbook could not be stored.');
}Inspect the workbook and verify headers#
$workbook = MnbExcel::read($storedPath);
$inspection = $workbook->inspect();
$sheet = $workbook->sheetIfExists('Products');
if ($sheet === null) {
throw new RuntimeException(
'Products worksheet not found. Available: ' . implode(', ', $workbook->sheetNames())
);
}
$session = $sheet
->withHeaderRow(1)
->withOptions([
'skip_empty_rows' => true,
'max_rows' => 250000,
])
->assertHasRows();
$firstRow = $session->first();
$actualHeaders = array_keys($firstRow ?? []);
$missingHeaders = array_values(array_diff($requiredHeaders, $actualHeaders));
if ($missingHeaders !== []) {
throw new RuntimeException('Missing required headers: ' . implode(', ', $missingHeaders));
}Dry-run before changing the database#
$preview = $session->dryRunImportToSql($pdo, 'products', [
'map' => $columnMap,
'unique_by' => ['sku'],
'duplicate_strategy' => 'update',
'batch_size' => 500,
]);
print_r($preview);Expected output
Array
(
[would_insert] => 1720
[would_update] => 85
[invalid] => 4
[skipped] => 2
)Review invalid rows and duplicate behavior before enabling the final import action.
Import with a safe application boundary#
try {
$result = $session->importToSql($pdo, 'products', [
'map' => $columnMap,
'unique_by' => ['sku'],
'duplicate_strategy' => 'update',
'batch_size' => 500,
]);
echo json_encode([
'ok' => true,
'result' => $result,
], JSON_THROW_ON_ERROR);
} catch (MnbExcelException|PDOException $error) {
error_log(json_encode([
'message' => $error->getMessage(),
'file' => $storedPath,
], JSON_THROW_ON_ERROR));
http_response_code(422);
echo json_encode([
'ok' => false,
'message' => 'The workbook could not be imported.',
], JSON_THROW_ON_ERROR);
} finally {
if (is_file($storedPath)) {
unlink($storedPath);
}
}Production checklist#
- Require authentication and CSRF protection before accepting uploads.
- Enforce PHP, web-server, application, package, worksheet, row, and execution-time limits.
- Use generated filenames and private storage; never trust a browser-supplied path.
- Inspect the workbook and verify the expected worksheet and headers before SQL.
- Use a dry-run or approval step for high-impact imports.
- Create a database unique index for the keys used in duplicate handling.
- Keep detailed exception context in server logs and return safe messages to users.
- Delete uploaded and temporary files on success, failure, and expiry.
- Run very large imports in a queue or separate worker process.
Was this guide useful?Use GitHub issues for corrections, missing examples, or unclear behavior.
Open an issue