diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28b6326b..1ddd2e89 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ This is the changelog between releases of PHPWord. Releases are listed in revers
- Table: Add `exactHeight` to row style to define whether row height should be exact or atLeast - @jcarignan GH-168
- Element: New `CheckBox` element for sections and table cells - @ozilion GH-156
- Settings: Ability to use PCLZip as alternative to ZipArchive - @bskrtich @ivanlanin GH-106 GH-140 GH-185
+- Template: Ability to find & replace variables in headers & footers - @dgudgeon GH-190
- Table: Ability to add footnote in table cell - @ivanlanin GH-187
- Footnote: Ability to add image in footnote - @ivanlanin GH-187
- ListItem: Ability to add list item in header/footer - @ivanlanin GH-187
diff --git a/samples/Sample_07_TemplateCloneRow.php b/samples/Sample_07_TemplateCloneRow.php
index 4344fafc..72464d84 100755
--- a/samples/Sample_07_TemplateCloneRow.php
+++ b/samples/Sample_07_TemplateCloneRow.php
@@ -7,6 +7,11 @@ $phpWord = new \PhpOffice\PhpWord\PhpWord();
$document = $phpWord->loadTemplate('resources/Sample_07_TemplateCloneRow.docx');
+// Variables on different parts of document
+$document->setValue('weekday', date('l')); // On section/content
+$document->setValue('time', date('H:i')); // On footer
+$document->setValue('serverName', $_SERVER['SERVER_NAME']); // On header
+
// Simple table
$document->cloneRow('rowValue', 10);
@@ -32,9 +37,6 @@ $document->setValue('rowNumber#8', '8');
$document->setValue('rowNumber#9', '9');
$document->setValue('rowNumber#10', '10');
-$document->setValue('weekday', date('l'));
-$document->setValue('time', date('H:i'));
-
// Table with a spanned cell
$document->cloneRow('userId', 3);
diff --git a/samples/resources/Sample_07_TemplateCloneRow.docx b/samples/resources/Sample_07_TemplateCloneRow.docx
index 25a8c418..9ac6c2a1 100755
Binary files a/samples/resources/Sample_07_TemplateCloneRow.docx and b/samples/resources/Sample_07_TemplateCloneRow.docx differ
diff --git a/src/PhpWord/Template.php b/src/PhpWord/Template.php
index 0711ecd5..142228c3 100644
--- a/src/PhpWord/Template.php
+++ b/src/PhpWord/Template.php
@@ -23,46 +23,72 @@ class Template
*
* @var mixed
*/
- private $_objZip;
+ private $zipClass;
/**
* Temporary file name
*
* @var string
*/
- private $_tempFileName;
+ private $tempFileName;
/**
* Document XML
*
* @var string
*/
- private $_documentXML;
+ private $documentXML;
+ /**
+ * Document header XML
+ *
+ * @var string[]
+ */
+ private $headerXMLs = array();
+
+ /**
+ * Document footer XML
+ *
+ * @var string[]
+ */
+ private $footerXMLs = array();
/**
* Create a new Template Object
*
* @param string $strFilename
- * @throws \PhpOffice\PhpWord\Exception\Exception
+ * @throws Exception
*/
public function __construct($strFilename)
{
- $this->_tempFileName = tempnam(sys_get_temp_dir(), '');
- if ($this->_tempFileName === false) {
+ $this->tempFileName = tempnam(sys_get_temp_dir(), '');
+ if ($this->tempFileName === false) {
throw new Exception('Could not create temporary file with unique name in the default temporary directory.');
}
// Copy the source File to the temp File
- if (!copy($strFilename, $this->_tempFileName)) {
- throw new Exception("Could not copy the template from {$strFilename} to {$this->_tempFileName}.");
+ if (!copy($strFilename, $this->tempFileName)) {
+ throw new Exception("Could not copy the template from {$strFilename} to {$this->tempFileName}.");
}
$zipClass = Settings::getZipClass();
- $this->_objZip = new $zipClass();
- $this->_objZip->open($this->_tempFileName);
+ $this->zipClass = new $zipClass();
+ $this->zipClass->open($this->tempFileName);
- $this->_documentXML = $this->_objZip->getFromName('word/document.xml');
+ // Find and load headers and footers
+ $i = 1;
+ while ($this->zipClass->locateName($this->getHeaderName($i)) !== false) {
+ $this->headerXMLs[$i] = $this->zipClass->getFromName($this->getHeaderName($i));
+ $i++;
+ }
+
+ $i = 1;
+ while ($this->zipClass->locateName($this->getFooterName($i)) !== false) {
+ $this->footerXMLs[$i] = $this->zipClass->getFromName($this->getFooterName($i));
+ $i++;
+ }
+
+ $this->documentXML = $this->zipClass->getFromName('word/document.xml');
}
/**
@@ -71,7 +97,7 @@ class Template
* @param \DOMDocument $xslDOMDocument
* @param array $xslOptions
* @param string $xslOptionsURI
- * @throws \PhpOffice\PhpWord\Exception\Exception
+ * @throws Exception
*/
public function applyXslStyleSheet(&$xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '')
{
@@ -84,7 +110,7 @@ class Template
}
$xmlDOMDocument = new \DOMDocument();
- if ($xmlDOMDocument->loadXML($this->_documentXML) === false) {
+ if ($xmlDOMDocument->loadXML($this->documentXML) === false) {
throw new Exception('Could not load XML from the given template.');
}
@@ -93,7 +119,7 @@ class Template
throw new Exception('Could not transform the given XML document.');
}
- $this->_documentXML = $xmlTransformed;
+ $this->documentXML = $xmlTransformed;
}
/**
@@ -104,13 +130,151 @@ class Template
* @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);
+ }
+ }
+
+ /**
+ * Returns array of all variables in template
+ * @return string[]
+ */
+ public function getVariables()
+ {
+ $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);
+ }
+
+ /**
+ * Clone a table row in a template document
+ *
+ * @param string $search
+ * @param int $numberOfClones
+ * @throws Exception
+ */
+ public function cloneRow($search, $numberOfClones)
+ {
+ if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') {
+ $search = '${' . $search . '}';
+ }
+
+ $tagPos = strpos($this->documentXML, $search);
+ if (!$tagPos) {
+ throw new Exception("Can not clone row, template variable not found or variable contains markup.");
+ }
+
+ $rowStart = $this->findRowStart($tagPos);
+ $rowEnd = $this->findRowEnd($tagPos);
+ $xmlRow = $this->getSlice($rowStart, $rowEnd);
+
+ // Check if there's a cell spanning multiple rows.
+ if (preg_match('##', $xmlRow)) {
+ $extraRowStart = $rowEnd;
+ $extraRowEnd = $rowEnd;
+ while (true) {
+ $extraRowStart = $this->findRowStart($extraRowEnd + 1);
+ $extraRowEnd = $this->findRowEnd($extraRowEnd + 1);
+
+ // If extraRowEnd is lower then 7, there was no next row found.
+ if ($extraRowEnd < 7) {
+ break;
+ }
+
+ // If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row.
+ $tmpXmlRow = $this->getSlice($extraRowStart, $extraRowEnd);
+ if (!preg_match('##', $tmpXmlRow) && !preg_match('##', $tmpXmlRow)) {
+ break;
+ }
+ // This row was a spanned row, update $rowEnd and search for the next row.
+ $rowEnd = $extraRowEnd;
+ }
+ $xmlRow = $this->getSlice($rowStart, $rowEnd);
+ }
+
+ $result = $this->getSlice(0, $rowStart);
+ for ($i = 1; $i <= $numberOfClones; $i++) {
+ $result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow);
+ }
+ $result .= $this->getSlice($rowEnd);
+
+ $this->documentXML = $result;
+ }
+
+ /**
+ * Save XML to temporary file
+ *
+ * @return string
+ * @throws Exception
+ */
+ public function save()
+ {
+ foreach ($this->headerXMLs as $index => $headerXML) {
+ $this->zipClass->addFromString($this->getHeaderName($index), $this->headerXMLs[$index]);
+ }
+
+ $this->zipClass->addFromString('word/document.xml', $this->documentXML);
+
+ foreach ($this->footerXMLs as $index => $headerXML) {
+ $this->zipClass->addFromString($this->getFooterName($index), $this->footerXMLs[$index]);
+ }
+
+ // Close zip file
+ if ($this->zipClass->close() === false) {
+ throw new Exception('Could not close zip file.');
+ }
+
+ return $this->tempFileName;
+ }
+
+ /**
+ * Save XML to defined name
+ *
+ * @param string $strFilename
+ */
+ public function saveAs($strFilename)
+ {
+ $tempFilename = $this->save();
+
+ if (\file_exists($strFilename)) {
+ unlink($strFilename);
+ }
+
+ rename($tempFilename, $strFilename);
+ }
+
+ /**
+ * Find and replace placeholders in the given XML section.
+ *
+ * @param string $documentPartXML
+ * @param string $search
+ * @param mixed $replace
+ * @param integer $limit
+ * @return string
+ */
+ 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,30 +294,53 @@ 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);
}
/**
- * Returns array of all variables in template
+ * Find all variables in $documentPartXML
+ * @param string $documentPartXML
+ * @return string[]
*/
- public function getVariables()
+ protected function getVariablesForPart($documentPartXML)
{
- preg_match_all('/\$\{(.*?)}/i', $this->_documentXML, $matches);
+ preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
+
return $matches[1];
}
+ /**
+ * 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);
+ }
+
/**
* Find the start position of the nearest table row before $offset
*
* @param int $offset
* @return int
- * @throws \PhpOffice\PhpWord\Exception\Exception
+ * @throws Exception
*/
- private function _findRowStart($offset)
+ private function findRowStart($offset)
{
- $rowStart = strrpos($this->_documentXML, "_documentXML) - $offset) * -1));
+ $rowStart = strrpos($this->documentXML, "documentXML) - $offset) * -1));
if (!$rowStart) {
- $rowStart = strrpos($this->_documentXML, "", ((strlen($this->_documentXML) - $offset) * -1));
+ $rowStart = strrpos($this->documentXML, "", ((strlen($this->documentXML) - $offset) * -1));
}
if (!$rowStart) {
throw new Exception("Can not find the start position of the row to clone.");
@@ -167,9 +354,9 @@ class Template
* @param int $offset
* @return int
*/
- private function _findRowEnd($offset)
+ private function findRowEnd($offset)
{
- $rowEnd = strpos($this->_documentXML, "", $offset) + 7;
+ $rowEnd = strpos($this->documentXML, "", $offset) + 7;
return $rowEnd;
}
@@ -180,100 +367,11 @@ class Template
* @param int $endPosition
* @return string
*/
- private function _getSlice($startPosition, $endPosition = 0)
+ private function getSlice($startPosition, $endPosition = 0)
{
if (!$endPosition) {
- $endPosition = strlen($this->_documentXML);
+ $endPosition = strlen($this->documentXML);
}
- return substr($this->_documentXML, $startPosition, ($endPosition - $startPosition));
- }
-
- /**
- * Clone a table row in a template document
- *
- * @param string $search
- * @param int $numberOfClones
- * @throws \PhpOffice\PhpWord\Exception\Exception
- */
- public function cloneRow($search, $numberOfClones)
- {
- if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') {
- $search = '${' . $search . '}';
- }
-
- $tagPos = strpos($this->_documentXML, $search);
- if (!$tagPos) {
- throw new Exception("Can not clone row, template variable not found or variable contains markup.");
- }
-
- $rowStart = $this->_findRowStart($tagPos);
- $rowEnd = $this->_findRowEnd($tagPos);
- $xmlRow = $this->_getSlice($rowStart, $rowEnd);
-
- // Check if there's a cell spanning multiple rows.
- if (preg_match('##', $xmlRow)) {
- $extraRowStart = $rowEnd;
- $extraRowEnd = $rowEnd;
- while (true) {
- $extraRowStart = $this->_findRowStart($extraRowEnd + 1);
- $extraRowEnd = $this->_findRowEnd($extraRowEnd + 1);
-
- // If extraRowEnd is lower then 7, there was no next row found.
- if ($extraRowEnd < 7) {
- break;
- }
-
- // If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row.
- $tmpXmlRow = $this->_getSlice($extraRowStart, $extraRowEnd);
- if (!preg_match('##', $tmpXmlRow) && !preg_match('##', $tmpXmlRow)) {
- break;
- }
- // This row was a spanned row, update $rowEnd and search for the next row.
- $rowEnd = $extraRowEnd;
- }
- $xmlRow = $this->_getSlice($rowStart, $rowEnd);
- }
-
- $result = $this->_getSlice(0, $rowStart);
- for ($i = 1; $i <= $numberOfClones; $i++) {
- $result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow);
- }
- $result .= $this->_getSlice($rowEnd);
-
- $this->_documentXML = $result;
- }
-
- /**
- * Save XML to temporary file
- *
- * @return string
- * @throws \PhpOffice\PhpWord\Exception\Exception
- */
- public function save()
- {
- $this->_objZip->addFromString('word/document.xml', $this->_documentXML);
-
- // Close zip file
- if ($this->_objZip->close() === false) {
- throw new Exception('Could not close zip file.');
- }
-
- return $this->_tempFileName;
- }
-
- /**
- * Save XML to defined name
- *
- * @param string $strFilename
- */
- public function saveAs($strFilename)
- {
- $tempFilename = $this->save();
-
- if (\file_exists($strFilename)) {
- unlink($strFilename);
- }
-
- rename($tempFilename, $strFilename);
+ return substr($this->documentXML, $startPosition, ($endPosition - $startPosition));
}
}
diff --git a/tests/PhpWord/Tests/TemplateTest.php b/tests/PhpWord/Tests/TemplateTest.php
index eb766772..6929c4fd 100644
--- a/tests/PhpWord/Tests/TemplateTest.php
+++ b/tests/PhpWord/Tests/TemplateTest.php
@@ -156,4 +156,27 @@ final class TemplateTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedVar, $actualVar);
$this->assertTrue($docFound);
}
+
+ /**
+ * Replace variables in header and footer
+ */
+ 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 00000000..647d5222
Binary files /dev/null and b/tests/PhpWord/Tests/_files/templates/header-footer.docx differ