Cell.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Cell;
  3. use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
  4. use PhpOffice\PhpSpreadsheet\Collection\Cells;
  5. use PhpOffice\PhpSpreadsheet\Exception;
  6. use PhpOffice\PhpSpreadsheet\RichText\RichText;
  7. use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
  8. use PhpOffice\PhpSpreadsheet\Style\Style;
  9. use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
  10. use Throwable;
  11. class Cell
  12. {
  13. /**
  14. * Value binder to use.
  15. *
  16. * @var IValueBinder
  17. */
  18. private static $valueBinder;
  19. /**
  20. * Value of the cell.
  21. *
  22. * @var mixed
  23. */
  24. private $value;
  25. /**
  26. * Calculated value of the cell (used for caching)
  27. * This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
  28. * create the original spreadsheet file.
  29. * Note that this value is not guaranteed to reflect the actual calculated value because it is
  30. * possible that auto-calculation was disabled in the original spreadsheet, and underlying data
  31. * values used by the formula have changed since it was last calculated.
  32. *
  33. * @var mixed
  34. */
  35. private $calculatedValue;
  36. /**
  37. * Type of the cell data.
  38. *
  39. * @var string
  40. */
  41. private $dataType;
  42. /**
  43. * Collection of cells.
  44. *
  45. * @var Cells
  46. */
  47. private $parent;
  48. /**
  49. * Index to cellXf.
  50. *
  51. * @var int
  52. */
  53. private $xfIndex = 0;
  54. /**
  55. * Attributes of the formula.
  56. */
  57. private $formulaAttributes;
  58. /**
  59. * Update the cell into the cell collection.
  60. *
  61. * @return $this
  62. */
  63. public function updateInCollection(): self
  64. {
  65. $this->parent->update($this);
  66. return $this;
  67. }
  68. public function detach(): void
  69. {
  70. // @phpstan-ignore-next-line
  71. $this->parent = null;
  72. }
  73. public function attach(Cells $parent): void
  74. {
  75. $this->parent = $parent;
  76. }
  77. /**
  78. * Create a new Cell.
  79. *
  80. * @param mixed $value
  81. * @param string $dataType
  82. */
  83. public function __construct($value, $dataType, Worksheet $worksheet)
  84. {
  85. // Initialise cell value
  86. $this->value = $value;
  87. // Set worksheet cache
  88. $this->parent = $worksheet->getCellCollection();
  89. // Set datatype?
  90. if ($dataType !== null) {
  91. if ($dataType == DataType::TYPE_STRING2) {
  92. $dataType = DataType::TYPE_STRING;
  93. }
  94. $this->dataType = $dataType;
  95. } elseif (!self::getValueBinder()->bindValue($this, $value)) {
  96. throw new Exception('Value could not be bound to cell.');
  97. }
  98. }
  99. /**
  100. * Get cell coordinate column.
  101. *
  102. * @return string
  103. */
  104. public function getColumn()
  105. {
  106. return $this->parent->getCurrentColumn();
  107. }
  108. /**
  109. * Get cell coordinate row.
  110. *
  111. * @return int
  112. */
  113. public function getRow()
  114. {
  115. return $this->parent->getCurrentRow();
  116. }
  117. /**
  118. * Get cell coordinate.
  119. *
  120. * @return string
  121. */
  122. public function getCoordinate()
  123. {
  124. try {
  125. $coordinate = $this->parent->getCurrentCoordinate();
  126. } catch (Throwable $e) {
  127. $coordinate = null;
  128. }
  129. if ($coordinate === null) {
  130. throw new Exception('Coordinate no longer exists');
  131. }
  132. return $coordinate;
  133. }
  134. /**
  135. * Get cell value.
  136. *
  137. * @return mixed
  138. */
  139. public function getValue()
  140. {
  141. return $this->value;
  142. }
  143. /**
  144. * Get cell value with formatting.
  145. *
  146. * @return string
  147. */
  148. public function getFormattedValue()
  149. {
  150. return (string) NumberFormat::toFormattedString(
  151. $this->getCalculatedValue(),
  152. $this->getStyle()
  153. ->getNumberFormat()->getFormatCode()
  154. );
  155. }
  156. /**
  157. * Set cell value.
  158. *
  159. * Sets the value for a cell, automatically determining the datatype using the value binder
  160. *
  161. * @param mixed $value Value
  162. *
  163. * @return $this
  164. */
  165. public function setValue($value)
  166. {
  167. if (!self::getValueBinder()->bindValue($this, $value)) {
  168. throw new Exception('Value could not be bound to cell.');
  169. }
  170. return $this;
  171. }
  172. /**
  173. * Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
  174. *
  175. * @param mixed $value Value
  176. * @param string $dataType Explicit data type, see DataType::TYPE_*
  177. *
  178. * @return Cell
  179. */
  180. public function setValueExplicit($value, $dataType)
  181. {
  182. // set the value according to data type
  183. switch ($dataType) {
  184. case DataType::TYPE_NULL:
  185. $this->value = $value;
  186. break;
  187. case DataType::TYPE_STRING2:
  188. $dataType = DataType::TYPE_STRING;
  189. // no break
  190. case DataType::TYPE_STRING:
  191. // Synonym for string
  192. case DataType::TYPE_INLINE:
  193. // Rich text
  194. $this->value = DataType::checkString($value);
  195. break;
  196. case DataType::TYPE_NUMERIC:
  197. if (is_string($value) && !is_numeric($value)) {
  198. throw new Exception('Invalid numeric value for datatype Numeric');
  199. }
  200. $this->value = 0 + $value;
  201. break;
  202. case DataType::TYPE_FORMULA:
  203. $this->value = (string) $value;
  204. break;
  205. case DataType::TYPE_BOOL:
  206. $this->value = (bool) $value;
  207. break;
  208. case DataType::TYPE_ERROR:
  209. $this->value = DataType::checkErrorCode($value);
  210. break;
  211. default:
  212. throw new Exception('Invalid datatype: ' . $dataType);
  213. break;
  214. }
  215. // set the datatype
  216. $this->dataType = $dataType;
  217. return $this->updateInCollection();
  218. }
  219. /**
  220. * Get calculated cell value.
  221. *
  222. * @param bool $resetLog Whether the calculation engine logger should be reset or not
  223. *
  224. * @return mixed
  225. */
  226. public function getCalculatedValue($resetLog = true)
  227. {
  228. if ($this->dataType == DataType::TYPE_FORMULA) {
  229. try {
  230. $index = $this->getWorksheet()->getParent()->getActiveSheetIndex();
  231. $selected = $this->getWorksheet()->getSelectedCells();
  232. $result = Calculation::getInstance(
  233. $this->getWorksheet()->getParent()
  234. )->calculateCellValue($this, $resetLog);
  235. $this->getWorksheet()->setSelectedCells($selected);
  236. $this->getWorksheet()->getParent()->setActiveSheetIndex($index);
  237. // We don't yet handle array returns
  238. if (is_array($result)) {
  239. while (is_array($result)) {
  240. $result = array_shift($result);
  241. }
  242. }
  243. } catch (Exception $ex) {
  244. if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
  245. return $this->calculatedValue; // Fallback for calculations referencing external files.
  246. } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) {
  247. return \PhpOffice\PhpSpreadsheet\Calculation\Functions::NAME();
  248. }
  249. throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception(
  250. $this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage()
  251. );
  252. }
  253. if ($result === '#Not Yet Implemented') {
  254. return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
  255. }
  256. return $result;
  257. } elseif ($this->value instanceof RichText) {
  258. return $this->value->getPlainText();
  259. }
  260. return $this->value;
  261. }
  262. /**
  263. * Set old calculated value (cached).
  264. *
  265. * @param mixed $originalValue Value
  266. *
  267. * @return Cell
  268. */
  269. public function setCalculatedValue($originalValue)
  270. {
  271. if ($originalValue !== null) {
  272. $this->calculatedValue = (is_numeric($originalValue)) ? (float) $originalValue : $originalValue;
  273. }
  274. return $this->updateInCollection();
  275. }
  276. /**
  277. * Get old calculated value (cached)
  278. * This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
  279. * create the original spreadsheet file.
  280. * Note that this value is not guaranteed to reflect the actual calculated value because it is
  281. * possible that auto-calculation was disabled in the original spreadsheet, and underlying data
  282. * values used by the formula have changed since it was last calculated.
  283. *
  284. * @return mixed
  285. */
  286. public function getOldCalculatedValue()
  287. {
  288. return $this->calculatedValue;
  289. }
  290. /**
  291. * Get cell data type.
  292. *
  293. * @return string
  294. */
  295. public function getDataType()
  296. {
  297. return $this->dataType;
  298. }
  299. /**
  300. * Set cell data type.
  301. *
  302. * @param string $dataType see DataType::TYPE_*
  303. *
  304. * @return Cell
  305. */
  306. public function setDataType($dataType)
  307. {
  308. if ($dataType == DataType::TYPE_STRING2) {
  309. $dataType = DataType::TYPE_STRING;
  310. }
  311. $this->dataType = $dataType;
  312. return $this->updateInCollection();
  313. }
  314. /**
  315. * Identify if the cell contains a formula.
  316. *
  317. * @return bool
  318. */
  319. public function isFormula()
  320. {
  321. return $this->dataType == DataType::TYPE_FORMULA;
  322. }
  323. /**
  324. * Does this cell contain Data validation rules?
  325. *
  326. * @return bool
  327. */
  328. public function hasDataValidation()
  329. {
  330. if (!isset($this->parent)) {
  331. throw new Exception('Cannot check for data validation when cell is not bound to a worksheet');
  332. }
  333. return $this->getWorksheet()->dataValidationExists($this->getCoordinate());
  334. }
  335. /**
  336. * Get Data validation rules.
  337. *
  338. * @return DataValidation
  339. */
  340. public function getDataValidation()
  341. {
  342. if (!isset($this->parent)) {
  343. throw new Exception('Cannot get data validation for cell that is not bound to a worksheet');
  344. }
  345. return $this->getWorksheet()->getDataValidation($this->getCoordinate());
  346. }
  347. /**
  348. * Set Data validation rules.
  349. */
  350. public function setDataValidation(?DataValidation $dataValidation = null): self
  351. {
  352. if (!isset($this->parent)) {
  353. throw new Exception('Cannot set data validation for cell that is not bound to a worksheet');
  354. }
  355. $this->getWorksheet()->setDataValidation($this->getCoordinate(), $dataValidation);
  356. return $this->updateInCollection();
  357. }
  358. /**
  359. * Does this cell contain valid value?
  360. *
  361. * @return bool
  362. */
  363. public function hasValidValue()
  364. {
  365. $validator = new DataValidator();
  366. return $validator->isValid($this);
  367. }
  368. /**
  369. * Does this cell contain a Hyperlink?
  370. *
  371. * @return bool
  372. */
  373. public function hasHyperlink()
  374. {
  375. if (!isset($this->parent)) {
  376. throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet');
  377. }
  378. return $this->getWorksheet()->hyperlinkExists($this->getCoordinate());
  379. }
  380. /**
  381. * Get Hyperlink.
  382. *
  383. * @return Hyperlink
  384. */
  385. public function getHyperlink()
  386. {
  387. if (!isset($this->parent)) {
  388. throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet');
  389. }
  390. return $this->getWorksheet()->getHyperlink($this->getCoordinate());
  391. }
  392. /**
  393. * Set Hyperlink.
  394. *
  395. * @return Cell
  396. */
  397. public function setHyperlink(?Hyperlink $hyperlink = null)
  398. {
  399. if (!isset($this->parent)) {
  400. throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet');
  401. }
  402. $this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink);
  403. return $this->updateInCollection();
  404. }
  405. /**
  406. * Get cell collection.
  407. *
  408. * @return Cells
  409. */
  410. public function getParent()
  411. {
  412. return $this->parent;
  413. }
  414. /**
  415. * Get parent worksheet.
  416. *
  417. * @return Worksheet
  418. */
  419. public function getWorksheet()
  420. {
  421. try {
  422. $worksheet = $this->parent->getParent();
  423. } catch (Throwable $e) {
  424. $worksheet = null;
  425. }
  426. if ($worksheet === null) {
  427. throw new Exception('Worksheet no longer exists');
  428. }
  429. return $worksheet;
  430. }
  431. /**
  432. * Is this cell in a merge range.
  433. *
  434. * @return bool
  435. */
  436. public function isInMergeRange()
  437. {
  438. return (bool) $this->getMergeRange();
  439. }
  440. /**
  441. * Is this cell the master (top left cell) in a merge range (that holds the actual data value).
  442. *
  443. * @return bool
  444. */
  445. public function isMergeRangeValueCell()
  446. {
  447. if ($mergeRange = $this->getMergeRange()) {
  448. $mergeRange = Coordinate::splitRange($mergeRange);
  449. [$startCell] = $mergeRange[0];
  450. if ($this->getCoordinate() === $startCell) {
  451. return true;
  452. }
  453. }
  454. return false;
  455. }
  456. /**
  457. * If this cell is in a merge range, then return the range.
  458. *
  459. * @return false|string
  460. */
  461. public function getMergeRange()
  462. {
  463. foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) {
  464. if ($this->isInRange($mergeRange)) {
  465. return $mergeRange;
  466. }
  467. }
  468. return false;
  469. }
  470. /**
  471. * Get cell style.
  472. *
  473. * @return Style
  474. */
  475. public function getStyle()
  476. {
  477. return $this->getWorksheet()->getStyle($this->getCoordinate());
  478. }
  479. /**
  480. * Re-bind parent.
  481. *
  482. * @return Cell
  483. */
  484. public function rebindParent(Worksheet $parent)
  485. {
  486. $this->parent = $parent->getCellCollection();
  487. return $this->updateInCollection();
  488. }
  489. /**
  490. * Is cell in a specific range?
  491. *
  492. * @param string $range Cell range (e.g. A1:A1)
  493. *
  494. * @return bool
  495. */
  496. public function isInRange($range)
  497. {
  498. [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
  499. // Translate properties
  500. $myColumn = Coordinate::columnIndexFromString($this->getColumn());
  501. $myRow = $this->getRow();
  502. // Verify if cell is in range
  503. return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn) &&
  504. ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow);
  505. }
  506. /**
  507. * Compare 2 cells.
  508. *
  509. * @param Cell $a Cell a
  510. * @param Cell $b Cell b
  511. *
  512. * @return int Result of comparison (always -1 or 1, never zero!)
  513. */
  514. public static function compareCells(self $a, self $b)
  515. {
  516. if ($a->getRow() < $b->getRow()) {
  517. return -1;
  518. } elseif ($a->getRow() > $b->getRow()) {
  519. return 1;
  520. } elseif (Coordinate::columnIndexFromString($a->getColumn()) < Coordinate::columnIndexFromString($b->getColumn())) {
  521. return -1;
  522. }
  523. return 1;
  524. }
  525. /**
  526. * Get value binder to use.
  527. *
  528. * @return IValueBinder
  529. */
  530. public static function getValueBinder()
  531. {
  532. if (self::$valueBinder === null) {
  533. self::$valueBinder = new DefaultValueBinder();
  534. }
  535. return self::$valueBinder;
  536. }
  537. /**
  538. * Set value binder to use.
  539. */
  540. public static function setValueBinder(IValueBinder $binder): void
  541. {
  542. self::$valueBinder = $binder;
  543. }
  544. /**
  545. * Implement PHP __clone to create a deep clone, not just a shallow copy.
  546. */
  547. public function __clone()
  548. {
  549. $vars = get_object_vars($this);
  550. foreach ($vars as $key => $value) {
  551. if ((is_object($value)) && ($key != 'parent')) {
  552. $this->$key = clone $value;
  553. } else {
  554. $this->$key = $value;
  555. }
  556. }
  557. }
  558. /**
  559. * Get index to cellXf.
  560. *
  561. * @return int
  562. */
  563. public function getXfIndex()
  564. {
  565. return $this->xfIndex;
  566. }
  567. /**
  568. * Set index to cellXf.
  569. *
  570. * @param int $indexValue
  571. *
  572. * @return Cell
  573. */
  574. public function setXfIndex($indexValue)
  575. {
  576. $this->xfIndex = $indexValue;
  577. return $this->updateInCollection();
  578. }
  579. /**
  580. * Set the formula attributes.
  581. *
  582. * @param mixed $attributes
  583. *
  584. * @return $this
  585. */
  586. public function setFormulaAttributes($attributes)
  587. {
  588. $this->formulaAttributes = $attributes;
  589. return $this;
  590. }
  591. /**
  592. * Get the formula attributes.
  593. */
  594. public function getFormulaAttributes()
  595. {
  596. return $this->formulaAttributes;
  597. }
  598. /**
  599. * Convert to string.
  600. *
  601. * @return string
  602. */
  603. public function __toString()
  604. {
  605. return (string) $this->getValue();
  606. }
  607. }