diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9745d7..145b6a31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Place announcement text here. - Introduced writer for the "Table Alignment" element (see `\PhpOffice\PhpWord\Writer\Word2007\Element\TableAlignment`). - @RomanSyroeshko - Supported indexed arrays in arguments of `TemplateProcessor::setValue()`. - @RomanSyroeshko #618 - Introduced automatic output escaping for OOXML, ODF, HTML, and RTF. To turn the feature on use `phpword.ini` or `\PhpOffice\PhpWord\Settings`. - @RomanSyroeshko #483 +- Supported processing of headers and footers in `TemplateProcessor::applyXslStyleSheet()`. - @RomanSyroeshko #335 ### Changed - Improved error message for the case when `autoload.php` is not found. - @RomanSyroeshko #371 diff --git a/README.md b/README.md index 3c2bd5c1..949238a7 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ With PHPWord, you can create OOXML, ODF, or RTF documents dynamically using your - Insert charts (pie, doughnut, bar, line, area, scatter, radar) - Insert form fields (textinput, checkbox, and dropdown) - Create document from templates -- Use XSL 1.0 style sheets to transform main document part of OOXML template +- Use XSL 1.0 style sheets to transform headers, main document part, and footers of an OOXML template - ... and many more features on progress ## Requirements diff --git a/composer.json b/composer.json index e9399914..c49eb9cd 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "ext-zip": "Allows writing OOXML and ODF", "ext-gd2": "Allows adding images", "ext-xmlwriter": "Allows writing OOXML and ODF", - "ext-xsl": "Allows applying XSL style sheet to main document part of OOXML template", + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", "dompdf/dompdf": "Allows writing PDF" }, "autoload": { diff --git a/docs/intro.rst b/docs/intro.rst index 0ef27c9f..d1c791cf 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -50,8 +50,7 @@ Features - Insert charts (pie, doughnut, bar, line, area, scatter, radar) - Insert form fields (textinput, checkbox, and dropdown) - Create document from templates -- Use XSL 1.0 style sheets to transform main document part of OOXML - template +- Use XSL 1.0 style sheets to transform headers, main document part, and footers of an OOXML template - ... and many more features on progress File formats diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst index 1c0b8b03..af03b245 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -15,7 +15,7 @@ Example: $templateProcessor->setValue('Name', 'John Doe'); $templateProcessor->setValue(array('City', 'Street'), array('Detroit', '12th Street')); -It is not possible to directly add new OOXML elements to the template file being processed, but it is possible to transform main document part of the template using XSLT (see ``TemplateProcessor::applyXslStyleSheet``). +It is not possible to directly add new OOXML elements to the template file being processed, but it is possible to transform headers, main document part, and footers of the template using XSLT (see ``TemplateProcessor::applyXslStyleSheet``). See ``Sample_07_TemplateCloneRow.php`` for example on how to create multirow from a single row in a template by using ``TemplateProcessor::cloneRow``. diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 5f131901..d2a306e9 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -100,7 +100,49 @@ class TemplateProcessor ); $index++; } - $this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName('word/document.xml')); + $this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName($this->getMainPartName())); + } + + /** + * @param string $xml + * @param \XSLTProcessor $xsltProcessor + * + * @return string + * + * @throws \PhpOffice\PhpWord\Exception\Exception + */ + protected function transformSingleXml($xml, $xsltProcessor) + { + $domDocument = new \DOMDocument(); + if (false === $domDocument->loadXML($xml)) { + throw new Exception('Could not load the given XML document.'); + } + + $transformedXml = $xsltProcessor->transformToXml($domDocument); + if (false === $transformedXml) { + throw new Exception('Could not transform the given XML document.'); + } + + return $transformedXml; + } + + /** + * @param mixed $xml + * @param \XSLTProcessor $xsltProcessor + * + * @return mixed + */ + protected function transformXml($xml, $xsltProcessor) + { + if (is_array($xml)) { + foreach ($xml as &$item) { + $item = $this->transformSingleXml($item, $xsltProcessor); + } + } else { + $xml = $this->transformSingleXml($xml, $xsltProcessor); + } + + return $xml; } /** @@ -109,35 +151,26 @@ class TemplateProcessor * Note: since the method doesn't make any guess on logic of the provided XSL style sheet, * make sure that output is correctly escaped. Otherwise you may get broken document. * - * @param \DOMDocument $xslDOMDocument + * @param \DOMDocument $xslDomDocument * @param array $xslOptions - * @param string $xslOptionsURI + * @param string $xslOptionsUri * * @return void * * @throws \PhpOffice\PhpWord\Exception\Exception */ - public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '') + public function applyXslStyleSheet($xslDomDocument, $xslOptions = array(), $xslOptionsUri = '') { $xsltProcessor = new \XSLTProcessor(); - $xsltProcessor->importStylesheet($xslDOMDocument); - - if (false === $xsltProcessor->setParameter($xslOptionsURI, $xslOptions)) { + $xsltProcessor->importStylesheet($xslDomDocument); + if (false === $xsltProcessor->setParameter($xslOptionsUri, $xslOptions)) { throw new Exception('Could not set values for the given XSL style sheet parameters.'); } - $xmlDOMDocument = new \DOMDocument(); - if (false === $xmlDOMDocument->loadXML($this->tempDocumentMainPart)) { - throw new Exception('Could not load XML from the given template.'); - } - - $xmlTransformed = $xsltProcessor->transformToXml($xmlDOMDocument); - if (false === $xmlTransformed) { - throw new Exception('Could not transform the given XML document.'); - } - - $this->tempDocumentMainPart = $xmlTransformed; + $this->tempDocumentHeaders = $this->transformXml($this->tempDocumentHeaders, $xsltProcessor); + $this->tempDocumentMainPart = $this->transformXml($this->tempDocumentMainPart, $xsltProcessor); + $this->tempDocumentFooters = $this->transformXml($this->tempDocumentFooters, $xsltProcessor); } /** @@ -365,14 +398,14 @@ class TemplateProcessor */ public function save() { - foreach ($this->tempDocumentHeaders as $index => $headerXML) { - $this->zipClass->addFromString($this->getHeaderName($index), $this->tempDocumentHeaders[$index]); + foreach ($this->tempDocumentHeaders as $index => $xml) { + $this->zipClass->addFromString($this->getHeaderName($index), $xml); } - $this->zipClass->addFromString('word/document.xml', $this->tempDocumentMainPart); + $this->zipClass->addFromString($this->getMainPartName(), $this->tempDocumentMainPart); - foreach ($this->tempDocumentFooters as $index => $headerXML) { - $this->zipClass->addFromString($this->getFooterName($index), $this->tempDocumentFooters[$index]); + foreach ($this->tempDocumentFooters as $index => $xml) { + $this->zipClass->addFromString($this->getFooterName($index), $xml); } // Close zip file @@ -414,8 +447,6 @@ class TemplateProcessor * Finds parts of broken macros and sticks them together. * Macros, while being edited, could be implicitly broken by some of the word processors. * - * @since 0.13.0 - * * @param string $documentPart The document part in XML representation. * * @return string @@ -470,18 +501,6 @@ class TemplateProcessor return $matches[1]; } - /** - * Get the name of the footer file for $index. - * - * @param integer $index - * - * @return string - */ - protected function getFooterName($index) - { - return sprintf('word/footer%d.xml', $index); - } - /** * Get the name of the header file for $index. * @@ -494,6 +513,26 @@ class TemplateProcessor return sprintf('word/header%d.xml', $index); } + /** + * @return string + */ + protected function getMainPartName() + { + return 'word/document.xml'; + } + + /** + * Get the name of the footer file for $index. + * + * @param integer $index + * + * @return string + */ + protected function getFooterName($index) + { + return sprintf('word/footer%d.xml', $index); + } + /** * Find the start position of the nearest table row before $offset. * diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index c5a4b1a3..3c2b8e46 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -35,10 +35,10 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase $templateFqfn = __DIR__ . '/_files/templates/with_table_macros.docx'; $templateProcessor = new TemplateProcessor($templateFqfn); - $xslDOMDocument = new \DOMDocument(); - $xslDOMDocument->load(__DIR__ . "/_files/xsl/remove_tables_by_needle.xsl"); - foreach (array('${employee.', '${scoreboard.') as $needle) { - $templateProcessor->applyXslStyleSheet($xslDOMDocument, array('needle' => $needle)); + $xslDomDocument = new \DOMDocument(); + $xslDomDocument->load(__DIR__ . "/_files/xsl/remove_tables_by_needle.xsl"); + foreach (array('${employee.', '${scoreboard.', '${reference.') as $needle) { + $templateProcessor->applyXslStyleSheet($xslDomDocument, array('needle' => $needle)); } $documentFqfn = $templateProcessor->save(); @@ -48,19 +48,25 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase $templateZip = new \ZipArchive(); $templateZip->open($templateFqfn); - $templateXml = $templateZip->getFromName('word/document.xml'); + $templateHeaderXml = $templateZip->getFromName('word/header1.xml'); + $templateMainPartXml = $templateZip->getFromName('word/document.xml'); + $templateFooterXml = $templateZip->getFromName('word/footer1.xml'); if (false === $templateZip->close()) { throw new \Exception("Could not close zip file \"{$templateZip}\"."); } $documentZip = new \ZipArchive(); $documentZip->open($documentFqfn); - $documentXml = $documentZip->getFromName('word/document.xml'); + $documentHeaderXml = $documentZip->getFromName('word/header1.xml'); + $documentMainPartXml = $documentZip->getFromName('word/document.xml'); + $documentFooterXml = $documentZip->getFromName('word/footer1.xml'); if (false === $documentZip->close()) { throw new \Exception("Could not close zip file \"{$documentZip}\"."); } - $this->assertNotEquals($documentXml, $templateXml); + $this->assertNotEquals($templateHeaderXml, $documentHeaderXml); + $this->assertNotEquals($templateMainPartXml, $documentMainPartXml); + $this->assertNotEquals($templateFooterXml, $documentFooterXml); return $documentFqfn; } @@ -82,19 +88,25 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase $actualDocumentZip = new \ZipArchive(); $actualDocumentZip->open($actualDocumentFqfn); - $actualDocumentXml = $actualDocumentZip->getFromName('word/document.xml'); + $actualHeaderXml = $actualDocumentZip->getFromName('word/header1.xml'); + $actualMainPartXml = $actualDocumentZip->getFromName('word/document.xml'); + $actualFooterXml = $actualDocumentZip->getFromName('word/footer1.xml'); if (false === $actualDocumentZip->close()) { throw new \Exception("Could not close zip file \"{$actualDocumentFqfn}\"."); } $expectedDocumentZip = new \ZipArchive(); $expectedDocumentZip->open($expectedDocumentFqfn); - $expectedDocumentXml = $expectedDocumentZip->getFromName('word/document.xml'); + $expectedHeaderXml = $expectedDocumentZip->getFromName('word/header1.xml'); + $expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml'); + $expectedFooterXml = $expectedDocumentZip->getFromName('word/footer1.xml'); if (false === $expectedDocumentZip->close()) { throw new \Exception("Could not close zip file \"{$expectedDocumentFqfn}\"."); } - $this->assertXmlStringEqualsXmlString($expectedDocumentXml, $actualDocumentXml); + $this->assertXmlStringEqualsXmlString($expectedHeaderXml, $actualHeaderXml); + $this->assertXmlStringEqualsXmlString($expectedMainPartXml, $actualMainPartXml); + $this->assertXmlStringEqualsXmlString($expectedFooterXml, $actualFooterXml); } /** @@ -109,14 +121,14 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/blank.docx'); - $xslDOMDocument = new \DOMDocument(); - $xslDOMDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl'); + $xslDomDocument = new \DOMDocument(); + $xslDomDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl'); /* * We have to use error control below, because \XSLTProcessor::setParameter omits warning on failure. * This warning fails the test. */ - @$templateProcessor->applyXslStyleSheet($xslDOMDocument, array(1 => 'somevalue')); + @$templateProcessor->applyXslStyleSheet($xslDomDocument, array(1 => 'somevalue')); } /** @@ -124,21 +136,21 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase * * @covers ::applyXslStyleSheet * @expectedException \PhpOffice\PhpWord\Exception\Exception - * @expectedExceptionMessage Could not load XML from the given template. + * @expectedExceptionMessage Could not load the given XML document. * @test */ final public function testXslStyleSheetCanNotBeAppliedOnFailureOfLoadingXmlFromTemplate() { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/corrupted_main_document_part.docx'); - $xslDOMDocument = new \DOMDocument(); - $xslDOMDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl'); + $xslDomDocument = new \DOMDocument(); + $xslDomDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl'); /* * We have to use error control below, because \DOMDocument::loadXML omits warning on failure. * This warning fails the test. */ - @$templateProcessor->applyXslStyleSheet($xslDOMDocument); + @$templateProcessor->applyXslStyleSheet($xslDomDocument); } /** diff --git a/tests/PhpWord/_files/documents/without_table_macros.docx b/tests/PhpWord/_files/documents/without_table_macros.docx index e4e9767f..316a1df7 100644 Binary files a/tests/PhpWord/_files/documents/without_table_macros.docx and b/tests/PhpWord/_files/documents/without_table_macros.docx differ diff --git a/tests/PhpWord/_files/templates/with_table_macros.docx b/tests/PhpWord/_files/templates/with_table_macros.docx index cd5ed6ce..4653c6f1 100644 Binary files a/tests/PhpWord/_files/templates/with_table_macros.docx and b/tests/PhpWord/_files/templates/with_table_macros.docx differ