いよいよ給与明細系処理の作りこみ
明細データをCSVでアップロードしてデータベースへ保存します
環境設定他関連記事はこちら
Laravel + Vue + Vuetify で業務サイト作ってみる
アップロード用画面の作成
給与明細のCSVをアップロードする画面を作成
明細が何年何月のものか、対象年月を指定してアップロードできるようにしときます
対象年月の指定はVuetifyのDatePickerを使ってみます
https://vuetifyjs.com/ja/components/date-pickers
アップロードの実処理は以前に作成したCSVアップロード用のボタンコンポーネントを再利用です
<template>
<v-flex>
<!-- 1段目 :検索?登録? -->
<v-card xs12 class="m-3 px-3" v-show="!csvList">
<v-card-title class="title">
<v-icon class="pr-2">{{ $route.meta.icon }}</v-icon> {{ $route.meta.name }} {{ /* 給与明細 */ }}
【{{ activeTab }}】
<v-spacer></v-spacer>
<v-btn block depressed
:outline="activeTab == '検索'"
:color="activeTab == '検索' ? 'primary' : 'blue-gray'"
@click="activeTab = '検索'"
>
<v-icon class="pr-1">search</v-icon>検索
</v-btn>
<v-btn block depressed
:outline="activeTab != '検索'"
:color="activeTab != '検索' ? 'primary' : 'blue-gray'"
@click="activeTab = '登録'"
>
<v-icon class="pr-1">cloud_upload</v-icon>登録
</v-btn>
</v-card-title>
<v-layout row wrap class="mx-3 my-2">
<!-- 共通: 対象年月(カレンダーで月を選択)-->
<v-flex xs2 md4 lg3>
<v-menu
ref="menu"
v-model="menu"
:return-value.sync="target.ym"
:close-on-content-click="false"
:nudge-right="20"
lazy transition="scale-transition" offset-y full-width max-width="290px" min-width="290px"
show-current="true"
>
<v-text-field
readonly
clearable
autofocus
slot="activator"
v-model="target.ym"
label="対象年月"
placeholder="明細の対象年月を選択"
:hint="'明細の対象年月を選択' + (activeTab=='検索'?'(指定なしで全期間対象)':'')"
></v-text-field>
<v-date-picker v-model="target.ym" type="month" no-title scrollable locale="ja">
<v-spacer></v-spacer>
<v-btn flat color="primary" @click="menu = false">Cancel</v-btn>
<v-btn flat color="primary" @click="$refs.menu.save(target.ym)">OK</v-btn>
</v-date-picker>
</v-menu>
</v-flex>
</v-layout>
<v-card-actions class="pb-2">
<!-- 登録ボタン -->
<csv-upload
v-show="activeTab == '登録'"
:updata="target"
color="primary"
icon="cloud_upload"
@reload="reload"
@axios-logout="$emit('axios-logout')"
url="/api/admin/payslip/upload"
></csv-upload>
<!-- 検索ボタン -->
<v-btn block flat
v-show="activeTab == '検索'"
@click="getCsvPayslip"
color="primary"
>
<v-icon class="pr-2">search</v-icon>検索
</v-btn>
</v-card-actions>
</v-card>
<!-- 2段目 :CSVリスト表示 -->
<v-card xs12 class="m-3 px-3" v-if="csvList">
<v-card-title class="title">
<v-icon class="pr-2" @click="closeList">{{ $route.meta.icon }}</v-icon> {{ csvTitle }} {{ /* 給与明細CSV検索 */ }}
<v-spacer></v-spacer>
<v-spacer></v-spacer>
<v-text-field
v-model="search"
prepend-icon="search"
label="絞り込み表示"
single-line
hide-details
clearable
></v-text-field>
<v-icon @click="closeList" class="accent ml-5" dark>close</v-icon>
</v-card-title>
<v-data-table
:headers="headers"
:items="tabledata"
:pagination.sync="pagination"
:rows-per-page-items='[5,10,20,{"text":"All","value":-1}]'
:loading="loading"
:search="search"
class="elevation-0 p-1"
>
<v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>
<template slot="items" slot-scope="props">
<td class="text-xs-center" xs1>{{ (props.index + 1) + (pagination.page - 1) * pagination.rowsPerPage }}</td>
<template v-for="n in (headers.length - 1)">
<td :class="'text-xs-' + headers[n].align" style="white-space: nowrap;" v-text="props.item[headers[n].value]"></td>
</template>
</template>
</v-data-table>
</v-card>
</v-flex>
</template>
<script>
import csv_upload from './CsvUpload.vue'
export default {
name: 'PayslipComponent',
components: {
'csv-upload': csv_upload,
},
props: {
},
data: () => ({
loading: false,
search: '',
pagination: { sortBy: 'created_at', descending: true, },
// csv list
tabledata: [],
headers: [
{ align: 'center', sortable: false, text: 'No', },
{ align: 'center', sortable: true, text: '年月', value: 'ym' },
{ align: 'center', sortable: true, text: '状態', value: 'status' },
{ align: 'center', sortable: true, text: 'CSV-ID', value: 'id' },
{ align: 'center', sortable: true, text: '対象者数', value: 'line' },
{ align: 'center', sortable: true, text: 'エラー数', value: 'error' },
{ align: 'left', sortable: true, text: 'ファイル名', value: 'filename' },
{ align: 'left', sortable: true, text: '登録日時', value: 'created_at' },
{ align: 'left', sortable: true, text: '公開日時', value: 'published_at' },
{ align: 'center', sortable: false, text: 'アクション', },
],
menu: false,
activeTab: '検索',
csvList: false,
csvTitle: '',
target: {
ym: '',
},
}),
created() {
if (process.env.MIX_DEBUG) console.log('Payslip Component created.')
this.initialize()
},
methods: {
initialize() {
// 初期表示時は現在年月を設定しておく
this.target.ym = moment().format('YYYY-MM').toString()
},
reload() {
if (process.env.MIX_DEBUG) console.log('Payslip Component reload')
this.getCsvPayslip()
},
initList() {
if (process.env.MIX_DEBUG) console.log('Payslip Component initList')
this.setCsvTitle()
this.tabledata = []
this.csvList = true
},
closeList() {
this.tabledata = []
this.csvList = false
},
setCsvTitle() {
if (process.env.MIX_DEBUG) console.log('Payslip Component set csv title')
this.csvTitle = ''
if (this.target.ym) this.csvTitle += this.target.ym + ' '
else this.csvTitle += '全期間 '
},
// 登録済みCSVのリストをサーバから取得する
getCsvPayslip() {
if (process.env.MIX_DEBUG) console.log('Payslip Component getCsvPayslip')
// 初期化
this.initList()
// 検索パラメータ設定
var params = new URLSearchParams()
params.append('ym', (this.target.ym ? this.target.ym : ''))
// 検索要求
this.loading = true
axios.post('/api/admin/payslip/csvlist', params)
// 検索結果[正常]
.then( function (response) {
this.loading = false
if (process.env.MIX_DEBUG) console.log(response)
if (response.data.data) {
this.tabledata = response.data.data
this.setStatus()
}
}.bind(this))
// 検索結果[異常]
.catch(function (error) {
this.loading = false
console.log(error)
if (error.response) {
if ([401, 419].includes(error.response.status)) {
this.$emit('axios-logout')
}
else {
alert('ERROR ' + error.response.status + ' ' + error.response.statusText)
}
}
else {
alert('ERROR ' + error)
}
}.bind(this))
},
setStatus() {
var wk = '不明'
for (var i=0; i<this.tabledata.length; i++) {
if (this.tabledata[i].status) {
wk = '不明'
if (this.tabledata[i].status == 0) { wk = '非公開' }
else if (this.tabledata[i].status == 1) { wk = '公開' }
this.tabledata[i].status = wk
}
}
},
},
}
</script>
アップロード後に自動的に検索して一覧表示するようにしてあります
しかし。。長いな。。
画面イメージ
アップロードデータを確認できるように同じ画面でタブ切り替えで検索もできるようにしてます
タブといってもボタンイメージで表示
給与明細管理のリンク作成
アップロード用の画面を作成したので呼び出せるようにリンクを作成しておきます
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
//
import example_component from '../components/ExampleComponent.vue'
import admin_component from '../components/AdminComponent.vue'
import r_link from '../components/RouterLink.vue'
//
Vue.component('example-component', example_component)
Vue.component('admin-component', admin_component)
Vue.component('r-link', r_link)
//
import home from '../components/HomeComponent.vue'
import admin_user from '../components/Admin/UserComponent.vue'
★ import admin_payslip from '../components/Admin/PayslipComponent.vue'
export default new Router({
mode: 'history',
routes: [
{ path: '/admin/user', name: 'admin_user', component: admin_user, meta: {name: '社員管理', icon: 'supervisor_account'}},
{ path: '/home', name: 'home', component: home, meta: {name: 'ホーム', icon: 'home'}},
★ { path: '/admin/payslip',name: 'admin_payslip', component: admin_payslip, meta: {name: '給与明細', icon: 'fa-file-invoice-dollar'}},
{ path: '*', redirect: '/home' },
],
})
作成したコンポーネントを読み込むように設定して、
~~~
<v-navigation-drawer v-model="drawer" clipped fixed app >
<v-list dense>
<r-link linkname='home'></r-link>
<r-link linkname='admin_user'></r-link>
★ <r-link linkname='admin_payslip'></r-link>
</v-list>
</v-navigation-drawer>
~~~
リンクを追加します
アイコンフォント追加
給与明細用の良いアイコンがなかったので FontAwesome を導入
追加しておきます
npm install --save-dev @fortawesome/fontawesome-free
追加したフォントを読み込むように設定
~~~
// Font Awesome
import '@fortawesome/fontawesome-free/css/all.css'
~~~
FontAwesome はアイコン名に [ fa- ] をつけて指定すると利用できます
スタイル指定も可能
fas fa-xxxx とすると Solid(塗りつぶし)
far fa-xxxx とすると Regular(線・標準の太さ)
などなど
ただし Freeの無料版では使えないものもあるのでサイトで確認がよろしいかと
FontAwesomeのサイトの左メニューで FREE を選んでおくと無料版のアイコンのみが表示されるようになります
CSV アップロードの修正
アップロードする際に明細の「年月」情報をくっつけてアップできるようにちょっと修正です
<template>
<v-btn block flat
:color="color ? color : 'primary'"
:loading="csvuploading"
:disabled="csvuploading"
@click="$refs.input_csvup.click()"
>
<v-icon dark class="mr-1">{{icon ? icon : 'cloud_upload' }}</v-icon> {{title ? title : 'CSV アップロード'}}
<v-progress-circular slot="csvuploading" indeterminate color="primary" dark></v-progress-circular>
<input
name="file"
:value="csvupfile"
type="file"
style="display: none"
ref="input_csvup"
accept=".csv,.txt"
:multiple="multiple"
@change="onFilePicked"
>
<upload-dialog ref="UploadDialog"></upload-dialog>
</v-btn>
</template>
<script>
import upload_dialog from './CsvUploadDialog.vue'
export default {
name: 'CSVUpload',
components: {
'upload-dialog': upload_dialog,
},
props: {
color: String,
icon: String,
title: String,
url: String,
multiple: String,
updata: Object,
},
data: () => ({
csvuploading: false,
csvupfile: null,
}),
created() {
if (process.env.MIX_DEBUG) console.log('CSV Upload Btn created.')
},
methods: {
// アップロードボタンを押すと "<input type=file" を実行してファイル選択ダイアログを開く
// ファイル選択ダイアログで何らかの動き(change)があると関数(onFilePicked)を呼ぶ
// 複数ファイル選択の指示(multple)があった場合に備えてファイル1つずつを非同期で処理しとく(未完)
async onFilePicked(e) {
if (process.env.MIX_DEBUG) console.log('CSV Upload onFilePicked')
const files = e.target.files
if(files[0] == undefined) return // ファイル未選択は何もしない
// ファイルを1つずつ送信 - 非同期処理
for (var i=0 ; i<files.length ; i++) {
if (process.env.MIX_DEBUG) console.log("FILE:" + files[i].name + " (" + files[i].size + " byte)")
await this.csvupload(files[i])
}
},
csvupload(file) {
return new Promise((resolve, reject) => {
if (process.env.MIX_DEBUG) console.log('CSV Upload csv upload')
var config = {
headers: {'Content-Type': 'multipart/form-data'}
}
// 送信ファイル設定
var formData = new FormData()
formData.append('csvfile', file)
// 追加送信パラメータ設定(指定があれば)
★ if (this.updata) {
for (let key in this.updata) {
formData.append(key, this.updata[key])
}
}
// アップロード実行
this.csvuploading = true
axios.post(this.url, formData, config)
// アップロード 正常
.then( function (response) {
this.csvuploading = false
if (process.env.MIX_DEBUG) console.log("CSV Upload success")
if (process.env.MIX_DEBUG) console.log(response.data)
this.resultDialog(file, response.data.import)
}.bind(this))
// アップロード 異常
.catch(function (error) {
this.csvuploading = false
if (process.env.MIX_DEBUG) console.log("CSV Upload error")
if (process.env.MIX_DEBUG) console.log(error)
if (error.response) {
if ([401, 419].includes(error.response.status)) {
this.$emit('axios-logout')
}
else if ([422].includes(error.response.status)) {
if (process.env.MIX_DEBUG) console.log("CSV Upload error 422")
if (process.env.MIX_DEBUG) console.log(error.response.data.errors)
if (error.response.data.errors) {
for (let key in error.response.data.errors) {
if (error.response.data.errors[key]) {
alert(file.name + ' : ' + error.response.data.errors[key])
}
}
}
}
else {
alert('ERROR ' + error.response.status + ' ' + error.response.statusText)
}
}
else {
alert('ERROR ' + error)
}
}.bind(this))
return resolve(file)
})
},
resultDialog(file, data) {
var res = []
// ERROR
res.error = ''
if (data.errors) {
res.error = this.getResult(data.errors, 'データエラー: ', ' 箇所')
}
// UPDATE
res.result = ''
if (data.update) {
res.result = this.getResult(data.update, '更新 : ', ' レコード')
res.result += '\n'
}
// INSERT
if (data.insert) {
res.result += this.getResult(data.insert, '新規 : ', ' レコード')
}
// 一覧を再読み込み
this.$emit('reload')
// ダイアログを開く
this.$refs.UploadDialog.open(file, res)
},
getResult(data, comment1, comment2) {
var res = comment1 + data.length + comment2 + '\n'
for (var i=0; i<data.length; i++) {
res += ' ' + data[i].line + ' 行目 : ' + data[i].message + '\n'
}
return res
},
},
}
</script>
長いけど、 ★ のあたりに注目
Laravel 側のコントローラを作成
Laravel側の処理も作ります
まずはコントローラの作成
php artisan make:controller PayslipController
スケルトン作成したコントローラに処理を作りこんでいきます
CSVファイルのアップロードとアップしたCSVファイルの一覧表示処理をば作りこみ
エラー時に Jsonで返すために JsonResponse を使ったりもしてます
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use App\User;
use App\CsvPayslip;
use App\Payslip;
use App\Facades\Csv;
use App\Http\Requests\UploadCsvFile;
class PayslipController extends Controller
{
public function csvlist(Request $request)
{
Log::Debug(__CLASS__.':'.__FUNCTION__, $request->all());
// INIT
$where = array();
// 検索条件確認 - 対象年月(未指定の場合は全期間を対象とする)
$ym = $this -> getYM($request -> all(), false);
if (is_object($ym)) { return $ym; } // 期間エラー
if ($ym) {
$where['ym'] = $ym;
}
if ($where) {
$CsvPayslips = CsvPayslip::where($where)->get();
}
else {
$CsvPayslips = CsvPayslip::all();
}
return ['data' => $CsvPayslips];
}
public function upload(UploadCsvFile $request)
{
Log::Debug(__CLASS__.':'.__FUNCTION__, $request->all());
// CSV をパース
$file = $request -> file('csvfile');
$rows = $this -> parse_csv($file);
if (is_object($rows)) { return $rows; }
// 対象年月を取得 (数字以外はすべて削除してから取得)
$ym = $this -> getYM($request -> all());
if (is_object($ym)) { return $ym; }
// INIT
$ret = array();
$header_columns = 0;
// 1行ずつ処理 - ループ処理でデータエラーがあっても処理は継続
foreach ($rows as $line => $value) {
Log::Debug(__CLASS__.':'.__FUNCTION__.' '. ($line + 1) .'/'. count($rows) .' : '. print_r($value, true));
// INIT
$wk = '';
$data = array();
// 1行目ならヘッダー情報を保存
if ($line == 0) {
// CSV 情報を保存
$csv_payslip = $this -> insertCsvPayslip( $ym, $file, $value );
if (!$csv_payslip) {
return new JsonResponse(['errors' => [ 'database' => '内部エラー csv payslip insert error']], 422);
}
// ヘッダーの項目数を取得
$header_columns = count($value);
continue; // ヘッダーはそのまま次の行へ
}
// CSV行データのカラム数チェック(ヘッダーのカラム数と違ったらエラー)
if (count($value) != $header_columns) {
$wk = 'ヘッダーの項目数('.$header_columns.')と行の項目数('.count($value).')が一致しません';
$ret['errors'][] = ['line' => $line, 'message' => $wk];
$data['error'] = $wk;
Log::Debug(__CLASS__.':'.__FUNCTION__.' '. ($line + 1) .'/'. count($rows) .' : '. $wk);
}
// CSVの最初のカラムはユーザのログインIDで固定:ユーザを検索
$data['loginid'] = trim($value['No0']); // 対象者ログインID
$user = User::where('loginid', $data['loginid']) -> first();
// CSVに指定のユーザの存在チェック(ユーザが存在しなければエラー)
if (!$user) {
$wk = '該当社員(id: '.$data['loginid'] .')が見つかりませんでした';
$ret['errors'][] = ['line' => $line, 'message' => $wk];
$data['error'] = $wk;
Log::Debug(__CLASS__.':'.__FUNCTION__.' '. ($line + 1) .'/'. count($rows) .' : '. $wk);
}
// CSV行データ保存設定
$data['csv_id'] = $csv_payslip['id']; // CsvPayslip id
$data['line'] = $line; // CSV行番号
$data['ym'] = $ym; // 明細年月
$data['data'] = $value; // CSV行データ
$data['user_id'] = $user['id']; // 対象者内部ID
// CSVの2番目のカラムは明細のファイル名:指定があればファイル名を保存
if (trim($value['No1']) != '') {
$data['filename'] = trim($value['No1']);
}
// CSV行データ保存
Payslip::create($data);
// CSV行カウンタ設定 - エラーあり?正常?
if (isset($data['error'])) {
$csv_payslip -> error ++;
} else {
$csv_payslip -> line ++;
$ret['insert'][] = ['line' => $line, 'message' => $user['name'] ." の明細を登録しました."];
}
} // 1行ずつ処理
// CSV行データ読み込み結果保存(正常行数、エラー数)
$csv_payslip -> save();
// 結果を戻す
return ['import' => $ret];
}
private function parse_csv($file)
{
Log::Debug(__CLASS__.':'.__FUNCTION__);
// 拡張子チェックがうまく動かないことがあるので独自で実施
// -- https://api.symfony.com/3.0/Symfony/Component/HttpFoundation/File/UploadedFile.html
if ($file ->getClientOriginalExtension() != 'csv') {
Log::Debug(__CLASS__.':'.__FUNCTION__.' File Name: '. $file ->getClientOriginalName());
Log::Debug(__CLASS__.':'.__FUNCTION__.' File Extension: '. $file ->getClientOriginalExtension());
Log::Debug(__CLASS__.':'.__FUNCTION__.' ClientMimeType: '. $file ->getClientMimeType());
Log::Debug(__CLASS__.':'.__FUNCTION__.' MimeType: '. $file ->getMimeType());
return new JsonResponse(['errors' => [ 'csvfile' => 'CSVファイルを指定してください']], 422);
}
// CSV をパース
try {
$rows = Csv::parse($file, true);
} catch (\Exception $e) {
Log::Debug(__CLASS__.':'.__FUNCTION__.' parse Exception : '. $e -> getMessage());
return new JsonResponse(['errors' => [
'csvfile' => 'CSVファイルの読み込みでエラーが発生しました',
'exception' => $e -> getMessage()
]]
, 422);
}
// CSVレコード情報を戻す
return $rows;
}
private function insertCsvPayslip($ym, $file, $value)
{
Log::Debug(__CLASS__.':'.__FUNCTION__);
// CsvPayslip テーブル保存準備
$data = array();
$data['ym'] = $ym;
$data['filename'] = $file -> getClientOriginalName();
$data['header'] = $value;
// CsvPayslip テーブル保存
$csv_payslip = CsvPayslip::create($data);
// 結果を戻す
return $csv_payslip;
}
private function getYM($request, bool $required = true)
{
Log::Debug(__CLASS__.':'.__FUNCTION__);
// 対象年月を取得 (数字以外はすべて削除してから取得)
if (array_key_exists('ym', $request)) {
$ym = preg_replace("/\D/", "", $request['ym']);
}
// 対象年月が未指定ならエラー
else {
return new JsonResponse(['errors' => ['target_ym1' => '対象年月を指定してください']], 422);
}
// 対象年月のチェックは必須?
if ($required) {
// 対象年月は 2010~2099年01~12月であること
if (! preg_match('/^20([1-9]{1}[0-9]{1})(0[1-9]{1}|1[0-2]{1})$/', $ym)) {
return new JsonResponse(['errors' => ['target_ym2' => '対象年月は2010年01月~2099年12月で指定してください']], 422);
}
}
// 対象年月を戻す
return $ym;
}
}
アップロードしたCSVファイルの情報は App\CsvPayslip に保存
CSVファイルの中身は1行毎に App\Payslip に保存してます
CSVファイルの1カラム目は社員のログインIDを指定
CSVファイルの2カラム目は明細のファイル名を指定できるようにしておきました(訂正版などファイル名に変えたい場合に利用予定)
明細の実データは3カラム目以降にプログラム上は特に制限なしにしてます(上限はDBに入る長さまでかな?)
CSVファイルに記載のログインIDが見つからなかったりのエラーが発生しても、処理は継続して全行処理するようにしてあります
ちょっと冗長な書き方の部分も多いけど、とりあえず動くので現状はこれで良しとしときます
ルートの登録
コントローラを作ったのでルートの登録もしておきます
~~~
// Admin
Route::group( ['middleware' => ['auth', 'can:admin']], function() {
~~~
// Payslip
Route::post('/api/admin/payslip/csvlist', 'PayslipController@csvlist')->name('admin/payslip/csvlist');
Route::post('/api/admin/payslip/upload', 'PayslipController@upload')->name('admin/payslip/upload');
});
~~~
CSVファイルのアップロード用と、CSVファイルの一覧表示用です
CSVの文字コード自動判定
以前に作成したCSVサービスを改修して文字コードも自動判定できるようにしておきます(簡易判定だけど。。)
実験したところ BOM付きの場合にうまく動かなかったので手動でBOM削除も追加しときました
CSV読み込むときに削除してくれると嬉しいんだけどな(Goodby CSVでやってくれないのかしら。。)
ついでに「カラム名」でのアクセスではなくて、カラム番号でのアクセスも可能にしておきます
(明細のカラム名やカラム数は不定にしちゃったから。。)
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Response;
use Goodby\CSV\Import\Standard\Lexer;
use Goodby\CSV\Import\Standard\Interpreter;
use Goodby\CSV\Import\Standard\LexerConfig;
class Csv
{
/**
* CSVダウンロード
* @param array $csv_data
* @param array $csv_header
* @param string $csv_filename
* @return \Illuminate\Http\Response
*/
public function download($csv_data, $csv_header, $csv_filename)
{
// ヘッダー指定あれば1行目にヘッダーをセット
if (count($csv_header) > 0) {
array_unshift($csv_data, $csv_header);
}
// ストリームでレスポンス ::
// vendor/laravel/framework/src/Illuminate/Routing/ResponseFactory.php
// streamDownload($callback, $name = null, array $headers = [], $disposition = 'attachment')
return response() -> streamDownload(
function () use($csv_data) {
$file = new \SplFileObject('php://output', 'w');
foreach ($csv_data as $row) {
$file->fputcsv($row);
}
},
$csv_filename,
array('Content-Type' => 'application/octet-stream')
);
}
/**
* CSVアップロード(CSV 取り込み)
* @param file $file
* @param bool $headless
* @return rows
**/
public function parse($file, $headless = false)
{
// Goodby CSVのconfig設定
$config = new LexerConfig();
$interpreter = new Interpreter();
$lexer = new Lexer($config);
★ // 文字コード判定 - ファイルの最初から1MB分の文字列から判定
$str = file_get_contents($file, NULL, NULL, 0, 1024*1024);
$enc = mb_detect_encoding($str,['UTF-8', 'SJIS-win', 'SJIS', 'JIS', 'Unicode', 'ASCII'], true);
if ($enc === false) { $enc = 'SJIS-win'; } // 文字コード自動判定できなかったら。。SJISにしとく
Log::Debug(__CLASS__.':'.__FUNCTION__.' CSV FILE CHARSET : '. $enc);
// CharsetをUTF-8に変換
$config->setToCharset("UTF-8");
$config->setFromCharset($enc);
// CSVデータをパース
$rows = array();
try {
$interpreter->addObserver(function(array $row) use (&$rows) {
$rows[] = $row;
});
$lexer->parse($file, $interpreter);
} catch (\Exception $e) {
throw $e;
}
// 1行ずつ処理
$data = array();
foreach ($rows as $key => $value) {
// 最初の行はヘッダー - BOMが含まれている場合にうまく削除されていないようなので手動で削除する
if($key == 0) {
//$header = $value;
★ $header = preg_replace("/^". pack('H*', 'EFBBBF') ."/", '', $value);
if (!$headless) continue; // ヘッダー要らなければ continue
}
// 配列化 - 2行目以降はヘッダーに沿って配列に
// - 元のCSV列番号[No999]でも取得できるようにしておく
foreach ($value as $k => $v) {
★ if ($headless) { $data[$key]['No'.$k] = $v; }
else { $data[$key][$header[$k]] = $v; }
}
}
// CSVを配列で戻す
Log::Debug(__CLASS__.':'.__FUNCTION__.' data ', $data);
return $data;
}
}
動作確認
いろいろ作ったのでコンパイルして実行
管理者でログインして
npm run dev
php artisan serve --host=172.16.0.100 --port=8000
登録画面を表示
CSVファイルを用意して
loginid,filename,あああ
maple_user,,1111
maple_admin,,2222
test,test,test
アップロード
正常にアップロードされました
CSVファイルをUTF-8 BOMあり で用意してアップロード
(ファイル内容はさっきと同じ)
※ アップロード完了後は指定年月で自動で一覧を開きます
一応データベースの内容も確認しとく
明細CSV情報
mysql> select * from csv_payslips;
+----+--------+--------+----------------------------------------+-------------------------------------------------+------+-------+--------------+---------------------+---------------------+------------+
| id | ym | status | filename | header | line | error | published_at | created_at | updated_at | deleted_at |
+----+--------+--------+----------------------------------------+-------------------------------------------------+------+-------+--------------+---------------------+---------------------+------------+
| 1 | 201901 | 0 | 201901-明細CSV(SJIS).csv | eyJpdiI6ImJyaDRoK1VCbU9zYWhMRUNYzYzNjFlYiJ9 | 2 | 1 | NULL | 2019-01-04 07:34:31 | 2019-01-04 07:34:32 | NULL |
| 2 | 201901 | 0 | 201901-明細CSV(UTF-8 BOM有).csv | eyJpdiI6IkpIb0RlTjEzSFdMaTQ2cUNY2E3ZTNjZGM4YiJ9 | 2 | 1 | NULL | 2019-01-04 07:34:43 | 2019-01-04 07:34:44 | NULL |
+----+--------+--------+----------------------------------------+-------------------------------------------------+------+-------+--------------+---------------------+---------------------+------------+
2 rows in set (0.00 sec)
ちゃんと登録したCSVの情報が登録されているようです
ヘッダー情報も暗号化して保存されてますね(内容は見やすいように短く削ってます)
明細CSV詳細情報
mysql> select * from payslips;
+----+--------+------+--------+--------+---------+-------------+-------------------------------------------------+----------+----------+---------------------------------------------------------+----------------+---------------------+---------------------+------------+
| id | csv_id | line | ym | status | user_id | loginid | data | filename | download | error | delete_user_id | created_at | updated_at | deleted_at |
+----+--------+------+--------+--------+---------+-------------+-------------------------------------------------+----------+----------+---------------------------------------------------------+----------------+---------------------+---------------------+------------+
| 1 | 1 | 1 | 201901 | 0 | 2 | maple_user | eyJpdiI6IlA5THA2dWwNDk3ZjdjNDE1ZTM0NzgxMTY3In0= | NULL | 0 | NULL | NULL | 2019-01-04 07:34:32 | 2019-01-04 07:34:32 | NULL |
| 2 | 1 | 2 | 201901 | 0 | 1 | maple_admin | eyJpdiI6IlMxZmxmbj5N2JmNzkzMmM3YzY2Y2U4NCJ9 | NULL | 0 | NULL | NULL | 2019-01-04 07:34:32 | 2019-01-04 07:34:32 | NULL |
| 3 | 1 | 3 | 201901 | 0 | NULL | test | eyJpdiI6IjhHSUMzbV9 | test | 0 | 該当社員(id: test)が見つかりませんでした | NULL | 2019-01-04 07:34:32 | 2019-01-04 07:34:32 | NULL |
| 4 | 2 | 1 | 201901 | 0 | 2 | maple_user | eyJpdiI6IkpLYWd6eDhZDBjOTEzZDkwZDMyNmE1MTkifQ== | NULL | 0 | NULL | NULL | 2019-01-04 07:34:43 | 2019-01-04 07:34:43 | NULL |
| 5 | 2 | 2 | 201901 | 0 | 1 | maple_admin | eyJpdiI6Iks0YWZqS02YTQ4OTIwN2FlODIyNzA0MDAifQ== | NULL | 0 | NULL | NULL | 2019-01-04 07:34:44 | 2019-01-04 07:34:44 | NULL |
| 6 | 2 | 3 | 201901 | 0 | NULL | test | eyJpdiI6IkpmUjh6Tl9 | test | 0 | 該当社員(id: test)が見つかりませんでした | NULL | 2019-01-04 07:34:44 | 2019-01-04 07:34:44 | NULL |
+----+--------+------+--------+--------+---------+-------------+-------------------------------------------------+----------+----------+---------------------------------------------------------+----------------+---------------------+---------------------+------------+
6 rows in set (0.00 sec)
こちらもCSVの行毎の情報が登録されているようです
データ内容もちゃんと暗号化して保存されてますね(内容は見やすいように短く削ってます)
以上
明細のCSVファイルをアップロードしてDBへ保存するところまで完成です
今回もソースはこちら
https://github.com/u9m31/u9m31/tree/step11