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;
}
}