Hope everyone is doing great.
i am having trouble using PHPWord extension for my web Application that I installed using composer using:
"phpoffice/phpword": "v0.13.*"
Now, when It comes to add a single value variable, the setValue() works like a charm. but when I need to copy a set of lines, the cloneBlock() method does not work at all.
What is required:
I Need to make a copy of my block
what I get:
All I get is the block in its own place and no copy at all.
Here is my template file content (.docx):
${EDUCATIONBLOCK} ${degree} - ${dates} ${company} ${summary} ${/EDUCATIONBLOCK}
Here is the code for my php file
````
$myProfile = [
'EDUCATION' => [
[
'degree' => 'Degree A',
'company' => 'Company A',
'dates' => 'Date Earned',
'summary' => 'You might want to include your GPA and a summary of relevant coursework, awards, and honors.',
],
[
'degree' => 'Degree B',
'company' => 'Company B',
'dates' => 'Date Earned',
'summary' => 'You might want to include your GPA and a summary of relevant coursework, awards, and honors.',
],
],
];
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor('resources/templateFile.docx');
$templateProcessor->cloneBlock('EDUCATIONBLOCK', count($myProfile['EDUCATION']));
$templateProcessor->saveAs('output/generated_templateFile.docx');
````
My output generated file has the following in it:
${EDUCATIONBLOCK} ${degree} - ${dates} ${company} ${summary} ${/EDUCATIONBLOCK}
No change at all. I don't see any cloned block in my document. Please help.
My application specifications are:
Framework: Yii2
composer version: 1.4.2
Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.
I tried with the current development branch with the following code
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor('resources/template.docx');
$templateProcessor->cloneBlock('EDUCATIONBLOCK', count($myProfile['EDUCATION']));
$templateProcessor->saveAs('results/generated_templateFile.docx');
The result is the following word document

@troosan That too is not helpful you know.
I can imagine that's not the end result you are looking for :-)
But that is what your code is doing, duplicating a block 2 times ...
After that you'll need to replace.
As described in https://stackoverflow.com/questions/27362945/how-to-duplicate-variables-into-template-with-phpword-and-using-cloneblock you'll have to replace limiting to 1 replacement as many times as you duplicated the block.
If you don't mind modifying sourcecode:
./vendor/phpoffice/phpword/src/PhpWord/TemplateProcessor.php
Go to the public function cloneBlock() and there add a new parameter:
public function cloneBlock($blockname, $clones = 1, $replace = true, $incrementVariables = true)
{
$xmlBlock = null;
preg_match(
#'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
'/(<\?xml.*)(<w:p\b[^>]*>.*?\${' . $blockname . '}.*?<\/w:p>)(.*)(<w:p\b[^>]*>.*?\${\/' . $blockname . '}.*?<\/w:p>)/is',
$this->tempDocumentMainPart,
$matches
);
if (isset($matches[3])) {
$xmlBlock = $matches[3];
$cloned = array();
for ($i = 1; $i <= $clones; $i++) {
if($incrementVariables) {
$xmlBlock = $matches[3];
$xmlBlock = preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlBlock);
}
$cloned[] = $xmlBlock;
}
if ($replace) {
$this->tempDocumentMainPart = str_replace(
$matches[2] . $matches[3] . $matches[4],
implode('', $cloned),
$this->tempDocumentMainPart
);
}
}
return $xmlBlock;
}
Note I also changed the regexp to suit my needs (as the current version seems to spew out malformed xml and LibreOffice complains about it, I'll test it at work this week on MSOffice and maybe ask a pull request, after learning how to do that). The thing is: both ${yourblock} needs to be on a separate line (paragraph) for my modification to work. So if the old regexp works for you, then remove mine and uncomment the original back in. Will probably use more fancy functions like cloneRow() does to search for places to snip and cut.
EDIT 2017-09-25:
Modified the regexp so that the test also works, from the ./vendor/ directory:
[prima@cvprima vendor]$ phpunit --bootstrap autoload.php phpoffice/phpword --filter 'testCloneDeleteBlock' -v -v -v
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.
Runtime: PHP 7.0.22
.
Time: 152 ms, Memory: 8.00MB
OK (1 test, 3 assertions)
So it now goes to the first <w:p> block, irregardless of LibreOffice <w:p> or MSOffice <w:p w:rsidR="00AC46F7" w:rsidRDefault="00AC46F7" w:rsidP="00AC46F7">
./phpoffice/phpword/tests/PhpWord/TemplateProcessorTest.php has been added to:
public function testCloneDeleteBlock()
{
$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';
$templateProcessor->cloneBlock('CLONEME', 3);
$templateProcessor->deleteBlock('DELETEME');
$templateProcessor->saveAs($docName);
$docFound = file_exists($docName);
if($docFound){
$templateProcessorNEWFILE = new TemplateProcessor($docName);
$this->assertEquals(
[],
$templateProcessorNEWFILE->getVariables()
);
}
unlink($docName);
$this->assertTrue($docFound);
}
EDIT2:
forget about the regexp, it seems PHP7 has a problem with multiple non-greedy captures. The faster solution is the way cloneRow() works. Trying a pull request later.
@ahmednawazbutt
Try to remove the blocks, then add a newline, and write your ${EDUCATIONBLOCK} and ${/EDUCATIONBLOCK} in one go. Then try again. Sometimes, you will have to rewrite your template. See also the temporal cloneBlock() you can use that fixes the problem.
I've had persistent and hard to trace issues with the cloneBlock failing too, the XML written by Word can break the regex given (or the modified ones I've seen). Also imho regex can be quite brittle. The below is a replacement using standard string functions. It's undoubtedly slower and less elegant that the regex version, but it's proven more robust for me - and easier to understand.
` public function cloneBlock($blockname, $clones = 1, $replace = true)
{
$cloneXML = '';
$replaceXML = null;
// location of blockname open tag
$startpos = strpos($this->tempDocumentMainPart,'${'.$blockname.'}');
if ($startpos) {
// start position of area to be replaced, this is from the start of the <w:p before the blockname
$startreplace = strrpos($this->tempDocumentMainPart,'<w:p',-(strlen($this->tempDocumentMainPart) - $startpos));
// start position of text we're going to clone, from after the </w:p> after the blockname
$startclone = strpos($this->tempDocumentMainPart,'</w:p>',$startpos) + strlen('</w:p>');
// location of the blockname close tag
$endpos = strpos($this->tempDocumentMainPart,'${/'.$blockname.'}');
if ($endpos) {
// end position of the area to be replaced, to the end of the </w:p> after the close blockname
$endreplace = strpos($this->tempDocumentMainPart,'</w:p>',$endpos) + strlen('</w:p>');
// end position of the text we're cloning, from the start of the <w:p before the close blockname
$endclone = strrpos($this->tempDocumentMainPart,'<w:p',-(strlen($this->tempDocumentMainPart) - $endpos));
$clonelength = ($endclone-$startclone);
$replacelength = ($endreplace-$startreplace);
$preReplace = substr($this->tempDocumentMainPart,0,$startreplace);
$cloneXML = substr($this->tempDocumentMainPart,$startclone,$clonelength);
$replaceXML = substr($this->tempDocumentMainPart,$startreplace,$replacelength);
$postReplace = substr($this->tempDocumentMainPart,$endreplace);
}
}
if ($replaceXML!=null) {
$cloned = array();
for ($i = 1; $i <= $clones; $i++) {
$cloned[] = $cloneXML;
}
if ($replace) {
$this->tempDocumentMainPart = str_replace(
$replaceXML,
implode('', $cloned),
$this->tempDocumentMainPart
);
}
}
return $cloneXML;
}
`
Hi @troosan, I would like to suggest you add the method posted by @cruachan as e.g. cloneBlockString(). So far I've had major issues with cloneBlock() and it just keeps on failing over and over. In my documents I need several of them and one might work, but then can't get others to work, even if I copy the working block and just change the wrapper name. The solution posted by @cruachan does work constantly for me.
If you want I can send a PR?
Try to change cloneBlock regexp to following, might help:
preg_match(
'/(<\?xml.+?>.*?)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
$this->tempDocumentMainPart,
$matches
);
I've created a class extending project's template processor:
`
namespace PhpOffice\PhpWord;
class TemplateProcessorMod extends TemplateProcessor {
/**
* Clone a block.
*
* @param string $blockname
* @param int $clones
* @param bool $replace
*
* @return string|null
*/
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
);
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
);
}
}
return $xmlBlock;
}
}
`
This regex prevents catastrophic backtracking for xml. Sometimes this was the issue for me.
hm
Hi, you should take a look at my ticket because I have the same problem : #1836
I've just posted a rewrite of the function which bypasses the issue for my cases.
I've just noticed that setting pcre.jit = 1 in /etc/php.ini makes a world of difference! Before I had some blocks replaced and some not. Also the code was sluggish. After setting pcre.jit = 1 all is fine using phpOffice::phpWord v0.17.0 (01 oct 2019).
looks like old pcre version error
PCRE (Perl Compatible Regular Expressions) Support => enabled
PCRE Library Version => 8.38 2015-11-23
PCRE JIT Support => enabled
don't clone block
PCRE (Perl Compatible Regular Expressions) Support => enabled
PCRE Library Version => 8.44 2020-02-12
PCRE JIT Support => enabled
clone block
replace buggy preg_match
public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false, $variableReplacements = null)
{
$xmlBlock = null;
[$block, $content] = $this->findBlockParts($blockname);
if (isset($content)) {
$xmlBlock = $content;
if ($indexVariables) {
$cloned = $this->indexClonedVariables($clones, $xmlBlock);
} elseif ($variableReplacements !== null && is_array($variableReplacements)) {
$cloned = $this->replaceClonedVariables($variableReplacements, $xmlBlock);
} else {
$cloned = array();
for ($i = 1; $i <= $clones; $i++) {
$cloned[] = $xmlBlock;
}
}
if ($replace) {
$this->tempDocumentMainPart = str_replace(
$block,
implode('', $cloned),
$this->tempDocumentMainPart
);
}
}
return $xmlBlock;
}
public function replaceBlock($blockname, $replacement)
{
[$block, $content] = $this->findBlockParts($blockname);
if (isset($content)) {
$this->tempDocumentMainPart = str_replace(
$block,
$replacement,
$this->tempDocumentMainPart
);
}
}
protected function findBlockParts($blockname)
{
$open = mb_strpos($this->tempDocumentMainPart, '${' . $blockname . '}');
if($open === false) {
return [null, null];
}
$close = mb_strpos($this->tempDocumentMainPart, '${/' . $blockname . '}');
if($close === false) {
return [null, null];
}
$start = mb_strrpos(mb_substr($this->tempDocumentMainPart, 0, $open), '<w:p>');
$end = mb_strpos($this->tempDocumentMainPart, '</w:p>', $close) + mb_strlen('</w:p>');
$openEnd = mb_strpos($this->tempDocumentMainPart, '</w:p>', $open) + mb_strlen('</w:p>');
$closeStart = mb_strrpos(mb_substr($this->tempDocumentMainPart, 0, $close), '<w:p>');
$block = mb_substr($this->tempDocumentMainPart, $start, $end - $start);
$content = mb_substr($this->tempDocumentMainPart, $openEnd, $closeStart - $openEnd);
return [$block, $content];
}
Most helpful comment
Try to change cloneBlock regexp to following, might help:
preg_match( '/(<\?xml.+?>.*?)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is', $this->tempDocumentMainPart, $matches );I've created a class extending project's template processor:
`
namespace PhpOffice\PhpWord;
class TemplateProcessorMod extends TemplateProcessor {
}
`
This regex prevents catastrophic backtracking for xml. Sometimes this was the issue for me.