Update 2 Template processor setValue() improvements #614
This commit is contained in:
parent
873d41a872
commit
b446a23b61
|
|
@ -1,5 +1,4 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of PHPWord - A pure PHP library for reading and writing
|
||||
* word processing documents.
|
||||
|
|
@ -26,41 +25,41 @@ use PhpOffice\PhpWord\Shared\ZipArchive;
|
|||
|
||||
class TemplateProcessor
|
||||
{
|
||||
const MAXIMUM_REPLACEMENTS_DEFAULT = - 1;
|
||||
|
||||
const MAXIMUM_REPLACEMENTS_DEFAULT = -1;
|
||||
|
||||
/**
|
||||
* ZipArchive object.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $zipClass;
|
||||
|
||||
|
||||
/**
|
||||
* @var string Temporary document filename (with path).
|
||||
*/
|
||||
protected $tempDocumentFilename;
|
||||
|
||||
|
||||
/**
|
||||
* Content of main document part (in XML format) of the temporary document.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tempDocumentMainPart;
|
||||
|
||||
|
||||
/**
|
||||
* Content of headers (in XML format) of the temporary document.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $tempDocumentHeaders = array();
|
||||
|
||||
|
||||
/**
|
||||
* Content of footers (in XML format) of the temporary document.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $tempDocumentFooters = array();
|
||||
|
||||
|
||||
/**
|
||||
* @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception.
|
||||
*
|
||||
|
|
@ -69,35 +68,39 @@ class TemplateProcessor
|
|||
* @throws \PhpOffice\PhpWord\Exception\CreateTemporaryFileException
|
||||
* @throws \PhpOffice\PhpWord\Exception\CopyFileException
|
||||
*/
|
||||
public function __construct($documentTemplate) {
|
||||
|
||||
public function __construct($documentTemplate)
|
||||
{
|
||||
// Temporary document filename initialization
|
||||
$this->tempDocumentFilename = tempnam(Settings::getTempDir(), 'PhpWord');
|
||||
if (false === $this->tempDocumentFilename) {
|
||||
throw new CreateTemporaryFileException();
|
||||
}
|
||||
|
||||
|
||||
// Template file cloning
|
||||
if (false === copy($documentTemplate, $this->tempDocumentFilename)) {
|
||||
throw new CopyFileException($documentTemplate, $this->tempDocumentFilename);
|
||||
}
|
||||
|
||||
|
||||
// Temporary document content extraction
|
||||
$this->zipClass = new ZipArchive();
|
||||
$this->zipClass->open($this->tempDocumentFilename);
|
||||
$index = 1;
|
||||
while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
|
||||
$this->tempDocumentHeaders[$index] = $this->fixBrokenMacros($this->zipClass->getFromName($this->getHeaderName($index)));
|
||||
$this->tempDocumentHeaders[$index] = $this->fixBrokenMacros(
|
||||
$this->zipClass->getFromName($this->getHeaderName($index))
|
||||
);
|
||||
$index++;
|
||||
}
|
||||
$index = 1;
|
||||
while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
|
||||
$this->tempDocumentFooters[$index] = $this->fixBrokenMacros($this->zipClass->getFromName($this->getFooterName($index)));
|
||||
$this->tempDocumentFooters[$index] = $this->fixBrokenMacros(
|
||||
$this->zipClass->getFromName($this->getFooterName($index))
|
||||
);
|
||||
$index++;
|
||||
}
|
||||
$this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName('word/document.xml'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies XSL style sheet to template's parts.
|
||||
*
|
||||
|
|
@ -109,28 +112,29 @@ class TemplateProcessor
|
|||
*
|
||||
* @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)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param mixed $macro
|
||||
* @param mixed $replace
|
||||
|
|
@ -138,45 +142,47 @@ class TemplateProcessor
|
|||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setValue($macro, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT) {
|
||||
public function setValue($macro, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT)
|
||||
{
|
||||
if (substr($macro, 0, 2) !== '${' && substr($macro, -1) !== '}') {
|
||||
$macro = '${' . $macro . '}';
|
||||
}
|
||||
|
||||
|
||||
if (!String::isUTF8($replace)) {
|
||||
$replace = utf8_encode($replace);
|
||||
}
|
||||
|
||||
|
||||
foreach ($this->tempDocumentHeaders as $index => $headerXML) {
|
||||
$this->tempDocumentHeaders[$index] = $this->setValueForPart($this->tempDocumentHeaders[$index], $macro, $replace, $limit);
|
||||
}
|
||||
|
||||
|
||||
$this->tempDocumentMainPart = $this->setValueForPart($this->tempDocumentMainPart, $macro, $replace, $limit);
|
||||
|
||||
|
||||
foreach ($this->tempDocumentFooters as $index => $headerXML) {
|
||||
$this->tempDocumentFooters[$index] = $this->setValueForPart($this->tempDocumentFooters[$index], $macro, $replace, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns array of all variables in template.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getVariables() {
|
||||
public function getVariables()
|
||||
{
|
||||
$variables = $this->getVariablesForPart($this->tempDocumentMainPart);
|
||||
|
||||
|
||||
foreach ($this->tempDocumentHeaders as $headerXML) {
|
||||
$variables = array_merge($variables, $this->getVariablesForPart($headerXML));
|
||||
}
|
||||
|
||||
|
||||
foreach ($this->tempDocumentFooters as $footerXML) {
|
||||
$variables = array_merge($variables, $this->getVariablesForPart($footerXML));
|
||||
}
|
||||
|
||||
|
||||
return array_unique($variables);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clone a table row in a template document.
|
||||
*
|
||||
|
|
@ -187,55 +193,55 @@ class TemplateProcessor
|
|||
*
|
||||
* @throws \PhpOffice\PhpWord\Exception\Exception
|
||||
*/
|
||||
public function cloneRow($search, $numberOfClones) {
|
||||
public function cloneRow($search, $numberOfClones)
|
||||
{
|
||||
if ('${' !== substr($search, 0, 2) && '}' !== substr($search, -1)) {
|
||||
$search = '${' . $search . '}';
|
||||
}
|
||||
|
||||
|
||||
$tagPos = strpos($this->tempDocumentMainPart, $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('#<w:vMerge w:val="restart"/>#', $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('#<w:vMerge/>#', $tmpXmlRow) && !preg_match('#<w:vMerge w:val="continue" />#', $tmpXmlRow)) {
|
||||
if (!preg_match('#<w:vMerge/>#', $tmpXmlRow) &&
|
||||
!preg_match('#<w:vMerge w:val="continue" />#', $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 .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow);
|
||||
}
|
||||
$result.= $this->getSlice($rowEnd);
|
||||
|
||||
$result .= $this->getSlice($rowEnd);
|
||||
|
||||
$this->tempDocumentMainPart = $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clone a block.
|
||||
*
|
||||
|
|
@ -245,25 +251,34 @@ class TemplateProcessor
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function cloneBlock($blockname, $clones = 1, $replace = true) {
|
||||
public function cloneBlock($blockname, $clones = 1, $replace = true)
|
||||
{
|
||||
$xmlBlock = null;
|
||||
preg_match('/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is', $this->tempDocumentMainPart, $matches);
|
||||
|
||||
preg_match(
|
||||
'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
|
||||
$this->tempDocumentMainPart,
|
||||
$matches
|
||||
);
|
||||
|
||||
if (isset($matches[3])) {
|
||||
$xmlBlock = $matches[3];
|
||||
$cloned = array();
|
||||
for ($i = 1; $i <= $clones; $i++) {
|
||||
$cloned[] = $xmlBlock;
|
||||
}
|
||||
|
||||
|
||||
if ($replace) {
|
||||
$this->tempDocumentMainPart = str_replace($matches[2] . $matches[3] . $matches[4], implode('', $cloned), $this->tempDocumentMainPart);
|
||||
$this->tempDocumentMainPart = str_replace(
|
||||
$matches[2] . $matches[3] . $matches[4],
|
||||
implode('', $cloned),
|
||||
$this->tempDocumentMainPart
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $xmlBlock;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replace a block.
|
||||
*
|
||||
|
|
@ -272,14 +287,23 @@ class TemplateProcessor
|
|||
*
|
||||
* @return void
|
||||
*/
|
||||
public function replaceBlock($blockname, $replacement) {
|
||||
preg_match('/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is', $this->tempDocumentMainPart, $matches);
|
||||
|
||||
public function replaceBlock($blockname, $replacement)
|
||||
{
|
||||
preg_match(
|
||||
'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
|
||||
$this->tempDocumentMainPart,
|
||||
$matches
|
||||
);
|
||||
|
||||
if (isset($matches[3])) {
|
||||
$this->tempDocumentMainPart = str_replace($matches[2] . $matches[3] . $matches[4], $replacement, $this->tempDocumentMainPart);
|
||||
$this->tempDocumentMainPart = str_replace(
|
||||
$matches[2] . $matches[3] . $matches[4],
|
||||
$replacement,
|
||||
$this->tempDocumentMainPart
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a block of text.
|
||||
*
|
||||
|
|
@ -287,10 +311,11 @@ class TemplateProcessor
|
|||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteBlock($blockname) {
|
||||
public function deleteBlock($blockname)
|
||||
{
|
||||
$this->replaceBlock($blockname, '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves the result document.
|
||||
*
|
||||
|
|
@ -298,25 +323,26 @@ class TemplateProcessor
|
|||
*
|
||||
* @throws \PhpOffice\PhpWord\Exception\Exception
|
||||
*/
|
||||
public function save() {
|
||||
public function save()
|
||||
{
|
||||
foreach ($this->tempDocumentHeaders as $index => $headerXML) {
|
||||
$this->zipClass->addFromString($this->getHeaderName($index), $this->tempDocumentHeaders[$index]);
|
||||
}
|
||||
|
||||
|
||||
$this->zipClass->addFromString('word/document.xml', $this->tempDocumentMainPart);
|
||||
|
||||
|
||||
foreach ($this->tempDocumentFooters as $index => $headerXML) {
|
||||
$this->zipClass->addFromString($this->getFooterName($index), $this->tempDocumentFooters[$index]);
|
||||
}
|
||||
|
||||
|
||||
// Close zip file
|
||||
if (false === $this->zipClass->close()) {
|
||||
throw new Exception('Could not close zip file.');
|
||||
}
|
||||
|
||||
|
||||
return $this->tempDocumentFilename;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves the result document to the user defined file.
|
||||
*
|
||||
|
|
@ -326,23 +352,24 @@ class TemplateProcessor
|
|||
*
|
||||
* @return void
|
||||
*/
|
||||
public function saveAs($fileName) {
|
||||
public function saveAs($fileName)
|
||||
{
|
||||
$tempFileName = $this->save();
|
||||
|
||||
|
||||
if (file_exists($fileName)) {
|
||||
unlink($fileName);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Note: we do not use ``rename`` function here, because it looses file ownership data on Windows platform.
|
||||
* As a result, user cannot open the file directly getting "Access denied" message.
|
||||
*
|
||||
* @see https://github.com/PHPOffice/PHPWord/issues/532
|
||||
*/
|
||||
*/
|
||||
copy($tempFileName, $fileName);
|
||||
unlink($tempFileName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds parts of broken macros and sticks them together.
|
||||
* Macros, while being edited, could be implicitly broken by some of the word processors.
|
||||
|
|
@ -353,39 +380,43 @@ class TemplateProcessor
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function fixBrokenMacros($documentPart) {
|
||||
protected function fixBrokenMacros($documentPart)
|
||||
{
|
||||
$fixedDocumentPart = $documentPart;
|
||||
|
||||
$fixedDocumentPart = preg_replace_callback('|\$\{([^\}]+)\}|U', function ($match) {
|
||||
return strip_tags($match[0]);
|
||||
}, $fixedDocumentPart);
|
||||
|
||||
|
||||
$fixedDocumentPart = preg_replace_callback(
|
||||
'|\$\{([^\}]+)\}|U',
|
||||
function ($match) {
|
||||
return strip_tags($match[0]);
|
||||
},
|
||||
$fixedDocumentPart
|
||||
);
|
||||
|
||||
return $fixedDocumentPart;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find and replace macros in the given XML section.
|
||||
*
|
||||
* @param string $documentPartXML
|
||||
* @param string $searchP
|
||||
* @param string $search
|
||||
* @param string $replace
|
||||
* @param integer $limit
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function setValueForPart($documentPartXML, $search, $replace, $limit) {
|
||||
|
||||
protected function setValueForPart($documentPartXML, $search, $replace, $limit)
|
||||
{
|
||||
// Note: we can't use the same function for both cases here, because of performance considerations.
|
||||
if (self::MAXIMUM_REPLACEMENTS_DEFAULT === $limit) {
|
||||
return str_replace($search, $replace, $documentPartXML);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$regExpDelim = '/';
|
||||
$escapedSearch = preg_quote($search, $regExpDelim);
|
||||
return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find all variables in $documentPartXML.
|
||||
*
|
||||
|
|
@ -393,12 +424,13 @@ class TemplateProcessor
|
|||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getVariablesForPart($documentPartXML) {
|
||||
protected function getVariablesForPart($documentPartXML)
|
||||
{
|
||||
preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
|
||||
|
||||
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the name of the footer file for $index.
|
||||
*
|
||||
|
|
@ -406,10 +438,11 @@ class TemplateProcessor
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFooterName($index) {
|
||||
protected function getFooterName($index)
|
||||
{
|
||||
return sprintf('word/footer%d.xml', $index);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the name of the header file for $index.
|
||||
*
|
||||
|
|
@ -417,10 +450,11 @@ class TemplateProcessor
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getHeaderName($index) {
|
||||
protected function getHeaderName($index)
|
||||
{
|
||||
return sprintf('word/header%d.xml', $index);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the start position of the nearest table row before $offset.
|
||||
*
|
||||
|
|
@ -430,19 +464,20 @@ class TemplateProcessor
|
|||
*
|
||||
* @throws \PhpOffice\PhpWord\Exception\Exception
|
||||
*/
|
||||
protected function findRowStart($offset) {
|
||||
protected function findRowStart($offset)
|
||||
{
|
||||
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tr ', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
|
||||
|
||||
|
||||
if (!$rowStart) {
|
||||
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tr>', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
|
||||
}
|
||||
if (!$rowStart) {
|
||||
throw new Exception('Can not find the start position of the row to clone.');
|
||||
}
|
||||
|
||||
|
||||
return $rowStart;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the end position of the nearest table row after $offset.
|
||||
*
|
||||
|
|
@ -450,10 +485,11 @@ class TemplateProcessor
|
|||
*
|
||||
* @return integer
|
||||
*/
|
||||
protected function findRowEnd($offset) {
|
||||
protected function findRowEnd($offset)
|
||||
{
|
||||
return strpos($this->tempDocumentMainPart, '</w:tr>', $offset) + 7;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a slice of a string.
|
||||
*
|
||||
|
|
@ -462,11 +498,12 @@ class TemplateProcessor
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getSlice($startPosition, $endPosition = 0) {
|
||||
protected function getSlice($startPosition, $endPosition = 0)
|
||||
{
|
||||
if (!$endPosition) {
|
||||
$endPosition = strlen($this->tempDocumentMainPart);
|
||||
}
|
||||
|
||||
|
||||
return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue