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

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

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

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

## チェック処理 (mimes)
下記のコードが該当のルール処理のようです。

```php: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()` 」が使われている。

```php:vendor/symfony/http-foundation/File/File.php
/**
 * ...
 *
 * @see MimeTypes
 * @see getMimeType()
 */
public function guessExtension()
{
    return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null;
}
```
このメソッドは、「MimeTypes」クラスが使われている。

```php: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`と判定されてしまった...

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

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

```bash
$ php artisan make:rule FileExtension
```
追加したクラスには、以下のように記述してみました。

```php: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に追加する。

```php:config/app.php
'providers' => [
    ...,
    App\Providers\ValidatorServiceProvider::class, // ← 追記する
],
```
ValidatorServiceProviderクラスを追加する。

```php: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`というルールを定義してみる。

```php: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)),
        ];
    }
}
```
これで、問題ないようでした。

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

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

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

## 参考資料
* [Laravel 6.x バリデーション](https://readouble.com/laravel/6.x/ja/validation.html#rule-mimes)
* [Laravel でファイルアップロードでcsv の拡張子が取得できない](https://teratail.com/questions/106888)
* [Laravelで200%お世話になるであろうUploadedFileクラスを見てみよう](https://qiita.com/mashirou_yuguchi/items/14d3614173c114c30f02)
* [How to pass parameters in Laravel Rule?](https://stackoverflow.com/questions/50174006/how-to-pass-parameters-in-laravel-rule)

以上