Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Загрузка файлов

Файл, отправленный через форму, приходит в запросе отдельно от остальных полей. Slim оборачивает его в объект загруженного файла — у него есть методы, чтобы узнать имя и размер и переместить файл в постоянное место. Добавим к статье обложку.

Форма

Чтобы браузер отправил файл, форме нужен атрибут enctype:

{# 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">Загрузить</button>
    </form>
{% endblock %}

Действие, которое показывает эту форму, пишется так же, как NewPostAction.

Куда сохранять

Создайте папку public/uploads. Она лежит внутри public, поэтому сохранённая картинка сразу доступна по адресу /uploads/имя-файла.

Приём файла

// 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');

Маршрут добавляется в группу с AuthMiddleware. Метод getUploadedFiles() возвращает массив, где ключ — имя поля формы. getError() сообщает, дошёл ли файл целиком, а moveTo() переносит его из временной папки в указанное место. После загрузки файл появится в public/uploads/post-1.jpg.

Методы загруженного файла

МетодЧто возвращает
getError()Код ошибки; UPLOAD_ERR_OK — файл получен
getSize()Размер в байтах
getClientFilename()Имя файла на компьютере посетителя
getClientMediaType()Тип, который сообщил браузер
moveTo($path)Перемещает файл

Проверки

Первый пример принимает что угодно. Перед moveTo() добавьте проверку размера и расширения:

// 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;

Разрешены только расширения из списка и файлы не больше 2 МБ. Имя файла приложение придумывает само — случайную строку; её сохраняют в таблице posts, чтобы потом вывести картинку в шаблоне.

Ограничения PHP

Большой файл может не дойти до приложения вовсе: его отсекает сам PHP. За это отвечают настройки upload_max_filesize и post_max_size в php.ini. Если getError() возвращает UPLOAD_ERR_INI_SIZE, файл превысил первое из этих значений.

Частая ошибка

Имя и тип файла присылает браузер посетителя, и подделать их ничего не стоит. Если сохранить файл под исходным именем, злоумышленник загрузит shell.php в папку public и выполнит его, открыв по адресу. Никогда не используйте getClientFilename() как имя на сервере: расширение — только из списка разрешённых, имя — своё.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.