diff --git a/README.md b/README.md index ba8c062..4b772a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ CSV parser/generator ==================== +![](https://phpci.gitnet.fr//build-status/image/6) + A simple PHP library to parse and generate CSV files. ## Composer installation @@ -53,16 +55,16 @@ $csv->prependData(['Boo', '$3000']); $csv->setDatas([[...], [...]]); // Defines the header -$csv->setHeaders(["Product", "Price"]); +$csv->setHeaders(['Product', 'Price']); // Rendering $result = $csv->render(); // Rendering to a file -$result = $csv->render("products.csv"); +$result = $csv->render('products.csv'); // Appending to a file -$result = $csv->render("products.csv", FILE_APPEND); +$result = $csv->render('products.csv', FILE_APPEND); ``` ### Parser @@ -70,10 +72,27 @@ $result = $csv->render("products.csv", FILE_APPEND); ```php use Deblan\Csv\CsvParser; -$csv = new CsvParser('products.csv'); -$csv->setHasLegend(true); -$csv->parse(); +$csv = new CsvParser(); -$legend = $csv->getLegend(); +// Defines the delimiter (default is ;) +$csv->setDelimiter(";"); + +// Defines the enclosure (default is ") +$csv->setEnclosure('"'); + +// Defines the end of line (default is \n) +$csv->setEndOfLine("\n"); + +// Headers? +$csv->setHasHeaders(true); + +// Parse a file +$csv->parseFile('products.csv'); + +// Parse a string +$csv->parseString($myString); + +// Headers and datas +$headers = $csv->getHeaders(); $products = $csv->getDatas(); ``` diff --git a/src/Deblan/Csv/CsvParser.php b/src/Deblan/Csv/CsvParser.php index 99abe49..756bf1a 100644 --- a/src/Deblan/Csv/CsvParser.php +++ b/src/Deblan/Csv/CsvParser.php @@ -2,149 +2,225 @@ namespace Deblan\Csv; -use Deblan\Csv\Exception\CsvParserInvalidParameterException; -use Deblan\Csv\Exception\CsvParserException; - +/** + * class Csv. + * + * @author Simon Vieille + */ class CsvParser { - private $filename; + /** + * @var string + */ + protected $delimiter = ';'; - private $delimiter; + /** + * @var string + */ + protected $enclosure = '"'; - private $enclosure; + /** + * @var string + */ + protected $endOfLine = "\n"; - private $escapeChar; + /** + * @var array + */ + protected $datas = []; - private $hasLegend; + /** + * @var array + */ + protected $headers = []; - private $datas = array(); + /** + * @var bool + */ + protected $hasHeaders = false; - private $legend = array(); - - private $nullValues = array(); - - public function __construct($filename, $delimiter = ';', $enclosure = '"', $escapeChar = '\\', $hasLegend = false, array $nullValues = array('')) + /** + * Set the value of "delimiter". + * + * @param string $delimiter + * + * @return Csv + */ + public function setDelimiter($delimiter) { - $this->setFilename($filename); - $this->setDelimiter($delimiter); - $this->setEnclosure($enclosure); - $this->setEscapeChar($escapeChar); - $this->setHasLegend($hasLegend); - $this->setNullValues($nullValues); - } + $this->delimiter = (string) $delimiter; - public function setFilename($v) - { - if (!is_string($v)) { - throw new CsvParserInvalidParameterException(sprintf('"%s" is not a valid string.', $v)); - } - - if (!file_exists($v)) { - throw new CsvParserException(sprintf('"%s" does not exist.', $v)); - } - - if (!is_readable($v)) { - throw new CsvParserException(sprintf('"%s" is not readable.', $v)); - } - - $this->filename = $v; - } - - public function setDelimiter($v) - { - if (!is_string($v)) { - throw new CsvParserInvalidParameterException(sprintf('"%s" is not a valid string.', $v)); - } - - $this->delimiter = $v; - } - - public function setEnclosure($v) - { - if (!is_string($v)) { - throw new CsvParserInvalidParameterException(sprintf('"%s" is not a valid string.', $v)); - } - - $this->enclosure = $v; - } - - public function setEscapeChar($v) - { - if (!is_string($v)) { - throw new CsvParserInvalidParameterException(sprintf('"%s" is not a valid string.', $v)); - } - - $this->escapeChar = $v; - } - - public function setHasLegend($v) - { - if (!is_bool($v)) { - throw new CsvParserInvalidParameterException(sprintf('"%s" is not a valid bool.', $v)); - } - - $this->hasLegend = $v; - } - - public function getHasLegend() - { - return $this->hasLegend; - } - - public function getLegend() - { - return $this->legend; - } - - public function setNullValues(array $v) - { - $this->nullValues = $v; + return $this; } /** + * Get the value of "delimiter". * - * To improve... - * + * @return array */ - protected function cleanNullValues($line) + public function getDelimiter() { - return str_replace($this->nullValues, '', $line); + return $this->delimiter; } + /** + * Set the value of "enclosure". + * + * @param string $enclosure + * + * @return Csv + */ + public function setEnclosure($enclosure) + { + $this->enclosure = (string) $enclosure; + + return $this; + } + + /** + * Get the value of "enclosure". + * + * @return string + */ + public function getEnclosure() + { + return $this->enclosure; + } + + /** + * Set the value of "endOfLine". + * + * @param string $endOfLine + * + * @return Csv + */ + public function setEndOfLine($endOfLine) + { + $this->endOfLine = (string) $endOfLine; + + return $this; + } + + /** + * Get the value of "endOfLine". + * + * @return string + */ + public function getEndOfLine() + { + return $this->endOfLine; + } + + /** + * Get the value of "hasHeaders". + * + * @return bool + */ + public function getHasHeaders() + { + return $this->hasHeaders; + } + + /** + * Set the value of "headers". + * + * @param bool $hasHeaders + * + * @return Csv + */ + public function setHasHeaders($hasHeaders) + { + $this->hasHeaders = (bool) $hasHeaders; + + return $this; + } + + /** + * Get the value of "headers". + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Get the value of "datas". + * + * @return array + */ public function getDatas() { return $this->datas; } - public function parse() + /* + * Parses a string. + * + * @param string $string + * + * @return CsvParser + */ + public function parseString($string) { - if (!empty($this->datas)) { - return $this->datas; - } + $this->datas = []; + $this->headers = []; + $lines = str_getcsv($string, $this->endOfLine); - $lines = file($this->filename); + foreach ($lines as $key => $line) { + $data = $this->parseLine($line, $this->hasHeaders && $key === 0); - if (empty($lines)) { - return true; - } - - if ($this->hasLegend) { - $this->legend = str_getcsv($lines[0], $this->delimiter, $this->enclosure, $this->escapeChar); - unset($lines[0]); - } - - foreach ($lines as $l => $line) { - $datas = str_getcsv($this->cleanNullValues($line), $this->delimiter, $this->enclosure, $this->escapeChar); - - if ($this->hasLegend) { - foreach ($this->legend as $k => $v) { - $datas[$v] = isset($datas[$k]) ? $datas[$k] : null; - } + if ($data === null) { + continue; } - $this->datas[] = $datas; + if ($this->hasHeaders && $key === 0) { + $this->headers = $data; + } else { + $this->datas[] = $data; + } } - return true; + return $this; + } + + /* + * Parses a line. + * + * @param string $line + * @param bool $isHeaders + * + * @return array + */ + public function parseLine($line, $isHeaders = false) + { + $line = trim($line); + + if (empty($line)) { + return null; + } + + $csv = str_getcsv($line, $this->delimiter, $this->enclosure); + + if (!$isHeaders && $this->hasHeaders && !empty($this->headers)) { + foreach ($this->headers as $key => $header) { + $csv[$header] = isset($csv[$key]) ? $csv[$key] : null; + } + } + + return $csv; + } + + /* + * Parses a file. + * + * @param string $filaname + * + * @return CsvParser + */ + public function parseFile($filename) + { + return $this->parseString(file_get_contents($filename)); } } diff --git a/tests/CsvParserTest.php b/tests/CsvParserTest.php index 0c36620..70c9f83 100644 --- a/tests/CsvParserTest.php +++ b/tests/CsvParserTest.php @@ -1,8 +1,188 @@ assertEquals(';', $parser->getDelimiter()); + $parser->setDelimiter('#'); + $this->assertEquals('#', $parser->getDelimiter()); + + $parser = new CsvParser(); + $this->assertEquals("\n", $parser->getEndOfLine()); + $parser->setEndOfLine("\r\n"); + $this->assertEquals("\r\n", $parser->getEndOfLine()); + + $parser = new CsvParser(); + $this->assertEquals('"', $parser->getEnclosure()); + $parser->setEnclosure("'"); + $this->assertEquals("'", $parser->getEnclosure()); + + $parser = new CsvParser(); + $this->assertEquals([], $parser->getDatas()); + $this->assertEquals([], $parser->getHeaders()); + $this->assertEquals(false, $parser->getHasHeaders()); + $parser->setHasHeaders(true); + $this->assertEquals(true, $parser->getHasHeaders()); + } + + public function testParser() + { + $parser = new CsvParser(); + $this->assertEquals(['foo', 'bar'], $parser->parseLine('"foo";"bar"')); + $this->assertEquals([], $parser->getDatas()); + $this->assertEquals([], $parser->getHeaders()); + + $parser = new CsvParser(); + $parser->parseString('"foo";"bar"'); + $this->assertEquals([['foo', 'bar']], $parser->getDatas()); + $this->assertEquals([], $parser->getHeaders()); + + $parser = new CsvParser(); + $parser->parseString('"foo";"bar"'."\n".'"foo2";"bar2"'); + $this->assertEquals([['foo', 'bar'], ['foo2', 'bar2']], $parser->getDatas()); + $this->assertEquals([], $parser->getHeaders()); + + $parser = new CsvParser(); + $parser->setHasHeaders(true); + $parser->parseString('"foo";"bar"'."\n".'"foo2";"bar2"'); + $this->assertEquals([['foo2', 'bar2', 'foo' => 'foo2', 'bar' => 'bar2']], $parser->getDatas()); + $this->assertEquals(['foo', 'bar'], $parser->getHeaders()); + + $parser = new CsvParser(); + $parser->setHasHeaders(true); + $parser->setEnclosure(null); + $parser->parseString('foo;bar'."\n".'foo2;bar2;boo2'); + $this->assertEquals([['foo2', 'bar2', 'boo2', 'foo' => 'foo2', 'bar' => 'bar2']], $parser->getDatas()); + $this->assertEquals(['foo', 'bar'], $parser->getHeaders()); + + $parser = new CsvParser(); + $parser->setHasHeaders(true); + $parser->parseString('foo;bar'."\n".'foo2'); + $this->assertEquals([['foo2', 'foo' => 'foo2', 'bar' => null]], $parser->getDatas()); + $this->assertEquals(['foo', 'bar'], $parser->getHeaders()); + + $parser = new CsvParser(); + $parser->setHasHeaders(true); + $parser->parseFile(__DIR__.'/fixtures/example.csv'); + $this->assertEquals( + [ + [ + 'foo 1', + 'bar 1', + 'FOO' => 'foo 1', + 'BAR' => 'bar 1', + ], + [ + 'foo 2', + 'bar 2', + 'FOO' => 'foo 2', + 'BAR' => 'bar 2', + ], + [ + 'foo 3', + 'bar 3', + 'FOO' => 'foo 3', + 'BAR' => 'bar 3', + ], + ], + $parser->getDatas() + ); + $this->assertEquals(['FOO', 'BAR'], $parser->getHeaders()); + + $parser = new CsvParser(); + $parser->setHasHeaders(false); + $parser->parseFile(__DIR__.'/fixtures/example2.csv'); + $this->assertEquals( + [ + [ + 'foo 1', + 'bar 1', + ], + [ + 'foo 2', + 'ba"r 2', + ], + [ + 'foo 3', + 'bar 3', + ], + ], + $parser->getDatas() + ); + + $parser = new CsvParser(); + $parser->setHasHeaders(true); + $parser->setEnclosure("'"); + $parser->setDelimiter('#'); + $parser->parseFile(__DIR__.'/fixtures/example3.csv'); + $this->assertEquals( + [ + [ + 'foo 1', + '', + "FO'O" => 'foo 1', + 'BAR' => '', + ], + [ + 'foo 1b', + "FO'O" => 'foo 1b', + 'BAR' => null, + ], + [ + 'foo 2', + 'bar 2', + 'unexpected 3', + "FO'O" => 'foo 2', + 'BAR' => 'bar 2', + ], + [ + 'foo 3', + 'bar 3', + "FO'O" => 'foo 3', + 'BAR' => 'bar 3', + ], + ], + $parser->getDatas() + ); + + $parser = new CsvParser(); + $parser->setHasHeaders(true); + $parser->setEnclosure("'"); + $parser->setDelimiter('#'); + $parser->setEndOfLine("\r\n"); + $parser->parseFile(__DIR__.'/fixtures/example4.csv'); + $this->assertEquals( + [ + [ + 'foo 1', + '', + "FO'O" => 'foo 1', + 'BAR' => '', + ], + [ + 'foo 1b', + "FO'O" => 'foo 1b', + 'BAR' => null, + ], + [ + 'foo 2', + 'bar 2', + 'unexpected 3', + "FO'O" => 'foo 2', + 'BAR' => 'bar 2', + ], + [ + 'foo 3', + 'bar 3', + "FO'O" => 'foo 3', + 'BAR' => 'bar 3', + ], + ], + $parser->getDatas() + ); + } } diff --git a/tests/CsvTest.php b/tests/CsvTest.php index 11d9da7..be67a6a 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -1,14 +1,13 @@ */ -class CsvTest extends PHPUnit_Framework_TestCase +class CsvTest extends \PHPUnit_Framework_TestCase { public function testGettersAndSettersAndDefaultValues() { @@ -16,6 +15,11 @@ class CsvTest extends PHPUnit_Framework_TestCase $this->assertEquals(';', $csv->getDelimiter()); $csv->setDelimiter('#'); $this->assertEquals('#', $csv->getDelimiter()); + + $csv = new Csv(); + $this->assertEquals('"', $csv->getEnclosure()); + $csv->setEnclosure('#'); + $this->assertEquals('#', $csv->getEnclosure()); $csv = new Csv(); $this->assertEquals("\n", $csv->getEndOfLine()); diff --git a/tests/fixtures/example.csv b/tests/fixtures/example.csv new file mode 100644 index 0000000..d6b62ce --- /dev/null +++ b/tests/fixtures/example.csv @@ -0,0 +1,4 @@ +"FOO";"BAR" +"foo 1";"bar 1" +"foo 2";"bar 2" +"foo 3";"bar 3" diff --git a/tests/fixtures/example2.csv b/tests/fixtures/example2.csv new file mode 100644 index 0000000..a4e9ac5 --- /dev/null +++ b/tests/fixtures/example2.csv @@ -0,0 +1,3 @@ +"foo 1";"bar 1" +"foo 2";"ba""r 2" +"foo 3";"bar 3" diff --git a/tests/fixtures/example3.csv b/tests/fixtures/example3.csv new file mode 100644 index 0000000..9077a11 --- /dev/null +++ b/tests/fixtures/example3.csv @@ -0,0 +1,5 @@ +'FO''O'#'BAR' +'foo 1'# +'foo 1b' +'foo 2'#'bar 2'#'unexpected 3' +'foo 3'#'bar 3' diff --git a/tests/fixtures/example4.csv b/tests/fixtures/example4.csv new file mode 100644 index 0000000..e56aef3 --- /dev/null +++ b/tests/fixtures/example4.csv @@ -0,0 +1,5 @@ +'FO''O'#'BAR' +'foo 1'# +'foo 1b' +'foo 2'#'bar 2'#'unexpected 3' +'foo 3'#'bar 3'