|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Carbon_CSV; |
| 4 | +use \SplFileObject as File; |
| 5 | + |
| 6 | +/** |
| 7 | + * Enhanced CSV file object |
| 8 | + */ |
| 9 | +class CsvFile extends File implements \Countable { |
| 10 | +private $file_path; |
| 11 | +private $encoding = 'utf-8'; |
| 12 | +private $is_head_row = false; |
| 13 | +/** |
| 14 | + * Current row. |
| 15 | + */ |
| 16 | +private $row_counter = 0; |
| 17 | +private $column_names; |
| 18 | +private $uses_column_names = false; |
| 19 | +private $offset_row = 0; |
| 20 | +private $start_column = 0; |
| 21 | +private $columns_to_skip = array(); |
| 22 | + |
| 23 | +function __construct($file_path, $delimiter = ',', $enclosure = '"', $escape = "\\") { |
| 24 | +if (!file_exists($file_path)) { |
| 25 | +throw new Exception("File $file_path does not exist. "); |
| 26 | +} |
| 27 | + |
| 28 | +if (filesize($file_path) === 0) { |
| 29 | +throw new Exception("Empty file. "); |
| 30 | +} |
| 31 | + |
| 32 | +$this->file_path = $file_path; |
| 33 | +parent::__construct($file_path, 'r'); |
| 34 | +$this->setFlags(File::READ_CSV | File::READ_AHEAD | File::SKIP_EMPTY | File::DROP_NEW_LINE); |
| 35 | +$this->setCsvControl($delimiter, $enclosure, $escape); |
| 36 | +} |
| 37 | + |
| 38 | +/** |
| 39 | + * Read number of lines in CSV |
| 40 | + * @return int number of lines |
| 41 | + */ |
| 42 | +function count() { |
| 43 | +return count($this->to_array()); |
| 44 | +} |
| 45 | + |
| 46 | +public function to_array() { |
| 47 | +$rows = []; |
| 48 | +foreach ($this as $row) { |
| 49 | +$rows[] = $row; |
| 50 | +} |
| 51 | + |
| 52 | +return $rows; |
| 53 | +} |
| 54 | + |
| 55 | +public function rewind() { |
| 56 | +$this->seek($this->offset_row); |
| 57 | +} |
| 58 | + |
| 59 | +/** |
| 60 | + * Override the key function in order to allow shifting in indecies according |
| 61 | + * to the current offset. |
| 62 | + */ |
| 63 | +public function key() { |
| 64 | +return $this->row_counter - 1; |
| 65 | +} |
| 66 | + |
| 67 | +public function current() { |
| 68 | +$this->row_counter++; |
| 69 | +$row = parent::current(); |
| 70 | + |
| 71 | +$row_keys = array_keys($row); |
| 72 | +if (!in_array($this->start_column, $row_keys)) { |
| 73 | +throw new Exception(sprintf('Start column must be between %d and %d.', min($row_keys), max($row_keys))); |
| 74 | +} |
| 75 | + |
| 76 | +$formatted_row = $this->format_row($row); |
| 77 | + |
| 78 | +return $formatted_row; |
| 79 | +} |
| 80 | + |
| 81 | +private function remove_columns($old_row) { |
| 82 | +$new_row = array(); |
| 83 | + |
| 84 | +$index = 0; |
| 85 | +foreach ($old_row as $column_name => $column_value) { |
| 86 | +if (!in_array($index, $this->columns_to_skip)) { |
| 87 | +$new_row[$column_name] = $column_value; |
| 88 | +} |
| 89 | + |
| 90 | +$index++; |
| 91 | +} |
| 92 | + |
| 93 | +return $new_row; |
| 94 | +} |
| 95 | + |
| 96 | +private function format_row($row) { |
| 97 | +$row = array_combine( |
| 98 | +$this->get_column_names($row), |
| 99 | +$row |
| 100 | +); |
| 101 | + |
| 102 | +// don't remove columns from the head row |
| 103 | +// we remove columns after the row is combined with the header columns |
| 104 | +if (!$this->is_head_row) { |
| 105 | +$row = $this->remove_columns($row); |
| 106 | +} |
| 107 | + |
| 108 | +if (!$this->uses_column_names) { |
| 109 | +$row = array_values($row); |
| 110 | +} |
| 111 | + |
| 112 | +return $row; |
| 113 | +} |
| 114 | + |
| 115 | +private function get_column_names($row) { |
| 116 | +if (!empty($this->column_names)) { |
| 117 | +return $this->column_names; |
| 118 | +} |
| 119 | + |
| 120 | +return array_keys($row); |
| 121 | +} |
| 122 | + |
| 123 | +public function set_column_names($mapping) { |
| 124 | +$this->uses_column_names = true; |
| 125 | + |
| 126 | +if (empty($this->column_names)) { |
| 127 | +$this->column_names = $mapping; |
| 128 | +} else { |
| 129 | +$this->column_names = array_combine( |
| 130 | +array_flip($this->column_names), |
| 131 | +$mapping |
| 132 | +); |
| 133 | +} |
| 134 | +} |
| 135 | + |
| 136 | +public function use_first_row_as_header() { |
| 137 | +if ($this->row_counter !== 0) { |
| 138 | +throw new \LogicException("Column mapping can't be changed after CSV processing has been started"); |
| 139 | +} |
| 140 | + |
| 141 | +$this->uses_column_names = true; |
| 142 | + |
| 143 | +$this->is_head_row = true; |
| 144 | +$this->column_names = $this->current(); |
| 145 | +$this->is_head_row = false; |
| 146 | + |
| 147 | +// Start processing from the second row(since the first one isn't part of the data) |
| 148 | +$this->offset_row++; |
| 149 | +$this->rewind(); |
| 150 | +} |
| 151 | + |
| 152 | +public function skip_to_row($row) { |
| 153 | +$this->offset_row = $row; |
| 154 | +$this->rewind(); |
| 155 | +} |
| 156 | + |
| 157 | +public function skip_columns($indexes) { |
| 158 | +$this->set_columns_to_skip($indexes); |
| 159 | +} |
| 160 | + |
| 161 | +public function skip_to_column($column_index) { |
| 162 | +if (!is_int($column_index)) { |
| 163 | +throw new Exception('Only numbers are allowed for skip to column.'); |
| 164 | +} |
| 165 | + |
| 166 | +if ($column_index < 0) { |
| 167 | +throw new Exception('Please use numbers larger than zero.'); |
| 168 | +} |
| 169 | + |
| 170 | +$this->start_column = $column_index; |
| 171 | + |
| 172 | +// this is to handle the strange case, when the user wants to start from the first column (which happens by default) |
| 173 | +if ($column_index === 0) { |
| 174 | +$last_column_index = 0; |
| 175 | +} else { |
| 176 | +$last_column_index = $column_index - 1; |
| 177 | +} |
| 178 | + |
| 179 | +$this->set_columns_to_skip(range(0, $last_column_index)); |
| 180 | +} |
| 181 | + |
| 182 | +private function set_columns_to_skip($columns) { |
| 183 | +$this->columns_to_skip = array_unique(array_merge($columns, $this->columns_to_skip)); |
| 184 | +} |
| 185 | +} |
0 commit comments