From 763de347df1aad981e98f041f46d71079955d37c Mon Sep 17 00:00:00 2001 From: Dave Gudgeon Date: Tue, 1 Apr 2014 13:25:05 +0100 Subject: [PATCH] Add support for document headers and footers to Template class. --- src/PhpWord/Template.php | 113 ++++++++++++++++-- tests/PhpWord/Tests/TemplateTest.php | 20 ++++ .../Tests/_files/templates/header-footer.docx | Bin 0 -> 4495 bytes 3 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 tests/PhpWord/Tests/_files/templates/header-footer.docx diff --git a/src/PhpWord/Template.php b/src/PhpWord/Template.php index f54d729e..e38e2a5a 100644 --- a/src/PhpWord/Template.php +++ b/src/PhpWord/Template.php @@ -32,6 +32,13 @@ class Template */ private $_tempFileName; + /** + * Document header XML + * + * @var string[] + */ + private $_headerXMLs = array(); + /** * Document XML * @@ -39,6 +46,12 @@ class Template */ private $_documentXML; + /** + * Document footer XML + * + * @var string[] + */ + private $_footerXMLs = array(); /** * Create a new Template Object @@ -62,9 +75,41 @@ class Template $this->_objZip = new $zipClass(); $this->_objZip->open($this->_tempFileName); + // Find and load up to three headers and footers + for ($i = 1; $i <= 3; $i++) { + $headerName = $this->getHeaderName($i); + $footerName = $this->getFooterName($i); + if ($this->_objZip->locateName($headerName) !== false) { + $this->_headerXMLs[$i] = $this->_objZip->getFromName($headerName); + } + if ($this->_objZip->locateName($footerName) !== false) { + $this->_footerXMLs[$i] = $this->_objZip->getFromName($footerName); + } + } + $this->_documentXML = $this->_objZip->getFromName('word/document.xml'); } + /** + * Get the name of the footer file for $index + * @param integer $index + * @return string + */ + private function getFooterName($index) + { + return sprintf('word/footer%d.xml', $index); + } + + /** + * Get the name of the header file for $index + * @param integer $index + * @return string + */ + private function getHeaderName($index) + { + return sprintf('word/header%d.xml', $index); + } + /** * Applies XSL style sheet to template's parts * @@ -97,20 +142,22 @@ class Template } /** - * Set a Template value + * Find and replace placeholders in the given XML section. * - * @param mixed $search + * @param string $documentPartXML + * @param string $search * @param mixed $replace * @param integer $limit + * @return string */ - public function setValue($search, $replace, $limit = -1) + protected function setValueForPart($documentPartXML, $search, $replace, $limit) { $pattern = '|\$\{([^\}]+)\}|U'; - preg_match_all($pattern, $this->_documentXML, $matches); + preg_match_all($pattern, $documentPartXML, $matches); foreach ($matches[0] as $value) { $valueCleaned = preg_replace('/<[^>]+>/', '', $value); $valueCleaned = preg_replace('/<\/[^>]+>/', '', $valueCleaned); - $this->_documentXML = str_replace($value, $valueCleaned, $this->_documentXML); + $documentPartXML = str_replace($value, $valueCleaned, $documentPartXML); } if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') { @@ -130,16 +177,58 @@ class Template $regExpDelim = '/'; $escapedSearch = preg_quote($search, $regExpDelim); - $this->_documentXML = preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $this->_documentXML, $limit); + return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit); + } + + /** + * Set a Template value + * + * @param mixed $search + * @param mixed $replace + * @param integer $limit + */ + public function setValue($search, $replace, $limit = -1) + { + foreach ($this->_headerXMLs as $index => $headerXML) { + $this->_headerXMLs[$index] = $this->setValueForPart($this->_headerXMLs[$index], $search, $replace, $limit); + } + + $this->_documentXML = $this->setValueForPart($this->_documentXML, $search, $replace, $limit); + + foreach ($this->_footerXMLs as $index => $headerXML) { + $this->_footerXMLs[$index] = $this->setValueForPart($this->_footerXMLs[$index], $search, $replace, $limit); + } + } + + /** + * Find all variables in $documentPartXML + * @param string $documentPartXML + * @return string[] + */ + protected function getVariablesForPart($documentPartXML) + { + preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches); + + return $matches[1]; } /** * Returns array of all variables in template + * @return string[] */ public function getVariables() { - preg_match_all('/\$\{(.*?)}/i', $this->_documentXML, $matches); - return $matches[1]; + $variables = $this->getVariablesForPart($this->_documentXML); + + foreach ($this->_headerXMLs as $headerXML) { + $variables = array_merge($variables, $this->getVariablesForPart($headerXML)); + } + + foreach ($this->_footerXMLs as $footerXML) { + $variables = array_merge($variables, $this->getVariablesForPart($footerXML)); + } + + return array_unique($variables); } /** @@ -251,8 +340,16 @@ class Template */ public function save() { + foreach ($this->_headerXMLs as $index => $headerXML) { + $this->_objZip->addFromString($this->getHeaderName($index), $this->_headerXMLs[$index]); + } + $this->_objZip->addFromString('word/document.xml', $this->_documentXML); + foreach ($this->_footerXMLs as $index => $headerXML) { + $this->_objZip->addFromString($this->getFooterName($index), $this->_footerXMLs[$index]); + } + // Close zip file if ($this->_objZip->close() === false) { throw new Exception('Could not close zip file.'); diff --git a/tests/PhpWord/Tests/TemplateTest.php b/tests/PhpWord/Tests/TemplateTest.php index 669ae946..848d7736 100644 --- a/tests/PhpWord/Tests/TemplateTest.php +++ b/tests/PhpWord/Tests/TemplateTest.php @@ -156,4 +156,24 @@ final class TemplateTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedVar, $actualVar); $this->assertTrue($docFound); } + + public function testVariablesCanBeReplacedInHeaderAndFooter() + { + $template = __DIR__ . "/_files/templates/header-footer.docx"; + $expectedVar = array('documentContent', 'headerValue', 'footerValue'); + $docName = 'header-footer-test-result.docx'; + + $document = new Template($template); + $actualVar = $document->getVariables(); + $document->setValue('headerValue', 'Header Value'); + $document->setValue('documentContent', 'Document text.'); + $document->setValue('footerValue', 'Footer Value'); + $document->saveAs($docName); + $docFound = file_exists($docName); + unlink($docName); + + $this->assertEquals($expectedVar, $actualVar); + $this->assertTrue($docFound); + + } } diff --git a/tests/PhpWord/Tests/_files/templates/header-footer.docx b/tests/PhpWord/Tests/_files/templates/header-footer.docx new file mode 100644 index 0000000000000000000000000000000000000000..647d52222d612ef33a0113e8ea3ad84ad3c96085 GIT binary patch literal 4495 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