CSVでDBにインサートできるボタンを作る

PHPcsv,DB,Laravel,PHP,アップロード,ダウンロード

+2

環境: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)

次に実際の動きを実装します。
ルーティングで設定したコントローラーにメソッドを追加していきます。

ダウンロード

大きく分けて以下の流れになっています。

  1. Customerモデルからオブジェクトを持ってくる。
  2. ヘッダーを読み込む。
  3. 1のオブジェクトから必要な情報を配列に入れ直す
  4. 出力

となっています。

    //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);

結合文字列が文字化けするバグを改善するメソッドです。

アップロード

アップロードはさらに長いので、頑張ってついてきてください!

大きく分けて以下の流れになっています。

  1. 配列に置き換えたファイルのヘッダーを見て、ズレがないか確認。
  2. IDで既存データを探す。あれば更新。なければ新規作成
  3. 必須項目が足りていない場合はログを出力
  4. 結果を出力

となっています。

    //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)

おまけ、数字をランク分けしてあげる処理。

以上です!!コメントお待ちしてます!

+2

Posted by riku