diff --git a/.travis.yml b/.travis.yml index 3b450d9..1945f93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: php +dist: trusty php: - '7.3' diff --git a/src/Csv.php b/src/Csv.php index 0980a93..ef73092 100644 --- a/src/Csv.php +++ b/src/Csv.php @@ -169,7 +169,7 @@ class Csv { /** * Convert Encoding - * Should we convert the csv encoding? + * Should we convert the CSV character encoding? * * @var bool */ @@ -278,8 +278,20 @@ class Csv { public $error_info = array(); /** - * Titles - * CSV titles if they exists + * $titles has 4 distinct tasks: + * 1. After reading in CSV data, $titles will contain the column headers + * present in the data. + * + * 2. It defines which fields from the $data array to write e.g. when + * calling unparse(), and in which order. This lets you skip columns you + * don't want in your output, but are present in $data. + * See examples/save_to_file_without_header_row.php. + * + * 3. It lets you rename columns. See StreamTest::testWriteStream for an + * example. + * + * 4. When writing data and $header is true, then $titles is also used for + * the first row. * * @var array */ @@ -299,14 +311,14 @@ class Csv { * Constructor * Class constructor * - * @param string|null $input The CSV string or a direct file path - * @param integer|null $offset Number of rows to ignore from the + * @param string|null $input The CSV string or a direct file path + * @param integer|null $offset Number of rows to ignore from the * beginning of the data - * @param integer|null $limit Limits the number of returned rows + * @param integer|null $limit Limits the number of returned rows * to specified amount - * @param string|null $conditions Basic SQL-like conditions for row + * @param string|null $conditions Basic SQL-like conditions for row * matching - * @param null|true $keep_file_data Keep raw file data in memory after + * @param null|true $keep_file_data Keep raw file data in memory after * successful parsing * (useful for debugging) */ @@ -319,13 +331,13 @@ class Csv { } /** - * @param integer|null $offset Number of rows to ignore from the + * @param integer|null $offset Number of rows to ignore from the * beginning of the data - * @param integer|null $limit Limits the number of returned rows + * @param integer|null $limit Limits the number of returned rows * to specified amount - * @param string|null $conditions Basic SQL-like conditions for row + * @param string|null $conditions Basic SQL-like conditions for row * matching - * @param null|true $keep_file_data Keep raw file data in memory after + * @param null|true $keep_file_data Keep raw file data in memory after * successful parsing * (useful for debugging) */ @@ -355,12 +367,12 @@ class Csv { * Parse * Parse a CSV file or string * - * @param string|null $input The CSV string or a direct file path - * @param integer $offset Number of rows to ignore from the + * @param string|null $input The CSV string or a direct file path + * @param integer $offset Number of rows to ignore from the * beginning of the data - * @param integer $limit Limits the number of returned rows to + * @param integer $limit Limits the number of returned rows to * specified amount - * @param string $conditions Basic SQL-like conditions for row + * @param string $conditions Basic SQL-like conditions for row * matching * * @return bool True on success @@ -392,14 +404,15 @@ class Csv { * Save * Save changes, or write a new file and/or data * - * @param string $file File location to save to - * @param array $data 2D array of data - * @param bool $append Append current data to end of target CSV, if file + * @param string $file File location to save to + * @param array $data 2D array of data + * @param bool $append Append current data to end of target CSV, if file * exists - * @param array $fields Field names. Sets the header. If it is not set + * @param array $fields Field names. Sets the header. If it is not set * $this->titles would be used instead. * * @return bool + * True on success */ public function save($file = '', $data = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $fields = array()) { if (empty($file)) { @@ -416,16 +429,16 @@ class Csv { * Output * Generate a CSV based string for output. * - * @param string|null $filename If a filename is specified here or in the + * @param string|null $filename If a filename is specified here or in the * object, headers and data will be output * directly to browser as a downloadable * file. This file doesn't have to exist on * the server; the parameter only affects * how the download is called to the * browser. - * @param array[] $data 2D array with data - * @param array $fields Field names - * @param string|null $delimiter character used to separate data + * @param array[] $data 2D array with data + * @param array $fields Field names + * @param string|null $delimiter character used to separate data * * @return string The resulting CSV string */ @@ -461,8 +474,8 @@ class Csv { * Encoding * Convert character encoding * - * @param string $input Input character encoding, uses default if left blank - * @param string $output Output character encoding, uses default if left blank + * @param string $input Input character encoding, uses default if left blank + * @param string $output Output character encoding, uses default if left blank */ public function encoding($input = null, $output = null) { $this->convert_encoding = true; @@ -480,11 +493,11 @@ class Csv { * Auto-Detect Delimiter: Find delimiter by analyzing a specific number of * rows to determine most probable delimiter character * - * @param string|null $file Local CSV file - * @param bool $parse True/false parse file directly - * @param int $search_depth Number of rows to analyze - * @param string $preferred Preferred delimiter characters - * @param string|null $enclosure Enclosure character, default is double quote ("). + * @param string|null $file Local CSV file + * @param bool $parse True/false parse file directly + * @param int $search_depth Number of rows to analyze + * @param string $preferred Preferred delimiter characters + * @param string|null $enclosure Enclosure character, default is double quote ("). * * @return string The detected field delimiter */ @@ -575,7 +588,7 @@ class Csv { * Parse File * Read file to string and call _parse_string() * - * @param string|null $file Local CSV file + * @param string|null $file Local CSV file * * @return array|bool */ @@ -709,7 +722,7 @@ class Csv { $col++; // end of row - if ($ch == "\n" || $ch == "\r" || $ch === false) { + if (in_array($ch, ["\n", "\r", false], true)) { if ($this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions)) { if ($this->heading && empty($head)) { $head = $row; @@ -832,7 +845,17 @@ class Csv { } if ($this->convert_encoding) { - $string = iconv($this->input_encoding, $this->output_encoding, $string); + /** @noinspection PhpComposerExtensionStubsInspection + * + * If you receive an error at the following 3 lines, you must enable + * the following PHP extension: + * + * - if $use_mb_convert_encoding is true: mbstring + * - if $use_mb_convert_encoding is false: iconv + */ + $string = $this->use_mb_convert_encoding ? + mb_convert_encoding($string, $this->output_encoding, $this->input_encoding) : + iconv($this->input_encoding, $this->output_encoding, $string); } return $string; @@ -927,6 +950,14 @@ class Csv { } if ($this->convert_encoding && $this->input_encoding !== $this->output_encoding) { + /** @noinspection PhpComposerExtensionStubsInspection + * + * If you receive an error at the following 3 lines, you must enable + * the following PHP extension: + * + * - if $use_mb_convert_encoding is true: mbstring + * - if $use_mb_convert_encoding is false: iconv + */ $data = $this->use_mb_convert_encoding ? mb_convert_encoding($data, $this->output_encoding, $this->input_encoding) : iconv($this->input_encoding, $this->output_encoding, $data); @@ -1026,7 +1057,7 @@ class Csv { $op = $capture[2]; $value = $capture[3]; - if (preg_match('/^([\'\"]{1})(.*)([\'\"]{1})$/', $value, $capture) && $capture[1] == $capture[3]) { + if (preg_match('/^([\'"])(.*)([\'"])$/', $value, $capture) && $capture[1] == $capture[3]) { $value = strtr($capture[2], array( "\\n" => "\n", "\\r" => "\r", @@ -1037,7 +1068,8 @@ class Csv { } if (array_key_exists($field, $row)) { - if (($op == '=' || $op == 'equals' || $op == 'is') && $row[$field] == $value) { + $op_equals = in_array($op, ['=', 'equals', 'is'], true); + if ($op_equals && $row[$field] == $value) { return '1'; } elseif (($op == '!=' || $op == 'is not') && $row[$field] != $value) { return '1'; @@ -1093,7 +1125,7 @@ class Csv { : ''; $enclosure_quoted = preg_quote($this->enclosure, '/'); $pattern = "/" . $delimiter_quoted . $enclosure_quoted . "|\n|\r/i"; - if ($this->enclose_all || preg_match($pattern, $value) || ($value{0} == ' ' || substr($value, -1) == ' ')) { + if ($this->enclose_all || preg_match($pattern, $value) || (strpos($value, ' ') === 0 || substr($value, -1) == ' ')) { $value = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $value); $value = $this->enclosure . $value . $this->enclosure; } @@ -1105,7 +1137,7 @@ class Csv { /** * Check file data * - * @param string|null $file local filename + * @param string|null $file local filename * * @return bool */ @@ -1125,10 +1157,10 @@ class Csv { * Check if passed info might be delimiter * Only used by find_delimiter * - * @param string $char Potential field separating character - * @param array $array Frequency - * @param int $depth Number of analyzed rows - * @param string $preferred Preferred delimiter characters + * @param string $char Potential field separating character + * @param array $array Frequency + * @param int $depth Number of analyzed rows + * @param string $preferred Preferred delimiter characters * * @return string|false special string used for delimiter selection, or false */ @@ -1185,12 +1217,14 @@ class Csv { /** * Write to local file * - * @param string $file local filename - * @param string $content data to write to file - * @param string $mode fopen() mode - * @param int $lock flock() mode + * @param string $file local filename + * @param string $content data to write to file + * @param string $mode fopen() mode + * @param int $lock flock() mode + * + * @return bool + * True on success * - * @return true or false */ protected function _wfile($file, $content = '', $mode = 'wb', $lock = LOCK_EX) { if ($fp = fopen($file, $mode)) { diff --git a/tests/methods/ExampleStream.php b/tests/methods/ExampleStream.php new file mode 100644 index 0000000..c36281c --- /dev/null +++ b/tests/methods/ExampleStream.php @@ -0,0 +1,101 @@ + strlen(self::$stream_content)]; + } + + + public function stream_tell() { + return self::$position; + } + + public function stream_eof() { + return self::$position >= strlen(self::$stream_content); + } + + public function url_stat() { + return ['size' => strlen(self::$stream_content)]; + } + + public function stream_seek($offset, $whence) { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen(self::$stream_content) && $offset >= 0) { + self::$position = $offset; + return true; + } else { + return false; + } + break; + + case SEEK_CUR: + if ($offset >= 0) { + self::$position += $offset; + return true; + } else { + return false; + } + break; + + case SEEK_END: + if (strlen(self::$stream_content) + $offset >= 0) { + self::$position = strlen(self::$stream_content) + $offset; + return true; + } else { + return false; + } + break; + + default: + return false; + } + } + + public function stream_lock($operation) { + return true; + } + + public function stream_metadata() { + return false; + } +} + diff --git a/tests/methods/StreamTest.php b/tests/methods/StreamTest.php new file mode 100644 index 0000000..be9b594 --- /dev/null +++ b/tests/methods/StreamTest.php @@ -0,0 +1,71 @@ +auto(file_get_contents($filename))); + self::assertCount(4, $csv->data); + self::assertCount(6, reset($csv->data)); + } + + public function testWriteStream() { + $csv = new Csv(); + $csv->linefeed = "\n"; + $many_dots = str_repeat('.', 1000 * 1000); + $csv->data = [ + [ + 'Name' => 'Rudolf', + 'Question' => 'Which color is his nose?', + ], + [ + 'Name' => 'Sponge Bob', + 'Question' => 'Which shape are his pants?', + ], + [ + 'Name' => $many_dots, + 'Question' => 'Can you count one million dots?', + ], + ]; + + // Just export the first column, but with a new name + $csv->titles = ['Name' => 'Character']; + + // Write data to our stream: + $filename = 'example://data'; + copy(__DIR__ . '/fixtures/datatype.csv', $filename); + + self::assertSame(true, $csv->save($filename)); + $expected = "Character\nRudolf\nSponge Bob\n"; + $expected .= $many_dots . "\n"; + self::assertSame($expected, file_get_contents($filename)); + } +}