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

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

## 前提と背景

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

## 使い方


```php
$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', '青森県'));
```


## クラス

```php: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;
    }
}
```