Skip to content

Instantly share code, notes, and snippets.

@Petah
Created February 10, 2012 04:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Petah/1786683 to your computer and use it in GitHub Desktop.
Save Petah/1786683 to your computer and use it in GitHub Desktop.
PHP CSV Class
<?php
/**
* XMod\Common\Args
*
* @file
* @author David Neilsen <david@panmedia.co.nz>
*/
namespace XMod\Common;
use IteratorAggregate;
use RecursiveIteratorIterator;
use RecursiveArrayIterator;
/**
* Used to iterate a variable number of arguments to a function. Allows you to
* pass either an array, a multidimensional array, or a variable number of
* arguments to a function. Work well when chaining arguments to multiple
* funtions that use func_get_args.
*
* @code
* function testArgs($arg) {
* foreach (new Args(func_get_args()) as $arg) {
* echo $arg;
* }
* }
* testArgs(array(1, 2, 3), 4, 5, array(array(6, array(7))));
* // Output: 1234567
* @endcode
*
*/
class Args implements IteratorAggregate {
/**
* An array (possibly multidimensional) of data to iterate (recursively).
* @var mixed[]
*/
public $args;
/**
* Creates a new instance of XMod\Common\Args using func_get_args.
* @param mixed[] $data...
*/
public function __construct() {
$this->args = func_get_args();
}
/**
* Returns an instance of Args with the first arguments shifted off. Useful
* for mixing an arbitrary anount of arguments with a variable amount of
* arguments.
*
* @code
* function writeRow($file, $mode, $data) {
* foreach (Args::shift(func_get_args(), 2) as $arg) {
* echo $arg;
* }
* // etc
* }
* writeRow('test.txt', 'w', 1, 2, array(3, 4, 5));
* // Output: 12345
* @endcode
*
* @param type $args
* @param type $count
* @return \XMod\Common\static
*/
public static function shift($args, $count = 1) {
$args = new static($args);
$args = $args->toArray();
while ($count-- > 0) {
array_shift($args);
}
return new static($args);
}
/**
* Converts the arguments to a single dimensional array.
*
* @return XMod\Common\Args
*/
public function toArray() {
$result = array();
foreach ($this as $arg) {
$result[] = $arg;
}
return $result;
}
/**
* Converts the arguments to a single dimensional array, maintainin keys
* where possible (nested array with the same key will overwrite previous
* keys).
*
* @return XMod\Common\Args
*/
public function toAssocArray() {
$result = array();
foreach ($this as $key => $arg) {
$result[$key] = $arg;
}
return $result;
}
/**
* Returns and iterator instance that iterates over each argument.
*
* @return RecursiveIteratorIterator
*/
public function getIterator() {
$arrayIterator = new RecursiveArrayIterator($this->args);
return new RecursiveIteratorIterator($arrayIterator);
}
}
<?php
/**
* XMod\Common\CSV
* @link http://www.csvreader.com/csv_format.php
* @todo Might need option to replace characters like " with '
* @todo Needs option to escape non printable characters
* Escape codes: \### and \o### Octal, \x## Hex, \d### Decimal, and \u#### Unicode
* @author David Neilsen <david@panmedia.co.nz>
*/
namespace XMod\Format;
use XMod\Common\Args;
use XMod\Helper\MimeType;
class CSV {
/**
* @var type If true, quote all cells in the CSV.
*/
public $quoteAll = false;
/**
* @var string[] An array of regular expressions that if present in a
* cell, the cell is quoted.
*/
public $quoteChars = array(',', '"', "\n", "\r");
/**
* @var string[] An array of regular expressions that if present will be
* removed.
*/
public $trimChars = array();
/**
* @var string[] An array of regular expressions that if present will be
* replaced with their escaped version.
*/
public $escapeChars = array('"' => '""');
/**
* @var string Quote character to use (where applicable).
*/
public $quote = '"';
/**
* @var string End of line (EOL) character to use.
*/
public $eol = "\r\n";
/**
* @var string Cell seperator to use.
*/
public $seperator = ',';
/**
* @var boolean If true, all EOL charaters (line feed, carriage return) are
* replaced with the specified EOL character(s).
*/
public $normaliseEOL = true;
/**
* Creates an instance of CSV the output in an Excel CSV format.
*
* @return XMod\Format\CSV
*/
public static function newExcel() {
$csv = new static;
$csv->setQuoteChars(',', '"', "\n", "\r");
$csv->setEscapeChars(array('"' => '""'));
$csv->setTrimChars();
$csv->setEOL("\r\n");
$csv->setQuote('"');
$csv->setSeperator(',');
$csv->setNormaliseEOL(true);
return $csv;
}
/**
* Tests a file to check if it is a CSV file. If the file does not exists
* the result is undefined.
*
* @param string $file File path be checked.
* @return boolean True if given file is a CSV file, otherwise false.
*/
public static function isCSV($file) {
return MimeType::is($file, 'text/plain', 'text/csv');
}
/**
* Sends the correct header to force a browser to download a CSV file.
*
* @param string $name Name of the file to download.
*/
public static function download($name) {
header('Content-type: text/csv');
header('Cache-Control: no-store, no-cache');
header('Content-Disposition: attachment; filename="' . $name . '"');
}
/**
* Attempts to detect the column delimiter in a string.
*
* @param string $string
* @param string[] $characters Acceptable delimiters.
* @return string
*/
public static function detectDelimiter($string,
$characters = array(',', '|', "\t")) {
$characters = array_flip($characters);
foreach ($characters as $char => $value) {
$characters[$char] = substr_count($string, $char);
}
$max = max($characters);
if ($max === 0) {
return null;
}
foreach ($characters as $char => $value) {
if ($value === $max) {
break;
}
}
return $char;
}
/**
* Escapes and returns a value to the classes settings.
*
* @param mixed $value
* @return string
*/
public function escape($value) {
// Normalise EOLs
if ($this->normaliseEOL === true) {
// @todo Convert this to a single regular expression
// Normalise line feeds
$value = preg_replace('/(?<!\r)\n/', $this->eol, $value);
// Normalise carriage returns
$value = preg_replace('/\r(?!\n)/', $this->eol, $value);
// Normalise carriage return line feeds
$value = str_replace("\r\n", $this->eol, $value);
}
// Trim illegal characters
foreach ($this->trimChars as $char) {
$value = preg_replace("/$char/", '', $value);
}
// Escape characters
foreach ($this->escapeChars as $char => $escapedChar) {
$value = str_replace($char, $escapedChar, $value);
}
// Quote cell
$quote = false;
if ($this->quoteAll === true) {
$quote = true;
} else {
foreach ($this->quoteChars as $char) {
if (preg_match("/$char/", $value) === 1) {
$quote = true;
break;
}
}
}
if ($quote === true) {
$value = $this->quote . $value . $this->quote;
}
return $value;
}
/**
* Converts an array of data, or an array of arguments to a CSV string.
*
* @param mixed[]... $data
* @return string
*/
public function implode($data) {
$result = array();
foreach (new Args(func_get_args()) as $value) {
$result[] = $this->escape($value);
}
return implode($this->seperator, $result);
}
/**
* Converts an array of data, or an array of arguments to a CSV string. The
* ends with the EOL character.
*
* @param mixed[]... $data
* @return string
*/
public function row($data) {
return $this->implode(func_get_args()) . $this->eol;
}
/**
* Converts an array of arrays to a CSV string.
*
* @param mixed[][] $data
* @param boolean $header If true the keys from the first array are output
* to the first row of the CSV.
*/
public function data($data, $header = false) {
$result = '';
if ($header === true && empty($data) === false) {
$result .= $this->row(array_keys($data[0]));
}
foreach ($data as $row) {
$result .= $this->row($row);
}
return $result;
}
/**
* Reads a file and returns and array made from the CSV data.
*
* @todo Needs rewriting for custom delimiters, new lines, and escape methods.
* @param string $file Path to the file.
* @param string $delimiter
* @return string[][]
*/
public function readFile($file) {
$csv = array();
$lines = file($file, FILE_IGNORE_NEW_LINES);
foreach ($lines as $value) {
$csv[] = str_getcsv($value, $this->seperator, $this->quote);
}
return $csv;
}
/**
* @param boolean $quoteAll
*/
public function setQuoteAll($quoteAll) {
$this->quoteAll = $quoteAll;
}
/**
* @param string[] $quoteChars
*/
public function setQuoteChars() {
$quoteChars = new Args(func_get_args());
$this->quoteChars = $quoteChars->toArray();
}
/**
* @param string[]... $trimChars
*/
public function setTrimChars() {
$trimChars = new Args(func_get_args());
$this->trimChars = $trimChars->toArray();
}
/**
* @param string[]... $escapeChars
*/
public function setEscapeChars() {
$escapeChars = new Args(func_get_args());
$this->escapeChars = $escapeChars->toAssocArray();
}
/**
* @param string $quote
*/
public function setQuote($quote) {
$this->quote = $quote;
}
/**
* @param string $eol
*/
public function setEOL($eol) {
$this->eol = $eol;
}
/**
* @param string $seperator
*/
public function setSeperator($seperator) {
$this->seperator = $seperator;
}
/**
* @param boolean $normaliseEOL
*/
public function setNormaliseEOL($normaliseEOL) {
$this->normaliseEOL = $normaliseEOL;
}
}
<?php
/**
* XMod\Format\CSVTest
*
* @author David Neilsen <david@panmedia.co.nz>
*/
namespace XMod\Format;
use PHPUnit_Framework_TestCase;
class CSVTest extends PHPUnit_Framework_TestCase {
public function testEscape() {
$csv = CSV::newExcel();
$this->assertSame('test', $csv->escape('test'));
$this->assertSame('"test ""test"" test"', $csv->escape('test "test" test'));
$this->assertSame('"test, test"', $csv->escape('test, test'));
$this->assertSame("\"test\r\ntest\"", $csv->escape("test\ntest"));
}
public function testRow() {
$csv = CSV::newExcel();
$row = $csv->row('test', 'test "test" test', 'test, test', "test\ntest");
$expected = "test,\"test \"\"test\"\" test\",\"test, test\",\"test\r\ntest\"\r\n";
$this->assertSame($expected, $row);
$csv->setQuoteAll(true);
$row = $csv->row('test', 'test "test" test', 'test, test', "test\ntest");
$expected = "\"test\",\"test \"\"test\"\" test\",\"test, test\",\"test\r\ntest\"\r\n";
$this->assertSame($expected, $row);
$csv->setQuoteAll(false);
$csv->setTrimChars('"', ',', "\r\n");
$row = $csv->row('test', 'test "test" test', 'test, test', "test\ntest");
$expected = "test,test test test,test test,testtest\r\n";
$this->assertSame($expected, $row);
}
public function testDownload() {
CSV::download('test.csv');
}
public function testReadFile() {
$csv = CSV::newExcel();
$expected = array(
array('test','test','test','test','','',),
array('1','2','3','4','5','6',),
array('1/01/1970','12/12/2012','','','','',),
array('$45.90','23%','1/4','3.142','','',),
);
$data = $csv->readFile(__DIR__ . '/../../data/test.csv');
$this->assertSame($expected, $data);
}
public function testData() {
$csv = CSV::newExcel();
$data = array(
array('test','test','test','test','','',),
array('1','2','3','4','5','6',),
array('1/01/1970','12/12/2012','','','','',),
array('$45.90','23%','1/4','3.142','','',),
);
$data = $csv->data($data);
$expected = file_get_contents(__DIR__ . '/../../data/test.csv');
$this->assertSame($expected, $data);
$data = array(
array(
'one' => 1,
'two' => 2,
'three' => 3,
),
);
$data = $csv->data($data, true);
$expected = "one,two,three\r\n1,2,3\r\n";
$this->assertSame($expected, $data);
}
public function testDetectDelimiter() {
$data = file_get_contents(__DIR__ . '/../../data/test.csv');
$this->assertSame(',', CSV::detectDelimiter($data));
$data = file_get_contents(__DIR__ . '/../../data/test.psv');
$this->assertSame('|', CSV::detectDelimiter($data));
$data = file_get_contents(__DIR__ . '/../../data/test.tsv');
$this->assertSame("\t", CSV::detectDelimiter($data));
$this->assertSame(null, CSV::detectDelimiter($data, array(',', '|')));
}
public function testIsCSV() {
$this->assertSame(true, CSV::isCSV(__DIR__ . '/../../data/test.csv'));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment