Added support for cell formats when writing Excel files
This commit is contained in:
parent
16a2f91a22
commit
b802cce694
@ -71,6 +71,12 @@ class Style
|
|||||||
/** @var bool */
|
/** @var bool */
|
||||||
private $hasSetBackgroundColor = false;
|
private $hasSetBackgroundColor = false;
|
||||||
|
|
||||||
|
/** @var string Format */
|
||||||
|
private $format = null;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $hasSetFormat = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int|null
|
* @return int|null
|
||||||
*/
|
*/
|
||||||
@ -383,4 +389,34 @@ class Style
|
|||||||
{
|
{
|
||||||
return $this->hasSetBackgroundColor;
|
return $this->hasSetBackgroundColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets format
|
||||||
|
* @param string $format
|
||||||
|
* @return Style
|
||||||
|
*/
|
||||||
|
public function setFormat($format)
|
||||||
|
{
|
||||||
|
$this->hasSetFormat = true;
|
||||||
|
$this->format = $format;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFormat()
|
||||||
|
{
|
||||||
|
return $this->format;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return bool Whether format should be applied
|
||||||
|
*/
|
||||||
|
public function shouldApplyFormat()
|
||||||
|
{
|
||||||
|
return $this->hasSetFormat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,18 @@ class StyleBuilder
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Sets a format
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
* @param string $format Format
|
||||||
|
* @return StyleBuilder
|
||||||
|
*/
|
||||||
|
public function setFormat($format)
|
||||||
|
{
|
||||||
|
$this->style->setFormat($format);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the configured style. The style is cached and can be reused.
|
* Returns the configured style. The style is cached and can be reused.
|
||||||
|
@ -88,6 +88,9 @@ class StyleMerger
|
|||||||
if (!$style->getBorder() && $baseStyle->shouldApplyBorder()) {
|
if (!$style->getBorder() && $baseStyle->shouldApplyBorder()) {
|
||||||
$styleToUpdate->setBorder($baseStyle->getBorder());
|
$styleToUpdate->setBorder($baseStyle->getBorder());
|
||||||
}
|
}
|
||||||
|
if (!$style->getFormat() && $baseStyle->shouldApplyFormat()) {
|
||||||
|
$styleToUpdate->setFormat($baseStyle->getFormat());
|
||||||
|
}
|
||||||
if (!$style->shouldApplyBackgroundColor() && $baseStyle->shouldApplyBackgroundColor()) {
|
if (!$style->shouldApplyBackgroundColor() && $baseStyle->shouldApplyBackgroundColor()) {
|
||||||
$styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor());
|
$styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor());
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,10 @@ class StyleManager extends \Box\Spout\Writer\Common\Manager\Style\StyleManager
|
|||||||
$associatedBorderId = $this->styleRegistry->getBorderIdForStyleId($styleId);
|
$associatedBorderId = $this->styleRegistry->getBorderIdForStyleId($styleId);
|
||||||
$hasStyleCustomBorders = ($associatedBorderId !== null && $associatedBorderId !== 0);
|
$hasStyleCustomBorders = ($associatedBorderId !== null && $associatedBorderId !== 0);
|
||||||
|
|
||||||
return ($hasStyleCustomFill || $hasStyleCustomBorders);
|
$associatedFormatId = $this->styleRegistry->getFormatIdForStyleId($styleId);
|
||||||
|
$hasStyleCustomFormats = ($associatedFormatId !== null && $associatedFormatId !== 0);
|
||||||
|
|
||||||
|
return ($hasStyleCustomFill || $hasStyleCustomBorders || $hasStyleCustomFormats);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,6 +51,7 @@ class StyleManager extends \Box\Spout\Writer\Common\Manager\Style\StyleManager
|
|||||||
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||||
EOD;
|
EOD;
|
||||||
|
|
||||||
|
$content .= $this->getFormatsSectionContent();
|
||||||
$content .= $this->getFontsSectionContent();
|
$content .= $this->getFontsSectionContent();
|
||||||
$content .= $this->getFillsSectionContent();
|
$content .= $this->getFillsSectionContent();
|
||||||
$content .= $this->getBordersSectionContent();
|
$content .= $this->getBordersSectionContent();
|
||||||
@ -62,6 +66,35 @@ EOD;
|
|||||||
return $content;
|
return $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content of the "<numFmts>" section.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getFormatsSectionContent()
|
||||||
|
{
|
||||||
|
$tags = [];
|
||||||
|
$registeredFormats = $this->styleRegistry->getRegisteredFormats();
|
||||||
|
foreach ($registeredFormats as $styleId) {
|
||||||
|
$numFmtId = $this->styleRegistry->getFormatIdForStyleId($styleId);
|
||||||
|
|
||||||
|
//Built-in formats do not need to be declared, skip them
|
||||||
|
if ($numFmtId < 164) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Style $style */
|
||||||
|
$style = $this->styleRegistry->getStyleFromStyleId($styleId);
|
||||||
|
$format = $style->getFormat();
|
||||||
|
$tags[] = '<numFmt numFmtId="' . $numFmtId . '" formatCode="' . $format . '"/>';
|
||||||
|
}
|
||||||
|
$content = '<numFmts count="' . count($tags) . '">';
|
||||||
|
$content .= implode('', $tags);
|
||||||
|
$content .= '</numFmts>';
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the content of the "<fonts>" section.
|
* Returns the content of the "<fonts>" section.
|
||||||
*
|
*
|
||||||
@ -206,8 +239,9 @@ EOD;
|
|||||||
$styleId = $style->getId();
|
$styleId = $style->getId();
|
||||||
$fillId = $this->getFillIdForStyleId($styleId);
|
$fillId = $this->getFillIdForStyleId($styleId);
|
||||||
$borderId = $this->getBorderIdForStyleId($styleId);
|
$borderId = $this->getBorderIdForStyleId($styleId);
|
||||||
|
$numFmtId = $this->getFormatIdForStyleId($styleId);
|
||||||
|
|
||||||
$content .= '<xf numFmtId="0" fontId="' . $styleId . '" fillId="' . $fillId . '" borderId="' . $borderId . '" xfId="0"';
|
$content .= '<xf numFmtId="' . $numFmtId . '" fontId="' . $styleId . '" fillId="' . $fillId . '" borderId="' . $borderId . '" xfId="0"';
|
||||||
|
|
||||||
if ($style->shouldApplyFont()) {
|
if ($style->shouldApplyFont()) {
|
||||||
$content .= ' applyFont="1"';
|
$content .= ' applyFont="1"';
|
||||||
@ -261,6 +295,22 @@ EOD;
|
|||||||
return $isDefaultStyle ? 0 : ($this->styleRegistry->getBorderIdForStyleId($styleId) ?: 0);
|
return $isDefaultStyle ? 0 : ($this->styleRegistry->getBorderIdForStyleId($styleId) ?: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the format ID associated to the given style ID.
|
||||||
|
* For the default style use general format.
|
||||||
|
*
|
||||||
|
* @param int $styleId
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function getFormatIdForStyleId($styleId)
|
||||||
|
{
|
||||||
|
// For the default style (ID = 0), we don't want to override the format.
|
||||||
|
// Otherwise all cells of the spreadsheet will have a format.
|
||||||
|
$isDefaultStyle = ($styleId === 0);
|
||||||
|
|
||||||
|
return $isDefaultStyle ? 0 : ($this->styleRegistry->getFormatIdForStyleId($styleId) ?: 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the content of the "<cellStyles>" section.
|
* Returns the content of the "<cellStyles>" section.
|
||||||
*
|
*
|
||||||
|
@ -10,6 +10,81 @@ use Box\Spout\Common\Entity\Style\Style;
|
|||||||
*/
|
*/
|
||||||
class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
|
class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx
|
||||||
|
* @var array Mapping between built-in format and the associated numFmtId
|
||||||
|
*/
|
||||||
|
protected static $builtinNumFormatToIdMapping = [
|
||||||
|
|
||||||
|
'General' => 0,
|
||||||
|
'0' => 1,
|
||||||
|
'0.00' => 2,
|
||||||
|
'#,##0' => 3,
|
||||||
|
'#,##0.00' => 4,
|
||||||
|
'$#,##0,\-$#,##0' => 5,
|
||||||
|
'$#,##0,[Red]\-$#,##0' => 6,
|
||||||
|
'$#,##0.00,\-$#,##0.00' => 7,
|
||||||
|
'$#,##0.00,[Red]\-$#,##0.00' => 8,
|
||||||
|
'0%' => 9,
|
||||||
|
'0.00%' => 10,
|
||||||
|
'0.00E+00' => 11,
|
||||||
|
'# ?/?' => 12,
|
||||||
|
'# ??/??' => 13,
|
||||||
|
'mm-dd-yy' => 14,
|
||||||
|
'd-mmm-yy' => 15,
|
||||||
|
'd-mmm' => 16,
|
||||||
|
'mmm-yy' => 17,
|
||||||
|
'h:mm AM/PM' => 18,
|
||||||
|
'h:mm:ss AM/PM' => 19,
|
||||||
|
'h:mm' => 20,
|
||||||
|
'h:mm:ss' => 21,
|
||||||
|
'm/d/yy h:mm' => 22,
|
||||||
|
|
||||||
|
'#,##0 ,(#,##0)' => 37,
|
||||||
|
'#,##0 ,[Red](#,##0)' => 38,
|
||||||
|
'#,##0.00,(#,##0.00)' => 39,
|
||||||
|
'#,##0.00,[Red](#,##0.00)' => 40,
|
||||||
|
|
||||||
|
'_("$"* #,##0.00_),_("$"* \(#,##0.00\),_("$"* "-"??_),_(@_)' => 44,
|
||||||
|
'mm:ss' => 45,
|
||||||
|
'[h]:mm:ss' => 46,
|
||||||
|
'mm:ss.0' => 47,
|
||||||
|
|
||||||
|
'##0.0E+0' => 48,
|
||||||
|
'@' => 49,
|
||||||
|
|
||||||
|
'[$-404]e/m/d' => 27,
|
||||||
|
'm/d/yy' => 30,
|
||||||
|
't0' => 59,
|
||||||
|
't0.00' => 60,
|
||||||
|
't#,##0' => 61,
|
||||||
|
't#,##0.00' => 62,
|
||||||
|
't0%' => 67,
|
||||||
|
't0.00%' => 68,
|
||||||
|
't# ?/?' => 69,
|
||||||
|
't# ??/??' => 70,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $registeredFormats = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array [STYLE_ID] => [FORMAT_ID] maps a style to a format declaration
|
||||||
|
*/
|
||||||
|
protected $styleIdToFormatsMappingTable = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the numFmtId is lower than 0xA4 (164 in decimal)
|
||||||
|
* then it's a built-in number format.
|
||||||
|
* Since Excel is the dominant vendor - we play along here
|
||||||
|
*
|
||||||
|
* @var int The fill index counter for custom fills.
|
||||||
|
*/
|
||||||
|
protected $formatIndex = 164;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
@ -48,11 +123,58 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
|
|||||||
{
|
{
|
||||||
$registeredStyle = parent::registerStyle($style);
|
$registeredStyle = parent::registerStyle($style);
|
||||||
$this->registerFill($registeredStyle);
|
$this->registerFill($registeredStyle);
|
||||||
|
$this->registerFormat($registeredStyle);;
|
||||||
$this->registerBorder($registeredStyle);
|
$this->registerBorder($registeredStyle);
|
||||||
|
|
||||||
return $registeredStyle;
|
return $registeredStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a format definition
|
||||||
|
*
|
||||||
|
* @param Style $style
|
||||||
|
*/
|
||||||
|
protected function registerFormat(Style $style)
|
||||||
|
{
|
||||||
|
$styleId = $style->getId();
|
||||||
|
|
||||||
|
$format = $style->getFormat();
|
||||||
|
if ($format) {
|
||||||
|
$isFormatRegistered = isset($this->registeredFormats[$format]);
|
||||||
|
|
||||||
|
// We need to track the already registered format definitions
|
||||||
|
if ($isFormatRegistered) {
|
||||||
|
$registeredStyleId = $this->registeredFormats[$format];
|
||||||
|
$registeredFormatId = $this->styleIdToFormatsMappingTable[$registeredStyleId];
|
||||||
|
$this->styleIdToFormatsMappingTable[$styleId] = $registeredFormatId;
|
||||||
|
} else {
|
||||||
|
$this->registeredFormats[$format] = $styleId;
|
||||||
|
if (isset(self::$builtinNumFormatToIdMapping[$format])) {
|
||||||
|
$id = self::$builtinNumFormatToIdMapping[$format];
|
||||||
|
} else {
|
||||||
|
$id = $this->formatIndex++;
|
||||||
|
}
|
||||||
|
$this->styleIdToFormatsMappingTable[$styleId] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// The formatId maps a style to a format declaration
|
||||||
|
// When there is no format definition - we default to 0 ( General )
|
||||||
|
$this->styleIdToFormatsMappingTable[$styleId] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $styleId
|
||||||
|
* @return int|null Format ID associated to the given style ID
|
||||||
|
*/
|
||||||
|
public function getFormatIdForStyleId($styleId)
|
||||||
|
{
|
||||||
|
return (isset($this->styleIdToFormatsMappingTable[$styleId])) ?
|
||||||
|
$this->styleIdToFormatsMappingTable[$styleId] :
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a fill definition
|
* Register a fill definition
|
||||||
*
|
*
|
||||||
@ -136,6 +258,7 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
|
|||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
@ -151,4 +274,14 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
|
|||||||
{
|
{
|
||||||
return $this->registeredBorders;
|
return $this->registeredBorders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getRegisteredFormats()
|
||||||
|
{
|
||||||
|
return $this->registeredFormats;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -210,6 +210,47 @@ class WriterWithStyleTest extends TestCase
|
|||||||
$this->assertEquals('1', $cellDomElements[1]->getAttribute('s'));
|
$this->assertEquals('1', $cellDomElements[1]->getAttribute('s'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testAddRowWithNumFmtStyles()
|
||||||
|
{
|
||||||
|
$fileName = 'test_add_row_with_numfmt.xlsx';
|
||||||
|
$style = (new StyleBuilder())
|
||||||
|
->setFontBold()
|
||||||
|
->setFormat('0.00')//Builtin format
|
||||||
|
->build();
|
||||||
|
$style2 = (new StyleBuilder())
|
||||||
|
->setFontBold()
|
||||||
|
->setFormat('0.000')
|
||||||
|
->build();
|
||||||
|
|
||||||
|
|
||||||
|
$dataRows = [
|
||||||
|
$this->createStyledRowFromValues([1.123456789], $style),
|
||||||
|
$this->createStyledRowFromValues([12.1], $style2),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->writeToXLSXFile($dataRows, $fileName);
|
||||||
|
|
||||||
|
|
||||||
|
$formatsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'numFmts');
|
||||||
|
$this->assertEquals(
|
||||||
|
1,
|
||||||
|
$formatsDomElement->getAttribute('count'),
|
||||||
|
'There should be 2 formats, including the 1 default ones'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
$cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs');
|
||||||
|
|
||||||
|
foreach ([2, 164] as $index => $expected) {
|
||||||
|
$xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item($index + 1);
|
||||||
|
$this->assertEquals($expected, $xfElement->getAttribute('numFmtId'));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user