Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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