Help us understand the problem. What is going on with this article?

【Laravel】CSVインポート機能の実装(ブラウザ編)

More than 1 year has passed since last update.

想定として、社内システム(Laravel製)のブラウザ上からCSVファイル(先頭行にID欄がある形)をアップロードして、そのまま登録OR更新処理を実行する、といった機能の実装です。
(ゆえにセキュリティ関連はおざなりになってると思います。あんまりじしんがない)
今回は新規登録と更新でそれぞれ別に処理をおこなっていますが、それぞれ前処理が不要ならば同一の処理でも大丈夫だとおもいます。
(というかバリデーションも同じなら分けずに一緒にすべきですね)

改めて見ると汚いコードでリファクタリングしがいがありまくりなんですけど、まあ未来の自分の参考になればいいかなぁとか思ったりなかったりって感じです

Controller

CsvImportController.php

CsvImportController.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use App\Http\Controllers\Controller;
use App\Http\Test;

class CsvImportController extends Controller
{
    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        // アップロードファイルに対してのバリデート
        $validator = $this->validateUploadFile($request);

        if ($validator->fails() === true){
            return redirect('/form')->with('message', $validator->errors()->first('csv_file'));
        }

        // CSVファイルをサーバーに保存
        $temporary_csv_file = $request->file('csv_file')->store('csv');

        $fp = fopen(storage_path('app/') . $temporary_csv_file, 'r');

        // 一行目(ヘッダ)読み込み
        $headers = fgetcsv($fp);

        $column_names = [];

        // CSVヘッダ確認
        foreach ($headers as $header) {
            $result = Test::retrieveTestColumnsByValue($header, 'SJIS-win');
            if ($result === null) {
                fclose($fp);
                Storage::delete($temporary_csv_file);
                return redirect('/form')
                    ->with('message', '登録に失敗しました。CSVファイルのフォーマットが正しいことを確認してださい。');
            }
            $column_names[] = $result;
        }

        $registration_errors_list = [];
        $update_errors_list       = [];
        $i = 0;

        // TODO:サイズが大きいCSVファイルを読み込む場合、この処理ではメモリ不足になる可能性がある為改修が必要になる
        while ($row = fgetcsv($fp)) {

            // Excelで編集されるのが多いと思うのでSJIS-win→UTF-8へエンコード
            mb_convert_variables('UTF-8', 'SJIS-win', $row);
            $is_registration_row = false;

            foreach ($column_names as $column_no => $column_name) {

                // idがなければ登録、あれば更新と判断
                if ($column_name === 'id' && $row[$column_no] === '') {
                    $is_registration_row = true;
                }

                // 新規登録か更新かのチェック
                if($is_registration_row === true){
                    if ($column_name !== 'id') {
                        $registration_csv_list[$i][$column_name] = $row[$column_no] === '' ? null : $row[$column_no];
                    }
                } else {
                    $update_csv_list[$i][$column_name] = $row[$column_no] === '' ? null : $row[$column_no];
                }

            }

            // バリデーションチェック
            $validator = \Validator::make(
                $is_registration_row === true ? $registration_csv_list[$i] : $update_csv_list[$i],
                $this->defineValidationRules(),
                $this->defineValidationMessages()
            );

            if ($validator->fails() === true) {
                if ($is_registration_row === true) {
                    $registration_errors_list[$i + 2] = $validator->errors()->all();
                } else {
                    $update_errors_list[$i + 2] = $validator->errors()->all();
                }
            }

            $i++;
        }

        // バリデーションエラーチェック
        if (count($registration_errors_list) > 0 || count($update_errors_list) > 0) {
            return redirect('/form')
                ->with('errors', ['registration_errors' => $registration_errors_list, 'update_errors' => $update_errors_list]);
        }

        // 既存更新処理
        if (isset($update_csv_list) === true) {
            foreach ($update_csv_list as $update_csv) {
                // ~更新用の処理~
                if ($this->fill($update_csv)->save() === false) {
                    return redirect('/form')
                        ->with('message', '既存データの更新に失敗しました。(新規登録処理は行われずに終了しました)');
                }
            }
        }

        // 新規登録処理
        if (isset($registration_csv_list) === true) {
            foreach ($registration_csv_list as $registration_csv) {
                // ~登録用の処理~
                if ($this->fill($registration_csv)->save() === false) {
                    return redirect('/form')->with('message', '新規登録処理に失敗しました。');
                }
            }
        }

        return redirect('/form')->with('message', 'CSV登録が完了しました。' );
    }

    /**
     * アップロードファイルのバリデート
     * (※本来はFormRequestClassで行うべき)
     *
     * @param Request $request
     * @return Illuminate\Validation\Validator
     */
    private function validateUploadFile(Request $request)
    {
        return \Validator::make($request->all(), [
                'csv_file' => 'required|file|mimetypes:text/plain|mimes:csv,txt',
            ], [
                'csv_file.required'  => 'ファイルを選択してください。',
                'csv_file.file'      => 'ファイルアップロードに失敗しました。',
                'csv_file.mimetypes' => 'ファイル形式が不正です。',
                'csv_file.mimes'     => 'ファイル拡張子が異なります。',
            ]
        );
    }

    /**
     * バリデーションの定義
     *
     * @return array
     */
    private function defineValidationRules()
    {
        return [
            // CSVデータ用バリデーションルール
            'content' => 'required',
        ];
    }

    /**
     * バリデーションメッセージの定義
     *
     * @return array
     */
    private function defineValidationMessages()
    {
        return [
            // CSVデータ用バリデーションエラーメッセージ
            'content.required' => '内容を入力してください。',
        ];
    }
}

Model

Test.php
(ControllerがFatなので本来ならここに処理かくべきですね・・・)

Test.php
class Test extends Model
{
    /**
     * CSVヘッダ項目の定義値があれば定義配列のkeyを返す   
     *
     * @param string $header
     * @param string $encoding
     * @return string|null
     */
    public static function retrieveTestColumnsByValue(string $header ,string $encoding)
    {
        // CSVヘッダとテーブルのカラムを関連付けておく
        $list = [
            'content' => "内容",
            'memo'    => "備考",
        ];

        foreach ($list as $key => $value) {
            if ($header === mb_convert_encoding($value, $encoding)) {
                return $key;
            }
        }
        return null;
    }
}

View

form.php(CSVアップロード画面)

form.php
@if(Session::has('message'))
メッセージ{{ session('message') }}
@endif

@if (is_array($errors))
<div class="flushComment">
    CSVインポートエラーが発生しました以下の内容を確認してください<br>
    @if (count($errors['registration_errors']) > 0)
        [対象のデータ新規登録]
        <ul>
        @foreach ($errors['registration_errors'] as $line => $columns)
            @foreach ($columns as $error)
            <li>{{ $line }}行目{{ $error }}</li>
            @endforeach
        @endforeach
        </ul>
    @endif
    @if (count($errors['update_errors']) > 0)
        [対象のデータ編集登録]<br>
        <ul>
        @foreach ($errors['update_errors'] as $line => $columns)
            @foreach ($columns as $error)
            <li>{{ $line }}行目{{ $error }}</li>
            @endforeach
        @endforeach
        </ul>
    @endif
</div>
@endif

<form action="/form/import-csv" method="post" enctype="multipart/form-data" id="csvUpload">
<input type="file" value="ファイルを選択" name="csv_file">
{{ csrf_field() }}
<button type="submit">インポート</button>
</form>

ルーティング

web.php

Route::get('/form', function () {
    return view('form');
});

Route::post('form/import-csv', 'CsvImportController@store');
sola-msr
ミセ*゚ー゚)リ そんな事言われてもウチ、ポン・デ・ライオンやし
andfactory
Smartphone Idea Companyとして、人々の生活に「&(アンド)」を届ける。
https://andfactory.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした