Laravel5.3で、jQueryでPOST(ajax)されたcsvファイルを受け取り、 一行ずつ読み込んでインポートをする処理を実装したので、手順をまとめます。

csvファイルの文字コードはSJIS-winの想定です。 UTF-8で送られてくるとエラーとなります。

UTF-8で送られた場合でも上手く処理できるようにしたかったのですが、 エンコード周りが上手くできませんでした・・・ 誰か詳しい方がいらっしゃいましたら、コメントをお願いします。

csvの処理周りは、今回はこちらのLaravel Excelを使いました。 Laravel Excel

こちらの記事のファサードやサービスプロバイダの登録部分を行わないと 上手く動きませんので、合わせて読んでいただければと思います。 Laravel 5.3でcsvのダウンロード機能を実装

では早速インストール手順から。

インストール

コマンド実行

$ composer require maatwebsite/excel:~2.1.0

config/app.phpの編集

    'providers' => [
        ~~ 省略 ~~
        Maatwebsite\Excel\ExcelServiceProvider::class,
    ],
    'aliases' => [
        ~~ 省略 ~~
        'Excel' => Maatwebsite\Excel\Facades\Excel::class,
    ],

設定のロード、キャッシュ削除

必要であれば行ってください。

$ composer update
$ APP_ENV=local php artisan config:cache
$ composer dump-autoload

これで設定は完了です。 続いてソースコードを作成していきます。

クライアント側

html

<form id="csv_import_form"
        method="POST"
        action="/csv_import"
        enctype="multipart/form-data">
  <fieldset>
    <legend>ファイル選択</legend>
    <p>
      csvファイル:<input type="file" id="csv_file" name="csv_file">
    </p>
  </fieldset>
  <p>
    <input type="submit" id="submit_button" name="submit_button" value="インポート" disabled>
  </p>
  {{ csrf_field() }}
</form>

jQuery

二重送信防止、 ファイルを選択していない場合の「インポート」ボタンのdisabled、 送信後にformの値をリセット

などの処理を入れています。

/**
 * csvファイルの選択が変更された時に動作。
 * インポートボタンのdisabledの切り替えを行う。
 */
$('#csv_file').change(function(event) {
    var file_obj = $(this)[0].files[0];

    if (typeof(file_obj) === "undefined") {
        $('#submit_button').prop("disabled", true);
    } else {
        $('#submit_button').prop("disabled", false);
    }
});

/**
 * インポートボタンが押された時に動作。
 * サーバへcsvファイルを送り、結果を受け取り処理を行う。
 */
$('#csv_import_form').submit(function(event) {
  // HTMLでの送信をキャンセル
  event.preventDefault();

  // 操作対象のフォーム要素を取得
  var $form = $(this);

  // 送信ボタンを取得
  var $button = $form.find('submit_button');

  // 送信データ
  // $form.serialize()
  var formData = new FormData($(this).get(0));

  // 送信
  $.ajax({
    url: $form.attr('action'),
    type: $form.attr('method'),
    processData: false,
    contentType: false,
    data: formData,
    timeout: 10000,  // 単位はミリ秒

    // 送信前
    beforeSend: function(xhr, settings) {
      // ボタンを無効化し、二重送信を防止
      $button.attr('disabled', true);
    },

    // 応答後
    complete: function(xhr, textStatus) {
      // ボタンを有効化し、再送信を許可
      $button.attr('disabled', false);
    },

    // 通信成功時の処理
    success: function(result, textStatus, xhr) {
      console.log("success");
      console.log(result);
      console.log(textStatus);

      // csvファイルのファイル名を取得
      // $('#csv_file')[0].files[0].name

      // 入力値を初期化
      $form[0].reset();
      $('#submit_button').prop("disabled", true);
    },

    // 通信失敗時の処理
    error: function(xhr, textStatus, error) {
      console.log("error");
      console.log(error);
    }
  });
});

サーバ側

router

Route::post('/csv_import', 'Controller@csv_import');

controller

validateにエラーがあった場合は-1を返して、それ以外は1。 それによって読み込んだcsvファイルに何件エラーがあったのかカウントできるようにしてます。

use Illuminate\Support\Facades\Log;
use CSV;
use Validator;

class Controller extends Controller {
  ~~省略~~

  /**
    * faqのcsvインポートを行う。
    * @param Requrest $request - リクエスト
    */
   public function csv_import(Request $request) {
       Log::debug($request);

       // validate
       if (!isset($request["csv_file"])) {
           return response()->json("csv_file not found.", 400);
       }

       // csvファイルを取得
       $file = $request->file('csv_file');

       /**
         * 一行ずつ、csvの取り込みを行う
         * @param array $row - 取り込みデータ
         * @return number - 1 = 成功、 -1 = 失敗
         */
       $result = CSV::read($file, function($row) {
           Log::debug("csv_import read()");
           log::debug($row);

           // validation
           $validator = Validator::make($row, [
               'id' => 'required',
               'text' => 'required'
           ]);

           if ($validator->fails()) {
               return -1;
           }

           return 1;
       });

       Log::debug($result);
       return response()->json($result, 200);
   }
}

CSV

<?php

namespace App\Repositories;

use Response;
use Config;
use Excel;
use Illuminate\Support\Facades\Log;

class CSV {
    public function __construct() {
    }

    /**
     * CSVファイルの読み込みを行う
     * @param Illuminate\Http\UploadedFile $csv_file - csvファイル
     * @param function $callback - 一行読み込み毎に呼び出されるコールバック関数
     * @return Number 読み込んだ行数
     */
    public function read($csv_file, $callback) {
        Log::debug($csv_file->getRealPath());

        //$enc = ['UTF-8','ASCII', 'SJIS-win', 'SJIS', 'EUC-JP'];
        //mb_detect_order($enc);
        Log::debug(mb_detect_encoding($csv_file));

        /*
        if (mb_detect_encoding($csv_file) == "UTF-8") {
            Log::debug("encoding");
            // "auto" は、"ASCII,JIS,UTF-8,EUC-JP,SJIS" に展開される
            $csv_file = mb_convert_encoding($csv_file, "SJIS-win", "UTF-8");
        }
        */

        // "auto" は、"ASCII,JIS,UTF-8,EUC-JP,SJIS" に展開される
        $csv_file = mb_convert_encoding($csv_file, "SJIS-win", "UTF-8, ASCII, SJIS-win");

        $reader = Excel::load($csv_file, 'SJIS-win');

        $rows = $reader->toArray();

        $success_cnt = 0;
        $error_cnt = 0;
        foreach ($rows as $row){
            $result = $callback($row);
            if ($result == 1) {
                $success_cnt++;
            } else {
                $error_cnt++;
            }
        }
        return ["success_count" => $success_cnt, "error_count" => $error_cnt];
    }

    /**
     * CSVダウンロード
     * @param array $list
     * @param array $header
     * @param string $filename
     * @return \Illuminate\Http\Response
     */
    public function download($list, $header, $filename) {
        if (count($header) > 0) {
            array_unshift($list, $header);
        }
        $stream = fopen('php://temp', 'r+b');
        foreach ($list as $row) {
            fputcsv($stream, $row);
        }
        rewind($stream);
        $csv = str_replace(PHP_EOL, "\r\n", stream_get_contents($stream));
        $csv = mb_convert_encoding($csv, 'SJIS-win', 'UTF-8');
        $headers = array(
            'Content-Type' => 'text/csv',
            'Content-Disposition' => "attachment; filename=$filename",
        );
        return \Response::make($csv, 200, $headers);
    }
}

あとは、ControllerのCSV::readの関数内に処理を記述していくだけです。 このサンプルだと、送られてくるcsvファイルは以下のようなフォーマットを想定。

id, text
1, test1,
2, テストデータ,
3,,

これをSJISで保存して送ります。 レスポンス結果は、success_count = 2件、error_count = 1件になります。

実装中に起こった問題

日本語がfalseとなる

Log::debug();でデータを表示してみたところ、日本語の部分がfalseになってました。 要はマルチバイト文字の読み込みが上手くできていなかった。

local.DEBUG: array (
  'id' => 2.0,
  'text' => false,
)

原因は、ExcelのファイルがUTF-8で保存されていたからでした。 SJISで保存し直したら読み込めました。

local.DEBUG: array (
  'id' => 2.0,
  'text' => 'テストデータ',
)

windowsで作られることが多いことを想定して、 SJIS-winでloadを指定していたので、当然ですね。

$reader = Excel::load($csv_file->getRealPath(), 'SJIS-win');

参考

以下のサイトを参考にさせていただきました、ありがとうございました。