Skip to content

Commit

Permalink
Csv Handling of Booleans (and an 8.1 Deprecation) (#2232)
Browse files Browse the repository at this point in the history
* Csv Handling of Booleans (and an 8.1 Deprecation)

PhpSpreadsheet writes boolean values to a Csv as null-string/1, and treats input values of 'true' and 'false' as if they were strings. On the other hand, Excel writes boolean values to a Csv as TRUE/FALSE, and case-insensitively treats a matching string as boolean on read. This PR changes PhpSpreadsheet to match Excel.

A side-effect of this change is that it fixes behavior incorrectly reported as a bug in PR #2048. That issue was closed, correctly, as user error. The user had altered Csv Writer, including adding ```declare(strict_types=1);```; that declaration was the cause of the error. The "offending" statements, calls to strpbrk and str_replace, will now work correctly whether or not strict_types is in use.

And, just as I was getting ready to push this, the dailies for PHP 8.1 introduced a change deprecating auto_detect_line_endings. Csv Reader uses that setting; it allows it to process a Csv with Mac line endings, which happens to be something that Excel can do. As they say in https://wiki.php.net/rfc/deprecations_php_8_1, where the proposal passed without a single dissenting vote, "These newlines were used by “Classic” Mac OS, a system which has been discontinued in 2001, nearly two decades ago. Interoperability with such systems is no longer relevant." I tend to agree, but I don't know that we're ready to pull the plug yet. I don't see an easy way to emulate that functionality. For now, I have silenced the deprecation notices with at signs. I have also added a test case which will fail when support for that setting is pulled; this will give time to consider alternatives.

* Scrutinizer: Handling ini_set

This could be interesting. It doesn't like not handling an error condition for ini_set. Let's see if this satisfies it.
  • Loading branch information
oleibman authored Aug 4, 2021
1 parent b9f6c70 commit 0cd20f3
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 34 deletions.
15 changes: 0 additions & 15 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4985,11 +4985,6 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Worksheet/Worksheet.php

-
message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Csv\\:\\:\\$enclosureRequired has no typehint specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Writer/Csv.php

-
message: "#^Call to function array_key_exists\\(\\) with int and array\\('none' \\=\\> 'none', 'dashDot' \\=\\> '1px dashed', 'dashDotDot' \\=\\> '1px dotted', 'dashed' \\=\\> '1px dashed', 'dotted' \\=\\> '1px dotted', 'double' \\=\\> '3px double', 'hair' \\=\\> '1px solid', 'medium' \\=\\> '2px solid', \\.\\.\\.\\) will always evaluate to false\\.$#"
count: 1
Expand Down Expand Up @@ -6535,16 +6530,6 @@ parameters:
count: 1
path: tests/PhpSpreadsheetTests/Worksheet/RowCellIterator2Test.php

-
message: "#^Property PhpOffice\\\\PhpSpreadsheetTests\\\\Writer\\\\Csv\\\\CsvEnclosureTest\\:\\:\\$cellValues has no typehint specified\\.$#"
count: 1
path: tests/PhpSpreadsheetTests/Writer/Csv/CsvEnclosureTest.php

-
message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|false given\\.$#"
count: 4
path: tests/PhpSpreadsheetTests/Writer/Csv/CsvEnclosureTest.php

-
message: "#^Method PhpOffice\\\\PhpSpreadsheetTests\\\\Writer\\\\Html\\\\CallbackTest\\:\\:yellowBody\\(\\) should return string but returns string\\|null\\.$#"
count: 1
Expand Down
40 changes: 36 additions & 4 deletions src/PhpSpreadsheet/Reader/Csv.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,26 @@ private function openFileOrMemory(string $pFilename): void
}
}

private static function setAutoDetect(?string $value): ?string
{
$retVal = null;
if ($value !== null) {
$retVal2 = @ini_set('auto_detect_line_endings', $value);
if (is_string($retVal2)) {
$retVal = $retVal2;
}
}

return $retVal;
}

/**
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
*/
public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): Spreadsheet
{
$lineEnding = ini_get('auto_detect_line_endings') ?: '0';
ini_set('auto_detect_line_endings', '1');
// Deprecated in Php8.1
$iniset = self::setAutoDetect('1');

// Open file
$this->openFileOrMemory($pFilename);
Expand All @@ -305,7 +318,8 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S
$noOutputYet = true;
$columnLetter = 'A';
foreach ($rowData as $rowDatum) {
if ($rowDatum != '' && $this->readFilter->readCell($columnLetter, $currentRow)) {
self::convertBoolean($rowDatum);
if ($rowDatum !== '' && $this->readFilter->readCell($columnLetter, $currentRow)) {
if ($this->contiguous) {
if ($noOutputYet) {
$noOutputYet = false;
Expand All @@ -326,12 +340,30 @@ public function loadIntoExisting(string $pFilename, Spreadsheet $spreadsheet): S
// Close file
fclose($fileHandle);

ini_set('auto_detect_line_endings', $lineEnding);
self::setAutoDetect($iniset);

// Return
return $spreadsheet;
}

/**
* Convert string true/false to boolean, and null to null-string.
*
* @param mixed $rowDatum
*/
private static function convertBoolean(&$rowDatum): void
{
if (is_string($rowDatum)) {
if (strcasecmp('true', $rowDatum) === 0) {
$rowDatum = true;
} elseif (strcasecmp('false', $rowDatum) === 0) {
$rowDatum = false;
}
} elseif ($rowDatum === null) {
$rowDatum = '';
}
}

public function getDelimiter(): ?string
{
return $this->delimiter;
Expand Down
16 changes: 16 additions & 0 deletions src/PhpSpreadsheet/Writer/Csv.php
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ public function setOutputEncoding($pValue)
return $this;
}

/** @var bool */
private $enclosureRequired = true;

public function setEnclosureRequired(bool $value): self
Expand All @@ -343,6 +344,20 @@ public function getEnclosureRequired(): bool
return $this->enclosureRequired;
}

/**
* Convert boolean to TRUE/FALSE; otherwise return element cast to string.
*
* @param mixed $element
*/
private static function elementToString($element): string
{
if (is_bool($element)) {
return $element ? 'TRUE' : 'FALSE';
}

return (string) $element;
}

/**
* Write line to CSV file.
*
Expand All @@ -358,6 +373,7 @@ private function writeLine($pFileHandle, array $pValues): void
$line = '';

foreach ($pValues as $element) {
$element = self::elementToString($element);
// Add delimiter
$line .= $delimiter;
$delimiter = $this->delimiter;
Expand Down
47 changes: 47 additions & 0 deletions tests/PhpSpreadsheetTests/Reader/Csv/CsvLineEndingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv;

use PhpOffice\PhpSpreadsheet\Reader\Csv;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PHPUnit\Framework\TestCase;

class CsvLineEndingTest extends TestCase
{
/** @var string */
private $tempFile = '';

protected function tearDown(): void
{
if ($this->tempFile !== '') {
unlink($this->tempFile);
$this->tempFile = '';
}
}

/**
* @dataProvider providerEndings
*/
public function testEndings(string $ending): void
{
$this->tempFile = $filename = File::temporaryFilename();
$data = ['123', '456', '789'];
file_put_contents($filename, implode($ending, $data));
$reader = new Csv();
$spreadsheet = $reader->load($filename);
$sheet = $spreadsheet->getActiveSheet();
self::assertEquals($data[0], $sheet->getCell('A1')->getValue());
self::assertEquals($data[1], $sheet->getCell('A2')->getValue());
self::assertEquals($data[2], $sheet->getCell('A3')->getValue());
$spreadsheet->disconnectWorksheets();
}

public function providerEndings(): array
{
return [
'Unix endings' => ["\n"],
'Mac endings' => ["\r"],
'Windows endings' => ["\r\n"],
];
}
}
Loading

0 comments on commit 0cd20f3

Please sign in to comment.