FeedsCSVParser.inc 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. <?php
  2. /**
  3. * @file
  4. * Contains the FeedsCSVParser class.
  5. */
  6. /**
  7. * Parses a given file as a CSV file.
  8. */
  9. class FeedsCSVParser extends FeedsParser {
  10. /**
  11. * Implements FeedsParser::parse().
  12. */
  13. public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
  14. $source_config = $source->getConfigFor($this);
  15. $state = $source->state(FEEDS_PARSE);
  16. // Load and configure parser.
  17. feeds_include_library('ParserCSV.inc', 'ParserCSV');
  18. $parser = new ParserCSV();
  19. $delimiter = $source_config['delimiter'] == 'TAB' ? "\t" : $source_config['delimiter'];
  20. $parser->setDelimiter($delimiter);
  21. $iterator = new ParserCSVIterator($fetcher_result->getFilePath());
  22. if (empty($source_config['no_headers'])) {
  23. // Get first line and use it for column names, convert them to lower case.
  24. $header = $this->parseHeader($parser, $iterator);
  25. if (!$header) {
  26. return;
  27. }
  28. $parser->setColumnNames($header);
  29. }
  30. // Determine section to parse, parse.
  31. $start = $state->pointer ? $state->pointer : $parser->lastLinePos();
  32. $limit = $source->importer->getLimit();
  33. $rows = $this->parseItems($parser, $iterator, $start, $limit);
  34. // Report progress.
  35. $state->total = filesize($fetcher_result->getFilePath());
  36. $state->pointer = $parser->lastLinePos();
  37. $progress = $parser->lastLinePos() ? $parser->lastLinePos() : $state->total;
  38. $state->progress($state->total, $progress);
  39. // Create a result object and return it.
  40. return new FeedsParserResult($rows, $source->feed_nid);
  41. }
  42. /**
  43. * Get first line and use it for column names, convert them to lower case.
  44. * Be aware that the $parser and iterator objects can be modified in this
  45. * function since they are passed in by reference
  46. *
  47. * @param ParserCSV $parser
  48. * @param ParserCSVIterator $iterator
  49. * @return
  50. * An array of lower-cased column names to use as keys for the parsed items.
  51. */
  52. protected function parseHeader(ParserCSV $parser, ParserCSVIterator $iterator) {
  53. $parser->setLineLimit(1);
  54. $rows = $parser->parse($iterator);
  55. if (!count($rows)) {
  56. return FALSE;
  57. }
  58. $header = array_shift($rows);
  59. foreach ($header as $i => $title) {
  60. $header[$i] = trim(drupal_strtolower($title));
  61. }
  62. return $header;
  63. }
  64. /**
  65. * Parse all of the items from the CSV.
  66. *
  67. * @param ParserCSV $parser
  68. * @param ParserCSVIterator $iterator
  69. * @return
  70. * An array of rows of the CSV keyed by the column names previously set
  71. */
  72. protected function parseItems(ParserCSV $parser, ParserCSVIterator $iterator, $start = 0, $limit = 0) {
  73. $parser->setLineLimit($limit);
  74. $parser->setStartByte($start);
  75. $rows = $parser->parse($iterator);
  76. return $rows;
  77. }
  78. /**
  79. * Override parent::getMappingSources().
  80. */
  81. public function getMappingSources() {
  82. return FALSE;
  83. }
  84. /**
  85. * Override parent::getSourceElement() to use only lower keys.
  86. */
  87. public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
  88. return parent::getSourceElement($source, $result, drupal_strtolower($element_key));
  89. }
  90. /**
  91. * Define defaults.
  92. */
  93. public function sourceDefaults() {
  94. return array(
  95. 'delimiter' => $this->config['delimiter'],
  96. 'no_headers' => $this->config['no_headers'],
  97. );
  98. }
  99. /**
  100. * Source form.
  101. *
  102. * Show mapping configuration as a guidance for import form users.
  103. */
  104. public function sourceForm($source_config) {
  105. $form = array();
  106. $form['#weight'] = -10;
  107. $mappings = feeds_importer($this->id)->processor->config['mappings'];
  108. $sources = $uniques = array();
  109. foreach ($mappings as $mapping) {
  110. $sources[] = check_plain($mapping['source']);
  111. if ($mapping['unique']) {
  112. $uniques[] = check_plain($mapping['source']);
  113. }
  114. }
  115. $output = t('Import !csv_files with one or more of these columns: !columns.', array('!csv_files' => l(t('CSV files'), 'http://en.wikipedia.org/wiki/Comma-separated_values'), '!columns' => implode(', ', $sources)));
  116. $items = array();
  117. $items[] = format_plural(count($uniques), t('Column <strong>!column</strong> is mandatory and considered unique: only one item per !column value will be created.', array('!column' => implode(', ', $uniques))), t('Columns <strong>!columns</strong> are mandatory and values in these columns are considered unique: only one entry per value in one of these column will be created.', array('!columns' => implode(', ', $uniques))));
  118. $items[] = l(t('Download a template'), 'import/' . $this->id . '/template');
  119. $form['help']['#markup'] = '<div class="help"><p>' . $output . '</p>' . theme('item_list', array('items' => $items)) . '</div>';
  120. $form['delimiter'] = array(
  121. '#type' => 'select',
  122. '#title' => t('Delimiter'),
  123. '#description' => t('The character that delimits fields in the CSV file.'),
  124. '#options' => array(
  125. ',' => ',',
  126. ';' => ';',
  127. 'TAB' => 'TAB',
  128. ),
  129. '#default_value' => isset($source_config['delimiter']) ? $source_config['delimiter'] : ',',
  130. );
  131. $form['no_headers'] = array(
  132. '#type' => 'checkbox',
  133. '#title' => t('No Headers'),
  134. '#description' => t('Check if the imported CSV file does not start with a header row. If checked, mapping sources must be named \'0\', \'1\', \'2\' etc.'),
  135. '#default_value' => isset($source_config['no_headers']) ? $source_config['no_headers'] : 0,
  136. );
  137. return $form;
  138. }
  139. /**
  140. * Define default configuration.
  141. */
  142. public function configDefaults() {
  143. return array(
  144. 'delimiter' => ',',
  145. 'no_headers' => 0,
  146. );
  147. }
  148. /**
  149. * Build configuration form.
  150. */
  151. public function configForm(&$form_state) {
  152. $form = array();
  153. $form['delimiter'] = array(
  154. '#type' => 'select',
  155. '#title' => t('Default delimiter'),
  156. '#description' => t('Default field delimiter.'),
  157. '#options' => array(
  158. ',' => ',',
  159. ';' => ';',
  160. 'TAB' => 'TAB',
  161. ),
  162. '#default_value' => $this->config['delimiter'],
  163. );
  164. $form['no_headers'] = array(
  165. '#type' => 'checkbox',
  166. '#title' => t('No headers'),
  167. '#description' => t('Check if the imported CSV file does not start with a header row. If checked, mapping sources must be named \'0\', \'1\', \'2\' etc.'),
  168. '#default_value' => $this->config['no_headers'],
  169. );
  170. return $form;
  171. }
  172. public function getTemplate() {
  173. $mappings = feeds_importer($this->id)->processor->config['mappings'];
  174. $sources = $uniques = array();
  175. foreach ($mappings as $mapping) {
  176. if ($mapping['unique']) {
  177. $uniques[] = check_plain($mapping['source']);
  178. }
  179. else {
  180. $sources[] = check_plain($mapping['source']);
  181. }
  182. }
  183. $sep = ',';
  184. $columns = array();
  185. foreach (array_merge($uniques, $sources) as $col) {
  186. if (strpos($col, $sep) !== FALSE) {
  187. $col = '"' . str_replace('"', '""', $col) . '"';
  188. }
  189. $columns[] = $col;
  190. }
  191. drupal_add_http_header('Cache-Control', 'max-age=60, must-revalidate');
  192. drupal_add_http_header('Content-Disposition', 'attachment; filename="' . $this->id . '_template.csv"');
  193. drupal_add_http_header('Content-type', 'text/csv; charset=utf-8');
  194. print implode($sep, $columns);
  195. return;
  196. }
  197. }