PageRenderTime 45ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/library/spoon/file/csv.php

http://github.com/forkcms/forkcms
PHP | 389 lines | 157 code | 70 blank | 162 comment | 37 complexity | 9e5275f2a0bfc15b56b211eeb8409736 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, MIT, AGPL-3.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /**
  3. * Spoon Library
  4. *
  5. * This source file is part of the Spoon Library. More information,
  6. * documentation and tutorials can be found @ http://www.spoon-library.com
  7. *
  8. * @package spoon
  9. * @subpackage file
  10. *
  11. *
  12. * @author Davy Hellemans <davy@spoon-library.com>
  13. * @since 1.2.0
  14. */
  15. /**
  16. * This class provides functions to work with CSV-files.
  17. *
  18. * @package spoon
  19. * @subpackage file
  20. *
  21. *
  22. * @author Davy Hellemans <davy@spoon-library.com>
  23. * @author Tijs Verkoyen <tijs@spoon-library.com>
  24. * @author Dave Lens <dave@spoon-library.com>
  25. * @since 1.2.0
  26. */
  27. class SpoonFileCSV
  28. {
  29. const DEFAULT_DELIMITER = ',';
  30. const DEFAULT_ENCLOSURE = '"';
  31. /**
  32. * Converts an array to a CSV file
  33. *
  34. * @return mixed
  35. * @param string $path The full path to the file you wish to create.
  36. * @param array $array The array to convert.
  37. * @param array[optional] $columns The column names you want to use.
  38. * @param array[optional] $excludeColumns The columns you want to exclude.
  39. * @param string[optional] $delimiter The field delimiter of the CSV.
  40. * @param string[optional] $enclosure The enclosure character of the CSV.
  41. * @param bool[optional] $download Should the file be downloaded?
  42. */
  43. public static function arrayToFile($path, array $array, array $columns = null, array $excludeColumns = null, $delimiter = ',', $enclosure = '"', $download = false)
  44. {
  45. // get the content of the file
  46. $csv = self::arrayToString($array, $columns, $excludeColumns, $delimiter, $enclosure);
  47. // set file content
  48. SpoonFile::setContent($path, $csv);
  49. // user chose to download this file
  50. if($download) return self::download($path);
  51. }
  52. /**
  53. * Converts an array into a CSV-formatted string
  54. *
  55. * @return string
  56. * @param array $array The array to convert.
  57. * @param array[optional] $columns The column names you want to use.
  58. * @param array[optional] $excludeColumns The columns you want to exclude.
  59. * @param string[optional] $delimiter The field delimiter of the CSV.
  60. * @param string[optional] $enclosure The enclosure character of the CSV.
  61. * @param string[optional] $lineEnding The line-ending of the CSV.
  62. */
  63. public static function arrayToString(array $array, array $columns = null, array $excludeColumns = null, $delimiter = ',', $enclosure = '"', $lineEnding = null)
  64. {
  65. // validate array
  66. if(!empty($array) && !isset($array[0])) throw new SpoonFileException('Invalid array format.');
  67. // no columns set means we will use the keys as the column name if they're not integers
  68. if(empty($columns) && isset($array[0][0])) throw new SpoonFileException('Provide column names or use strings as array keys.');
  69. // column names are set
  70. if(empty($columns)) $columns = array_keys($array[0]);
  71. // check for delimiter/enclosure
  72. if($delimiter === null) $delimiter = self::DEFAULT_DELIMITER;
  73. if($enclosure === null) $enclosure = self::DEFAULT_ENCLOSURE;
  74. if($lineEnding === null) $lineEnding = PHP_EOL;
  75. // unset the excluded columns
  76. if(!empty($excludeColumns)) foreach($excludeColumns as $column) unset($columns[array_search($column, $columns)]);
  77. // escape enclosure chars
  78. $columns = self::escapeEnclosure($columns, $enclosure);
  79. // start the string with the columns
  80. $csv = $enclosure . implode($enclosure . $delimiter . $enclosure, $columns) . $enclosure . $lineEnding;
  81. // stop here if the array is empty
  82. if(empty($array)) return $csv;
  83. // count columns and cells
  84. $countCells = count($array[0]);
  85. // loop the array
  86. foreach($array as $row)
  87. {
  88. // cellcount check
  89. if($countCells != count($row)) throw new SpoonFileException('Each row has to have the same number of cells as the first row.');
  90. // some columns are excluded
  91. if(!empty($excludeColumns))
  92. {
  93. // unset the excluded columns
  94. foreach($excludeColumns as $column) unset($row[$column], $columns[array_search($column, $columns)]);
  95. }
  96. // escape enclosure chars
  97. $row = self::escapeEnclosure($row, $enclosure);
  98. // add this row to the CSV
  99. $csv .= $enclosure . implode($enclosure . $delimiter . $enclosure, (array) $row) . $enclosure . $lineEnding;
  100. }
  101. // no input
  102. return $csv;
  103. }
  104. /**
  105. * Sets the headers so we may download the CSV file in question
  106. *
  107. * @return array
  108. * @param string $path The full path to the CSV file you wish to download.
  109. */
  110. private static function download($path)
  111. {
  112. // check if the file exists
  113. if(!SpoonFile::exists($path)) throw new SpoonFileException('The file ' . $path . ' doesn\'t exist.');
  114. // fetch the filename from the path string
  115. $explodedFilename = explode('/', $path);
  116. $filename = end($explodedFilename);
  117. // set headers for download
  118. $headers = array();
  119. $headers[] = 'Content-type: text/csv; charset=utf-8';
  120. $headers[] = 'Content-Disposition: attachment; filename="' . $filename . '"';
  121. // overwrite the headers
  122. SpoonHTTP::setHeaders($headers);
  123. // get the file contents
  124. $content = SpoonFile::getContent($path);
  125. // delete the CSV file
  126. SpoonFile::delete($path);
  127. // output the file contents
  128. echo $content;
  129. // exit here
  130. exit;
  131. }
  132. /**
  133. * Escape the character that is being used as enclosure in a csv row.
  134. *
  135. * @return array The escaped version of the row coming in.
  136. * @param array $row The row to escape.
  137. * @param string $enclosure The character being used as enclosure.
  138. */
  139. public static function escapeEnclosure($row, $enclosure)
  140. {
  141. // init var
  142. $escaped = array();
  143. // apply enclosure
  144. foreach($row as $key => $value)
  145. {
  146. $escaped[$key] = str_replace($enclosure, $enclosure . $enclosure, $value);
  147. }
  148. return $escaped;
  149. }
  150. /**
  151. * Converts a CSV file to an array
  152. *
  153. * @return array
  154. * @param array $path The full path to the CSV-file you want to extract an array from.
  155. * @param array[optional] $columns The column names you want to use.
  156. * @param array[optional] $excludeColumns The columns to exclude.
  157. * @param string[optional] $delimiter The field delimiter of the CSV.
  158. * @param string[optional] $enclosure The enclosure character of the CSV.
  159. */
  160. public static function fileToArray($path, array $columns = array(), array $excludeColumns = null, $delimiter = ',', $enclosure = '"')
  161. {
  162. // reset variables
  163. $path = (string) $path;
  164. // validation
  165. if(!SpoonFile::exists($path)) throw new SpoonFileException($path . ' doesn\'t exists.');
  166. // get delimiter and enclosure from the contents
  167. if(!$delimiter || !$enclosure) $autoDetect = self::getDelimiterAndEnclosure(SpoonFile::getContent($path), $delimiter, $enclosure);
  168. if(!$delimiter) $delimiter = $autoDetect[0];
  169. if(!$enclosure) $enclosure = $autoDetect[1];
  170. // automagicaly detect line endings
  171. @ini_set('auto_detect_line_endings', 1);
  172. // init var
  173. $rows = array();
  174. // open file
  175. $handle = @fopen($path, 'r');
  176. // loop lines and store the rows
  177. while(($row = @fgetcsv($handle, 0, (($delimiter == '') ? ',' : $delimiter), (($enclosure == '') ? '"' : $enclosure))) !== false) $rows[] = $row;
  178. // close file
  179. @fclose($handle);
  180. // no lines
  181. if(count($rows) == 0) return false;
  182. // no column names are set
  183. if(empty($columns)) $columns = array_values($rows[0]);
  184. // remove the first row
  185. array_shift($rows);
  186. // loop the rows
  187. foreach($rows as $rowId => &$row)
  188. {
  189. // the keys of this row
  190. $keys = array_keys($row);
  191. // some columns are excluded
  192. if(!empty($excludeColumns))
  193. {
  194. // unset the keys related to the excluded columns
  195. foreach($excludeColumns as $columnKey => $column) unset($keys[array_search($columnKey, $columns)], $row[$columnKey]);
  196. }
  197. // loop the keys
  198. foreach($keys as $columnId)
  199. {
  200. // add the field to this row
  201. $row[$columns[$columnId]] = $row[$columnId];
  202. // remove the original field from this row
  203. unset($row[$columnId]);
  204. }
  205. }
  206. // return the array
  207. return $rows;
  208. }
  209. /**
  210. * Returns an array with the delimiter and enclosure used in the given string
  211. *
  212. * @return array
  213. * @param string $string The string you want to extract delimiter and enclosure from.
  214. * @param string[optional] $delimiter The delimiter you wish to use instead of the default.
  215. * @param string[optional] $enclosure The enclosure you wish to use instead of the default.
  216. */
  217. private static function getDelimiterAndEnclosure($string, $delimiter = null, $enclosure = null)
  218. {
  219. // reset variables
  220. $string = (string) $string;
  221. $delimiter = ($delimiter == null) ? self::DEFAULT_DELIMITER : $delimiter;
  222. $enclosure = ($enclosure == null) ? self::DEFAULT_ENCLOSURE : $enclosure;
  223. $delimiterCount = 0;
  224. $enclosureCount = 0;
  225. // check for comma delimiter
  226. if(preg_match_all('/,/', $string, $matches))
  227. {
  228. // count the commas
  229. $newCount = count($matches[0]);
  230. // replace the delimiter, if need be
  231. if($delimiterCount < $newCount)
  232. {
  233. // overwrite the count
  234. $delimiterCount = $newCount;
  235. $delimiter = ',';
  236. }
  237. }
  238. // check for semicolon delimiter
  239. if(preg_match_all('/;/', $string, $matches))
  240. {
  241. // count the commas
  242. $newCount = count($matches[0]);
  243. // replace the delimiter, if need be
  244. if($delimiterCount < $newCount)
  245. {
  246. // overwrite the count
  247. $delimiterCount = $newCount;
  248. $delimiter = ';';
  249. }
  250. }
  251. // check for tab delimiter
  252. if(preg_match_all('/[\t]/', $string, $matches))
  253. {
  254. // count the commas
  255. $newCount = count($matches[0]);
  256. // replace the delimiter, if need be
  257. if($delimiterCount < $newCount)
  258. {
  259. // overwrite the count
  260. $delimiterCount = $newCount;
  261. $delimiter = '\t';
  262. }
  263. }
  264. // if delimiter is empty it might mean there is only one column
  265. // check for double quotes enclosure
  266. if(preg_match_all('/"/', $string, $matches))
  267. {
  268. // count the commas
  269. $newCount = count($matches[0]);
  270. // replace the delimiter, if need be
  271. if($enclosureCount < $newCount)
  272. {
  273. // overwrite the count
  274. $enclosureCount = $newCount;
  275. $enclosure = '"';
  276. }
  277. }
  278. // check for single quotes enclosure
  279. if(preg_match_all("/'/", $string, $matches))
  280. {
  281. // count the commas
  282. $newCount = count($matches[0]);
  283. // replace the delimiter, if need be
  284. if($enclosureCount < $newCount)
  285. {
  286. // overwrite the count
  287. $enclosureCount = $newCount;
  288. $enclosure = "'";
  289. }
  290. }
  291. // return the results
  292. return array($delimiter, $enclosure);
  293. }
  294. /**
  295. * Converts a CSV-formatted string to an array
  296. *
  297. * @return array
  298. * @param string $string The string you wish to convert to an array.
  299. * @param array[optional] $columns The column names you want to use.
  300. * @param array[optional] $excludeColumns The columns to exclude.
  301. * @param string[optional] $delimiter The field delimiter of the CSV.
  302. * @param string[optional] $enclosure The enclosure character of the CSV.
  303. */
  304. public static function stringToArray($string, array $columns = array(), array $excludeColumns = null, $delimiter = ',', $enclosure = '"')
  305. {
  306. // reset variables
  307. $string = (string) $string;
  308. $filename = dirname(__FILE__) . '/' . uniqid();
  309. // save a tempfile
  310. SpoonFile::setContent($filename, $string);
  311. // return the file to array
  312. $array = self::fileToArray($filename, $columns, $excludeColumns, $delimiter, $enclosure);
  313. // remove the created file
  314. SpoonFile::delete($filename);
  315. // return the array
  316. return $array;
  317. }
  318. }