LoginSignup
5
10

More than 5 years have passed since last update.

PHPでCSVを出力するクラス(fputcsv不使用)はこれでいい?

Last updated at Posted at 2017-09-28

PHPでCSVを出力するクラスを作りました。これでいいですかね……?

前提と背景

  • fputcsvが使えない事情はCSVの国際規格に準拠していないシステムに対応するため。
    • エスケープ文字がバックスラッシュのシステム
    • すべてのフィールドをダブルクオートで囲まないといけない
  • 実装はGo言語のencoding/csvのWriterを参考にしました…。
  • PHP 5.3対応必須

使い方

$file = new SplFileObject('php://output', 'w+');
$csv = new CsvWriter($file, ',', '"', '"', "\r\n", true);
$toCharset = 'SJIS-win';
$fromCharset = 'UTF-8';

// SJISに変換する
$csv->setOnWriteLine(function ($line) use ($toCharset, $fromCharset) {
    return mb_convert_encoding($line, $toCharset, $fromCharset);
});
$csv->write(array('ID', '都道府県'));
$csv->write(array('1', '北海道'));
$csv->write(array('2', '青森県'));

クラス

CsvWriter.php
class CsvWriter
{
    /**
     * @var string
     */
    private $delimiter;

    /**
     * @var string
     */
    private $enclosure;

    /**
     * @var string
     */
    private $escape;

    /**
     * @var string
     */
    private $newline;

    /**
     * @var bool
     */
    private $forceEncloses;

    /**
     * @var callable
     */
    private $onWriteLine;

    /**
     * @var SplFileObject
     */
    private $file;

    public function __construct(
        SplFileObject $file,
        $delimiter = ',',
        $enclosure = '"',
        $escape = '"',
        $newline = "\r\n",
        $forceEncloses = false
    )
    {
        if (strlen($delimiter) !== 1) throw new InvalidArgumentException('delimiter must be a single character');
        if (strlen($enclosure) !== 1) throw new InvalidArgumentException('enclosure must be a single character');
        if (strlen($escape) !== 1) throw new InvalidArgumentException('escape must be a single character');
        if (!in_array($newline, array("\r", "\n", "\r\n"))) throw new InvalidArgumentException('newline must be CR, LF or CRLF');
        if (!is_bool($forceEncloses)) throw new InvalidArgumentException('forceEncloses must be boolean');

        $this->delimiter = $delimiter;
        $this->enclosure = $enclosure;
        $this->escape = $escape;
        $this->newline = $newline;
        $this->forceEncloses = $forceEncloses;
        $this->file = $file;
        $this->setOnWriteLine(function ($line) {
            return $line;
        });
    }

    /**
     * @param callable $onWriteLine
     * @return self
     */
    public function setOnWriteLine($onWriteLine)
    {
        if (!is_callable($onWriteLine)) throw new InvalidArgumentException('onWriteLine must be a callable');
        $this->onWriteLine = $onWriteLine;
        return $this;
    }

    /**
     * @param array $fields
     * @return void
     */
    public function write(array $fields)
    {
        $line = '';
        foreach (array_values($fields) as $n => $field) {
            if ($n > 0) {
                $line .= $this->delimiter;
            }
            if ($this->fieldNeedsQuotes($field) || $this->forceEncloses) {
                $line .= $this->quoteField($field);
            } else {
                $line .= $field;
            }
        }
        $line .= $this->newline;
        $this->file->fwrite(call_user_func($this->onWriteLine, $line));
    }

    /**
     * @param string $field
     * @return bool
     */
    private function fieldNeedsQuotes($field)
    {
        if ($field == '') {
            return false;
        }

        if ($field == '\.') {
            return true; // for Postgres, quote the data terminating string '\.'.
        }

        $specialCharacters =
            array(
                $this->delimiter,
                $this->enclosure,
            )
            // Unicode white spaces in the Latin-1 space
            + array(
                "\t",
                "\n",
                "\v",
                "\f",
                "\r",
                ' ',
                "\x85", // NEL
                "\xA0", // NO-BREAK SPACE
            );

        foreach ($specialCharacters as $character) {
            if (strpos($field, $character) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param string $string
     * @return string
     */
    private function quoteField($string)
    {
        $output = '';
        $output .= $this->enclosure;

        foreach (mb_split('//', $string) as $rune) {
            switch ($rune) {
                case $this->enclosure:
                    $output .= $this->escape . $this->enclosure;
                    break;
                case "\r":
                    if ($this->newline !== "\r\n") {
                        $output .= "\r";
                    }
                    break;
                case "\n":
                    if ($this->newline === "\r\n") {
                        $output .= "\r\n";
                    } else {
                        $output .= "\n";
                    }
                    break;
                default:
                    $output .= $rune;
            }
        }

        $output .= $this->enclosure;
        return $output;
    }
}
5
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
10