ReferenceHelper.php 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet;
  3. use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
  4. use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
  5. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  6. use PhpOffice\PhpSpreadsheet\Cell\DataType;
  7. use PhpOffice\PhpSpreadsheet\Style\Conditional;
  8. use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
  9. use PhpOffice\PhpSpreadsheet\Worksheet\Table;
  10. use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
  11. class ReferenceHelper
  12. {
  13. /** Constants */
  14. /** Regular Expressions */
  15. private const SHEETNAME_PART = '((\w*|\'[^!]*\')!)';
  16. private const SHEETNAME_PART_WITH_SLASHES = '/' . self::SHEETNAME_PART . '/';
  17. const REFHELPER_REGEXP_CELLREF = self::SHEETNAME_PART . '?(?<![:a-z1-9_\.\$])(\$?[a-z]{1,3}\$?\d+)(?=[^:!\d\'])';
  18. const REFHELPER_REGEXP_CELLRANGE = self::SHEETNAME_PART . '?(\$?[a-z]{1,3}\$?\d+):(\$?[a-z]{1,3}\$?\d+)';
  19. const REFHELPER_REGEXP_ROWRANGE = self::SHEETNAME_PART . '?(\$?\d+):(\$?\d+)';
  20. const REFHELPER_REGEXP_COLRANGE = self::SHEETNAME_PART . '?(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
  21. /**
  22. * Instance of this class.
  23. *
  24. * @var ?ReferenceHelper
  25. */
  26. private static ?ReferenceHelper $instance = null;
  27. private ?CellReferenceHelper $cellReferenceHelper = null;
  28. /**
  29. * Get an instance of this class.
  30. */
  31. public static function getInstance(): self
  32. {
  33. if (self::$instance === null) {
  34. self::$instance = new self();
  35. }
  36. return self::$instance;
  37. }
  38. /**
  39. * Create a new ReferenceHelper.
  40. */
  41. protected function __construct()
  42. {
  43. }
  44. /**
  45. * Compare two column addresses
  46. * Intended for use as a Callback function for sorting column addresses by column.
  47. *
  48. * @param string $a First column to test (e.g. 'AA')
  49. * @param string $b Second column to test (e.g. 'Z')
  50. */
  51. public static function columnSort(string $a, string $b): int
  52. {
  53. return strcasecmp(strlen($a) . $a, strlen($b) . $b);
  54. }
  55. /**
  56. * Compare two column addresses
  57. * Intended for use as a Callback function for reverse sorting column addresses by column.
  58. *
  59. * @param string $a First column to test (e.g. 'AA')
  60. * @param string $b Second column to test (e.g. 'Z')
  61. */
  62. public static function columnReverseSort(string $a, string $b): int
  63. {
  64. return -strcasecmp(strlen($a) . $a, strlen($b) . $b);
  65. }
  66. /**
  67. * Compare two cell addresses
  68. * Intended for use as a Callback function for sorting cell addresses by column and row.
  69. *
  70. * @param string $a First cell to test (e.g. 'AA1')
  71. * @param string $b Second cell to test (e.g. 'Z1')
  72. */
  73. public static function cellSort(string $a, string $b): int
  74. {
  75. sscanf($a, '%[A-Z]%d', $ac, $ar);
  76. /** @var int $ar */
  77. /** @var string $ac */
  78. sscanf($b, '%[A-Z]%d', $bc, $br);
  79. /** @var int $br */
  80. /** @var string $bc */
  81. if ($ar === $br) {
  82. return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
  83. }
  84. return ($ar < $br) ? -1 : 1;
  85. }
  86. /**
  87. * Compare two cell addresses
  88. * Intended for use as a Callback function for sorting cell addresses by column and row.
  89. *
  90. * @param string $a First cell to test (e.g. 'AA1')
  91. * @param string $b Second cell to test (e.g. 'Z1')
  92. */
  93. public static function cellReverseSort(string $a, string $b): int
  94. {
  95. sscanf($a, '%[A-Z]%d', $ac, $ar);
  96. /** @var int $ar */
  97. /** @var string $ac */
  98. sscanf($b, '%[A-Z]%d', $bc, $br);
  99. /** @var int $br */
  100. /** @var string $bc */
  101. if ($ar === $br) {
  102. return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
  103. }
  104. return ($ar < $br) ? 1 : -1;
  105. }
  106. /**
  107. * Update page breaks when inserting/deleting rows/columns.
  108. *
  109. * @param Worksheet $worksheet The worksheet that we're editing
  110. * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
  111. * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
  112. */
  113. protected function adjustPageBreaks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
  114. {
  115. $aBreaks = $worksheet->getBreaks();
  116. ($numberOfColumns > 0 || $numberOfRows > 0)
  117. ? uksort($aBreaks, [self::class, 'cellReverseSort'])
  118. : uksort($aBreaks, [self::class, 'cellSort']);
  119. foreach ($aBreaks as $cellAddress => $value) {
  120. /** @var CellReferenceHelper */
  121. $cellReferenceHelper = $this->cellReferenceHelper;
  122. if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
  123. // If we're deleting, then clear any defined breaks that are within the range
  124. // of rows/columns that we're deleting
  125. $worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE);
  126. } else {
  127. // Otherwise update any affected breaks by inserting a new break at the appropriate point
  128. // and removing the old affected break
  129. $newReference = $this->updateCellReference($cellAddress);
  130. if ($cellAddress !== $newReference) {
  131. $worksheet->setBreak($newReference, $value)
  132. ->setBreak($cellAddress, Worksheet::BREAK_NONE);
  133. }
  134. }
  135. }
  136. }
  137. /**
  138. * Update cell comments when inserting/deleting rows/columns.
  139. *
  140. * @param Worksheet $worksheet The worksheet that we're editing
  141. */
  142. protected function adjustComments(Worksheet $worksheet): void
  143. {
  144. $aComments = $worksheet->getComments();
  145. $aNewComments = []; // the new array of all comments
  146. foreach ($aComments as $cellAddress => &$value) {
  147. // Any comments inside a deleted range will be ignored
  148. /** @var CellReferenceHelper */
  149. $cellReferenceHelper = $this->cellReferenceHelper;
  150. if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) {
  151. // Otherwise build a new array of comments indexed by the adjusted cell reference
  152. $newReference = $this->updateCellReference($cellAddress);
  153. $aNewComments[$newReference] = $value;
  154. }
  155. }
  156. // Replace the comments array with the new set of comments
  157. $worksheet->setComments($aNewComments);
  158. }
  159. /**
  160. * Update hyperlinks when inserting/deleting rows/columns.
  161. *
  162. * @param Worksheet $worksheet The worksheet that we're editing
  163. * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
  164. * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
  165. */
  166. protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
  167. {
  168. $aHyperlinkCollection = $worksheet->getHyperlinkCollection();
  169. ($numberOfColumns > 0 || $numberOfRows > 0)
  170. ? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort'])
  171. : uksort($aHyperlinkCollection, [self::class, 'cellSort']);
  172. foreach ($aHyperlinkCollection as $cellAddress => $value) {
  173. $newReference = $this->updateCellReference($cellAddress);
  174. /** @var CellReferenceHelper */
  175. $cellReferenceHelper = $this->cellReferenceHelper;
  176. if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
  177. $worksheet->setHyperlink($cellAddress, null);
  178. } elseif ($cellAddress !== $newReference) {
  179. $worksheet->setHyperlink($newReference, $value);
  180. $worksheet->setHyperlink($cellAddress, null);
  181. }
  182. }
  183. }
  184. /**
  185. * Update conditional formatting styles when inserting/deleting rows/columns.
  186. *
  187. * @param Worksheet $worksheet The worksheet that we're editing
  188. * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
  189. * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
  190. */
  191. protected function adjustConditionalFormatting(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
  192. {
  193. $aStyles = $worksheet->getConditionalStylesCollection();
  194. ($numberOfColumns > 0 || $numberOfRows > 0)
  195. ? uksort($aStyles, [self::class, 'cellReverseSort'])
  196. : uksort($aStyles, [self::class, 'cellSort']);
  197. foreach ($aStyles as $cellAddress => $cfRules) {
  198. $worksheet->removeConditionalStyles($cellAddress);
  199. $newReference = $this->updateCellReference($cellAddress);
  200. foreach ($cfRules as &$cfRule) {
  201. /** @var Conditional $cfRule */
  202. $conditions = $cfRule->getConditions();
  203. foreach ($conditions as &$condition) {
  204. if (is_string($condition)) {
  205. /** @var CellReferenceHelper */
  206. $cellReferenceHelper = $this->cellReferenceHelper;
  207. $condition = $this->updateFormulaReferences(
  208. $condition,
  209. $cellReferenceHelper->beforeCellAddress(),
  210. $numberOfColumns,
  211. $numberOfRows,
  212. $worksheet->getTitle(),
  213. true
  214. );
  215. }
  216. }
  217. $cfRule->setConditions($conditions);
  218. }
  219. $worksheet->setConditionalStyles($newReference, $cfRules);
  220. }
  221. }
  222. /**
  223. * Update data validations when inserting/deleting rows/columns.
  224. *
  225. * @param Worksheet $worksheet The worksheet that we're editing
  226. * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
  227. * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
  228. */
  229. protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
  230. {
  231. $aDataValidationCollection = $worksheet->getDataValidationCollection();
  232. ($numberOfColumns > 0 || $numberOfRows > 0)
  233. ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort'])
  234. : uksort($aDataValidationCollection, [self::class, 'cellSort']);
  235. foreach ($aDataValidationCollection as $cellAddress => $dataValidation) {
  236. $newReference = $this->updateCellReference($cellAddress);
  237. if ($cellAddress !== $newReference) {
  238. $dataValidation->setSqref($newReference);
  239. $worksheet->setDataValidation($newReference, $dataValidation);
  240. $worksheet->setDataValidation($cellAddress, null);
  241. }
  242. }
  243. }
  244. /**
  245. * Update merged cells when inserting/deleting rows/columns.
  246. *
  247. * @param Worksheet $worksheet The worksheet that we're editing
  248. */
  249. protected function adjustMergeCells(Worksheet $worksheet): void
  250. {
  251. $aMergeCells = $worksheet->getMergeCells();
  252. $aNewMergeCells = []; // the new array of all merge cells
  253. foreach ($aMergeCells as $cellAddress => &$value) {
  254. $newReference = $this->updateCellReference($cellAddress);
  255. $aNewMergeCells[$newReference] = $newReference;
  256. }
  257. $worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array
  258. }
  259. /**
  260. * Update protected cells when inserting/deleting rows/columns.
  261. *
  262. * @param Worksheet $worksheet The worksheet that we're editing
  263. * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
  264. * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
  265. */
  266. protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
  267. {
  268. $aProtectedCells = $worksheet->getProtectedCells();
  269. ($numberOfColumns > 0 || $numberOfRows > 0)
  270. ? uksort($aProtectedCells, [self::class, 'cellReverseSort'])
  271. : uksort($aProtectedCells, [self::class, 'cellSort']);
  272. foreach ($aProtectedCells as $cellAddress => $value) {
  273. $newReference = $this->updateCellReference($cellAddress);
  274. if ($cellAddress !== $newReference) {
  275. $worksheet->protectCells($newReference, $value, true);
  276. $worksheet->unprotectCells($cellAddress);
  277. }
  278. }
  279. }
  280. /**
  281. * Update column dimensions when inserting/deleting rows/columns.
  282. *
  283. * @param Worksheet $worksheet The worksheet that we're editing
  284. */
  285. protected function adjustColumnDimensions(Worksheet $worksheet): void
  286. {
  287. $aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true);
  288. if (!empty($aColumnDimensions)) {
  289. foreach ($aColumnDimensions as $objColumnDimension) {
  290. $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1');
  291. [$newReference] = Coordinate::coordinateFromString($newReference);
  292. if ($objColumnDimension->getColumnIndex() !== $newReference) {
  293. $objColumnDimension->setColumnIndex($newReference);
  294. }
  295. }
  296. $worksheet->refreshColumnDimensions();
  297. }
  298. }
  299. /**
  300. * Update row dimensions when inserting/deleting rows/columns.
  301. *
  302. * @param Worksheet $worksheet The worksheet that we're editing
  303. * @param int $beforeRow Number of the row we're inserting/deleting before
  304. * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
  305. */
  306. protected function adjustRowDimensions(Worksheet $worksheet, int $beforeRow, int $numberOfRows): void
  307. {
  308. $aRowDimensions = array_reverse($worksheet->getRowDimensions(), true);
  309. if (!empty($aRowDimensions)) {
  310. foreach ($aRowDimensions as $objRowDimension) {
  311. $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex());
  312. [, $newReference] = Coordinate::coordinateFromString($newReference);
  313. $newRoweference = (int) $newReference;
  314. if ($objRowDimension->getRowIndex() !== $newRoweference) {
  315. $objRowDimension->setRowIndex($newRoweference);
  316. }
  317. }
  318. $worksheet->refreshRowDimensions();
  319. $copyDimension = $worksheet->getRowDimension($beforeRow - 1);
  320. for ($i = $beforeRow; $i <= $beforeRow - 1 + $numberOfRows; ++$i) {
  321. $newDimension = $worksheet->getRowDimension($i);
  322. $newDimension->setRowHeight($copyDimension->getRowHeight());
  323. $newDimension->setVisible($copyDimension->getVisible());
  324. $newDimension->setOutlineLevel($copyDimension->getOutlineLevel());
  325. $newDimension->setCollapsed($copyDimension->getCollapsed());
  326. }
  327. }
  328. }
  329. /**
  330. * Insert a new column or row, updating all possible related data.
  331. *
  332. * @param string $beforeCellAddress Insert before this cell address (e.g. 'A1')
  333. * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
  334. * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
  335. * @param Worksheet $worksheet The worksheet that we're editing
  336. */
  337. public function insertNewBefore(
  338. string $beforeCellAddress,
  339. int $numberOfColumns,
  340. int $numberOfRows,
  341. Worksheet $worksheet
  342. ): void {
  343. $remove = ($numberOfColumns < 0 || $numberOfRows < 0);
  344. if (
  345. $this->cellReferenceHelper === null
  346. || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
  347. ) {
  348. $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows);
  349. }
  350. // Get coordinate of $beforeCellAddress
  351. [$beforeColumn, $beforeRow, $beforeColumnString] = Coordinate::indexesFromString($beforeCellAddress);
  352. // Clear cells if we are removing columns or rows
  353. $highestColumn = $worksheet->getHighestColumn();
  354. $highestDataColumn = $worksheet->getHighestDataColumn();
  355. $highestRow = $worksheet->getHighestRow();
  356. $highestDataRow = $worksheet->getHighestDataRow();
  357. // 1. Clear column strips if we are removing columns
  358. if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) {
  359. $this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet);
  360. }
  361. // 2. Clear row strips if we are removing rows
  362. if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) {
  363. $this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet);
  364. }
  365. // Find missing coordinates. This is important when inserting or deleting column before the last column
  366. $startRow = $startCol = 1;
  367. $startColString = 'A';
  368. if ($numberOfRows === 0) {
  369. $startCol = $beforeColumn;
  370. $startColString = $beforeColumnString;
  371. } elseif ($numberOfColumns === 0) {
  372. $startRow = $beforeRow;
  373. }
  374. $highColumn = Coordinate::columnIndexFromString($highestDataColumn);
  375. for ($row = $startRow; $row <= $highestDataRow; ++$row) {
  376. for ($col = $startCol, $colString = $startColString; $col <= $highColumn; ++$col, ++$colString) {
  377. $worksheet->getCell("$colString$row"); // create cell if it doesn't exist
  378. }
  379. }
  380. $allCoordinates = $worksheet->getCoordinates();
  381. if ($remove) {
  382. // It's faster to reverse and pop than to use unshift, especially with large cell collections
  383. $allCoordinates = array_reverse($allCoordinates);
  384. }
  385. // Loop through cells, bottom-up, and change cell coordinate
  386. while ($coordinate = array_pop($allCoordinates)) {
  387. $cell = $worksheet->getCell($coordinate);
  388. $cellIndex = Coordinate::columnIndexFromString($cell->getColumn());
  389. if ($cellIndex - 1 + $numberOfColumns < 0) {
  390. continue;
  391. }
  392. // New coordinate
  393. $newCoordinate = Coordinate::stringFromColumnIndex($cellIndex + $numberOfColumns) . ($cell->getRow() + $numberOfRows);
  394. // Should the cell be updated? Move value and cellXf index from one cell to another.
  395. if (($cellIndex >= $beforeColumn) && ($cell->getRow() >= $beforeRow)) {
  396. // Update cell styles
  397. $worksheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex());
  398. // Insert this cell at its new location
  399. if ($cell->getDataType() === DataType::TYPE_FORMULA) {
  400. // Formula should be adjusted
  401. $worksheet->getCell($newCoordinate)
  402. ->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true));
  403. } else {
  404. // Cell value should not be adjusted
  405. $worksheet->getCell($newCoordinate)->setValueExplicit($cell->getValue(), $cell->getDataType());
  406. }
  407. // Clear the original cell
  408. $worksheet->getCellCollection()->delete($coordinate);
  409. } else {
  410. /* We don't need to update styles for rows/columns before our insertion position,
  411. but we do still need to adjust any formulae in those cells */
  412. if ($cell->getDataType() === DataType::TYPE_FORMULA) {
  413. // Formula should be adjusted
  414. $cell->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true));
  415. }
  416. }
  417. }
  418. // Duplicate styles for the newly inserted cells
  419. $highestColumn = $worksheet->getHighestColumn();
  420. $highestRow = $worksheet->getHighestRow();
  421. if ($numberOfColumns > 0 && $beforeColumn - 2 > 0) {
  422. $this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns);
  423. }
  424. if ($numberOfRows > 0 && $beforeRow - 1 > 0) {
  425. $this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows);
  426. }
  427. // Update worksheet: column dimensions
  428. $this->adjustColumnDimensions($worksheet);
  429. // Update worksheet: row dimensions
  430. $this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows);
  431. // Update worksheet: page breaks
  432. $this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows);
  433. // Update worksheet: comments
  434. $this->adjustComments($worksheet);
  435. // Update worksheet: hyperlinks
  436. $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows);
  437. // Update worksheet: conditional formatting styles
  438. $this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows);
  439. // Update worksheet: data validations
  440. $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows);
  441. // Update worksheet: merge cells
  442. $this->adjustMergeCells($worksheet);
  443. // Update worksheet: protected cells
  444. $this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows);
  445. // Update worksheet: autofilter
  446. $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns);
  447. // Update worksheet: table
  448. $this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns);
  449. // Update worksheet: freeze pane
  450. if ($worksheet->getFreezePane()) {
  451. $splitCell = $worksheet->getFreezePane();
  452. $topLeftCell = $worksheet->getTopLeftCell() ?? '';
  453. $splitCell = $this->updateCellReference($splitCell);
  454. $topLeftCell = $this->updateCellReference($topLeftCell);
  455. $worksheet->freezePane($splitCell, $topLeftCell);
  456. }
  457. // Page setup
  458. if ($worksheet->getPageSetup()->isPrintAreaSet()) {
  459. $worksheet->getPageSetup()->setPrintArea(
  460. $this->updateCellReference($worksheet->getPageSetup()->getPrintArea())
  461. );
  462. }
  463. // Update worksheet: drawings
  464. $aDrawings = $worksheet->getDrawingCollection();
  465. foreach ($aDrawings as $objDrawing) {
  466. $newReference = $this->updateCellReference($objDrawing->getCoordinates());
  467. if ($objDrawing->getCoordinates() != $newReference) {
  468. $objDrawing->setCoordinates($newReference);
  469. }
  470. if ($objDrawing->getCoordinates2() !== '') {
  471. $newReference = $this->updateCellReference($objDrawing->getCoordinates2());
  472. if ($objDrawing->getCoordinates2() != $newReference) {
  473. $objDrawing->setCoordinates2($newReference);
  474. }
  475. }
  476. }
  477. // Update workbook: define names
  478. if (count($worksheet->getParentOrThrow()->getDefinedNames()) > 0) {
  479. $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
  480. }
  481. // Garbage collect
  482. $worksheet->garbageCollect();
  483. }
  484. private static function matchSheetName(?string $match, string $worksheetName): bool
  485. {
  486. return $match === null || $match === '' || $match === "'\u{fffc}'" || $match === "'\u{fffb}'" || strcasecmp(trim($match, "'"), $worksheetName) === 0;
  487. }
  488. private static function sheetnameBeforeCells(string $match, string $worksheetName, string $cells): string
  489. {
  490. $toString = ($match > '') ? "$match!" : '';
  491. return str_replace(["\u{fffc}", "'\u{fffb}'"], $worksheetName, $toString) . $cells;
  492. }
  493. /**
  494. * Update references within formulas.
  495. *
  496. * @param string $formula Formula to update
  497. * @param string $beforeCellAddress Insert before this one
  498. * @param int $numberOfColumns Number of columns to insert
  499. * @param int $numberOfRows Number of rows to insert
  500. * @param string $worksheetName Worksheet name/title
  501. *
  502. * @return string Updated formula
  503. */
  504. public function updateFormulaReferences(
  505. string $formula = '',
  506. string $beforeCellAddress = 'A1',
  507. int $numberOfColumns = 0,
  508. int $numberOfRows = 0,
  509. string $worksheetName = '',
  510. bool $includeAbsoluteReferences = false,
  511. bool $onlyAbsoluteReferences = false
  512. ): string {
  513. $callback = fn (array $matches): string => (strcasecmp(trim($matches[2], "'"), $worksheetName) === 0) ? (($matches[2][0] === "'") ? "'\u{fffc}'!" : "'\u{fffb}'!") : "'\u{fffd}'!";
  514. if (
  515. $this->cellReferenceHelper === null
  516. || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
  517. ) {
  518. $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows);
  519. }
  520. // Update cell references in the formula
  521. $formulaBlocks = explode('"', $formula);
  522. $i = false;
  523. foreach ($formulaBlocks as &$formulaBlock) {
  524. // Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode)
  525. $i = $i === false;
  526. if ($i) {
  527. $adjustCount = 0;
  528. $newCellTokens = $cellTokens = [];
  529. // Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5)
  530. $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
  531. $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
  532. if ($matchCount > 0) {
  533. foreach ($matches as $match) {
  534. $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
  535. $modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences), 2);
  536. $modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences), 2);
  537. if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
  538. if (self::matchSheetName($match[2], $worksheetName)) {
  539. $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
  540. // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
  541. $column = 100000;
  542. $row = 10000000 + (int) trim($match[3], '$');
  543. $cellIndex = "{$column}{$row}";
  544. $newCellTokens[$cellIndex] = preg_quote($toString, '/');
  545. $cellTokens[$cellIndex] = '/(?<!\d\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
  546. ++$adjustCount;
  547. }
  548. }
  549. }
  550. }
  551. // Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E)
  552. $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
  553. $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
  554. if ($matchCount > 0) {
  555. foreach ($matches as $match) {
  556. $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
  557. $modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences), 0, -2);
  558. $modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences), 0, -2);
  559. if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
  560. if (self::matchSheetName($match[2], $worksheetName)) {
  561. $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
  562. // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
  563. $column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000;
  564. $row = 10000000;
  565. $cellIndex = "{$column}{$row}";
  566. $newCellTokens[$cellIndex] = preg_quote($toString, '/');
  567. $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?![A-Z])/i';
  568. ++$adjustCount;
  569. }
  570. }
  571. }
  572. }
  573. // Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5)
  574. $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, "$formulaBlock") ?? "$formulaBlock") . ' ';
  575. $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
  576. if ($matchCount > 0) {
  577. foreach ($matches as $match) {
  578. $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
  579. $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences);
  580. $modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences);
  581. if ($match[3] . $match[4] !== $modified3 . $modified4) {
  582. if (self::matchSheetName($match[2], $worksheetName)) {
  583. $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
  584. [$column, $row] = Coordinate::coordinateFromString($match[3]);
  585. // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
  586. $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
  587. $row = (int) trim($row, '$') + 10000000;
  588. $cellIndex = "{$column}{$row}";
  589. $newCellTokens[$cellIndex] = preg_quote($toString, '/');
  590. $cellTokens[$cellIndex] = '/(?<![A-Z]\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
  591. ++$adjustCount;
  592. }
  593. }
  594. }
  595. }
  596. // Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5)
  597. $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
  598. $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
  599. if ($matchCount > 0) {
  600. foreach ($matches as $match) {
  601. $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}");
  602. $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences);
  603. if ($match[3] !== $modified3) {
  604. if (self::matchSheetName($match[2], $worksheetName)) {
  605. $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3");
  606. [$column, $row] = Coordinate::coordinateFromString($match[3]);
  607. $columnAdditionalIndex = $column[0] === '$' ? 1 : 0;
  608. $rowAdditionalIndex = $row[0] === '$' ? 1 : 0;
  609. // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
  610. $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
  611. $row = (int) trim($row, '$') + 10000000;
  612. $cellIndex = $row . $rowAdditionalIndex . $column . $columnAdditionalIndex;
  613. $newCellTokens[$cellIndex] = preg_quote($toString, '/');
  614. $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?!\d)/i';
  615. ++$adjustCount;
  616. }
  617. }
  618. }
  619. }
  620. if ($adjustCount > 0) {
  621. if ($numberOfColumns > 0 || $numberOfRows > 0) {
  622. krsort($cellTokens);
  623. krsort($newCellTokens);
  624. } else {
  625. ksort($cellTokens);
  626. ksort($newCellTokens);
  627. } // Update cell references in the formula
  628. $formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock));
  629. }
  630. }
  631. }
  632. unset($formulaBlock);
  633. // Then rebuild the formula string
  634. return implode('"', $formulaBlocks);
  635. }
  636. /**
  637. * Update all cell references within a formula, irrespective of worksheet.
  638. */
  639. public function updateFormulaReferencesAnyWorksheet(string $formula = '', int $numberOfColumns = 0, int $numberOfRows = 0): string
  640. {
  641. $formula = $this->updateCellReferencesAllWorksheets($formula, $numberOfColumns, $numberOfRows);
  642. if ($numberOfColumns !== 0) {
  643. $formula = $this->updateColumnRangesAllWorksheets($formula, $numberOfColumns);
  644. }
  645. if ($numberOfRows !== 0) {
  646. $formula = $this->updateRowRangesAllWorksheets($formula, $numberOfRows);
  647. }
  648. return $formula;
  649. }
  650. private function updateCellReferencesAllWorksheets(string $formula, int $numberOfColumns, int $numberOfRows): string
  651. {
  652. $splitCount = preg_match_all(
  653. '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
  654. $formula,
  655. $splitRanges,
  656. PREG_OFFSET_CAPTURE
  657. );
  658. $columnLengths = array_map('strlen', array_column($splitRanges[6], 0));
  659. $rowLengths = array_map('strlen', array_column($splitRanges[7], 0));
  660. $columnOffsets = array_column($splitRanges[6], 1);
  661. $rowOffsets = array_column($splitRanges[7], 1);
  662. $columns = $splitRanges[6];
  663. $rows = $splitRanges[7];
  664. while ($splitCount > 0) {
  665. --$splitCount;
  666. $columnLength = $columnLengths[$splitCount];
  667. $rowLength = $rowLengths[$splitCount];
  668. $columnOffset = $columnOffsets[$splitCount];
  669. $rowOffset = $rowOffsets[$splitCount];
  670. $column = $columns[$splitCount][0];
  671. $row = $rows[$splitCount][0];
  672. if (!empty($column) && $column[0] !== '$') {
  673. $column = ((Coordinate::columnIndexFromString($column) + $numberOfColumns) % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
  674. $column = Coordinate::stringFromColumnIndex($column);
  675. $rowOffset -= ($columnLength - strlen($column));
  676. $formula = substr($formula, 0, $columnOffset) . $column . substr($formula, $columnOffset + $columnLength);
  677. }
  678. if (!empty($row) && $row[0] !== '$') {
  679. $row = (((int) $row + $numberOfRows) % AddressRange::MAX_ROW) ?: AddressRange::MAX_ROW;
  680. $formula = substr($formula, 0, $rowOffset) . $row . substr($formula, $rowOffset + $rowLength);
  681. }
  682. }
  683. return $formula;
  684. }
  685. private function updateColumnRangesAllWorksheets(string $formula, int $numberOfColumns): string
  686. {
  687. $splitCount = preg_match_all(
  688. '/' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '/mui',
  689. $formula,
  690. $splitRanges,
  691. PREG_OFFSET_CAPTURE
  692. );
  693. $fromColumnLengths = array_map('strlen', array_column($splitRanges[1], 0));
  694. $fromColumnOffsets = array_column($splitRanges[1], 1);
  695. $toColumnLengths = array_map('strlen', array_column($splitRanges[2], 0));
  696. $toColumnOffsets = array_column($splitRanges[2], 1);
  697. $fromColumns = $splitRanges[1];
  698. $toColumns = $splitRanges[2];
  699. while ($splitCount > 0) {
  700. --$splitCount;
  701. $fromColumnLength = $fromColumnLengths[$splitCount];
  702. $toColumnLength = $toColumnLengths[$splitCount];
  703. $fromColumnOffset = $fromColumnOffsets[$splitCount];
  704. $toColumnOffset = $toColumnOffsets[$splitCount];
  705. $fromColumn = $fromColumns[$splitCount][0];
  706. $toColumn = $toColumns[$splitCount][0];
  707. if (!empty($fromColumn) && $fromColumn[0] !== '$') {
  708. $fromColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($fromColumn) + $numberOfColumns);
  709. $formula = substr($formula, 0, $fromColumnOffset) . $fromColumn . substr($formula, $fromColumnOffset + $fromColumnLength);
  710. }
  711. if (!empty($toColumn) && $toColumn[0] !== '$') {
  712. $toColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($toColumn) + $numberOfColumns);
  713. $formula = substr($formula, 0, $toColumnOffset) . $toColumn . substr($formula, $toColumnOffset + $toColumnLength);
  714. }
  715. }
  716. return $formula;
  717. }
  718. private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows): string
  719. {
  720. $splitCount = preg_match_all(
  721. '/' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '/mui',
  722. $formula,
  723. $splitRanges,
  724. PREG_OFFSET_CAPTURE
  725. );
  726. $fromRowLengths = array_map('strlen', array_column($splitRanges[1], 0));
  727. $fromRowOffsets = array_column($splitRanges[1], 1);
  728. $toRowLengths = array_map('strlen', array_column($splitRanges[2], 0));
  729. $toRowOffsets = array_column($splitRanges[2], 1);
  730. $fromRows = $splitRanges[1];
  731. $toRows = $splitRanges[2];
  732. while ($splitCount > 0) {
  733. --$splitCount;
  734. $fromRowLength = $fromRowLengths[$splitCount];
  735. $toRowLength = $toRowLengths[$splitCount];
  736. $fromRowOffset = $fromRowOffsets[$splitCount];
  737. $toRowOffset = $toRowOffsets[$splitCount];
  738. $fromRow = $fromRows[$splitCount][0];
  739. $toRow = $toRows[$splitCount][0];
  740. if (!empty($fromRow) && $fromRow[0] !== '$') {
  741. $fromRow = (int) $fromRow + $numberOfRows;
  742. $formula = substr($formula, 0, $fromRowOffset) . $fromRow . substr($formula, $fromRowOffset + $fromRowLength);
  743. }
  744. if (!empty($toRow) && $toRow[0] !== '$') {
  745. $toRow = (int) $toRow + $numberOfRows;
  746. $formula = substr($formula, 0, $toRowOffset) . $toRow . substr($formula, $toRowOffset + $toRowLength);
  747. }
  748. }
  749. return $formula;
  750. }
  751. /**
  752. * Update cell reference.
  753. *
  754. * @param string $cellReference Cell address or range of addresses
  755. *
  756. * @return string Updated cell range
  757. */
  758. private function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false): string
  759. {
  760. // Is it in another worksheet? Will not have to update anything.
  761. if (str_contains($cellReference, '!')) {
  762. return $cellReference;
  763. }
  764. // Is it a range or a single cell?
  765. if (!Coordinate::coordinateIsRange($cellReference)) {
  766. // Single cell
  767. /** @var CellReferenceHelper */
  768. $cellReferenceHelper = $this->cellReferenceHelper;
  769. return $cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences);
  770. }
  771. // Range
  772. return $this->updateCellRange($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences);
  773. }
  774. /**
  775. * Update named formulae (i.e. containing worksheet references / named ranges).
  776. *
  777. * @param Spreadsheet $spreadsheet Object to update
  778. * @param string $oldName Old name (name to replace)
  779. * @param string $newName New name
  780. */
  781. public function updateNamedFormulae(Spreadsheet $spreadsheet, string $oldName = '', string $newName = ''): void
  782. {
  783. if ($oldName == '') {
  784. return;
  785. }
  786. foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
  787. foreach ($sheet->getCoordinates(false) as $coordinate) {
  788. $cell = $sheet->getCell($coordinate);
  789. if ($cell->getDataType() === DataType::TYPE_FORMULA) {
  790. $formula = $cell->getValue();
  791. if (str_contains($formula, $oldName)) {
  792. $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula);
  793. $formula = str_replace($oldName . '!', $newName . '!', $formula);
  794. $cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
  795. }
  796. }
  797. }
  798. }
  799. }
  800. private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
  801. {
  802. foreach ($worksheet->getParentOrThrow()->getDefinedNames() as $definedName) {
  803. if ($definedName->isFormula() === false) {
  804. $this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
  805. } else {
  806. $this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
  807. }
  808. }
  809. }
  810. private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
  811. {
  812. $cellAddress = $definedName->getValue();
  813. $asFormula = ($cellAddress[0] === '=');
  814. if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) {
  815. /**
  816. * If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF!
  817. * PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells.
  818. * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
  819. * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
  820. * them with a #REF!
  821. */
  822. if ($asFormula === true) {
  823. $formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true, true);
  824. $definedName->setValue($formula);
  825. } else {
  826. $definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true));
  827. }
  828. }
  829. }
  830. private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
  831. {
  832. if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) {
  833. /**
  834. * If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF!
  835. * PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells.
  836. * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
  837. * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
  838. * them with a #REF!
  839. */
  840. $formula = $definedName->getValue();
  841. $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true);
  842. $definedName->setValue($formula);
  843. }
  844. }
  845. /**
  846. * Update cell range.
  847. *
  848. * @param string $cellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3')
  849. *
  850. * @return string Updated cell range
  851. */
  852. private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false): string
  853. {
  854. if (!Coordinate::coordinateIsRange($cellRange)) {
  855. throw new Exception('Only cell ranges may be passed to this method.');
  856. }
  857. // Update range
  858. $range = Coordinate::splitRange($cellRange);
  859. $ic = count($range);
  860. for ($i = 0; $i < $ic; ++$i) {
  861. $jc = count($range[$i]);
  862. for ($j = 0; $j < $jc; ++$j) {
  863. /** @var CellReferenceHelper */
  864. $cellReferenceHelper = $this->cellReferenceHelper;
  865. if (ctype_alpha($range[$i][$j])) {
  866. $range[$i][$j] = Coordinate::coordinateFromString(
  867. $cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences, $onlyAbsoluteReferences)
  868. )[0];
  869. } elseif (ctype_digit($range[$i][$j])) {
  870. $range[$i][$j] = Coordinate::coordinateFromString(
  871. $cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences)
  872. )[1];
  873. } else {
  874. $range[$i][$j] = $cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences);
  875. }
  876. }
  877. }
  878. // Recreate range string
  879. return Coordinate::buildRange($range);
  880. }
  881. private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void
  882. {
  883. $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns);
  884. $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn);
  885. for ($row = 1; $row <= $highestRow - 1; ++$row) {
  886. for ($column = $startColumnId; $column !== $endColumnId; ++$column) {
  887. $coordinate = $column . $row;
  888. $this->clearStripCell($worksheet, $coordinate);
  889. }
  890. }
  891. }
  892. private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void
  893. {
  894. $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn);
  895. ++$highestColumn;
  896. for ($column = $startColumnId; $column !== $highestColumn; ++$column) {
  897. for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) {
  898. $coordinate = $column . $row;
  899. $this->clearStripCell($worksheet, $coordinate);
  900. }
  901. }
  902. }
  903. private function clearStripCell(Worksheet $worksheet, string $coordinate): void
  904. {
  905. $worksheet->removeConditionalStyles($coordinate);
  906. $worksheet->setHyperlink($coordinate);
  907. $worksheet->setDataValidation($coordinate);
  908. $worksheet->removeComment($coordinate);
  909. if ($worksheet->cellExists($coordinate)) {
  910. $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL);
  911. $worksheet->getCell($coordinate)->setXfIndex(0);
  912. }
  913. }
  914. private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
  915. {
  916. $autoFilter = $worksheet->getAutoFilter();
  917. $autoFilterRange = $autoFilter->getRange();
  918. if (!empty($autoFilterRange)) {
  919. if ($numberOfColumns !== 0) {
  920. $autoFilterColumns = $autoFilter->getColumns();
  921. if (count($autoFilterColumns) > 0) {
  922. $column = '';
  923. $row = 0;
  924. sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
  925. $columnIndex = Coordinate::columnIndexFromString((string) $column);
  926. [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange);
  927. if ($columnIndex <= $rangeEnd[0]) {
  928. if ($numberOfColumns < 0) {
  929. $this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter);
  930. }
  931. $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];
  932. // Shuffle columns in autofilter range
  933. if ($numberOfColumns > 0) {
  934. $this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter);
  935. } else {
  936. $this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter);
  937. }
  938. }
  939. }
  940. }
  941. $worksheet->setAutoFilter(
  942. $this->updateCellReference($autoFilterRange)
  943. );
  944. }
  945. }
  946. private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void
  947. {
  948. // If we're actually deleting any columns that fall within the autofilter range,
  949. // then we delete any rules for those columns
  950. $deleteColumn = $columnIndex + $numberOfColumns - 1;
  951. $deleteCount = abs($numberOfColumns);
  952. for ($i = 1; $i <= $deleteCount; ++$i) {
  953. $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
  954. if (isset($autoFilterColumns[$columnName])) {
  955. $autoFilter->clearColumn($columnName);
  956. }
  957. ++$deleteColumn;
  958. }
  959. }
  960. private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void
  961. {
  962. $startColRef = $startCol;
  963. $endColRef = $rangeEnd;
  964. $toColRef = $rangeEnd + $numberOfColumns;
  965. do {
  966. $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef));
  967. --$endColRef;
  968. --$toColRef;
  969. } while ($startColRef <= $endColRef);
  970. }
  971. private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void
  972. {
  973. // For delete, we shuffle from beginning to end to avoid overwriting
  974. $startColID = Coordinate::stringFromColumnIndex($startCol);
  975. $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
  976. $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);
  977. do {
  978. $autoFilter->shiftColumn($startColID, $toColID);
  979. ++$startColID;
  980. ++$toColID;
  981. } while ($startColID !== $endColID);
  982. }
  983. private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
  984. {
  985. $tableCollection = $worksheet->getTableCollection();
  986. foreach ($tableCollection as $table) {
  987. $tableRange = $table->getRange();
  988. if (!empty($tableRange)) {
  989. if ($numberOfColumns !== 0) {
  990. $tableColumns = $table->getColumns();
  991. if (count($tableColumns) > 0) {
  992. $column = '';
  993. $row = 0;
  994. sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
  995. $columnIndex = Coordinate::columnIndexFromString((string) $column);
  996. [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange);
  997. if ($columnIndex <= $rangeEnd[0]) {
  998. if ($numberOfColumns < 0) {
  999. $this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table);
  1000. }
  1001. $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];
  1002. // Shuffle columns in table range
  1003. if ($numberOfColumns > 0) {
  1004. $this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table);
  1005. } else {
  1006. $this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table);
  1007. }
  1008. }
  1009. }
  1010. }
  1011. $table->setRange($this->updateCellReference($tableRange));
  1012. }
  1013. }
  1014. }
  1015. private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void
  1016. {
  1017. // If we're actually deleting any columns that fall within the table range,
  1018. // then we delete any rules for those columns
  1019. $deleteColumn = $columnIndex + $numberOfColumns - 1;
  1020. $deleteCount = abs($numberOfColumns);
  1021. for ($i = 1; $i <= $deleteCount; ++$i) {
  1022. $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
  1023. if (isset($tableColumns[$columnName])) {
  1024. $table->clearColumn($columnName);
  1025. }
  1026. ++$deleteColumn;
  1027. }
  1028. }
  1029. private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
  1030. {
  1031. $startColRef = $startCol;
  1032. $endColRef = $rangeEnd;
  1033. $toColRef = $rangeEnd + $numberOfColumns;
  1034. do {
  1035. $table->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef));
  1036. --$endColRef;
  1037. --$toColRef;
  1038. } while ($startColRef <= $endColRef);
  1039. }
  1040. private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
  1041. {
  1042. // For delete, we shuffle from beginning to end to avoid overwriting
  1043. $startColID = Coordinate::stringFromColumnIndex($startCol);
  1044. $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
  1045. $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);
  1046. do {
  1047. $table->shiftColumn($startColID, $toColID);
  1048. ++$startColID;
  1049. ++$toColID;
  1050. } while ($startColID !== $endColID);
  1051. }
  1052. private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void
  1053. {
  1054. $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1);
  1055. for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) {
  1056. // Style
  1057. $coordinate = $beforeColumnName . $i;
  1058. if ($worksheet->cellExists($coordinate)) {
  1059. $xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
  1060. for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) {
  1061. if (!empty($xfIndex) || $worksheet->cellExists([$j, $i])) {
  1062. $worksheet->getCell([$j, $i])->setXfIndex($xfIndex);
  1063. }
  1064. }
  1065. }
  1066. }
  1067. }
  1068. private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void
  1069. {
  1070. $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
  1071. for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) {
  1072. // Style
  1073. $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1);
  1074. if ($worksheet->cellExists($coordinate)) {
  1075. $xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
  1076. for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) {
  1077. if (!empty($xfIndex) || $worksheet->cellExists([$j, $i])) {
  1078. $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex);
  1079. }
  1080. }
  1081. }
  1082. }
  1083. }
  1084. /**
  1085. * __clone implementation. Cloning should not be allowed in a Singleton!
  1086. */
  1087. final public function __clone()
  1088. {
  1089. throw new Exception('Cloning a Singleton is not allowed!');
  1090. }
  1091. }