Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

MethodWhat 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.

Thanks for reading!

Donate to the site — we work hard for you.