File uploads allow web applications to transfer files efficiently and reliably, while preserving file content and metadata in a standardized way that works across browsers and servers.
The native PHP $_FILES superglobal has several limitations and pitfalls that developers often run into:
$_FILES becomes a deeply nested array that is difficult to iterate over and easy to misuse.$_FILES['error'] can result in processing incomplete or failed uploads.$_FILES may contain entries even when no file was uploaded, typically with the UPLOAD_ERR_NO_FILE error code, which can be misleading if not handled explicitly.To address these issues, a dedicated file upload component was introduced.
To submit a file using an HTML form, ensure the form uses the POST method and the correct encoding type:
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="document">Choose a document:</label>
<input type="file" id="document" name="document">
<button type="submit">Upload</button>
</form>
Alternatively, files can be sent via JavaScript using FormData:
const fileInput = document.getElementById("document");
const file = fileInput.files[0];
const formData = new FormData();
formData.append("file", file);
fetch("/upload", {
method: "POST",
body: formData
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
To access uploaded files, call the getFiles() method on the request service from within your controller. This returns an instance of Krystal\Http\FileTransfer\FileEntity.
public function uploadAction()
{
// Get file instance from input named "document"
$file = $this->request->getFiles('document');
if ($file) {
$file->getName(); // Original filename
$file->getUniqueName(); // Unique filename (preserves extension)
$file->getType(); // Guessed MIME type
$file->getMimeType(); // Verified MIME type (server-side check)
$file->getTmpName(); // Temporary file path
$file->getError(); // Native PHP upload error code
$file->getSize(); // File size in bytes
$file->getHumanSize(); // Formatted size (e.g., "200.00 KB")
$file->isDangerous(); // Security check
// ...
}
// ...
}
If no file was uploaded, getFiles() returns an empty array.
Regardless of input name or nesting depth, this method always returns a normalized and predictable collection of uploaded files, unlike the native $_FILES superglobal.
Always call $file->isDangerous() before moving or processing any uploaded file.
Why & How
$file->isDangerous() protects against common malicious uploads by checking:
File extension against a deny-list (.php, .htaccess, etc.)
Real MIME type using magic bytes (prevents disguised scripts)
$file = $this->request->getFiles('avatar');
// Basic
if ($file && $file->isDangerous()) {
// Dangerous file type detected!
}
// With extra extensions to verify
if ($file->isDangerous(['docm', 'xlsm', 'jar'])) {
// Dangerous file type detected!
}
// Only check extension (skip MIME verification)
if ($file->isDangerous([], false)) {
// Dangerous file type detected!
}
To persist uploaded files, use the dedicated uploader component:
<?php
namespace Site\Controller;
use Krystal\Http\FileTransfer\FileUploader;
use Krystal\Application\Controller;
final class User extends AbstractController
{
public function upload()
{
$files = $this->request->getFiles('files'); // If parameter provided, only from that input data is exacracted
if ($files) { // If there's a file
$destination = $this->appConfig->getUploadsDir(); // e.g /data/uploads/
$uploader = new FileUploader();
$uploader->upload($destination, $files); // Returns boolean value
// Other methods
$uploader->getUploadedFiles(); // Returns array of uploaded files
$uploader->getFailedFiles(); // Returns array of failed files
}
}
}
Instead of using the full FileUploader service, you can also move an individual file directly from the entity. By default, it will use the secure name generated by getUniqueName().
public function uploadAction()
{
$file = $this->request->getFiles('document');
if ($file) {
$path = '/path/to/your/uploads';
// Moves the file and returns a boolean
if ($file->move($path)) {
echo "File saved as: " . $file->getUniqueName();
}
}
}
Custom naming
If you prefer to keep the original filename (not recommended for public uploads) or provide a custom one, you can pass it as the second argument:
// Saves the file with its original name
$file->move($path, $file->getName());
// Saves the file with a custom name
$file->move($path, 'profile_v1.png');