前回laravelでcsvインポートの記事を書いたのですが、今回はSplFileObjectを使って実装した記事になります。

前回の記事 - Laravel 5.3でcsvインポート機能を実装 csvの実装部分以外は前回の記事と同じ手順で導入可能です。

前回の記事はPHPExcelを使ったのですが、環境によっては以下のExceptionが発生します。

発生したException

Symfony\Component\Debug\Exception\FatalThrowableError: Class 'ZipArchive' not found in /var/www/vendor/phpoffice/phpexcel/Classes/PHPExcel/Reader/Excel2007.php:94
Stack trace:
#0 /var/www/vendor/maatwebsite/excel/src/Maatwebsite/Excel/Classes/FormatIdentifier.php(249): PHPExcel_Reader_Excel2007->canRead('/tmp/php2BSOj0')
#1 /var/www/vendor/maatwebsite/excel/src/Maatwebsite/Excel/Classes/FormatIdentifier.php(229): Maatwebsite\Excel\Classes\FormatIdentifier->canRead('Excel2007', '/tmp/php2BSOj0')
#2 /var/www/vendor/maatwebsite/excel/src/Maatwebsite/Excel/Classes/FormatIdentifier.php(55): Maatwebsite\Excel\Classes\FormatIdentifier->lastResort('/tmp/php2BSOj0', NULL, '')
#3 /var/www/vendor/maatwebsite/excel/src/Maatwebsite/Excel/Readers/LaravelExcelReader.php(1341): Maatwebsite\Excel\Classes\FormatIdentifier->getFormatByFile('/tmp/php2BSOj0')
#4 /var/www/vendor/maatwebsite/excel/src/Maatwebsite/Excel/Readers/LaravelExcelReader.php(777): Maatwebsite\Excel\Readers\LaravelExcelReader->_setFormat()

原因

ちょいちょい情報が古いかもしれませんが、

どうやら、zipサポートを有効にしていない設定でコンパイルしたphpだと発生する模様。 解決するには、phpを再コンパイルするか、 必要なライブラリをインストールした後、php.iniファイルにextension=zip.soを追記して webサーバを再起動する必要がありそう。

解決方法

私の環境だとphpの再コンパイルやphp.iniの編集は難しいので、 PHPExcelを使わない方法で対応しました。

調べると、SplFileObjectという便利なものがありましたので、 早速リファクタリングを実施。

SplFileObjectはUTF-8でファイルを読み込む必要があるので、エンコードをしています。 私の環境だとSJIS(windows)を使って仕事をしている人が多いので、 csvファイルがSJISでアップロードされてくる想定で作りました。

CSV.php

<?php

namespace App\Repositories;

use Response;
use Config;
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());
        Log::debug(mb_detect_encoding($csv_file));

        // tmpファイル名取得
        $file_tmp_name = "/var/tmp/csv_file.csv";

        // 文字コードを変換してファイルを置換(csvファイルはsjis-winでくる想定)
        $detect_order = 'SJIS-win,ASCII,JIS,UTF-8,CP51932';
        $buffer = file_get_contents($csv_file->getRealPath());
        if (!$encoding = mb_detect_encoding($buffer, $detect_order, true)) {
            // 文字コードの自動判定に失敗
            unset($buffer);
            throw new \RuntimeException('Character set detection failed');
        }

        // tmpファイル作成
        file_put_contents($file_tmp_name, mb_convert_encoding($buffer, 'UTF-8', $encoding));
        unset($buffer);

        // utf-8でファイル読み込み
        $rows = new \SplFileObject($file_tmp_name);
        $rows->setFlags(\SplFileObject::READ_CSV
            | \SplFileObject::READ_AHEAD             //先読み・巻き戻しで読み出す。
            | \SplFileObject::SKIP_EMPTY             //ファイルの空行を読み飛ばす。read_aheadとセットで使わないとダメ。
            | \SplFileObject::DROP_NEW_LINE          //行末の改行を読み飛ばす。
        );

        $row_cnt = 0;
        $success_cnt = 0;
        $error_cnt = 0;

        // csvファイルのデータ数分ループ
        $csv_header = array('id', 'text');
        foreach ($rows as $row) {
            // タイトル行以外(一行目がタイトル行の想定)
            if ($row_cnt > 0) {
                $row_data = [];

                // データ作成
                Log::debug($row);
                for($i = 0; $i < count($csv_header); $i++) {
                    $row_data[$csv_header[$i]] = $row[$i];
                }

                // コールバック実行
                $result = $callback($row_data);

                // 成功した場合
                if ($result == 1) {
                    $success_cnt++;

                // 失敗した場合
                } else {
                    $error_cnt++;
                }
            }
            $row_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);
    }
}

使い方は前回の記事と変わりません。 phpもLaravelも、まだまだ自分の知らない機能や実装方法がありますね。

参考