230 Commits

Author SHA1 Message Date
oleibman 6f678802ee Update Sheetname in Charts When Sheetname Changes
Fix #744 (marked stale in 2019, but now reopened). The fix draws heavily on the work of @guillaume-ro-fr in PR #745, which was closed because of a unit test error which is no longer possible, and because it was targeted to a branch which no longer existed.

Changing a worksheet name can wind up invalidating charts. To fix this: when a worksheet name changes, inspect all the charts in the spreadsheet (the old PR changed only the current worksheet) and change worksheet references from the old name to the new one.

During testing, found and corrected some minor problems with cloning some Chart objects.
2026-09-04 09:41:23 -07:00
oleibman 608da6024e Merge branch 'master' into stancomments 2026-08-04 20:56:58 -07:00
oleibman 08eb18d7ae Unset PivotTableCollection at Destruct, plus some Performance Tweaks 2026-08-03 19:05:00 -07:00
saifulislamferoz 2e4cc54a0d Fix corrupt date-grouped pivot table and import ordering
Excel reported sample 3's ByQuarter sheet as corrupt: a date field
group (rangePr groupBy="quarters") was emitted without the date bounds
Excel requires. When no explicit start/end date is supplied, fall back
to the sentinel range (1900-01-01 .. 9999-12-31) so both the sharedItems
(minDate/maxDate) and the rangePr (startDate/endDate) are always present,
matching the <1/1/1900 / >12/31/9999 out-of-range group items.

Also fixes ordered_imports in Worksheet.php (php-cs-fixer CI failure):
the PivotTable\PivotTable use statement is moved before the Sparkline
imports.
2026-08-03 16:16:35 +06:00
oleibman 4bf2f8f326 Merge branch 'master' into stancomments 2026-08-03 00:45:46 -07:00
saifulislamferoz 5efba3ee64 Increase pivot table test coverage
Cover the previously-unexercised code paths flagged by Coveralls:
- writer: month and year date grouping, and a fractional numeric interval
  (the non-integer num() path);
- reader: reading an explicit dataField subtotal attribute back from a saved
  file;
- worksheet: the getPivotTables() alias, addPivotTable(), case-insensitive
  getPivotTableByName(), the no-match lookup, and removePivotTableCollection().
2026-08-03 11:52:58 +06:00
saifulislamferoz 2046913138 Add unit tests for pivot table model accessors
Cover the PivotFieldGroup, PivotField, PivotCacheDefinition and PivotTable
accessors that were not exercised by the reader/builder tests (numeric and
date grouping getters, field-group storage on the cache definition, pivot
field setters, and PivotTable::__toString), restoring code coverage.
2026-08-03 11:52:58 +06:00
saifulislamferoz e5b0477559 Fix PHPStan errors in pivot table tests
Use getSheetByNameOrThrow() instead of the nullable getSheetByName() for
chained calls, assert non-null before dereferencing getCacheDefinition() and
getPivotTableByName(), and replace an inline array_map closure with a typed
helper so PHPStan (level 8) is satisfied.
2026-08-03 11:52:58 +06:00
saifulislamferoz c899e5cb75 Add pivot page (filter) fields and numeric/date grouping
Extends PivotTableBuilder with the two remaining common pivot capabilities.

Page fields: addPageField() places a field on the page (report filter) axis,
and the writer emits a <pageFields> section (in its correct schema position,
after colFields and before dataFields) with the field marked axis="axisPage".

Grouping: a new Worksheet\PivotTable\PivotFieldGroup value object captures how a
field is grouped, and the builder gains groupFieldByNumericRange() (fixed-width
buckets between a start and end number) and groupFieldByDate() (group a date
field by a calendar unit such as quarters or months). The grouping is written
into the pivot cache definition as a <fieldGroup> with <rangePr> and computed
<groupItems>; with refresh-on-load the spreadsheet application materialises the
buckets when it opens the file.

The generated cache definition, workbook wiring and content types remain a
structurally consistent OPC package, and pivots with page fields or grouping
read back into the object model. Tests cover page-field placement/output,
numeric range grouping, date quarter grouping, and validation of unknown
fields.
2026-08-03 11:52:55 +06:00
saifulislamferoz 5aa558b6d6 Add pivot table creation via PivotTableBuilder (Xlsx)
Adds the ability to create a new pivot table from a range of source data,
building on the existing read model and round-trip preservation.

New Worksheet\PivotTable\PivotTableBuilder provides a fluent API: point it at a
source worksheet and range (its first row supplies the field names), place
fields on the row/column axes and add value fields with an aggregation
function, then build() produces a PivotTable and registers it on a target
sheet. Source field names are resolved from the header row (RichText/inline
string headers included), and distinct values are collected for the cache
definition's sharedItems.

The model gains what generation needs: PivotTable::isGenerated(), per-field
data-field captions, subtotal-function constants on PivotField, and sharedItems
on PivotCacheDefinition.

Writer: a new Writer\Xlsx\PivotTable generates the pivotTableDefinition,
pivotCacheDefinition (with refreshOnLoad set) and an empty pivotCacheRecords
part, plus their rels. On save, generated pivot tables are assigned indices
that continue past any preserved pivot parts (so a workbook can mix loaded and
new pivots without collision) and are wired into the worksheet relationships,
workbook <pivotCaches> registry, workbook relationships and content types via
the same unparsed-data path used for preservation. Values are computed by the
spreadsheet application on open via refresh-on-load, so we don't reimplement
the aggregation engine.

Output is a structurally consistent OPC package and reads back into the object
model. Tests cover building, validation errors, the generated parts and
wiring, aggregation attributes, and mixing preserved with generated pivots.
2026-08-03 11:52:45 +06:00
oleibman 40911aa104 Update Phpstan and Tcpdf
Many problems with Dependabot this month. Phpstan introduced a lot of new "errors". These are now fixed or annotated. I combined this with a change to require comments for `phpstan-ignore`. These won't always be useful, but I think requiring them makes sense.

Tcpdf is more of a non-update. Our composer.json specified `^6.5`. For some reason, Dependabot decided it was okay to change that to `^6.5||^7.0`, which seems presumptuous. (One of the triggers was probably the elimination of Php8.1, since the new product requires 8.2+.) Tcpdf is nominally deprecated, replaced by tc-lib-pdf. Tcpdf 7 passes control to the new product. However, the upgrade is not straightforward. The user needs to supply font files which were formerly distributed with the product, and a code change to define a (shudder) global constant is required. Consequently, Dependabot's upgrade failed its unit tests. While I may evaluate what might be needed at some point in the future, for now I am just updating composer.json to reject Tcpdf 7+.
2026-08-01 22:06:38 -07:00
saifulislamferoz 041c85060f Improve sparkline test coverage and harden reader
Add tests for previously-uncovered SparklineGroup setters (axis types,
manual limits, additional display options and colours) and a hand-crafted
Xlsx fixture exercising the reader's edge cases (non-sparkline ext, group
without sparklines, sparkline with empty sqref, missing colour elements).

Harden Reader/Xlsx/Sparklines against malformed input: guard the
sparklineGroup and sparkline iterations against a null children set so an
empty <x14:sparklineGroups> or <x14:sparklines> element no longer emits a
PHP warning.
2026-07-28 06:40:12 +06:00
saifulislamferoz 41c051f54d Add support for Excel sparklines
Implements native sparkline support (line, column, and win/loss) in the
Xlsx reader and writer, resolving issue #4941.

- New model classes under Worksheet/Sparkline: Sparkline, SparklineGroup,
  and the SparklineType enum.
- Worksheet gains a sparkline group collection with addSparkline(),
  addSparklineGroup(), getSparklineGroupCollection(), and
  removeSparklineGroupCollection(), plus deep-clone support.
- Reader/Xlsx/Sparklines parses x14:sparklineGroups from the sheet extLst.
- Writer/Xlsx/Worksheet emits x14:sparklineGroups; writeExtLst is
  refactored so conditional-formatting data bars and sparklines share a
  single extLst.
- Adds unit tests, an Xlsx round-trip test, a runnable sample, and docs.
2026-07-28 06:04:37 +06:00
oleibman d8c6890861 Merge pull request #4926 from oleibman/issue4917
Very Minor Changes to Worksheet, Reader/Xlsx, and Worksheet/AutoFilter
2026-06-29 03:44:56 +00:00
oleibman 13edf0d8a2 More AutoFilter Tweaks 2026-06-25 17:08:15 -07:00
oleibman 8fd7c56ef0 Consistent HighestRow/Column After Row/Column Delete
Fix #943, which went stale a long time ago, and is now reopened. I believe that the specific problem in that issue was actually mostly resolved some time ago. However, `highestRow/Column` produces questionable results when the delete range overlaps the highest row/column. That is fixed by this PR.

In addition, `removeRow/Column` allow the specification of a non-negative value for `numberOfRows/Columns`. But the expected results in those cases are not defined, and the actual results probably do not meet user expectations. I believe that very few, if any, users are taking advantage of this "feature", but we may as well straighten it out. For 0 rows/columns, I think it makes perfect sense to do nothing. As for negative - if `removeRow(5, 2)` says to remove 2 rows *starting* with 5, I think `removeRow(5, -2)` ought to remove (up to) 2 rows *ending* with 5. The undefined behaviors are changed to act as described in this paragraph.
2026-06-24 08:41:09 -07:00
oleibman 9698573e32 getChartIndex
It requires an index to an array which needs to be supplied as a string, even though the array is indexed exclusively by int. It is called only once in the code base, by some inefficient code which becomes much simpler when restructured and eliminates the call altogether. It is not independently tested. The use case for its existence is not clear. It should probably just be deprecated. However, for now, I'm just allowing the parameter to be int|string, rather than just string, and adding tests.
2026-06-04 12:40:57 -07:00
oleibman 14a641f924 Phpstan Changes
A large volume of changes due to "sealed" arrays. Some are sensible; some are very confusing and are handled with `ignore` annotations.

Modules in src with changed code (as opposed to doc-blocks or annotations) are:

- DateTimeExcelDays
- Reader/Xlsx
- Reader/Xlsx/AutoFilter
- Reader/Xlsx/Chart
- Reader/Xlsx/ConditionalStyles
- Reader/Xlsx/TableReader
2026-06-01 01:14:59 -07:00
oleibman b45c4f7ee0 Correct Spacing 2026-04-03 21:12:33 -07:00
oleibman 823f7ee535 More Tests 2026-04-03 20:49:02 -07:00
oleibman 10305b3a48 Additional Test 2026-04-03 15:32:45 -07:00
oleibman 40f7cd00db Consistent Usage of Column and Row Limits
We have identical constants defined in several places, and use literals in others. We aren't consistent in checking limits. This PR makes the use of the constants in Cell/AddressRange the "official" source, deprecates all other constants, and substitutes the constants wherever literals are used. A number of different edge case tests are added.

During testing, I discovered that `columnIndexFromString` correctly throws an exception for 4-character string, but allows `XFE` through `ZZZ`, all of which are also invalid. There are similar inconsistencies with related routines, and this PR attempts to make them operate consistently. One suprise is that throwing for `row=0` causes serious regression problems, so it continues to be permitted (but the high row limit is enforced).

Further, Reference Helper sometimes dips into negative numbers, resulting in totally unexpected results (-1 affects column Z, -2 column Y, etc.). It is changed to ignore rows and columns outside the limits.
2026-03-03 19:04:11 -08:00
oleibman c735e2fc82 Use Cell Value if OldCalculatedValue is Null 2026-01-23 18:15:41 -08:00
oleibman 7aef8b24cc Option To Use OldCalculatedValue in ToArray and its Relatives
Fix #1810, just before it turns 6 years old. Reporter thinks it can save time. Easy to implement, especially with the advent of named parameters which were not available when issue was opened.
2026-01-23 14:56:16 -08:00
oleibman 2241b2ca00 A Bit More Coverage 2025-12-27 17:20:49 -08:00
oleibman 2bb1c9df3c Dependencies
A dependency checker found the following minor problems:
- `TIMEVALUE` uses the wrong case for `DateTime`.
- The `filter` extension is used but is not declared as a dependency.
- The `exif` extension is used but is not declared as a dependency.

Php tolerates the first, but the solution is so trivial that it might as well be applied.

You have to go out of your way to not include `filter` - is it almost always builtin. Nevertheless, it shouldn't hurt to explicitly declare it.

`exif` is used in only one place, and, if it's not available there, we fall back to `GD`, which *is* a dependency. Getting rid of the `exif` portion should be harmless.
2025-12-27 16:15:30 -08:00
oleibman 01f9b2a60b Single Character Table Names
Fix #4739. Change `+` to `*` in regexp. Add test.
2025-12-06 05:58:28 -08:00
oleibman aa719cdb02 Merge branch 'master' into explicit 2025-11-21 16:57:24 -08:00
oleibman 78190ffdb6 Prepare to Deprecate Optionality of Second Arg to Cell::setValueExplicit
In PR #4716, @RobinvanderVliet points out that Cell::setValueExplicit and Worksheet::setCellValueExplicit are strangely inconsistent, where the latter requires you to specify a DataType but the former does not. The fix in that PR is to make the latter offer a default DataType rather than requiring the parameter. While that does eliminate inconsistency, I think it does so in the wrong direction - the solution should be to eliminate the optionality in the former - an implicit value for setValueExplicit just doesn't make sense. We can't do that without a breaking change, which this is not. However, we can update the doc-block and change log to indicate our intention to make that change in the next breaking release.
2025-11-21 00:12:07 -08:00
Robin van der Vliet 603b92e827 Fix lots of typos throughout codebase 2025-11-09 17:10:55 +01:00
oleibman 2bc326bb16 Merge branch 'master' into issue4695 2025-11-07 08:28:30 -08:00
oleibman f1562c967b Protected Ranges and Insert/Delete Columns/Rows
Fix #4695. PhpSpreadsheet has assumed that each Protected Range on a Worksheet is defined in the Xml using a single range. However, Excel itself will often combine ranges together. So, in the xml for the spreadsheet supplied with the issue, we see:
```xml
<protectedRange name="Champs_non_bloqués_2" sqref="B2:D4 J2:L4 F2:H4"/>
```

When inserting or deleting rows or columns, ReferenceHelper is used to adjust (among other things) Protected Ranges. The existing code (probably) works well enough if there is only one range in `sqref`, but fails to parse correctly and throws an Exception when there is more than one. It is replaced with a fairly straightforward manner of updating when rows/columns are inserted, and a slightly more convoluted manner of updating when rows/columns are deleted.
2025-11-01 21:58:23 -07:00
oleibman b9857e4804 AutoFilter Exposures 2025-10-31 10:12:17 -07:00
oleibman 26dfae12e8 Merge branch 'master' into intersect 2025-09-16 17:50:18 -07:00
oleibman f3e2eba517 Additional Coverage 2025-09-15 19:48:43 -07:00
oleibman abf357ea90 1904 Calendar 2025-09-14 08:08:46 -07:00
oleibman 00191100ba Deprecate Worksheet::getHashInt and Spreadsheet::getId
Php8.5 *may* deprecate the use of `__wakeup` (it is planned, but not yet implemented, and it is convtroversial). We use it only twice. In the first instance, it just throws an exception to prevent a security exploit. This can be trivially replaced with `__unserialize`.

The other instance is merely to initialize a Worksheet instance variable. Converting this use to `__unserialize` is *not* trivial (one of the reasons for the controversy). However, I see no useful purpose for that variable. Since it has no use, there is no real need for `__wakeup`, so we will just remove the routine altogether, and deprecate the variable's getter (there is no setter).

A similar instance variable in Spreadsheet also serves no useful purpose, so we will deprecate its getter as well (again no setter).
2025-09-10 00:27:22 -07:00
oleibman f8f78a7ed3 String Increments and Php8.5
Fix #4600. String incrementation through the `++` operator is deprecated in Php 8.5. Because we make use of that operator to iterate through columns, we are particularly hard hit by that change - unaddressed, it causes over 2,000 errors in our test suite! It is, fortunately, not as difficult as I feared to correct. Replacing the `++` operator with a call to new method `StringHelper::stringIncrement` in 79 statements scattered over 31 source modules (in src, samples, test, and infra) eliminates all the messages in the test suite. It is possible that others are lurking, but I don't know a systematic way of determining if there are others. We'll stick with this for now, and deal with any others as they show up.

This PR will be applied to the master, release390, and release222 branches. It will not be applied to the release210 or release1291 branches, which will now accept security changes only.
2025-08-27 18:18:40 -07:00
oleibman b99dc608a0 Merge pull request #4598 from oleibman/issue2912
Better Handling of Print Area After Column/Row Removal
2025-08-26 16:15:58 +00:00
oleibman 31502b05d5 Merge branch 'master' into maxwidth 2025-08-26 07:36:50 -07:00
oleibman 5e02e282c7 Better Handling of Print Area After Column/Row Removal
Fix #2912, another oldie (3+ years). Removal of rows or columns which include part of the print area is not recalculating the print area correctly. This PR will correct that problem, but only if the print area consists of a single range. I think that is by far the most common use case. If there is a demonstrated need to handle multiple ranges, I will respond to a new issue.
2025-08-24 09:05:12 -07:00
oleibman c3d24e5bb5 WIP Some Additional Support for Intersection and Union
Add some additional support for Intersection and Union in the Calculation Engine. This allows me to reinstate 2 tests which were formerly skipped, without breaking any existing tests. There are almost certainly edge cases which I haven't thought of yet; I will leave this PR as a draft for several weeks before moving it formward.

This change gives some opportunities for users to go wrong. If you place the following formula in a cell:
```
=B1:B8 A7:D7
```
Excel will treat the space as an intersection operator, and will return the value in B7, which is where the ranges intersect. PhpSpreadsheet will do so as well. But, if you use the following formula:
```
=B1:B8,A7:D7
```
Excel will return `#VALUE!`. This seems like something they forgot to take care of when adding dynamic arrays. The comma should be interpreted as a union operator, and PhpSpreadsheet will now return the union of the ranges. It seems very difficult for PhpSpreadsheet to return an error when the formula seems easily evaluated. Furthermore, if you use the following formula:
```
=SUM(B1:B8,A7:D7)
```
Excel evaluates it as you would expect, summing the union of the ranges. So does PhpSpreadsheet. I guess there's no rule requiring Excel to be consistent, but ...

Another way that users might go wrong is by actually entering the union or intersection symbols in a formula rather than comma or space. Excel will not allow this; PhpSpreadsheet needs to change comma or space to the appropriate symbol in order for the rest of this PR to work properly. By the time the parser gets to it, it can't tell whether the symbol was part of the cell's original formula or if PhpSpreadsheet substituted it, so it just has to permit it. I do not expect many users to fall afoul of this problem, at least not more than once.

Just by way of explanation, the reason why PhpSpreadsheet has to make the substitution is because intersection has a higher priority than union, just as multiplication has a higher priority than addition. So, if you don't change the symbols beforehand, you may wind up figuring out that you need to perform an intersection too late - a lower-priority union may have already taken place.
2025-08-22 16:16:55 -07:00
oleibman 06fef872be Maximum Column Width
PR #455 was submitted by @Aketos in 2018. It added no unit tests, so it was not merged, and it eventually went stale. Without the tests, I'm not sure exactly what the user had in mind. But my investigation indicates the following:
- for Xls files, if the column width in the file specifies a value greater than 255, Excel will choose its own width when it opens the file.
- for Xlsx files:
    - Excel does not allow you to set a column width > 255, neither by dragging the column boundary, nor by right-clicking and setting a value, nor by auto-fitting the column width.
    - Nevertheless, if the XML specifies a column width > 255, Excel will honor that value in the displayed spreadsheet, and even write it out if the file is saved.


I have taken a different approach than the original PR, which changed `Worksheet::calculateColumnWidths`. Instead, this PR adds an option to the Xlsx Writer to either restrict column widths to 255 or not. The default is "not" in order to avoid a breaking change. For emulating Excel's behavior, in the unusual situation where it might matter, the user might consider using the non-default option.

This PR also restricts column dimension width to 255 when saving an Xls file.
2025-08-17 20:29:47 -07:00
oleibman 3174773a81 Merge pull request #4580 from oleibman/issue1457
splitRange and ProtectedRange
2025-08-17 04:12:03 +00:00
oleibman 05fa51581c splitRange and ProtectedRange
Fix #1457, which had gone stale but is now re-opened. The `Coordinate::splitRange` method expects a string of cell ranges, but it is a bit limited. Excel sometimes uses comma for union and space for intersection, and sometimes vice versa. `splitRange` uses comma for union, and doesn't do anything with spaces. This PR adds a new method `Coordinate::allRanges` which handles both union and intersection, and adds a parameter to indicate whether comma means union or intersection (with space meaning the other). Also, since the issue specifically mentioned this as a problem for `ProtectedRange`, an `allRanges` method is added to that class.
2025-08-10 15:05:51 -07:00
oleibman 1e6668fe58 Copy Cell Adjusting Formula
Fix #1203. The issue actually complains about the documentation, but I think it wants documented functionality that doesn't yet exist. This PR adds a method for copying a formula from one cell to another, adjusting cell references in the formula as Excel would. For a non-formula, it copies the value without making any adjustments.
2025-08-10 13:45:03 -07:00
oleibman fa2c32996a Merge branch 'master' into issue4588 2025-08-06 21:57:24 -07:00
oleibman 3dcdbcac3e Redo ComplexAssert
Its use is already causes an issue with Phpstan. It uses interfaces marked as internal by Phpunit, and it will not work with Phpunit 12. It is more complicated than needed. This PR corrects all these problems. It also corrects a handful of other problems that will show up with Phpunit 12. Only tests are changed - no source code.
2025-08-03 08:25:05 -07:00
oleibman 150b3849a8 Create Uninitialized Cell If Used in Calculation
Fix #4558. Fix #4530. 4558 deals with a problem with COUNTA. Excel does not count empty cells, but it does count literal nulls (usually empty parameters) in the argument list. PhpSpreadsheet has till now not created empty cells during calculation, and so winds up treating them as literals. It is changed to create the cell when appropriate.

4530 has a similar problem. When INDIRECT winds up evaluating an uninitialized cell, it treats it as a null literal, leading to incorrect results. It is now changed to create the missing cell.

A number of functions, almost all of them Financial, have a related problem. They test for a null literal when initializing some of their parameters, but they should be testing for null literal or cell containing null. They are changed to do the right thing.

SUMIF and related functions are slightly affected by this change, and are changed so that they are no longer affected.

New tests have been added. Only one existing test had to change. MergeBehaviorTest for `[12, '=5+1', '=A1/A2']`, where cell A2 was uninitialized formerly returned one row, but now returns a second all-null row because A2, by virtue of being used in a calculation, now winds up defined. I do not consider this a significant difference.
2025-07-31 23:58:05 -07:00
oleibman a6e2e71791 Merge branch 'master' into issue1425 2025-05-18 12:43:54 -07:00