想定として、社内システム(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');