Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHPWord - CloneBlock not working #1071

Open
ahmednawazbutt opened this issue Jun 8, 2017 · 13 comments
Open

PHPWord - CloneBlock not working #1071

ahmednawazbutt opened this issue Jun 8, 2017 · 13 comments

Comments

@ahmednawazbutt
Copy link

ahmednawazbutt commented Jun 8, 2017

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.

@troosan
Copy link
Contributor

troosan commented Jun 26, 2017

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

image

@ahmednawazbutt
Copy link
Author

@troosan That too is not helpful you know.

@troosan
Copy link
Contributor

troosan commented Jun 29, 2017

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.

@FBnil
Copy link

FBnil commented Sep 24, 2017

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.

@FBnil
Copy link

FBnil commented Oct 8, 2017

@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.

@cruachan
Copy link

cruachan commented Feb 2, 2018

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;
}

`

@rkorebrits
Copy link

rkorebrits commented Apr 30, 2018

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?

rkorebrits pushed a commit to rkorebrits/PHPWord that referenced this issue Apr 30, 2018
@megasent1
Copy link

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:
`<?php

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.

@alfiansaniputra
Copy link

hm

@cedrictailly
Copy link

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.

@renenieuwenhuizen
Copy link

renenieuwenhuizen commented Jul 25, 2020

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).

@DrDeath72
Copy link

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

@DrDeath72
Copy link

DrDeath72 commented Dec 11, 2020

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];
	}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

10 participants