ReferenceHelper.php 54 KB

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