Passed
Push — master ( 8d8bc2...886a8c )
by
unknown
17:08 queued 09:10
created

Csv::getOutputEncoding()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9
10
class Csv extends BaseWriter
11
{
12
    /**
13
     * PhpSpreadsheet object.
14
     */
15
    private Spreadsheet $spreadsheet;
16
17
    /**
18
     * Delimiter.
19
     */
20
    private string $delimiter = ',';
21
22
    /**
23
     * Enclosure.
24
     */
25
    private string $enclosure = '"';
26
27
    /**
28
     * Line ending.
29
     */
30
    private string $lineEnding = PHP_EOL;
31
32
    /**
33
     * Sheet index to write.
34
     */
35
    private int $sheetIndex = 0;
36
37
    /**
38
     * Whether to write a UTF8 BOM.
39
     */
40
    private bool $useBOM = false;
41
42
    /**
43
     * Whether to write a Separator line as the first line of the file
44
     *     sep=x.
45
     */
46
    private bool $includeSeparatorLine = false;
47
48
    /**
49
     * Whether to write a fully Excel compatible CSV file.
50
     */
51
    private bool $excelCompatibility = false;
52
53
    /**
54
     * Output encoding.
55
     */
56
    private string $outputEncoding = '';
57
58
    /**
59
     * Whether number of columns should be allowed to vary
60
     * between rows, or use a fixed range based on the max
61
     * column overall.
62
     */
63
    private bool $variableColumns = false;
64
65
    private bool $preferHyperlinkToLabel = false;
66
67
    /**
68
     * Create a new CSV.
69
     */
70 33
    public function __construct(Spreadsheet $spreadsheet)
71
    {
72 33
        $this->spreadsheet = $spreadsheet;
73
    }
74
75
    /**
76
     * Save PhpSpreadsheet to file.
77
     *
78
     * @param resource|string $filename
79
     */
80 31
    public function save($filename, int $flags = 0): void
81
    {
82 31
        $this->processFlags($flags);
83
84
        // Fetch sheet
85 31
        $sheet = $this->spreadsheet->getSheet($this->sheetIndex);
86
87 31
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
88 31
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
89 31
        $sheet->calculateArrays($this->preCalculateFormulas);
90
91
        // Open file
92 31
        $this->openFileHandle($filename);
93
94 30
        if ($this->excelCompatibility) {
95 1
            $this->setUseBOM(true); //  Enforce UTF-8 BOM Header
96 1
            $this->setIncludeSeparatorLine(true); //  Set separator line
97 1
            $this->setEnclosure('"'); //  Set enclosure to "
98 1
            $this->setDelimiter(';'); //  Set delimiter to a semi-colon
99 1
            $this->setLineEnding("\r\n");
100
        }
101
102 30
        if ($this->useBOM) {
103
            // Write the UTF-8 BOM code if required
104 3
            fwrite($this->fileHandle, "\xEF\xBB\xBF");
105
        }
106
107 30
        if ($this->includeSeparatorLine) {
108
            // Write the separator line if required
109 1
            fwrite($this->fileHandle, 'sep=' . $this->getDelimiter() . $this->lineEnding);
110
        }
111
112
        //    Identify the range that we need to extract from the worksheet
113 30
        $maxCol = $sheet->getHighestDataColumn();
114 30
        $maxRow = $sheet->getHighestDataRow();
115
116
        // Write rows to file
117 30
        $row = 0;
118 30
        foreach ($sheet->rangeToArrayYieldRows("A1:$maxCol$maxRow", '', $this->preCalculateFormulas) as $cellsArray) {
119 30
            ++$row;
120 30
            if ($this->variableColumns) {
121 1
                $column = $sheet->getHighestDataColumn($row);
122 1
                if ($column === 'A' && !$sheet->cellExists("A$row")) {
123 1
                    $cellsArray = [];
124
                } else {
125 1
                    array_splice($cellsArray, Coordinate::columnIndexFromString($column));
126
                }
127
            }
128 30
            if ($this->preferHyperlinkToLabel) {
129 1
                foreach ($cellsArray as $key => $value) {
130 1
                    $url = $sheet->getCell([$key + 1, $row])->getHyperlink()->getUrl();
131 1
                    if ($url !== '') {
132 1
                        $cellsArray[$key] = $url;
133
                    }
134
                }
135
            }
136
            /** @var string[] $cellsArray */
137 30
            $this->writeLine($this->fileHandle, $cellsArray);
138
        }
139
140 30
        $this->maybeCloseFileHandle();
141 30
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
142
    }
143
144 1
    public function getDelimiter(): string
145
    {
146 1
        return $this->delimiter;
147
    }
148
149 10
    public function setDelimiter(string $delimiter): self
150
    {
151 10
        $this->delimiter = $delimiter;
152
153 10
        return $this;
154
    }
155
156 2
    public function getEnclosure(): string
157
    {
158 2
        return $this->enclosure;
159
    }
160
161 10
    public function setEnclosure(string $enclosure = '"'): self
162
    {
163 10
        $this->enclosure = $enclosure;
164
165 10
        return $this;
166
    }
167
168 1
    public function getLineEnding(): string
169
    {
170 1
        return $this->lineEnding;
171
    }
172
173 2
    public function setLineEnding(string $lineEnding): self
174
    {
175 2
        $this->lineEnding = $lineEnding;
176
177 2
        return $this;
178
    }
179
180
    /**
181
     * Get whether BOM should be used.
182
     */
183 1
    public function getUseBOM(): bool
184
    {
185 1
        return $this->useBOM;
186
    }
187
188
    /**
189
     * Set whether BOM should be used, typically when non-ASCII characters are used.
190
     */
191 4
    public function setUseBOM(bool $useBOM): self
192
    {
193 4
        $this->useBOM = $useBOM;
194
195 4
        return $this;
196
    }
197
198
    /**
199
     * Get whether a separator line should be included.
200
     */
201 1
    public function getIncludeSeparatorLine(): bool
202
    {
203 1
        return $this->includeSeparatorLine;
204
    }
205
206
    /**
207
     * Set whether a separator line should be included as the first line of the file.
208
     */
209 1
    public function setIncludeSeparatorLine(bool $includeSeparatorLine): self
210
    {
211 1
        $this->includeSeparatorLine = $includeSeparatorLine;
212
213 1
        return $this;
214
    }
215
216
    /**
217
     * Get whether the file should be saved with full Excel Compatibility.
218
     */
219 1
    public function getExcelCompatibility(): bool
220
    {
221 1
        return $this->excelCompatibility;
222
    }
223
224
    /**
225
     * Set whether the file should be saved with full Excel Compatibility.
226
     *
227
     * @param bool $excelCompatibility Set the file to be written as a fully Excel compatible csv file
228
     *                                Note that this overrides other settings such as useBOM, enclosure and delimiter
229
     */
230 1
    public function setExcelCompatibility(bool $excelCompatibility): self
231
    {
232 1
        $this->excelCompatibility = $excelCompatibility;
233
234 1
        return $this;
235
    }
236
237 2
    public function getSheetIndex(): int
238
    {
239 2
        return $this->sheetIndex;
240
    }
241
242 2
    public function setSheetIndex(int $sheetIndex): self
243
    {
244 2
        $this->sheetIndex = $sheetIndex;
245
246 2
        return $this;
247
    }
248
249 1
    public function getOutputEncoding(): string
250
    {
251 1
        return $this->outputEncoding;
252
    }
253
254 1
    public function setOutputEncoding(string $outputEnconding): self
255
    {
256 1
        $this->outputEncoding = $outputEnconding;
257
258 1
        return $this;
259
    }
260
261
    private bool $enclosureRequired = true;
262
263 5
    public function setEnclosureRequired(bool $value): self
264
    {
265 5
        $this->enclosureRequired = $value;
266
267 5
        return $this;
268
    }
269
270 3
    public function getEnclosureRequired(): bool
271
    {
272 3
        return $this->enclosureRequired;
273
    }
274
275
    /**
276
     * Write line to CSV file.
277
     *
278
     * @param resource $fileHandle PHP filehandle
279
     * @param string[] $values Array containing values in a row
280
     */
281 30
    private function writeLine($fileHandle, array $values): void
282
    {
283
        // No leading delimiter
284 30
        $delimiter = '';
285
286
        // Build the line
287 30
        $line = '';
288
289 30
        foreach ($values as $element) {
290 30
            if (Preg::isMatch('/^([+-])?(\d+)[.](\d+)/', $element, $matches)) {
291
                // Excel will "convert" file with pop-up
292
                // if there are more than 15 digits precision.
293 9
                $whole = $matches[2];
294 9
                if ($whole !== '0') {
295 8
                    $wholeLen = strlen($whole);
296 8
                    $frac = $matches[3];
297 8
                    $maxFracLen = 15 - $wholeLen;
298 8
                    if ($maxFracLen >= 0 && strlen($frac) > $maxFracLen) {
299 2
                        $result = sprintf("%.{$maxFracLen}F", $element);
300 2
                        if (str_contains($result, '.')) {
301 2
                            $element = Preg::replace('/[.]?0+$/', '', $result); // strip trailing zeros
302
                        }
303
                    }
304
                }
305
            }
306
            // Add delimiter
307 30
            $line .= $delimiter;
308 30
            $delimiter = $this->delimiter;
309
            // Escape enclosures
310 30
            $enclosure = $this->enclosure;
311 30
            if ($enclosure) {
312
                // If enclosure is not required, use enclosure only if
313
                // element contains newline, delimiter, or enclosure.
314 28
                if (!$this->enclosureRequired && strpbrk($element, "$delimiter$enclosure\n") === false) {
315 4
                    $enclosure = '';
316
                } else {
317 26
                    $element = str_replace($enclosure, $enclosure . $enclosure, $element);
318
                }
319
            }
320
            // Add enclosed string
321 30
            $line .= $enclosure . $element . $enclosure;
322
        }
323
324
        // Add line ending
325 30
        $line .= $this->lineEnding;
326
327
        // Write to file
328 30
        if ($this->outputEncoding != '') {
329 1
            $line = (string) mb_convert_encoding($line, $this->outputEncoding);
330
        }
331 30
        fwrite($fileHandle, $line);
332
    }
333
334
    /**
335
     * Get whether number of columns should be allowed to vary
336
     * between rows, or use a fixed range based on the max
337
     * column overall.
338
     */
339 1
    public function getVariableColumns(): bool
340
    {
341 1
        return $this->variableColumns;
342
    }
343
344
    /**
345
     * Set whether number of columns should be allowed to vary
346
     * between rows, or use a fixed range based on the max
347
     * column overall.
348
     */
349 1
    public function setVariableColumns(bool $pValue): self
350
    {
351 1
        $this->variableColumns = $pValue;
352
353 1
        return $this;
354
    }
355
356
    /**
357
     * Get whether hyperlink or label should be output.
358
     */
359 1
    public function getPreferHyperlinkToLabel(): bool
360
    {
361 1
        return $this->preferHyperlinkToLabel;
362
    }
363
364
    /**
365
     * Set whether hyperlink or label should be output.
366
     */
367 1
    public function setPreferHyperlinkToLabel(bool $preferHyperlinkToLabel): self
368
    {
369 1
        $this->preferHyperlinkToLabel = $preferHyperlinkToLabel;
370
371 1
        return $this;
372
    }
373
}
374