Issues (2882)

src/Dev/CSVParser.php (3 issues)

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
Documentation Bug introduced by
It seems like fopen($this->filename, 'r') can also be of type false. However, the property $fileHandle is declared as type resource. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
173
174
        if ($this->providedHeaderRow) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
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
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
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