Would it be to much to suggest an update for Tempalte/cloneRow function to include the third parameter, $search_end, null by default, that would serve as the value to look up to close the row?
if null, it is then made equal to the $search and update the functions that look up closing w:tr tag by the $search_end - thus allowing cloning multiple rows at a time?
Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.
Hi @cyrillkalita. Feel free to fork PHPWord and make any changes to the code as required by your project. You can also request a pull request if you think your enhancement will be useful for other users. To be integrated into the develop branch of PHPWord, we will review your changes because we need to make sure that the changes fit well with the rest of the code. cc: @RomanSyroeshko.
As for me, I clone rows with XSLT. :)

A little late, but maybe this will help (or would have helped).. it's a cloneTable function to clone a complete table. Nested tables should be supported as well, though i'm not 100% sure if my algorithm to look for the corresponding end tag of the table is correct...
Sorry that I'm too lazy to make a real contribution, but maybe someone working on the Template.php can add and verify that stuff !?
//Template.php
public function cloneTable($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 table, template variable not found or variable contains markup.");
}
$tableStart = $this->findTableStart($tagPos);
$tableEnd = $this->findTableEnd($tagPos);
// nested table logic: find correct end tag position
$openings = 0;
$tableStartNext = $this->findTableStart($tableStart + 1, true);
while($tableStartNext !== false && $tableStartNext < $tableEnd) {
$openings++;
$tableStartNext = $this->findTableStart($tableStartNext + 1, true);
while($openings > 0 && ($tableStartNext === false || $tableEnd < $tableStartNext)) {
$openings--;
$tableEnd = $this->findTableEnd($tableEnd + 1);
}
} // /nested table logic end
$xmlRow = $this->getSlice($tableStart, $tableEnd);
$result = $this->getSlice(0, $tableStart);
for ($i = 1; $i <= $numberOfClones; $i++) {
$result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow);
}
$result .= $this->getSlice($tableEnd);
$this->documentXML = $result;
}
private function findTableStart($offset, $forward = false)
{
// beware: strpos != strrpos
if($forward) {
$tableStart = strpos($this->documentXML, "<w:tbl ", $offset);
if (!$tableStart) {
$tableStart = strpos($this->documentXML, "<w:tbl>", $offset);
}
} else {
$tableStart = strrpos($this->documentXML, "<w:tbl ", ((strlen($this->documentXML) - $offset) * -1));
if (!$tableStart) {
$tableStart = strrpos($this->documentXML, "<w:tbl>", ((strlen($this->documentXML) - $offset) * -1));
}
if (!$tableStart) {
throw new Exception("Can not find the start position of the row to clone.");
}
}
return $tableStart;
}
private function findTableEnd($offset)
{
$tableEnd = strpos($this->documentXML, "</w:tbl>", $offset) + 8;
return $tableEnd;
}
And there's also a little proof of concept (done with PHPWord-0.11.1).. see the screenshot for a comparison of the template (left) and the resulting docx (right). The "${h1_bug_table}"-Tag cannot be seen, it has a font-size of 1, sits in the upper left table cell, will be replaced by an empty string and was only added to the template to find the table.
$t_bug_count = 3;
$document->cloneTable('h1_bug_table', $t_bug_count);
for ($i = 1; $i <= $t_bug_count; $i++) {
$document->setValue('h1_bug_table#' . $i, '');
$document->setValue('h1_bug#' . $i, 'Bug header 2 #' . $i);
$document->setValue('h1_bug_id#' . $i, 'Bug ID #' . $i);
$document->setValue('h1_bug_type#' . $i, 'Bug Type #' . $i);
$document->setValue('h1_bug_summary#' . $i, 'Bug Summary #' . $i);
$document->cloneTable('h1_inner3#' . $i, 2);
for ($j = 1; $j <= 2; $j++) {
$document->setValue('h1_inner3#' . $i . '#' . $j, 'inner3 #' . $i . '#' . $j);
$document->cloneRow('h1_inner31#' .$i.'#'.$j, 2);
for ($k = 1; $k <= 2; $k++) {
$document->setValue('h1_inner31#' .$i.'#'.$j.'#'.$k, 'inner31 #' .$i.'#'.$j.'#'.$k);
}
}
}
@flohmueller That is a nice table function, however, a table (or any other object) can be cloned with cloneBlock(), so there is no need for a specialized function
@FBnil Cloning a block only allows a single replacement, so if you have a table that has multiple replacements in it then a way to iterate over the cloned tables and replace all of the different placeholders is necessary. See https://github.com/PHPOffice/PHPWord/issues/1071
FYI, the additions by @flohmueller still seem to work, after replacing $this->documentXML with $this->tempDocumentMainPart
@jeffsrepoaccount Ah, yes, next version (0.15.0) will have enumeration, thus all macro's in your cloned block will have #1 or #2 (if it's the second cloned block) ... etc appended to them. This way you can setValue() without problems.
Most helpful comment
A little late, but maybe this will help (or would have helped).. it's a cloneTable function to clone a complete table. Nested tables should be supported as well, though i'm not 100% sure if my algorithm to look for the corresponding end tag of the table is correct...
Sorry that I'm too lazy to make a real contribution, but maybe someone working on the Template.php can add and verify that stuff !?
And there's also a little proof of concept (done with PHPWord-0.11.1).. see the screenshot for a comparison of the template (left) and the resulting docx (right). The "${h1_bug_table}"-Tag cannot be seen, it has a font-size of 1, sits in the upper left table cell, will be replaced by an empty string and was only added to the template to find the table.