[FEATURE] Introduce fluid data-processor for menus 08/50608/5
authorBenjamin Kott <benjamin.kott@wfp2.com>
Sat, 12 Nov 2016 15:18:32 +0000 (16:18 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Thu, 1 Dec 2016 13:29:49 +0000 (14:29 +0100)
This menu processor utilizes HMENU to generate a json encoded menu
string that will be decoded again and assigned to FLUIDTEMPLATE as
variable. Additional DataProcessing is supported and will be applied
to each record.

Resolves: #78672
Releases: master
Change-Id: Ic7aa230f8fbd524df77a54bf92f4145d6eadaf3e
Reviewed-on: https://review.typo3.org/50608
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Frans Saris <franssaris@gmail.com>
Tested-by: Frans Saris <franssaris@gmail.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
typo3/sysext/core/Documentation/Changelog/master/Feature-78672-IntroduceFluidDataProcessorForMenus.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/DataProcessing/MenuProcessor.php [new file with mode: 0644]

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-78672-IntroduceFluidDataProcessorForMenus.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-78672-IntroduceFluidDataProcessorForMenus.rst
new file mode 100644 (file)
index 0000000..8a40039
--- /dev/null
@@ -0,0 +1,47 @@
+.. include:: ../../Includes.txt
+
+==========================================================
+Feature: #78672 - Introduce fluid data processor for menus
+==========================================================
+
+See :issue:`78672`
+
+Description
+===========
+
+This menu processor utilizes HMENU to generate a json encoded menu
+string that will be decoded again and assigned to FLUIDTEMPLATE as
+variable. Additional DataProcessing is supported and will be applied
+to each record.
+
+Options:
+`as` The variable to be used within the result
+`levels` Number of levels of the menu
+`expandAll` If false, submenus will only render if the parent page is active
+`includeSpacer` If true, pagetype spacer will be included in the menu
+`titleField` Field that should be used for the title
+
+See HMENU docs for more options.
+https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Hmenu/Index.html
+
+Example TypoScript configuration:
+
+.. code-block:: typoscript
+   10 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
+   10 {
+      special = list
+      special.value.field = pages
+      levels = 7
+      as = menu
+      expandAll = 1
+      includeSpacer = 1
+      titleField = nav_title // title
+      dataProcessing {
+         10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
+         10 {
+            references.fieldName = media
+         }
+      }
+   }
+
+.. index:: Fluid, TypoScript, Frontend
diff --git a/typo3/sysext/frontend/Classes/DataProcessing/MenuProcessor.php b/typo3/sysext/frontend/Classes/DataProcessing/MenuProcessor.php
new file mode 100644 (file)
index 0000000..0bf0856
--- /dev/null
@@ -0,0 +1,494 @@
+<?php
+namespace TYPO3\CMS\Frontend\DataProcessing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
+
+/**
+ * This menu processor utilizes HMENU to generate a json encoded menu
+ * string that will be decoded again and assigned to FLUIDTEMPLATE as
+ * variable. Additional DataProcessing is supported and will be applied
+ * to each record.
+ *
+ * Options:
+ * as - The variable to be used within the result
+ * levels - Number of levels of the menu
+ * expandAll = If false, submenus will only render if the parent page is active
+ * includeSpacer = If true, pagetype spacer will be included in the menu
+ * titleField = Field that should be used for the title
+ *
+ * See HMENU docs for more options.
+ * https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Hmenu/Index.html
+ *
+ *
+ * Example TypoScript configuration:
+ *
+ * 10 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
+ * 10 {
+ *   special = list
+ *   special.value.field = pages
+ *   levels = 7
+ *   as = menu
+ *   expandAll = 1
+ *   includeSpacer = 1
+ *   titleField = nav_title // title
+ *   dataProcessing {
+ *     10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
+ *     10 {
+ *        references.fieldName = media
+ *     }
+ *   }
+ * }
+ *
+ */
+class MenuProcessor implements DataProcessorInterface
+{
+    const LINK_PLACEHOLDER = '###LINKPLACEHOLDER###';
+    const TARGET_PLACEHOLDER = '###TARGETPLACEHOLDER###';
+
+    /**
+     * The content object renderer
+     *
+     * @var ContentObjectRenderer
+     */
+    public $cObj;
+
+    /**
+     * The processor configuration
+     *
+     * @var array
+     */
+    protected $processorConfiguration;
+
+    /**
+     * Allowed configuration keys for menu generation, other keys
+     * will throw an exception to prevent configuration errors.
+     *
+     * @var array
+     */
+    public $allowedConfigurationKeys = [
+        'cache_period',
+        'entryLevel',
+        'entryLevel.',
+        'special',
+        'special.',
+        'minItems',
+        'minItems.',
+        'maxItems',
+        'maxItems.',
+        'begin',
+        'begin.',
+        'excludeUidList',
+        'excludeUidList.',
+        'excludeDoktypes',
+        'includeNotInMenu',
+        'alwaysActivePIDlist',
+        'alwaysActivePIDlist.',
+        'protectLvar',
+        'addQueryString',
+        'if',
+        'if.',
+        'levels',
+        'expandAll',
+        'includeSpacer',
+        'as',
+        'titleField',
+        'dataProcessing',
+        'dataProcessing.'
+    ];
+
+    /**
+     * Remove keys from configuration that should not be passed
+     * to HMENU to prevent configuration errors
+     *
+     * @var array
+     */
+    public $removeConfigurationKeysForHmenu = [
+        'levels',
+        'expandAll',
+        'includeSpacer',
+        'as',
+        'titleField',
+        'dataProcessing',
+        'dataProcessing.'
+    ];
+
+    /**
+     * @var array
+     */
+    protected $menuConfig = [
+        'wrap' => '[|]'
+    ];
+
+    /**
+     * @var array
+     */
+    protected $menuLevelConfig = [
+        'doNotLinkIt' => '1',
+        'wrapItemAndSub' => '{|}, |*| {|}, |*| {|}',
+        'stdWrap.' => [
+            'cObject' => 'COA',
+            'cObject.' => [
+                '10' => 'USER',
+                '10.' => [
+                    'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->getDataAsJson',
+                    'stdWrap.' => [
+                        'wrap' => '"data":|'
+                    ]
+                ],
+                '20' => 'TEXT',
+                '20.' => [
+                    'field' => 'nav_title // title',
+                    'trim' => '1',
+                    'wrap' => ',"title":|',
+                    'preUserFunc' => 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->jsonEncodeUserFunc'
+                ],
+                '21' => 'TEXT',
+                '21.' => [
+                    'value' => self::LINK_PLACEHOLDER,
+                    'wrap' => ',"link":|',
+                ],
+                '22' => 'TEXT',
+                '22.' => [
+                    'value' => self::TARGET_PLACEHOLDER,
+                    'wrap' => ',"target":|',
+                ],
+                '30' => 'TEXT',
+                '30.' => [
+                    'value' => '0',
+                    'wrap' => ',"active":|'
+                ],
+                '40' => 'TEXT',
+                '40.' => [
+                    'value' => '0',
+                    'wrap' => ',"current":|'
+                ],
+                '50' => 'TEXT',
+                '50.' => [
+                    'value' => '0',
+                    'wrap' => ',"spacer":|'
+                ]
+            ]
+        ]
+    ];
+
+    /**
+     * @var array
+     */
+    public $menuDefaults = [
+        'levels' => 1,
+        'expandAll' => 1,
+        'includeSpacer' => 0,
+        'as' => 'menu',
+        'titleField' => 'nav_title // title'
+    ];
+
+    /**
+     * @var int
+     */
+    protected $menuLevels;
+
+    /**
+     * @var int
+     */
+    protected $menuExpandAll;
+
+    /**
+     * @var int
+     */
+    protected $menuIncludeSpacer;
+
+    /**
+     * @var string
+     */
+    protected $menuTitleField;
+
+    /**
+     * @var string
+     */
+    protected $menuTargetVariableName;
+
+    /**
+     * @var ContentDataProcessor
+     */
+    protected $contentDataProcessor;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->contentDataProcessor = GeneralUtility::makeInstance(ContentDataProcessor::class);
+    }
+
+    /**
+     * Get configuration value from processorConfiguration
+     *
+     * @param string $key
+     * @return string
+     */
+    protected function getConfigurationValue($key)
+    {
+        return $this->cObj->stdWrapValue($key, $this->processorConfiguration, $this->menuDefaults[$key]);
+    }
+
+    /**
+     * Validate configuration
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function validateConfiguration()
+    {
+        $invalidArguments = [];
+        foreach ($this->processorConfiguration as $key => $value) {
+            if (!in_array($key, $this->allowedConfigurationKeys)) {
+                $invalidArguments[str_replace('.', '', $key)] = $key;
+            }
+        }
+        if (!empty($invalidArguments)) {
+            throw new \InvalidArgumentException('MenuProcessor Configuration contains invalid Arguments: ' . implode(', ', $invalidArguments), 1478806566);
+        }
+    }
+
+    /**
+     * Prepare Configuration
+     */
+    public function prepareConfiguration()
+    {
+        $this->menuConfig += $this->processorConfiguration;
+        // Filter configuration
+        foreach ($this->menuConfig as $key => $value) {
+            if (in_array($key, $this->removeConfigurationKeysForHmenu)) {
+                unset($this->menuConfig[$key]);
+            }
+        }
+        // Process special value
+        if (isset($this->menuConfig['special.']['value.'])) {
+            $this->menuConfig['special.']['value'] = $this->cObj->stdWrap($this->menuConfig['special.']['value'], $this->menuConfig['special.']['value.']);
+            unset($this->menuConfig['special.']['value.']);
+        }
+    }
+
+    /**
+     * @return void
+     */
+    public function prepareLevelConfiguration()
+    {
+        $this->menuLevelConfig['stdWrap.']['cObject.'] = array_replace_recursive(
+            $this->menuLevelConfig['stdWrap.']['cObject.'],
+            [
+                '20.' => [
+                    'field' => $this->menuTitleField,
+                ]
+            ]
+        );
+    }
+
+    /**
+     * @return void
+     */
+    public function prepareLevelLanguageConfiguration()
+    {
+        if ($this->menuConfig['special'] === 'language') {
+            $this->menuLevelConfig['stdWrap.']['cObject.'] = array_replace_recursive(
+                $this->menuLevelConfig['stdWrap.']['cObject.'],
+                [
+                    '60' => 'TEXT',
+                    '60.' => [
+                        'value' => '1',
+                        'wrap' => ',"available":|'
+                    ],
+                    '70' => 'TEXT',
+                    '70.' => [
+                        'value' => $this->menuConfig['special.']['value'],
+                        'listNum.' => [
+                            'stdWrap.' => [
+                                'data' => 'register:count_HMENU_MENUOBJ',
+                                'wrap' => '|-1'
+                            ],
+                            'splitChar' => ','
+                        ],
+                        'wrap' => ',"languageUid":"|"'
+                    ]
+                ]
+            );
+        }
+    }
+
+    /**
+     * @return void
+     */
+    public function buildConfiguration()
+    {
+        for ($i = 1; $i <= $this->menuLevels; $i++) {
+            $this->menuConfig[$i] = 'TMENU';
+            $this->menuConfig[$i . '.']['IProcFunc'] = 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->replacePlaceholderInRenderedMenuItem';
+            if ($i > 1) {
+                $this->menuConfig[$i . '.']['stdWrap.']['wrap'] = ',"children": [|]';
+            }
+            $this->menuConfig[$i . '.']['expAll'] = $this->menuExpandAll;
+            $this->menuConfig[$i . '.']['NO'] = '1';
+            $this->menuConfig[$i . '.']['NO.'] = $this->menuLevelConfig;
+            if ($this->menuIncludeSpacer) {
+                $this->menuConfig[$i . '.']['SPC'] = '1';
+                $this->menuConfig[$i . '.']['SPC.'] = $this->menuConfig[$i . '.']['NO.'];
+                $this->menuConfig[$i . '.']['SPC.']['stdWrap.']['cObject.']['50.']['value'] = '1';
+            }
+            $this->menuConfig[$i . '.']['IFSUB'] = '1';
+            $this->menuConfig[$i . '.']['IFSUB.'] = $this->menuConfig[$i . '.']['NO.'];
+            $this->menuConfig[$i . '.']['ACT'] = '1';
+            $this->menuConfig[$i . '.']['ACT.'] = $this->menuConfig[$i . '.']['NO.'];
+            $this->menuConfig[$i . '.']['ACT.']['stdWrap.']['cObject.']['30.']['value'] = '1';
+            $this->menuConfig[$i . '.']['ACTIFSUB'] = '1';
+            $this->menuConfig[$i . '.']['ACTIFSUB.'] = $this->menuConfig[$i . '.']['ACT.'];
+            $this->menuConfig[$i . '.']['CUR'] = '1';
+            $this->menuConfig[$i . '.']['CUR.'] = $this->menuConfig[$i . '.']['ACT.'];
+            $this->menuConfig[$i . '.']['CUR.']['stdWrap.']['cObject.']['40.']['value'] = '1';
+            $this->menuConfig[$i . '.']['CURIFSUB'] = '1';
+            $this->menuConfig[$i . '.']['CURIFSUB.'] = $this->menuConfig[$i . '.']['CUR.'];
+            if ($this->menuConfig['special'] === 'language') {
+                $this->menuConfig[$i . '.']['USERDEF1'] = $this->menuConfig[$i . '.']['NO'];
+                $this->menuConfig[$i . '.']['USERDEF1.'] = $this->menuConfig[$i . '.']['NO.'];
+                $this->menuConfig[$i . '.']['USERDEF1.']['stdWrap.']['cObject.']['60.']['value'] = '0';
+                $this->menuConfig[$i . '.']['USERDEF2'] = $this->menuConfig[$i . '.']['ACT'];
+                $this->menuConfig[$i . '.']['USERDEF2.'] = $this->menuConfig[$i . '.']['ACT.'];
+                $this->menuConfig[$i . '.']['USERDEF2.']['stdWrap.']['cObject.']['60.']['value'] = '0';
+            }
+        }
+    }
+
+    /**
+     * @param ContentObjectRenderer $cObj The data of the content element or page
+     * @param array $contentObjectConfiguration The configuration of Content Object
+     * @param array $processorConfiguration The configuration of this processor
+     * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
+     * @return array the processed data as key/value store
+     */
+    public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
+    {
+        $this->cObj = $cObj;
+        $this->processorConfiguration = $processorConfiguration;
+
+        // Get Configuration
+        $this->menuLevels = (int)$this->getConfigurationValue('levels') ?: 1;
+        $this->menuExpandAll = (int)$this->getConfigurationValue('expandAll');
+        $this->menuIncludeSpacer = (int)$this->getConfigurationValue('includeSpacer');
+        $this->menuTargetVariableName = $this->getConfigurationValue('as');
+        $this->menuTitleField = $this->getConfigurationValue('titleField');
+
+        // Validate Configuration
+        $this->validateConfiguration();
+
+        // Build Configuration
+        $this->prepareConfiguration();
+        $this->prepareLevelConfiguration();
+        $this->prepareLevelLanguageConfiguration();
+        $this->buildConfiguration();
+
+        // Process Configuration
+        $menuContentObject = $cObj->getContentObject('HMENU');
+        $renderedMenu = $menuContentObject->render($this->menuConfig);
+        if (!$renderedMenu) {
+            return $processedData;
+        }
+
+        // Process menu
+        $menu = json_decode($renderedMenu, true);
+        $processedMenu = [];
+
+        foreach ($menu as $key => $page) {
+            $processedMenu[$key] = $this->processAdditionalDataProcessors($page, $processorConfiguration);
+        }
+
+        // Return processed data
+        $processedData[$this->menuTargetVariableName] = $processedMenu;
+        return $processedData;
+    }
+
+    /**
+     * Process additional data processors
+     *
+     * @param array $page
+     * @param array $processorConfiguration
+     */
+    protected function processAdditionalDataProcessors($page, $processorConfiguration)
+    {
+        if (is_array($page['children'])) {
+            foreach ($page['children'] as $key => $item) {
+                $page['children'][$key] = $this->processAdditionalDataProcessors($item, $processorConfiguration);
+            }
+        }
+        /** @var ContentObjectRenderer $recordContentObjectRenderer */
+        $recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
+        $recordContentObjectRenderer->start($page['data'], 'pages');
+        $processedPage = $this->contentDataProcessor->process($recordContentObjectRenderer, $processorConfiguration, $page);
+        return $processedPage;
+    }
+
+    /**
+     * Gets the data of the current record in JSON format
+     *
+     * @return string JSON encoded data
+     */
+    public function getDataAsJson()
+    {
+        return $this->jsonEncode($this->cObj->data);
+    }
+
+    /**
+     * This UserFunc encodes the content as Json
+     *
+     * @param string $content
+     * @param array $conf
+     * @return string JSON encoded content
+     */
+    public function jsonEncodeUserFunc($content, $conf)
+    {
+        $content = $this->jsonEncode($content);
+        return $content;
+    }
+
+    /**
+     * JSON Encode
+     *
+     * @param mixed $value
+     * @return string
+     */
+    public function jsonEncode($value)
+    {
+        return json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);
+    }
+
+    /**
+     * This UserFunc gets the link and the target
+     *
+     * @param array $menuItem
+     * @param array $conf
+     * @return void
+     */
+    public function replacePlaceholderInRenderedMenuItem($menuItem, $conf)
+    {
+        $link = $this->jsonEncode($menuItem['linkHREF']['HREF']);
+        $target = $this->jsonEncode($menuItem['linkHREF']['TARGET']);
+
+        $menuItem['parts']['title'] = str_replace(self::LINK_PLACEHOLDER, $link, $menuItem['parts']['title']);
+        $menuItem['parts']['title'] = str_replace(self::TARGET_PLACEHOLDER, $target, $menuItem['parts']['title']);
+
+        return $menuItem;
+    }
+}