ZetCode

Symfony 文件上传

最后修改于 2020 年 7 月 5 日

Symfony 文件上传教程展示了如何在 Symfony 应用程序中上传文件。在示例中,我们使用普通表单发送文件;我们不使用表单构建器。

Symfony

Symfony 是一套可重用的 PHP 组件和一个用于 Web 项目的 PHP 框架。Symfony 于 2005 年发布为免费软件。Symfony 的原始作者是 Fabien Potencier。Symfony 的灵感主要来源于 Spring Framework 和 Ruby on Rails。

文件上传

要上传文件,form 必须将 enctype 设置为 multipart/form-data,并将 input 的类型设置为 file

此外,在 PHP 的 php.ini 文件中,文件上传是通过 file_uploads 选项控制的。

Symfony 文件上传示例

在示例中,我们有一个简单的表单,其中包含一个输入字段:要上传的文件。表单提交后,我们验证 CSRF 令牌并加载图像,检索其名称,然后将文件存储在 var 目录中。

$ symfony new symupl
$ cd symupl

我们创建一个新的 Symfony 项目并进入项目目录。

$ php bin/console --version
Symfony 5.0.8 (env: dev, debug: true)

我们使用 Symfony 5.0.8。

$ composer require annot twig

我们安装以下包:annotationstwig。这些是创建路由和模板所必需的。

$ composer require symfony/security-csrf monolog

symfony/security-csrf 包用于防止跨站请求伪造,monolog 用于日志记录。

$ composer require maker profiler --dev

在开发阶段,我们还安装了 maker 和 profiler。

构建 Symfony 应用程序

我们定义将上传图像的目录。

config/services.yaml
parameters:
    upload_dir: '../var/uploads'

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, ...

        bind:
            $uploadDir: '%upload_dir%'
...

我们定义了一个包含图像上传目录名称的参数。upload_dir 参数绑定到 $uploadDir 变量,该变量可以被注入。

$ php bin/console make:controller HomeController

我们创建一个 HomeController。该控制器将表单发送给客户端。

src/Controller/HomeController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends AbstractController
{
    /**
     * @Route("/", name="home")
     */
    public function index()
    {
        return $this->render('home/index.html.twig');
    }
}

这是一个简单的控制器,它将包含 Web 表单的视图发送给用户。

templates/home/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Upload file{% endblock %}

{% block body %}

  <form action="{{ path('do-upload') }}" method="post" enctype="multipart/form-data">

        <input type="hidden" name="token" value="{{ csrf_token('upload') }}" />

        <div>
            <label for="myfile">File to upload:</label>
            <input type="file" name="myfile" id="myfile">
        </div>

        <button type="submit">Send</button>

  </form>

{% endblock %}

此视图创建一个表单。它定义了 multipart/form-data enctype 和 file 输入。此外,它还有一个 CSRF 隐藏输入令牌。

$ php bin/console make:controller UploadController

我们创建一个 UploadController 来响应表单提交。此控制器不需要生成的 twig 模板;因此,我们将其删除。

src/Controller/UploadController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Service\FileUploader;
use Psr\Log\LoggerInterface;

class UploadController extends AbstractController
{
    /**
     * @Route("/doUpload", name="do-upload")
     * @param Request $request
     * @param string $uploadDir
     * @param FileUploader $uploader
     * @param LoggerInterface $logger
     * @return Response
     */
    public function index(Request $request, string $uploadDir,
                          FileUploader $uploader, LoggerInterface $logger): Response
    {
        $token = $request->get("token");

        if (!$this->isCsrfTokenValid('upload', $token))
        {
            $logger->info("CSRF failure");

            return new Response("Operation not allowed",  Response::HTTP_BAD_REQUEST,
                ['content-type' => 'text/plain']);
        }

        $file = $request->files->get('myfile');

        if (empty($file))
        {
            return new Response("No file specified",
               Response::HTTP_UNPROCESSABLE_ENTITY, ['content-type' => 'text/plain']);
        }

        $filename = $file->getClientOriginalName();
        $uploader->upload($uploadDir, $file, $filename);

        return new Response("File uploaded",  Response::HTTP_OK,
            ['content-type' => 'text/plain']);
    }
}

UploadController 中,我们检查 CSRF 令牌,从请求中获取文件,并调用 uploader 服务 upload() 方法。

public function index(Request $request, string $uploadDir,
                      FileUploader $uploader, LoggerInterface $logger): Response
{

我们注入请求对象、上传目录参数、FileUploader 服务和日志记录器。

$token = $request->get("token");

if (!$this->isCsrfTokenValid('upload', $token))
{
    $logger->info("CSRF failure");

    return new Response("Operation not allowed",  Response::HTTP_BAD_REQUEST,
        ['content-type' => 'text/plain']);
}

我们检索令牌并使用 isCsrfTokenValid() 方法对其进行验证。如果验证失败,我们会记录事件并发送一个纯文本响应“Operation not allowed”和 Response::HTTP_BAD_REQUEST 响应代码。

$file = $request->files->get('myfile');

if (empty($file))
{
    return new Response("No file specified",  Response::HTTP_UNPROCESSABLE_ENTITY,
        ['content-type' => 'text/plain']);
}

我们使用 empty() 方法检查用户是否在表单中指定了任何文件。如果输入字段为空,我们会将纯文本“No file specified”发送回客户端,响应代码为 Response::HTTP_UNPROCESSABLE_ENTITY

$filename = $file->getClientOriginalName();

我们使用 getClientOriginalName() 获取文件名。

$uploader->upload($uploadDir, $file, $filename);

我们调用 uploader 服务 upload() 方法,该方法将文件移动到选定的目录。我们将目录名称、文件数据和文件名传递给该方法。

return new Response("File uploaded",  Response::HTTP_OK,
    ['content-type' => 'text/plain']);

如果一切顺利,我们会将纯文本消息“File uploaded”发送回客户端,响应代码为 Response::HTTP_OK

src/Service/FileUploader.php
<?php

namespace App\Service;

use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Psr\Log\LoggerInterface;

class FileUploader
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function upload($uploadDir, $file, $filename)
    {
        try {

            $file->move($uploadDir, $filename);
        } catch (FileException $e){

            $this->logger->error('failed to upload image: ' . $e->getMessage());
            throw new FileException('Failed to upload file');
        }
    }
}

FileUploader 服务使用 move() 将文件移动到上传目录。操作失败时,我们会抛出 FileException。这将导致生成错误页面。

templates/bundles/TwigBundle/Exception/error.html.twig
{% extends "base.html.twig" %}
{% block title %}
    Problem detected
{% endblock %}
{% block body %}
    <div>
        <p>
            There was a problem: {{ exception.message }}
        </p>
    </div>
{% endblock %}

我们覆盖 Twig 的 error.html.twig 模板。我们需要在 templates 目录中创建这个确切的目录路径:bundles/TwigBundle/Exception/。当发生 FileException 且环境设置为 production 时,将为用户生成此错误视图。

templates/base.html.twig
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

这是基本的 Twig 模板。

在本教程中,我们展示了如何在 Symfony 应用程序中上传文件。

查看所有 Symfony 教程