Fix Spreadsheet Copy, Disable Clone, Improve Coverage (#2951)

* Fix Spreadsheet Copy, Disable Clone, Improve Coverage

This PR was supposed to be merely to increase coverage in Spreadsheet. However, in doing so, I discovered that neither clone nor copy worked correctly. Neither had been covered in the test suite. Copy not only did not work, it broke the source spreadsheet as well. I tried to debug and got nowhere; I even tried using myclabs/deep-copy which is already in use in the test suite, but it failed as well. However, write and reload ought to work just fine for copy. It can't be used for clone; however, since copy does what clone ought to do, there's no reason why clone needs to be used, so __clone is changed to throw an exception if attempted.

One other source change was needed, an obvious bug where an if condition uses 'or' when it should use 'and'. Also, one docblock declaration needed a change. Aside from that, the rest of this PR is test cases, and overall coverage passes 89% for the first time.

* Clone is Okay After All

But copy wasn't, changing it to just return clone. Perhaps save and reload will be needed instead at some point, but not yet.

* An Error I Cannot Reproduce

PHP8.1 unit test says error because GdImage can't be serialized. I can't reproduce this error on any of my test systems. I have no idea why GdImage is even involved. Using try/catch to see if it helps.

* Weird Failures in Github

I thought restoring clone was a good idea. That left me in a state where, after one change, copy/clone no longer worked on Github (unable to reproduce on any of my test systems). After a second change, copy worked but clone didn't, again unable to reproduce. So, reverting to original version - copy does save and reload, clone throws exception.
This commit is contained in:
oleibman 2022-07-28 07:03:26 -07:00 committed by GitHub
parent e460c82606
commit c0809b0c6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 297 additions and 33 deletions

View File

@ -2860,11 +2860,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Spreadsheet.php path: src/PhpSpreadsheet/Spreadsheet.php
-
message: "#^Comparison operation \"\\<\\=\" between int\\<min, \\-1\\> and 1000 is always true\\.$#"
count: 1
path: src/PhpSpreadsheet/Spreadsheet.php
- -
message: "#^Parameter \\#1 \\$worksheet of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getIndex\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#" message: "#^Parameter \\#1 \\$worksheet of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getIndex\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#"
count: 1 count: 1
@ -2875,21 +2870,11 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Spreadsheet.php path: src/PhpSpreadsheet/Spreadsheet.php
-
message: "#^Result of \\|\\| is always true\\.$#"
count: 1
path: src/PhpSpreadsheet/Spreadsheet.php
- -
message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet and null will always evaluate to false\\.$#" message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet and null will always evaluate to false\\.$#"
count: 1 count: 1
path: src/PhpSpreadsheet/Spreadsheet.php path: src/PhpSpreadsheet/Spreadsheet.php
-
message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#"
count: 1
path: src/PhpSpreadsheet/Spreadsheet.php
- -
message: "#^Unreachable statement \\- code above always terminates\\.$#" message: "#^Unreachable statement \\- code above always terminates\\.$#"
count: 1 count: 1

View File

@ -3,10 +3,13 @@
namespace PhpOffice\PhpSpreadsheet; namespace PhpOffice\PhpSpreadsheet;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Style\Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
class Spreadsheet class Spreadsheet
{ {
@ -1120,28 +1123,24 @@ class Spreadsheet
*/ */
public function copy() public function copy()
{ {
$copied = clone $this; $filename = File::temporaryFilename();
$writer = new XlsxWriter($this);
$writer->setIncludeCharts(true);
$writer->save($filename);
$worksheetCount = count($this->workSheetCollection); $reader = new XlsxReader();
for ($i = 0; $i < $worksheetCount; ++$i) { $reader->setIncludeCharts(true);
$this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy(); $reloadedSpreadsheet = $reader->load($filename);
$this->workSheetCollection[$i]->rebindParent($this); unlink($filename);
return $reloadedSpreadsheet;
} }
return $copied;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone() public function __clone()
{ {
// @phpstan-ignore-next-line throw new Exception(
foreach ($this as $key => $val) { 'Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'
if (is_object($val) || (is_array($val))) { );
$this->{$key} = unserialize(serialize($val));
}
}
} }
/** /**
@ -1562,7 +1561,7 @@ class Spreadsheet
* Workbook window is hidden and cannot be shown in the * Workbook window is hidden and cannot be shown in the
* user interface. * user interface.
* *
* @param string $visibility visibility status of the workbook * @param null|string $visibility visibility status of the workbook
*/ */
public function setVisibility($visibility): void public function setVisibility($visibility): void
{ {
@ -1596,7 +1595,7 @@ class Spreadsheet
*/ */
public function setTabRatio($tabRatio): void public function setTabRatio($tabRatio): void
{ {
if ($tabRatio >= 0 || $tabRatio <= 1000) { if ($tabRatio >= 0 && $tabRatio <= 1000) {
$this->tabRatio = (int) $tabRatio; $this->tabRatio = (int) $tabRatio;
} else { } else {
throw new Exception('Tab ratio must be between 0 and 1000.'); throw new Exception('Tab ratio must be between 0 and 1000.');

View File

@ -85,6 +85,18 @@ class DefinedNameTest extends TestCase
self::assertCount(1, $this->spreadsheet->getDefinedNames()); self::assertCount(1, $this->spreadsheet->getDefinedNames());
} }
public function testRemoveGlobalDefinedName(): void
{
$this->spreadsheet->addDefinedName(
DefinedName::createInstance('Any', $this->spreadsheet->getActiveSheet(), '=A1')
);
self::assertCount(1, $this->spreadsheet->getDefinedNames());
$this->spreadsheet->removeDefinedName('Any');
self::assertCount(0, $this->spreadsheet->getDefinedNames());
$this->spreadsheet->removeDefinedName('Other');
}
public function testRemoveGlobalDefinedNameWhenDuplicateNames(): void public function testRemoveGlobalDefinedNameWhenDuplicateNames(): void
{ {
$this->spreadsheet->addDefinedName( $this->spreadsheet->addDefinedName(

View File

@ -133,4 +133,10 @@ class NamedFormulaTest extends TestCase
$formula->getValue() $formula->getValue()
); );
} }
public function testRemoveNonExistentNamedFormula(): void
{
self::assertCount(0, $this->spreadsheet->getNamedFormulae());
$this->spreadsheet->removeNamedFormula('Any');
}
} }

View File

@ -133,4 +133,10 @@ class NamedRangeTest extends TestCase
$range->getValue() $range->getValue()
); );
} }
public function testRemoveNonExistentNamedRange(): void
{
self::assertCount(0, $this->spreadsheet->getNamedRanges());
$this->spreadsheet->removeNamedRange('Any');
}
} }

View File

@ -44,4 +44,34 @@ class RibbonTest extends AbstractFunctional
self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); self::assertNull($reloadedSpreadsheet->getRibbonBinObjects());
$reloadedSpreadsheet->disconnectWorksheets(); $reloadedSpreadsheet->disconnectWorksheets();
} }
/**
* Same as above but discard macros.
*/
public function testDiscardMacros(): void
{
$filename = 'tests/data/Reader/XLSX/ribbon.donotopen.zip';
$reader = IOFactory::createReader('Xlsx');
$spreadsheet = $reader->load($filename);
self::assertTrue($spreadsheet->hasRibbon());
$target = $spreadsheet->getRibbonXMLData('target');
self::assertSame('customUI/customUI.xml', $target);
$data = $spreadsheet->getRibbonXMLData('data');
self::assertIsString($data);
self::assertSame(1522, strlen($data));
$vbaCode = (string) $spreadsheet->getMacrosCode();
self::assertSame(13312, strlen($vbaCode));
$spreadsheet->discardMacros();
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
$spreadsheet->disconnectWorksheets();
self::assertTrue($reloadedSpreadsheet->hasRibbon());
$ribbonData = $reloadedSpreadsheet->getRibbonXmlData();
self::assertIsArray($ribbonData);
self::assertSame($target, $ribbonData['target'] ?? '');
self::assertSame($data, $ribbonData['data'] ?? '');
self::assertNull($reloadedSpreadsheet->getMacrosCode());
self::assertNull($reloadedSpreadsheet->getRibbonBinObjects());
$reloadedSpreadsheet->disconnectWorksheets();
}
} }

View File

@ -0,0 +1,209 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests;
use PhpOffice\PhpSpreadsheet\Exception as SSException;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Style;
use PHPUnit\Framework\TestCase;
class SpreadsheetCoverageTest extends TestCase
{
public function testDocumentProperties(): void
{
$spreadsheet = new Spreadsheet();
$properties = $spreadsheet->getProperties();
$properties->setCreator('Anyone');
$properties->setTitle('Description');
$spreadsheet2 = new Spreadsheet();
self::assertNotEquals($properties, $spreadsheet2->getProperties());
$properties2 = clone $properties;
$spreadsheet2->setProperties($properties2);
self::assertEquals($properties, $spreadsheet2->getProperties());
$spreadsheet->disconnectWorksheets();
$spreadsheet2->disconnectWorksheets();
}
public function testDocumentSecurity(): void
{
$spreadsheet = new Spreadsheet();
$security = $spreadsheet->getSecurity();
$security->setLockRevision(true);
$revisionsPassword = 'revpasswd';
$security->setRevisionsPassword($revisionsPassword);
$spreadsheet2 = new Spreadsheet();
self::assertNotEquals($security, $spreadsheet2->getSecurity());
$security2 = clone $security;
$spreadsheet2->setSecurity($security2);
self::assertEquals($security, $spreadsheet2->getSecurity());
$spreadsheet->disconnectWorksheets();
$spreadsheet2->disconnectWorksheets();
}
public function testCellXfCollection(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getStyle('A1')->getFont()->setName('font1');
$sheet->getStyle('A2')->getFont()->setName('font2');
$sheet->getStyle('A3')->getFont()->setName('font3');
$sheet->getStyle('B1')->getFont()->setName('font1');
$sheet->getStyle('B2')->getFont()->setName('font2');
$collection = $spreadsheet->getCellXfCollection();
self::assertCount(4, $collection);
$font1Style = $collection[1];
self::assertTrue($spreadsheet->cellXfExists($font1Style));
self::assertSame('font1', $spreadsheet->getCellXfCollection()[1]->getFont()->getName());
self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName());
self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName());
self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName());
self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName());
self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName());
$spreadsheet->removeCellXfByIndex(1);
self::assertFalse($spreadsheet->cellXfExists($font1Style));
self::assertSame('font2', $spreadsheet->getCellXfCollection()[1]->getFont()->getName());
self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName());
self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName());
self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName());
self::assertSame('Calibri', $sheet->getStyle('B1')->getFont()->getName());
self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName());
$spreadsheet->disconnectWorksheets();
}
public function testInvalidRemoveCellXfByIndex(): void
{
$this->expectException(SSException::class);
$this->expectExceptionMessage('CellXf index is out of bounds.');
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getStyle('A1')->getFont()->setName('font1');
$sheet->getStyle('A2')->getFont()->setName('font2');
$sheet->getStyle('A3')->getFont()->setName('font3');
$sheet->getStyle('B1')->getFont()->setName('font1');
$sheet->getStyle('B2')->getFont()->setName('font2');
$spreadsheet->removeCellXfByIndex(5);
$spreadsheet->disconnectWorksheets();
}
public function testInvalidRemoveDefaultStyle(): void
{
$this->expectException(SSException::class);
$this->expectExceptionMessage('No default style found for this workbook');
// Removing default style probably should be disallowed.
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$spreadsheet->removeCellXfByIndex(0);
$style = $spreadsheet->getDefaultStyle();
$spreadsheet->disconnectWorksheets();
}
public function testCellStyleXF(): void
{
$spreadsheet = new Spreadsheet();
$collection = $spreadsheet->getCellStyleXfCollection();
self::assertCount(1, $collection);
$styleXf = $collection[0];
self::assertSame($styleXf, $spreadsheet->getCellStyleXfByIndex(0));
$hash = $styleXf->getHashCode();
self::assertSame($styleXf, $spreadsheet->getCellStyleXfByHashCode($hash));
self::assertFalse($spreadsheet->getCellStyleXfByHashCode($hash . 'x'));
$spreadsheet->disconnectWorksheets();
}
public function testInvalidRemoveCellStyleXfByIndex(): void
{
$this->expectException(SSException::class);
$this->expectExceptionMessage('CellStyleXf index is out of bounds.');
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$spreadsheet->removeCellStyleXfByIndex(5);
$spreadsheet->disconnectWorksheets();
}
public function testInvalidFirstSheetIndex(): void
{
$this->expectException(SSException::class);
$this->expectExceptionMessage('First sheet index must be a positive integer.');
$spreadsheet = new Spreadsheet();
$spreadsheet->setFirstSheetIndex(-1);
$spreadsheet->disconnectWorksheets();
}
public function testInvalidVisibility(): void
{
$this->expectException(SSException::class);
$this->expectExceptionMessage('Invalid visibility value.');
$spreadsheet = new Spreadsheet();
$spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN);
self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $spreadsheet->getVisibility());
$spreadsheet->setVisibility(null);
self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $spreadsheet->getVisibility());
$spreadsheet->setVisibility('badvalue');
$spreadsheet->disconnectWorksheets();
}
public function testInvalidTabRatio(): void
{
$this->expectException(SSException::class);
$this->expectExceptionMessage('Tab ratio must be between 0 and 1000.');
$spreadsheet = new Spreadsheet();
$spreadsheet->setTabRatio(2000);
$spreadsheet->disconnectWorksheets();
}
public function testCopy(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getStyle('A1')->getFont()->setName('font1');
$sheet->getStyle('A2')->getFont()->setName('font2');
$sheet->getStyle('A3')->getFont()->setName('font3');
$sheet->getStyle('B1')->getFont()->setName('font1');
$sheet->getStyle('B2')->getFont()->setName('font2');
$sheet->getCell('A1')->setValue('this is a1');
$sheet->getCell('A2')->setValue('this is a2');
$sheet->getCell('A3')->setValue('this is a3');
$sheet->getCell('B1')->setValue('this is b1');
$sheet->getCell('B2')->setValue('this is b2');
$copied = $spreadsheet->copy();
$copysheet = $copied->getActiveSheet();
$copysheet->getStyle('A2')->getFont()->setName('font12');
$copysheet->getCell('A2')->setValue('this was a2');
self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName());
self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName());
self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName());
self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName());
self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName());
self::assertSame('this is a1', $sheet->getCell('A1')->getValue());
self::assertSame('this is a2', $sheet->getCell('A2')->getValue());
self::assertSame('this is a3', $sheet->getCell('A3')->getValue());
self::assertSame('this is b1', $sheet->getCell('B1')->getValue());
self::assertSame('this is b2', $sheet->getCell('B2')->getValue());
self::assertSame('font1', $copysheet->getStyle('A1')->getFont()->getName());
self::assertSame('font12', $copysheet->getStyle('A2')->getFont()->getName());
self::assertSame('font3', $copysheet->getStyle('A3')->getFont()->getName());
self::assertSame('font1', $copysheet->getStyle('B1')->getFont()->getName());
self::assertSame('font2', $copysheet->getStyle('B2')->getFont()->getName());
self::assertSame('this is a1', $copysheet->getCell('A1')->getValue());
self::assertSame('this was a2', $copysheet->getCell('A2')->getValue());
self::assertSame('this is a3', $copysheet->getCell('A3')->getValue());
self::assertSame('this is b1', $copysheet->getCell('B1')->getValue());
self::assertSame('this is b2', $copysheet->getCell('B2')->getValue());
$spreadsheet->disconnectWorksheets();
$copied->disconnectWorksheets();
}
public function testClone(): void
{
$this->expectException(SSException::class);
$this->expectExceptionMessage('Do not use clone on spreadsheet. Use spreadsheet->copy() instead.');
$spreadsheet = new Spreadsheet();
$clone = clone $spreadsheet;
$spreadsheet->disconnectWorksheets();
$clone->disconnectWorksheets();
}
}

View File

@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheetTests\Style; namespace PhpOffice\PhpSpreadsheetTests\Style;
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class FontTest extends TestCase class FontTest extends TestCase
@ -88,4 +89,20 @@ class FontTest extends TestCase
self::assertEquals('Calibri', $font->getName(), 'Null string changed to default'); self::assertEquals('Calibri', $font->getName(), 'Null string changed to default');
$spreadsheet->disconnectWorksheets(); $spreadsheet->disconnectWorksheets();
} }
public function testUnderlineHash(): void
{
$font1 = new Font();
$font2 = new Font();
$font2aHash = $font2->getHashCode();
self::assertSame($font1->getHashCode(), $font2aHash);
$font2->setUnderlineColor(
[
'type' => 'srgbClr',
'value' => 'FF0000',
]
);
$font2bHash = $font2->getHashCode();
self::assertNotEquals($font1->getHashCode(), $font2bHash);
}
} }