1 | <?php |
||
2 | |||
3 | namespace SilverStripe\Dev; |
||
4 | |||
5 | use League\Csv\Reader; |
||
6 | use SilverStripe\Core\Injector\Injectable; |
||
7 | use Iterator; |
||
8 | |||
9 | use SilverStripe\Control\Director; |
||
10 | |||
11 | /** |
||
12 | * Class to handle parsing of CSV files, where the column headers are in the |
||
13 | * first row. |
||
14 | * |
||
15 | * The idea is that you pass it another object to handle the actual processing |
||
16 | * of the data in the CSV file. |
||
17 | * |
||
18 | * Usage: |
||
19 | * |
||
20 | * <code> |
||
21 | * $parser = new CSVParser('myfile.csv'); |
||
22 | * $parser->mapColumns(array( |
||
23 | * 'first name' => 'FirstName', |
||
24 | * 'lastname' => 'Surname', |
||
25 | * 'last name' => 'Surname', |
||
26 | * )); |
||
27 | * foreach($parser as $row) { |
||
28 | * // $row is a map of column name => column value |
||
29 | * $obj = new MyDataObject(); |
||
30 | * $obj->update($row); |
||
31 | * $obj->write(); |
||
32 | * } |
||
33 | * </code> |
||
34 | */ |
||
35 | class CSVParser implements Iterator |
||
36 | { |
||
37 | use Injectable; |
||
38 | |||
39 | /** |
||
40 | * @var string $filename |
||
41 | */ |
||
42 | protected $filename; |
||
43 | |||
44 | /** |
||
45 | * @var resource $fileHandle |
||
46 | */ |
||
47 | protected $fileHandle; |
||
48 | |||
49 | /** |
||
50 | * Map of source columns to output columns. |
||
51 | * |
||
52 | * Once they get into this variable, all of the source columns are in |
||
53 | * lowercase. |
||
54 | * |
||
55 | * @var array |
||
56 | */ |
||
57 | protected $columnMap = array(); |
||
58 | |||
59 | /** |
||
60 | * The header row used to map data in the CSV file. |
||
61 | * |
||
62 | * To begin with, this is null. Once it has been set, data will get |
||
63 | * returned from the CSV file. |
||
64 | * |
||
65 | * @var array |
||
66 | */ |
||
67 | protected $headerRow = null; |
||
68 | |||
69 | /** |
||
70 | * A custom header row provided by the caller. |
||
71 | * |
||
72 | * @var array |
||
73 | */ |
||
74 | protected $providedHeaderRow = null; |
||
75 | |||
76 | /** |
||
77 | * The data of the current row. |
||
78 | * |
||
79 | * @var array |
||
80 | */ |
||
81 | protected $currentRow = null; |
||
82 | |||
83 | /** |
||
84 | * The current row number. |
||
85 | * |
||
86 | * 1 is the first data row in the CSV file; the header row, if it exists, |
||
87 | * is ignored. |
||
88 | * |
||
89 | * @var int |
||
90 | */ |
||
91 | protected $rowNum = 0; |
||
92 | |||
93 | /** |
||
94 | * The character for separating columns. |
||
95 | * |
||
96 | * @var string |
||
97 | */ |
||
98 | protected $delimiter = ","; |
||
99 | |||
100 | /** |
||
101 | * The character for quoting columns. |
||
102 | * |
||
103 | * @var string |
||
104 | */ |
||
105 | protected $enclosure = '"'; |
||
106 | |||
107 | /** |
||
108 | * Open a CSV file for parsing. |
||
109 | * |
||
110 | * You can use the object returned in a foreach loop to extract the data. |
||
111 | * |
||
112 | * @param string $filename The name of the file. If relative, it will be relative to the site's base dir |
||
113 | * @param string $delimiter The character for seperating columns |
||
114 | * @param string $enclosure The character for quoting or enclosing columns |
||
115 | */ |
||
116 | public function __construct($filename, $delimiter = ",", $enclosure = '"') |
||
117 | { |
||
118 | Deprecation::notice('5.0', __CLASS__ . ' is deprecated, use ' . Reader::class . ' instead'); |
||
119 | $filename = Director::getAbsFile($filename); |
||
120 | $this->filename = $filename; |
||
121 | $this->delimiter = $delimiter; |
||
122 | $this->enclosure = $enclosure; |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * Re-map columns in the CSV file. |
||
127 | * |
||
128 | * This can be useful for identifying synonyms in the file. For example: |
||
129 | * |
||
130 | * <code> |
||
131 | * $csv->mapColumns(array( |
||
132 | * 'firstname' => 'FirstName', |
||
133 | * 'last name' => 'Surname', |
||
134 | * )); |
||
135 | * </code> |
||
136 | * |
||
137 | * @param array |
||
138 | */ |
||
139 | public function mapColumns($columnMap) |
||
140 | { |
||
141 | if ($columnMap) { |
||
142 | $lowerColumnMap = array(); |
||
143 | |||
144 | foreach ($columnMap as $k => $v) { |
||
145 | $lowerColumnMap[strtolower($k)] = $v; |
||
146 | } |
||
147 | |||
148 | $this->columnMap = array_merge($this->columnMap, $lowerColumnMap); |
||
149 | } |
||
150 | } |
||
151 | |||
152 | /** |
||
153 | * If your CSV file doesn't have a header row, then you can call this |
||
154 | * function to provide one. |
||
155 | * |
||
156 | * If you call this function, then the first row of the CSV will be |
||
157 | * included in the data returned. |
||
158 | * |
||
159 | * @param array |
||
160 | */ |
||
161 | public function provideHeaderRow($headerRow) |
||
162 | { |
||
163 | $this->providedHeaderRow = $headerRow; |
||
164 | } |
||
165 | |||
166 | /** |
||
167 | * Open the CSV file for reading. |
||
168 | */ |
||
169 | protected function openFile() |
||
170 | { |
||
171 | ini_set('auto_detect_line_endings', 1); |
||
172 | $this->fileHandle = fopen($this->filename, 'r'); |
||
0 ignored issues
–
show
|
|||
173 | |||
174 | if ($this->providedHeaderRow) { |
||
0 ignored issues
–
show
The expression
$this->providedHeaderRow of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||
175 | $this->headerRow = $this->remapHeader($this->providedHeaderRow); |
||
176 | } |
||
177 | } |
||
178 | |||
179 | /** |
||
180 | * Close the CSV file and re-set all of the internal variables. |
||
181 | */ |
||
182 | protected function closeFile() |
||
183 | { |
||
184 | if ($this->fileHandle) { |
||
185 | fclose($this->fileHandle); |
||
186 | } |
||
187 | |||
188 | $this->fileHandle = null; |
||
189 | $this->rowNum = 0; |
||
190 | $this->currentRow = null; |
||
191 | $this->headerRow = null; |
||
192 | } |
||
193 | |||
194 | |||
195 | /** |
||
196 | * Get a header row from the CSV file. |
||
197 | */ |
||
198 | protected function fetchCSVHeader() |
||
199 | { |
||
200 | $srcRow = fgetcsv( |
||
201 | $this->fileHandle, |
||
202 | 0, |
||
203 | $this->delimiter, |
||
204 | $this->enclosure |
||
205 | ); |
||
206 | |||
207 | $this->headerRow = $this->remapHeader($srcRow); |
||
208 | } |
||
209 | |||
210 | /** |
||
211 | * Map the contents of a header array using $this->mappedColumns. |
||
212 | * |
||
213 | * @param array |
||
214 | * |
||
215 | * @return array |
||
216 | */ |
||
217 | protected function remapHeader($header) |
||
218 | { |
||
219 | $mappedHeader = array(); |
||
220 | |||
221 | foreach ($header as $item) { |
||
222 | if (isset($this->columnMap[strtolower($item)])) { |
||
223 | $item = $this->columnMap[strtolower($item)]; |
||
224 | } |
||
225 | |||
226 | $mappedHeader[] = $item; |
||
227 | } |
||
228 | return $mappedHeader; |
||
229 | } |
||
230 | |||
231 | /** |
||
232 | * Get a row from the CSV file and update $this->currentRow; |
||
233 | * |
||
234 | * @return array |
||
235 | */ |
||
236 | protected function fetchCSVRow() |
||
237 | { |
||
238 | if (!$this->fileHandle) { |
||
239 | $this->openFile(); |
||
240 | } |
||
241 | |||
242 | if (!$this->headerRow) { |
||
0 ignored issues
–
show
The expression
$this->headerRow of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||
243 | $this->fetchCSVHeader(); |
||
244 | } |
||
245 | |||
246 | $this->rowNum++; |
||
247 | |||
248 | $srcRow = fgetcsv( |
||
249 | $this->fileHandle, |
||
250 | 0, |
||
251 | $this->delimiter, |
||
252 | $this->enclosure |
||
253 | ); |
||
254 | |||
255 | if ($srcRow) { |
||
256 | $row = array(); |
||
257 | |||
258 | foreach ($srcRow as $i => $value) { |
||
259 | // Allow escaping of quotes and commas in the data |
||
260 | $value = str_replace( |
||
261 | array('\\' . $this->enclosure,'\\' . $this->delimiter), |
||
262 | array($this->enclosure, $this->delimiter), |
||
263 | $value |
||
264 | ); |
||
265 | // Trim leading tab |
||
266 | // [SS-2017-007] Ensure all cells with leading [@=+] have a leading tab |
||
267 | $value = ltrim($value, "\t"); |
||
268 | if (array_key_exists($i, $this->headerRow)) { |
||
269 | if ($this->headerRow[$i]) { |
||
270 | $row[$this->headerRow[$i]] = $value; |
||
271 | } |
||
272 | } else { |
||
273 | user_error("No heading for column $i on row $this->rowNum", E_USER_WARNING); |
||
274 | } |
||
275 | } |
||
276 | |||
277 | $this->currentRow = $row; |
||
278 | } else { |
||
279 | $this->closeFile(); |
||
280 | } |
||
281 | |||
282 | return $this->currentRow; |
||
283 | } |
||
284 | |||
285 | /** |
||
286 | * @ignore |
||
287 | */ |
||
288 | public function __destruct() |
||
289 | { |
||
290 | $this->closeFile(); |
||
291 | } |
||
292 | |||
293 | //// ITERATOR FUNCTIONS |
||
294 | |||
295 | /** |
||
296 | * @ignore |
||
297 | */ |
||
298 | public function rewind() |
||
299 | { |
||
300 | $this->closeFile(); |
||
301 | $this->fetchCSVRow(); |
||
302 | } |
||
303 | |||
304 | /** |
||
305 | * @ignore |
||
306 | */ |
||
307 | public function current() |
||
308 | { |
||
309 | return $this->currentRow; |
||
310 | } |
||
311 | |||
312 | /** |
||
313 | * @ignore |
||
314 | */ |
||
315 | public function key() |
||
316 | { |
||
317 | return $this->rowNum; |
||
318 | } |
||
319 | |||
320 | /** |
||
321 | * @ignore |
||
322 | */ |
||
323 | public function next() |
||
324 | { |
||
325 | $this->fetchCSVRow(); |
||
326 | |||
327 | return $this->currentRow; |
||
328 | } |
||
329 | |||
330 | /** |
||
331 | * @ignore |
||
332 | */ |
||
333 | public function valid() |
||
334 | { |
||
335 | return $this->currentRow ? true : false; |
||
336 | } |
||
337 | } |
||
338 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.