Commit 1d2b3b7c authored by Jo Hasenau's avatar Jo Hasenau Committed by Benni Mack
Browse files

[FEATURE] Add DataProcessor to resolve FlexForm data

This adds a new data processor which converts the
XML structure of a given FlexForm field into a PHP
array to be used in Fluid templates.

Resolves: #89509
Releases: master
Change-Id: I267defe0b26bca33f636c44edd53d695f1bcb572
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62108

Tested-by: Jo Hasenau's avatarJo Hasenau <info@cybercraft.de>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Jo Hasenau's avatarJo Hasenau <info@cybercraft.de>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent e4d3e3bc
.. include:: ../../Includes.txt
=========================================================
Feature: #89509 - Data Processor to resolve FlexForm data
=========================================================
See :issue:`89509`
Description
===========
TYPO3 offeres "FlexForms", which can be used to store data within an XML
structure inside a single DB column. Since this information could also be
relevant in the view, a new data processor
:php:`TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor` is added. It
converts the FlexForm data of a given field into a Fluid readable array.
Options
-------
:`fieldName`: Field name of the column the FlexForm data is stored in (default: `pi_flexform`).
:`as`: The variable to be used within the result (default: `flexFormData`).
Example of a minimal TypoScript configuration
---------------------------------------------
.. code-block:: typoscript
10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
The converted array can be accessed within the Fluid template
with the :html:`{flexFormData}` variable.
Example of an advanced TypoScript configuration
-----------------------------------------------
.. code-block:: typoscript
10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
10 {
fieldName = my_flexform_field
as = myOutputVariable
}
The converted array can be accessed within the Fluid template
with the :html:`{myOutputVariable}` variable.
Example with a custom sub processor
------------------------------------
.. code-block:: typoscript
10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
10 {
fieldName = my_flexform_field
as = myOutputVariable
dataProcessing {
10 = Vendor\MyExtension\DataProcessing\CustomFlexFormProcessor
}
}
Impact
======
It's now possible to access the FlexForm data of a field in a
readable way in the Fluid template.
.. index:: Fluid, TypoScript, Frontend
<?php
declare(strict_types=1);
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\Service\FlexFormService;
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 data processor converts the XML structure of a given FlexForm field
* into a fluid readable array.
*
* Options:
* fieldname - The name of the field containing the FlexForm to be converted
* as - The variable, the generated array should be assigned to
*
* Example of a minimal TypoScript configuration, which processes the field
* `pi_flexform` and assigns the array to the `flexFormData` variable:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
*
* Example of an advanced TypoScript configuration, which processes the field
* `my_flexform_field` and assigns the array to the `myOutputVariable` variable:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
* 10 {
* fieldName = my_flexform_field
* as = myOutputVariable
* }
*/
class FlexFormProcessor implements DataProcessorInterface
{
/**
* @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
): array {
// The field name to process
$fieldName = $cObj->stdWrapValue('fieldName', $processorConfiguration, 'pi_flexform');
if (!isset($processedData['data'][$fieldName])) {
return $processedData;
}
// Process FlexForm
$originalValue = $processedData['data'][$fieldName];
if (!is_string($originalValue)) {
return $processedData;
}
$flexFormData = GeneralUtility::makeInstance(FlexFormService::class)
->convertFlexFormContentToArray($originalValue);
// Set the target variable
$targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'flexFormData');
if (isset($processorConfiguration['dataProcessing.']) && is_array($processorConfiguration['dataProcessing.'])) {
$flexFormData = $this->processAdditionalDataProcessors($flexFormData, $processorConfiguration);
}
$processedData[$targetVariableName] = $flexFormData;
return $processedData;
}
/**
* Recursively process sub processors of a data processor
*
* @param array $data
* @param array $processorConfiguration
* @return array
*/
public function processAdditionalDataProcessors(array $data, array $processorConfiguration): array
{
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObjectRenderer->start([$data]);
return GeneralUtility::makeInstance(ContentDataProcessor::class)->process(
$contentObjectRenderer,
$processorConfiguration,
$data
);
}
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Frontend\Tests\Unit\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\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
/**
* Testcase
*/
class FlexFormProcessorTest extends UnitTestCase
{
/**
* @var ContentObjectRenderer
*/
protected $contentObjectRenderer;
protected function setUp(): void
{
parent::setUp();
$this->resetSingletonInstances = true;
$this->contentObjectRenderer = $this->prophesize(ContentObjectRenderer::class);
$this->prepareFlexFormService();
}
/**
* @test
*/
public function customFieldNameDoesNotExistsWillReturnUnchangedProcessedData(): void
{
$processorConfiguration = ['as' => 'myOutputVariable', 'fieldName' => 'non_existing_field'];
$this->contentObjectRenderer
->stdWrapValue('fieldName', $processorConfiguration, 'pi_flexform')
->willReturn('non_existing_field');
$this->contentObjectRenderer
->stdWrapValue('as', $processorConfiguration, 'flexFormData')
->willReturn('myOutputVariable');
$processedData = [
'data' => [
'pi_flexform' => $this->getFlexFormStructure()
]
];
$subject = new FlexFormProcessor();
$expected = $subject->process(
$this->contentObjectRenderer->reveal(),
[],
$processorConfiguration,
$processedData
);
self::assertSame($expected, $processedData);
}
/**
* @test
*/
public function customFieldNameDoesNotContainFlexFormDataWillReturnUnchangedProcessedData(): void
{
$processorConfiguration = ['as' => 'myOutputVariable', 'fieldName' => 'custom_field'];
$this->contentObjectRenderer
->stdWrapValue('fieldName', $processorConfiguration, 'pi_flexform')
->willReturn('custom_field');
$this->contentObjectRenderer
->stdWrapValue('as', $processorConfiguration, 'flexFormData')
->willReturn('myOutputVariable');
$processedData = [
'data' => [
'custom_field' => 123456789
]
];
$subject = new FlexFormProcessor();
$expected = $subject->process(
$this->contentObjectRenderer->reveal(),
[],
$processorConfiguration,
$processedData
);
self::assertSame($expected, $processedData);
}
/**
* @test
*/
public function customOutputVariableForProcessorWillReturnParsedFlexFormToDataCustomVariable(): void
{
$processorConfiguration = ['as' => 'myCustomVar'];
$this->contentObjectRenderer
->stdWrapValue('fieldName', $processorConfiguration, 'pi_flexform')
->willReturn('pi_flexform');
$this->contentObjectRenderer
->stdWrapValue('as', $processorConfiguration, 'flexFormData')
->willReturn('myCustomVar');
$processedData = [
'data' => [
'pi_flexform' => $this->getFlexFormStructure()
]
];
$subject = new FlexFormProcessor();
$expected = $subject->process(
$this->contentObjectRenderer->reveal(),
[],
$processorConfiguration,
$processedData
);
self::assertIsArray($expected['myCustomVar']);
}
/**
* @test
*/
public function defaultOutputVariableForProcessorWillBeUsed(): void
{
$processorConfiguration = [];
$this->contentObjectRenderer
->stdWrapValue('fieldName', $processorConfiguration, 'pi_flexform')
->willReturn('pi_flexform');
$this->contentObjectRenderer
->stdWrapValue('as', $processorConfiguration, 'flexFormData')
->willReturn('flexFormData');
$processedData = [
'data' => [
'pi_flexform' => $this->getFlexFormStructure()
]
];
$subject = new FlexFormProcessor();
$expected = $subject->process(
$this->contentObjectRenderer->reveal(),
[],
$processorConfiguration,
$processedData
);
self::assertSame($expected['data']['pi_flexform'], $processedData['data']['pi_flexform']);
self::assertIsArray($expected['flexFormData']);
}
/**
* @test
*/
public function defaultConfigurationWithCustomFieldNameWillReturnParsedFlexFormToDefaultOutputVariable(): void
{
$processorConfiguration = ['as' => 'myOutputVariable', 'fieldName' => 'my_flexform'];
$this->contentObjectRenderer
->stdWrapValue('fieldName', $processorConfiguration, 'pi_flexform')
->willReturn('my_flexform');
$this->contentObjectRenderer
->stdWrapValue('as', $processorConfiguration, 'flexFormData')
->willReturn('myOutputVariable');
$processedData = [
'data' => [
'my_flexform' => $this->getFlexFormStructure()
]
];
$subject = new FlexFormProcessor();
$expected = $subject->process(
$this->contentObjectRenderer->reveal(),
[],
$processorConfiguration,
$processedData
);
self::assertIsArray($expected['myOutputVariable']);
}
/**
* @test
*/
public function subDataProcessorIsResolved(): void
{
$this->prepareFlexFormServiceWithSubDataProcessorData();
$processorConfiguration['dataProcessing.'] = [10 => 'Vendor\Acme\DataProcessing\FooProcessor'];
$processedData = [
'data' => [
'pi_flexform' => $this->getFlexFormStructure()
]
];
$this->contentObjectRenderer
->stdWrapValue('fieldName', $processorConfiguration, 'pi_flexform')
->willReturn('pi_flexform');
$this->contentObjectRenderer
->stdWrapValue('as', $processorConfiguration, 'flexFormData')
->willReturn('flexFormData');
$convertedFlexFormData = [
'options' => [
'hotels' => 0,
'images' => '12'
]
];
$this->contentObjectRenderer->start([$convertedFlexFormData])->shouldBeCalled();
$contentDataProcessor = $this->prophesize(ContentDataProcessor::class);
$renderedDataFromProcessors = [
'options' => [
'hotels' => 0,
'images' => 'img/foo.jpg'
]
];
$contentDataProcessor
->process($this->contentObjectRenderer->reveal(), $processorConfiguration, $convertedFlexFormData)
->willReturn($renderedDataFromProcessors);
GeneralUtility::addInstance(ContentObjectRenderer::class, $this->contentObjectRenderer->reveal());
GeneralUtility::addInstance(ContentDataProcessor::class, $contentDataProcessor->reveal());
$subject = new FlexFormProcessor();
$actual = $subject->process(
$this->contentObjectRenderer->reveal(),
[],
$processorConfiguration,
$processedData
);
self::assertSame(array_merge($processedData, ['flexFormData' => $renderedDataFromProcessors]), $actual);
}
private function getFlexFormStructure(): string
{
return '<![CDATA[<?xml version="1.0" encoding="utf-8" standalone="yes" ?>'
. '<T3FlexForms>
<data>
<sheet index="options">
<language index="lDEF">
<field index="hotels">
<value index="vDEF">0</value>
</field>
</language>
</sheet>
</data>
</T3FlexForms>'
. ']]>';
}
private function prepareFlexFormService(): void
{
$convertedFlexFormData = [
'options' => [
'hotels' => 0
]
];
$flexFormService = $this->prophesize(FlexFormService::class);
$flexFormService->convertFlexFormContentToArray($this->getFlexFormStructure())->willReturn($convertedFlexFormData);
GeneralUtility::setSingletonInstance(FlexFormService::class, $flexFormService->reveal());
}
private function prepareFlexFormServiceWithSubDataProcessorData(): void
{
$convertedFlexFormData = [
'options' => [
'hotels' => 0,
'images' => '12'
]
];
$flexFormService = $this->prophesize(FlexFormService::class);
$flexFormService->convertFlexFormContentToArray($this->getFlexFormStructure())->willReturn($convertedFlexFormData);
GeneralUtility::setSingletonInstance(FlexFormService::class, $flexFormService->reveal());
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment