Featured image of post SymfonyのバリデーションでmimeTypeのチェックがうまく動かないケースに遭遇したお話

SymfonyのバリデーションでmimeTypeのチェックがうまく動かないケースに遭遇したお話

Twitter ツイート Hatena Bookmark ブックマーク

※今回はSymfony3.4で問題が発生したお話です。

SymfonyでValidationを使う場合は大体DoctrineのEntityクラスにAnnotationを使って設定するやり方が一般的です。
しかし、時にはEntityを使わないForm(FormType)を使うことがあります。
Validationの設定に関しては「symfony validation処理の方法」という記事に詳しくかかれているので、そちらを読むのをおすすめします。

今回Formに直接Validationを設定するやりかたでCSVアップロード用のFormTypeを作成していましたが、mimeTypeのValidationをやろうとしたらうまく動きませんでした。

validation条件でmimeTypeをtext/csv設定しているのにtext/plainとして判定される

なぜかFormTypeでFileバリデーションでmimeTypeをtext/csv設定しているのにtext/plainとして判定されるちょっとよくわからない現象に遭遇しました。 普通にFileでmimeTypeをtext/csvにしてるだけです。 特に問題内容に見えます。

 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
<?php

namespace App\Form\Type;


use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Validator\Constraints as Assert;


class CsvUploadType extends AbstractType
{
    /**
     * @var DecoderInterface
     */
    private $decoder;

    /**
     * CsvFileType constructor.
     * @param DecoderInterface $decoder
     */
    public function __construct(DecoderInterface $decoder)
    {
        $this->decoder = $decoder;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('csv', FileType::class,[
            'label' => 'csvファイル',
            'constraints' => [
                new Assert\File([
                    'mimeTypes' => 'text/csv',
                ]),
            ],
            'error_bubbling' => true,
        ]);

        $builder->addModelTransformer(new CallbackTransformer(function($data){
            return $data;
        }, function(array $data) {
            /** @var UploadedFile $csvFile */
            $csvFile = $data['csv'];
            return $this->decoder->decode(file_get_contents($csvFile->getRealPath()),'csv');
        }));
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(['translation_domain' => false]);
    }

}

原因はFileinfoMimeTypeGuesserでのmimeTypeの判定だった・・・。

FileのmimeTypeのvalidationが実際にどこで行われているのか。
これはFileValidatorクラスのここの部分で実行されています。
validator/FileValidator.php

ここで実行されているgetMimeTypeメソッドですがこれは
最終的にFileinfoMimeTypeGuesser::guess()が呼ばれています。
http-foundation/FileinfoMimeTypeGuesser.php

このときに渡されるfilepathがテンポラリディレクトリに格納されているアップロードされたファイルなんですよね・・・。
つまりファイル名に拡張子がない状態になります。

例:

1
/private/var/folders/tn/2mr79vzx7pq5ff29tb15p90r0000gn/T/phpD9xCri

ということはFileinfoMimeTypeGuesserはCSVファイルをtext/plainと判定してしまいます・・・。

解決方法

いくつか方法は思いつきましたが、自分でValidatorを作ることにしました。
今回は管理画面でのCSVアップロードを想定しているので、UploadedFile::getClientMimeType()をつかったカスタムバリデーションを作成しました。

src/Validator/Constraints/ClientMimeType

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php

namespace App\Validator\Constraints;


use Symfony\Component\Validator\Constraint;

class ClientMimeType extends Constraint
{
    public $mimeTypes = array();
    public $mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.';
}

src/Validator/Constraints/ClientMimeType/ClientMimeTypeValidator

 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
<?php

namespace App\Validator\Constraints;


use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class ClientMimeTypeValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if ($value instanceof UploadedFile && $value->isValid()) {
            $mimeTypes = (array) $constraint->mimeTypes;
            $mime = $value->getClientMimeType();
            foreach ($mimeTypes as $mimeType) {
                if ($mimeType === $mime) {
                    return;
                }
            }

            $this->context->buildViolation($constraint->mimeTypesMessage)
                ->setParameter('{{ type }}', $mime)
                ->setParameter('{{ types }}', implode(', ', $mimeTypes))
                ->addViolation();
        }
    }

}
comments powered by Disqus
Built with Hugo
テーマ StackJimmy によって設計されています。