phplaravelphpword

Extend Writer class to be able to add own xml to Word document


I'm trying to extend the Word2007 Writer class to be able to add a custom Part class to the writerParts array.

Unfortunately, even simply creating a new class and extending Writer\Word2007 results in an empty Word document after save($filename) executes.

Here is the code for the custom Writer class:

namespace App\WordGen\Custom\Writer;

use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\Writer\Word2007;
use PhpOffice\PhpWord\Writer\WriterInterface;

class WordGen extends Word2007 implements WriterInterface {
    public function __construct(PhpWord $phpWord) {
        parent::__construct($phpWord);

        if (class_exists('\\App\\WordGen\\Custom\\Writer\\Chart')) {
            $part = new Chart();
            $part->setParentWriter($this);
            $this->writerParts['chart'] = $part;
        }
    }
}

I even went so far as to create a custom abstract Writer class that returns a WordGen writer object to the calling class like with PHPWord's IOFactory class.

When using php artisan tinker to run a few test scripts, I can see I successfully changed the writerPart reference, but when saving, I get the following error:

> $writer = $reportGen->getWriter();
= App\ReportGen\Custom\Writer\WordGen {#5028}

> $writer->save('\var\srv\www\reporter\doctest.docx');

   Error  Call to a member function setMedia() on null.

What could I be missing?

EDIT:

The issue with the writer class was that the parent constructor was looking for PhpOffice's classes in my Writer namespace. None of those parts were being initialized. I edited my constructor to instead override without calling the parent constructor and simply changed the foreach loop to look for the Parts in the respective PhpOffice library namespaces.

    foreach (array_keys($this->parts) as $partName) {
        $phpWordClass = '\\PhpOffice\\PhpWord\\Writer\\Word2007\\Part\\' . $partName;
        if (class_exists($phpWordClass)) {
            /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $part Type hint */
            $part = new $phpWordClass();
            $part->setParentWriter($this);
            $this->writerParts[strtolower($partName)] = $part;
        }
    }
   
    if (class_exists('\\App\\WordGen\\Custom\\Writer\\Chart')) { ...

Although now I want to be able to add raw XML as opposed to having the write() function build the Chart based on styles, etc. For this I created my own custom Chart element that extends the PhpOffice Chart element, and added properties $hasRawXml which is a boolean, and $rawXml that will contain the full string of xml that I want my custom Chart writer class to use instead.


Solution

  • Alright, so I managed to do this myself.

    For any other person's reference...

    After extending your Writer of choice and extending the Element Part, you can extend the Element itself, assign the element to the writer's part, and also extend the Element's Style to add your own properties.

    You need to be able to add this custom element to your section eventually. For this, you create a class that extends an AbstractContainer like Section and then add the required functions for adding the element to the Section. My class looks like this:

    <?php
    
    namespace App\WordGen\Custom;
    
    use PhpOffice\PhpWord\Element\Section;
    use PhpOffice\PhpWord\PhpWord;
    
    class SectionController extends Section
    {
        protected $phpWord;
        protected $section;
    
        public function __construct(PhpWord $phpWord, $sectionIndex = null)
        {
            $this->phpWord = $phpWord;
    
            $sections = $phpWord->getSections();
    
            if (is_null($sectionIndex)) {
                $this->section = $sections[count($sections) - 1];
            } else {
                $this->section = $sections[$sectionIndex];
            }
        }
    
        public function addNewElement($element, $index = null) {
            $element->setParentContainer($this->section);
            $element->setElementId();
    
            if (is_null($index)) {
                $element->setElementIndex($this->section->countElements() + 1);
        
                $this->section->elements[] = $element;
            } else {
                $elements = $this->section->getElements();
    
                $initial = array_splice($elements, 0, $index - 1);
                $initial[] = $element;
    
                /** @var \PhpOffice\PhpWord\Element\AbstractContainer[] $newElementsArray Type hint */
                $newElementsArray = $initial + $elements;
    
                for ($k = 0; $k < count($newElementsArray); $k++) {
                    $newElementsArray[$k]->setElementIndex($k + 1);
                }
    
                $this->section->elements = $newElementsArray;
            }
        }
    }
    
    

    In essence, you're extending 5 classes.

    You can then add the element this way:

        $chart = new Custom\Element\Chart(
            'column',
            ['A', 'B', 'C'],
            [1, 2, 3],
            $style,
            'Visits'
        );
    
        $control = new SectionController($document, 1);
        $control->addNewElement($chart);