How to Upload Files

Note

ノート

Instead of handling file uploading yourself, you may consider using the VichUploaderBundle community bundle. This bundle provides all the common operations (such as file renaming, saving and deleting) and it's tightly integrated with Doctrine ORM, MongoDB ODM, PHPCR ODM and Propel.

ファイルのアップロードを自分で処理する代わりに、VichUploaderBundle コミュニティ バンドルの使用を検討できます。このバンドルは、すべての一般的な操作 (ファイルの名前変更、保存、削除など) を提供し、Doctrine ORM、MongoDB ODM、PHPCR ODM、および Propel と緊密に統合されています。

Imagine that you have a Product entity in your application and you want to add a PDF brochure for each product. To do so, add a new property called brochureFilename in the Product entity:

アプリケーションに Product エンティティがあり、各製品の PDF パンフレットを追加したいとします。これを行うには、Product エンティティに brochureFilename という名前の新しいプロパティを追加します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Entity/Product.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

class Product
{
    // ...

    #[ORM\Column(type: 'string')]
    private $brochureFilename;

    public function getBrochureFilename()
    {
        return $this->brochureFilename;
    }

    public function setBrochureFilename($brochureFilename)
    {
        $this->brochureFilename = $brochureFilename;

        return $this;
    }
}

Note that the type of the brochureFilename column is string instead of binary or blob because it only stores the PDF file name instead of the file contents.

The next step is to add a new field to the form that manages the Product entity. This must be a FileType field so the browsers can display the file upload widget. The trick to make it work is to add the form field as "unmapped", so Symfony doesn't try to get/set its value from the related entity:

次のステップは、Productentity を管理するフォームに新しいフィールドを追加することです。ブラウザーがファイルアップロードウィジェットを表示できるように、これは FileType フィールドである必要があります。それを機能させる秘訣は、フォームフィールドを「マップされていない」ものとして追加することです。そのため、Symfony は関連するエンティティからその値を取得/設定しようとしません:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// src/Form/ProductType.php
namespace App\Form;

use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ...
            ->add('brochure', FileType::class, [
                'label' => 'Brochure (PDF file)',

                // unmapped means that this field is not associated to any entity property
                'mapped' => false,

                // make it optional so you don't have to re-upload the PDF file
                // every time you edit the Product details
                'required' => false,

                // unmapped fields can't define their validation using annotations
                // in the associated entity, so you can use the PHP constraint classes
                'constraints' => [
                    new File([
                        'maxSize' => '1024k',
                        'mimeTypes' => [
                            'application/pdf',
                            'application/x-pdf',
                        ],
                        'mimeTypesMessage' => 'Please upload a valid PDF document',
                    ])
                ],
            ])
            // ...
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Product::class,
        ]);
    }
}

Now, update the template that renders the form to display the new brochure field (the exact template code to add depends on the method used by your application to customize form rendering):

ここで、フォームをレンダリングするテンプレートを更新して、新しいパンフレットフィールドを表示します (追加する正確なテンプレート コードは、フォームのレンダリングをカスタマイズするためにアプリケーションで使用される方法によって異なります)。
1
2
3
4
5
6
7
8
{# templates/product/new.html.twig #}
<h1>Adding a new product</h1>

{{ form_start(form) }}
    {# ... #}

    {{ form_row(form.brochure) }}
{{ form_end(form) }}

Finally, you need to update the code of the controller that handles the form:

最後に、フォームを処理するコントローラーのコードを更新する必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use App\Form\ProductType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\String\Slugger\SluggerInterface;

class ProductController extends AbstractController
{
    #[Route('/product/new', name: 'app_product_new')]
    public function new(Request $request, SluggerInterface $slugger)
    {
        $product = new Product();
        $form = $this->createForm(ProductType::class, $product);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            /** @var UploadedFile $brochureFile */
            $brochureFile = $form->get('brochure')->getData();

            // this condition is needed because the 'brochure' field is not required
            // so the PDF file must be processed only when a file is uploaded
            if ($brochureFile) {
                $originalFilename = pathinfo($brochureFile->getClientOriginalName(), PATHINFO_FILENAME);
                // this is needed to safely include the file name as part of the URL
                $safeFilename = $slugger->slug($originalFilename);
                $newFilename = $safeFilename.'-'.uniqid().'.'.$brochureFile->guessExtension();

                // Move the file to the directory where brochures are stored
                try {
                    $brochureFile->move(
                        $this->getParameter('brochures_directory'),
                        $newFilename
                    );
                } catch (FileException $e) {
                    // ... handle exception if something happens during file upload
                }

                // updates the 'brochureFilename' property to store the PDF file name
                // instead of its contents
                $product->setBrochureFilename($newFilename);
            }

            // ... persist the $product variable or any other work

            return $this->redirectToRoute('app_product_list');
        }

        return $this->renderForm('product/new.html.twig', [
            'form' => $form,
        ]);
    }
}

Now, create the brochures_directory parameter that was used in the controller to specify the directory in which the brochures should be stored:

ここで、パンフレットを保存するディレクトリを指定するためにコントローラで使用されたパンフレット_ディレクトリ パラメータを作成します。
1
2
3
4
5
# config/services.yaml

# ...
parameters:
    brochures_directory: '%kernel.project_dir%/public/uploads/brochures'

There are some important things to consider in the code of the above controller:

上記のコントローラーのコードには、考慮すべき重要な点がいくつかあります。
  1. In Symfony applications, uploaded files are objects of the UploadedFile class. This class provides methods for the most common operations when dealing with uploaded files;
    Symfony アプリケーションでは、アップロードされたファイルは、UploadedFile クラスのオブジェクトです。このクラスは、アップロードされたファイルを処理する際の最も一般的な操作のメソッドを提供します。
  2. A well-known security best practice is to never trust the input provided by users. This also applies to the files uploaded by your visitors. The UploadedFile class provides methods to get the original file extension (getClientOriginalExtension()), the original file size (getSize()) and the original file name (getClientOriginalName()). However, they are considered not safe because a malicious user could tamper that information. That's why it's always better to generate a unique name and use the guessExtension() method to let Symfony guess the right extension according to the file MIME type;
    よく知られているセキュリティのベスト プラクティスは、ユーザーが提供する入力を決して信頼しないことです。これは、訪問者がアップロードしたファイルにも適用されます。 UploadedFile クラスには、元のファイル拡張子 (getClientOriginalExtension())、元のファイル サイズ (getSize())、および元のファイル名 (getClientOriginalName()) を取得するメソッドが用意されています。 .そのため、一意の名前を生成し、guessExtension() メソッドを使用して、Symfony がファイルの MIME タイプに応じて適切な拡張子を推測できるようにする方が常に優れています。

You can use the following code to link to the PDF brochure of a product:

次のコードを使用して、製品の PDF パンフレットにリンクできます。
1
<a href="{{ asset('uploads/brochures/' ~ product.brochureFilename) }}">View brochure (PDF)</a>

Tip

ヒント

When creating a form to edit an already persisted item, the file form type still expects a File instance. As the persisted entity now contains only the relative file path, you first have to concatenate the configured upload path with the stored filename and create a new File class:

既に保持されているアイテムを編集するフォームを作成する場合、ファイル フォーム タイプは依然として Fileinstance を想定しています。永続化されたエンティティには相対ファイル パスのみが含まれるようになったため、最初に、構成されたアップロード パスを格納されたファイル名と連結し、新しい File クラスを作成する必要があります。
1
2
3
4
5
6
use Symfony\Component\HttpFoundation\File\File;
// ...

$product->setBrochureFilename(
    new File($this->getParameter('brochures_directory').'/'.$product->getBrochureFilename())
);

Creating an Uploader Service

To avoid logic in controllers, making them big, you can extract the upload logic to a separate service:

コントローラーのロジックを回避して大きくするために、アップロードロジックを別のサービスに抽出できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/Service/FileUploader.php
namespace App\Service;

use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;

class FileUploader
{
    private $targetDirectory;
    private $slugger;

    public function __construct($targetDirectory, SluggerInterface $slugger)
    {
        $this->targetDirectory = $targetDirectory;
        $this->slugger = $slugger;
    }

    public function upload(UploadedFile $file)
    {
        $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
        $safeFilename = $this->slugger->slug($originalFilename);
        $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();

        try {
            $file->move($this->getTargetDirectory(), $fileName);
        } catch (FileException $e) {
            // ... handle exception if something happens during file upload
        }

        return $fileName;
    }

    public function getTargetDirectory()
    {
        return $this->targetDirectory;
    }
}

Tip

ヒント

In addition to the generic FileException class there are other exception classes to handle failed file uploads: CannotWriteFileException, ExtensionFileException, FormSizeFileException, IniSizeFileException, NoFileException, NoTmpDirFileException, and PartialFileException.

一般的な FileException クラスに加えて、失敗したファイルのアップロードを処理する他の例外クラスがあります: CannotWriteFileException、ExtensionFileException、FormSizeFileException、IniSizeFileException、NoFileException、NoTmpDirFileException、および PartialFileException。

Then, define a service for this class:

次に、このクラスのサービスを定義します。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
# config/services.yaml
services:
    # ...

    App\Service\FileUploader:
        arguments:
            $targetDirectory: '%brochures_directory%'

Now you're ready to use this service in the controller:

これで、コントローラーでこのサービスを使用する準備が整いました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Controller/ProductController.php
namespace App\Controller;

use App\Service\FileUploader;
use Symfony\Component\HttpFoundation\Request;

// ...
public function new(Request $request, FileUploader $fileUploader)
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        /** @var UploadedFile $brochureFile */
        $brochureFile = $form->get('brochure')->getData();
        if ($brochureFile) {
            $brochureFileName = $fileUploader->upload($brochureFile);
            $product->setBrochureFilename($brochureFileName);
        }

        // ...
    }

    // ...
}

Using a Doctrine Listener

The previous versions of this article explained how to handle file uploads using Doctrine listeners. However, this is no longer recommended, because Doctrine events shouldn't be used for your domain logic.

この記事の以前のバージョンでは、Doctrine リスナーを使用してファイルのアップロードを処理する方法を説明しました。ただし、ドメイン ロジックに Doctrine イベントを使用するべきではないため、これは推奨されなくなりました。

Moreover, Doctrine listeners are often dependent on internal Doctrine behavior which may change in future versions. Also, they can introduce performance issues unwillingly (because your listener persists entities which cause other entities to be changed and persisted).

さらに、Doctrine リスナーは多くの場合、将来のバージョンで変更される可能性のある内部の Doctrine の動作に依存しています。また、パフォーマンスの問題を不本意に持ち込む可能性があります (リスナーがエンティティを永続化すると、他のエンティティが変更および永続化されるため)。

As an alternative, you can use Symfony events, listeners and subscribers.

別の方法として、Symfony のイベント、リスナー、およびサブスクライバーを使用できます。