Tinebase_Export_Doc - introduce sub_record in templates
authorPaul Mehrer <p.mehrer@metaways.de>
Thu, 20 Jul 2017 16:44:17 +0000 (18:44 +0200)
committerPaul Mehrer <p.mehrer@metaways.de>
Tue, 25 Jul 2017 08:06:29 +0000 (10:06 +0200)
Change-Id: I10d7870abb4055a499dbe65aa3e487c27ad71c48
Reviewed-on: http://gerrit.tine20.com/customers/5286
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Paul Mehrer <p.mehrer@metaways.de>
Tested-by: Paul Mehrer <p.mehrer@metaways.de>
tine20/Tinebase/CustomField.php
tine20/Tinebase/Export/Abstract.php
tine20/Tinebase/Export/Doc.php
tine20/Tinebase/Export/Richtext/TemplateProcessor.php
tine20/Tinebase/Model/CustomField/Config.php

index 408768d..53d575d 100644 (file)
@@ -529,9 +529,10 @@ class Tinebase_CustomField implements Tinebase_Controller_SearchInterface
                 $value = $customField->value;
             }
             if (true === $extendedResolving) {
-                $definition = is_object($config->definition) ? $config->definition->toArray() : (array)$config->definition;
-                $definition['value'] = $value;
-                $recordCfs[$config->name] = $definition;
+                //$definition = is_object($config->definition) ? $config->definition->toArray() : (array)$config->definition;
+                $clonedConfig = clone $config;
+                $clonedConfig->value = $value;
+                $recordCfs[$config->name] = $clonedConfig;
             } else {
                 $recordCfs[$config->name] = $value;
             }
index ed3b0fe..4d65573 100644 (file)
@@ -162,11 +162,15 @@ abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInt
      */
     protected $_hasTemplate = false;
 
+    /** @var Twig_Environment */
+    protected $_twigEnvironment = null;
     /**
      * @var Twig_TemplateWrapper|null
      */
     protected $_twigTemplate = null;
 
+    protected $_twigMapping = array();
+
     /**
      * @var string
      */
@@ -197,6 +201,11 @@ abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInt
 
     protected $_currentRowType = null;
 
+    /**
+     * @var Tinebase_Record_Abstract
+     */
+    protected $_currentRecord = null;
+
     protected $_getRelations = false;
 
     protected $_additionalRecords = array();
@@ -533,8 +542,9 @@ abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInt
     {
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' loading twig template...');
 
-        $tineTwigLoader = new Tinebase_Twig_CallBackLoader($this->_templateFileName, $this->_getLastModifiedTimeStamp(),
-            array($this, '_getTwigSource'));
+        $tineTwigLoader = new Twig_Loader_Chain(array(
+            new Tinebase_Twig_CallBackLoader($this->_templateFileName, $this->_getLastModifiedTimeStamp(),
+                array($this, '_getTwigSource'))));
 
         // TODO turn on caching
         // in order to cache the templates, we need to cache $this->_twigMapping too!
@@ -544,26 +554,27 @@ abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInt
             mkdir($cacheDir, 0777, true);
         }*/
 
-        $twig = new Twig_Environment($tineTwigLoader, array(
+        $this->_twigEnvironment = new Twig_Environment($tineTwigLoader, array(
             'autoescape' => 'json',
             'cache' => false, //$cacheDir
         ));
         /** @noinspection PhpUndefinedMethodInspection */
         /** @noinspection PhpUnusedParameterInspection */
-        $twig->getExtension('core')->setEscaper('json', function($twigEnv, $string, $charset) {
+        $this->_twigEnvironment->getExtension('core')->setEscaper('json', function($twigEnv, $string, $charset) {
             return json_encode($string);
         });
 
         $locale = $this->_locale;
         $translate = $this->_translate;
-        $twig->addFunction(new Twig_SimpleFunction('translate', function ($str) use($locale, $translate) {
-            return $translate->_($str, $locale);
-        }));
-        $twig->addFunction(new Twig_SimpleFunction('dateFormat', function ($date, $format) {
+        $this->_twigEnvironment->addFunction(new Twig_SimpleFunction('translate',
+            function ($str) use($locale, $translate) {
+                return $translate->_($str, $locale);
+            }));
+        $this->_twigEnvironment->addFunction(new Twig_SimpleFunction('dateFormat', function ($date, $format) {
             return Tinebase_Translation::dateToStringInTzAndLocaleFormat($date, null, null, $format);
         }));
 
-        $this->_twigTemplate = $twig->load($this->_templateFileName);
+        $this->_twigTemplate = $this->_twigEnvironment->load($this->_templateFileName);
     }
 
     /**
@@ -590,6 +601,28 @@ abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInt
         return filemtime($this->_templateFileName);
     }
 
+    protected function _getCurrentState()
+    {
+        return array(
+            '_firstIteration'       => $this->_firstIteration,
+            '_writeGenericHeader'   => $this->_writeGenericHeader,
+            '_groupByProperty'      => $this->_groupByProperty,
+            '_groupByProcessor'     => $this->_groupByProcessor,
+            '_lastGroupValue'       => $this->_lastGroupValue,
+            '_currentRecord'        => $this->_currentRecord,
+            '_currentRowType'       => $this->_currentRowType,
+            '_twigTemplate'         => $this->_twigTemplate,
+            '_twigMapping'          => $this->_twigMapping,
+        );
+    }
+
+    protected function _setCurrentState(array $array)
+    {
+        foreach ($array as $key => $value) {
+            $this->{$key} = $value;
+        }
+    }
+
     /**
      * add body rows
      *
@@ -632,11 +665,13 @@ abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInt
                     if (false === $first) {
                         $this->_endGroup();
                     }
+                    $this->_currentRecord = $record;
                     $this->_startGroup();
                 }
                 // TODO fix this?
                 //$this->_writeGroupHeading($record);
             }
+            $this->_currentRecord = $record;
 
             $this->_currentRowType = self::ROW_TYPE_RECORD;
 
@@ -701,6 +736,10 @@ abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInt
     protected function _resolveRecords(Tinebase_Record_RecordSet $_records)
     {
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' resolving export records...');
+        if ($_records->count() === 0) {
+            return;
+        }
+        $record = $_records->getFirstRecord();
         // FIXME think what to do
         // TODO fix ALL this!
 
@@ -735,58 +774,74 @@ abstract class Tinebase_Export_Abstract implements Tinebase_Record_IteratableInt
             Tinebase_Container::getInstance()->getGrantsOfRecords($_records, Tinebase_Core::getUser());
         }
 
-        $_records->customfields = array();
-        Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($_records, true);
+        if ($record->has('customfields')) {
+            $_records->customfields = array();
+            Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($_records, true);
+        }
+
+        if ($record->has('relations')) {
+            /** @var Tinebase_Record_Abstract $modelName */
+            $modelName = $_records->getRecordClassName();
 
-        /** @var Tinebase_Record_Abstract $modelName */
-        $modelName = $_records->getRecordClassName();
+            $relations = Tinebase_Relations::getInstance()->getMultipleRelations($modelName, 'Sql',
+                $_records->getArrayOfIds());
 
-        $relations = Tinebase_Relations::getInstance()->getMultipleRelations($modelName, 'Sql', $_records->getArrayOfIds());
+            $appConfig = Tinebase_Config::factory($this->_applicationName);
+            $modelConfig = $modelName::getConfiguration();
 
-        $appConfig = Tinebase_Config::factory($this->_applicationName);
-        if (null === ($modelConfig = $modelName::getConfiguration())) {
             /** @var Tinebase_Record_Abstract $record */
             foreach ($_records as $idx => $record) {
                 if (isset($relations[$idx])) {
                     $record->relations = $relations[$idx];
                 }
 
-                foreach ($this->_keyFields as $name => $keyField) {
-                    /** @var Tinebase_Config_KeyField $keyField */
-                    $keyField = $appConfig->{$keyField};
-                    $record->{$name} = $keyField->getTranslatedValue($record->{$name});
-                }
+                if (null === $modelConfig) {
+                    foreach ($this->_keyFields as $name => $keyField) {
+                        /** @var Tinebase_Config_KeyField $keyField */
+                        $keyField = $appConfig->{$keyField};
+                        $record->{$name} = $keyField->getTranslatedValue($record->{$name});
+                    }
 
-                foreach ($this->_virtualFields as $name => $virtualField) {
-                    $value = null;
-                    if (!empty($record->relations)) {
-                        /** @var Tinebase_Model_Relation $relation */
-                        foreach($record->relations as $relation) {
-                            if (    $relation->related_model  === $virtualField['relatedModel']  &&
+                    foreach ($this->_virtualFields as $name => $virtualField) {
+                        $value = null;
+                        if (!empty($record->relations)) {
+                            /** @var Tinebase_Model_Relation $relation */
+                            foreach ($record->relations as $relation) {
+                                if ($relation->related_model === $virtualField['relatedModel'] &&
                                     $relation->related_degree === $virtualField['relatedDegree'] &&
-                                    $relation->type           === $virtualField['type']) {
-                                $value = $relation->related_record;
-                                break;
+                                    $relation->type === $virtualField['type']
+                                ) {
+                                    $value = $relation->related_record;
+                                    break;
+                                }
                             }
                         }
+                        $record->{$name} = $value;
                     }
-                    $record->{$name} = $value;
-                }
 
-                foreach ($this->_foreignIdFields as $name => $controller) {
-                    if (!empty($record->{$name})) {
-                        $controller = $controller::getInstance();
-                        $record->{$name} = $controller->get($record->{$name});
+                    foreach ($this->_foreignIdFields as $name => $controller) {
+                        if (!empty($record->{$name})) {
+                            $controller = $controller::getInstance();
+                            $record->{$name} = $controller->get($record->{$name});
+                        }
+                    }
+                } else {
+                    foreach ($modelConfig->virtualFields as $field) {
+                        // resolve virtual relation record from relations property
+                        if (isset($field['type']) && $field['type'] === 'relation') {
+                            $fc = $field['config'];
+                            if (!empty($record->relations)) {
+                                foreach ($record->relations as $relation) {
+                                    if ($relation->type === $fc['type'] &&
+                                            $relation->related_model === ($fc['appName'] . '_Model_' . $fc['modelName'])) {
+                                        $record->{$field['key']} = $relation->related_record;
+                                    }
+                                }
+                            }
+                        }
                     }
                 }
             }
-        } else {
-            /** @var Tinebase_Record_Abstract $record */
-            /*foreach ($_records as $record) {
-                foreach ($modelConfig->getVirtualFields() as $field) {
-                    $modelConfig->
-                }
-            }*/
         }
 
         $_records->setTimezone(Tinebase_Core::getUserTimezone());
index 16b9fbb..c1eee31 100644 (file)
@@ -70,6 +70,9 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
      */
     protected $_currentProcessor = null;
 
+    protected $_subTwigTemplates = array();
+    protected $_subTwigMappings = array();
+
 
 
     /**
@@ -189,6 +192,25 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
 
     protected function _unwrapProcessors()
     {
+        /*if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD) {
+            $parent = $this->_currentProcessor->getParent();
+            $name = '${R' . $this->_currentProcessor->getConfig('name') . '}';
+            if ($parent->getType()  === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP) {
+                $this->_currentProcessor = $parent;
+            } elseif ($parent->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+                $parent = $parent->getParent();
+            }
+            $parent->setValue($name, '');
+        }
+        if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP) {
+            $parent = $this->_currentProcessor->getParent();
+            $name = '${R' . $this->_currentProcessor->getConfig('name') . '}';
+            if ($parent->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+                $parent = $parent->getParent();
+            }
+            $parent->setValue($name, '');
+        }*/
+
         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
             $this->_currentProcessor = $this->_currentProcessor->getParent();
             $this->_currentProcessor->setValue('${RECORD_BLOCK}', '');
@@ -217,7 +239,8 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
             $this->_currentProcessor = $this->_currentProcessor->getConfig('group');
         }
 
-        if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_GROUP) {
+        if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_GROUP ||
+                $this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP) {
             if ($this->_rowCount > 0 && $this->_currentProcessor->hasConfig('groupSeparator')) {
                 $this->_currentProcessor->append($this->_currentProcessor->getConfig('groupSeparator'));
             }
@@ -287,7 +310,8 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
                 $this->_currentProcessor = $this->_currentProcessor->getConfig('record');
             }
 
-            if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+            if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD ||
+                    $this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD) {
                 $data = '';
                 if ($this->_rowCount > 1 && $this->_currentProcessor->hasConfig('separator')) {
                     $data .= $this->_currentProcessor->getConfig('separator');
@@ -297,8 +321,18 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
                     $data .= $this->_currentProcessor->getConfig('header');
                 }
 
-                $data .= $this->_currentProcessor->getConfig('recordXml') . '${RECORD_BLOCK}';
-                $this->_currentProcessor->getParent()->setValue('${RECORD_BLOCK}', $data);
+                if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+                    $data .= $this->_currentProcessor->getConfig('recordXml') . '${RECORD_BLOCK}';
+                    $this->_currentProcessor->getParent()->setValue('${RECORD_BLOCK}', $data);
+                } else {
+                    $name = '${R' . $this->_currentProcessor->getConfig('name') . '}';
+                    $data .= $this->_currentProcessor->getConfig('recordXml') . $name;
+                    if (($parent = $this->_currentProcessor->getParent()) && $parent->getType() ===
+                            Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+                        $parent = $parent->getParent();
+                    }
+                    $parent->setValue($name, $data);
+                }
             } else {
                 throw new Tinebase_Exception_UnexpectedValue('template and definition do not match');
             }
@@ -311,6 +345,17 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
             return;
         }
 
+        if ($this->_currentProcessor->hasConfig('subgroups')) {
+            foreach ($this->_currentProcessor->getConfig('subgroups') as $property => $group) {
+                $this->_executeSubTemplate($property, $group);
+            }
+        }
+        if ($this->_currentProcessor->hasConfig('subrecords')) {
+            foreach ($this->_currentProcessor->getConfig('subrecords') as $property => $record) {
+                $this->_executeSubTemplate($property, $record);
+            }
+        }
+
         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD &&
                 $this->_currentProcessor->hasConfig('footer')) {
             $this->_currentProcessor->getParent()->setValue('${RECORD_BLOCK}',
@@ -319,6 +364,76 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
     }
 
     /**
+     * @param string $_name
+     * @param Tinebase_Export_Richtext_TemplateProcessor $_processor
+     */
+    protected function _executeSubTemplate($_name, Tinebase_Export_Richtext_TemplateProcessor $_processor)
+    {
+        $recordSet = $this->_currentRecord->{$_name};
+        if (is_array($recordSet)) {
+            if (count($recordSet) === 0) {
+                return;
+            }
+            if (($record = reset($recordSet)) instanceof Tinebase_Record_Abstract) {
+                $recordSet = new Tinebase_Record_RecordSet(get_class($record), $recordSet);
+            }
+        } elseif (is_object($recordSet)) {
+            if ($recordSet instanceof Tinebase_Record_Abstract) {
+                $recordSet = new Tinebase_Record_RecordSet(get_class($recordSet), array($recordSet));
+            } elseif (!$recordSet instanceof Tinebase_Record_RecordSet) {
+                return;
+            }
+        } else {
+            return;
+        }
+        $oldTemplateVariables = $this->_templateVariables;
+        $oldProcessor = $this->_currentProcessor;
+        $oldDocTemplate = $this->_docTemplate;
+        $oldRowCount = $this->_rowCount;
+        $subTempName = $this->_currentDataSource . '_' . $_name;
+        $oldState = $this->_getCurrentState();
+        foreach (array_keys($oldState) as $key) {
+            $this->{$key} = null;
+        }
+        $this->_templateVariables = null;
+
+
+        $this->_currentProcessor = $_processor;
+        $this->_rowCount = 0;
+        $this->_docTemplate = $_processor;
+
+        if (!isset($this->_subTwigTemplates[$subTempName])) {
+            $this->_twigEnvironment->getLoader()->addLoader(
+                new Tinebase_Twig_CallBackLoader($this->_templateFileName . $subTempName, $this->_getLastModifiedTimeStamp(),
+                    array($this, '_getTwigSource')));
+
+            $this->_twigTemplate = $this->_twigEnvironment->load($this->_templateFileName . $subTempName);
+            $this->_subTwigTemplates[$subTempName] = $this->_twigTemplate;
+            $this->_subTwigMappings[$subTempName] = $this->_twigMapping;
+        } else {
+            $this->_twigTemplate = $this->_subTwigTemplates[$subTempName];
+            $this->_twigMapping = $this->_subTwigMappings[$subTempName];
+        }
+
+        $this->processIteration($recordSet);
+
+        $result = $this->_cutXml($this->_currentProcessor->getMainPart());
+        $replacementName = ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP ?
+            'RSUBGROUP_' : 'RSUBRECORD_') . $_name;
+
+        $this->_templateVariables = $oldTemplateVariables;
+        $this->_docTemplate = $oldDocTemplate;
+        $this->_currentProcessor = $oldProcessor;
+        if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+            $this->_currentProcessor->getParent()->setValue($replacementName, $result);
+        } else {
+            $this->_currentProcessor->setValue($replacementName, $result);
+        }
+        $this->_rowCount = $oldRowCount;
+        $this->_setCurrentState($oldState);
+    }
+
+    /**
      * get word object
      *
      * @return \PhpOffice\PhpWord\PhpWord | \PhpOffice\PhpWord\TemplateProcessor
@@ -359,8 +474,13 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
 
         $this->_currentProcessor->setValue($_key, $_value);
 
-        if($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+        if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+            $this->_currentProcessor->getParent()->setValue($_key, $_value);
+        } elseif ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD) {
             $this->_currentProcessor->getParent()->setValue($_key, $_value);
+            if ($this->_currentProcessor->getParent()->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
+                $this->_currentProcessor->getParent()->getParent()->setValue($_key, $_value);
+            }
         }
     }
 
@@ -376,6 +496,9 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
 
     public function _getTwigSource()
     {
+        if (null === $this->_currentProcessor) {
+            $this->_onBeforeExportRecords();
+        }
         $i = 0;
         $source = '[';
         foreach ($this->_getTemplateVariables() as $placeholder) {
@@ -395,13 +518,12 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
 
     protected function _onBeforeExportRecords()
     {
-        if (null !== $this->_docTemplate) {
+        if (null !== $this->_docTemplate && null === $this->_currentProcessor) {
+            $this->_currentProcessor = $this->_docTemplate;
             $this->_findAndReplaceDatasources();
 
             if (empty($this->_dataSources)) {
                 $this->_findAndReplaceGroup($this->_docTemplate);
-                $this->_currentProcessor = $this->_docTemplate;
-
             }
         }
     }
@@ -454,9 +576,10 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
                 $_templateProcessor->replaceBlock('GROUP_SEPARATOR', '');
                 $config['groupSeparator'] = $groupSeparator;
             }
-            if (isset($config['recordRow']) && (isset($config['groupHeaderRow']) || isset($config['groupFooterRow']) ||
-                    isset($config['groupSeparatorRow']))) {
-                throw new Tinebase_Exception_UnexpectedValue('GROUP must not contain header, footer or separator rows');
+            if (isset($config['recordRow']) && (isset($config['groupHeader']) || isset($config['groupFooter']) ||
+                    isset($config['groupSeparator'])) && (isset($config['recordRow']['groupHeaderRow']) ||
+                    isset($config['recordRow']['groupFooterRow']) || isset($config['recordRow']['groupSeparatorRow']))) {
+                throw new Tinebase_Exception_UnexpectedValue('GROUP with record row must not contain header, footer or separator as table row and group block at the same time');
             }
             $config['groupXml'] = $this->_cutXml($groupProcessor->getMainPart());
             $groupProcessor->setMainPart('<?xml');
@@ -480,11 +603,13 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
     {
         if (null !== ($recordBlock = $_templateProcessor->cloneBlock('RECORD_BLOCK', 1, false))) {
             $_templateProcessor->replaceBlock('RECORD_BLOCK', '${RECORD_BLOCK}');
-            $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml', true,
+            $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $recordBlock, true,
                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD, $_templateProcessor);
+            $this->_findAndReplaceSubGroup($processor);
             $config = array(
-                'recordXml'     => $recordBlock
+                'recordXml'     => $this->_cutXml($processor->getMainPart())
             );
+            $processor->setMainPart('<?xml');
 
             if (null !== ($recordHeader = $_templateProcessor->cloneBlock('RECORD_HEADER', 1, false))) {
                 $_templateProcessor->replaceBlock('RECORD_HEADER', '');
@@ -500,7 +625,7 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
                 $_templateProcessor->replaceBlock('RECORD_SEPARATOR', '');
                 $config['separator'] = $recordSeparator;
             }
-            $processor->setConfig($config);
+            $processor->setConfig(array_merge($processor->getConfig(), $config));
             return $processor;
         }
 
@@ -519,6 +644,14 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
 
         if (preg_match('/<w:tbl.*?(\$\{twig:[^}]*record[^}]*})/is', $_templateProcessor->getMainPart(), $matches)) {
             $result['recordRow'] = $_templateProcessor->replaceRow($matches[1], '${RECORD_ROW}');
+            $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $result['recordRow'], true,
+                Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD, $_templateProcessor);
+            $this->_findAndReplaceSubGroup($processor);
+            $processor->setConfig(array(
+                'recordXml'     => $this->_cutXml($processor->getMainPart())
+            ));
+            $processor->setMainPart('<?xml');
+            $result['recordRowProcessor'] = $processor;
 
             if (strpos($_templateProcessor->getMainPart(), '${GROUP_HEADER}') !== false) {
                 $result['groupHeaderRow'] = str_replace('${GROUP_HEADER}', '', $_templateProcessor->replaceRow('${GROUP_HEADER}', ''));
@@ -538,6 +671,129 @@ class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_R
         return $result;
     }
 
+    /**
+     * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
+     * @throws Tinebase_Exception
+     */
+    protected function _findAndReplaceSubGroup(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
+    {
+        $parentConfig = array('subgroups' => array(), 'subrecords' => array());
+
+        do {
+            $foundGroup = null;
+            $foundRecord = null;
+            foreach ($_templateProcessor->getVariables() as $var) {
+                if (strpos($var, 'SUBGROUP') === 0) {
+                    $foundGroup = $var;
+                    break;
+                }
+                if (null === $foundRecord && strpos($var, 'SUBRECORD') === 0) {
+                    $foundRecord = $var;
+                }
+            }
+
+            $config = array();
+            if (null !== $foundGroup) {
+                if (null !== ($group = $_templateProcessor->cloneBlock($foundGroup, 1, false))) {
+                    list(,$propertyName) = explode('_', $foundGroup);
+                    $_templateProcessor->replaceBlock($foundGroup, '${R' . $foundGroup . '}');
+                    $groupProcessor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $group, true,
+                        Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP, $_templateProcessor);
+
+                    if (null === ($recordProcessor = $this->_findAndReplaceSubRecord($groupProcessor))) {
+                        throw new Tinebase_Exception('subgroup without record block: ' . $foundGroup);
+                    } else {
+                        $config['record'] = $recordProcessor;
+                    }
+                    if (null !== ($groupHeader = $_templateProcessor->cloneBlock('SUBG_HEADER_' . $propertyName, 1, false))) {
+                        $_templateProcessor->replaceBlock('SUBG_HEADER_' . $propertyName, '');
+                        $config['groupHeader'] = $groupHeader;
+                    }
+                    if (null !== ($groupFooter = $_templateProcessor->cloneBlock('SUBG_FOOTER_' . $propertyName, 1, false))) {
+                        $_templateProcessor->replaceBlock('SUBG_FOOTER_' . $propertyName, '');
+                        $config['groupFooter'] = $groupFooter;
+                    }
+                    if (null !== ($groupSeparator = $_templateProcessor->cloneBlock('SUBG_SEPARATOR_' . $propertyName, 1, false))) {
+                        $_templateProcessor->replaceBlock('SUBG_SEPARATOR_' . $propertyName, '');
+                        $config['groupSeparator'] = $groupSeparator;
+                    }
+                    $config['groupXml'] = $this->_cutXml($groupProcessor->getMainPart());
+                    $groupProcessor->setMainPart('<?xml');
+                    $groupProcessor->setConfig($config);
+                    $parentConfig['subgroups'][$propertyName] = $groupProcessor;
+                } else {
+                    throw new Tinebase_Exception('clone block failed after subgroup was found: ' . $foundGroup);
+                }
+            } elseif (null !== $foundRecord) {
+                if (null === ($recordProcessor = $this->_findAndReplaceSubRecord($_templateProcessor))) {
+                    throw new Tinebase_Exception('subrecord block failed: ' . $foundRecord);
+                }
+                list(,$propertyName) = explode('_', $foundRecord);
+                $parentConfig['subrecords'][$propertyName] = $recordProcessor;
+            } else {
+                break;
+            }
+        } while (true);
+
+        $config = $_templateProcessor->getConfig();
+        if (count($parentConfig['subgroups']) > 0) {
+            $config['subgroups'] = $parentConfig['subgroups'];
+        }
+        if (count($parentConfig['subrecords']) > 0) {
+            $config['subrecords'] = $parentConfig['subrecords'];
+        }
+        $_templateProcessor->setConfig($config);
+    }
+
+    /**
+     * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
+     * @return null|Tinebase_Export_Richtext_TemplateProcessor
+     * @throws Tinebase_Exception
+     */
+    protected function _findAndReplaceSubRecord(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
+    {
+        $foundRecord = null;
+        foreach ($_templateProcessor->getVariables() as $var) {
+            if (null === $foundRecord && strpos($var, 'SUBRECORD') === 0) {
+                $foundRecord = $var;
+                break;
+            }
+        }
+        if (null === $foundRecord) {
+            return null;
+        }
+
+        if (null !== ($recordBlock = $_templateProcessor->cloneBlock($foundRecord, 1, false))) {
+            list(,$propertyName) = explode('_', $foundRecord);
+            $_templateProcessor->replaceBlock($foundRecord, '${R' . $foundRecord . '}');
+            $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml', true,
+                Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD, $_templateProcessor);
+            $config = array(
+                'recordXml'     => $recordBlock,
+                'name'          => $foundRecord,
+            );
+
+            if (null !== ($recordHeader = $_templateProcessor->cloneBlock('SUBR_HEADER_' . $propertyName, 1, false))) {
+                $_templateProcessor->replaceBlock('SUBR_HEADER_' . $propertyName, '');
+                $config['header'] = $recordHeader;
+            }
+
+            if (null !== ($recordFooter = $_templateProcessor->cloneBlock('SUBR_FOOTER_' . $propertyName, 1, false))) {
+                $_templateProcessor->replaceBlock('SUBR_FOOTER_' . $propertyName, '');
+                $config['footer'] = $recordFooter;
+            }
+
+            if (null !== ($recordSeparator = $_templateProcessor->cloneBlock('SUBR_SEPARATOR_' . $propertyName, 1, false))) {
+                $_templateProcessor->replaceBlock('SUBR_SEPARATOR_' . $propertyName, '');
+                $config['separator'] = $recordSeparator;
+            }
+            $processor->setConfig($config);
+            return $processor;
+        } else {
+            throw new Tinebase_Exception('clone block failed after subrecord was found: ' . $foundRecord);
+        }
+    }
+
 
     /**
      * now simulate processIteration and finish with _onAfterExportRecords
index ecdd514..b44e3db 100644 (file)
@@ -22,7 +22,9 @@ class Tinebase_Export_Richtext_TemplateProcessor extends \PhpOffice\PhpWord\Temp
     const TYPE_STANDARD = 'standard';
     const TYPE_DATASOURCE = 'datasource';
     const TYPE_GROUP = 'group';
+    const TYPE_SUBGROUP = 'subgroup';
     const TYPE_RECORD = 'record';
+    const TYPE_SUBRECORD = 'subrecord';
 
     /**
      * Content of document rels (in XML format) of the temporary document.
@@ -257,7 +259,7 @@ class Tinebase_Export_Richtext_TemplateProcessor extends \PhpOffice\PhpWord\Temp
     {
         $xmlBlock = null;
         preg_match(
-            '/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p( [^>]+)?>.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
+            '/(<\?xml.*?)(<w:p>.*\${' . $blockname . '}.*?<\/w:p>)(.*)(<w:p>.*?\${\/' . $blockname . '}.*?<\/w:p>)/is',
             $this->tempDocumentMainPart,
             $matches
         );
@@ -270,6 +272,9 @@ class Tinebase_Export_Richtext_TemplateProcessor extends \PhpOffice\PhpWord\Temp
             }
 
             if ($replace) {
+                if (($pos = strrpos($matches[2], '<w:p>')) !== 0) {
+                    $matches[2] = substr($matches[2], $pos);
+                }
                 $this->tempDocumentMainPart = str_replace(
                     $matches[2] . $matches[3] . $matches[4],
                     implode('', $cloned),
@@ -312,6 +317,10 @@ class Tinebase_Export_Richtext_TemplateProcessor extends \PhpOffice\PhpWord\Temp
     public function setConfig(array $config)
     {
         $this->_config = $config;
+        if (Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD === $this->_type &&
+                isset($this->_config['recordXml'])) {
+
+        }
     }
 
     /**
@@ -334,4 +343,40 @@ class Tinebase_Export_Richtext_TemplateProcessor extends \PhpOffice\PhpWord\Temp
         }
         return $this->_config[$key];
     }
+
+    /**
+     * Returns array of all variables in template.
+     *
+     * @return string[]
+     */
+    public function getVariables()
+    {
+        $result = parent::getVariables();
+
+        switch($this->_type) {
+            /** @noinspection PhpMissingBreakStatementInspection */
+            case self::TYPE_DATASOURCE:
+                if (isset($this->_config['group'])) {
+                    $result = array_merge($result, $this->_config['group']->getVariables());
+                }
+            case self::TYPE_GROUP:
+                if (isset($this->_config['record'])) {
+                    $result = array_merge($result, $this->_config['record']->getVariables());
+                }
+                if (isset($this->_config['recordRow']) && isset($this->_config['recordRow']['recordRowProcessor'])) {
+                    /** @noinspection PhpUndefinedMethodInspection */
+                    $result = array_merge($result, $this->_config['recordRow']['recordRowProcessor']->getVariables());
+                }
+                // DO NOT return variables of sub groups or sub records
+                break;
+            case self::TYPE_SUBGROUP:
+            case self::TYPE_SUBRECORD:
+                if (isset($this->_config['recordXml'])) {
+                    $result = array_merge($result, $this->getVariablesForPart($this->_config['recordXml']));
+                }
+                break;
+        }
+
+        return array_unique($result);
+    }
 }
\ No newline at end of file
index 80feb06..ccf3be9 100644 (file)
@@ -21,6 +21,7 @@
  * @property    string      name
  * @property    string      definition
  * @property    string      account_grants
+ * @property    string      value
  */
 class Tinebase_Model_CustomField_Config extends Tinebase_Record_Abstract 
 {
@@ -44,7 +45,8 @@ class Tinebase_Model_CustomField_Config extends Tinebase_Record_Abstract
         'model'             => array('presence' => 'required', 'allowEmpty' => false ),
         'name'              => array('presence' => 'required', 'allowEmpty' => false ),
         'definition'        => array('presence' => 'required', 'allowEmpty' => false ),
-        'account_grants'    => array('allowEmpty' => true ),       
+        'account_grants'    => array('allowEmpty' => true ),
+        'value'             => array('allowEmpty' => true ),
     );
     
     /**