Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace OpenSpout\Writer\XLSX\Manager\Style;
   6  
   7  use OpenSpout\Common\Entity\Style\BorderPart;
   8  use OpenSpout\Common\Entity\Style\Color;
   9  use OpenSpout\Common\Entity\Style\Style;
  10  use OpenSpout\Writer\Common\Manager\Style\AbstractStyleManager as CommonStyleManager;
  11  use OpenSpout\Writer\XLSX\Helper\BorderHelper;
  12  
  13  /**
  14   * @internal
  15   *
  16   * @property StyleRegistry $styleRegistry
  17   */
  18  final class StyleManager extends CommonStyleManager
  19  {
  20      public function __construct(StyleRegistry $styleRegistry)
  21      {
  22          parent::__construct($styleRegistry);
  23      }
  24  
  25      /**
  26       * For empty cells, we can specify a style or not. If no style are specified,
  27       * then the software default will be applied. But sometimes, it may be useful
  28       * to override this default style, for instance if the cell should have a
  29       * background color different than the default one or some borders
  30       * (fonts property don't really matter here).
  31       *
  32       * @return bool Whether the cell should define a custom style
  33       */
  34      public function shouldApplyStyleOnEmptyCell(?int $styleId): bool
  35      {
  36          if (null === $styleId) {
  37              return false;
  38          }
  39          $associatedFillId = $this->styleRegistry->getFillIdForStyleId($styleId);
  40          $hasStyleCustomFill = (null !== $associatedFillId && 0 !== $associatedFillId);
  41  
  42          $associatedBorderId = $this->styleRegistry->getBorderIdForStyleId($styleId);
  43          $hasStyleCustomBorders = (null !== $associatedBorderId && 0 !== $associatedBorderId);
  44  
  45          $associatedFormatId = $this->styleRegistry->getFormatIdForStyleId($styleId);
  46          $hasStyleCustomFormats = (null !== $associatedFormatId && 0 !== $associatedFormatId);
  47  
  48          return $hasStyleCustomFill || $hasStyleCustomBorders || $hasStyleCustomFormats;
  49      }
  50  
  51      /**
  52       * Returns the content of the "styles.xml" file, given a list of styles.
  53       */
  54      public function getStylesXMLFileContent(): string
  55      {
  56          $content = <<<'EOD'
  57              <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  58              <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  59              EOD;
  60  
  61          $content .= $this->getFormatsSectionContent();
  62          $content .= $this->getFontsSectionContent();
  63          $content .= $this->getFillsSectionContent();
  64          $content .= $this->getBordersSectionContent();
  65          $content .= $this->getCellStyleXfsSectionContent();
  66          $content .= $this->getCellXfsSectionContent();
  67          $content .= $this->getCellStylesSectionContent();
  68  
  69          $content .= <<<'EOD'
  70              </styleSheet>
  71              EOD;
  72  
  73          return $content;
  74      }
  75  
  76      /**
  77       * Returns the content of the "<numFmts>" section.
  78       */
  79      private function getFormatsSectionContent(): string
  80      {
  81          $tags = [];
  82          $registeredFormats = $this->styleRegistry->getRegisteredFormats();
  83          foreach ($registeredFormats as $styleId) {
  84              $numFmtId = $this->styleRegistry->getFormatIdForStyleId($styleId);
  85  
  86              // Built-in formats do not need to be declared, skip them
  87              if ($numFmtId < 164) {
  88                  continue;
  89              }
  90  
  91              /** @var Style $style */
  92              $style = $this->styleRegistry->getStyleFromStyleId($styleId);
  93              $format = $style->getFormat();
  94              $tags[] = '<numFmt numFmtId="'.$numFmtId.'" formatCode="'.$format.'"/>';
  95          }
  96          $content = '<numFmts count="'.\count($tags).'">';
  97          $content .= implode('', $tags);
  98          $content .= '</numFmts>';
  99  
 100          return $content;
 101      }
 102  
 103      /**
 104       * Returns the content of the "<fonts>" section.
 105       */
 106      private function getFontsSectionContent(): string
 107      {
 108          $registeredStyles = $this->styleRegistry->getRegisteredStyles();
 109  
 110          $content = '<fonts count="'.\count($registeredStyles).'">';
 111  
 112          /** @var Style $style */
 113          foreach ($registeredStyles as $style) {
 114              $content .= '<font>';
 115  
 116              $content .= '<sz val="'.$style->getFontSize().'"/>';
 117              $content .= '<color rgb="'.Color::toARGB($style->getFontColor()).'"/>';
 118              $content .= '<name val="'.$style->getFontName().'"/>';
 119  
 120              if ($style->isFontBold()) {
 121                  $content .= '<b/>';
 122              }
 123              if ($style->isFontItalic()) {
 124                  $content .= '<i/>';
 125              }
 126              if ($style->isFontUnderline()) {
 127                  $content .= '<u/>';
 128              }
 129              if ($style->isFontStrikethrough()) {
 130                  $content .= '<strike/>';
 131              }
 132  
 133              $content .= '</font>';
 134          }
 135  
 136          $content .= '</fonts>';
 137  
 138          return $content;
 139      }
 140  
 141      /**
 142       * Returns the content of the "<fills>" section.
 143       */
 144      private function getFillsSectionContent(): string
 145      {
 146          $registeredFills = $this->styleRegistry->getRegisteredFills();
 147  
 148          // Excel reserves two default fills
 149          $fillsCount = \count($registeredFills) + 2;
 150          $content = sprintf('<fills count="%d">', $fillsCount);
 151  
 152          $content .= '<fill><patternFill patternType="none"/></fill>';
 153          $content .= '<fill><patternFill patternType="gray125"/></fill>';
 154  
 155          // The other fills are actually registered by setting a background color
 156          foreach ($registeredFills as $styleId) {
 157              /** @var Style $style */
 158              $style = $this->styleRegistry->getStyleFromStyleId($styleId);
 159  
 160              $backgroundColor = $style->getBackgroundColor();
 161              $content .= sprintf(
 162                  '<fill><patternFill patternType="solid"><fgColor rgb="%s"/></patternFill></fill>',
 163                  $backgroundColor
 164              );
 165          }
 166  
 167          $content .= '</fills>';
 168  
 169          return $content;
 170      }
 171  
 172      /**
 173       * Returns the content of the "<borders>" section.
 174       */
 175      private function getBordersSectionContent(): string
 176      {
 177          $registeredBorders = $this->styleRegistry->getRegisteredBorders();
 178  
 179          // There is one default border with index 0
 180          $borderCount = \count($registeredBorders) + 1;
 181  
 182          $content = '<borders count="'.$borderCount.'">';
 183  
 184          // Default border starting at index 0
 185          $content .= '<border><left/><right/><top/><bottom/></border>';
 186  
 187          foreach ($registeredBorders as $styleId) {
 188              $style = $this->styleRegistry->getStyleFromStyleId($styleId);
 189              $border = $style->getBorder();
 190              \assert(null !== $border);
 191              $content .= '<border>';
 192  
 193              // @see https://github.com/box/spout/issues/271
 194              foreach (BorderPart::allowedNames as $partName) {
 195                  $content .= BorderHelper::serializeBorderPart($border->getPart($partName));
 196              }
 197  
 198              $content .= '</border>';
 199          }
 200  
 201          $content .= '</borders>';
 202  
 203          return $content;
 204      }
 205  
 206      /**
 207       * Returns the content of the "<cellStyleXfs>" section.
 208       */
 209      private function getCellStyleXfsSectionContent(): string
 210      {
 211          return <<<'EOD'
 212              <cellStyleXfs count="1">
 213                  <xf borderId="0" fillId="0" fontId="0" numFmtId="0"/>
 214              </cellStyleXfs>
 215              EOD;
 216      }
 217  
 218      /**
 219       * Returns the content of the "<cellXfs>" section.
 220       */
 221      private function getCellXfsSectionContent(): string
 222      {
 223          $registeredStyles = $this->styleRegistry->getRegisteredStyles();
 224  
 225          $content = '<cellXfs count="'.\count($registeredStyles).'">';
 226  
 227          foreach ($registeredStyles as $style) {
 228              $styleId = $style->getId();
 229              $fillId = $this->getFillIdForStyleId($styleId);
 230              $borderId = $this->getBorderIdForStyleId($styleId);
 231              $numFmtId = $this->getFormatIdForStyleId($styleId);
 232  
 233              $content .= '<xf numFmtId="'.$numFmtId.'" fontId="'.$styleId.'" fillId="'.$fillId.'" borderId="'.$borderId.'" xfId="0"';
 234  
 235              if ($style->shouldApplyFont()) {
 236                  $content .= ' applyFont="1"';
 237              }
 238  
 239              $content .= sprintf(' applyBorder="%d"', (bool) $style->getBorder());
 240  
 241              if ($style->shouldApplyCellAlignment() || $style->shouldApplyCellVerticalAlignment() || $style->hasSetWrapText() || $style->shouldShrinkToFit()) {
 242                  $content .= ' applyAlignment="1">';
 243                  $content .= '<alignment';
 244                  if ($style->shouldApplyCellAlignment()) {
 245                      $content .= sprintf(' horizontal="%s"', $style->getCellAlignment());
 246                  }
 247                  if ($style->shouldApplyCellVerticalAlignment()) {
 248                      $content .= sprintf(' vertical="%s"', $style->getCellVerticalAlignment());
 249                  }
 250                  if ($style->hasSetWrapText()) {
 251                      $content .= ' wrapText="'.($style->shouldWrapText() ? '1' : '0').'"';
 252                  }
 253                  if ($style->shouldShrinkToFit()) {
 254                      $content .= ' shrinkToFit="true"';
 255                  }
 256  
 257                  $content .= '/>';
 258                  $content .= '</xf>';
 259              } else {
 260                  $content .= '/>';
 261              }
 262          }
 263  
 264          $content .= '</cellXfs>';
 265  
 266          return $content;
 267      }
 268  
 269      /**
 270       * Returns the content of the "<cellStyles>" section.
 271       */
 272      private function getCellStylesSectionContent(): string
 273      {
 274          return <<<'EOD'
 275              <cellStyles count="1">
 276                  <cellStyle builtinId="0" name="Normal" xfId="0"/>
 277              </cellStyles>
 278              EOD;
 279      }
 280  
 281      /**
 282       * Returns the fill ID associated to the given style ID.
 283       * For the default style, we don't a fill.
 284       */
 285      private function getFillIdForStyleId(int $styleId): int
 286      {
 287          // For the default style (ID = 0), we don't want to override the fill.
 288          // Otherwise all cells of the spreadsheet will have a background color.
 289          $isDefaultStyle = (0 === $styleId);
 290  
 291          return $isDefaultStyle ? 0 : ($this->styleRegistry->getFillIdForStyleId($styleId) ?? 0);
 292      }
 293  
 294      /**
 295       * Returns the fill ID associated to the given style ID.
 296       * For the default style, we don't a border.
 297       */
 298      private function getBorderIdForStyleId(int $styleId): int
 299      {
 300          // For the default style (ID = 0), we don't want to override the border.
 301          // Otherwise all cells of the spreadsheet will have a border.
 302          $isDefaultStyle = (0 === $styleId);
 303  
 304          return $isDefaultStyle ? 0 : ($this->styleRegistry->getBorderIdForStyleId($styleId) ?? 0);
 305      }
 306  
 307      /**
 308       * Returns the format ID associated to the given style ID.
 309       * For the default style use general format.
 310       */
 311      private function getFormatIdForStyleId(int $styleId): int
 312      {
 313          // For the default style (ID = 0), we don't want to override the format.
 314          // Otherwise all cells of the spreadsheet will have a format.
 315          $isDefaultStyle = (0 === $styleId);
 316  
 317          return $isDefaultStyle ? 0 : ($this->styleRegistry->getFormatIdForStyleId($styleId) ?? 0);
 318      }
 319  }