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

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

July 19, 2018,
tags: symfony validator mimeType


このエントリーをはてなブックマークに追加

※今回は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にしてるだけです。 特に問題内容に見えます。

<?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がテンポラリディレクトリに格納されているアップロードされたファイルなんですよね・・・。
つまりファイル名に拡張子がない状態になります。

例:

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

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

解決方法

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

src/Validator/Constraints/ClientMimeType

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

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