Skip to content

Commit

Permalink
[FEATURE] Add inheritance mode control
Browse files Browse the repository at this point in the history
Adds an extension configuration option and corresponding Form option
and option ViewHelper which allows a site to control how Flux handles
inheritance of FlexForm values (specific to pages).

Normally, Flux will only allow FlexForm values to be inherited from a
parent page to children if the child uses the same page layout (template)
as the parent page(s). This new option allows changing this inheritance
mode from the default "restricted" to an "unrestricted" mode which
allows FlexForm values to be inherited even if the child and parent uses
different page layouts.

This is useful when you have different page templates which use the
same FlexForm fields (for example, a shared set of fields) and you wish
to inherit such fields from parents even if you use different page
layouts on the child/parent(s).

There is a similar Form option which allows setting this inheritance
mode on a per-template basis. Use the new ViewHelper
flux:form.option.inheritanceMode for this, with either "restricted"
or "unrestricted" as value. If no mode is specified on a template then
the inheritance mode defined in extension configuration will be used.
  • Loading branch information
NamelessCoder committed May 9, 2024
1 parent 37cd93a commit 871fcf5
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 51 deletions.
1 change: 1 addition & 0 deletions Classes/Enum/ExtensionOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ class ExtensionOption
public const OPTION_PLUG_AND_PLAY_DIRECTORY = 'plugAndPlayDirectory';
public const OPTION_PAGE_INTEGRATION = 'pageIntegration';
public const OPTION_FLEXFORM_TO_IRRE = 'flexFormToIrre';
public const OPTION_INHERITANCE_MODE = 'inheritanceMode';
}
1 change: 1 addition & 0 deletions Classes/Enum/FormOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ class FormOption
public const RECORD_TABLE = 'recordTable';
public const TRANSFORM = 'transform';
public const HIDE_NATIVE_FIELDS = 'hideNativeFields';
public const INHERITANCE_MODE = 'inheritanceMode';
}
3 changes: 2 additions & 1 deletion Classes/Integration/NormalizedData/DataAccessTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
declare(strict_types=1);
namespace FluidTYPO3\Flux\Integration\NormalizedData;

use FluidTYPO3\Flux\Enum\ExtensionOption;
use FluidTYPO3\Flux\Form\Transformation\FormDataTransformer;
use FluidTYPO3\Flux\Provider\Interfaces\FormProviderInterface;
use FluidTYPO3\Flux\Provider\ProviderResolver;
Expand All @@ -27,7 +28,7 @@ public function injectConfigurationManager(ConfigurationManagerInterface $config
$this->settings = $this->configurationManager->getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS
);
if (!ExtensionConfigurationUtility::getOption(ExtensionConfigurationUtility::OPTION_FLEXFORM_TO_IRRE)) {
if (!ExtensionConfigurationUtility::getOption(ExtensionOption::OPTION_FLEXFORM_TO_IRRE)) {
return;
}

Expand Down
42 changes: 27 additions & 15 deletions Classes/Provider/PageProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
*/

use FluidTYPO3\Flux\Builder\ViewBuilder;
use FluidTYPO3\Flux\Enum\ExtensionOption;
use FluidTYPO3\Flux\Enum\FormOption;
use FluidTYPO3\Flux\Enum\PreviewOption;
use FluidTYPO3\Flux\Form;
use FluidTYPO3\Flux\Form\Transformation\FormDataTransformer;
use FluidTYPO3\Flux\Service\CacheService;
use FluidTYPO3\Flux\Service\PageService;
use FluidTYPO3\Flux\Service\TypoScriptService;
use FluidTYPO3\Flux\Service\WorkspacesAwareRecordService;
use FluidTYPO3\Flux\Utility\ExtensionConfigurationUtility;
use FluidTYPO3\Flux\Utility\ExtensionNamingUtility;
use FluidTYPO3\Flux\Utility\MiscellaneousUtility;
use FluidTYPO3\Flux\Utility\RecursiveArrayUtility;
Expand Down Expand Up @@ -456,12 +459,33 @@ protected function convertXmlToArray(string $xml): ?array
*/
protected function getInheritanceTree(array $row): array
{
$previousTemplate = $row[self::FIELD_ACTION_MAIN] ?? null;
$configuredInheritance = ExtensionConfigurationUtility::getOption(ExtensionOption::OPTION_INHERITANCE_MODE);

$form = $this->getForm($row);

$defaultInheritanceMode = ($form ? $form->getOption(FormOption::INHERITANCE_MODE) : $configuredInheritance)
?? $configuredInheritance;

$records = $this->loadRecordTreeFromDatabase($row);
foreach ($records as $index => $record) {
$hasSubAction = false === empty($record[self::FIELD_ACTION_SUB]);
if ($hasSubAction) {
$childForm = $this->getForm($record);
$subAction = $record[self::FIELD_ACTION_SUB] ?? null;
$hasSubAction = !empty($subAction);

if ($childForm) {
$inheritanceMode = $childForm->getOption(FormOption::INHERITANCE_MODE) ?? $defaultInheritanceMode;
} else {
$inheritanceMode = $defaultInheritanceMode;
}

if ($inheritanceMode === 'restricted'
&& $hasSubAction
&& ($subAction ?? $previousTemplate) !== $previousTemplate
) {
return array_slice($records, 0, $index + 1);
}
$previousTemplate = $subAction ?? $previousTemplate;
}
return $records;
}
Expand All @@ -473,7 +497,7 @@ protected function getInheritedConfiguration(array $row): array
$uid = $row['uid'] ?? '';
$cacheKey = $tableName . $tableFieldName . $uid;
if (false === isset(self::$cache[$cacheKey])) {
$tree = $this->getInheritanceTree($row);
$tree = array_reverse($this->getInheritanceTree($row), true);
$data = [];
foreach ($tree as $branch) {
$values = $this->getFlexFormValuesSingle($branch, self::FIELD_NAME_SUB);
Expand Down Expand Up @@ -513,18 +537,6 @@ protected function unsetInheritedValues(Form\FormInterface $field, array $values
return $values;
}

/**
* @return mixed
*/
protected function getParentFieldValue(array $row)
{
$parentFieldName = $this->getParentFieldName($row);
if (null !== $parentFieldName && false === isset($row[$parentFieldName])) {
$row = $this->recordService->getSingle((string) $this->getTableName($row), $parentFieldName, $row['uid']);
}
return $row[$parentFieldName] ?? null;
}

protected function loadRecordTreeFromDatabase(array $record): array
{
if (empty($record)) {
Expand Down
8 changes: 1 addition & 7 deletions Classes/Utility/ExtensionConfigurationUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@

class ExtensionConfigurationUtility
{
public const OPTION_DEBUG_MODE = 'debugMode';
public const OPTION_DOKTYPES = 'doktypes';
public const OPTION_HANDLE_ERRORS = 'handleErrors';
public const OPTION_AUTOLOAD = 'autoload';
public const OPTION_PLUG_AND_PLAY = 'plugAndPlay';
public const OPTION_PLUG_AND_PLAY_DIRECTORY = 'plugAndPlayDirectory';
public const OPTION_PAGE_INTEGRATION = 'pageIntegration';
public const OPTION_FLEXFORM_TO_IRRE = 'flexFormToIrre';

protected static array $defaults = [
Expand All @@ -27,6 +20,7 @@ class ExtensionConfigurationUtility
ExtensionOption::OPTION_PLUG_AND_PLAY_DIRECTORY => DropInContentTypeDefinition::DESIGN_DIRECTORY,
ExtensionOption::OPTION_PAGE_INTEGRATION => true,
ExtensionOption::OPTION_FLEXFORM_TO_IRRE => false,
ExtensionOption::OPTION_INHERITANCE_MODE => 'restricted',
];

public static function initialize(?string $extensionConfiguration): void
Expand Down
67 changes: 67 additions & 0 deletions Classes/ViewHelpers/Form/Option/InheritanceModeViewHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace FluidTYPO3\Flux\ViewHelpers\Form\Option;

/*
* This file is part of the FluidTYPO3/Flux project under GPLv2 or later.
*
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/

use FluidTYPO3\Flux\Enum\FormOption;
use FluidTYPO3\Flux\ViewHelpers\Form\OptionViewHelper;

/**
* ### Inheritance mode option
*
* Control how this Form will handle inheritance (page context only).
* There are two possible values of this option:
*
* - restricted
* - unrestricted
*
* Note that the default (the mode which is used if you do NOT specify
* the mode with this ViewHelper/option) is defined by the Flux extension
* configuration. If you do not change the extension configuration then
* the default behavior is "restricted". Any template that wants to use
* a mode other than the default *MUST* specify the mode with this option.
*
* When the option is set to "restricted" either by this ViewHelper or
* by extension configuration, the inheritance behavior matches the
* Flux behavior pre version 10.1.x, meaning that inheritance will only
* happen if the parent (page) has selected the same Form (layout) as
* the current page. As soon as a different Form is encountered in a
* parent, the inheritance stops. In short: inheritance only works for
* identical Forms.
*
* Alternatively, when the option is set to "unrestricted", the above
* constraint is removed and inheritance can happen for Forms which are
* NOT the same.
*
* This makes sense to use if you have different page templates which
* use the same values (for example a shared set of fields) and you want
* child pages to be able to inherit these values from parents even if
* the child page has selected a different page layout.
*
* #### Example
*
* <flux:form.option.inheritanceMode value="unrestricted" />
* (which is the same as:)
* <flux:form.option.inheritanceMode>unrestricted</flux:form.option.inheritanceMode>
*
* Or:
*
* <flux:form.option.inheritanceMode value="restricted" />
* (which is the same as:)
* <flux:form.option.inheritanceMode>restricted</flux:form.option.inheritanceMode>
*/
class InheritanceModeViewHelper extends OptionViewHelper
{
public static string $option = FormOption::INHERITANCE_MODE;

public function initializeArguments(): void
{
$this->registerArgument('value', 'string', 'Mode of inheritance, either "restricted" or "unrestricted".');
}
}
3 changes: 3 additions & 0 deletions Resources/Private/Language/locallang.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
<trans-unit id="extension_configuration.flexFormToIrre">
<source>EXPERIMENTAL - FlexForm-to-IRRE: FlexForm XML to IRRE storage. WARNING - destroys existing FlexForm data in DB when saving records! Enabling this option makes Flux capable of storing FlexForm XML data structures as normalized IRRE records. By using the DataAccessTrait, classes such as controllers can read out the data in an array structure compatible with vanilla FlexForm data.</source>
</trans-unit>
<trans-unit id="extension_configuration.inheritanceMode">
<source>Default mode of Form value inheritance: The default mode is "restricted" which means inheritance follows the Flux pre 10.1.x rules that page variable inheritance only happens if the parent page uses the same page layout as the child page. This can be set to "unrestrited" to remove that constraint and allow inheritance when parent and child pages do not use the same page layout. This option changes the global default - individual templates can also set the mode which that specific template uses, with the flux:form.option.inheritanceMode ViewHelper.</source>
</trans-unit>
<trans-unit id="content_types">
<source>Flux-based Content Type</source>
</trans-unit>
Expand Down
97 changes: 69 additions & 28 deletions Tests/Unit/Provider/PageProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

use FluidTYPO3\Flux\Builder\ViewBuilder;
use FluidTYPO3\Flux\Enum\FormOption;
use FluidTYPO3\Flux\Form;
use FluidTYPO3\Flux\Form\Transformation\FormDataTransformer;
use FluidTYPO3\Flux\Provider\PageProvider;
Expand All @@ -21,6 +22,8 @@
use FluidTYPO3\Flux\Tests\Unit\AbstractTestCase;
use PHPUnit\Framework\MockObject\MockObject;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
use TYPO3\CMS\Fluid\View\TemplatePaths;
Expand Down Expand Up @@ -203,34 +206,48 @@ public function testGetControllerExtensionKeyFromRecordReturnsPresetKeyOnUnrecog
/**
* @dataProvider getInheritanceTreeTestValues
*/
public function testGetInheritanceTree(array $input, array $expected): void
public function testGetInheritanceTree(array $input, array $forms, ?array $expected = null): void
{
$record = ['uid' => 1];
$instance = $this->getMockBuilder($this->createInstanceClassName())
->setConstructorArgs($this->getConstructorArguments())
->onlyMethods(['loadRecordTreeFromDatabase'])
->onlyMethods(['loadRecordTreeFromDatabase', 'getForm'])
->getMock();
$instance->method('getForm')->willReturnOnConsecutiveCalls(...$forms);
$instance->method('loadRecordTreeFromDatabase')->with($record)->willReturn($input);
$result = $this->callInaccessibleMethod($instance, 'getInheritanceTree', $record);
$this->assertEquals($expected, $result);
$this->assertEquals($expected ?? $input, $result);
}

public function getInheritanceTreeTestValues(): array
{
return [
'empty tree returns empty' => [[], []],
'empty tree returns empty' => [[], [], []],
'no sub action returns full tree' => [
[[PageProvider::FIELD_ACTION_MAIN => 'testmain']],
[[PageProvider::FIELD_ACTION_MAIN => 'testmain']]
[Form::create()],
],
'defined sub action halts reading' => [
[
[PageProvider::FIELD_ACTION_MAIN => ''],
[PageProvider::FIELD_ACTION_SUB => 'testsub'],
[PageProvider::FIELD_ACTION_SUB => 'notincluded']
],
[Form::create(), Form::create(), Form::create()],
[[PageProvider::FIELD_ACTION_MAIN => ''], [PageProvider::FIELD_ACTION_SUB => 'testsub']],
],
'inheritanceMode=unrestricted continues even if sub-template differs' => [
[
[PageProvider::FIELD_ACTION_MAIN => ''],
[PageProvider::FIELD_ACTION_SUB => 'testsub'],
[PageProvider::FIELD_ACTION_SUB => 'beyondDifferent']
],
[
Form::create(),
Form::create(),
Form::create(['options' => [FormOption::INHERITANCE_MODE => 'unrestricted']])
],
],
];
}

Expand Down Expand Up @@ -360,29 +377,6 @@ public function getRemoveInheritedTestValues(): array
];
}

/**
* @test
*/
public function getParentFieldValueLoadsRecordFromDatabaseIfRecordLacksParentFieldValue(): void
{
$row = Records::$contentRecordWithoutParentAndWithoutChildren;
$row['uid'] = 2;
$rowWithPid = $row;
$rowWithPid['pid'] = 1;
$className = str_replace('Tests\\Unit\\', '', substr(get_class($this), 0, -4));

$this->recordService->expects($this->exactly(1))->method('getSingle')->will($this->returnValue($rowWithPid));

$instance = $this->getMockBuilder($className)
->setConstructorArgs($this->getConstructorArguments())
->onlyMethods(['getParentFieldName', 'getTableName'])
->getMock();
$instance->expects($this->once())->method('getParentFieldName')->with($row)->will($this->returnValue('pid'));

$result = $this->callInaccessibleMethod($instance, 'getParentFieldValue', $row);
$this->assertEquals($rowWithPid['pid'], $result);
}

/**
* @dataProvider getInheritedPropertyValueByDottedPathTestValues
* @param mixed $expected
Expand All @@ -408,6 +402,53 @@ public function getInheritedPropertyValueByDottedPathTestValues(): array
];
}

/**
* @dataProvider getInheritedConfigurationTestValues
*/
public function testGetInheritedConfiguration(string $expected, array $inheritedValues): void
{
$provider = $this->getMockBuilder(PageProvider::class)
->onlyMethods(['getInheritanceTree', 'getFlexFormValuesSingle'])
->setConstructorArgs($this->getConstructorArguments())
->getMock();
$provider->method('getFlexFormValuesSingle')->willReturnOnConsecutiveCalls(...$inheritedValues);
$inheritanceTree = [];
foreach ($inheritedValues as $inheritedValue) {
$inheritanceTree[] = [PageProvider::FIELD_NAME_SUB => $inheritedValue];
}
$provider->method('getInheritanceTree')->willReturn($inheritanceTree);

$output = $this->callInaccessibleMethod($provider, 'getInheritedConfiguration', ['uid' => rand(10000, 99999)]);
self::assertSame($expected, $output['test']);
}

public function getInheritedConfigurationTestValues(): array
{
return [
'first parent has value' => [
'first-parent',
[
['test' => 'second-parent'],
['test' => 'first-parent'],
],
],
'first parent is empty, second parent has value' => [
'second-parent',
[
['test' => 'second-parent'],
[],
],
],
'first-parent has value, second parent does not have value' => [
'first-parent',
[
[],
['test' => 'first-parent'],
],
],
];
}

protected function getBasicRecord(): array
{
$record = Records::$contentRecordWithoutParentAndWithoutChildren;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
namespace FluidTYPO3\Flux\Tests\Unit\ViewHelpers\Form\Option;

/*
* This file is part of the FluidTYPO3/Flux project under GPLv2 or later.
*
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/

use FluidTYPO3\Flux\Tests\Unit\ViewHelpers\AbstractFormViewHelperTestCase;

class InheritanceModeViewHelperTest extends AbstractFormViewHelperTestCase
{
}
3 changes: 3 additions & 0 deletions ext_conf_template.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ pageIntegration = 1

# cat=basic/enable; type=boolean; label=LLL:EXT:flux/Resources/Private/Language/locallang.xlf:extension_configuration.flexFormToIrre
flexFormToIrre = 0

# cat=basic/enable; type=options[restricted, unrestricted]; label=LLL:EXT:flux/Resources/Private/Language/locallang.xlf:extension_configuration.inheritanceMode
inheritanceMode = restricted

0 comments on commit 871fcf5

Please sign in to comment.