Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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       * Create a new Style.
  68       *
  69       * @param bool $isSupervisor Flag indicating if this is a supervisor or not
  70       *         Leave this value at default unless you understand exactly what
  71       *    its ramifications are
  72       * @param bool $isConditional Flag indicating if this is a conditional style or not
  73       *       Leave this value at default unless you understand exactly what
  74       *    its ramifications are
  75       */
  76      public function __construct($isSupervisor = false, $isConditional = false)
  77      {
  78          parent::__construct($isSupervisor);
  79  
  80          // Initialise values
  81          $this->font = new Font($isSupervisor, $isConditional);
  82          $this->fill = new Fill($isSupervisor, $isConditional);
  83          $this->borders = new Borders($isSupervisor, $isConditional);
  84          $this->alignment = new Alignment($isSupervisor, $isConditional);
  85          $this->numberFormat = new NumberFormat($isSupervisor, $isConditional);
  86          $this->protection = new Protection($isSupervisor, $isConditional);
  87  
  88          // bind parent if we are a supervisor
  89          if ($isSupervisor) {
  90              $this->font->bindParent($this);
  91              $this->fill->bindParent($this);
  92              $this->borders->bindParent($this);
  93              $this->alignment->bindParent($this);
  94              $this->numberFormat->bindParent($this);
  95              $this->protection->bindParent($this);
  96          }
  97      }
  98  
  99      /**
 100       * Get the shared style component for the currently active cell in currently active sheet.
 101       * Only used for style supervisor.
 102       *
 103       * @return Style
 104       */
 105      public function getSharedComponent()
 106      {
 107          $activeSheet = $this->getActiveSheet();
 108          $selectedCell = $this->getActiveCell(); // e.g. 'A1'
 109  
 110          if ($activeSheet->cellExists($selectedCell)) {
 111              $xfIndex = $activeSheet->getCell($selectedCell)->getXfIndex();
 112          } else {
 113              $xfIndex = 0;
 114          }
 115  
 116          return $this->parent->getCellXfByIndex($xfIndex);
 117      }
 118  
 119      /**
 120       * Get parent. Only used for style supervisor.
 121       *
 122       * @return Spreadsheet
 123       */
 124      public function getParent()
 125      {
 126          return $this->parent;
 127      }
 128  
 129      /**
 130       * Build style array from subcomponents.
 131       *
 132       * @param array $array
 133       *
 134       * @return array
 135       */
 136      public function getStyleArray($array)
 137      {
 138          return ['quotePrefix' => $array];
 139      }
 140  
 141      /**
 142       * Apply styles from array.
 143       *
 144       * <code>
 145       * $spreadsheet->getActiveSheet()->getStyle('B2')->applyFromArray(
 146       *     [
 147       *         'font' => [
 148       *             'name' => 'Arial',
 149       *             'bold' => true,
 150       *             'italic' => false,
 151       *             'underline' => Font::UNDERLINE_DOUBLE,
 152       *             'strikethrough' => false,
 153       *             'color' => [
 154       *                 'rgb' => '808080'
 155       *             ]
 156       *         ],
 157       *         'borders' => [
 158       *             'bottom' => [
 159       *                 'borderStyle' => Border::BORDER_DASHDOT,
 160       *                 'color' => [
 161       *                     'rgb' => '808080'
 162       *                 ]
 163       *             ],
 164       *             'top' => [
 165       *                 'borderStyle' => Border::BORDER_DASHDOT,
 166       *                 'color' => [
 167       *                     'rgb' => '808080'
 168       *                 ]
 169       *             ]
 170       *         ],
 171       *         'alignment' => [
 172       *             'horizontal' => Alignment::HORIZONTAL_CENTER,
 173       *             'vertical' => Alignment::VERTICAL_CENTER,
 174       *             'wrapText' => true,
 175       *         ],
 176       *         'quotePrefix'    => true
 177       *     ]
 178       * );
 179       * </code>
 180       *
 181       * @param array $pStyles Array containing style information
 182       * @param bool $pAdvanced advanced mode for setting borders
 183       *
 184       * @return $this
 185       */
 186      public function applyFromArray(array $pStyles, $pAdvanced = true)
 187      {
 188          if ($this->isSupervisor) {
 189              $pRange = $this->getSelectedCells();
 190  
 191              // Uppercase coordinate
 192              $pRange = strtoupper($pRange);
 193  
 194              // Is it a cell range or a single cell?
 195              if (strpos($pRange, ':') === false) {
 196                  $rangeA = $pRange;
 197                  $rangeB = $pRange;
 198              } else {
 199                  [$rangeA, $rangeB] = explode(':', $pRange);
 200              }
 201  
 202              // Calculate range outer borders
 203              $rangeStart = Coordinate::coordinateFromString($rangeA);
 204              $rangeEnd = Coordinate::coordinateFromString($rangeB);
 205  
 206              // Translate column into index
 207              $rangeStart0 = $rangeStart[0];
 208              $rangeEnd0 = $rangeEnd[0];
 209              $rangeStart[0] = Coordinate::columnIndexFromString($rangeStart[0]);
 210              $rangeEnd[0] = Coordinate::columnIndexFromString($rangeEnd[0]);
 211  
 212              // Make sure we can loop upwards on rows and columns
 213              if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
 214                  $tmp = $rangeStart;
 215                  $rangeStart = $rangeEnd;
 216                  $rangeEnd = $tmp;
 217              }
 218  
 219              // ADVANCED MODE:
 220              if ($pAdvanced && isset($pStyles['borders'])) {
 221                  // 'allBorders' is a shorthand property for 'outline' and 'inside' and
 222                  //        it applies to components that have not been set explicitly
 223                  if (isset($pStyles['borders']['allBorders'])) {
 224                      foreach (['outline', 'inside'] as $component) {
 225                          if (!isset($pStyles['borders'][$component])) {
 226                              $pStyles['borders'][$component] = $pStyles['borders']['allBorders'];
 227                          }
 228                      }
 229                      unset($pStyles['borders']['allBorders']); // not needed any more
 230                  }
 231                  // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left'
 232                  //        it applies to components that have not been set explicitly
 233                  if (isset($pStyles['borders']['outline'])) {
 234                      foreach (['top', 'right', 'bottom', 'left'] as $component) {
 235                          if (!isset($pStyles['borders'][$component])) {
 236                              $pStyles['borders'][$component] = $pStyles['borders']['outline'];
 237                          }
 238                      }
 239                      unset($pStyles['borders']['outline']); // not needed any more
 240                  }
 241                  // 'inside' is a shorthand property for 'vertical' and 'horizontal'
 242                  //        it applies to components that have not been set explicitly
 243                  if (isset($pStyles['borders']['inside'])) {
 244                      foreach (['vertical', 'horizontal'] as $component) {
 245                          if (!isset($pStyles['borders'][$component])) {
 246                              $pStyles['borders'][$component] = $pStyles['borders']['inside'];
 247                          }
 248                      }
 249                      unset($pStyles['borders']['inside']); // not needed any more
 250                  }
 251                  // width and height characteristics of selection, 1, 2, or 3 (for 3 or more)
 252                  $xMax = min($rangeEnd[0] - $rangeStart[0] + 1, 3);
 253                  $yMax = min($rangeEnd[1] - $rangeStart[1] + 1, 3);
 254  
 255                  // loop through up to 3 x 3 = 9 regions
 256                  for ($x = 1; $x <= $xMax; ++$x) {
 257                      // start column index for region
 258                      $colStart = ($x == 3) ?
 259                          Coordinate::stringFromColumnIndex($rangeEnd[0])
 260                              : Coordinate::stringFromColumnIndex($rangeStart[0] + $x - 1);
 261                      // end column index for region
 262                      $colEnd = ($x == 1) ?
 263                          Coordinate::stringFromColumnIndex($rangeStart[0])
 264                              : Coordinate::stringFromColumnIndex($rangeEnd[0] - $xMax + $x);
 265  
 266                      for ($y = 1; $y <= $yMax; ++$y) {
 267                          // which edges are touching the region
 268                          $edges = [];
 269                          if ($x == 1) {
 270                              // are we at left edge
 271                              $edges[] = 'left';
 272                          }
 273                          if ($x == $xMax) {
 274                              // are we at right edge
 275                              $edges[] = 'right';
 276                          }
 277                          if ($y == 1) {
 278                              // are we at top edge?
 279                              $edges[] = 'top';
 280                          }
 281                          if ($y == $yMax) {
 282                              // are we at bottom edge?
 283                              $edges[] = 'bottom';
 284                          }
 285  
 286                          // start row index for region
 287                          $rowStart = ($y == 3) ?
 288                              $rangeEnd[1] : $rangeStart[1] + $y - 1;
 289  
 290                          // end row index for region
 291                          $rowEnd = ($y == 1) ?
 292                              $rangeStart[1] : $rangeEnd[1] - $yMax + $y;
 293  
 294                          // build range for region
 295                          $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd;
 296  
 297                          // retrieve relevant style array for region
 298                          $regionStyles = $pStyles;
 299                          unset($regionStyles['borders']['inside']);
 300  
 301                          // what are the inner edges of the region when looking at the selection
 302                          $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges);
 303  
 304                          // inner edges that are not touching the region should take the 'inside' border properties if they have been set
 305                          foreach ($innerEdges as $innerEdge) {
 306                              switch ($innerEdge) {
 307                                  case 'top':
 308                                  case 'bottom':
 309                                      // should pick up 'horizontal' border property if set
 310                                      if (isset($pStyles['borders']['horizontal'])) {
 311                                          $regionStyles['borders'][$innerEdge] = $pStyles['borders']['horizontal'];
 312                                      } else {
 313                                          unset($regionStyles['borders'][$innerEdge]);
 314                                      }
 315  
 316                                      break;
 317                                  case 'left':
 318                                  case 'right':
 319                                      // should pick up 'vertical' border property if set
 320                                      if (isset($pStyles['borders']['vertical'])) {
 321                                          $regionStyles['borders'][$innerEdge] = $pStyles['borders']['vertical'];
 322                                      } else {
 323                                          unset($regionStyles['borders'][$innerEdge]);
 324                                      }
 325  
 326                                      break;
 327                              }
 328                          }
 329  
 330                          // apply region style to region by calling applyFromArray() in simple mode
 331                          $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false);
 332                      }
 333                  }
 334  
 335                  // restore initial cell selection range
 336                  $this->getActiveSheet()->getStyle($pRange);
 337  
 338                  return $this;
 339              }
 340  
 341              // SIMPLE MODE:
 342              // Selection type, inspect
 343              if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) {
 344                  $selectionType = 'COLUMN';
 345              } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) {
 346                  $selectionType = 'ROW';
 347              } else {
 348                  $selectionType = 'CELL';
 349              }
 350  
 351              // First loop through columns, rows, or cells to find out which styles are affected by this operation
 352              switch ($selectionType) {
 353                  case 'COLUMN':
 354                      $oldXfIndexes = [];
 355                      for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
 356                          $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true;
 357                      }
 358                      foreach ($this->getActiveSheet()->getColumnIterator($rangeStart0, $rangeEnd0) as $columnIterator) {
 359                          $cellIterator = $columnIterator->getCellIterator();
 360                          $cellIterator->setIterateOnlyExistingCells(true);
 361                          foreach ($cellIterator as $columnCell) {
 362                              $columnCell->getStyle()->applyFromArray($pStyles);
 363                          }
 364                      }
 365  
 366                      break;
 367                  case 'ROW':
 368                      $oldXfIndexes = [];
 369                      for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
 370                          if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() == null) {
 371                              $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style
 372                          } else {
 373                              $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true;
 374                          }
 375                      }
 376                      foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) {
 377                          $cellIterator = $rowIterator->getCellIterator();
 378                          $cellIterator->setIterateOnlyExistingCells(true);
 379                          foreach ($cellIterator as $rowCell) {
 380                              $rowCell->getStyle()->applyFromArray($pStyles);
 381                          }
 382                      }
 383  
 384                      break;
 385                  case 'CELL':
 386                      $oldXfIndexes = [];
 387                      for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
 388                          for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
 389                              $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true;
 390                          }
 391                      }
 392  
 393                      break;
 394              }
 395  
 396              // clone each of the affected styles, apply the style array, and add the new styles to the workbook
 397              $workbook = $this->getActiveSheet()->getParent();
 398              foreach ($oldXfIndexes as $oldXfIndex => $dummy) {
 399                  $style = $workbook->getCellXfByIndex($oldXfIndex);
 400                  $newStyle = clone $style;
 401                  $newStyle->applyFromArray($pStyles);
 402  
 403                  if ($existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode())) {
 404                      // there is already such cell Xf in our collection
 405                      $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex();
 406                  } else {
 407                      // we don't have such a cell Xf, need to add
 408                      $workbook->addCellXf($newStyle);
 409                      $newXfIndexes[$oldXfIndex] = $newStyle->getIndex();
 410                  }
 411              }
 412  
 413              // Loop through columns, rows, or cells again and update the XF index
 414              switch ($selectionType) {
 415                  case 'COLUMN':
 416                      for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
 417                          $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col);
 418                          $oldXfIndex = $columnDimension->getXfIndex();
 419                          $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
 420                      }
 421  
 422                      break;
 423                  case 'ROW':
 424                      for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
 425                          $rowDimension = $this->getActiveSheet()->getRowDimension($row);
 426                          $oldXfIndex = $rowDimension->getXfIndex() === null ?
 427                              0 : $rowDimension->getXfIndex(); // row without explicit style should be formatted based on default style
 428                          $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
 429                      }
 430  
 431                      break;
 432                  case 'CELL':
 433                      for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
 434                          for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
 435                              $cell = $this->getActiveSheet()->getCellByColumnAndRow($col, $row);
 436                              $oldXfIndex = $cell->getXfIndex();
 437                              $cell->setXfIndex($newXfIndexes[$oldXfIndex]);
 438                          }
 439                      }
 440  
 441                      break;
 442              }
 443          } else {
 444              // not a supervisor, just apply the style array directly on style object
 445              if (isset($pStyles['fill'])) {
 446                  $this->getFill()->applyFromArray($pStyles['fill']);
 447              }
 448              if (isset($pStyles['font'])) {
 449                  $this->getFont()->applyFromArray($pStyles['font']);
 450              }
 451              if (isset($pStyles['borders'])) {
 452                  $this->getBorders()->applyFromArray($pStyles['borders']);
 453              }
 454              if (isset($pStyles['alignment'])) {
 455                  $this->getAlignment()->applyFromArray($pStyles['alignment']);
 456              }
 457              if (isset($pStyles['numberFormat'])) {
 458                  $this->getNumberFormat()->applyFromArray($pStyles['numberFormat']);
 459              }
 460              if (isset($pStyles['protection'])) {
 461                  $this->getProtection()->applyFromArray($pStyles['protection']);
 462              }
 463              if (isset($pStyles['quotePrefix'])) {
 464                  $this->quotePrefix = $pStyles['quotePrefix'];
 465              }
 466          }
 467  
 468          return $this;
 469      }
 470  
 471      /**
 472       * Get Fill.
 473       *
 474       * @return Fill
 475       */
 476      public function getFill()
 477      {
 478          return $this->fill;
 479      }
 480  
 481      /**
 482       * Get Font.
 483       *
 484       * @return Font
 485       */
 486      public function getFont()
 487      {
 488          return $this->font;
 489      }
 490  
 491      /**
 492       * Set font.
 493       *
 494       * @return $this
 495       */
 496      public function setFont(Font $font)
 497      {
 498          $this->font = $font;
 499  
 500          return $this;
 501      }
 502  
 503      /**
 504       * Get Borders.
 505       *
 506       * @return Borders
 507       */
 508      public function getBorders()
 509      {
 510          return $this->borders;
 511      }
 512  
 513      /**
 514       * Get Alignment.
 515       *
 516       * @return Alignment
 517       */
 518      public function getAlignment()
 519      {
 520          return $this->alignment;
 521      }
 522  
 523      /**
 524       * Get Number Format.
 525       *
 526       * @return NumberFormat
 527       */
 528      public function getNumberFormat()
 529      {
 530          return $this->numberFormat;
 531      }
 532  
 533      /**
 534       * Get Conditional Styles. Only used on supervisor.
 535       *
 536       * @return Conditional[]
 537       */
 538      public function getConditionalStyles()
 539      {
 540          return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell());
 541      }
 542  
 543      /**
 544       * Set Conditional Styles. Only used on supervisor.
 545       *
 546       * @param Conditional[] $pValue Array of conditional styles
 547       *
 548       * @return $this
 549       */
 550      public function setConditionalStyles(array $pValue)
 551      {
 552          $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $pValue);
 553  
 554          return $this;
 555      }
 556  
 557      /**
 558       * Get Protection.
 559       *
 560       * @return Protection
 561       */
 562      public function getProtection()
 563      {
 564          return $this->protection;
 565      }
 566  
 567      /**
 568       * Get quote prefix.
 569       *
 570       * @return bool
 571       */
 572      public function getQuotePrefix()
 573      {
 574          if ($this->isSupervisor) {
 575              return $this->getSharedComponent()->getQuotePrefix();
 576          }
 577  
 578          return $this->quotePrefix;
 579      }
 580  
 581      /**
 582       * Set quote prefix.
 583       *
 584       * @param bool $pValue
 585       *
 586       * @return $this
 587       */
 588      public function setQuotePrefix($pValue)
 589      {
 590          if ($pValue == '') {
 591              $pValue = false;
 592          }
 593          if ($this->isSupervisor) {
 594              $styleArray = ['quotePrefix' => $pValue];
 595              $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
 596          } else {
 597              $this->quotePrefix = (bool) $pValue;
 598          }
 599  
 600          return $this;
 601      }
 602  
 603      /**
 604       * Get hash code.
 605       *
 606       * @return string Hash code
 607       */
 608      public function getHashCode()
 609      {
 610          return md5(
 611              $this->fill->getHashCode() .
 612              $this->font->getHashCode() .
 613              $this->borders->getHashCode() .
 614              $this->alignment->getHashCode() .
 615              $this->numberFormat->getHashCode() .
 616              $this->protection->getHashCode() .
 617              ($this->quotePrefix ? 't' : 'f') .
 618              __CLASS__
 619          );
 620      }
 621  
 622      /**
 623       * Get own index in style collection.
 624       *
 625       * @return int
 626       */
 627      public function getIndex()
 628      {
 629          return $this->index;
 630      }
 631  
 632      /**
 633       * Set own index in style collection.
 634       *
 635       * @param int $pValue
 636       */
 637      public function setIndex($pValue): void
 638      {
 639          $this->index = $pValue;
 640      }
 641  
 642      protected function exportArray1(): array
 643      {
 644          $exportedArray = [];
 645          $this->exportArray2($exportedArray, 'alignment', $this->getAlignment());
 646          $this->exportArray2($exportedArray, 'borders', $this->getBorders());
 647          $this->exportArray2($exportedArray, 'fill', $this->getFill());
 648          $this->exportArray2($exportedArray, 'font', $this->getFont());
 649          $this->exportArray2($exportedArray, 'numberFormat', $this->getNumberFormat());
 650          $this->exportArray2($exportedArray, 'protection', $this->getProtection());
 651          $this->exportArray2($exportedArray, 'quotePrefx', $this->getQuotePrefix());
 652  
 653          return $exportedArray;
 654      }
 655  }