[FEATURE] Introduce conditional variants for form elements 82/54982/64
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Fri, 8 Dec 2017 08:03:38 +0000 (09:03 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Mon, 9 Jul 2018 13:51:11 +0000 (15:51 +0200)
Variants allow you to change properties of a form element.

Variants can contain conditions. If a concrete condition
is TRUE the variant is applied. If no condition exists the
variant will be ignored.

Variants make it possible to manipulate form element values,
validator options, and finisher options based on conditions.
This allows you among other things:

* translation of form element values depending on the frontend
  language
* set and remove validators of one form element depending on the
  value of another form element
* hide entire pages depending on the value of a form element
* set finisher values depending on the value of a form element
* hiding a form element in certain finishers and on the
  summary page

This feature implements variants for the frontend rendering and
the ability to define variants in the formDefinition. The
implementation to define variants in the form editor is out of
scope of this patchset.

Releases: master
Resolves: #84133
Change-Id: I9efeeea5af67df2d2f9252339c26baf8a03cf9c8
Reviewed-on: https://review.typo3.org/54982
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Björn Jacob <bjoern.jacob@tritum.de>
Tested-by: Björn Jacob <bjoern.jacob@tritum.de>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
27 files changed:
composer.json
composer.lock
typo3/sysext/core/Documentation/Changelog/master/Deprecation-84133-Deprecate_isHiddenFormElementAnd_isReadOnlyFormElement.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-84133-IntroduceVariants.rst [new file with mode: 0644]
typo3/sysext/form/Classes/Controller/FormEditorController.php
typo3/sysext/form/Classes/Domain/Condition/ConditionContext.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Condition/ConditionResolver.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Condition/ExpressionLanguageVariableProviderInterface.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Model/FormDefinition.php
typo3/sysext/form/Classes/Domain/Model/Renderable/AbstractRenderable.php
typo3/sysext/form/Classes/Domain/Model/Renderable/RenderableVariant.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Model/Renderable/RenderableVariantInterface.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Model/Renderable/VariableRenderableInterface.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php
typo3/sysext/form/Classes/ViewHelpers/RenderAllFormValuesViewHelper.php
typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml
typo3/sysext/form/Documentation/Config/configuration/Index.rst
typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/ContentElement.rst
typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/ContentElement/renderingOptions/_isReadOnlyFormElement.rst [deleted file]
typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/Hidden.rst
typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/Hidden/renderingOptions/_isHiddenFormElement.rst [deleted file]
typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/Honeypot.rst
typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/Honeypot/renderingOptions/_isHiddenFormElement.rst [deleted file]
typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/StaticText.rst
typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/StaticText/renderingOptions/_isReadOnlyFormElement.rst [deleted file]
typo3/sysext/form/Tests/Unit/Domain/Runtime/FormRuntimeTest.php
typo3/sysext/form/composer.json

index b214c0f..26908d6 100644 (file)
@@ -49,6 +49,7 @@
                "psr/log": "~1.0.0",
                "swiftmailer/swiftmailer": "~5.4.5",
                "symfony/console": "^2.7 || ^3.0 || ^4.0",
+               "symfony/expression-language": "^3.4 || ^4.0",
                "symfony/finder": "^2.7 || ^3.0 || ^4.0",
                "symfony/polyfill-intl-icu": "^1.6",
                "symfony/polyfill-mbstring": "^1.2",
index c250065..db907ae 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "6f16d9031c0614955697202f7c44bc49",
+    "content-hash": "cdd3f50178d65296d5e324c3258fc785",
     "packages": [
         {
             "name": "cogpowered/finediff",
             "time": "2018-03-25T17:35:16+00:00"
         },
         {
+            "name": "psr/cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/cache.git",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Cache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for caching libraries",
+            "keywords": [
+                "cache",
+                "psr",
+                "psr-6"
+            ],
+            "time": "2016-08-06T20:24:11+00:00"
+        },
+        {
             "name": "psr/container",
             "version": "1.0.0",
             "source": {
             "time": "2016-10-10T12:19:37+00:00"
         },
         {
+            "name": "psr/simple-cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/simple-cache.git",
+                "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+                "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\SimpleCache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interfaces for simple caching",
+            "keywords": [
+                "cache",
+                "caching",
+                "psr",
+                "psr-16",
+                "simple-cache"
+            ],
+            "time": "2017-10-23T01:57:42+00:00"
+        },
+        {
             "name": "swiftmailer/swiftmailer",
             "version": "v5.4.5",
             "source": {
             "time": "2016-12-29T10:02:40+00:00"
         },
         {
+            "name": "symfony/cache",
+            "version": "v4.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/cache.git",
+                "reference": "be95ef3665747e6ff9d883a8adc87085769009f0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/cache/zipball/be95ef3665747e6ff9d883a8adc87085769009f0",
+                "reference": "be95ef3665747e6ff9d883a8adc87085769009f0",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "psr/cache": "~1.0",
+                "psr/log": "~1.0",
+                "psr/simple-cache": "^1.0"
+            },
+            "conflict": {
+                "symfony/var-dumper": "<3.4"
+            },
+            "provide": {
+                "psr/cache-implementation": "1.0",
+                "psr/simple-cache-implementation": "1.0"
+            },
+            "require-dev": {
+                "cache/integration-tests": "dev-master",
+                "doctrine/cache": "~1.6",
+                "doctrine/dbal": "~2.4",
+                "predis/predis": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Cache\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Cache component with PSR-6, PSR-16, and tags",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "caching",
+                "psr6"
+            ],
+            "time": "2018-06-22T08:59:39+00:00"
+        },
+        {
             "name": "symfony/console",
             "version": "v3.2.2",
             "source": {
             "time": "2017-01-02T20:32:22+00:00"
         },
         {
+            "name": "symfony/expression-language",
+            "version": "v4.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/expression-language.git",
+                "reference": "81653bbb8e0feff271bebfdea492386f1c75c098"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/expression-language/zipball/81653bbb8e0feff271bebfdea492386f1c75c098",
+                "reference": "81653bbb8e0feff271bebfdea492386f1c75c098",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "symfony/cache": "~3.4|~4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\ExpressionLanguage\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony ExpressionLanguage Component",
+            "homepage": "https://symfony.com",
+            "time": "2018-06-21T11:15:46+00:00"
+        },
+        {
             "name": "symfony/finder",
             "version": "v3.2.2",
             "source": {
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-84133-Deprecate_isHiddenFormElementAnd_isReadOnlyFormElement.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-84133-Deprecate_isHiddenFormElementAnd_isReadOnlyFormElement.rst
new file mode 100644 (file)
index 0000000..3c5adba
--- /dev/null
@@ -0,0 +1,42 @@
+.. include:: ../../Includes.txt
+
+===============================================================================
+Deprecation: #84133 - Deprecate _isHiddenFormElement and _isReadOnlyFormElement
+===============================================================================
+
+See :issue:`84133`
+
+Description
+===========
+
+The following properties have been deprecated and should not be used any longer:
+
+* :yaml:`renderingOptions._isHiddenFormElement`
+* :yaml:`renderingOptions._isReadOnlyFormElement`
+
+Those properties are available for the following form elements of the form framework:
+
+* ContentElement
+* Hidden
+* Honeypot
+
+
+Impact
+======
+
+The above mentioned properties are still available in TYPO3 v9, but they will be dropped in v10.
+
+
+Affected Installations
+======================
+
+Any form built with the form framework is affected as soon as those properties have been manually
+added to the form definition.
+
+
+Migration
+=========
+
+Usages of the above mentioned properties should be switched to the variants feature instead.
+
+.. index:: Frontend, NotScanned, ext:form
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-84133-IntroduceVariants.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-84133-IntroduceVariants.rst
new file mode 100644 (file)
index 0000000..5e67cf5
--- /dev/null
@@ -0,0 +1,490 @@
+.. include:: ../../Includes.txt
+
+====================================
+Feature: #84133 - Introduce variants
+====================================
+
+See :issue:`84133`
+
+Description
+===========
+
+
+Short Description
+-----------------
+
+Variants allow you to change properties of a form element and can be activated based on conditions.
+
+This makes it possible to manipulate form element properties, validator options, and finisher options based on conditions.
+
+This allows you among other things:
+
+* translate form element values depending on the frontend language
+* set and remove validators of one form element depending on the value of another form element
+* hide entire steps (form pages) depending on the value of a form element
+* set finisher options depending on the value of a form element
+* hiding a form element in certain finishers and on the summary step
+
+This feature implements variants for frontend rendering and the ability to define variants in form definitions.
+The implementation to define variants graphically in the form editor is out of scope of this patchset.
+
+
+Basics
+------
+
+Variants allow you to change properties of form elements, validators, and finishers and are activated by conditions.
+They are defined on the form element level either statically in form definitions or created programmatically through an API.
+
+The variants defined within a form definition are automatically applied to the form based on their conditions at runtime.
+Programmatically, variants can be applied at any time.
+
+Furthermore, the conditions of a variant can be evaluated programmatically at any time. However, some conditions are only
+available at runtime, for example a check for a form element value.
+
+Custom conditions and operators can be added easily.
+
+Only the form element properties listed in a variant are applied to the form element, all other properties are retained.
+An exception to this rule are finishers and validators. If finishers or validators are **not** defined within a variant, the
+original finishers and validators will be used. If at least one finisher or validator is defined in a variant, the
+originally defined finishers or validators are overwritten by the list of finishers and validators of the variant.
+
+Variants defined within a form definition are **all** processed and applied in the order of their condition matches. This means
+if variant 1 sets the label of a form element to "X" and variant 2 sets the label to "Y", then variant 2 is applied, i.e. the label
+will be "Y".
+
+
+Variants definition
+-------------------
+
+Variants are defined on the form element level. Check the following - incomplete - example:
+
+.. code-block:: yaml
+
+    type: Text
+    identifier: text-1
+    label: ''
+    variants:
+      -
+        identifier: variant-1
+        condition: 'formValues["checkbox-1"] == 1'
+        # If the condition matches, the label property of the form element is set to the value 'foo'
+        label: foo
+
+
+As usual :yaml:`identifier` must be a unique name of the variant on the form element level.
+
+Each variant has a single :yaml:`condition` which lets the variants' changes get applied if it matches.
+
+If the :yaml:`condition` of a variant matches, the remaining properties are applied to the form element. In the
+aforementioned example the label of the form element :yaml:`text-1` is changed to ``foo`` if the checkbox
+:yaml:`checkbox-1` is checked.
+
+The following properties can be overwritten by variants within the topmost element (:yaml:`Form`):
+
+* :yaml:`label`
+* :yaml:`renderingOptions`
+* :yaml:`finishers`
+* :yaml:`rendererClassName`
+
+The following properties can be overwritten by variants within all of the other form elements:
+
+* :yaml:`enabled`
+* :yaml:`label`
+* :yaml:`defaultValue`
+* :yaml:`properties`
+* :yaml:`renderingOptions`
+* :yaml:`validators`
+
+
+Conditions
+----------
+
+The form framework uses the Symfony component `expression language` to match the conditions. (@see https://symfony.com/doc/4.1/components/expression_language.html)
+An expression is a one-liner that returns a boolean value like :yaml:`applicationContext matches "#Production/Local#"`.
+Please read https://symfony.com/doc/4.1/components/expression_language/syntax.html to learn more about this topic.
+The form framework extends the expression language with some variables which can be used to access form values and environment settings.
+
+
+``formRuntime`` (object)
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+You can access every public method from the :php:`\TYPO3\CMS\Form\Domain\Runtime\FormRuntime` (@see https://docs.typo3.org/typo3cms/extensions/form/ApiReference/Index.html#typo3-cms-form-domain-model-formruntime).
+
+Example
+'''''''
+
+:yaml:`formRuntime.getIdentifier() == "test"`.
+
+
+``formValues`` (array)
+^^^^^^^^^^^^^^^^^^^^^^
+
+:yaml:`formValues` holds all of the submitted form element values. Each key within this array represents a form element identifier.
+
+Example
+'''''''
+
+:yaml:`formValues["text-1"] == "yes"`.
+
+
+``stepIdentifier`` (string)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:yaml:`stepIdentifier` is set to the :yaml:`identifier` of the current step.
+
+Example
+'''''''
+
+:yaml:`stepIdentifier == "page-1"`.
+
+
+``stepType`` (string)
+^^^^^^^^^^^^^^^^^^^^^
+
+:yaml:`stepType` is set to the :yaml:`type` of the current step.
+
+Example
+'''''''
+
+:yaml:`stepType == "SummaryPage"`.
+
+
+``finisherIdentifier`` (string)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:yaml:`finisherIdentifier` is set to the :yaml:`identifier` of the current finisher or an empty string (while no finishers are executed).
+
+Example
+'''''''
+
+:yaml:`finisherIdentifier == "EmailToSender"`.
+
+
+``siteLanguage`` (object)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+You can access every public method from :php:`\TYPO3\CMS\Core\Site\Entity\SiteLanguage`.
+The most needed ones are probably:
+
+* getLanguageId() / Aka sys_language_uid.
+* getLocale() / The language locale. Something like 'en_US.UTF-8'.
+* getTypo3Language() / The language key for XLF files. Something like 'de' or 'default'.
+* getTwoLetterIsoCode() / Returns the ISO-639-1 language ISO code. Something like 'de'.
+
+Example
+'''''''
+
+:yaml:`siteLanguage.getLocale() == "de_DE"`.
+
+
+``applicationContext`` (string)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:yaml:`applicationContext` is set to the application context (@see GeneralUtility::getApplicationContext()).
+
+Example
+'''''''
+
+:yaml:`applicationContext matches "#Production/Local#"`.
+
+
+``contentObject`` (array)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:yaml:`contentObject` is set to the data of the current content object or to an empty array if no content object is available.
+
+Example
+'''''''
+
+:yaml:`contentObject["pid"] in [23, 42]`.
+
+
+Working with variants programmatically
+--------------------------------------
+
+Create a variant with conditions through the PHP API:
+
+.. code-block:: php
+
+    /** @var TYPO3\CMS\Form\Domain\Model\Renderable\RenderableVariantInterface $variant */
+    $variant = $formElement->createVariant([
+        'identifier' => 'variant-1',
+        'condition' => 'formValues["checkbox-1"] == 1',
+        'label' => 'foo',
+    ]);
+
+Get all variants of a form element:
+
+.. code-block:: php
+
+    /** @var TYPO3\CMS\Form\Domain\Model\Renderable\RenderableVariantInterface[] $variants */
+    $variants = $formElement->getVariants();
+
+Apply a variant to a form element regardless of its defined conditions:
+
+.. code-block:: php
+
+    $formElement->applyVariant($variant);
+
+
+Examples
+--------
+
+Translate form element values depending on the frontend language:
+
+.. code-block:: yaml
+
+    type: Form
+    identifier: test
+    prototypeName: standard
+    label: DE
+    renderingOptions:
+      submitButtonLabel: Abschicken
+    variants:
+      -
+        identifier: language-variant-1
+        condition: 'siteLanguage.getLocale() == "en_US.UTF-8"'
+        label: EN
+        renderingOptions:
+          submitButtonLabel: Submit
+    renderables:
+      -
+        type: Page
+        identifier: page-1
+        label: DE
+        renderingOptions:
+          previousButtonLabel: 'zurück'
+          nextButtonLabel: 'weiter'
+        variants:
+          -
+            identifier: language-variant-1
+            condition: 'siteLanguage.getLocale() == "en_US.UTF-8"'
+            label: EN
+            renderingOptions:
+              previousButtonLabel: 'Previous step'
+              nextButtonLabel: 'Next step'
+        renderables:
+          -
+            type: Text
+            identifier: text-1
+            label: DE
+            properties:
+              fluidAdditionalAttributes:
+                placeholder: Platzhalter
+            variants:
+              -
+                identifier: language-variant-1
+                condition: 'siteLanguage.getLocale() == "en_US.UTF-8"'
+                label: EN
+                properties:
+                  fluidAdditionalAttributes:
+                    placeholder: Placeholder
+
+Set validators of one form element depending on the value of another form element:
+
+.. code-block:: yaml
+
+
+    type: Form
+    identifier: test
+    label: test
+    prototypeName: standard
+    renderables:
+      -
+        type: Page
+        identifier: page-1
+        label: Step
+        renderables:
+          -
+            defaultValue: ''
+            type: Text
+            identifier: text-1
+            label: 'Email address'
+            variants:
+              -
+                identifier: variant-1
+                condition: 'formValues["checkbox-1"] == 1'
+                properties:
+                  fluidAdditionalAttributes:
+                    required: 'required'
+                validators:
+                  -
+                    identifier: NotEmpty
+                  -
+                    identifier: EmailAddress
+          -
+            type: Checkbox
+            identifier: checkbox-1
+            label: 'Subscribe to newsletter'
+
+Hide entire steps depending on the value of a form element:
+
+.. code-block:: yaml
+
+    type: Form
+    identifier: test
+    prototypeName: standard
+    label: Test
+    renderables:
+      -
+        type: Page
+        identifier: page-1
+        label: 'Page 1'
+        renderables:
+          -
+            type: Text
+            identifier: text-1
+            label: 'Text 1'
+          -
+            type: Checkbox
+            identifier: checkbox-1
+            label: 'Skip page 2'
+            variants:
+              -
+                identifier: hide-1
+                condition: 'stepType == "SummaryPage"'
+                renderingOptions:
+                  enabled: false
+      -
+        type: Page
+        identifier: page-2
+        label: 'Page 2'
+        variants:
+          -
+            identifier: variant-1
+            condition: 'formValues["checkbox-1"] == 1'
+            renderingOptions:
+              enabled: false
+        renderables:
+          -
+            type: Text
+            identifier: text-2
+            label: 'Text 2'
+      -
+        type: SummaryPage
+        identifier: summarypage-1
+        label: 'Summary step'
+
+Set finisher values depending on the application context:
+
+.. code-block:: yaml
+
+    type: Form
+    identifier: test
+    prototypeName: standard
+    label: Test
+    renderingOptions:
+      submitButtonLabel: Submit
+    finishers:
+      -
+        identifier: Confirmation
+        options:
+          message: 'Thank you'
+    variants:
+      -
+        identifier: variant-1
+        condition: 'applicationContext matches "#Production/Local#"'
+        finishers:
+          -
+            identifier: Confirmation
+            options:
+              message: 'ouy knahT'
+    renderables:
+      -
+        type: Page
+        identifier: page-1
+        label: 'Page 1'
+        renderingOptions:
+          previousButtonLabel: 'Previous step'
+          nextButtonLabel: 'Next step'
+
+Hide a form element in certain finishers and on the summary step:
+
+.. code-block:: yaml
+
+    type: Form
+    identifier: test
+    prototypeName: standard
+    label: Test
+    finishers:
+      -
+        identifier: EmailToReceiver
+        options:
+          subject: Testmail
+          recipientAddress: tritum@example.org
+          recipientName: 'Test'
+          senderAddress: tritum@example.org
+          senderName: tritum@example.org
+    renderables:
+      -
+        type: Page
+        identifier: page-1
+        label: 'Page 1'
+        renderables:
+          -
+            type: Text
+            identifier: text-1
+            label: 'Text 1'
+            variants:
+              -
+                identifier: hide-1
+                renderingOptions:
+                  enabled: false
+                condition: 'stepType == "SummaryPage" || finisherIdentifier in ["EmailToSender", "EmailToReceiver"]'
+          -
+            type: Text
+            identifier: text-2
+            label: 'Text 2'
+      -
+        type: SummaryPage
+        identifier: summarypage-1
+        label: 'Summary step'
+
+
+Adding own expression language provider
+---------------------------------------
+
+If you need to extend the expression language with custom functions you can extend it. For more
+information @see https://symfony.com/doc/4.1/components/expression_language/extending.html#using-expression-providers.
+
+First of all, you have to register the expression language provider within the form setup:
+
+.. code-block:: yaml
+
+    TYPO3:
+      CMS:
+        Form:
+          prototypes:
+            standard:
+              conditionContextDefinition:
+                expressionLanguageProvider:
+                  MyCustomExpressionLanguageProvider:
+                    implementationClassName: '\Vendor\MyExtension\CustomExpressionLanguageProvider'
+
+Your expression language provider must implement :php`Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface`.
+
+
+Adding own expression language variables
+----------------------------------------
+
+If you need to add custom variables to the expression language you can extend it.
+Then the variables are ready to be checked in conditions.
+
+First of all, you have to register the variable provider within the form setup:
+
+.. code-block:: yaml
+
+    TYPO3:
+      CMS:
+        Form:
+          prototypes:
+            standard:
+              conditionContextDefinition:
+                expressionLanguageVariableProvider:
+                  MyCustomExpressionLanguageVariableProvider:
+                    implementationClassName: '\Vendor\MyExtension\CustomExpressionLanguageVariableProvider'
+
+Your expression language variable provider must implement :php`TYPO3\CMS\Form\Domain\Condition\ExpressionLanguageVariableProviderInterface`.
+
+
+.. index:: Frontend, ext:form, NotScanned
index 1c3d1df..bb1c1e5 100644 (file)
@@ -21,6 +21,8 @@ use TYPO3\CMS\Backend\View\BackendTemplateView;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Mvc\View\JsonView;
@@ -204,12 +206,45 @@ class FormEditorController extends AbstractBackendController
         $formDefinition = $formFactory->build($formDefinition->getArrayCopy(), $prototypeName);
         $formDefinition->setRenderingOption('previewMode', true);
         $form = $formDefinition->bind($this->request, $this->response);
+        $form->setCurrentSiteLanguage($this->buildFakeSiteLanguage(0, 0));
         $form->overrideCurrentPage($pageIndex);
 
         return $form->render();
     }
 
     /**
+     * Build a SiteLanguage object to render the form preview with a
+     * specific language.
+     *
+     * @param int $pageId
+     * @param int $languageId
+     * @return SiteLanguage
+     */
+    protected function buildFakeSiteLanguage(int $pageId, int $languageId): SiteLanguage
+    {
+        $fakeSiteConfiguration = [
+            'languages' => [
+                [
+                    'languageId' => $languageId,
+                    'title' => 'Dummy',
+                    'navigationTitle' => '',
+                    'typo3Language' => '',
+                    'flag' => '',
+                    'locale' => '',
+                    'iso-639-1' => '',
+                    'hreflang' => '',
+                    'direction' => '',
+                ],
+            ],
+        ];
+
+        /** @var \TYPO3\CMS\Core\Site\Entity\SiteLanguage $currentSiteLanguage */
+        $currentSiteLanguage = GeneralUtility::makeInstance(Site::class, 'form-dummy', $pageId, $fakeSiteConfiguration)
+            ->getLanguageById($languageId);
+        return $currentSiteLanguage;
+    }
+
+    /**
      * Prepare the formElements.*.formEditor section from the YAML settings.
      * Sort all formElements into groups and add additional data.
      *
diff --git a/typo3/sysext/form/Classes/Domain/Condition/ConditionContext.php b/typo3/sysext/form/Classes/Domain/Condition/ConditionContext.php
new file mode 100644 (file)
index 0000000..f59b5db
--- /dev/null
@@ -0,0 +1,136 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Condition;
+
+/*
+ * 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\Form\Domain\Exception;
+use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+
+/**
+ * Scope: frontend
+ * **This class is NOT meant to be sub classed by developers.**
+ *
+ * @internal
+ */
+class ConditionContext
+{
+
+    /**
+     * @var array
+     */
+    public $expressionLanguageProviders = [];
+
+    /**
+     * @var array
+     */
+    public $expressionLanguageVariables = [];
+
+    /**
+     * @param FormRuntime $formRuntime
+     */
+    public function __construct(FormRuntime $formRuntime)
+    {
+        $this->expressionLanguageVariables = $this->getInitialExpressionLanguageVariables($formRuntime);
+
+        $conditionContextDefinition = $formRuntime->getFormDefinition()->getConditionContextDefinition();
+
+        foreach ($conditionContextDefinition['expressionLanguageProvider'] ?? [] as $expressionLanguageProviderName => $expressionLanguageProviderDefinition) {
+            if (!isset($expressionLanguageProviderDefinition['implementationClassName'])) {
+                throw new Exception(sprintf('The "implementationClassName" was not set for expression language provider "%s".', $expressionLanguageProviderName), 1526695869);
+            }
+            $implementationClassName = $expressionLanguageProviderDefinition['implementationClassName'];
+
+            /** @see https://symfony.com/doc/4.0/components/expression_language/extending.html#using-expression-providers */
+            $this->expressionLanguageProviders[] = new $implementationClassName();
+        }
+
+        foreach ($conditionContextDefinition['expressionLanguageVariableProvider'] ?? [] as $expressionLanguageVariableProviderName => $expressionLanguageVariableProviderDefinition) {
+            if (!isset($expressionLanguageVariableProviderDefinition['implementationClassName'])) {
+                throw new Exception(sprintf('The "implementationClassName" was not set for expression language variable provider "%s".', $expressionLanguageVariableProviderName), 1526695870);
+            }
+
+            $implementationClassName = $expressionLanguageVariableProviderDefinition['implementationClassName'];
+            $expressionLanguageVariableProvider = new $implementationClassName($formRuntime);
+            if (!($expressionLanguageVariableProvider instanceof ExpressionLanguageVariableProviderInterface)) {
+                throw new Exception(sprintf('The expression language provider "%s" must implement "%s".', $implementationClassName, ExpressionLanguageVariableProviderInterface::class), 1526695874);
+            }
+            /** @see https://symfony.com/doc/4.0/components/expression_language.html#passing-in-variables */
+            $this->expressionLanguageVariables[$expressionLanguageVariableProvider->getVariableName()] = $expressionLanguageVariableProvider->getVariableValue();
+        }
+    }
+
+    /**
+     * @return array
+     */
+    public function getExpressionLanguageProviders(): array
+    {
+        return $this->expressionLanguageProviders;
+    }
+
+    /**
+     * @return array
+     */
+    public function getExpressionLanguageVariables(): array
+    {
+        return $this->expressionLanguageVariables;
+    }
+
+    /**
+     * @param FormRuntime $formRuntime
+     * @return array
+     */
+    protected function getInitialExpressionLanguageVariables(FormRuntime $formRuntime): array
+    {
+        $formValues = array_replace_recursive($formRuntime->getFormState()->getFormValues(), $formRuntime->getRequest()->getArguments());
+        $page = $formRuntime->getCurrentPage() ?? $formRuntime->getFormDefinition()->getPageByIndex(0);
+
+        $finisherIdentifier = '';
+        if ($formRuntime->getCurrentFinisher() !== null) {
+            $finisherIdentifier = (new \ReflectionClass($formRuntime->getCurrentFinisher()))->getShortName();
+            $finisherIdentifier = preg_replace('/Finisher$/', '', $finisherIdentifier);
+        }
+
+        $contentObjectData = [];
+        if (
+            TYPO3_MODE === 'FE'
+            && $this->getTypoScriptFrontendController()->cObj instanceof ContentObjectRenderer
+        ) {
+            $contentObjectData = $this->getTypoScriptFrontendController()->cObj->data;
+        }
+
+        return [
+            'formRuntime' => $formRuntime,
+            // some shortcuts
+            'formValues' => $formValues,
+            'stepIdentifier' => $page->getIdentifier(),
+            'stepType' => $page->getType(),
+            'finisherIdentifier' => $finisherIdentifier,
+            'siteLanguage' => $formRuntime->getCurrentSiteLanguage(),
+            'applicationContext' => GeneralUtility::getApplicationContext()->__toString(),
+            'contentObject' => $contentObjectData,
+        ];
+    }
+
+    /**
+     * @return TypoScriptFrontendController
+     */
+    protected function getTypoScriptFrontendController(): TypoScriptFrontendController
+    {
+        return $GLOBALS['TSFE'];
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Condition/ConditionResolver.php b/typo3/sysext/form/Classes/Domain/Condition/ConditionResolver.php
new file mode 100644 (file)
index 0000000..68cfa3e
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Condition;
+
+/*
+ * 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 Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+
+/**
+ * Scope: frontend
+ * **This class is NOT meant to be sub classed by developers.**
+ *
+ * @internal
+ */
+class ConditionResolver
+{
+
+    /**
+     * @var \TYPO3\CMS\Form\Domain\Condition\ConditionContext
+     */
+    protected $conditionContext;
+
+    /**
+     * @var \Symfony\Component\ExpressionLanguage\ExpressionLanguage
+     */
+    protected $expressionLanguage;
+
+    /**
+     * @var array
+     */
+    public $expressionLanguageVariables = [];
+
+    /**
+     * @param ConditionContext $conditionContext
+     */
+    public function __construct(ConditionContext $conditionContext)
+    {
+        $this->conditionContext = $conditionContext;
+        $this->expressionLanguage = new ExpressionLanguage(null, $conditionContext->getExpressionLanguageProviders());
+        $this->expressionLanguageVariables = $conditionContext->getExpressionLanguageVariables();
+    }
+
+    /**
+     * @param string $condition
+     * @return bool
+     */
+    public function resolveCondition(string $condition): bool
+    {
+        return (bool)$this->expressionLanguage->evaluate($condition, $this->expressionLanguageVariables);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Condition/ExpressionLanguageVariableProviderInterface.php b/typo3/sysext/form/Classes/Domain/Condition/ExpressionLanguageVariableProviderInterface.php
new file mode 100644 (file)
index 0000000..feaa915
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Condition;
+
+/*
+ * 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\Form\Domain\Runtime\FormRuntime;
+
+/**
+ * Scope: frontend / backend
+ * @api
+ */
+interface ExpressionLanguageVariableProviderInterface
+{
+
+    /**
+     * @param FormRuntime $formRuntime
+     */
+    public function __construct(FormRuntime $formRuntime);
+
+    /**
+     * @return string
+     */
+    public function getVariableName(): string;
+
+    /**
+     * @return mixed
+     */
+    public function getVariableValue();
+}
index b2b4c55..a7b12bd 100644 (file)
@@ -31,6 +31,7 @@ use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
 use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
 use TYPO3\CMS\Form\Domain\Model\Renderable\AbstractCompositeRenderable;
 use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
+use TYPO3\CMS\Form\Domain\Model\Renderable\VariableRenderableInterface;
 use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
 use TYPO3\CMS\Form\Exception as FormException;
 use TYPO3\CMS\Form\Mvc\ProcessingRule;
@@ -213,7 +214,7 @@ use TYPO3\CMS\Form\Mvc\ProcessingRule;
  * Scope: frontend
  * **This class is NOT meant to be sub classed by developers.**
  */
-class FormDefinition extends AbstractCompositeRenderable
+class FormDefinition extends AbstractCompositeRenderable implements VariableRenderableInterface
 {
 
     /**
@@ -273,6 +274,11 @@ class FormDefinition extends AbstractCompositeRenderable
     protected $finishersDefinition;
 
     /**
+     * @var array
+     */
+    protected $conditionContextDefinition;
+
+    /**
      * The persistence identifier of the form
      *
      * @var string
@@ -307,6 +313,7 @@ class FormDefinition extends AbstractCompositeRenderable
         $this->typeDefinitions = $prototypeConfiguration['formElementsDefinition'] ?? [];
         $this->validatorsDefinition = $prototypeConfiguration['validatorsDefinition'] ?? [];
         $this->finishersDefinition = $prototypeConfiguration['finishersDefinition'] ?? [];
+        $this->conditionContextDefinition = $prototypeConfiguration['conditionContextDefinition'] ?? [];
 
         if (!is_string($identifier) || strlen($identifier) === 0) {
             throw new IdentifierNotValidException('The given identifier was not a string or the string was empty.', 1477082503);
@@ -342,9 +349,10 @@ class FormDefinition extends AbstractCompositeRenderable
      * the passed $options array.
      *
      * @param array $options
+     * @param bool $resetFinishers
      * @internal
      */
-    public function setOptions(array $options)
+    public function setOptions(array $options, bool $resetFinishers = false)
     {
         if (isset($options['rendererClassName'])) {
             $this->setRendererClassName($options['rendererClassName']);
@@ -358,14 +366,23 @@ class FormDefinition extends AbstractCompositeRenderable
             }
         }
         if (isset($options['finishers'])) {
+            if ($resetFinishers) {
+                $this->finishers = [];
+            }
             foreach ($options['finishers'] as $finisherConfiguration) {
                 $this->createFinisher($finisherConfiguration['identifier'], $finisherConfiguration['options'] ?? []);
             }
         }
 
+        if (isset($options['variants'])) {
+            foreach ($options['variants'] as $variantConfiguration) {
+                $this->createVariant($variantConfiguration);
+            }
+        }
+
         ArrayUtility::assertAllArrayKeysAreValid(
             $options,
-            ['rendererClassName', 'renderingOptions', 'finishers', 'formEditor', 'label']
+            ['rendererClassName', 'renderingOptions', 'finishers', 'formEditor', 'label', 'variants']
         );
     }
 
@@ -682,6 +699,15 @@ class FormDefinition extends AbstractCompositeRenderable
     }
 
     /**
+     * @return array
+     * @internal
+     */
+    public function getConditionContextDefinition(): array
+    {
+        return $this->conditionContextDefinition;
+    }
+
+    /**
      * Get the persistence identifier of the form
      *
      * @return string
index 49f2b81..fcfdc88 100644 (file)
@@ -34,7 +34,7 @@ use TYPO3\CMS\Form\Domain\Model\FormDefinition;
  * **This class is NOT meant to be sub classed by developers.**
  * @internal
  */
-abstract class AbstractRenderable implements RenderableInterface
+abstract class AbstractRenderable implements RenderableInterface, VariableRenderableInterface
 {
 
     /**
@@ -89,6 +89,13 @@ abstract class AbstractRenderable implements RenderableInterface
     protected $templateName = '';
 
     /**
+     * associative array of rendering variants
+     *
+     * @var array
+     */
+    protected $variants = [];
+
+    /**
      * Get the type of the renderable
      *
      * @return string
@@ -127,9 +134,10 @@ abstract class AbstractRenderable implements RenderableInterface
      * the passed $options array.
      *
      * @param array $options
+     * @param bool $resetValidators
      * @api
      */
-    public function setOptions(array $options)
+    public function setOptions(array $options, bool $resetValidators = false)
     {
         if (isset($options['label'])) {
             $this->setLabel($options['label']);
@@ -154,6 +162,15 @@ abstract class AbstractRenderable implements RenderableInterface
         if (isset($options['validators'])) {
             $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
             $configurationHashes = $runtimeCache->get('formAbstractRenderableConfigurationHashes') ?: [];
+
+            if ($resetValidators) {
+                $processingRule = $this->getRootForm()->getProcessingRule($this->getIdentifier());
+                foreach ($this->getValidators() as $validator) {
+                    $processingRule->removeValidator($validator);
+                }
+                $configurationHashes = [];
+            }
+
             foreach ($options['validators'] as $validatorConfiguration) {
                 $configurationHash = md5($this->getIdentifier() . json_encode($validatorConfiguration));
                 if (in_array($configurationHash, $configurationHashes)) {
@@ -165,9 +182,15 @@ abstract class AbstractRenderable implements RenderableInterface
             }
         }
 
+        if (isset($options['variants'])) {
+            foreach ($options['variants'] as $variantConfiguration) {
+                $this->createVariant($variantConfiguration);
+            }
+        }
+
         ArrayUtility::assertAllArrayKeysAreValid(
             $options,
-            ['label', 'defaultValue', 'properties', 'renderingOptions', 'validators', 'formEditor']
+            ['label', 'defaultValue', 'properties', 'renderingOptions', 'validators', 'formEditor', 'variants']
         );
     }
 
@@ -422,4 +445,55 @@ abstract class AbstractRenderable implements RenderableInterface
     {
         return !isset($this->renderingOptions['enabled']) || (bool)$this->renderingOptions['enabled'] === true;
     }
+
+    /**
+     * Get all rendering variants
+     *
+     * @return RenderableVariantInterface[]
+     * @api
+     */
+    public function getVariants(): array
+    {
+        return $this->variants;
+    }
+
+    /**
+     * @param array $options
+     * @return RenderableVariantInterface
+     * @api
+     */
+    public function createVariant(array $options): RenderableVariantInterface
+    {
+        $identifier = $options['identifier'] ?? '';
+        unset($options['identifier']);
+
+        $variant = GeneralUtility::makeInstance(ObjectManager::class)
+            ->get(RenderableVariant::class, $identifier, $options, $this);
+
+        $this->addVariant($variant);
+        return $variant;
+    }
+
+    /**
+     * Adds the specified variant to this form element
+     *
+     * @param RenderableVariantInterface $variant
+     * @api
+     */
+    public function addVariant(RenderableVariantInterface $variant)
+    {
+        $this->variants[$variant->getIdentifier()] = $variant;
+    }
+
+    /**
+     * Apply the specified variant to this form element
+     * regardless of their conditions
+     *
+     * @param RenderableVariantInterface $variant
+     * @api
+     */
+    public function applyVariant(RenderableVariantInterface $variant)
+    {
+        $variant->apply();
+    }
 }
diff --git a/typo3/sysext/form/Classes/Domain/Model/Renderable/RenderableVariant.php b/typo3/sysext/form/Classes/Domain/Model/Renderable/RenderableVariant.php
new file mode 100644 (file)
index 0000000..271afcb
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Model\Renderable;
+
+/*
+ * 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\Form\Domain\Condition\ConditionResolver;
+use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
+
+/**
+ * Scope: frontend
+ * **This class is NOT meant to be sub classed by developers.**
+ * @internal
+ */
+class RenderableVariant implements RenderableVariantInterface
+{
+
+    /**
+     * @var string
+     */
+    protected $identifier;
+
+    /**
+     * @var array
+     */
+    protected $options;
+
+    /**
+     * @var VariableRenderableInterface
+     */
+    protected $renderable;
+
+    /**
+     * @var string
+     */
+    protected $condition = '';
+
+    /**
+     * @var bool
+     */
+    protected $applied = false;
+
+    /**
+     * @param string $identifier
+     * @param array $options
+     * @param VariableRenderableInterface $renderable
+     * @throws IdentifierNotValidException
+     */
+    public function __construct(
+        string $identifier,
+        array $options,
+        VariableRenderableInterface $renderable
+    ) {
+        if ('' === $identifier) {
+            throw new IdentifierNotValidException('The given variant identifier was empty.', 1519998923);
+        }
+        $this->identifier = $identifier;
+        $this->renderable = $renderable;
+
+        if (isset($options['condition']) && is_string($options['condition'])) {
+            $this->condition = $options['condition'];
+        }
+
+        unset($options['condition'], $options['identifier'], $options['variants']);
+
+        $this->options = $options;
+    }
+
+    /**
+     * Apply the specified variant to this form element
+     * regardless of their conditions
+     */
+    public function apply(): void
+    {
+        $this->renderable->setOptions($this->options, true);
+        $this->applied = true;
+    }
+
+    /**
+     * @param ConditionResolver $conditionResolver
+     * @return bool
+     */
+    public function conditionMatches(ConditionResolver $conditionResolver): bool
+    {
+        if (empty($this->condition)) {
+            return false;
+        }
+
+        return $conditionResolver->resolveCondition($this->condition);
+    }
+
+    /**
+     * @return string
+     */
+    public function getIdentifier(): string
+    {
+        return $this->identifier;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isApplied(): bool
+    {
+        return $this->applied;
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Model/Renderable/RenderableVariantInterface.php b/typo3/sysext/form/Classes/Domain/Model/Renderable/RenderableVariantInterface.php
new file mode 100644 (file)
index 0000000..45316b1
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Model\Renderable;
+
+/*
+ * 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\Form\Domain\Condition\ConditionResolver;
+
+/**
+ * Scope: frontend
+ * **This class is NOT meant to be sub classed by developers.**
+ * @internal
+ */
+interface RenderableVariantInterface
+{
+
+    /**
+     * @return string
+     */
+    public function getIdentifier(): string;
+
+    /**
+     * Apply the specified variant to this form element
+     * regardless of their conditions
+     */
+    public function apply(): void;
+
+    /**
+     * @return bool
+     */
+    public function isApplied(): bool;
+
+    /**
+     * @param ConditionResolver $conditionResolver
+     * @return bool
+     */
+    public function conditionMatches(ConditionResolver $conditionResolver): bool;
+}
diff --git a/typo3/sysext/form/Classes/Domain/Model/Renderable/VariableRenderableInterface.php b/typo3/sysext/form/Classes/Domain/Model/Renderable/VariableRenderableInterface.php
new file mode 100644 (file)
index 0000000..f01b99c
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Model\Renderable;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It originated from the Neos.Form package (www.neos.io)
+ *
+ * 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!
+ */
+
+/**
+ * Scope: frontend
+ * **This class is NOT meant to be sub classed by developers.**
+ */
+interface VariableRenderableInterface
+{
+
+    /**
+     * Set multiple properties of this object at once.
+     * Every property which has a corresponding set* method can be set using
+     * the passed $options array.
+     *
+     * @param array $options
+     * @param bool $reset
+     * @internal
+     */
+    public function setOptions(array $options, bool $reset = false);
+
+    /**
+     * Get all rendering variants
+     *
+     * @return RenderableVariantInterface[]
+     * @api
+     */
+    public function getVariants(): array;
+
+    /**
+     * Adds the specified variant to this form element
+     *
+     * @param RenderableVariantInterface $variant
+     * @api
+     */
+    public function addVariant(RenderableVariantInterface $variant);
+}
index 6247af1..dc67e49 100644 (file)
@@ -17,7 +17,10 @@ namespace TYPO3\CMS\Form\Domain\Runtime;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -28,17 +31,22 @@ use TYPO3\CMS\Extbase\Mvc\Web\Request;
 use TYPO3\CMS\Extbase\Mvc\Web\Response;
 use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
 use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
+use TYPO3\CMS\Form\Domain\Condition\ConditionContext;
+use TYPO3\CMS\Form\Domain\Condition\ConditionResolver;
 use TYPO3\CMS\Form\Domain\Exception\RenderingException;
 use TYPO3\CMS\Form\Domain\Finishers\FinisherContext;
+use TYPO3\CMS\Form\Domain\Finishers\FinisherInterface;
 use TYPO3\CMS\Form\Domain\Model\FormDefinition;
 use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
 use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
 use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
+use TYPO3\CMS\Form\Domain\Model\Renderable\VariableRenderableInterface;
 use TYPO3\CMS\Form\Domain\Renderer\RendererInterface;
 use TYPO3\CMS\Form\Domain\Runtime\Exception\PropertyMappingException;
 use TYPO3\CMS\Form\Exception as FormException;
 use TYPO3\CMS\Form\Mvc\Validation\EmptyValidator;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 
 /**
  * This class implements the *runtime logic* of a form, i.e. deciding which
@@ -134,6 +142,20 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
     protected $hashService;
 
     /**
+     * The current site language configuration.
+     *
+     * @var SiteLanguage
+     */
+    protected $currentSiteLanguage = null;
+
+    /**
+     * Reference to the current running finisher
+     *
+     * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface
+     */
+    protected $currentFinisher = null;
+
+    /**
      * @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
      * @internal
      */
@@ -175,7 +197,9 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
      */
     public function initializeObject()
     {
+        $this->initializeCurrentSiteLanguage();
         $this->initializeFormStateFromRequest();
+        $this->processVariants();
         $this->initializeCurrentPageFromRequest();
         $this->initializeHoneypotFromRequest();
 
@@ -399,6 +423,25 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
     }
 
     /**
+     */
+    protected function processVariants()
+    {
+        $conditionResolver = $this->getConditionResolver();
+
+        $renderables = array_merge([$this->formDefinition], $this->formDefinition->getRenderablesRecursively());
+        foreach ($renderables as $renderable) {
+            if ($renderable instanceof VariableRenderableInterface) {
+                $variants = $renderable->getVariants();
+                foreach ($variants as $variant) {
+                    if ($variant->conditionMatches($conditionResolver)) {
+                        $variant->apply();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
      * Returns TRUE if the last page of the form has been submitted, otherwise FALSE
      *
      * @return bool
@@ -534,7 +577,7 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
      * Override the current page taken from the request, rendering the page with index $pageIndex instead.
      *
      * This is typically not needed in production code, but it is very helpful when displaying
-     * some kind of "preview" of the form.
+     * some kind of "preview" of the form (e.g. form editor).
      *
      * @param int $pageIndex
      * @api
@@ -556,6 +599,7 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
         if ($this->isAfterLastPage()) {
             return $this->invokeFinishers();
         }
+        $this->processVariants();
 
         $this->formState->setLastDisplayedPageIndex($this->currentPage->getIndex());
 
@@ -592,6 +636,9 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
         $originalContent = $this->response->getContent();
         $this->response->setContent(null);
         foreach ($this->formDefinition->getFinishers() as $finisher) {
+            $this->currentFinisher = $finisher;
+            $this->processVariants();
+
             $finisherOutput = $finisher->execute($finisherContext);
             if (is_string($finisherOutput) && !empty($finisherOutput)) {
                 $output .= $finisherOutput;
@@ -869,10 +916,10 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
     }
 
     /**
-     * @return FormState
+     * @return FormState|null
      * @internal
      */
-    public function getFormState(): FormState
+    public function getFormState(): ?FormState
     {
         return $this->formState;
     }
@@ -934,10 +981,108 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess
     }
 
     /**
+     * Get the current site language configuration.
+     *
+     * @return SiteLanguage
+     * @api
+     */
+    public function getCurrentSiteLanguage(): ?SiteLanguage
+    {
+        return $this->currentSiteLanguage;
+    }
+
+    /**
+     * Override the the current site language configuration.
+     *
+     * This is typically not needed in production code, but it is very
+     * helpful when displaying some kind of "preview" of the form (e.g. form editor).
+     *
+     * @param SiteLanguage $currentSiteLanguage
+     * @api
+     */
+    public function setCurrentSiteLanguage(SiteLanguage $currentSiteLanguage): void
+    {
+        $this->currentSiteLanguage = $currentSiteLanguage;
+    }
+
+    /**
+     * Initialize the SiteLanguage object.
+     * This is mainly used by the condition matcher.
+     */
+    protected function initializeCurrentSiteLanguage(): void
+    {
+        if (
+            $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface
+            && $GLOBALS['TYPO3_REQUEST']->getAttribute('language') instanceof SiteLanguage
+        ) {
+            $this->currentSiteLanguage = $GLOBALS['TYPO3_REQUEST']->getAttribute('language');
+        } else {
+            $pageId = 0;
+            $languageId = 0;
+
+            if (TYPO3_MODE === 'FE') {
+                $pageId = $this->getTypoScriptFrontendController()->id;
+                $languageId = $this->getTypoScriptFrontendController()->sys_language_uid;
+            }
+
+            $fakeSiteConfiguration = [
+                'languages' => [
+                    [
+                        'languageId' => $languageId,
+                        'title' => 'Dummy',
+                        'navigationTitle' => '',
+                        'typo3Language' => '',
+                        'flag' => '',
+                        'locale' => '',
+                        'iso-639-1' => '',
+                        'hreflang' => '',
+                        'direction' => '',
+                    ],
+                ],
+            ];
+
+            $this->currentSiteLanguage = GeneralUtility::makeInstance(Site::class, 'form-dummy', $pageId, $fakeSiteConfiguration)
+                ->getLanguageById($languageId);
+        }
+    }
+
+    /**
+     * Reference to the current running finisher
+     *
+     * @return FinisherInterface|null
+     * @api
+     */
+    public function getCurrentFinisher(): ?FinisherInterface
+    {
+        return $this->currentFinisher;
+    }
+
+    /**
+     * @return ConditionResolver
+     */
+    protected function getConditionResolver(): ConditionResolver
+    {
+        /** @var \TYPO3\CMS\Form\Domain\Condition\ConditionResolver $conditionResolver */
+        $conditionResolver = $this->objectManager->get(
+            ConditionResolver::class,
+            GeneralUtility::makeInstance(ConditionContext::class, $this)
+        );
+        return $conditionResolver;
+    }
+
+    /**
      * @return FrontendUserAuthentication
      */
     protected function getFrontendUser(): FrontendUserAuthentication
     {
-        return $GLOBALS['TSFE']->fe_user;
+        return $this->getTypoScriptFrontendController()->fe_user;
+    }
+
+    /**
+     * @return TypoScriptFrontendController
+     */
+    protected function getTypoScriptFrontendController(): TypoScriptFrontendController
+    {
+        return $GLOBALS['TSFE'];
     }
 }
index c280a41..5ad12f4 100644 (file)
@@ -84,15 +84,24 @@ class RenderAllFormValuesViewHelper extends AbstractViewHelper
         $output = '';
         foreach ($elements as $element) {
             $renderingOptions = $element->getRenderingOptions();
+
             if (
                 !$element instanceof FormElementInterface
                 || (isset($renderingOptions['_isCompositeFormElement']) && (bool)$renderingOptions['_isCompositeFormElement'] === true)
                 || !$element->isEnabled()
                 || self::hasDisabledParent($element)
-                // @todo: we can remove the next 2 conditions if variants are implemented
-                || (isset($renderingOptions['_isHiddenFormElement']) && (bool)$renderingOptions['_isHiddenFormElement'] === true)
+            ) {
+                continue;
+            }
+
+            if (
+                (isset($renderingOptions['_isHiddenFormElement']) && (bool)$renderingOptions['_isHiddenFormElement'] === true)
                 || (isset($renderingOptions['_isReadOnlyFormElement']) && (bool)$renderingOptions['_isReadOnlyFormElement'] === true)
             ) {
+                trigger_error(
+                    'Using the properties "renderingOptions._isHiddenFormElement" and "renderingOptions._isReadOnlyFormElement" has been deprecated in v9 and will be removed in v10. Use variants instead.',
+                    E_USER_DEPRECATED
+                );
                 continue;
             }
 
index b9a00ed..f7046e7 100644 (file)
@@ -150,14 +150,22 @@ TYPO3:
               properties:
                 renderAsHiddenField: false
                 styleAttribute: 'position:absolute; margin:0 0 0 -999em;'
-              renderingOptions:
-                _isHiddenFormElement: true
+              variants:
+                -
+                  identifier: hide-1
+                  renderingOptions:
+                    enabled: false
+                  condition: 'stepType == "SummaryPage" || finisherIdentifier in ["EmailToSender", "EmailToReceiver"]'
 
             Hidden:
               __inheritances:
                 10: 'TYPO3.CMS.Form.mixins.formElementMixins.FormElementMixin'
-              renderingOptions:
-                _isHiddenFormElement: true
+              variants:
+                -
+                  identifier: hide-1
+                  renderingOptions:
+                    enabled: false
+                  condition: 'stepType == "SummaryPage" || finisherIdentifier in ["EmailToSender", "EmailToReceiver"]'
 
             ### FORM ELEMENTS: HTML5 ###
             Email:
@@ -405,8 +413,12 @@ TYPO3:
             __inheritances:
               10: 'TYPO3.CMS.Form.mixins.formElementMixins.BaseFormElementMixin'
             implementationClassName: 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement'
-            renderingOptions:
-              _isReadOnlyFormElement: true
+            variants:
+              -
+                identifier: hide-1
+                renderingOptions:
+                  enabled: false
+                condition: 'stepType == "SummaryPage" || finisherIdentifier in ["EmailToSender", "EmailToReceiver"]'
 
           FormElementMixin:
             __inheritances:
index c3dedd4..500380a 100644 (file)
@@ -1786,8 +1786,6 @@ Full default configuration
               elementErrorClassAttribute: error
               renderAsHiddenField: false
               styleAttribute: 'position:absolute; margin:0 0 0 -999em;'
-            renderingOptions:
-              _isHiddenFormElement: true
           Hidden:
             formEditor:
               editors:
@@ -1840,8 +1838,6 @@ Full default configuration
               containerClassAttribute: input
               elementClassAttribute: ''
               elementErrorClassAttribute: error
-            renderingOptions:
-              _isHiddenFormElement: true
           Email:
             formEditor:
               editors:
@@ -3202,8 +3198,6 @@ Full default configuration
               enableDatePicker: true
               displayTimeSelector: false
           StaticText:
-            renderingOptions:
-              _isReadOnlyFormElement: true
             formEditor:
               editors:
                 100:
@@ -3233,8 +3227,6 @@ Full default configuration
             properties:
               text: ''
           ContentElement:
-            renderingOptions:
-              _isReadOnlyFormElement: true
             formEditor:
               editors:
                 100:
index 037d7ab..6c5312f 100644 (file)
@@ -15,9 +15,6 @@ Properties
 .. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.contentelement.implementationclassname:
 .. include:: ContentElement/implementationClassName.rst
 
-.. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.contentelement.renderingoptions._isReadOnlyFormElement:
-.. include:: ContentElement/renderingOptions/_isReadOnlyFormElement.rst
-
 .. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.contentelement.properties.contentelementuid:
 .. include:: ContentElement/properties/contentElementUid.rst
 
diff --git a/typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/ContentElement/renderingOptions/_isReadOnlyFormElement.rst b/typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/ContentElement/renderingOptions/_isReadOnlyFormElement.rst
deleted file mode 100644 (file)
index 98b2b52..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-renderingOptions._isReadOnlyFormElement
----------------------------------------
-
-:aspect:`Option path`
-      TYPO3.CMS.Form.prototypes.<prototypeIdentifier>.formElementsDefinition.ContentElement.renderingOptions._isReadOnlyFormElement
-
-:aspect:`Data type`
-      bool
-
-:aspect:`Needed by`
-      Frontend
-
-:aspect:`Overwritable within form definition`
-      Yes
-
-:aspect:`form editor can write this property into the form definition (for prototype 'standard')`
-      No
-
-:aspect:`Mandatory`
-      No
-
-:aspect:`Default value (for prototype 'standard')`
-      .. code-block:: yaml
-         :linenos:
-         :emphasize-lines: 3
-
-         ContentElement:
-           renderingOptions:
-             _isReadOnlyFormElement: true
-
-:aspect:`Description`
-      Internal control setting to define that the form element is not visible within the summary page and emails.
\ No newline at end of file
index ef81c92..98b775c 100644 (file)
@@ -16,9 +16,6 @@ Properties
 .. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.hidden.implementationclassname:
 .. include:: Hidden/implementationClassName.rst
 
-.. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.hidden.renderingoptions._isHiddenFormElement:
-.. include:: Hidden/renderingOptions/_isHiddenFormElement.rst
-
 .. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.hidden.properties.containerclassattribute:
 .. include:: Hidden/properties/containerClassAttribute.rst
 
diff --git a/typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/Hidden/renderingOptions/_isHiddenFormElement.rst b/typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/Hidden/renderingOptions/_isHiddenFormElement.rst
deleted file mode 100644 (file)
index 1ab6b18..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-renderingOptions._isHiddenFormElement
--------------------------------------
-
-:aspect:`Option path`
-      TYPO3.CMS.Form.prototypes.<prototypeIdentifier>.formElementsDefinition.Hidden.renderingOptions._isHiddenFormElement
-
-:aspect:`Data type`
-      bool
-
-:aspect:`Needed by`
-      Frontend
-
-:aspect:`Overwritable within form definition`
-      Yes
-
-:aspect:`form editor can write this property into the form definition (for prototype 'standard')`
-      No
-
-:aspect:`Mandatory`
-      No
-
-:aspect:`Default value (for prototype 'standard')`
-      .. code-block:: yaml
-         :linenos:
-         :emphasize-lines: 3
-
-         Hidden:
-           renderingOptions:
-             _isHiddenFormElement: true
-
-:aspect:`Description`
-      Internal control setting to define that the form element is not visible within the summary page and emails.
\ No newline at end of file
index 58534a9..e0a691e 100644 (file)
@@ -15,9 +15,6 @@ Properties
 .. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.honeypot.implementationclassname:
 .. include:: Honeypot/implementationClassName.rst
 
-.. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.honeypot.renderingoptions._isHiddenFormElement:
-.. include:: Honeypot/renderingOptions/_isHiddenFormElement.rst
-
 .. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.honeypot.properties.containerclassattribute:
 .. include:: Honeypot/properties/containerClassAttribute.rst
 
diff --git a/typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/Honeypot/renderingOptions/_isHiddenFormElement.rst b/typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/Honeypot/renderingOptions/_isHiddenFormElement.rst
deleted file mode 100644 (file)
index 0c7cd5b..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-renderingOptions._isHiddenFormElement
--------------------------------------
-
-:aspect:`Option path`
-      TYPO3.CMS.Form.prototypes.<prototypeIdentifier>.formElementsDefinition.Honeypot.renderingOptions._isHiddenFormElement
-
-:aspect:`Data type`
-      bool
-
-:aspect:`Needed by`
-      Frontend
-
-:aspect:`Overwritable within form definition`
-      Yes
-
-:aspect:`form editor can write this property into the form definition (for prototype 'standard')`
-      No
-
-:aspect:`Mandatory`
-      No
-
-:aspect:`Default value (for prototype 'standard')`
-      .. code-block:: yaml
-         :linenos:
-         :emphasize-lines: 3
-
-         Honeypot:
-           renderingOptions:
-             _isHiddenFormElement: true
-
-:aspect:`Description`
-      Internal control setting to define that the form element is not visible within the summary page and emails.
\ No newline at end of file
index 20ba50d..5b68549 100644 (file)
@@ -15,9 +15,6 @@ Properties
 .. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.statictext.implementationclassname:
 .. include:: StaticText/implementationClassName.rst
 
-.. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.statictext.renderingoptions._isReadOnlyFormElement:
-.. include:: StaticText/renderingOptions/_isReadOnlyFormElement.rst
-
 .. _typo3.cms.form.prototypes.<prototypeIdentifier>.formelementsdefinition.statictext.properties.text:
 .. include:: StaticText/properties/text.rst
 
diff --git a/typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/StaticText/renderingOptions/_isReadOnlyFormElement.rst b/typo3/sysext/form/Documentation/Config/proto/formElements/formElementTypes/StaticText/renderingOptions/_isReadOnlyFormElement.rst
deleted file mode 100644 (file)
index f4d0c0a..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-renderingOptions._isReadOnlyFormElement
----------------------------------------
-
-:aspect:`Option path`
-      TYPO3.CMS.Form.prototypes.<prototypeIdentifier>.formElementsDefinition.StaticText.renderingOptions._isReadOnlyFormElement
-
-:aspect:`Data type`
-      bool
-
-:aspect:`Needed by`
-      Frontend
-
-:aspect:`Overwritable within form definition`
-      Yes
-
-:aspect:`form editor can write this property into the form definition (for prototype 'standard')`
-      No
-
-:aspect:`Mandatory`
-      No
-
-:aspect:`Default value (for prototype 'standard')`
-      .. code-block:: yaml
-         :linenos:
-         :emphasize-lines: 3
-
-         StaticText:
-           renderingOptions:
-             _isReadOnlyFormElement: true
-
-:aspect:`Description`
-      Internal control setting to define that the form element is not visible within the summary page and emails.
\ No newline at end of file
index b6cfd9c..8d4ae65 100644 (file)
@@ -39,7 +39,7 @@ class FormRuntimeTest extends UnitTestCase
     public function renderThrowsExceptionIfFormDefinitionReturnsNoRendererClassName()
     {
         $mockFormRuntime = $this->getAccessibleMock(FormRuntime::class, [
-            'isAfterLastPage'
+            'isAfterLastPage', 'processVariants'
         ], [], '', false);
 
         $mockPage = $this->getAccessibleMock(Page::class, [
@@ -75,6 +75,11 @@ class FormRuntimeTest extends UnitTestCase
             ->method('isAfterLastPage')
             ->willReturn(false);
 
+        $mockFormRuntime
+            ->expects($this->any())
+            ->method('processVariants')
+            ->willReturn(null);
+
         $mockFormRuntime->_set('formState', $mockFormState);
         $mockFormRuntime->_set('currentPage', $mockPage);
         $mockFormRuntime->_set('formDefinition', $mockFormDefinition);
@@ -94,7 +99,7 @@ class FormRuntimeTest extends UnitTestCase
         GeneralUtility::setSingletonInstance(ObjectManager::class, $objectManagerProphecy->reveal());
 
         $mockFormRuntime = $this->getAccessibleMock(FormRuntime::class, [
-            'isAfterLastPage'
+            'isAfterLastPage', 'processVariants'
         ], [], '', false);
 
         $mockPage = $this->getAccessibleMock(Page::class, [
@@ -130,6 +135,11 @@ class FormRuntimeTest extends UnitTestCase
             ->method('isAfterLastPage')
             ->willReturn(false);
 
+        $mockFormRuntime
+            ->expects($this->any())
+            ->method('processVariants')
+            ->willReturn(null);
+
         $objectManagerProphecy
             ->get('fooRenderer')
             ->willReturn(new \stdClass);
index 379a98b..89c77c2 100644 (file)
@@ -13,7 +13,8 @@
                "sort-packages": true
        },
        "require": {
-               "typo3/cms-core": "9.4.*@dev"
+               "typo3/cms-core": "9.4.*@dev",
+               "symfony/expression-language": "^3.4 || ^4.0"
        },
        "conflict": {
                "typo3/cms": "*"