PHPOffice / PHPWord

A pure PHP library for reading and writing word processing documents
https://phpoffice.github.io/PHPWord/
Other
7.3k stars 2.7k forks source link

Conditions in Templates #1394

Open DIDoS opened 6 years ago

DIDoS commented 6 years ago

This is:

Expected Behavior

Being able to put conditions in a template block rather than in the code. Current macros only support replacement and repeats (clone, replace, set).
I want to start a discussion about this before I might implement it and send a PR.

I guess it would look something like that:

In the docx "template":

Guests:
${IF GUESTS>3}
Don't forget to make a special arrangement for more than 3 guests!
${/ENDIF}

In the code:

$templateProcessor->setVariable('GUESTS', 4);

This could be extended by several operands (<, >, <=, >=, !=, ==) or even nested logical expressions.

Pro: More transparent templates (view logic where it belongs)

Con: More complexity in template processor

rkorebrits commented 6 years ago

Why do you want to put this in the template file? I think this kind of logic is fairly "edge-case" and to implement this level of conditional logic into the library is a bit hectic and will add to maintenance complexity.

Doing this yourself in the current code is quite easy:

Guests:
${MORE_THAN_3_GUESTS_BLOCK}
Don't forget to make a special arrangement for more than 3 guests!
${/MORE_THAN_3_GUESTS_BLOCK}

PHP

if(count($guests) > 3) {
    $templateProcessor->cloneBlock('MORE_THAN_3_GUESTS_BLOCK', 1);
} else {
    $templateProcessor->deleteBlock('MORE_THAN_3_GUESTS_BLOCK');
}

Not sure how many cases you have where you'd need this kind of conditional logic, but I'm pretty sure it will be easier to manage the code in your controller than trying to implement it into the library and your templates.

gruessung commented 6 years ago

I support this request. We create different PHP Sources for documents and create different documents out of one source. So it's easier to create conditions in the template and not in php code.

icy2003 commented 5 years ago

I support this request. We create different PHP Sources for documents and create different documents out of one source. So it's easier to create conditions in the template and not in php code.

I agree with @rkorebrits ,and @gruessung can fit your requirement with this way: set a rule like "greater_books3", they are separated by "", the first part means "condition", it can be some values such as "greater", "equals". Second part means your variable, in this case, it means "books", third part means numbers, and that you can code your php with this rule

DIDoS commented 5 years ago

@icy2003 @rkorebrits Ofc I am fully aware how this can be done in the code rather than in the template (as mentioned in my original PR "...rather than in the code."). The reason why this makes sense is the same reason templating languages exist at all: separation of concerns. You want to get your data together in the code and feed it to a template that executes the "presentation" logic. This way it's easier to debug, maintain and it's separated in modules by function. It also enforces a good programming practice by keeping business logic away from presentation logic.

troosan commented 5 years ago

Better

Guests:
${IF GUESTS>3}
Don't forget to make a special arrangement for more than 3 guests!
${/ENDIF}

or

Guests:
{IF $GUESTS>3}
Don't forget to make a special arrangement for more than 3 guests!
{/ENDIF}

To avoid mixing variables and functions.

icy2003 commented 5 years ago

Better

Guests:
${IF GUESTS>3}
Don't forget to make a special arrangement for more than 3 guests!
${/ENDIF}

or

Guests:
{IF $GUESTS>3}
Don't forget to make a special arrangement for more than 3 guests!
{/ENDIF}

To avoid mixing variables and functions.

is this the next version function?

DIDoS commented 5 years ago

Better

Guests:
${IF GUESTS>3}
Don't forget to make a special arrangement for more than 3 guests!
${/ENDIF}

or

Guests:
{IF $GUESTS>3}
Don't forget to make a special arrangement for more than 3 guests!
{/ENDIF}

To avoid mixing variables and functions.

I don't see the difference in the first one? Did you mean the first "or" the second one or the second one instead of the first one. The reason why I did it that way was because of the way the Template Processor is written right now. Every macro must start with a Dollar sign. The additional logic would be quiet easy to add with a whitelist of key word (functions such as IF) and a check of operands for this function (as regex for example). Also an open/close sanity check (e.g. IF/ENDIF) would have to be added.

Any other issues someone sees with this feature?

AidanHak commented 5 years ago

Why do you want to put this in the template file? I think this kind of logic is fairly "edge-case" and to implement this level of conditional logic into the library is a bit hectic and will add to maintenance complexity.

Doing this yourself in the current code is quite easy:

Guests:
${MORE_THAN_3_GUESTS_BLOCK}
Don't forget to make a special arrangement for more than 3 guests!
${/MORE_THAN_3_GUESTS_BLOCK}

PHP

if(count($guests) > 3) {
    $templateProcessor->cloneBlock('MORE_THAN_3_GUESTS_BLOCK', 1);
} else {
    $templateProcessor->deleteBlock('MORE_THAN_3_GUESTS_BLOCK');
}

Not sure how many cases you have where you'd need this kind of conditional logic, but I'm pretty sure it will be easier to manage the code in your controller than trying to implement it into the library and your templates.

This doesn't seem to work within a numbered list.

AidanHak commented 5 years ago

@DIDoS any update on implementing this feature?

@troosan any idea how I could make your suggestion work with numbered lists?

vpiskunov commented 3 years ago

Any news on this? Would really solve a lot of hassle in multiple implementations. Python's docx templating lib supports Jinja2 - which also provides in-template-logic handling, so would be good to see it here in PhpWord

irisda commented 3 years ago

Is any update on this issue?

g-schmitz commented 3 years ago

Any update? 😀 Maybe I should just start implementing myself I guess...

yogeshwar-chaudhari-20 commented 3 years ago

Hello Community,

@vpiskunov Thanks for your suggestion of using python-docx-template. It sure looks promising for handling the in-template logic.

I was wondering if we have any update on this request though?

I believe the following reasons are worth considering for developing this feature. 1) This will surely reduce the reliance on developers to get every change pushed to the market. 2) Any person businessperson with a basic understanding of templating logic can make changes in the text and/or conditions. 3) If the template document is revising frequently, handling template logic in code will require frequent releases 4) As pointed by @AidanHak, the suggested alternative implementation does not handle numbered_list.

AidanHak commented 3 years ago

I moved over to OpenTBS around the time I first noticed this issue. Works like a charm for my needs.

VoronovAlexander commented 3 years ago

My template has boolean variables, this variables can be "Yes" or "No" for using in template. Additional I want add dynamic text by boolean variables

I solve this problem with small function:

private function handleConditions(TemplateProcessor $templateProcessor, array &$variables, ?int $index = null): void
{
    $conditions = array_filter($templateProcessor->getVariables(), fn($variable) => Str::startsWith($variable, 'IF('));
    foreach ($conditions as $condition) {
        $rule = Str::between($condition, 'IF(', ')'); // Str its Laravel's Facade
        [$variable, $trueValue, $falseValue] = explode(';', $rule);
        $variableName = $index ? $variable . "#$index" : $variable;
        if (isset($variables[$variableName])) {
            $variables[$condition] = $variables[$variableName] === 'Yes' ? $trueValue : $falseValue;
        }
    }
}

in template I have row "IF(VARIABLE;True text;else text)"


Example of use:

$variables = [
  'Name' => $this->name,
  'Checked' => $this->isChecked ? 'Yes' : 'No',
];

$this->handleConditions($templateProcessor, $variables);
$templateProcessor->setValues($variables);

And example for cloned blocks:

$templateProcessor->cloneBlock(
   'Step',
  $this->count(),
  true,
  true,
);

foreach ($steps as $index => $step) {
  $index++;
  $stepVariables = [
    'StepName' => $step->name,
    'IsChecked' => $step->isChecked ? 'Yes' : 'No',
  ];
  $this->handleConditions($templateProcessor, $stepVariables, $index);
  $templateProcessor->setValues($stepVariables);
}
elvis005 commented 7 months ago

I also require this functionality. Using the setValue method to assign an empty value doesn't eliminate the element; rather, it merely empties the text, leaving a line in the document, which is less than ideal. removeBlock does not work in nested block. no choice but to implement:

Create a new class that extends TemplateProcessor and incorporated a new method called removeValue.

    public function removeValue($blockname)
    {
        $xml = simplexml_load_string($this->tempDocumentMainPart);
        $paragraphs = $xml->xpath('//w:p');
        foreach ($paragraphs as $paragraph)
        {
            // Check if paragraph and text node exist
            $t = (string)$paragraph->asXML();
            if (strpos($t, $blockname) !== false) {
                // Convert SimpleXML to DOMDocument
                $dom = dom_import_simplexml($paragraph);
                $dom->parentNode->removeChild($dom);
                break;
            }
        }
        $this->tempDocumentMainPart = $xml->asXML();
    }

hope it helps!