getVariables()); } /** * Template can be saved in temporary location. * * @covers ::save * @covers ::zip */ public function testTemplateCanBeSavedInTemporaryLocation() { $templateFqfn = __DIR__ . '/_files/templates/with_table_macros.docx'; $templateProcessor = new TemplateProcessor($templateFqfn); $xslDomDocument = new DOMDocument(); $xslDomDocument->load(__DIR__ . '/_files/xsl/remove_tables_by_needle.xsl'); foreach (['${employee.', '${scoreboard.', '${reference.'] as $needle) { $templateProcessor->applyXslStyleSheet($xslDomDocument, ['needle' => $needle]); } $embeddingText = 'The quick Brown Fox jumped over the lazy^H^H^H^Htired unitTester'; $templateProcessor->zip()->AddFromString('word/embeddings/fox.bin', $embeddingText); $documentFqfn = $templateProcessor->save(); self::assertNotEmpty($documentFqfn, 'FQFN of the saved document is empty.'); self::assertFileExists($documentFqfn, "The saved document \"{$documentFqfn}\" doesn't exist."); $templateZip = new ZipArchive(); $templateZip->open($templateFqfn); $templateHeaderXml = $templateZip->getFromName('word/header1.xml'); $templateMainPartXml = $templateZip->getFromName('word/document.xml'); $templateFooterXml = $templateZip->getFromName('word/footer1.xml'); if (false === $templateZip->close()) { throw new Exception("Could not close zip file \"{$templateZip}\"."); } $documentZip = new ZipArchive(); $documentZip->open($documentFqfn); $documentHeaderXml = $documentZip->getFromName('word/header1.xml'); $documentMainPartXml = $documentZip->getFromName('word/document.xml'); $documentFooterXml = $documentZip->getFromName('word/footer1.xml'); $documentEmbedding = $documentZip->getFromName('word/embeddings/fox.bin'); if (false === $documentZip->close()) { throw new Exception("Could not close zip file \"{$documentZip}\"."); } self::assertNotEquals($templateHeaderXml, $documentHeaderXml); self::assertNotEquals($templateMainPartXml, $documentMainPartXml); self::assertNotEquals($templateFooterXml, $documentFooterXml); self::assertEquals($embeddingText, $documentEmbedding); return $documentFqfn; } /** * XSL stylesheet can be applied. * * @covers ::applyXslStyleSheet * * @depends testTemplateCanBeSavedInTemporaryLocation * * @param string $actualDocumentFqfn */ public function testXslStyleSheetCanBeApplied($actualDocumentFqfn): void { $expectedDocumentFqfn = __DIR__ . '/_files/documents/without_table_macros.docx'; $actualDocumentZip = new ZipArchive(); $actualDocumentZip->open($actualDocumentFqfn); $actualHeaderXml = $actualDocumentZip->getFromName('word/header1.xml'); $actualMainPartXml = $actualDocumentZip->getFromName('word/document.xml'); $actualFooterXml = $actualDocumentZip->getFromName('word/footer1.xml'); if (false === $actualDocumentZip->close()) { throw new Exception("Could not close zip file \"{$actualDocumentFqfn}\"."); } $expectedDocumentZip = new ZipArchive(); $expectedDocumentZip->open($expectedDocumentFqfn); $expectedHeaderXml = $expectedDocumentZip->getFromName('word/header1.xml'); $expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml'); $expectedFooterXml = $expectedDocumentZip->getFromName('word/footer1.xml'); if (false === $expectedDocumentZip->close()) { throw new Exception("Could not close zip file \"{$expectedDocumentFqfn}\"."); } self::assertXmlStringEqualsXmlString($expectedHeaderXml, $actualHeaderXml); self::assertXmlStringEqualsXmlString($expectedMainPartXml, $actualMainPartXml); self::assertXmlStringEqualsXmlString($expectedFooterXml, $actualFooterXml); } /** * XSL stylesheet cannot be applied on failure in setting parameter value. * * @covers ::applyXslStyleSheet */ public function testXslStyleSheetCanNotBeAppliedOnFailureOfSettingParameterValue(): void { $this->expectException(\PhpOffice\PhpWord\Exception\Exception::class); $this->expectExceptionMessage('Could not set values for the given XSL style sheet parameters.'); // Test is not needed for PHP 8.0, because internally validation throws TypeError exception. if (\PHP_VERSION_ID >= 80000) { self::markTestSkipped('not needed for PHP 8.0'); } $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/blank.docx'); $xslDomDocument = new DOMDocument(); $xslDomDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl'); /* * We have to use error control below, because \XSLTProcessor::setParameter omits warning on failure. * This warning fails the test. */ @$templateProcessor->applyXslStyleSheet($xslDomDocument, [1 => 'somevalue']); } /** * XSL stylesheet can be applied on failure of loading XML from template. * * @covers ::applyXslStyleSheet */ public function testXslStyleSheetCanNotBeAppliedOnFailureOfLoadingXmlFromTemplate(): void { $this->expectException(\PhpOffice\PhpWord\Exception\Exception::class); $this->expectExceptionMessage('Could not load the given XML document.'); $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/corrupted_main_document_part.docx'); $xslDomDocument = new DOMDocument(); $xslDomDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl'); /* * We have to use error control below, because \DOMDocument::loadXML omits warning on failure. * This warning fails the test. */ @$templateProcessor->applyXslStyleSheet($xslDomDocument); } /** * @covers ::cloneRow * @covers ::saveAs * @covers ::setValue */ public function testCloneRow(): void { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-merge.docx'); self::assertEquals( ['tableHeader', 'userId', 'userName', 'userLocation'], $templateProcessor->getVariables() ); $docName = 'clone-test-result.docx'; $templateProcessor->setValue('tableHeader', utf8_decode('ééé')); $templateProcessor->cloneRow('userId', 1); $templateProcessor->setValue('userId#1', 'Test'); $templateProcessor->saveAs($docName); $docFound = file_exists($docName); unlink($docName); self::assertTrue($docFound); } /** * @covers ::cloneRow * @covers ::saveAs * @covers ::setValue */ public function testCloneRowAndSetValues(): void { $mainPart = ' ${userId} ${userName} ${userLocation} '; $templateProcessor = new TestableTemplateProcesor($mainPart); self::assertEquals( ['userId', 'userName', 'userLocation'], $templateProcessor->getVariables() ); $values = [ ['userId' => 1, 'userName' => 'Batman', 'userLocation' => 'Gotham City'], ['userId' => 2, 'userName' => 'Superman', 'userLocation' => 'Metropolis'], ]; $templateProcessor->setValue('tableHeader', 'My clonable table'); $templateProcessor->cloneRowAndSetValues('userId', $values); self::assertStringContainsString('Superman', $templateProcessor->getMainPart()); self::assertStringContainsString('Metropolis', $templateProcessor->getMainPart()); } public function testCloneNotExistingRowShouldThrowException(): void { $this->expectException(Exception::class); $mainPart = 'text'; $templateProcessor = new TestableTemplateProcesor($mainPart); $templateProcessor->cloneRow('fake_search', 2); } /** * @covers ::saveAs * @covers ::setValue */ public function testMacrosCanBeReplacedInHeaderAndFooter(): void { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx'); self::assertEquals(['documentContent', 'headerValue:100:100', 'footerValue'], $templateProcessor->getVariables()); $macroNames = ['headerValue', 'documentContent', 'footerValue']; $macroValues = ['Header Value', 'Document text.', 'Footer Value']; $templateProcessor->setValue($macroNames, $macroValues); $docName = 'header-footer-test-result.docx'; $templateProcessor->saveAs($docName); $docFound = file_exists($docName); unlink($docName); self::assertTrue($docFound); } /** * @covers ::setValue */ public function testSetValue(): void { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-merge.docx'); Settings::setOutputEscapingEnabled(true); $helloworld = "hello\nworld"; $templateProcessor->setValue('userName', $helloworld); self::assertEquals( ['tableHeader', 'userId', 'userLocation'], $templateProcessor->getVariables() ); } public function testSetComplexValue(): void { $title = new TextRun(); $title->addText('This is my title'); $firstname = new Text('Donald'); $lastname = new Text('Duck'); $mainPart = ' Hello ${document-title} Hello ${firstname} ${lastname} '; $result = ' This is my title Hello Donald Duck '; $templateProcessor = new TestableTemplateProcesor($mainPart); $templateProcessor->setComplexBlock('document-title', $title); $templateProcessor->setComplexValue('firstname', $firstname); $templateProcessor->setComplexValue('lastname', $lastname); self::assertEquals(preg_replace('/>\s+<', $result), preg_replace('/>\s+<', $templateProcessor->getMainPart())); } /** * @covers ::setValues */ public function testSetValues(): void { $mainPart = ' Hello ${firstname} ${lastname} '; $templateProcessor = new TestableTemplateProcesor($mainPart); $templateProcessor->setValues(['firstname' => 'John', 'lastname' => 'Doe']); self::assertStringContainsString('Hello John Doe', $templateProcessor->getMainPart()); } /** * @covers ::setImageValue */ public function testSetImageValue(): void { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx'); $imagePath = __DIR__ . '/_files/images/earth.jpg'; $variablesReplace = [ 'headerValue' => function () use ($imagePath) { return $imagePath; }, 'documentContent' => ['path' => $imagePath, 'width' => 500, 'height' => 500], 'footerValue' => ['path' => $imagePath, 'width' => 100, 'height' => 50, 'ratio' => false], ]; $templateProcessor->setImageValue(array_keys($variablesReplace), $variablesReplace); $docName = 'header-footer-images-test-result.docx'; $templateProcessor->saveAs($docName); self::assertFileExists($docName, "Generated file '{$docName}' not found!"); $expectedDocumentZip = new ZipArchive(); $expectedDocumentZip->open($docName); $expectedContentTypesXml = $expectedDocumentZip->getFromName('[Content_Types].xml'); $expectedDocumentRelationsXml = $expectedDocumentZip->getFromName('word/_rels/document.xml.rels'); $expectedHeaderRelationsXml = $expectedDocumentZip->getFromName('word/_rels/header1.xml.rels'); $expectedFooterRelationsXml = $expectedDocumentZip->getFromName('word/_rels/footer1.xml.rels'); $expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml'); $expectedHeaderPartXml = $expectedDocumentZip->getFromName('word/header1.xml'); $expectedFooterPartXml = $expectedDocumentZip->getFromName('word/footer1.xml'); $expectedImage = $expectedDocumentZip->getFromName('word/media/image_rId11_document.jpeg'); if (false === $expectedDocumentZip->close()) { throw new Exception("Could not close zip file \"{$docName}\"."); } self::assertNotEmpty($expectedImage, 'Embed image doesn\'t found.'); self::assertStringContainsString('/word/media/image_rId11_document.jpeg', $expectedContentTypesXml, '[Content_Types].xml missed "/word/media/image5_document.jpeg"'); self::assertStringContainsString('/word/_rels/header1.xml.rels', $expectedContentTypesXml, '[Content_Types].xml missed "/word/_rels/header1.xml.rels"'); self::assertStringContainsString('/word/_rels/footer1.xml.rels', $expectedContentTypesXml, '[Content_Types].xml missed "/word/_rels/footer1.xml.rels"'); self::assertStringNotContainsString('${documentContent}', $expectedMainPartXml, 'word/document.xml has no image.'); self::assertStringNotContainsString('${headerValue}', $expectedHeaderPartXml, 'word/header1.xml has no image.'); self::assertStringNotContainsString('${footerValue}', $expectedFooterPartXml, 'word/footer1.xml has no image.'); self::assertStringContainsString('media/image_rId11_document.jpeg', $expectedDocumentRelationsXml, 'word/_rels/document.xml.rels missed "media/image5_document.jpeg"'); self::assertStringContainsString('media/image_rId11_document.jpeg', $expectedHeaderRelationsXml, 'word/_rels/header1.xml.rels missed "media/image5_document.jpeg"'); self::assertStringContainsString('media/image_rId11_document.jpeg', $expectedFooterRelationsXml, 'word/_rels/footer1.xml.rels missed "media/image5_document.jpeg"'); unlink($docName); // dynamic generated doc $testFileName = 'images-test-sample.docx'; $phpWord = new \PhpOffice\PhpWord\PhpWord(); $section = $phpWord->addSection(); $section->addText('${Test:width=100:ratio=true}'); $objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007'); $objWriter->save($testFileName); self::assertFileExists($testFileName, "Generated file '{$testFileName}' not found!"); $resultFileName = 'images-test-result.docx'; $templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor($testFileName); unlink($testFileName); $templateProcessor->setImageValue('Test', $imagePath); $templateProcessor->setImageValue('Test1', $imagePath); $templateProcessor->setImageValue('Test2', $imagePath); $templateProcessor->saveAs($resultFileName); self::assertFileExists($resultFileName, "Generated file '{$resultFileName}' not found!"); $expectedDocumentZip = new ZipArchive(); $expectedDocumentZip->open($resultFileName); $expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml'); if (false === $expectedDocumentZip->close()) { throw new Exception("Could not close zip file \"{$resultFileName}\"."); } unlink($resultFileName); self::assertStringNotContainsString('${Test}', $expectedMainPartXml, 'word/document.xml has no image.'); } /** * @covers ::cloneBlock * @covers ::deleteBlock * @covers ::saveAs */ public function testCloneDeleteBlock(): void { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-delete-block.docx'); self::assertEquals( ['DELETEME', '/DELETEME', 'CLONEME', 'blockVariable', '/CLONEME'], $templateProcessor->getVariables() ); $docName = 'clone-delete-block-result.docx'; $templateProcessor->cloneBlock('CLONEME', 3); $templateProcessor->deleteBlock('DELETEME'); $templateProcessor->setValue('blockVariable#3', 'Test'); $templateProcessor->saveAs($docName); $docFound = file_exists($docName); unlink($docName); self::assertTrue($docFound); } /** * @covers ::getVariableCount */ public function testGetVariableCountCountsHowManyTimesEachPlaceholderIsPresent(): void { // create template with placeholders $phpWord = new PhpWord(); $section = $phpWord->addSection(); $header = $section->addHeader(); $header->addText('${a_field_that_is_present_three_times}'); $footer = $section->addFooter(); $footer->addText('${a_field_that_is_present_twice}'); $section2 = $phpWord->addSection(); $section2->addText(' ${a_field_that_is_present_one_time} ${a_field_that_is_present_three_times} ${a_field_that_is_present_twice} ${a_field_that_is_present_three_times} '); $objWriter = IOFactory::createWriter($phpWord); $templatePath = 'test.docx'; $objWriter->save($templatePath); $templateProcessor = new TemplateProcessor($templatePath); $variableCount = $templateProcessor->getVariableCount(); unlink($templatePath); self::assertEquals( [ 'a_field_that_is_present_three_times' => 3, 'a_field_that_is_present_twice' => 2, 'a_field_that_is_present_one_time' => 1, ], $variableCount ); } /** * @covers ::cloneBlock */ public function testCloneBlockCanCloneABlockTwice(): void { // create template with placeholders and block $phpWord = new PhpWord(); $section = $phpWord->addSection(); $documentElements = [ 'Title: ${title}', '${subreport}', '${subreport.id}: ${subreport.text}. ', '${/subreport}', ]; foreach ($documentElements as $documentElement) { $section->addText($documentElement); } $objWriter = IOFactory::createWriter($phpWord); $templatePath = 'test.docx'; $objWriter->save($templatePath); // replace placeholders and save the file $templateProcessor = new TemplateProcessor($templatePath); $templateProcessor->setValue('title', 'Some title'); $templateProcessor->cloneBlock('subreport', 2); $templateProcessor->setValue('subreport.id', '123', 1); $templateProcessor->setValue('subreport.text', 'Some text', 1); $templateProcessor->setValue('subreport.id', '456', 1); $templateProcessor->setValue('subreport.text', 'Some other text', 1); $templateProcessor->saveAs($templatePath); // assert the block has been cloned twice // and the placeholders have been replaced correctly $phpWord = IOFactory::load($templatePath); $sections = $phpWord->getSections(); /** @var \PhpOffice\PhpWord\Element\TextRun[] $actualElements */ $actualElements = $sections[0]->getElements(); unlink($templatePath); $expectedElements = [ 'Title: Some title', '123: Some text. ', '456: Some other text. ', ]; self::assertCount(count($expectedElements), $actualElements); foreach ($expectedElements as $i => $expectedElement) { self::assertEquals( $expectedElement, $actualElements[$i]->getElement(0)->getText() ); } } /** * @covers ::cloneBlock */ public function testCloneBlock(): void { $mainPart = ' ${CLONEME} This block will be cloned with ${variable} ${/CLONEME} '; $templateProcessor = new TestableTemplateProcesor($mainPart); $templateProcessor->cloneBlock('CLONEME', 3); self::assertEquals(3, substr_count($templateProcessor->getMainPart(), 'This block will be cloned with ${variable}')); } /** * @covers ::cloneBlock */ public function testCloneBlockWithVariables(): void { $mainPart = ' ${CLONEME} Address ${address}, Street ${street} ${/CLONEME} '; $templateProcessor = new TestableTemplateProcesor($mainPart); $templateProcessor->cloneBlock('CLONEME', 3, true, true); self::assertStringContainsString('Address ${address#1}, Street ${street#1}', $templateProcessor->getMainPart()); self::assertStringContainsString('Address ${address#2}, Street ${street#2}', $templateProcessor->getMainPart()); self::assertStringContainsString('Address ${address#3}, Street ${street#3}', $templateProcessor->getMainPart()); } public function testCloneBlockWithVariableReplacements(): void { $mainPart = ' ${CLONEME} City: ${city}, Street: ${street} ${/CLONEME} '; $replacements = [ ['city' => 'London', 'street' => 'Baker Street'], ['city' => 'New York', 'street' => '5th Avenue'], ['city' => 'Rome', 'street' => 'Via della Conciliazione'], ]; $templateProcessor = new TestableTemplateProcesor($mainPart); $templateProcessor->cloneBlock('CLONEME', 0, true, false, $replacements); self::assertStringContainsString('City: London, Street: Baker Street', $templateProcessor->getMainPart()); self::assertStringContainsString('City: New York, Street: 5th Avenue', $templateProcessor->getMainPart()); self::assertStringContainsString('City: Rome, Street: Via della Conciliazione', $templateProcessor->getMainPart()); } /** * Template macros can be fixed. * * @covers ::fixBrokenMacros */ public function testFixBrokenMacros(): void { $templateProcessor = new TestableTemplateProcesor(); $fixed = $templateProcessor->fixBrokenMacros('normal text'); self::assertEquals('normal text', $fixed); $fixed = $templateProcessor->fixBrokenMacros('${documentContent}'); self::assertEquals('${documentContent}', $fixed); $fixed = $templateProcessor->fixBrokenMacros('${documentContent}'); self::assertEquals('${documentContent}', $fixed); $fixed = $templateProcessor->fixBrokenMacros('$1500${documentContent}'); self::assertEquals('$1500${documentContent}', $fixed); $fixed = $templateProcessor->fixBrokenMacros('$1500${documentContent}'); self::assertEquals('$1500${documentContent}', $fixed); $fixed = $templateProcessor->fixBrokenMacros('25$ plus some info {hint}'); self::assertEquals('25$ plus some info {hint}', $fixed); $fixed = $templateProcessor->fixBrokenMacros('$15,000.00. ${variable_name}'); self::assertEquals('$15,000.00. ${variable_name}', $fixed); } /** * @covers ::getMainPartName */ public function testMainPartNameDetection(): void { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/document22-xml.docx'); $variables = ['test']; self::assertEquals($variables, $templateProcessor->getVariables()); } /** * @covers ::getVariables */ public function testGetVariables(): void { $templateProcessor = new TestableTemplateProcesor(); $variables = $templateProcessor->getVariablesForPart('normal text'); self::assertEquals([], $variables); $variables = $templateProcessor->getVariablesForPart('${documentContent}'); self::assertEquals(['documentContent'], $variables); $variables = $templateProcessor->getVariablesForPart('$15,000.00. ${variable_name}'); self::assertEquals(['variable_name'], $variables); } /** * @covers ::textNeedsSplitting */ public function testTextNeedsSplitting(): void { $templateProcessor = new TestableTemplateProcesor(); self::assertFalse($templateProcessor->textNeedsSplitting('${nothing-to-replace}')); $text = 'Hello ${firstname} ${lastname}'; self::assertTrue($templateProcessor->textNeedsSplitting($text)); $splitText = $templateProcessor->splitTextIntoTexts($text); self::assertFalse($templateProcessor->textNeedsSplitting($splitText)); } /** * @covers ::splitTextIntoTexts */ public function testSplitTextIntoTexts(): void { $templateProcessor = new TestableTemplateProcesor(); $splitText = $templateProcessor->splitTextIntoTexts('${nothing-to-replace}'); self::assertEquals('${nothing-to-replace}', $splitText); $splitText = $templateProcessor->splitTextIntoTexts('Hello ${firstname} ${lastname}'); self::assertEquals('Hello ${firstname} ${lastname}', $splitText); } public function testFindXmlBlockStart(): void { $toFind = ' This whole paragraph will be replaced with my ${title} '; $mainPart = ' ${value1} ${value2} . ' . $toFind . ' '; $templateProcessor = new TestableTemplateProcesor($mainPart); $position = $templateProcessor->findContainingXmlBlockForMacro('${title}', 'w:r'); self::assertEquals($toFind, $templateProcessor->getSlice($position['start'], $position['end'])); } public function testShouldReturnFalseIfXmlBlockNotFound(): void { $mainPart = ' this is my text containing a ${macro} '; $templateProcessor = new TestableTemplateProcesor($mainPart); //non-existing macro $result = $templateProcessor->findContainingXmlBlockForMacro('${fake-macro}', 'w:p'); self::assertFalse($result); //existing macro but not inside node looked for $result = $templateProcessor->findContainingXmlBlockForMacro('${macro}', 'w:fake-node'); self::assertFalse($result); //existing macro but end tag not found after macro $result = $templateProcessor->findContainingXmlBlockForMacro('${macro}', 'w:rPr'); self::assertFalse($result); } public function testShouldMakeFieldsUpdateOnOpen(): void { $settingsPart = ' '; $templateProcessor = new TestableTemplateProcesor(null, $settingsPart); $templateProcessor->setUpdateFields(true); self::assertStringContainsString('', $templateProcessor->getSettingsPart()); $templateProcessor->setUpdateFields(false); self::assertStringContainsString('', $templateProcessor->getSettingsPart()); } }