• WEB

Laravelでファイルのアップロードと格闘した話

Laravelでファイルのアップロードと格闘した話

ご無沙汰しております。きんです。

唐突ですが、ファイルアップロードの小話を一つ。
is_uploaded_file()だのmove_uploaded_file()だのと、
初めてファイルのアップロード処理について習った時から、
こいつはめんどくさいなぁ。いけ好かんなぁ。と感じていたのですが、
何気に割とどんなアプリでも付いて回ってきます。
ついこの間もちょっと画像ファイル勢と格闘しましたので、
その時のお話をしようと思います。

Laravelとファイルアップロード

事の発端は、とあるアプリケーションのテスト中に
画像がアップロードできない場合があると判明した事から。
でも、普通にアップロードできる場合もあるし、
何が悪いんだかさっぱり分からん。
誰が悪いの?私?私がいけないの?
もう駄目だ。おうちに帰りたい。
と、完全に弱気になっていたのですが、
エイチさんの力を借りてようやく(やっぱり私が悪かった事が)判明。

どうやらphp.iniのpost_max_sizeとupload_max_filesizeの
上限を超えている画像をアップロードしようとした場合に、
サーバー側でひっかかって
/tmpに一時ファイルが作成できなかったのが原因との事。
post_max_sizeとupload_max_filesizeの値を上げると、
たしかに問題なくアップロードできました。

とはいえ、仮にそこの値を上げたとしても、
更にそれを超過するファイルサイズのものが
アップロードされないとも限らない。
laravelではそんな場合に
TokenMismatchExceptionのエラーが出てしまうのだが、
果たしてそのままでいいのだろうか。とまたひと悶着。。。

で、こうなりました。

upload_max_filesizeは超過してるけど、post_max_sizeは超過してない場合

upload_max_filesizeを超過してしまうと、
サーバー側で/tmpに一時ファイルを作ってくれません。
ついでに、Requestのvalidateチェックも見てくれません。
TestRequest
でもコントローラー側ではエラーが拾えたので、
そこからリクエストに渡してあげる事は出来ました。

入力画面からフォームをpostしたら、
まずはフォーム全体のエラーチェックはせずに、
画像の処理を行います。
その理由は、タイトルや名前などの別の項目のエラーで
入力画面にリダイレクトした時に、
再度画像をアップロードさせる手間をはぶくためです。

  1. upload_max_filesizeにひっかからなくても、画像のリサイズ用のライブラリの上限にひっかかる場合があるので、そのチェック。
  2. 問題なければリサイズしてアプリケーション内に一時ファイル作成。
  3. ファイルのエラーが出ている場合(upload_max_filesize超過)は、TestRequestにアップロードがエラーになっている事を伝えるフラグを追加。
  4. フォーム全体のエラーチェックのために、TestRequest呼び出し。

の順番で処理をしています。

↓TestController.php


public function editConfirm(Request $request)
{
    # バリデート前に画像格納
    //メイン画像
    if ($request->hasFile('img')) {
        // 画像の幅と高さの上限チェック
        $companyMainImageSizeFlag = checkImageWidthHeight($request->file('img')->getRealPath(), config('const_common.validate_limits.upload_image.width'), config('const_common.validate_limits.upload_image.height'));
        if ($companyMainImageSizeFlag) {
            $tmpFileName = ImageUpload::createTrimTmpFile($request->file('img')->getRealPath(), config('const_common.img_folder.tmp.path'));
            $request->merge(['img_tmp' => $tmpFileName, 'img_tmp_url' => config('const_common.img_folder.tmp.url') . $tmpFileName]);
        }
    } elseif (!empty($request->input('img'))) {
        if ($request->input('img')->getError()) {
            $request->merge(['img_update_flag' => false]);
        }
    }

    # フォームのバリデート
    $testRequest = new TestRequest();
    $rules = $testRequest->rules();
    $errorMsg = $testRequest->messages();
    $result = $this->validate($request, $rules, $errorMsg);

↓TestRequest.php


class TestRequest extends Request
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title'                => 'required',
            'address'              => 'required',
            'tel'                  => 'tel_check',
            'url'                  => 'url',
            'img'                  => 'mimes:jpeg,gif,png|max:' . config('const_common.validate_limits.upload_image.size'),
            'img_update_flag'      => 'sometimes|accepted',
        ];
    }

    /**
     * エラーメッセージカスタム
     */
    public function messages()
    {
        return [
            'title.required'               => 'タイトルは必須です',
            'address.required'             => '住所は必須です',
            'tel.tel_check'                => '電話番号が不正です',
            'url.url'                      => 'このURLは不正です(http://またはhttps://から記述してください)',
            'img.mimes'                    => '選択できる画像はJPEG・GIF・PNG形式のみです',
            'img.max'                      => '1MB以下のファイルを選択してください',
            'img_update_flag.accepted'     => '1MB以下のファイルを選択してください',
        ];
    }
}

post_max_sizeの値を超過した場合

post_max_sizeを超過すると、
/tmpに一時ファイルが作れないのぉ~(ノд・。) グスン
なんて可愛い感じではなくなります。
サーバーさんは、すべてのフォームの値を放り捨てます。
必須項目だとか、
どんなに一生懸命作ったtextareaの文章も、問答無用でポイ。
ついでに、LaravelのCSRF対策用のトークンもポイ。
そのため、laravelでpost_max_sizeをオーバーしたデータ量をpostすると
TokenMismatchExceptionのエラーが発生する訳です。

ちなみに、tokenエラー用のMiddlewareである
\App\Http\Middleware\VerifyCsrfToken
らへんをいじれば、例外を投げる処理から、別の処理に変える事はできます。
ただ、今回はpostのデータ量のチェックなど、
ちょっと本来のVerifyCsrfTokenの処理とは意味合いとは違うので、
外に出した方が良いとのご指摘を頂きましたので、
新しくMiddlewareを作成して、
そこでデータ量のチェック→フォームの入力画面にリダイレクト
という処理を行わせる事になりました。

というわけで新たにMiddleware作成。
Kernelに登録。


protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\RedirectIfExceedPostMaxSize::class,//post_max_sizeチェック用
        \App\Http\Middleware\VerifyCsrfToken::class,
        \App\Http\Middleware\TrimInput::class,
    ],

↑VerifyCsrfTokenより前に読み込むようにしないと意味なしです。
次、ソース。


namespace App\Http\Middleware;

use Closure;
class RedirectIfExceedPostMaxSize
{
    /**
     * php.iniのpost_max_sizeとフォームのデータサイズの比較をするページ
     *
     * @var array
     */
    protected $include = [
        [
            'url' => 'test/edit/confirm',
        ],
        [
            'url'      => 'test/edit/save',
            'redirect' => 'test/edit/{id}',
        ],
    ];

    /**
     * php.iniのpost_max_sizeとフォームのデータサイズの比較をして、サイズオーバーの場合にはリダイレクト
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $checkUrl = $this->checkIncludeUrl($request);//現在のページが上の$includeに登録されているページかチェック
        if ($checkUrl) {
            if(!empty($_SERVER['CONTENT_LENGTH'])) {//$_SERVER['CONTENT_LENGTH']はpostで送られてきたデータ量です。
                if ($_SERVER['CONTENT_LENGTH'] > returnBytes(ini_get('post_max_size'))) {//$_SERVER['CONTENT_LENGTH']とphp.iniに登録されているpost_max_sizeの値を比較
                    $validator = \Validator::make([],[]);
                    $validator->getMessageBag()->add('upload_error', '不正な値が入力されました。最初からやり直してください。');//_SERVER['CONTENT_LENGTH']が超過していた場合には、バリデートのエラーメッセージを登録

                    $redirectUrl = $this->createRedirectUrl($request, $checkUrl);//リダイレクト先のURLを作ります。$includeにredirect先が登録されている場合は、そこ。なければrefererです。
                    return redirect($redirectUrl)->withErrors($validator);//リダイレクトして、エラーメッセージ表示
                }
            }
        }
        return $next($request);
    }

(結構無理やり感がすごいけど、一応)出来た~(ゝω・)v
テストフォーム

ちなみに、
「出来ました~♪」とエイチさんにご報告した所、
「(こんな無理やりな感じなら)やっぱり普通にtokenエラー出しとけば良かっ…」
と言われた所で、
「やめて下さい。その続きは聞きたくありません。」
と現実逃避の末現在に至りますので、
もし上記ソースを参考にして頂くような事がありました際には、
くれぐれもその辺をご留意ください。

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

きんが最近書いた記事

WRITERS POSTS もっと見る

他にもこんな記事が読まれています!

  • WEB
  • マーケティング
  • サーバー・ネットワーク
  • ライフスタイル
  • お知らせ