CSVでDBにインサートできるボタンを作る
環境:PHP Laravel
今回はcustomerというテーブルにcsvアップロードによる更新機能と、DBのダウンロード機能を作ります。
フロント部分(HTML)
フロントはHTML(Blade)になります。Bootstrapを適用後の画像となっていますので実際とは違うかもしれません。
ダウンロード機能
リンクを貼り付けるだけなので、そのままです。
<p class="download"><a href="{{ route('customer.download') }}">全件CSV出力</a></p>
アップロード機能

ファイルを選択というボタンとアップロードボタンを作ります。nameの部分がファイルの一時的な名前になります。
<div class="upload">
<form action="{{ route('customer.upload') }}" method="POST" enctype="multipart/form-data">
{{ csrf_field() }}
<div class="customer-upload">
//ファイルを選択の部分
<input type="file" id="customer_data" name="customer_data" class="form-control-file" required/>
</div>
<div>
<input type="submit" value="アップロード" class="btn btn-primary"/>
</div>
</form>
</div>
ルーティング(rutes.php)
上を読み込んでもエラーになると思います。route()のnameを解決できないとエラーになってしまいます。
POSTとGETを作ります。POSTはCSVをアップロードして、DBを書き換えます。GETはCSVのダウンロードメソッドです。
//'/customer/upload'というディレクトリにアクセスした時、 'Admin\AdminController'にある'customerStore' というメソッドにアクセスする。
//それに'customer.upload'という名前をつける。
Route::get('/customer/download', 'Admin\AdminController@customerDownload')->name('customer.download');
Route::post('/customer/upload', 'Admin\AdminController@customerStore')->name('customer.upload');
これで一旦は表示できることが確認できるはずです。
コントローラー(AdminController.php)
次に実際の動きを実装します。
ルーティングで設定したコントローラーにメソッドを追加していきます。
ダウンロード
大きく分けて以下の流れになっています。
- Customerモデルからオブジェクトを持ってくる。
- ヘッダーを読み込む。
- 1のオブジェクトから必要な情報を配列に入れ直す
- 出力
となっています。
//Admin\AdminController.php
/**
* 顧客一覧のCSVダウンロード
*
* @return void
*/
public function customerDownload()
{
$customers = Customer::isNotDeleted()->sortDescWithId()->get();
$stream = fopen('php://temp', 'r+b');
$header = config('customerListCsv.csv_header');
fputcsv($stream, $header);
$customerColumn = [];
foreach ($customers as $customer) {
$customerColumn = [
$customer->customer_id,
$customer->name,
'="' .$customer->postcard. '"',
'="' . $customer->phone . '"',
$customer->address1,
$customer->address2,
$customer->tdb_grade,
$customer->credit_rank_change,
$customer->memo,
$customer->created,
$customer->updated,
];
fputcsv($stream, $customerColumn);
}
rewind($stream);
$csv = str_replace(PHP_EOL, "\r\n", stream_get_contents($stream));
$csv = $this->replaceDakuten($csv);
$csv = mb_convert_encoding($csv, 'SJIS-win', 'UTF-8');
$file_name = date('Y_m_d H_i_s').'_顧客一覧'.'.csv';
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="'. $file_name . '"',
];
return response()->make($csv, 200, $headers);
}
/**
* 結合文字列を一文字に変換し返却
*
* @param str
* @return str
*/
public function replaceDakuten($csv)
{
$csv = substr(json_encode($csv), 1, -1);
$csv = str_replace("\u3099", '\u309b', $csv);
$csv = str_replace("\u309a", '\u309c', $csv);
$csv = json_decode(sprintf('"%s"', $csv));
$csv = mb_convert_kana($csv, "aks", "utf-8");
$csv = mb_convert_kana($csv, "KV", "utf-8");
return $csv;
}
$header = config('customerListCsv.csv_header');
ここではヘッダーを定義している配列を呼び出しています。
'="' . $customer->phone . '"',
これは0から始まる文字列について、0が除去されてしまうので="03000000″となるように付加しています。
replaceDakuten($csv);
結合文字列が文字化けするバグを改善するメソッドです。
アップロード
アップロードはさらに長いので、頑張ってついてきてください!
大きく分けて以下の流れになっています。
- 配列に置き換えたファイルのヘッダーを見て、ズレがないか確認。
- IDで既存データを探す。あれば更新。なければ新規作成
- 必須項目が足りていない場合はログを出力
- 結果を出力
となっています。
//Admin\AdminController.php
/**
* CSVファイルのアップロードで顧客データを更新
*
* @param Request $request
* @return void
*/
public function customerStore(Request $request)
{
ini_set('max_execution_time', 300);
$customers = $this->csvToArray($_FILES["customer_data"]["tmp_name"]);
$checkHeaders = $this->checkCsvHeader($customers[0]);
if ($checkHeaders) {
$row = 0;
$errorLog = "";
foreach ($customers as $customer) {
if ($row !== 0) {
$customerData = Customer::find($customer[0]);
$check = $this->checkCustomerRequire($customer);
if ($customerData && $check) {
$user_info = $this->saveCustmer($customerData, $customer);
$this->log->add(3, $user_info['user_id'], '', $customer[0], 1, 2, 5);
} elseif (!$customerData && $check) {
$customerData = new Customer;
$user_info = $this->saveCustmer($customerData, $customer);
$this->log->add(3, $user_info['user_id'], '', $customerData->customer_id, 1, 1, 5);
} else {
$errorLog = $errorLog.$customer[0]."の更新・登録ができませんでした。<br/>";
}
}
$row ++;
}
if (!empty($errorLog)) {
return '一部で登録エラーが発生しました。必須項目などを確認してください。<br/>' . $errorLog . '<br/>
<a href="/customer/">顧客一覧に戻る</a>';
} else {
return redirect('customer')
->with('message', '登録が完了しました');
}
} else {
return 'CSVアップロードに失敗しました。CSVファイルを確認してください。<br/>
<a href="/customer/">顧客一覧に戻る</a>';
}
}
/**
* csvをUTF-8にデコードして配列にして返却
*
* @return array
*/
public function csvToArray($file)
{
$data = file_get_contents($file);
$data = mb_convert_encoding($data, 'UTF-8', 'UTF-8, sjis-win');
$temp = tmpfile();
$csv = array();
fwrite($temp, $data);
rewind($temp);
while (($data = fgetcsv($temp, 0, ",")) !== false) {
$csv[] = $data;
}
fclose($temp);
return $csv;
}
/**
* 顧客データを更新してユーザーを返却
*
* @param customerData object
* @param customer array
* @return array
*/
public function saveCustmer($customerData, $customer)
{
$customerData->name = $customer[1];
$customerData->postcard = $customer[2];
$customerData->phone = $customer[3];
$customerData->address1 = $customer[4];
$customerData->address2 = $customer[5];
$customerData->tdb_grade = $customer[6];
$customerData->credit_rank_change = $this->getCreditRank($customer[6]);
$customerData->updated = date("Y-m-d H:i:s");
$customerData->save();
$user_info = session('user');
return $user_info;
}
/**
* TODO:CSVヘッダーのチェック
*
* @return bool
*/
private function checkCsvHeader($headers)
{
return preg_match('/顧客ID/', $headers[0])
&& preg_match('/顧客名/', $headers[1])
&& preg_match('/郵便番号/', $headers[2])
&& preg_match('/電話番号/', $headers[3])
&& preg_match('/住所/', $headers[4])
&& preg_match('/ビル名/', $headers[5])
&& preg_match('/TDB評点/', $headers[6])
&& preg_match('/与信ランク/', $headers[7])
&& preg_match('/メモ/', $headers[8])
&& preg_match('/作成日/', $headers[9])
&& preg_match('/更新日/', $headers[10]);
}
/**
* 顧客データの必須項目を満たしているか
*
* @return bool
*/
private function checkCustomerRequire($customer)
{
$valid = new Valid();
$config = array(
'1' => array(
array('isNotNull', 'customname_not_null'),
),
'2' => array(
array('isNotNull', 'postcard_not_null'),
),
'3' => array(
array('isNotNull', 'phone_not_null'),
array('isJaPhone', 'phone_number_invalid'),
),
'4' => array(
array('isNotNull', 'address_not_null'),
),
'6' => array(
array('isNumber', 'is_number'),
),
);
return $valid->valid($config, $customer);
}
/**
* 与信ランク返却
*
* @param string $value
* @return string
*/
public function getCreditRank($value)
{
switch ($value) {
case $value > 60:
return "A";
case $value > 44:
return "B";
case $value > 40:
return "C";
case $value > 0:
return "D";
default:
return " ";
}
}
customerStore(Request $request)
メインで動いている子。インサートor新規作成している。
csvToArray($file)
csvから配列を作って返却している。
saveCustmer($customerData, $customer)
オブジェクトに代入してセーブ、ついでにログ出力用のセッションユーザーを返す。
checkCsvHeader($headers)
ヘッダーを取り出して文字列が一致しているか検証する。今思えば、$header = config('customerListCsv.csv_header’);こいつを使ってあげればよかったな。
checkCustomerRequire($customer)
必須項目のバリデーションをしてあげる。
getCreditRank($value)
おまけ、数字をランク分けしてあげる処理。
以上です!!コメントお待ちしてます!