File uploads
A file sent through a form arrives in the request apart from the other fields. Slim wraps it in an uploaded file object — it has methods to learn the name and size and to move the file to a permanent place. Let us add a cover image to a post.
The form
For the browser to send a file, the form needs the enctype attribute:
{# templates/posts/cover.twig #}
{% extends 'layout.twig' %}
{% block content %}
<form method="post" action="{{ url_for('posts.cover', {id: post.id}) }}" enctype="multipart/form-data">
{% include '_csrf.twig' %}
<input type="file" name="cover">
<button type="submit">Upload</button>
</form>
{% endblock %}The action that shows this form is written the same way as NewPostAction.
Where to save
Create the public/uploads folder. It is inside public, so a saved image is immediately reachable at /uploads/file-name.
Receiving the file
// src/Action/UploadCoverAction.php
final class UploadCoverAction
{
public function __invoke(Request $request, Response $response, array $args): Response
{
$file = $request->getUploadedFiles()['cover'] ?? null;
if ($file === null || $file->getError() !== UPLOAD_ERR_OK) {
return $response->withStatus(400);
}
$name = 'post-' . (int) $args['id'] . '.jpg';
$file->moveTo(__DIR__ . '/../../public/uploads/' . $name);
return $response->withHeader('Location', '/posts/' . (int) $args['id'])->withStatus(302);
}
}// config/routes.php
$group->post('/posts/{id:[0-9]+}/cover', UploadCoverAction::class)->setName('posts.cover');The route is added to the group with AuthMiddleware. getUploadedFiles() returns an array keyed by the form field name. getError() says whether the file arrived in full, and moveTo() moves it from the temporary folder to the given place. After the upload the file appears at public/uploads/post-1.jpg.
Uploaded file methods
| Method | What it returns |
|---|---|
getError() | The error code; UPLOAD_ERR_OK means the file was received |
getSize() | The size in bytes |
getClientFilename() | The file name on the visitor's computer |
getClientMediaType() | The type reported by the browser |
moveTo($path) | Moves the file |
Checks
The first example accepts anything. Before moveTo() add a size and extension check:
// src/Action/UploadCoverAction.php
$extension = strtolower(pathinfo($file->getClientFilename(), PATHINFO_EXTENSION));
if (!in_array($extension, ['jpg', 'jpeg', 'png', 'webp'], true)) {
return $response->withStatus(422);
}
if ($file->getSize() > 2 * 1024 * 1024) {
return $response->withStatus(422);
}
$name = bin2hex(random_bytes(16)) . '.' . $extension;Only extensions from the list and files up to 2 MB are allowed. The application invents the file name itself — a random string; it is stored in the posts table so the image can be shown in a template later.
PHP limits
A large file may never reach the application at all: PHP itself cuts it off. The upload_max_filesize and post_max_size settings in php.ini control this. If getError() returns UPLOAD_ERR_INI_SIZE, the file exceeded the first of those values.
Common mistake
The file name and type are sent by the visitor's browser and are trivial to fake. If the file is saved under its original name, an attacker uploads shell.php into public and runs it by opening its address. Never use getClientFilename() as the name on the server: the extension comes only from the allowed list, the name is your own.