Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
1 <?php 2 3 namespace PhpOffice\PhpSpreadsheet\Style; 4 5 use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 6 use PhpOffice\PhpSpreadsheet\Spreadsheet; 7 8 class Style extends Supervisor 9 { 10 /** 11 * Font. 12 * 13 * @var Font 14 */ 15 protected $font; 16 17 /** 18 * Fill. 19 * 20 * @var Fill 21 */ 22 protected $fill; 23 24 /** 25 * Borders. 26 * 27 * @var Borders 28 */ 29 protected $borders; 30 31 /** 32 * Alignment. 33 * 34 * @var Alignment 35 */ 36 protected $alignment; 37 38 /** 39 * Number Format. 40 * 41 * @var NumberFormat 42 */ 43 protected $numberFormat; 44 45 /** 46 * Protection. 47 * 48 * @var Protection 49 */ 50 protected $protection; 51 52 /** 53 * Index of style in collection. Only used for real style. 54 * 55 * @var int 56 */ 57 protected $index; 58 59 /** 60 * Use Quote Prefix when displaying in cell editor. Only used for real style. 61 * 62 * @var bool 63 */ 64 protected $quotePrefix = false; 65 66 /** 67 * Internal cache for styles 68 * Used when applying style on range of cells (column or row) and cleared when 69 * all cells in range is styled. 70 * 71 * PhpSpreadsheet will always minimize the amount of styles used. So cells with 72 * same styles will reference the same Style instance. To check if two styles 73 * are similar Style::getHashCode() is used. This call is expensive. To minimize 74 * the need to call this method we can cache the internal PHP object id of the 75 * Style in the range. Style::getHashCode() will then only be called when we 76 * encounter a unique style. 77 * 78 * @see Style::applyFromArray() 79 * @see Style::getHashCode() 80 * 81 * @phpstan-var null|array{styleByHash: array<string, Style>, hashByObjId: array<int, string>} 82 * 83 * @var array<string, array> 84 */ 85 private static $cachedStyles; 86 87 /** 88 * Create a new Style. 89 * 90 * @param bool $isSupervisor Flag indicating if this is a supervisor or not 91 * Leave this value at default unless you understand exactly what 92 * its ramifications are 93 * @param bool $isConditional Flag indicating if this is a conditional style or not 94 * Leave this value at default unless you understand exactly what 95 * its ramifications are 96 */ 97 public function __construct($isSupervisor = false, $isConditional = false) 98 { 99 parent::__construct($isSupervisor); 100 101 // Initialise values 102 $this->font = new Font($isSupervisor, $isConditional); 103 $this->fill = new Fill($isSupervisor, $isConditional); 104 $this->borders = new Borders($isSupervisor); 105 $this->alignment = new Alignment($isSupervisor, $isConditional); 106 $this->numberFormat = new NumberFormat($isSupervisor, $isConditional); 107 $this->protection = new Protection($isSupervisor, $isConditional); 108 109 // bind parent if we are a supervisor 110 if ($isSupervisor) { 111 $this->font->bindParent($this); 112 $this->fill->bindParent($this); 113 $this->borders->bindParent($this); 114 $this->alignment->bindParent($this); 115 $this->numberFormat->bindParent($this); 116 $this->protection->bindParent($this); 117 } 118 } 119 120 /** 121 * Get the shared style component for the currently active cell in currently active sheet. 122 * Only used for style supervisor. 123 */ 124 public function getSharedComponent(): self 125 { 126 $activeSheet = $this->getActiveSheet(); 127 $selectedCell = $this->getActiveCell(); // e.g. 'A1' 128 129 if ($activeSheet->cellExists($selectedCell)) { 130 $xfIndex = $activeSheet->getCell($selectedCell)->getXfIndex(); 131 } else { 132 $xfIndex = 0; 133 } 134 135 return $activeSheet->getParent()->getCellXfByIndex($xfIndex); 136 } 137 138 /** 139 * Get parent. Only used for style supervisor. 140 */ 141 public function getParent(): Spreadsheet 142 { 143 return $this->getActiveSheet()->getParent(); 144 } 145 146 /** 147 * Build style array from subcomponents. 148 * 149 * @param array $array 150 * 151 * @return array 152 */ 153 public function getStyleArray($array) 154 { 155 return ['quotePrefix' => $array]; 156 } 157 158 /** 159 * Apply styles from array. 160 * 161 * <code> 162 * $spreadsheet->getActiveSheet()->getStyle('B2')->applyFromArray( 163 * [ 164 * 'font' => [ 165 * 'name' => 'Arial', 166 * 'bold' => true, 167 * 'italic' => false, 168 * 'underline' => Font::UNDERLINE_DOUBLE, 169 * 'strikethrough' => false, 170 * 'color' => [ 171 * 'rgb' => '808080' 172 * ] 173 * ], 174 * 'borders' => [ 175 * 'bottom' => [ 176 * 'borderStyle' => Border::BORDER_DASHDOT, 177 * 'color' => [ 178 * 'rgb' => '808080' 179 * ] 180 * ], 181 * 'top' => [ 182 * 'borderStyle' => Border::BORDER_DASHDOT, 183 * 'color' => [ 184 * 'rgb' => '808080' 185 * ] 186 * ] 187 * ], 188 * 'alignment' => [ 189 * 'horizontal' => Alignment::HORIZONTAL_CENTER, 190 * 'vertical' => Alignment::VERTICAL_CENTER, 191 * 'wrapText' => true, 192 * ], 193 * 'quotePrefix' => true 194 * ] 195 * ); 196 * </code> 197 * 198 * @param array $styleArray Array containing style information 199 * @param bool $advancedBorders advanced mode for setting borders 200 * 201 * @return $this 202 */ 203 public function applyFromArray(array $styleArray, $advancedBorders = true) 204 { 205 if ($this->isSupervisor) { 206 $pRange = $this->getSelectedCells(); 207 208 // Uppercase coordinate 209 $pRange = strtoupper($pRange); 210 211 // Is it a cell range or a single cell? 212 if (strpos($pRange, ':') === false) { 213 $rangeA = $pRange; 214 $rangeB = $pRange; 215 } else { 216 [$rangeA, $rangeB] = explode(':', $pRange); 217 } 218 219 // Calculate range outer borders 220 $rangeStart = Coordinate::coordinateFromString($rangeA); 221 $rangeEnd = Coordinate::coordinateFromString($rangeB); 222 $rangeStartIndexes = Coordinate::indexesFromString($rangeA); 223 $rangeEndIndexes = Coordinate::indexesFromString($rangeB); 224 225 $columnStart = $rangeStart[0]; 226 $columnEnd = $rangeEnd[0]; 227 228 // Make sure we can loop upwards on rows and columns 229 if ($rangeStartIndexes[0] > $rangeEndIndexes[0] && $rangeStartIndexes[1] > $rangeEndIndexes[1]) { 230 $tmp = $rangeStartIndexes; 231 $rangeStartIndexes = $rangeEndIndexes; 232 $rangeEndIndexes = $tmp; 233 } 234 235 // ADVANCED MODE: 236 if ($advancedBorders && isset($styleArray['borders'])) { 237 // 'allBorders' is a shorthand property for 'outline' and 'inside' and 238 // it applies to components that have not been set explicitly 239 if (isset($styleArray['borders']['allBorders'])) { 240 foreach (['outline', 'inside'] as $component) { 241 if (!isset($styleArray['borders'][$component])) { 242 $styleArray['borders'][$component] = $styleArray['borders']['allBorders']; 243 } 244 } 245 unset($styleArray['borders']['allBorders']); // not needed any more 246 } 247 // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left' 248 // it applies to components that have not been set explicitly 249 if (isset($styleArray['borders']['outline'])) { 250 foreach (['top', 'right', 'bottom', 'left'] as $component) { 251 if (!isset($styleArray['borders'][$component])) { 252 $styleArray['borders'][$component] = $styleArray['borders']['outline']; 253 } 254 } 255 unset($styleArray['borders']['outline']); // not needed any more 256 } 257 // 'inside' is a shorthand property for 'vertical' and 'horizontal' 258 // it applies to components that have not been set explicitly 259 if (isset($styleArray['borders']['inside'])) { 260 foreach (['vertical', 'horizontal'] as $component) { 261 if (!isset($styleArray['borders'][$component])) { 262 $styleArray['borders'][$component] = $styleArray['borders']['inside']; 263 } 264 } 265 unset($styleArray['borders']['inside']); // not needed any more 266 } 267 // width and height characteristics of selection, 1, 2, or 3 (for 3 or more) 268 $xMax = min($rangeEndIndexes[0] - $rangeStartIndexes[0] + 1, 3); 269 $yMax = min($rangeEndIndexes[1] - $rangeStartIndexes[1] + 1, 3); 270 271 // loop through up to 3 x 3 = 9 regions 272 for ($x = 1; $x <= $xMax; ++$x) { 273 // start column index for region 274 $colStart = ($x == 3) ? 275 Coordinate::stringFromColumnIndex($rangeEndIndexes[0]) 276 : Coordinate::stringFromColumnIndex($rangeStartIndexes[0] + $x - 1); 277 // end column index for region 278 $colEnd = ($x == 1) ? 279 Coordinate::stringFromColumnIndex($rangeStartIndexes[0]) 280 : Coordinate::stringFromColumnIndex($rangeEndIndexes[0] - $xMax + $x); 281 282 for ($y = 1; $y <= $yMax; ++$y) { 283 // which edges are touching the region 284 $edges = []; 285 if ($x == 1) { 286 // are we at left edge 287 $edges[] = 'left'; 288 } 289 if ($x == $xMax) { 290 // are we at right edge 291 $edges[] = 'right'; 292 } 293 if ($y == 1) { 294 // are we at top edge? 295 $edges[] = 'top'; 296 } 297 if ($y == $yMax) { 298 // are we at bottom edge? 299 $edges[] = 'bottom'; 300 } 301 302 // start row index for region 303 $rowStart = ($y == 3) ? 304 $rangeEndIndexes[1] : $rangeStartIndexes[1] + $y - 1; 305 306 // end row index for region 307 $rowEnd = ($y == 1) ? 308 $rangeStartIndexes[1] : $rangeEndIndexes[1] - $yMax + $y; 309 310 // build range for region 311 $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd; 312 313 // retrieve relevant style array for region 314 $regionStyles = $styleArray; 315 unset($regionStyles['borders']['inside']); 316 317 // what are the inner edges of the region when looking at the selection 318 $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges); 319 320 // inner edges that are not touching the region should take the 'inside' border properties if they have been set 321 foreach ($innerEdges as $innerEdge) { 322 switch ($innerEdge) { 323 case 'top': 324 case 'bottom': 325 // should pick up 'horizontal' border property if set 326 if (isset($styleArray['borders']['horizontal'])) { 327 $regionStyles['borders'][$innerEdge] = $styleArray['borders']['horizontal']; 328 } else { 329 unset($regionStyles['borders'][$innerEdge]); 330 } 331 332 break; 333 case 'left': 334 case 'right': 335 // should pick up 'vertical' border property if set 336 if (isset($styleArray['borders']['vertical'])) { 337 $regionStyles['borders'][$innerEdge] = $styleArray['borders']['vertical']; 338 } else { 339 unset($regionStyles['borders'][$innerEdge]); 340 } 341 342 break; 343 } 344 } 345 346 // apply region style to region by calling applyFromArray() in simple mode 347 $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false); 348 } 349 } 350 351 // restore initial cell selection range 352 $this->getActiveSheet()->getStyle($pRange); 353 354 return $this; 355 } 356 357 // SIMPLE MODE: 358 // Selection type, inspect 359 if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) { 360 $selectionType = 'COLUMN'; 361 362 // Enable caching of styles 363 self::$cachedStyles = ['hashByObjId' => [], 'styleByHash' => []]; 364 } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) { 365 $selectionType = 'ROW'; 366 367 // Enable caching of styles 368 self::$cachedStyles = ['hashByObjId' => [], 'styleByHash' => []]; 369 } else { 370 $selectionType = 'CELL'; 371 } 372 373 // First loop through columns, rows, or cells to find out which styles are affected by this operation 374 $oldXfIndexes = $this->getOldXfIndexes($selectionType, $rangeStartIndexes, $rangeEndIndexes, $columnStart, $columnEnd, $styleArray); 375 376 // clone each of the affected styles, apply the style array, and add the new styles to the workbook 377 $workbook = $this->getActiveSheet()->getParent(); 378 $newXfIndexes = []; 379 foreach ($oldXfIndexes as $oldXfIndex => $dummy) { 380 $style = $workbook->getCellXfByIndex($oldXfIndex); 381 382 // $cachedStyles is set when applying style for a range of cells, either column or row 383 if (self::$cachedStyles === null) { 384 // Clone the old style and apply style-array 385 $newStyle = clone $style; 386 $newStyle->applyFromArray($styleArray); 387 388 // Look for existing style we can use instead (reduce memory usage) 389 $existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode()); 390 } else { 391 // Style cache is stored by Style::getHashCode(). But calling this method is 392 // expensive. So we cache the php obj id -> hash. 393 $objId = spl_object_id($style); 394 395 // Look for the original HashCode 396 $styleHash = self::$cachedStyles['hashByObjId'][$objId] ?? null; 397 if ($styleHash === null) { 398 // This object_id is not cached, store the hashcode in case encounter again 399 $styleHash = self::$cachedStyles['hashByObjId'][$objId] = $style->getHashCode(); 400 } 401 402 // Find existing style by hash. 403 $existingStyle = self::$cachedStyles['styleByHash'][$styleHash] ?? null; 404 405 if (!$existingStyle) { 406 // The old style combined with the new style array is not cached, so we create it now 407 $newStyle = clone $style; 408 $newStyle->applyFromArray($styleArray); 409 410 // Look for similar style in workbook to reduce memory usage 411 $existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode()); 412 413 // Cache the new style by original hashcode 414 self::$cachedStyles['styleByHash'][$styleHash] = $existingStyle instanceof self ? $existingStyle : $newStyle; 415 } 416 } 417 418 if ($existingStyle) { 419 // there is already such cell Xf in our collection 420 $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex(); 421 } else { 422 if (!isset($newStyle)) { 423 // Handle bug in PHPStan, see https://github.com/phpstan/phpstan/issues/5805 424 // $newStyle should always be defined. 425 // This block might not be needed in the future 426 $newStyle = clone $style; 427 $newStyle->applyFromArray($styleArray); 428 } 429 430 // we don't have such a cell Xf, need to add 431 $workbook->addCellXf($newStyle); 432 $newXfIndexes[$oldXfIndex] = $newStyle->getIndex(); 433 } 434 } 435 436 // Loop through columns, rows, or cells again and update the XF index 437 switch ($selectionType) { 438 case 'COLUMN': 439 for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) { 440 $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col); 441 $oldXfIndex = $columnDimension->getXfIndex(); 442 $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]); 443 } 444 445 // Disable caching of styles 446 self::$cachedStyles = null; 447 448 break; 449 case 'ROW': 450 for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) { 451 $rowDimension = $this->getActiveSheet()->getRowDimension($row); 452 // row without explicit style should be formatted based on default style 453 $oldXfIndex = $rowDimension->getXfIndex() ?? 0; 454 $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]); 455 } 456 457 // Disable caching of styles 458 self::$cachedStyles = null; 459 460 break; 461 case 'CELL': 462 for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) { 463 for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) { 464 $cell = $this->getActiveSheet()->getCellByColumnAndRow($col, $row); 465 $oldXfIndex = $cell->getXfIndex(); 466 $cell->setXfIndex($newXfIndexes[$oldXfIndex]); 467 } 468 } 469 470 break; 471 } 472 } else { 473 // not a supervisor, just apply the style array directly on style object 474 if (isset($styleArray['fill'])) { 475 $this->getFill()->applyFromArray($styleArray['fill']); 476 } 477 if (isset($styleArray['font'])) { 478 $this->getFont()->applyFromArray($styleArray['font']); 479 } 480 if (isset($styleArray['borders'])) { 481 $this->getBorders()->applyFromArray($styleArray['borders']); 482 } 483 if (isset($styleArray['alignment'])) { 484 $this->getAlignment()->applyFromArray($styleArray['alignment']); 485 } 486 if (isset($styleArray['numberFormat'])) { 487 $this->getNumberFormat()->applyFromArray($styleArray['numberFormat']); 488 } 489 if (isset($styleArray['protection'])) { 490 $this->getProtection()->applyFromArray($styleArray['protection']); 491 } 492 if (isset($styleArray['quotePrefix'])) { 493 $this->quotePrefix = $styleArray['quotePrefix']; 494 } 495 } 496 497 return $this; 498 } 499 500 private function getOldXfIndexes(string $selectionType, array $rangeStart, array $rangeEnd, string $columnStart, string $columnEnd, array $styleArray): array 501 { 502 $oldXfIndexes = []; 503 switch ($selectionType) { 504 case 'COLUMN': 505 for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { 506 $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true; 507 } 508 foreach ($this->getActiveSheet()->getColumnIterator($columnStart, $columnEnd) as $columnIterator) { 509 $cellIterator = $columnIterator->getCellIterator(); 510 $cellIterator->setIterateOnlyExistingCells(true); 511 foreach ($cellIterator as $columnCell) { 512 if ($columnCell !== null) { 513 $columnCell->getStyle()->applyFromArray($styleArray); 514 } 515 } 516 } 517 518 break; 519 case 'ROW': 520 for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { 521 if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() === null) { 522 $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style 523 } else { 524 $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true; 525 } 526 } 527 foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) { 528 $cellIterator = $rowIterator->getCellIterator(); 529 $cellIterator->setIterateOnlyExistingCells(true); 530 foreach ($cellIterator as $rowCell) { 531 if ($rowCell !== null) { 532 $rowCell->getStyle()->applyFromArray($styleArray); 533 } 534 } 535 } 536 537 break; 538 case 'CELL': 539 for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { 540 for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { 541 $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true; 542 } 543 } 544 545 break; 546 } 547 548 return $oldXfIndexes; 549 } 550 551 /** 552 * Get Fill. 553 * 554 * @return Fill 555 */ 556 public function getFill() 557 { 558 return $this->fill; 559 } 560 561 /** 562 * Get Font. 563 * 564 * @return Font 565 */ 566 public function getFont() 567 { 568 return $this->font; 569 } 570 571 /** 572 * Set font. 573 * 574 * @return $this 575 */ 576 public function setFont(Font $font) 577 { 578 $this->font = $font; 579 580 return $this; 581 } 582 583 /** 584 * Get Borders. 585 * 586 * @return Borders 587 */ 588 public function getBorders() 589 { 590 return $this->borders; 591 } 592 593 /** 594 * Get Alignment. 595 * 596 * @return Alignment 597 */ 598 public function getAlignment() 599 { 600 return $this->alignment; 601 } 602 603 /** 604 * Get Number Format. 605 * 606 * @return NumberFormat 607 */ 608 public function getNumberFormat() 609 { 610 return $this->numberFormat; 611 } 612 613 /** 614 * Get Conditional Styles. Only used on supervisor. 615 * 616 * @return Conditional[] 617 */ 618 public function getConditionalStyles() 619 { 620 return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell()); 621 } 622 623 /** 624 * Set Conditional Styles. Only used on supervisor. 625 * 626 * @param Conditional[] $conditionalStyleArray Array of conditional styles 627 * 628 * @return $this 629 */ 630 public function setConditionalStyles(array $conditionalStyleArray) 631 { 632 $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $conditionalStyleArray); 633 634 return $this; 635 } 636 637 /** 638 * Get Protection. 639 * 640 * @return Protection 641 */ 642 public function getProtection() 643 { 644 return $this->protection; 645 } 646 647 /** 648 * Get quote prefix. 649 * 650 * @return bool 651 */ 652 public function getQuotePrefix() 653 { 654 if ($this->isSupervisor) { 655 return $this->getSharedComponent()->getQuotePrefix(); 656 } 657 658 return $this->quotePrefix; 659 } 660 661 /** 662 * Set quote prefix. 663 * 664 * @param bool $quotePrefix 665 * 666 * @return $this 667 */ 668 public function setQuotePrefix($quotePrefix) 669 { 670 if ($quotePrefix == '') { 671 $quotePrefix = false; 672 } 673 if ($this->isSupervisor) { 674 $styleArray = ['quotePrefix' => $quotePrefix]; 675 $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); 676 } else { 677 $this->quotePrefix = (bool) $quotePrefix; 678 } 679 680 return $this; 681 } 682 683 /** 684 * Get hash code. 685 * 686 * @return string Hash code 687 */ 688 public function getHashCode() 689 { 690 return md5( 691 $this->fill->getHashCode() . 692 $this->font->getHashCode() . 693 $this->borders->getHashCode() . 694 $this->alignment->getHashCode() . 695 $this->numberFormat->getHashCode() . 696 $this->protection->getHashCode() . 697 ($this->quotePrefix ? 't' : 'f') . 698 __CLASS__ 699 ); 700 } 701 702 /** 703 * Get own index in style collection. 704 * 705 * @return int 706 */ 707 public function getIndex() 708 { 709 return $this->index; 710 } 711 712 /** 713 * Set own index in style collection. 714 * 715 * @param int $index 716 */ 717 public function setIndex($index): void 718 { 719 $this->index = $index; 720 } 721 722 protected function exportArray1(): array 723 { 724 $exportedArray = []; 725 $this->exportArray2($exportedArray, 'alignment', $this->getAlignment()); 726 $this->exportArray2($exportedArray, 'borders', $this->getBorders()); 727 $this->exportArray2($exportedArray, 'fill', $this->getFill()); 728 $this->exportArray2($exportedArray, 'font', $this->getFont()); 729 $this->exportArray2($exportedArray, 'numberFormat', $this->getNumberFormat()); 730 $this->exportArray2($exportedArray, 'protection', $this->getProtection()); 731 $this->exportArray2($exportedArray, 'quotePrefx', $this->getQuotePrefix()); 732 733 return $exportedArray; 734 } 735 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body