Skip to content

Commit 98026ae

Browse files
authored
Merge pull request #1565 from troosan/set_complex_type_in_template
Set complex type in template
2 parents af31fc5 + d2b0b31 commit 98026ae

File tree

7 files changed

+439
-1
lines changed

7 files changed

+439
-1
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ composer.phar
1313
vendor
1414
/report
1515
/build
16-
/samples/resources
1716
/samples/results
1817
/.settings
1918
phpword.ini

docs/templates-processing.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,32 @@ Applies the XSL stylesheet passed to header part, footer part and main part
215215
$xslDomDocument = new \DOMDocument();
216216
$xslDomDocument->load('/path/to/my/stylesheet.xsl');
217217
$templateProcessor->applyXslStyleSheet($xslDomDocument);
218+
219+
setComplexValue
220+
"""""""""""""""
221+
Raplaces a ${macro} with the ComplexType passed.
222+
See ``Sample_40_TemplateSetComplexValue.php`` for examples.
223+
224+
.. code-block:: php
225+
226+
$inline = new TextRun();
227+
$inline->addText('by a red italic text', array('italic' => true, 'color' => 'red'));
228+
$templateProcessor->setComplexValue('inline', $inline);
229+
230+
setComplexBlock
231+
"""""""""""""""
232+
Raplaces a ${macro} with the ComplexType passed.
233+
See ``Sample_40_TemplateSetComplexValue.php`` for examples.
234+
235+
.. code-block:: php
236+
237+
$table = new Table(array('borderSize' => 12, 'borderColor' => 'green', 'width' => 6000, 'unit' => TblWidth::TWIP));
238+
$table->addRow();
239+
$table->addCell(150)->addText('Cell A1');
240+
$table->addCell(150)->addText('Cell A2');
241+
$table->addCell(150)->addText('Cell A3');
242+
$table->addRow();
243+
$table->addCell(150)->addText('Cell B1');
244+
$table->addCell(150)->addText('Cell B2');
245+
$table->addCell(150)->addText('Cell B3');
246+
$templateProcessor->setComplexBlock('table', $table);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
use PhpOffice\PhpWord\Element\Field;
3+
use PhpOffice\PhpWord\Element\Table;
4+
use PhpOffice\PhpWord\Element\TextRun;
5+
use PhpOffice\PhpWord\SimpleType\TblWidth;
6+
7+
include_once 'Sample_Header.php';
8+
9+
// Template processor instance creation
10+
echo date('H:i:s'), ' Creating new TemplateProcessor instance...', EOL;
11+
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor('resources/Sample_40_TemplateSetComplexValue.docx');
12+
13+
$title = new TextRun();
14+
$title->addText('This title has been set ', array('bold' => true, 'italic' => true, 'color' => 'blue'));
15+
$title->addText('dynamically', array('bold' => true, 'italic' => true, 'color' => 'red', 'underline' => 'single'));
16+
$templateProcessor->setComplexBlock('title', $title);
17+
18+
$inline = new TextRun();
19+
$inline->addText('by a red italic text', array('italic' => true, 'color' => 'red'));
20+
$templateProcessor->setComplexValue('inline', $inline);
21+
22+
$table = new Table(array('borderSize' => 12, 'borderColor' => 'green', 'width' => 6000, 'unit' => TblWidth::TWIP));
23+
$table->addRow();
24+
$table->addCell(150)->addText('Cell A1');
25+
$table->addCell(150)->addText('Cell A2');
26+
$table->addCell(150)->addText('Cell A3');
27+
$table->addRow();
28+
$table->addCell(150)->addText('Cell B1');
29+
$table->addCell(150)->addText('Cell B2');
30+
$table->addCell(150)->addText('Cell B3');
31+
$templateProcessor->setComplexBlock('table', $table);
32+
33+
$field = new Field('DATE', array('dateformat' => 'dddd d MMMM yyyy H:mm:ss'), array('PreserveFormat'));
34+
$templateProcessor->setComplexValue('field', $field);
35+
36+
// $link = new Link('https://github.com/PHPOffice/PHPWord');
37+
// $templateProcessor->setComplexValue('link', $link);
38+
39+
echo date('H:i:s'), ' Saving the result document...', EOL;
40+
$templateProcessor->saveAs('results/Sample_40_TemplateSetComplexValue.docx');
41+
42+
echo getEndingNotes(array('Word2007' => 'docx'), 'results/Sample_40_TemplateSetComplexValue.docx');
43+
if (!CLI) {
44+
include_once 'Sample_Footer.php';
45+
}
Binary file not shown.

src/PhpWord/TemplateProcessor.php

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
namespace PhpOffice\PhpWord;
1919

2020
use PhpOffice\Common\Text;
21+
use PhpOffice\Common\XMLWriter;
2122
use PhpOffice\PhpWord\Escaper\RegExp;
2223
use PhpOffice\PhpWord\Escaper\Xml;
2324
use PhpOffice\PhpWord\Exception\CopyFileException;
@@ -249,6 +250,46 @@ protected static function ensureUtf8Encoded($subject)
249250
return $subject;
250251
}
251252

253+
/**
254+
* @param string $search
255+
* @param \PhpOffice\PhpWord\Element\AbstractElement $complexType
256+
*/
257+
public function setComplexValue($search, \PhpOffice\PhpWord\Element\AbstractElement $complexType)
258+
{
259+
$elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1);
260+
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName;
261+
262+
$xmlWriter = new XMLWriter();
263+
/** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
264+
$elementWriter = new $objectClass($xmlWriter, $complexType, true);
265+
$elementWriter->write();
266+
267+
$where = $this->findContainingXmlBlockForMacro($search, 'w:r');
268+
$block = $this->getSlice($where['start'], $where['end']);
269+
$textParts = $this->splitTextIntoTexts($block);
270+
$this->replaceXmlBlock($search, $textParts, 'w:r');
271+
272+
$search = static::ensureMacroCompleted($search);
273+
$this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:r');
274+
}
275+
276+
/**
277+
* @param string $search
278+
* @param \PhpOffice\PhpWord\Element\AbstractElement $complexType
279+
*/
280+
public function setComplexBlock($search, \PhpOffice\PhpWord\Element\AbstractElement $complexType)
281+
{
282+
$elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1);
283+
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName;
284+
285+
$xmlWriter = new XMLWriter();
286+
/** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
287+
$elementWriter = new $objectClass($xmlWriter, $complexType, false);
288+
$elementWriter->write();
289+
290+
$this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:p');
291+
}
292+
252293
/**
253294
* @param mixed $search
254295
* @param mixed $replace
@@ -685,6 +726,7 @@ public function cloneRowAndSetValues($search, $values)
685726
public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false, $variableReplacements = null)
686727
{
687728
$xmlBlock = null;
729+
$matches = array();
688730
preg_match(
689731
'/(<\?xml.*)(<w:p\b.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p\b.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
690732
$this->tempDocumentMainPart,
@@ -724,6 +766,7 @@ public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVaria
724766
*/
725767
public function replaceBlock($blockname, $replacement)
726768
{
769+
$matches = array();
727770
preg_match(
728771
'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
729772
$this->tempDocumentMainPart,
@@ -865,6 +908,7 @@ protected function setValueForPart($search, $replace, $documentPartXML, $limit)
865908
*/
866909
protected function getVariablesForPart($documentPartXML)
867910
{
911+
$matches = array();
868912
preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
869913

870914
return $matches[1];
@@ -893,6 +937,7 @@ protected function getMainPartName()
893937

894938
$pattern = '~PartName="\/(word\/document.*?\.xml)" ContentType="application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document\.main\+xml"~';
895939

940+
$matches = array();
896941
preg_match($pattern, $contentTypes, $matches);
897942

898943
return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
@@ -1031,4 +1076,141 @@ protected function replaceClonedVariables($variableReplacements, $xmlBlock)
10311076

10321077
return $results;
10331078
}
1079+
1080+
/**
1081+
* Replace an XML block surrounding a macro with a new block
1082+
*
1083+
* @param string $macro Name of macro
1084+
* @param string $block New block content
1085+
* @param string $blockType XML tag type of block
1086+
* @return \PhpOffice\PhpWord\TemplateProcessor Fluent interface
1087+
*/
1088+
protected function replaceXmlBlock($macro, $block, $blockType = 'w:p')
1089+
{
1090+
$where = $this->findContainingXmlBlockForMacro($macro, $blockType);
1091+
if (is_array($where)) {
1092+
$this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']);
1093+
}
1094+
1095+
return $this;
1096+
}
1097+
1098+
/**
1099+
* Find start and end of XML block containing the given macro
1100+
* e.g. <w:p>...${macro}...</w:p>
1101+
*
1102+
* Note that only the first instance of the macro will be found
1103+
*
1104+
* @param string $macro Name of macro
1105+
* @param string $blockType XML tag for block
1106+
* @return bool|int[] FALSE if not found, otherwise array with start and end
1107+
*/
1108+
protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p')
1109+
{
1110+
$macroPos = $this->findMacro($macro);
1111+
if (0 > $macroPos) {
1112+
return false;
1113+
}
1114+
$start = $this->findXmlBlockStart($macroPos, $blockType);
1115+
if (0 > $start) {
1116+
return false;
1117+
}
1118+
$end = $this->findXmlBlockEnd($start, $blockType);
1119+
//if not found or if resulting string does not contain the macro we are searching for
1120+
if (0 > $end || strstr($this->getSlice($start, $end), $macro) === false) {
1121+
return false;
1122+
}
1123+
1124+
return array('start' => $start, 'end' => $end);
1125+
}
1126+
1127+
/**
1128+
* Find the position of (the start of) a macro
1129+
*
1130+
* Returns -1 if not found, otherwise position of opening $
1131+
*
1132+
* Note that only the first instance of the macro will be found
1133+
*
1134+
* @param string $search Macro name
1135+
* @param int $offset Offset from which to start searching
1136+
* @return int -1 if macro not found
1137+
*/
1138+
protected function findMacro($search, $offset = 0)
1139+
{
1140+
$search = static::ensureMacroCompleted($search);
1141+
$pos = strpos($this->tempDocumentMainPart, $search, $offset);
1142+
1143+
return ($pos === false) ? -1 : $pos;
1144+
}
1145+
1146+
/**
1147+
* Find the start position of the nearest XML block start before $offset
1148+
*
1149+
* @param int $offset Search position
1150+
* @param string $blockType XML Block tag
1151+
* @return int -1 if block start not found
1152+
*/
1153+
protected function findXmlBlockStart($offset, $blockType)
1154+
{
1155+
$reverseOffset = (strlen($this->tempDocumentMainPart) - $offset) * -1;
1156+
// first try XML tag with attributes
1157+
$blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', $reverseOffset);
1158+
// if not found, or if found but contains the XML tag without attribute
1159+
if (false === $blockStart || strrpos($this->getSlice($blockStart, $offset), '<' . $blockType . '>')) {
1160+
// also try XML tag without attributes
1161+
$blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', $reverseOffset);
1162+
}
1163+
1164+
return ($blockStart === false) ? -1 : $blockStart;
1165+
}
1166+
1167+
/**
1168+
* Find the nearest block end position after $offset
1169+
*
1170+
* @param int $offset Search position
1171+
* @param string $blockType XML Block tag
1172+
* @return int -1 if block end not found
1173+
*/
1174+
protected function findXmlBlockEnd($offset, $blockType)
1175+
{
1176+
$blockEndStart = strpos($this->tempDocumentMainPart, '</' . $blockType . '>', $offset);
1177+
// return position of end of tag if found, otherwise -1
1178+
1179+
return ($blockEndStart === false) ? -1 : $blockEndStart + 3 + strlen($blockType);
1180+
}
1181+
1182+
/**
1183+
* Splits a w:r/w:t into a list of w:r where each ${macro} is in a separate w:r
1184+
*
1185+
* @param string $text
1186+
* @return string
1187+
*/
1188+
protected function splitTextIntoTexts($text)
1189+
{
1190+
if (!$this->textNeedsSplitting($text)) {
1191+
return $text;
1192+
}
1193+
$matches = array();
1194+
if (preg_match('/(<w:rPr.*<\/w:rPr>)/i', $text, $matches)) {
1195+
$extractedStyle = $matches[0];
1196+
} else {
1197+
$extractedStyle = '';
1198+
}
1199+
1200+
$unformattedText = preg_replace('/>\s+</', '><', $text);
1201+
$result = str_replace(array('${', '}'), array('</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">${', '}</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">'), $unformattedText);
1202+
1203+
return str_replace(array('<w:r>' . $extractedStyle . '<w:t xml:space="preserve"></w:t></w:r>', '<w:r><w:t xml:space="preserve"></w:t></w:r>', '<w:t>'), array('', '', '<w:t xml:space="preserve">'), $result);
1204+
}
1205+
1206+
/**
1207+
* Returns true if string contains a macro that is not in it's own w:r
1208+
*
1209+
* @param string $text
1210+
* @return bool
1211+
*/
1212+
protected function textNeedsSplitting($text)
1213+
{
1214+
return preg_match('/[^>]\${|}[^<]/i', $text) == 1;
1215+
}
10341216
}

0 commit comments

Comments
 (0)