Skip to content

Commit fb863cd

Browse files
author
Roman Syroeshko
committed
https://github.com/PHPOffice/PHPWord/issues/335.
1 parent 7deb010 commit fb863cd

File tree

9 files changed

+104
-53
lines changed

9 files changed

+104
-53
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Place announcement text here.
1414
- Introduced writer for the "Table Alignment" element (see `\PhpOffice\PhpWord\Writer\Word2007\Element\TableAlignment`). - @RomanSyroeshko
1515
- Supported indexed arrays in arguments of `TemplateProcessor::setValue()`. - @RomanSyroeshko #618
1616
- Introduced automatic output escaping for OOXML, ODF, HTML, and RTF. To turn the feature on use `phpword.ini` or `\PhpOffice\PhpWord\Settings`. - @RomanSyroeshko #483
17+
- Supported processing of headers and footers in `TemplateProcessor::applyXslStyleSheet()`. - @RomanSyroeshko #335
1718

1819
### Changed
1920
- Improved error message for the case when `autoload.php` is not found. - @RomanSyroeshko #371

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ With PHPWord, you can create OOXML, ODF, or RTF documents dynamically using your
4545
- Insert charts (pie, doughnut, bar, line, area, scatter, radar)
4646
- Insert form fields (textinput, checkbox, and dropdown)
4747
- Create document from templates
48-
- Use XSL 1.0 style sheets to transform main document part of OOXML template
48+
- Use XSL 1.0 style sheets to transform headers, main document part, and footers of an OOXML template
4949
- ... and many more features on progress
5050

5151
## Requirements

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"ext-zip": "Allows writing OOXML and ODF",
5454
"ext-gd2": "Allows adding images",
5555
"ext-xmlwriter": "Allows writing OOXML and ODF",
56-
"ext-xsl": "Allows applying XSL style sheet to main document part of OOXML template",
56+
"ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template",
5757
"dompdf/dompdf": "Allows writing PDF"
5858
},
5959
"autoload": {

docs/intro.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ Features
5050
- Insert charts (pie, doughnut, bar, line, area, scatter, radar)
5151
- Insert form fields (textinput, checkbox, and dropdown)
5252
- Create document from templates
53-
- Use XSL 1.0 style sheets to transform main document part of OOXML
54-
template
53+
- Use XSL 1.0 style sheets to transform headers, main document part, and footers of an OOXML template
5554
- ... and many more features on progress
5655

5756
File formats

docs/templates-processing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Example:
1515
$templateProcessor->setValue('Name', 'John Doe');
1616
$templateProcessor->setValue(array('City', 'Street'), array('Detroit', '12th Street'));
1717
18-
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``).
18+
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``).
1919

2020
See ``Sample_07_TemplateCloneRow.php`` for example on how to create
2121
multirow from a single row in a template by using ``TemplateProcessor::cloneRow``.

src/PhpWord/TemplateProcessor.php

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,49 @@ public function __construct($documentTemplate)
100100
);
101101
$index++;
102102
}
103-
$this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName('word/document.xml'));
103+
$this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName($this->getMainPartName()));
104+
}
105+
106+
/**
107+
* @param string $xml
108+
* @param \XSLTProcessor $xsltProcessor
109+
*
110+
* @return string
111+
*
112+
* @throws \PhpOffice\PhpWord\Exception\Exception
113+
*/
114+
protected function transformSingleXml($xml, $xsltProcessor)
115+
{
116+
$domDocument = new \DOMDocument();
117+
if (false === $domDocument->loadXML($xml)) {
118+
throw new Exception('Could not load the given XML document.');
119+
}
120+
121+
$transformedXml = $xsltProcessor->transformToXml($domDocument);
122+
if (false === $transformedXml) {
123+
throw new Exception('Could not transform the given XML document.');
124+
}
125+
126+
return $transformedXml;
127+
}
128+
129+
/**
130+
* @param mixed $xml
131+
* @param \XSLTProcessor $xsltProcessor
132+
*
133+
* @return mixed
134+
*/
135+
protected function transformXml($xml, $xsltProcessor)
136+
{
137+
if (is_array($xml)) {
138+
foreach ($xml as &$item) {
139+
$item = $this->transformSingleXml($item, $xsltProcessor);
140+
}
141+
} else {
142+
$xml = $this->transformSingleXml($xml, $xsltProcessor);
143+
}
144+
145+
return $xml;
104146
}
105147

106148
/**
@@ -109,35 +151,26 @@ public function __construct($documentTemplate)
109151
* Note: since the method doesn't make any guess on logic of the provided XSL style sheet,
110152
* make sure that output is correctly escaped. Otherwise you may get broken document.
111153
*
112-
* @param \DOMDocument $xslDOMDocument
154+
* @param \DOMDocument $xslDomDocument
113155
* @param array $xslOptions
114-
* @param string $xslOptionsURI
156+
* @param string $xslOptionsUri
115157
*
116158
* @return void
117159
*
118160
* @throws \PhpOffice\PhpWord\Exception\Exception
119161
*/
120-
public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '')
162+
public function applyXslStyleSheet($xslDomDocument, $xslOptions = array(), $xslOptionsUri = '')
121163
{
122164
$xsltProcessor = new \XSLTProcessor();
123165

124-
$xsltProcessor->importStylesheet($xslDOMDocument);
125-
126-
if (false === $xsltProcessor->setParameter($xslOptionsURI, $xslOptions)) {
166+
$xsltProcessor->importStylesheet($xslDomDocument);
167+
if (false === $xsltProcessor->setParameter($xslOptionsUri, $xslOptions)) {
127168
throw new Exception('Could not set values for the given XSL style sheet parameters.');
128169
}
129170

130-
$xmlDOMDocument = new \DOMDocument();
131-
if (false === $xmlDOMDocument->loadXML($this->tempDocumentMainPart)) {
132-
throw new Exception('Could not load XML from the given template.');
133-
}
134-
135-
$xmlTransformed = $xsltProcessor->transformToXml($xmlDOMDocument);
136-
if (false === $xmlTransformed) {
137-
throw new Exception('Could not transform the given XML document.');
138-
}
139-
140-
$this->tempDocumentMainPart = $xmlTransformed;
171+
$this->tempDocumentHeaders = $this->transformXml($this->tempDocumentHeaders, $xsltProcessor);
172+
$this->tempDocumentMainPart = $this->transformXml($this->tempDocumentMainPart, $xsltProcessor);
173+
$this->tempDocumentFooters = $this->transformXml($this->tempDocumentFooters, $xsltProcessor);
141174
}
142175

143176
/**
@@ -365,14 +398,14 @@ public function deleteBlock($blockname)
365398
*/
366399
public function save()
367400
{
368-
foreach ($this->tempDocumentHeaders as $index => $headerXML) {
369-
$this->zipClass->addFromString($this->getHeaderName($index), $this->tempDocumentHeaders[$index]);
401+
foreach ($this->tempDocumentHeaders as $index => $xml) {
402+
$this->zipClass->addFromString($this->getHeaderName($index), $xml);
370403
}
371404

372-
$this->zipClass->addFromString('word/document.xml', $this->tempDocumentMainPart);
405+
$this->zipClass->addFromString($this->getMainPartName(), $this->tempDocumentMainPart);
373406

374-
foreach ($this->tempDocumentFooters as $index => $headerXML) {
375-
$this->zipClass->addFromString($this->getFooterName($index), $this->tempDocumentFooters[$index]);
407+
foreach ($this->tempDocumentFooters as $index => $xml) {
408+
$this->zipClass->addFromString($this->getFooterName($index), $xml);
376409
}
377410

378411
// Close zip file
@@ -414,8 +447,6 @@ public function saveAs($fileName)
414447
* Finds parts of broken macros and sticks them together.
415448
* Macros, while being edited, could be implicitly broken by some of the word processors.
416449
*
417-
* @since 0.13.0
418-
*
419450
* @param string $documentPart The document part in XML representation.
420451
*
421452
* @return string
@@ -471,27 +502,35 @@ protected function getVariablesForPart($documentPartXML)
471502
}
472503

473504
/**
474-
* Get the name of the footer file for $index.
505+
* Get the name of the header file for $index.
475506
*
476507
* @param integer $index
477508
*
478509
* @return string
479510
*/
480-
protected function getFooterName($index)
511+
protected function getHeaderName($index)
481512
{
482-
return sprintf('word/footer%d.xml', $index);
513+
return sprintf('word/header%d.xml', $index);
483514
}
484515

485516
/**
486-
* Get the name of the header file for $index.
517+
* @return string
518+
*/
519+
protected function getMainPartName()
520+
{
521+
return 'word/document.xml';
522+
}
523+
524+
/**
525+
* Get the name of the footer file for $index.
487526
*
488527
* @param integer $index
489528
*
490529
* @return string
491530
*/
492-
protected function getHeaderName($index)
531+
protected function getFooterName($index)
493532
{
494-
return sprintf('word/header%d.xml', $index);
533+
return sprintf('word/footer%d.xml', $index);
495534
}
496535

497536
/**

tests/PhpWord/TemplateProcessorTest.php

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ final public function testTemplateCanBeSavedInTemporaryLocation()
3535
$templateFqfn = __DIR__ . '/_files/templates/with_table_macros.docx';
3636

3737
$templateProcessor = new TemplateProcessor($templateFqfn);
38-
$xslDOMDocument = new \DOMDocument();
39-
$xslDOMDocument->load(__DIR__ . "/_files/xsl/remove_tables_by_needle.xsl");
40-
foreach (array('${employee.', '${scoreboard.') as $needle) {
41-
$templateProcessor->applyXslStyleSheet($xslDOMDocument, array('needle' => $needle));
38+
$xslDomDocument = new \DOMDocument();
39+
$xslDomDocument->load(__DIR__ . "/_files/xsl/remove_tables_by_needle.xsl");
40+
foreach (array('${employee.', '${scoreboard.', '${reference.') as $needle) {
41+
$templateProcessor->applyXslStyleSheet($xslDomDocument, array('needle' => $needle));
4242
}
4343

4444
$documentFqfn = $templateProcessor->save();
@@ -48,19 +48,25 @@ final public function testTemplateCanBeSavedInTemporaryLocation()
4848

4949
$templateZip = new \ZipArchive();
5050
$templateZip->open($templateFqfn);
51-
$templateXml = $templateZip->getFromName('word/document.xml');
51+
$templateHeaderXml = $templateZip->getFromName('word/header1.xml');
52+
$templateMainPartXml = $templateZip->getFromName('word/document.xml');
53+
$templateFooterXml = $templateZip->getFromName('word/footer1.xml');
5254
if (false === $templateZip->close()) {
5355
throw new \Exception("Could not close zip file \"{$templateZip}\".");
5456
}
5557

5658
$documentZip = new \ZipArchive();
5759
$documentZip->open($documentFqfn);
58-
$documentXml = $documentZip->getFromName('word/document.xml');
60+
$documentHeaderXml = $documentZip->getFromName('word/header1.xml');
61+
$documentMainPartXml = $documentZip->getFromName('word/document.xml');
62+
$documentFooterXml = $documentZip->getFromName('word/footer1.xml');
5963
if (false === $documentZip->close()) {
6064
throw new \Exception("Could not close zip file \"{$documentZip}\".");
6165
}
6266

63-
$this->assertNotEquals($documentXml, $templateXml);
67+
$this->assertNotEquals($templateHeaderXml, $documentHeaderXml);
68+
$this->assertNotEquals($templateMainPartXml, $documentMainPartXml);
69+
$this->assertNotEquals($templateFooterXml, $documentFooterXml);
6470

6571
return $documentFqfn;
6672
}
@@ -82,19 +88,25 @@ final public function testXslStyleSheetCanBeApplied($actualDocumentFqfn)
8288

8389
$actualDocumentZip = new \ZipArchive();
8490
$actualDocumentZip->open($actualDocumentFqfn);
85-
$actualDocumentXml = $actualDocumentZip->getFromName('word/document.xml');
91+
$actualHeaderXml = $actualDocumentZip->getFromName('word/header1.xml');
92+
$actualMainPartXml = $actualDocumentZip->getFromName('word/document.xml');
93+
$actualFooterXml = $actualDocumentZip->getFromName('word/footer1.xml');
8694
if (false === $actualDocumentZip->close()) {
8795
throw new \Exception("Could not close zip file \"{$actualDocumentFqfn}\".");
8896
}
8997

9098
$expectedDocumentZip = new \ZipArchive();
9199
$expectedDocumentZip->open($expectedDocumentFqfn);
92-
$expectedDocumentXml = $expectedDocumentZip->getFromName('word/document.xml');
100+
$expectedHeaderXml = $expectedDocumentZip->getFromName('word/header1.xml');
101+
$expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml');
102+
$expectedFooterXml = $expectedDocumentZip->getFromName('word/footer1.xml');
93103
if (false === $expectedDocumentZip->close()) {
94104
throw new \Exception("Could not close zip file \"{$expectedDocumentFqfn}\".");
95105
}
96106

97-
$this->assertXmlStringEqualsXmlString($expectedDocumentXml, $actualDocumentXml);
107+
$this->assertXmlStringEqualsXmlString($expectedHeaderXml, $actualHeaderXml);
108+
$this->assertXmlStringEqualsXmlString($expectedMainPartXml, $actualMainPartXml);
109+
$this->assertXmlStringEqualsXmlString($expectedFooterXml, $actualFooterXml);
98110
}
99111

100112
/**
@@ -109,36 +121,36 @@ final public function testXslStyleSheetCanNotBeAppliedOnFailureOfSettingParamete
109121
{
110122
$templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/blank.docx');
111123

112-
$xslDOMDocument = new \DOMDocument();
113-
$xslDOMDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl');
124+
$xslDomDocument = new \DOMDocument();
125+
$xslDomDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl');
114126

115127
/*
116128
* We have to use error control below, because \XSLTProcessor::setParameter omits warning on failure.
117129
* This warning fails the test.
118130
*/
119-
@$templateProcessor->applyXslStyleSheet($xslDOMDocument, array(1 => 'somevalue'));
131+
@$templateProcessor->applyXslStyleSheet($xslDomDocument, array(1 => 'somevalue'));
120132
}
121133

122134
/**
123135
* XSL stylesheet can be applied on failure of loading XML from template.
124136
*
125137
* @covers ::applyXslStyleSheet
126138
* @expectedException \PhpOffice\PhpWord\Exception\Exception
127-
* @expectedExceptionMessage Could not load XML from the given template.
139+
* @expectedExceptionMessage Could not load the given XML document.
128140
* @test
129141
*/
130142
final public function testXslStyleSheetCanNotBeAppliedOnFailureOfLoadingXmlFromTemplate()
131143
{
132144
$templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/corrupted_main_document_part.docx');
133145

134-
$xslDOMDocument = new \DOMDocument();
135-
$xslDOMDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl');
146+
$xslDomDocument = new \DOMDocument();
147+
$xslDomDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl');
136148

137149
/*
138150
* We have to use error control below, because \DOMDocument::loadXML omits warning on failure.
139151
* This warning fails the test.
140152
*/
141-
@$templateProcessor->applyXslStyleSheet($xslDOMDocument);
153+
@$templateProcessor->applyXslStyleSheet($xslDomDocument);
142154
}
143155

144156
/**
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)