Laravelの入力検証 (Csv, Excelファイル)


概要

ファイルアップロードの際、CSVファイルやExcelファイルのチェックに、
下記ルールを使ったが、期待する動作を得られなかったので調べた結果をメモします。

※ 単純にファイル拡張子(ファイル名)をチェックしたかった...

・Rule mimes
 https://readouble.com/laravel/6.x/ja/validation.html#rule-mimes

チェック処理 (mimes)

下記のコードが該当のルール処理のようです。

vendor/laravel/framework/src/Illuminate/Validation/Concerns/ValidatesAttributes.php
public function validateMimes($attribute, $value, $parameters)
{
    ...
    return $value->getPath() !== '' && in_array($value->guessExtension(), $parameters);
}

この処理を見ると...
拡張子をチェックには「 $value->guessExtension() 」が使われている。

vendor/symfony/http-foundation/File/File.php
/**
 * ...
 *
 * @see MimeTypes
 * @see getMimeType()
 */
public function guessExtension()
{
    return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null;
}

このメソッドは、「MimeTypes」クラスが使われている。

vendor/symfony/mime/MimeTypes.php
/**
 * ...
 *
 * @see Resources/bin/update_mime_types.php
 */
private static $map = [
    'text/plain' => ['txt', 'text', 'conf', ... ],
    ...,
    'text/csv' => ['csv'],
    ...
];

そのため、実際は、ファイル名の拡張子ではく、mimeTypeで判定している。

mimeTypeが「text/plain」のCSVファイルの場合は、「txt」と判定される。

               ↓↓↓

純粋にファイル名の拡張子でチェックして欲しい要件の場合には、都合が良くない。
xlabという拡張子も、xlsxと判定されてしまった...

対応方法

期待する動作が得られないので、独自ルールを作るしかないかとやってみる...

独自ルール追加を追加する

下記コマンドで、独自ルールのクラスを追加する。

$ php artisan make:rule FileExtension

追加したクラスには、以下のように記述してみました。

App/Rules/FileExtension.php
<?php
namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class FileExtension implements Rule
{
    /**
     * @var array
     */
    private $extensions;

    /**
     * @param array $extensions
     */
    public function __construct(array $extensions)
    {
        $this->extensions = $extensions;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed   $value
     * @return bool
     */
    public function passes($attribute, $value): bool
    {
        return $value->getPath() !== '' && in_array($value->getClientOriginalExtension(), $this->extensions);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message(): string
    {
        return sprintf('The file extension is invalid. (%s)', implode(',', $this->extensions));
    }
}

独自ルールを登録する

独自ルールを、Validatorに登録しておいた方が使うのに楽そうです。

新規に作るValidatorServiceProviderをconfigに追加する。

config/app.php
'providers' => [
    ...,
    App\Providers\ValidatorServiceProvider::class, // ← 追記する
],

ValidatorServiceProviderクラスを追加する。

App/Providers/ValidatorServiceProvider.php
<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;
use App\Rules\FileExtension;

class ValidatorServiceProvider extends ServiceProvider
{
    public function register()
    {
        //
    }

    public function boot()
    {
        Validator::extend('extensions', function ($attribute, $value, $parameters) {
            $rule = app()->makeWith(FileExtension::class, [
                'extensions' => $parameters
            ]);
            return $rule->passes($attribute, $value);
        });
    }
}

独自ルールを使ってみる

formRequestクラスにextensionsというルールを定義してみる。

App/Http/Requests/UploadRequest.php
<?php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UploadRequest extends FormRequest
{
    private $mimetypes = [
        'text/csv',                                                          // csv
        'text/plain',                                                        // csv
        'application/vnd.ms-excel',                                          // excel ( .xls OFFICE2007より過去 )
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // excel ( .xlsx OFFICE 2007以降 )
    ];

    private $extensions = [
        'csv',
        'xls',
        'xlsx',
    ];

    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'file' => [
                'required',
                'file',
                'mimetypes:' . implode(',', $this->mimetypes),
                'extensions:' . implode(',', $this->extensions),
            ],
        ];
    }

    public function messages(): array
    {
        return [
            'file.required'   => 'ファイルを指定してください。',
            'file.mimetypes'  => '指定されたファイル形式はアップロードできません。',
            'file.extensions' => sprintf('ファイルの拡張子が間違っています(%s)。', implode(',', $this->extensions)),
        ];
    }
}

これで、問題ないようでした。

まとめ

とりあえず、独自実装で要件を満たす動作を構築できました。

ひとまず、学習のために作ってみましたが...

独自で作らなくても、簡単に実装できる方法(ライブラリなど)があれば、
そっちを使った方がいいかもですね(^^;)

参考資料

以上