mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-13 19:46:31 +00:00
Add forceFullCalc Option to Xlsx Writer
Fix #4269. In response to issue #456 PR #515, `forceFullCalc` was added to workbook.xml whenever `preCalculateFormulas` was set to false. It is not clear why this should have been needed; attempts to reproduce the error in the original 6.5-year-old issue are unable to reproduce it today. Nevertheless, it is, or was, there for a reason. Today, the forceFullCalc option sets an option where formulas *might* not be recalculated when a cell used in the formula changes. I have not succeeded in finding a situation where it doesn't automatically recalculate, but it probably exists on complicated spreadsheets. To overcome this possibility, Excel offers a button which can be used to recalculate on demand. By itself, this might not be a terrible problem. However, it seems to come with the strange property that any spreadsheets opened at the same time as a forceFullCalc spreadsheet operate as if they too specified forceFullCalc. That *is* a problem, especially since users when closing a spreasheet affected in this way will be prompted to save it even when they haven't changed anything. I am not willing to make a BC change at this time, although I might consider it in future (PR #4240). For now, I am adding a new property `forceFullCalc` with setter (no getter needed) to Xlsx Writer. That property can be `null` (default, in which case the Xml attribute is set as today), or `false` or `true` (in which case the Xml attribute will be set to the Writer attribute). I think that, when `preCalculateFormulas` is set to false, the calling application should give consideration to setting `forceFullCalc` to false as well. All other situations should just use the default.
This commit is contained in:
@@ -169,6 +169,12 @@ $writer->save("05featuredemo.xlsx");
|
||||
**Note** Formulas will still be calculated in any column set to be autosized
|
||||
even if pre-calculated is set to false
|
||||
|
||||
**Note** Prior to release 3.7.0, the use of this feature will cause Excel to be used in a mode where opening a sheet saved in this manner *might* not automatically recalculate a cell's formula when a cell used it the formula changes. Furthermore, that behavior might be applied to all spreadsheets open at the time. To avoid this behavior, add the following statement after `setPreCalculateFormulas` above:
|
||||
```php
|
||||
$writer->setForceFullCalc(false);
|
||||
```
|
||||
In a future release, the property's default may change to `false` and that statement may no longer be required.
|
||||
|
||||
#### Office 2003 compatibility pack
|
||||
|
||||
Because of a bug in the Office2003 compatibility pack, there can be some
|
||||
|
||||
@@ -140,6 +140,8 @@ class Xlsx extends BaseWriter
|
||||
|
||||
private bool $useDynamicArray = false;
|
||||
|
||||
private ?bool $forceFullCalc = null;
|
||||
|
||||
/**
|
||||
* Create a new Xlsx Writer.
|
||||
*/
|
||||
@@ -342,7 +344,7 @@ class Xlsx extends BaseWriter
|
||||
$zipContent['xl/styles.xml'] = $this->getWriterPartStyle()->writeStyles($this->spreadSheet);
|
||||
|
||||
// Add workbook to ZIP file
|
||||
$zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas);
|
||||
$zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas, $this->forceFullCalc);
|
||||
|
||||
$chartCount = 0;
|
||||
// Add worksheets
|
||||
@@ -747,4 +749,20 @@ class Xlsx extends BaseWriter
|
||||
{
|
||||
$this->useDynamicArray = $this->preCalculateFormulas && Calculation::getInstance($this->spreadSheet)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY && !$this->useCSEArrays;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is set when a spreadsheet is opened,
|
||||
* values may not be automatically re-calculated,
|
||||
* and a button will be available to force re-calculation.
|
||||
* This may apply to all spreadsheets open at that time.
|
||||
* If null, this will be set to the opposite of $preCalculateFormulas.
|
||||
* It is likely that false is the desired setting, although
|
||||
* cases have been reported where true is required (issue #456).
|
||||
*/
|
||||
public function setForceFullCalc(?bool $forceFullCalc): self
|
||||
{
|
||||
$this->forceFullCalc = $forceFullCalc;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ class Workbook extends WriterPart
|
||||
* Write workbook to XML format.
|
||||
*
|
||||
* @param bool $preCalculateFormulas If true, formulas will be calculated before writing
|
||||
* @param ?bool $forceFullCalc If null, !$preCalculateFormulas
|
||||
*
|
||||
* @return string XML Output
|
||||
*/
|
||||
public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false): string
|
||||
public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false, ?bool $forceFullCalc = null): string
|
||||
{
|
||||
// Create XML writer
|
||||
if ($this->getParentWriter()->getUseDiskCaching()) {
|
||||
@@ -57,7 +58,7 @@ class Workbook extends WriterPart
|
||||
(new DefinedNamesWriter($objWriter, $spreadsheet))->write();
|
||||
|
||||
// calcPr
|
||||
$this->writeCalcPr($objWriter, $preCalculateFormulas);
|
||||
$this->writeCalcPr($objWriter, $preCalculateFormulas, $forceFullCalc);
|
||||
|
||||
$objWriter->endElement();
|
||||
|
||||
@@ -148,7 +149,7 @@ class Workbook extends WriterPart
|
||||
*
|
||||
* @param bool $preCalculateFormulas If true, formulas will be calculated before writing
|
||||
*/
|
||||
private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas = true): void
|
||||
private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas, ?bool $forceFullCalc): void
|
||||
{
|
||||
$objWriter->startElement('calcPr');
|
||||
|
||||
@@ -160,7 +161,11 @@ class Workbook extends WriterPart
|
||||
// fullCalcOnLoad isn't needed if we will calculate before writing
|
||||
$objWriter->writeAttribute('calcCompleted', ($preCalculateFormulas) ? '1' : '0');
|
||||
$objWriter->writeAttribute('fullCalcOnLoad', ($preCalculateFormulas) ? '0' : '1');
|
||||
$objWriter->writeAttribute('forceFullCalc', ($preCalculateFormulas) ? '0' : '1');
|
||||
if ($forceFullCalc === null) {
|
||||
$objWriter->writeAttribute('forceFullCalc', $preCalculateFormulas ? '0' : '1');
|
||||
} else {
|
||||
$objWriter->writeAttribute('forceFullCalc', $forceFullCalc ? '1' : '0');
|
||||
}
|
||||
|
||||
$objWriter->endElement();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\File;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class Issue4269Test extends TestCase
|
||||
{
|
||||
private string $outputFile = '';
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->outputFile !== '') {
|
||||
unlink($this->outputFile);
|
||||
$this->outputFile = '';
|
||||
}
|
||||
}
|
||||
|
||||
#[DataProvider('validationProvider')]
|
||||
public function testWriteArrayFormulaTextJoin(
|
||||
bool $preCalculateFormulas,
|
||||
?bool $forceFullCalc,
|
||||
string $expected
|
||||
): void {
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setCellValue('A1', '=A2*2');
|
||||
$sheet->setCellValue('A2', 0);
|
||||
|
||||
$writer = new XlsxWriter($spreadsheet);
|
||||
$writer->setPreCalculateFormulas($preCalculateFormulas);
|
||||
if ($forceFullCalc !== null) {
|
||||
$writer->setForceFullCalc($forceFullCalc);
|
||||
}
|
||||
$this->outputFile = File::temporaryFilename();
|
||||
$writer->save($this->outputFile);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
|
||||
$file = 'zip://';
|
||||
$file .= $this->outputFile;
|
||||
$file .= '#xl/workbook.xml';
|
||||
$data = file_get_contents($file);
|
||||
if ($data === false) {
|
||||
self::fail('Unable to read file');
|
||||
} else {
|
||||
self::assertStringContainsString($expected, $data);
|
||||
}
|
||||
}
|
||||
|
||||
public static function validationProvider(): array
|
||||
{
|
||||
return [
|
||||
'normal case' => [true, null, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="0"'],
|
||||
'issue 456' => [false, null, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="1"'],
|
||||
'better choice for no precalc' => [false, false, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="0"'],
|
||||
'unlikely use case' => [true, true, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="1"'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user