Repository: PHPOffice/PhpSpreadsheet (php)

Issue:
Exception 'PhpOffice\PhpSpreadsheet\Exception' with message 'Sheet does not exist.'  

$excel = IOFactory::load("abc.xlsx");
$sheet1 = clone $excel->getSheet(0);
$sheet1->setCellValue('A1','1111111111111');
$excel->addSheet($sheet1);



![企业微信截图_20241016200007](https://github.com/user-attachments/assets/e96753ef-9a51-41bf-8188-4b012e733301)


Pull request:
Change Hash Code for Worksheet
Fix #4192. Although that issue can be dealt with by changing user code, it would be better to fix it within PhpSpreadsheet. A cloned worksheet may have a pointer to a spreadsheet to which it is not attached. Code can assume it does belong to the spreadsheet, and throw an exception when the spreadsheet cannot find the worksheet in question. It may also not throw an exception when it should.

In my comments to the issue, I was concerned that adding in the needed protection would add overhead to an extremely common situation (setting a cell's value) in order to avoid a pretty rare problem. However, there are problems with both the accuracy and efficiency of the existing code, and I think any performance losses caused by the additional checks will be offset by the performance gains and accuracy of the new code.

Spreadsheet `getIndex` attempts to find the index of a worksheet within its spreadsheet collection. It does so by comparing the hash codes of each sheet in its collection with the hash code of the sheet it is looking for. Its major problem problem is performance-related, namely that it recomputes the hash code of the target sheet with each iteration.

A more severe problem is the accuracy of the hash code. It generates this by hashing together the sheet title, the string range of its auto-filter, and a character representation of whether sheet protection is enabled. Title should definitely be part of the calculation (it must be unique for all sheets attached to a spreadsheet), but it is not clear why this subset of the other properties of Worksheet is used. It tries to save some cycles by using a `dirty` property to indicate whether re-hashing is necessary. It sets that property whenever the title changes, or when `setProtection` is called. So, it doesn't set it when auto-filter changes, and you can easily bypass `setProtection` when changing any of the `Protection` properties. Not to mention the many other properties of worksheet that can be changed. Additionally, if you clone a worksheet, the clone and the original will have the same hash code, which can lead to problems:
```php
$clone = clone $original;
$spreadsheet->getSheet($spreadsheet->getIndex($clone))
    ->setCellValue('A1', 100);
```
That code will change the value of A1 in the original, not the clone.

The `hash` property in Worksheet will now be calculated immediately when the object is constructed or cloned or unserialized. It will not be recalculated, and there is no longer a need for the `dirty` property, which is removed. Hash will be generated by spl_object_id, which was designed for this purpose. (So was spl_object_hash, but many online references suggest that \_id performs much better than \_hash.) Our problem example above will now throw an Exception, as it should, rather than changing the wrong cell. `setValueExplicit`, the problem in the original issue, will now test that the worksheet is attached to the spreadsheet before doing any style manipulation. In order that this not be a breaking change, `getHashCode` will continue to return string, but it is deprecated in favor of `getHashInt`, and Worksheet will no longer implement IComparable to facilitate the deprecation.

I had a vague hope that this change might help with issue #641. It doesn't.

This is:

- [x] a bugfix
- [ ] a new feature
- [ ] refactoring
- [ ] additional unit tests

Checklist:

- [x] Changes are covered by unit tests
  - [x] Changes are covered by existing unit tests
  - [x] New unit tests have been added
- [x] Code style is respected
- [x] Commit message explains **why** the change is made (see https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
- [ ] CHANGELOG.md contains a short summary of the change and a link to the pull request if applicable
- [ ] Documentation is updated as necessary

### Why this change is needed?

Provide an explanation of why this change is needed, with links to any Issues (if appropriate).
If this is a bugfix or a new feature, and there are no existing Issues, then please also create an issue that will make it easier to track progress with this PR.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd987eae6..6e7766421 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,11 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org).
 
 ### Deprecated
 
-- Nothing yet.
+- Worksheet::getHashCode is no longer needed.
 
 ### Fixed
 
 - Add support for `
` tag when converting HTML to RichText. [Issue #4223](https://github.com/PHPOffice/PhpSpreadsheet/issues/4223) [PR #4224](https://github.com/PHPOffice/PhpSpreadsheet/pull/4224)
+- Change hash code for worksheet. [Issue #4192](https://github.com/PHPOffice/PhpSpreadsheet/issues/4192) [PR #4207](https://github.com/PHPOffice/PhpSpreadsheet/pull/4207)
 
 ## 2024-11-10 - 3.4.0
 
diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php
index 1e353db91..89321abed 100644
--- a/src/PhpSpreadsheet/Cell/Cell.php
+++ b/src/PhpSpreadsheet/Cell/Cell.php
@@ -324,7 +324,7 @@ class Cell implements Stringable
         self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value);
         $worksheet = $this->getWorksheet();
         $spreadsheet = $worksheet->getParent();
-        if (isset($spreadsheet)) {
+        if (isset($spreadsheet) && $spreadsheet->getIndex($worksheet, true) >= 0) {
             $originalSelected = $worksheet->getSelectedCells();
             $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
             $style = $this->getStyle();
diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php
index f139a5d53..afcfa016e 100644
--- a/src/PhpSpreadsheet/ReferenceHelper.php
+++ b/src/PhpSpreadsheet/ReferenceHelper.php
@@ -921,7 +921,7 @@ class ReferenceHelper
     {
         $cellAddress = $definedName->getValue();
         $asFormula = ($cellAddress[0] === '=');
-        if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) {
+        if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashInt() === $worksheet->getHashInt()) {
             /**
              * If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF!
              * PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells.
@@ -940,7 +940,7 @@ class ReferenceHelper
 
     private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
     {
-        if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) {
+        if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashInt() === $worksheet->getHashInt()) {
             /**
              * If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF!
              * PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells.
diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php
index 6ec75c33c..943db95cc 100644
--- a/src/PhpSpreadsheet/Spreadsheet.php
+++ b/src/PhpSpreadsheet/Spreadsheet.php
@@ -660,13 +660,17 @@ class Spreadsheet implements JsonSerializable
      *
      * @return int index
      */
-    public function getIndex(Worksheet $worksheet): int
+    public function getIndex(Worksheet $worksheet, bool $noThrow = false): int
     {
+        $wsHash = $worksheet->getHashInt();
         foreach ($this->workSheetCollection as $key => $value) {
-            if ($value->getHashCode() === $worksheet->getHashCode()) {
+            if ($value->getHashInt() === $wsHash) {
                 return $key;
             }
         }
+        if ($noThrow) {
+            return -1;
+        }
 
         throw new Exception('Sheet does not exist.');
     }
diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php
index abf8c7afb..326b8d6d2 100644
--- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php
+++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php
@@ -414,7 +414,7 @@ class BaseDrawing implements IComparable
         return md5(
             $this->name
             . $this->description
-            . (($this->worksheet === null) ? '' : $this->worksheet->getHashCode())
+            . (($this->worksheet === null) ? '' : (string) $this->worksheet->getHashInt())
             . $this->coordinates
             . $this->offsetX
             . $this->offsetY
diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php
index 80339a8ea..546a6ffe5 100644
--- a/src/PhpSpreadsheet/Worksheet/Worksheet.php
+++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php
@@ -20,7 +20,6 @@ use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
 use PhpOffice\PhpSpreadsheet\Comment;
 use PhpOffice\PhpSpreadsheet\DefinedName;
 use PhpOffice\PhpSpreadsheet\Exception;
-use PhpOffice\PhpSpreadsheet\IComparable;
 use PhpOffice\PhpSpreadsheet\ReferenceHelper;
 use PhpOffice\PhpSpreadsheet\RichText\RichText;
 use PhpOffice\PhpSpreadsheet\Shared;
@@ -32,7 +31,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
 use PhpOffice\PhpSpreadsheet\Style\Protection as StyleProtection;
 use PhpOffice\PhpSpreadsheet\Style\Style;
 
-class Worksheet implements IComparable
+class Worksheet
 {
     // Break types
     public const BREAK_NONE = 0;
@@ -305,15 +304,10 @@ class Worksheet implements IComparable
      */
     private ?Color $tabColor = null;
 
-    /**
-     * Dirty flag.
-     */
-    private bool $dirty = true;
-
     /**
      * Hash.
      */
-    private string $hash;
+    private int $hash;
 
     /**
      * CodeName.
@@ -355,6 +349,7 @@ class Worksheet implements IComparable
         $this->autoFilter = new AutoFilter('', $this);
         // Table collection
         $this->tableCollection = new ArrayObject();
+        $this->hash = spl_object_id($this);
     }
 
     /**
@@ -383,6 +378,12 @@ class Worksheet implements IComparable
         unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->chartCollection, $this->autoFilter);
     }
 
+    public function __wakeup(): void
+    {
+        $this->hash = spl_object_id($this);
+        $this->parent = null;
+    }
+
     /**
      * Return the cell collection.
      */
@@ -897,7 +898,6 @@ class Worksheet implements IComparable
 
         // Set title
         $this->title = $title;
-        $this->dirty = true;
 
         if ($this->parent && $this->parent->getCalculationEngine()) {
             // New title
@@ -1032,7 +1032,6 @@ class Worksheet implements IComparable
     public function setProtection(Protection $protection): static
     {
         $this->protection = $protection;
-        $this->dirty = true;
 
         return $this;
     }
@@ -3014,7 +3013,7 @@ class Worksheet implements IComparable
 
         if ($namedRange->getLocalOnly()) {
             $worksheet = $namedRange->getWorksheet();
-            if ($worksheet === null || $this->getHashCode() !== $worksheet->getHashCode()) {
+            if ($worksheet === null || $this->hash !== $worksheet->getHashInt()) {
                 if ($returnNullIfInvalid) {
                     return null;
                 }
@@ -3154,17 +3153,15 @@ class Worksheet implements IComparable
     }
 
     /**
-     * Get hash code.
-     *
-     * @return string Hash code
+     * @deprecated 3.5.0 use getHashInt instead.
      */
     public function getHashCode(): string
     {
-        if ($this->dirty) {
-            $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
-            $this->dirty = false;
-        }
+        return (string) $this->hash;
+    }
 
+    public function getHashInt(): int
+    {
         return $this->hash;
     }
 
@@ -3493,6 +3490,7 @@ class Worksheet implements IComparable
                 }
             }
         }
+        $this->hash = spl_object_id($this);
     }
 
     /**
diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/src/PhpSpreadsheet/Writer/Xlsx/Style.php
index daac5d020..a31b8e249 100644
--- a/src/PhpSpreadsheet/Writer/Xlsx/Style.php
+++ b/src/PhpSpreadsheet/Writer/Xlsx/Style.php
@@ -142,6 +142,7 @@ class Style extends WriterPart
 
         // dxf
         for ($i = 0; $i < $this->getParentWriter()->getStylesConditionalHashTable()->count(); ++$i) {
+            /** @var ?Conditional */
             $thisstyle = $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i);
             if ($thisstyle !== null) {
                 $this->writeCellStyleDxf($objWriter, $thisstyle->getStyle());


Test patch:
diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php
index c276cf34b..c770ac260 100644
--- a/tests/PhpSpreadsheetTests/Shared/DateTest.php
+++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php
@@ -9,6 +9,7 @@ use DateTimeInterface;
 use DateTimeZone;
 use PhpOffice\PhpSpreadsheet\Exception;
 use PhpOffice\PhpSpreadsheet\Shared\Date;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
 use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
 use PHPUnit\Framework\TestCase;
 
@@ -207,7 +208,7 @@ class DateTest extends TestCase
         $date = Date::PHPToExcel('2020-01-01');
         self::assertEquals(43831.0, $date);
 
-        $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
+        $spreadsheet = new Spreadsheet();
         $sheet = $spreadsheet->getActiveSheet();
         $sheet->setCellValue('B1', 'x');
         /** @var float|int|string */
@@ -249,6 +250,7 @@ class DateTest extends TestCase
             ->getNumberFormat()
             ->setFormatCode('yyyy-mm-dd');
         self::assertFalse(Date::isDateTime($cella4));
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testRoundMicroseconds(): void
diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php
index eb46beaf3..13a58dfe1 100644
--- a/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php
+++ b/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php
@@ -43,5 +43,6 @@ class StringHelperInvalidCharTest extends TestCase
                 $sheet->getCell("A$row")->getValue()
             );
         }
+        $spreadsheet->disconnectWorksheets();
     }
 }
diff --git a/tests/PhpSpreadsheetTests/Style/BorderRangeTest.php b/tests/PhpSpreadsheetTests/Style/BorderRangeTest.php
index 844fa3fb5..8c50ddc17 100644
--- a/tests/PhpSpreadsheetTests/Style/BorderRangeTest.php
+++ b/tests/PhpSpreadsheetTests/Style/BorderRangeTest.php
@@ -62,6 +62,7 @@ class BorderRangeTest extends TestCase
                 }
             }
         }
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testBorderRangeDirectly(): void
@@ -71,5 +72,6 @@ class BorderRangeTest extends TestCase
         $sheet = $spreadsheet->getActiveSheet();
         $style = $sheet->getStyle('A1:C1')->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
         self::assertSame('A1:C1', $style->getSelectedCells(), 'getSelectedCells should not change after a style operation on a border range');
+        $spreadsheet->disconnectWorksheets();
     }
 }
diff --git a/tests/PhpSpreadsheetTests/Style/BorderTest.php b/tests/PhpSpreadsheetTests/Style/BorderTest.php
index 9f18f0766..dfe703213 100644
--- a/tests/PhpSpreadsheetTests/Style/BorderTest.php
+++ b/tests/PhpSpreadsheetTests/Style/BorderTest.php
@@ -31,6 +31,7 @@ class BorderTest extends TestCase
         self::assertSame(Border::BORDER_THIN, $borders->getRight()->getBorderStyle());
         self::assertSame(Border::BORDER_THIN, $borders->getLeft()->getBorderStyle());
         self::assertSame(Border::BORDER_NONE, $borders->getDiagonal()->getBorderStyle());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testAllBordersArray(): void
@@ -45,6 +46,7 @@ class BorderTest extends TestCase
         self::assertSame(Border::BORDER_THIN, $borders->getRight()->getBorderStyle());
         self::assertSame(Border::BORDER_THIN, $borders->getLeft()->getBorderStyle());
         self::assertSame(Border::BORDER_NONE, $borders->getDiagonal()->getBorderStyle());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testAllBordersArrayNotSupervisor(): void
@@ -86,6 +88,7 @@ class BorderTest extends TestCase
         self::assertSame(Border::BORDER_THIN, $sheet->getCell('B2')->getStyle()->getBorders()->getBottom()->getBorderStyle());
         self::assertSame(Border::BORDER_NONE, $sheet->getCell('B2')->getStyle()->getBorders()->getLeft()->getBorderStyle());
         self::assertSame(Border::BORDER_NONE, $sheet->getCell('B2')->getStyle()->getBorders()->getTop()->getBorderStyle());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testInside(): void
@@ -115,6 +118,7 @@ class BorderTest extends TestCase
         self::assertSame(Border::BORDER_NONE, $sheet->getCell('B2')->getStyle()->getBorders()->getBottom()->getBorderStyle());
         self::assertSame(Border::BORDER_THIN, $sheet->getCell('B2')->getStyle()->getBorders()->getLeft()->getBorderStyle());
         self::assertSame(Border::BORDER_THIN, $sheet->getCell('B2')->getStyle()->getBorders()->getTop()->getBorderStyle());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testHorizontal(): void
@@ -144,6 +148,7 @@ class BorderTest extends TestCase
         self::assertSame(Border::BORDER_NONE, $sheet->getCell('B2')->getStyle()->getBorders()->getBottom()->getBorderStyle());
         self::assertSame(Border::BORDER_NONE, $sheet->getCell('B2')->getStyle()->getBorders()->getLeft()->getBorderStyle());
         self::assertSame(Border::BORDER_THIN, $sheet->getCell('B2')->getStyle()->getBorders()->getTop()->getBorderStyle());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testVertical(): void
@@ -173,6 +178,7 @@ class BorderTest extends TestCase
         self::assertSame(Border::BORDER_NONE, $sheet->getCell('B2')->getStyle()->getBorders()->getBottom()->getBorderStyle());
         self::assertSame(Border::BORDER_THIN, $sheet->getCell('B2')->getStyle()->getBorders()->getLeft()->getBorderStyle());
         self::assertSame(Border::BORDER_NONE, $sheet->getCell('B2')->getStyle()->getBorders()->getTop()->getBorderStyle());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testNoSupervisorAllBorders(): void
@@ -213,6 +219,7 @@ class BorderTest extends TestCase
     public function testGetSharedComponentPseudo(): void
     {
         $this->expectException(PhpSpreadsheetException::class);
+        $this->expectExceptionMessage('pseudo-border');
         $spreadsheet = new Spreadsheet();
         $sheet = $spreadsheet->getActiveSheet();
         $sheet->getStyle('A1')->getBorders()->getHorizontal()->setBorderStyle(Border::BORDER_MEDIUM);
@@ -233,6 +240,7 @@ class BorderTest extends TestCase
         $border->setBorderStyle(Border::BORDER_THIN)->setColor(new Color('FFFF0000'));
         self::assertEquals('FFFF0000', $border->getColor()->getARGB());
         self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testDiagonalDirection(): void
@@ -245,5 +253,6 @@ class BorderTest extends TestCase
 
         self::assertSame(Border::BORDER_MEDIUM, $borders->getDiagonal()->getBorderStyle());
         self::assertSame(Borders::DIAGONAL_BOTH, $borders->getDiagonalDirection());
+        $spreadsheet->disconnectWorksheets();
     }
 }
diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php
index 04681b39a..5002bcaa1 100644
--- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php
+++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php
@@ -66,6 +66,7 @@ class WizardFactoryTest extends TestCase
             $wizard = Wizard::fromConditional($conditional);
             self::assertEquals($expectedWizads[$index], $wizard::class);
         }
+        $spreadsheet->disconnectWorksheets();
     }
 
     public static function conditionalProvider(): array
diff --git a/tests/PhpSpreadsheetTests/Style/StyleTest.php b/tests/PhpSpreadsheetTests/Style/StyleTest.php
index be67a5e79..6446bcf99 100644
--- a/tests/PhpSpreadsheetTests/Style/StyleTest.php
+++ b/tests/PhpSpreadsheetTests/Style/StyleTest.php
@@ -24,6 +24,7 @@ class StyleTest extends TestCase
         $styleArray = ['alignment' => ['textRotation' => 45]];
         $outArray = $cell1style->getStyleArray($styleArray);
         self::assertEquals($styleArray, $outArray['quotePrefix']);
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testStyleColumn(): void
@@ -58,6 +59,7 @@ class StyleTest extends TestCase
         self::assertTrue($sheet->getStyle('A1')->getFont()->getItalic());
         self::assertTrue($sheet->getStyle('B2')->getFont()->getItalic());
         self::assertFalse($sheet->getStyle('C3')->getFont()->getItalic());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testStyleIsReused(): void
@@ -81,6 +83,7 @@ class StyleTest extends TestCase
         $spreadsheet->garbageCollect();
 
         self::assertCount(3, $spreadsheet->getCellXfCollection());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testStyleRow(): void
@@ -115,6 +118,7 @@ class StyleTest extends TestCase
         self::assertFalse($sheet->getStyle('A1')->getFont()->getItalic());
         self::assertTrue($sheet->getStyle('B2')->getFont()->getItalic());
         self::assertTrue($sheet->getStyle('C3')->getFont()->getItalic());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testIssue1712A(): void
@@ -137,6 +141,7 @@ class StyleTest extends TestCase
             ->setRGB($rgb);
         self::assertEquals($rgb, $sheet->getCell('A1')->getStyle()->getFill()->getStartColor()->getRGB());
         self::assertEquals($rgb, $sheet->getCell('B1')->getStyle()->getFill()->getStartColor()->getRGB());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testIssue1712B(): void
@@ -159,6 +164,7 @@ class StyleTest extends TestCase
         $sheet->fromArray(['OK', 'KO']);
         self::assertEquals($rgb, $sheet->getCell('A1')->getStyle()->getFill()->getStartColor()->getRGB());
         self::assertEquals($rgb, $sheet->getCell('B1')->getStyle()->getFill()->getStartColor()->getRGB());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testStyleLoopUpwards(): void
@@ -184,6 +190,7 @@ class StyleTest extends TestCase
         self::assertFalse($sheet->getStyle('A1')->getFont()->getBold());
         self::assertFalse($sheet->getStyle('B2')->getFont()->getBold());
         self::assertTrue($sheet->getStyle('C3')->getFont()->getBold());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testStyleCellAddressObject(): void
@@ -195,6 +202,7 @@ class StyleTest extends TestCase
         $style->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDDSLASH);
 
         self::assertSame(NumberFormat::FORMAT_DATE_YYYYMMDDSLASH, $style->getNumberFormat()->getFormatCode());
+        $spreadsheet->disconnectWorksheets();
     }
 
     public function testStyleCellRangeObject(): void
@@ -208,5 +216,6 @@ class StyleTest extends TestCase
         $style->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDDSLASH);
 
         self::assertSame(NumberFormat::FORMAT_DATE_YYYYMMDDSLASH, $style->getNumberFormat()->getFormatCode());
+        $spreadsheet->disconnectWorksheets();
     }
 }
diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoSizeTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoSizeTest.php
index 4a4f488bb..558590bfb 100644
--- a/tests/PhpSpreadsheetTests/Worksheet/AutoSizeTest.php
+++ b/tests/PhpSpreadsheetTests/Worksheet/AutoSizeTest.php
@@ -13,16 +13,14 @@ use PHPUnit\Framework\TestCase;
 
 class AutoSizeTest extends TestCase
 {
-    protected Spreadsheet $spreadsheet;
+    private Spreadsheet $spreadsheet;
 
-    protected Worksheet $worksheet;
+    private Worksheet $worksheet;
 
     protected function setUp(): void
     {
-        parent::setUp();
-
-        $spreadsheet = new Spreadsheet();
-        $this->worksheet = $spreadsheet->getActiveSheet();
+        $this->spreadsheet = new Spreadsheet();
+        $this->worksheet = $this->spreadsheet->getActiveSheet();
 
         $this->worksheet->setCellValue('A1', 'YEAR')
             ->setCellValue('B1', 'QUARTER')
@@ -44,7 +42,13 @@ class AutoSizeTest extends TestCase
         }
     }
 
-    protected function setTable(): Table
+    protected function tearDown(): void
+    {
+        $this->spreadsheet->disconnectWorksheets();
+        unset($this->spreadsheet, $this->worksheet);
+    }
+
+    private function setTable(): Table
     {
         $table = new Table('A1:D5', 'Sales_Data');
         $tableStyle = new TableStyle();
@@ -55,7 +59,7 @@ class AutoSizeTest extends TestCase
         return $table;
     }
 
-    protected function readColumnSizes(): array
+    private function readColumnSizes(): array
     {
         $columnSizes = [];
         $toColumn = $this->worksheet->getHighestColumn();
diff --git a/tests/PhpSpreadsheetTests/Worksheet/CloneTest.php b/tests/PhpSpreadsheetTests/Worksheet/CloneTest.php
new file mode 100644
index 000000000..b66a9dd8d
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Worksheet/CloneTest.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+use PHPUnit\Framework\TestCase;
+
+class CloneTest extends TestCase
+{
+    public function testUnattachedIndex(): void
+    {
+        $spreadsheet = new Spreadsheet();
+        $sheet1 = $spreadsheet->getActiveSheet();
+        $sheet1->getCell('A1')->setValue(10);
+        $sheet2 = clone $sheet1;
+        $sheet2->getCell('A1')->setValue(20);
+        self::assertSame(0, $spreadsheet->getIndex($sheet1));
+        $idx = $spreadsheet->getIndex($sheet2, true);
+        self::assertSame(-1, $idx);
+        $sheet2->setTitle('clone');
+        $spreadsheet->addSheet($sheet2);
+        $idx = $spreadsheet->getIndex($sheet2, true);
+        self::assertSame(1, $idx);
+        self::assertSame(10, $spreadsheet->getSheet(0)->getCell('A1')->getValue());
+        self::assertSame(20, $spreadsheet->getSheet(1)->getCell('A1')->getValue());
+        $spreadsheet->disconnectWorksheets();
+    }
+
+    public function testGetCloneIndex(): void
+    {
+        $this->expectException(SpreadsheetException::class);
+        $this->expectExceptionMessage('Sheet does not exist');
+        $spreadsheet = new Spreadsheet();
+        $sheet1 = $spreadsheet->getActiveSheet();
+        $sheet1->getCell('A1')->setValue(10);
+        $sheet2 = clone $sheet1;
+        $spreadsheet->getSheet($spreadsheet->getIndex($sheet2))
+            ->setCellValue('A1', 100);
+    }
+
+    public function testSerialize1(): void
+    {
+        // If worksheet attached to spreadsheet, can't serialize it.
+        $this->expectException(SpreadsheetException::class);
+        $this->expectExceptionMessage('cannot be serialized');
+        $spreadsheet = new Spreadsheet();
+        $sheet1 = $spreadsheet->getActiveSheet();
+        serialize($sheet1);
+    }
+
+    public function testSerialize2(): void
+    {
+        $sheet1 = new Worksheet();
+        $sheet1->getCell('A1')->setValue(10);
+        $serialized = serialize($sheet1);
+        /** @var Worksheet */
+        $newSheet = unserialize($serialized);
+        self::assertSame(10, $newSheet->getCell('A1')->getValue());
+        self::assertNotEquals($newSheet->getHashInt(), $sheet1->getHashInt());
+    }
+}