diff --git a/CHANGELOG.md b/CHANGELOG.md index 6040c46e..f42d8104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ This release added form fields (textinput, checkbox, and dropdown), drawing shap - `Element\Section::getSettings()` and `Element\Section::setSettings()` replaced by `Element\Section::getStyle()` and `Element\Section::setStyle()` - `Shared\Drawing` and `Shared\Font` merged into `Shared\Converter` - `DocumentProperties` replaced by `Metadata\DocInfo` +- `Template` replaced by `TemplateProcessor` +- `PhpWord->loadTemplate($filename)` ### Miscellaneous @@ -49,6 +51,7 @@ This release added form fields (textinput, checkbox, and dropdown), drawing shap - Element: Refactor elements to move set relation Id from container to element - @ivanlanin - Introduced CreateTemporaryFileException, CopyFileException - @RomanSyroeshko - Settings: added method to set user defined temporary directory - @RomanSyroeshko GH-310 +- Renamed `Template` into `TemplateProcessor` - @RomanSyroeshko GH-216 ## 0.11.1 - 2 June 2014 diff --git a/composer.json b/composer.json index 74009e3a..b55d8340 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "phpoffice/phpword", "description": "PHPWord - A pure PHP library for reading and writing word processing documents (DOCX, ODT, RTF, HTML, PDF)", "keywords": [ - "PHP", "PhpOffice", "office", "PhpWord", "word", "template", "reader", "writer", + "PHP", "PhpOffice", "office", "PhpWord", "word", "template", "template processor", "reader", "writer", "docx", "OOXML", "OpenXML", "Office Open XML", "ISO IEC 29500", "WordprocessingML", "RTF", "Rich Text Format", "doc", "odt", "OpenDocument", "PDF", "HTML" ], @@ -50,7 +50,7 @@ "ext-zip": "Used to write DOCX and ODT", "ext-gd2": "Used to add images", "ext-xmlwriter": "Used to write DOCX and ODT", - "ext-xsl": "Used to apply XSL style sheet to template part", + "ext-xsl": "Used to apply XSL style sheet to main document part of OOXML template", "dompdf/dompdf": "Used to write PDF" }, "autoload": { diff --git a/docs/index.rst b/docs/index.rst index 4f200cca..671c32a6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,7 +23,7 @@ Format (RTF). containers elements styles - templates + templates-processing writersreaders recipes faq diff --git a/docs/src/documentation.md b/docs/src/documentation.md index 9c0c865b..3e7d41b0 100644 --- a/docs/src/documentation.md +++ b/docs/src/documentation.md @@ -44,7 +44,7 @@ Don't forget to change `code::` directive to `code-block::` in the resulting rst - [Font](#font) - [Paragraph](#paragraph) - [Table](#table) -- [Templates](#templates) +- [Templates processing](#templates-processing) - [Writers & readers](#writers-readers) - [OOXML](#ooxml) - [OpenDocument](#opendocument) @@ -873,21 +873,25 @@ Available image styles: - `font` Font name - `hint` See font style -# Templates +# Templates processing -You can create a docx template with included search-patterns that can be replaced by any value you wish. Only single-line values can be replaced. To load a template file, use the `loadTemplate` method. After loading the docx template, you can use the `setValue` method to change the value of a search pattern. The search-pattern model is: `${search-pattern}`. It is not possible to add new PHPWord elements to a loaded template file. +You can create a .docx document template with included search-patterns which can be replaced by any value you wish. Only single-line values can be replaced. + +To deal with a template file, use `new TemplateProcessor` statement. After TemplateProcessor instance creation the document template is copied into the temporary directory. Then you can use `TemplateProcessor::setValue` method to change the value of a search pattern. The search-pattern model is: `${search-pattern}`. Example: ```php -$template = $phpWord->loadTemplate('Template.docx'); -$template->setValue('Name', 'Somebody someone'); -$template->setValue('Street', 'Coming-Undone-Street 32'); +$templateProcessor = new TemplateProcessor('Template.docx'); +$templateProcessor->setValue('Name', 'Somebody someone'); +$templateProcessor->setValue('Street', 'Coming-Undone-Street 32'); ``` -See `Sample_07_TemplateCloneRow.php` for example on how to create multirow from a single row in a template by using `cloneRow`. +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`). -See `Sample_23_TemplateBlock.php` for example on how to clone a block of text using `cloneBlock` and delete a block of text using `deleteBlock`. +See `Sample_07_TemplateCloneRow.php` for example on how to create multirow from a single row in a template by using `TemplateProcessor::cloneRow`. + +See `Sample_23_TemplateBlock.php` for example on how to clone a block of text using `TemplateProcessor::cloneBlock` and delete a block of text using `TemplateProcessor::deleteBlock`. # Writers & readers diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst new file mode 100644 index 00000000..6a65ea0d --- /dev/null +++ b/docs/templates-processing.rst @@ -0,0 +1,25 @@ +.. _templates-processing: + +Templates processing +==================== + +You can create a .docx document template with included search-patterns which can be replaced by any value you wish. Only single-line values can be replaced. + +To deal with a template file, use ``new TemplateProcessor`` statement. After TemplateProcessor instance creation the document template is copied into the temporary directory. Then you can use ``TemplateProcessor::setValue`` method to change the value of a search pattern. The search-pattern model is: ``${search-pattern}``. + +Example: + +.. code-block:: php + + $templateProcessor = new TemplateProcessor('Template.docx'); + $templateProcessor->setValue('Name', 'Somebody someone'); + $templateProcessor->setValue('Street', 'Coming-Undone-Street 32'); + +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``). + +See ``Sample_07_TemplateCloneRow.php`` for example on how to create +multirow from a single row in a template by using ``TemplateProcessor::cloneRow``. + +See ``Sample_23_TemplateBlock.php`` for example on how to clone a block +of text using ``TemplateProcessor::cloneBlock`` and delete a block of text using +``TemplateProcessor::deleteBlock``. diff --git a/docs/templates.rst b/docs/templates.rst deleted file mode 100644 index b1d9d205..00000000 --- a/docs/templates.rst +++ /dev/null @@ -1,27 +0,0 @@ -.. _templates: - -Templates -========= - -You can create a docx template with included search-patterns that can be -replaced by any value you wish. Only single-line values can be replaced. -To load a template file, use the ``loadTemplate`` method. After loading -the docx template, you can use the ``setValue`` method to change the -value of a search pattern. The search-pattern model is: -``${search-pattern}``. It is not possible to add new PHPWord elements to -a loaded template file. - -Example: - -.. code-block:: php - - $template = $phpWord->loadTemplate('Template.docx'); - $template->setValue('Name', 'Somebody someone'); - $template->setValue('Street', 'Coming-Undone-Street 32'); - -See ``Sample_07_TemplateCloneRow.php`` for example on how to create -multirow from a single row in a template by using ``cloneRow``. - -See ``Sample_23_TemplateBlock.php`` for example on how to clone a block -of text using ``cloneBlock`` and delete a block of text using -``deleteBlock``. diff --git a/samples/Sample_07_TemplateCloneRow.php b/samples/Sample_07_TemplateCloneRow.php index 899bc82b..0712ddfc 100644 --- a/samples/Sample_07_TemplateCloneRow.php +++ b/samples/Sample_07_TemplateCloneRow.php @@ -1,64 +1,60 @@ loadTemplate('resources/Sample_07_TemplateCloneRow.docx'); +// Template processor instance creation +echo date('H:i:s') , ' Creating new TemplateProcessor instance...' , EOL; +$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor('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', realpath(__DIR__)); // On header +$templateProcessor->setValue('weekday', date('l')); // On section/content +$templateProcessor->setValue('time', date('H:i')); // On footer +$templateProcessor->setValue('serverName', realpath(__DIR__)); // On header // Simple table -$document->cloneRow('rowValue', 10); +$templateProcessor->cloneRow('rowValue', 10); -$document->setValue('rowValue#1', 'Sun'); -$document->setValue('rowValue#2', 'Mercury'); -$document->setValue('rowValue#3', 'Venus'); -$document->setValue('rowValue#4', 'Earth'); -$document->setValue('rowValue#5', 'Mars'); -$document->setValue('rowValue#6', 'Jupiter'); -$document->setValue('rowValue#7', 'Saturn'); -$document->setValue('rowValue#8', 'Uranus'); -$document->setValue('rowValue#9', 'Neptun'); -$document->setValue('rowValue#10', 'Pluto'); +$templateProcessor->setValue('rowValue#1', 'Sun'); +$templateProcessor->setValue('rowValue#2', 'Mercury'); +$templateProcessor->setValue('rowValue#3', 'Venus'); +$templateProcessor->setValue('rowValue#4', 'Earth'); +$templateProcessor->setValue('rowValue#5', 'Mars'); +$templateProcessor->setValue('rowValue#6', 'Jupiter'); +$templateProcessor->setValue('rowValue#7', 'Saturn'); +$templateProcessor->setValue('rowValue#8', 'Uranus'); +$templateProcessor->setValue('rowValue#9', 'Neptun'); +$templateProcessor->setValue('rowValue#10', 'Pluto'); -$document->setValue('rowNumber#1', '1'); -$document->setValue('rowNumber#2', '2'); -$document->setValue('rowNumber#3', '3'); -$document->setValue('rowNumber#4', '4'); -$document->setValue('rowNumber#5', '5'); -$document->setValue('rowNumber#6', '6'); -$document->setValue('rowNumber#7', '7'); -$document->setValue('rowNumber#8', '8'); -$document->setValue('rowNumber#9', '9'); -$document->setValue('rowNumber#10', '10'); +$templateProcessor->setValue('rowNumber#1', '1'); +$templateProcessor->setValue('rowNumber#2', '2'); +$templateProcessor->setValue('rowNumber#3', '3'); +$templateProcessor->setValue('rowNumber#4', '4'); +$templateProcessor->setValue('rowNumber#5', '5'); +$templateProcessor->setValue('rowNumber#6', '6'); +$templateProcessor->setValue('rowNumber#7', '7'); +$templateProcessor->setValue('rowNumber#8', '8'); +$templateProcessor->setValue('rowNumber#9', '9'); +$templateProcessor->setValue('rowNumber#10', '10'); // Table with a spanned cell -$document->cloneRow('userId', 3); +$templateProcessor->cloneRow('userId', 3); -$document->setValue('userId#1', '1'); -$document->setValue('userFirstName#1', 'James'); -$document->setValue('userName#1', 'Taylor'); -$document->setValue('userPhone#1', '+1 428 889 773'); +$templateProcessor->setValue('userId#1', '1'); +$templateProcessor->setValue('userFirstName#1', 'James'); +$templateProcessor->setValue('userName#1', 'Taylor'); +$templateProcessor->setValue('userPhone#1', '+1 428 889 773'); -$document->setValue('userId#2', '2'); -$document->setValue('userFirstName#2', 'Robert'); -$document->setValue('userName#2', 'Bell'); -$document->setValue('userPhone#2', '+1 428 889 774'); +$templateProcessor->setValue('userId#2', '2'); +$templateProcessor->setValue('userFirstName#2', 'Robert'); +$templateProcessor->setValue('userName#2', 'Bell'); +$templateProcessor->setValue('userPhone#2', '+1 428 889 774'); -$document->setValue('userId#3', '3'); -$document->setValue('userFirstName#3', 'Michael'); -$document->setValue('userName#3', 'Ray'); -$document->setValue('userPhone#3', '+1 428 889 775'); +$templateProcessor->setValue('userId#3', '3'); +$templateProcessor->setValue('userFirstName#3', 'Michael'); +$templateProcessor->setValue('userName#3', 'Ray'); +$templateProcessor->setValue('userPhone#3', '+1 428 889 775'); -$name = 'Sample_07_TemplateCloneRow.docx'; -echo date('H:i:s'), " Write to Word2007 format", EOL; -$document->saveAs($name); -rename($name, "results/{$name}"); +echo date('H:i:s'), ' Saving the result document...', EOL; +$templateProcessor->saveAs('results/Sample_07_TemplateCloneRow.docx'); echo getEndingNotes(array('Word2007' => 'docx')); if (!CLI) { diff --git a/samples/Sample_23_TemplateBlock.php b/samples/Sample_23_TemplateBlock.php index 8ee8fc6d..2b7e9f68 100644 --- a/samples/Sample_23_TemplateBlock.php +++ b/samples/Sample_23_TemplateBlock.php @@ -1,22 +1,18 @@ loadTemplate('resources/Sample_23_TemplateBlock.docx'); +// Template processor instance creation +echo date('H:i:s') , ' Creating new TemplateProcessor instance...' , EOL; +$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor('resources/Sample_23_TemplateBlock.docx'); // Will clone everything between ${tag} and ${/tag}, the number of times. By default, 1. -$document->cloneBlock('CLONEME', 3); +$templateProcessor->cloneBlock('CLONEME', 3); // Everything between ${tag} and ${/tag}, will be deleted/erased. -$document->deleteBlock('DELETEME'); +$templateProcessor->deleteBlock('DELETEME'); -$name = 'Sample_23_TemplateBlock.docx'; -echo date('H:i:s'), " Write to Word2007 format", EOL; -$document->saveAs($name); -rename($name, "results/{$name}"); +echo date('H:i:s'), ' Saving the result document...', EOL; +$templateProcessor->saveAs('results/Sample_23_TemplateBlock.docx'); echo getEndingNotes(array('Word2007' => 'docx')); if (!CLI) { diff --git a/src/PhpWord/PhpWord.php b/src/PhpWord/PhpWord.php index 84e5ebc7..aba1ee38 100644 --- a/src/PhpWord/PhpWord.php +++ b/src/PhpWord/PhpWord.php @@ -264,14 +264,16 @@ class PhpWord /** * Load template by filename * + * @deprecated 0.12.0 Use `new TemplateProcessor($documentTemplate)` instead. + * * @param string $filename Fully qualified filename. - * @return Template + * @return TemplateProcessor * @throws \PhpOffice\PhpWord\Exception\Exception */ public function loadTemplate($filename) { if (file_exists($filename)) { - return new Template($filename); + return new TemplateProcessor($filename); } else { throw new Exception("Template file {$filename} not found."); } diff --git a/src/PhpWord/Template.php b/src/PhpWord/Template.php index c8f88026..dbbb54f1 100644 --- a/src/PhpWord/Template.php +++ b/src/PhpWord/Template.php @@ -17,449 +17,10 @@ namespace PhpOffice\PhpWord; -use PhpOffice\PhpWord\Exception\CopyFileException; -use PhpOffice\PhpWord\Exception\CreateTemporaryFileException; -use PhpOffice\PhpWord\Exception\Exception; -use PhpOffice\PhpWord\Shared\String; -use PhpOffice\PhpWord\Shared\ZipArchive; - /** - * Template + * @deprecated 0.12.0 Use \PhpOffice\PhpWord\TemplateProcessor instead. */ -class Template +class Template extends TemplateProcessor { - /** - * ZipArchive object. - * - * @var mixed - */ - private $zipClass; - /** - * Temporary file name. - * - * @var string - */ - private $tempFileName; - - /** - * Document XML. - * - * @var string - */ - private $documentXML; - - /** - * Document header XML. - * - * @var string[] - */ - private $headerXMLs = array(); - - /** - * Create a new Template Object. - * - * @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception. - * - * @param string $fileName The fully qualified template file name. - * @throws \PhpOffice\PhpWord\Exception\CreateTemporaryFileException - * @throws \PhpOffice\PhpWord\Exception\CopyFileException - */ - public function __construct($fileName) - { - $this->tempFileName = tempnam(Settings::getTempDir(), 'PhpWord'); - if (false === $this->tempFileName) { - throw new CreateTemporaryFileException(); - } - - // Copy the source File to the temp File - if (false === copy($fileName, $this->tempFileName)) { - throw new CopyFileException($fileName, $this->tempFileName); - } - - $this->zipClass = new ZipArchive(); - $this->zipClass->open($this->tempFileName); - - // Find and load headers and footers - $index = 1; - while ($this->zipClass->locateName($this->getHeaderName($index)) !== false) { - $this->headerXMLs[$index] = $this->zipClass->getFromName($this->getHeaderName($index)); - $index++; - } - - $index = 1; - while ($this->zipClass->locateName($this->getFooterName($index)) !== false) { - $this->footerXMLs[$index] = $this->zipClass->getFromName($this->getFooterName($index)); - $index++; - } - - $this->documentXML = $this->zipClass->getFromName('word/document.xml'); - } - - /** - * Document footer XML. - * - * @var string[] - */ - private $footerXMLs = array(); - - /** - * Applies XSL style sheet to template's parts. - * - * @param \DOMDocument $xslDOMDocument - * @param array $xslOptions - * @param string $xslOptionsURI - * @return void - * @throws \PhpOffice\PhpWord\Exception\Exception - */ - public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '') - { - $processor = new \XSLTProcessor(); - - $processor->importStylesheet($xslDOMDocument); - - if (false === $processor->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->documentXML)) { - throw new Exception('Could not load XML from the given template.'); - } - - $xmlTransformed = $processor->transformToXml($xmlDOMDocument); - if (false === $xmlTransformed) { - throw new Exception('Could not transform the given XML document.'); - } - - $this->documentXML = $xmlTransformed; - } - - /** - * Set a Template value. - * - * @param mixed $search - * @param mixed $replace - * @param integer $limit - * @return void - */ - 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 integer $numberOfClones - * @return void - * @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; - } - - /** - * Clone a block. - * - * @param string $blockname - * @param integer $clones - * @param boolean $replace - * @return string|null - */ - public function cloneBlock($blockname, $clones = 1, $replace = true) - { - $xmlBlock = null; - preg_match( - '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', - $this->documentXML, - $matches - ); - - if (isset($matches[3])) { - $xmlBlock = $matches[3]; - $cloned = array(); - for ($i = 1; $i <= $clones; $i++) { - $cloned[] = $xmlBlock; - } - - if ($replace) { - $this->documentXML = str_replace( - $matches[2] . $matches[3] . $matches[4], - implode('', $cloned), - $this->documentXML - ); - } - } - - return $xmlBlock; - } - - /** - * Replace a block. - * - * @param string $blockname - * @param string $replacement - * @return void - */ - public function replaceBlock($blockname, $replacement) - { - preg_match( - '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', - $this->documentXML, - $matches - ); - - if (isset($matches[3])) { - $this->documentXML = str_replace( - $matches[2] . $matches[3] . $matches[4], - $replacement, - $this->documentXML - ); - } - } - - /** - * Delete a block of text. - * - * @param string $blockname - * @return void - */ - public function deleteBlock($blockname) - { - $this->replaceBlock($blockname, ''); - } - - /** - * Save XML to temporary file. - * - * @return string - * @throws \PhpOffice\PhpWord\Exception\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 (false === $this->zipClass->close()) { - throw new Exception('Could not close zip file.'); - } - - return $this->tempFileName; - } - - /** - * Save XML to defined name. - * - * @since 0.8.0 - * - * @param string $fileName - * @return void - */ - public function saveAs($fileName) - { - $tempFileName = $this->save(); - - if (file_exists($fileName)) { - unlink($fileName); - } - - rename($tempFileName, $fileName); - } - - /** - * Find and replace placeholders in the given XML section. - * - * @param string $documentPartXML - * @param string $search - * @param string $replace - * @param integer $limit - * @return string - */ - protected function setValueForPart($documentPartXML, $search, $replace, $limit) - { - $pattern = '|\$\{([^\}]+)\}|U'; - preg_match_all($pattern, $documentPartXML, $matches); - foreach ($matches[0] as $value) { - $valueCleaned = preg_replace('/<[^>]+>/', '', $value); - $valueCleaned = preg_replace('/<\/[^>]+>/', '', $valueCleaned); - $documentPartXML = str_replace($value, $valueCleaned, $documentPartXML); - } - - if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') { - $search = '${' . $search . '}'; - } - - if (!String::isUTF8($replace)) { - $replace = utf8_encode($replace); - } - $replace = htmlspecialchars($replace); - - $regExpDelim = '/'; - $escapedSearch = preg_quote($search, $regExpDelim); - return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit); - } - - /** - * Find all variables in $documentPartXML. - * - * @param string $documentPartXML - * @return string[] - */ - protected function getVariablesForPart($documentPartXML) - { - 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 integer $offset - * @return integer - * @throws \PhpOffice\PhpWord\Exception\Exception - */ - private function findRowStart($offset) - { - $rowStart = strrpos($this->documentXML, "documentXML) - $offset) * -1)); - if (!$rowStart) { - $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."); - } - return $rowStart; - } - - /** - * Find the end position of the nearest table row after $offset. - * - * @param integer $offset - * @return integer - */ - private function findRowEnd($offset) - { - $rowEnd = strpos($this->documentXML, "", $offset) + 7; - return $rowEnd; - } - - /** - * Get a slice of a string. - * - * @param integer $startPosition - * @param integer $endPosition - * @return string - */ - private function getSlice($startPosition, $endPosition = 0) - { - if (!$endPosition) { - $endPosition = strlen($this->documentXML); - } - return substr($this->documentXML, $startPosition, ($endPosition - $startPosition)); - } -} +} \ No newline at end of file diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php new file mode 100644 index 00000000..1de520f6 --- /dev/null +++ b/src/PhpWord/TemplateProcessor.php @@ -0,0 +1,456 @@ +temporaryDocumentFilename = tempnam(Settings::getTempDir(), 'PhpWord'); + if (false === $this->temporaryDocumentFilename) { + throw new CreateTemporaryFileException(); + } + + // Template file cloning + if (false === copy($documentTemplate, $this->temporaryDocumentFilename)) { + throw new CopyFileException($documentTemplate, $this->temporaryDocumentFilename); + } + + // Temporary document content extraction + $this->zipClass = new ZipArchive(); + $this->zipClass->open($this->temporaryDocumentFilename); + $index = 1; + while ($this->zipClass->locateName($this->getHeaderName($index)) !== false) { + $this->temporaryDocumentHeaders[$index] = $this->zipClass->getFromName($this->getHeaderName($index)); + $index++; + } + $index = 1; + while ($this->zipClass->locateName($this->getFooterName($index)) !== false) { + $this->temporaryDocumentFooters[$index] = $this->zipClass->getFromName($this->getFooterName($index)); + $index++; + } + $this->temporaryDocumentMainPart = $this->zipClass->getFromName('word/document.xml'); + } + + /** + * Applies XSL style sheet to template's parts. + * + * @param \DOMDocument $xslDOMDocument + * @param array $xslOptions + * @param string $xslOptionsURI + * @return void + * @throws \PhpOffice\PhpWord\Exception\Exception + */ + 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->temporaryDocumentMainPart)) { + 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->temporaryDocumentMainPart = $xmlTransformed; + } + + /** + * @param mixed $search + * @param mixed $replace + * @param integer $limit + * @return void + */ + public function setValue($search, $replace, $limit = -1) + { + foreach ($this->temporaryDocumentHeaders as $index => $headerXML) { + $this->temporaryDocumentHeaders[$index] = $this->setValueForPart($this->temporaryDocumentHeaders[$index], $search, $replace, $limit); + } + + $this->temporaryDocumentMainPart = $this->setValueForPart($this->temporaryDocumentMainPart, $search, $replace, $limit); + + foreach ($this->temporaryDocumentFooters as $index => $headerXML) { + $this->temporaryDocumentFooters[$index] = $this->setValueForPart($this->temporaryDocumentFooters[$index], $search, $replace, $limit); + } + } + + /** + * Returns array of all variables in template. + * + * @return string[] + */ + public function getVariables() + { + $variables = $this->getVariablesForPart($this->temporaryDocumentMainPart); + + foreach ($this->temporaryDocumentHeaders as $headerXML) { + $variables = array_merge($variables, $this->getVariablesForPart($headerXML)); + } + + foreach ($this->temporaryDocumentFooters 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 integer $numberOfClones + * @return void + * @throws \PhpOffice\PhpWord\Exception\Exception + */ + public function cloneRow($search, $numberOfClones) + { + if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') { + $search = '${' . $search . '}'; + } + + $tagPos = strpos($this->temporaryDocumentMainPart, $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->temporaryDocumentMainPart = $result; + } + + /** + * Clone a block. + * + * @param string $blockname + * @param integer $clones + * @param boolean $replace + * @return string|null + */ + public function cloneBlock($blockname, $clones = 1, $replace = true) + { + $xmlBlock = null; + preg_match( + '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', + $this->temporaryDocumentMainPart, + $matches + ); + + if (isset($matches[3])) { + $xmlBlock = $matches[3]; + $cloned = array(); + for ($i = 1; $i <= $clones; $i++) { + $cloned[] = $xmlBlock; + } + + if ($replace) { + $this->temporaryDocumentMainPart = str_replace( + $matches[2] . $matches[3] . $matches[4], + implode('', $cloned), + $this->temporaryDocumentMainPart + ); + } + } + + return $xmlBlock; + } + + /** + * Replace a block. + * + * @param string $blockname + * @param string $replacement + * @return void + */ + public function replaceBlock($blockname, $replacement) + { + preg_match( + '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', + $this->temporaryDocumentMainPart, + $matches + ); + + if (isset($matches[3])) { + $this->temporaryDocumentMainPart = str_replace( + $matches[2] . $matches[3] . $matches[4], + $replacement, + $this->temporaryDocumentMainPart + ); + } + } + + /** + * Delete a block of text. + * + * @param string $blockname + * @return void + */ + public function deleteBlock($blockname) + { + $this->replaceBlock($blockname, ''); + } + + /** + * Saves the result document. + * + * @return string + * @throws \PhpOffice\PhpWord\Exception\Exception + */ + public function save() + { + foreach ($this->temporaryDocumentHeaders as $index => $headerXML) { + $this->zipClass->addFromString($this->getHeaderName($index), $this->temporaryDocumentHeaders[$index]); + } + + $this->zipClass->addFromString('word/document.xml', $this->temporaryDocumentMainPart); + + foreach ($this->temporaryDocumentFooters as $index => $headerXML) { + $this->zipClass->addFromString($this->getFooterName($index), $this->temporaryDocumentFooters[$index]); + } + + // Close zip file + if (false === $this->zipClass->close()) { + throw new Exception('Could not close zip file.'); + } + + return $this->temporaryDocumentFilename; + } + + /** + * Saves the result document to the user defined file. + * + * @since 0.8.0 + * + * @param string $fileName + * @return void + */ + public function saveAs($fileName) + { + $tempFileName = $this->save(); + + if (file_exists($fileName)) { + unlink($fileName); + } + + rename($tempFileName, $fileName); + } + + /** + * Find and replace placeholders in the given XML section. + * + * @param string $documentPartXML + * @param string $search + * @param string $replace + * @param integer $limit + * @return string + */ + protected function setValueForPart($documentPartXML, $search, $replace, $limit) + { + $pattern = '|\$\{([^\}]+)\}|U'; + preg_match_all($pattern, $documentPartXML, $matches); + foreach ($matches[0] as $value) { + $valueCleaned = preg_replace('/<[^>]+>/', '', $value); + $valueCleaned = preg_replace('/<\/[^>]+>/', '', $valueCleaned); + $documentPartXML = str_replace($value, $valueCleaned, $documentPartXML); + } + + if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') { + $search = '${' . $search . '}'; + } + + if (!String::isUTF8($replace)) { + $replace = utf8_encode($replace); + } + $replace = htmlspecialchars($replace); + + $regExpDelim = '/'; + $escapedSearch = preg_quote($search, $regExpDelim); + return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit); + } + + /** + * Find all variables in $documentPartXML. + * + * @param string $documentPartXML + * @return string[] + */ + protected function getVariablesForPart($documentPartXML) + { + 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 integer $offset + * @return integer + * @throws \PhpOffice\PhpWord\Exception\Exception + */ + private function findRowStart($offset) + { + $rowStart = strrpos($this->temporaryDocumentMainPart, 'temporaryDocumentMainPart) - $offset) * -1)); + + if (!$rowStart) { + $rowStart = strrpos($this->temporaryDocumentMainPart, '', ((strlen($this->temporaryDocumentMainPart) - $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. + * + * @param integer $offset + * @return integer + */ + private function findRowEnd($offset) + { + return strpos($this->temporaryDocumentMainPart, '', $offset) + 7; + } + + /** + * Get a slice of a string. + * + * @param integer $startPosition + * @param integer $endPosition + * @return string + */ + private function getSlice($startPosition, $endPosition = 0) + { + if (!$endPosition) { + $endPosition = strlen($this->temporaryDocumentMainPart); + } + + return substr($this->temporaryDocumentMainPart, $startPosition, ($endPosition - $startPosition)); + } +} diff --git a/tests/PhpWord/Tests/PhpWordTest.php b/tests/PhpWord/Tests/PhpWordTest.php index 756f848f..85c6a7f2 100644 --- a/tests/PhpWord/Tests/PhpWordTest.php +++ b/tests/PhpWord/Tests/PhpWordTest.php @@ -119,6 +119,8 @@ class PhpWordTest extends \PHPUnit_Framework_TestCase /** * Test load template + * + * @deprecated 0.12.0 */ public function testLoadTemplate() { @@ -126,7 +128,7 @@ class PhpWordTest extends \PHPUnit_Framework_TestCase $phpWord = new PhpWord(); $this->assertInstanceOf( - 'PhpOffice\\PhpWord\\Template', + 'PhpOffice\\PhpWord\\TemplateProcessor', $phpWord->loadTemplate($templateFqfn) ); } @@ -134,6 +136,8 @@ class PhpWordTest extends \PHPUnit_Framework_TestCase /** * Test load template exception * + * @deprecated 0.12.0 + * * @expectedException \PhpOffice\PhpWord\Exception\Exception */ public function testLoadTemplateException() diff --git a/tests/PhpWord/Tests/TemplateTest.php b/tests/PhpWord/Tests/TemplateProcessorTest.php similarity index 64% rename from tests/PhpWord/Tests/TemplateTest.php rename to tests/PhpWord/Tests/TemplateProcessorTest.php index 57ee229e..04d1e777 100644 --- a/tests/PhpWord/Tests/TemplateTest.php +++ b/tests/PhpWord/Tests/TemplateProcessorTest.php @@ -17,35 +17,33 @@ namespace PhpOffice\PhpWord\Tests; -use PhpOffice\PhpWord\Template; +use PhpOffice\PhpWord\TemplateProcessor; /** - * Test class for PhpOffice\PhpWord\Template - * - * @covers \PhpOffice\PhpWord\Template - * @coversDefaultClass \PhpOffice\PhpWord\Template + * @covers \PhpOffice\PhpWord\TemplateProcessor + * @coversDefaultClass \PhpOffice\PhpWord\TemplateProcessor * @runTestsInSeparateProcesses */ -final class TemplateTest extends \PHPUnit_Framework_TestCase +final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase { /** - * Template can be saved in temporary location + * Template can be saved in temporary location. * * @covers ::save * @test */ final public function testTemplateCanBeSavedInTemporaryLocation() { - $templateFqfn = __DIR__ . "/_files/templates/with_table_macros.docx"; + $templateFqfn = __DIR__ . '/_files/templates/with_table_macros.docx'; - $document = new Template($templateFqfn); + $templateProcessor = new TemplateProcessor($templateFqfn); $xslDOMDocument = new \DOMDocument(); $xslDOMDocument->load(__DIR__ . "/_files/xsl/remove_tables_by_needle.xsl"); foreach (array('${employee.', '${scoreboard.') as $needle) { - $document->applyXslStyleSheet($xslDOMDocument, array('needle' => $needle)); + $templateProcessor->applyXslStyleSheet($xslDOMDocument, array('needle' => $needle)); } - $documentFqfn = $document->save(); + $documentFqfn = $templateProcessor->save(); $this->assertNotEmpty($documentFqfn, 'FQFN of the saved document is empty.'); $this->assertFileExists($documentFqfn, "The saved document \"{$documentFqfn}\" doesn't exist."); @@ -70,9 +68,10 @@ final class TemplateTest extends \PHPUnit_Framework_TestCase } /** - * XSL stylesheet can be applied + * XSL stylesheet can be applied. * * @param string $actualDocumentFqfn + * @throws \Exception * @covers ::applyXslStyleSheet * @depends testTemplateCanBeSavedInTemporaryLocation * @test @@ -99,7 +98,7 @@ final class TemplateTest extends \PHPUnit_Framework_TestCase } /** - * XSL stylesheet cannot be applied on failure in setting parameter value + * XSL stylesheet cannot be applied on failure in setting parameter value. * * @covers ::applyXslStyleSheet * @expectedException \PhpOffice\PhpWord\Exception\Exception @@ -108,20 +107,20 @@ final class TemplateTest extends \PHPUnit_Framework_TestCase */ final public function testXslStyleSheetCanNotBeAppliedOnFailureOfSettingParameterValue() { - $template = new Template(__DIR__ . "/_files/templates/blank.docx"); + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/blank.docx'); $xslDOMDocument = new \DOMDocument(); - $xslDOMDocument->load(__DIR__ . "/_files/xsl/passthrough.xsl"); + $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. */ - @$template->applyXslStyleSheet($xslDOMDocument, array(1 => 'somevalue')); + @$templateProcessor->applyXslStyleSheet($xslDOMDocument, array(1 => 'somevalue')); } /** - * XSL stylesheet can be applied on failure of loading XML from template + * XSL stylesheet can be applied on failure of loading XML from template. * * @covers ::applyXslStyleSheet * @expectedException \PhpOffice\PhpWord\Exception\Exception @@ -130,83 +129,88 @@ final class TemplateTest extends \PHPUnit_Framework_TestCase */ final public function testXslStyleSheetCanNotBeAppliedOnFailureOfLoadingXmlFromTemplate() { - $template = new Template(__DIR__ . "/_files/templates/corrupted_main_document_part.docx"); + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/corrupted_main_document_part.docx'); $xslDOMDocument = new \DOMDocument(); - $xslDOMDocument->load(__DIR__ . "/_files/xsl/passthrough.xsl"); + $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. */ - @$template->applyXslStyleSheet($xslDOMDocument); + @$templateProcessor->applyXslStyleSheet($xslDOMDocument); } /** - * Get variables and clone row + * @civers ::setValue + * @covers ::cloneRow + * @covers ::saveAs + * @test */ public function testCloneRow() { - $template = __DIR__ . "/_files/templates/clone-merge.docx"; - $expectedVar = array('tableHeader', 'userId', 'userName', 'userLocation'); - $docName = 'clone-test-result.docx'; + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-merge.docx'); - $document = new Template($template); - $actualVar = $document->getVariables(); - $document->setValue('tableHeader', utf8_decode('ééé')); - $document->cloneRow('userId', 1); - $document->setValue('userId#1', 'Test'); - $document->saveAs($docName); + $this->assertEquals( + array('tableHeader', 'userId', 'userName', 'userLocation'), + $templateProcessor->getVariables() + ); + + $docName = 'clone-test-result.docx'; + $templateProcessor->setValue('tableHeader', utf8_decode('ééé')); + $templateProcessor->cloneRow('userId', 1); + $templateProcessor->setValue('userId#1', 'Test'); + $templateProcessor->saveAs($docName); $docFound = file_exists($docName); unlink($docName); - - $this->assertEquals($expectedVar, $actualVar); $this->assertTrue($docFound); } /** - * Replace variables in header and footer + * @covers ::setValue + * @covers ::saveAs + * @test */ public function testVariablesCanBeReplacedInHeaderAndFooter() { - $template = __DIR__ . "/_files/templates/header-footer.docx"; - $expectedVar = array('documentContent', 'headerValue', 'footerValue'); - $docName = 'header-footer-test-result.docx'; + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer.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); + $this->assertEquals( + array('documentContent', 'headerValue', 'footerValue'), + $templateProcessor->getVariables() + ); + + $docName = 'header-footer-test-result.docx'; + $templateProcessor->setValue('headerValue', 'Header Value'); + $templateProcessor->setValue('documentContent', 'Document text.'); + $templateProcessor->setValue('footerValue', 'Footer Value'); + $templateProcessor->saveAs($docName); $docFound = file_exists($docName); unlink($docName); - - $this->assertEquals($expectedVar, $actualVar); $this->assertTrue($docFound); - } /** - * Clone and delete block + * @covers ::cloneBlock + * @covers ::deleteBlock + * @covers ::saveAs + * @test */ public function testCloneDeleteBlock() { - $template = __DIR__ . "/_files/templates/clone-delete-block.docx"; - $expectedVar = array('DELETEME', '/DELETEME', 'CLONEME', '/CLONEME'); + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-delete-block.docx'); + + $this->assertEquals( + array('DELETEME', '/DELETEME', 'CLONEME', '/CLONEME'), + $templateProcessor->getVariables() + ); + $docName = 'clone-delete-block-result.docx'; - - $document = new Template($template); - $actualVar = $document->getVariables(); - - $document->cloneBlock('CLONEME', 3); - $document->deleteBlock('DELETEME'); - - $document->saveAs($docName); + $templateProcessor->cloneBlock('CLONEME', 3); + $templateProcessor->deleteBlock('DELETEME'); + $templateProcessor->saveAs($docName); $docFound = file_exists($docName); unlink($docName); - - $this->assertEquals($expectedVar, $actualVar); $this->assertTrue($docFound); } }