This commit is contained in:
parent
7deb010318
commit
fb863cdf21
|
|
@ -14,6 +14,7 @@ Place announcement text here.
|
|||
- Introduced writer for the "Table Alignment" element (see `\PhpOffice\PhpWord\Writer\Word2007\Element\TableAlignment`). - @RomanSyroeshko
|
||||
- Supported indexed arrays in arguments of `TemplateProcessor::setValue()`. - @RomanSyroeshko #618
|
||||
- Introduced automatic output escaping for OOXML, ODF, HTML, and RTF. To turn the feature on use `phpword.ini` or `\PhpOffice\PhpWord\Settings`. - @RomanSyroeshko #483
|
||||
- Supported processing of headers and footers in `TemplateProcessor::applyXslStyleSheet()`. - @RomanSyroeshko #335
|
||||
|
||||
### Changed
|
||||
- Improved error message for the case when `autoload.php` is not found. - @RomanSyroeshko #371
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ With PHPWord, you can create OOXML, ODF, or RTF documents dynamically using your
|
|||
- Insert charts (pie, doughnut, bar, line, area, scatter, radar)
|
||||
- Insert form fields (textinput, checkbox, and dropdown)
|
||||
- Create document from templates
|
||||
- Use XSL 1.0 style sheets to transform main document part of OOXML template
|
||||
- Use XSL 1.0 style sheets to transform headers, main document part, and footers of an OOXML template
|
||||
- ... and many more features on progress
|
||||
|
||||
## Requirements
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
"ext-zip": "Allows writing OOXML and ODF",
|
||||
"ext-gd2": "Allows adding images",
|
||||
"ext-xmlwriter": "Allows writing OOXML and ODF",
|
||||
"ext-xsl": "Allows applying XSL style sheet to main document part of OOXML template",
|
||||
"ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template",
|
||||
"dompdf/dompdf": "Allows writing PDF"
|
||||
},
|
||||
"autoload": {
|
||||
|
|
|
|||
|
|
@ -50,8 +50,7 @@ Features
|
|||
- Insert charts (pie, doughnut, bar, line, area, scatter, radar)
|
||||
- Insert form fields (textinput, checkbox, and dropdown)
|
||||
- Create document from templates
|
||||
- Use XSL 1.0 style sheets to transform main document part of OOXML
|
||||
template
|
||||
- Use XSL 1.0 style sheets to transform headers, main document part, and footers of an OOXML template
|
||||
- ... and many more features on progress
|
||||
|
||||
File formats
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Example:
|
|||
$templateProcessor->setValue('Name', 'John Doe');
|
||||
$templateProcessor->setValue(array('City', 'Street'), array('Detroit', '12th Street'));
|
||||
|
||||
It is not possible to directly add new OOXML elements to the template file being processed, but it is possible to transform main document part of the template using XSLT (see ``TemplateProcessor::applyXslStyleSheet``).
|
||||
It is not possible to directly add new OOXML elements to the template file being processed, but it is possible to transform headers, main document part, and footers of the template using XSLT (see ``TemplateProcessor::applyXslStyleSheet``).
|
||||
|
||||
See ``Sample_07_TemplateCloneRow.php`` for example on how to create
|
||||
multirow from a single row in a template by using ``TemplateProcessor::cloneRow``.
|
||||
|
|
|
|||
|
|
@ -100,7 +100,49 @@ class TemplateProcessor
|
|||
);
|
||||
$index++;
|
||||
}
|
||||
$this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName('word/document.xml'));
|
||||
$this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName($this->getMainPartName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $xml
|
||||
* @param \XSLTProcessor $xsltProcessor
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \PhpOffice\PhpWord\Exception\Exception
|
||||
*/
|
||||
protected function transformSingleXml($xml, $xsltProcessor)
|
||||
{
|
||||
$domDocument = new \DOMDocument();
|
||||
if (false === $domDocument->loadXML($xml)) {
|
||||
throw new Exception('Could not load the given XML document.');
|
||||
}
|
||||
|
||||
$transformedXml = $xsltProcessor->transformToXml($domDocument);
|
||||
if (false === $transformedXml) {
|
||||
throw new Exception('Could not transform the given XML document.');
|
||||
}
|
||||
|
||||
return $transformedXml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $xml
|
||||
* @param \XSLTProcessor $xsltProcessor
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function transformXml($xml, $xsltProcessor)
|
||||
{
|
||||
if (is_array($xml)) {
|
||||
foreach ($xml as &$item) {
|
||||
$item = $this->transformSingleXml($item, $xsltProcessor);
|
||||
}
|
||||
} else {
|
||||
$xml = $this->transformSingleXml($xml, $xsltProcessor);
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,35 +151,26 @@ class TemplateProcessor
|
|||
* Note: since the method doesn't make any guess on logic of the provided XSL style sheet,
|
||||
* make sure that output is correctly escaped. Otherwise you may get broken document.
|
||||
*
|
||||
* @param \DOMDocument $xslDOMDocument
|
||||
* @param \DOMDocument $xslDomDocument
|
||||
* @param array $xslOptions
|
||||
* @param string $xslOptionsURI
|
||||
* @param string $xslOptionsUri
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \PhpOffice\PhpWord\Exception\Exception
|
||||
*/
|
||||
public function applyXslStyleSheet($xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '')
|
||||
public function applyXslStyleSheet($xslDomDocument, $xslOptions = array(), $xslOptionsUri = '')
|
||||
{
|
||||
$xsltProcessor = new \XSLTProcessor();
|
||||
|
||||
$xsltProcessor->importStylesheet($xslDOMDocument);
|
||||
|
||||
if (false === $xsltProcessor->setParameter($xslOptionsURI, $xslOptions)) {
|
||||
$xsltProcessor->importStylesheet($xslDomDocument);
|
||||
if (false === $xsltProcessor->setParameter($xslOptionsUri, $xslOptions)) {
|
||||
throw new Exception('Could not set values for the given XSL style sheet parameters.');
|
||||
}
|
||||
|
||||
$xmlDOMDocument = new \DOMDocument();
|
||||
if (false === $xmlDOMDocument->loadXML($this->tempDocumentMainPart)) {
|
||||
throw new Exception('Could not load XML from the given template.');
|
||||
}
|
||||
|
||||
$xmlTransformed = $xsltProcessor->transformToXml($xmlDOMDocument);
|
||||
if (false === $xmlTransformed) {
|
||||
throw new Exception('Could not transform the given XML document.');
|
||||
}
|
||||
|
||||
$this->tempDocumentMainPart = $xmlTransformed;
|
||||
$this->tempDocumentHeaders = $this->transformXml($this->tempDocumentHeaders, $xsltProcessor);
|
||||
$this->tempDocumentMainPart = $this->transformXml($this->tempDocumentMainPart, $xsltProcessor);
|
||||
$this->tempDocumentFooters = $this->transformXml($this->tempDocumentFooters, $xsltProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -365,14 +398,14 @@ class TemplateProcessor
|
|||
*/
|
||||
public function save()
|
||||
{
|
||||
foreach ($this->tempDocumentHeaders as $index => $headerXML) {
|
||||
$this->zipClass->addFromString($this->getHeaderName($index), $this->tempDocumentHeaders[$index]);
|
||||
foreach ($this->tempDocumentHeaders as $index => $xml) {
|
||||
$this->zipClass->addFromString($this->getHeaderName($index), $xml);
|
||||
}
|
||||
|
||||
$this->zipClass->addFromString('word/document.xml', $this->tempDocumentMainPart);
|
||||
$this->zipClass->addFromString($this->getMainPartName(), $this->tempDocumentMainPart);
|
||||
|
||||
foreach ($this->tempDocumentFooters as $index => $headerXML) {
|
||||
$this->zipClass->addFromString($this->getFooterName($index), $this->tempDocumentFooters[$index]);
|
||||
foreach ($this->tempDocumentFooters as $index => $xml) {
|
||||
$this->zipClass->addFromString($this->getFooterName($index), $xml);
|
||||
}
|
||||
|
||||
// Close zip file
|
||||
|
|
@ -414,8 +447,6 @@ class TemplateProcessor
|
|||
* Finds parts of broken macros and sticks them together.
|
||||
* Macros, while being edited, could be implicitly broken by some of the word processors.
|
||||
*
|
||||
* @since 0.13.0
|
||||
*
|
||||
* @param string $documentPart The document part in XML representation.
|
||||
*
|
||||
* @return string
|
||||
|
|
@ -470,18 +501,6 @@ class TemplateProcessor
|
|||
return $matches[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the footer file for $index.
|
||||
*
|
||||
* @param integer $index
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFooterName($index)
|
||||
{
|
||||
return sprintf('word/footer%d.xml', $index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the header file for $index.
|
||||
*
|
||||
|
|
@ -494,6 +513,26 @@ class TemplateProcessor
|
|||
return sprintf('word/header%d.xml', $index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getMainPartName()
|
||||
{
|
||||
return 'word/document.xml';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the footer file for $index.
|
||||
*
|
||||
* @param integer $index
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getFooterName($index)
|
||||
{
|
||||
return sprintf('word/footer%d.xml', $index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the start position of the nearest table row before $offset.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase
|
|||
$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 (array('${employee.', '${scoreboard.') as $needle) {
|
||||
$templateProcessor->applyXslStyleSheet($xslDOMDocument, array('needle' => $needle));
|
||||
$xslDomDocument = new \DOMDocument();
|
||||
$xslDomDocument->load(__DIR__ . "/_files/xsl/remove_tables_by_needle.xsl");
|
||||
foreach (array('${employee.', '${scoreboard.', '${reference.') as $needle) {
|
||||
$templateProcessor->applyXslStyleSheet($xslDomDocument, array('needle' => $needle));
|
||||
}
|
||||
|
||||
$documentFqfn = $templateProcessor->save();
|
||||
|
|
@ -48,19 +48,25 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase
|
|||
|
||||
$templateZip = new \ZipArchive();
|
||||
$templateZip->open($templateFqfn);
|
||||
$templateXml = $templateZip->getFromName('word/document.xml');
|
||||
$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);
|
||||
$documentXml = $documentZip->getFromName('word/document.xml');
|
||||
$documentHeaderXml = $documentZip->getFromName('word/header1.xml');
|
||||
$documentMainPartXml = $documentZip->getFromName('word/document.xml');
|
||||
$documentFooterXml = $documentZip->getFromName('word/footer1.xml');
|
||||
if (false === $documentZip->close()) {
|
||||
throw new \Exception("Could not close zip file \"{$documentZip}\".");
|
||||
}
|
||||
|
||||
$this->assertNotEquals($documentXml, $templateXml);
|
||||
$this->assertNotEquals($templateHeaderXml, $documentHeaderXml);
|
||||
$this->assertNotEquals($templateMainPartXml, $documentMainPartXml);
|
||||
$this->assertNotEquals($templateFooterXml, $documentFooterXml);
|
||||
|
||||
return $documentFqfn;
|
||||
}
|
||||
|
|
@ -82,19 +88,25 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase
|
|||
|
||||
$actualDocumentZip = new \ZipArchive();
|
||||
$actualDocumentZip->open($actualDocumentFqfn);
|
||||
$actualDocumentXml = $actualDocumentZip->getFromName('word/document.xml');
|
||||
$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);
|
||||
$expectedDocumentXml = $expectedDocumentZip->getFromName('word/document.xml');
|
||||
$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}\".");
|
||||
}
|
||||
|
||||
$this->assertXmlStringEqualsXmlString($expectedDocumentXml, $actualDocumentXml);
|
||||
$this->assertXmlStringEqualsXmlString($expectedHeaderXml, $actualHeaderXml);
|
||||
$this->assertXmlStringEqualsXmlString($expectedMainPartXml, $actualMainPartXml);
|
||||
$this->assertXmlStringEqualsXmlString($expectedFooterXml, $actualFooterXml);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,14 +121,14 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase
|
|||
{
|
||||
$templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/blank.docx');
|
||||
|
||||
$xslDOMDocument = new \DOMDocument();
|
||||
$xslDOMDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl');
|
||||
$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, array(1 => 'somevalue'));
|
||||
@$templateProcessor->applyXslStyleSheet($xslDomDocument, array(1 => 'somevalue'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,21 +136,21 @@ final class TemplateProcessorTest extends \PHPUnit_Framework_TestCase
|
|||
*
|
||||
* @covers ::applyXslStyleSheet
|
||||
* @expectedException \PhpOffice\PhpWord\Exception\Exception
|
||||
* @expectedExceptionMessage Could not load XML from the given template.
|
||||
* @expectedExceptionMessage Could not load the given XML document.
|
||||
* @test
|
||||
*/
|
||||
final public function testXslStyleSheetCanNotBeAppliedOnFailureOfLoadingXmlFromTemplate()
|
||||
{
|
||||
$templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/corrupted_main_document_part.docx');
|
||||
|
||||
$xslDOMDocument = new \DOMDocument();
|
||||
$xslDOMDocument->load(__DIR__ . '/_files/xsl/passthrough.xsl');
|
||||
$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);
|
||||
@$templateProcessor->applyXslStyleSheet($xslDomDocument);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue