From 8eb66df379911b1fbf0aa516d6384c8fc011d24f Mon Sep 17 00:00:00 2001
From: Nikita Hovratov <nikita.h@live.de>
Date: Fri, 29 Sep 2023 13:18:15 +0200
Subject: [PATCH] [FEATURE] Introduce PSR-14 BeforeTcaOverridesEvent
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This event can be used to dynamically generate TCA and add it as
additional base TCA. This is especially useful for "TCA generator"
extensions, which add TCA based on another resource, while still
enabling users to override TCA via TCA overrides as usual.

Resolves: #102067
Releases: main
Change-Id: I6d96e18b94f2a53693037b46e3b23d3b7f657154
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/81297
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: core-ci <typo3@b13.com>
---
 composer.json                                 |  1 +
 .../Event/BeforeTcaOverridesEvent.php         | 41 +++++++++++++
 .../Utility/ExtensionManagementUtility.php    | 11 ++++
 ...Feature-102067-BeforeTcaOverridesEvent.rst | 59 +++++++++++++++++++
 .../AddTcaAfterTcaCompilation.php             | 32 ++++++++++
 .../AddTcaBeforeTcaOverrides.php              | 33 +++++++++++
 .../Configuration/Services.yaml               |  8 +++
 .../Configuration/TCA/Overrides/fruit.php     |  6 ++
 .../Configuration/TCA/fruit.php               | 21 +++++++
 .../Extensions/test_tca_event/composer.json   | 19 ++++++
 .../Extensions/test_tca_event/ext_emconf.php  | 21 +++++++
 .../Tca/AfterTcaCompilationEventTest.php      | 35 +++++++++++
 .../Tca/BeforeTcaOverridesEventTest.php       | 43 ++++++++++++++
 ...ensionManagementUtilityAccessibleProxy.php |  4 ++
 14 files changed, 334 insertions(+)
 create mode 100644 typo3/sysext/core/Classes/Configuration/Event/BeforeTcaOverridesEvent.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.0/Feature-102067-BeforeTcaOverridesEvent.rst
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/EventListener/AddTcaAfterTcaCompilation.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/EventListener/AddTcaBeforeTcaOverrides.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/Services.yaml
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/TCA/Overrides/fruit.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/TCA/fruit.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/composer.json
 create mode 100644 typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/ext_emconf.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Tca/AfterTcaCompilationEventTest.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Tca/BeforeTcaOverridesEventTest.php

diff --git a/composer.json b/composer.json
index d6e9010cf6c1..0abdb839279a 100644
--- a/composer.json
+++ b/composer.json
@@ -311,6 +311,7 @@
 			"TYPO3Tests\\TestIrreForeignfield\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_irre_foreignfield/Classes/",
 			"TYPO3Tests\\TestLogger\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_logger/Classes/",
 			"TYPO3Tests\\TestMeta\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_meta/Classes/",
+			"TYPO3Tests\\TestTcaEvent\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/",
 			"TYPO3Tests\\TestTyposcriptAstFunctionEvent\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_ast_function_event/Classes/",
 			"TYPO3Tests\\TestTyposcriptPagetsconfigfactory\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_typoscript_pagetsconfigfactory/Classes/",
 			"TYPO3Tests\\TestTsconfigEvent\\": "typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tsconfig_event/Classes/",
diff --git a/typo3/sysext/core/Classes/Configuration/Event/BeforeTcaOverridesEvent.php b/typo3/sysext/core/Classes/Configuration/Event/BeforeTcaOverridesEvent.php
new file mode 100644
index 000000000000..db15c6e0ea42
--- /dev/null
+++ b/typo3/sysext/core/Classes/Configuration/Event/BeforeTcaOverridesEvent.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Core\Configuration\Event;
+
+/**
+ * Event before $GLOBALS['TCA'] is overridden by TCA/Overrides to allow to manipulate $tca, before overrides are merged.
+ *
+ * Side note: It is possible to check against the original TCA as this is stored within $GLOBALS['TCA']
+ * before this event is fired.
+ */
+final class BeforeTcaOverridesEvent
+{
+    public function __construct(private array $tca)
+    {
+    }
+
+    public function getTca(): array
+    {
+        return $this->tca;
+    }
+
+    public function setTca(array $tca): void
+    {
+        $this->tca = $tca;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
index 4ea383ef7c5a..3d2de19c1897 100644
--- a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
+++ b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
@@ -22,6 +22,7 @@ use Symfony\Component\Finder\Finder;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
+use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
 use TYPO3\CMS\Core\Migrations\TcaMigration;
@@ -1318,6 +1319,8 @@ tt_content.' . $key . $suffix . ' {
             }
         }
 
+        static::dispatchBaseTcaIsBeingBuiltEvent($GLOBALS['TCA']);
+
         // To require TCA Overrides in a safe scoped environment avoiding local variable clashes.
         // @see TYPO3\CMS\Core\Tests\Functional\Utility\ExtensionManagementUtility\ExtensionManagementUtilityTcaOverrideRequireTest
         $scopedRequire = static function (string $filename): void {
@@ -1355,6 +1358,14 @@ tt_content.' . $key . $suffix . ' {
         static::dispatchTcaIsBeingBuiltEvent($GLOBALS['TCA']);
     }
 
+    /**
+     * Triggers an event for manipulating the TCA before overrides are applied.
+     */
+    protected static function dispatchBaseTcaIsBeingBuiltEvent(array $tca): void
+    {
+        $GLOBALS['TCA'] = static::$eventDispatcher->dispatch(new BeforeTcaOverridesEvent($tca))->getTca();
+    }
+
     /**
      * Triggers an event for manipulating the final TCA
      */
diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102067-BeforeTcaOverridesEvent.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102067-BeforeTcaOverridesEvent.rst
new file mode 100644
index 000000000000..18666109a7bd
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102067-BeforeTcaOverridesEvent.rst
@@ -0,0 +1,59 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-102067-1695985288:
+
+=================================================
+Feature: #102067 - PSR-14 BeforeTcaOverridesEvent
+=================================================
+
+See :issue:`102067`
+
+Description
+===========
+
+A new PSR-14 :php:`\TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent`
+has been introduced, enabling developers to listen to the state between loaded
+base TCA and merging of TCA overrides.
+
+Example
+-------
+
+..  code-block:: php
+
+    <?php
+
+    declare(strict_types=1);
+
+    namespace Vendor\MyExtension\EventListener;
+
+    use TYPO3\CMS\Core\Attribute\AsEventListener;
+    use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
+
+    final class AddTcaBeforeTcaOverrides
+    {
+        #[AsEventListener('vendor/my-extension/before-tca-overrides')]
+        public function __invoke(BeforeTcaOverridesEvent $event): void
+        {
+            $tca = $event->getTca();
+            $tca['tt_content']['columns']['header']['config']['max'] = 100;
+            $event->setTca($tca);
+        }
+    }
+
+
+Impact
+======
+
+The new PSR-14 can be used to dynamically generate TCA and add it as additional
+base TCA. This is especially useful for "TCA generator" extensions, which add
+TCA based on another resource, while still enabling users to override TCA via
+the known TCA overrides API.
+
+.. note::
+
+    Please note that TCA is always "runtime cached". This means that dynamic
+    additions must never depend on runtime state, e.g. the current PSR-7
+    request or similar, because such information might not even exist when
+    the first call is e.g. done from CLI.
+
+.. index:: Backend, PHP-API, TCA, ext:core
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/EventListener/AddTcaAfterTcaCompilation.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/EventListener/AddTcaAfterTcaCompilation.php
new file mode 100644
index 000000000000..cfdfe1cc02f6
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/EventListener/AddTcaAfterTcaCompilation.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3Tests\TestTcaEvent\EventListener;
+
+use TYPO3\CMS\Core\Attribute\AsEventListener;
+use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
+
+#[AsEventListener(identifier: 'typo3tests/test-tca-event/after-tca-compilation')]
+final class AddTcaAfterTcaCompilation
+{
+    public function __invoke(AfterTcaCompilationEvent $event): void
+    {
+        $tca = $event->getTca();
+        $tca['fruit']['ctrl']['title'] = 'Vegetable';
+        $event->setTca($tca);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/EventListener/AddTcaBeforeTcaOverrides.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/EventListener/AddTcaBeforeTcaOverrides.php
new file mode 100644
index 000000000000..482d5a3a7d13
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Classes/EventListener/AddTcaBeforeTcaOverrides.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3Tests\TestTcaEvent\EventListener;
+
+use TYPO3\CMS\Core\Attribute\AsEventListener;
+use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
+
+#[AsEventListener(identifier: 'typo3tests/test-tca-event/before-tca-overrides')]
+final class AddTcaBeforeTcaOverrides
+{
+    public function __invoke(BeforeTcaOverridesEvent $event): void
+    {
+        $tca = $event->getTca();
+        $tca['fruit']['columns']['name']['config']['type'] = 'number';
+        $tca['fruit']['columns']['name']['config']['label'] = 'Monstera';
+        $event->setTca($tca);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/Services.yaml b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/Services.yaml
new file mode 100644
index 000000000000..c0a548a7bf09
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/Services.yaml
@@ -0,0 +1,8 @@
+services:
+  _defaults:
+    autowire: true
+    autoconfigure: true
+    public: false
+
+  TYPO3Tests\TestTcaEvent\:
+    resource: '../Classes/*'
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/TCA/Overrides/fruit.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/TCA/Overrides/fruit.php
new file mode 100644
index 000000000000..4e3b2c2346d2
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/TCA/Overrides/fruit.php
@@ -0,0 +1,6 @@
+<?php
+
+declare(strict_types=1);
+
+$GLOBALS['TCA']['fruit']['ctrl']['title'] = 'Plant';
+$GLOBALS['TCA']['fruit']['columns']['name']['config']['type'] = 'text';
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/TCA/fruit.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/TCA/fruit.php
new file mode 100644
index 000000000000..fb8c81fe04e4
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/Configuration/TCA/fruit.php
@@ -0,0 +1,21 @@
+<?php
+
+return [
+    'ctrl' => [
+        'title' => 'Fruit',
+        'label' => 'name',
+    ],
+    'types' => [
+        '1' => [
+            'showitem' => 'name',
+        ],
+    ],
+    'columns' => [
+        'name' => [
+            'label' => 'Name',
+            'config' => [
+                'type' => 'input',
+            ],
+        ],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/composer.json b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/composer.json
new file mode 100644
index 000000000000..494520e0475d
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/composer.json
@@ -0,0 +1,19 @@
+{
+	"name": "typo3tests/test-tca-event",
+	"type": "typo3-cms-extension",
+	"description": "This extension defines event listeners for TCA modification.",
+	"license": "GPL-2.0-or-later",
+	"require": {
+		"typo3/cms-core": "13.0.*@dev"
+	},
+	"extra": {
+		"typo3/cms": {
+			"extension-key": "test_tca_event"
+		}
+	},
+	"autoload": {
+		"psr-4": {
+			"TYPO3Tests\\TestTcaEvent\\": "Classes/"
+		}
+	}
+}
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/ext_emconf.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/ext_emconf.php
new file mode 100644
index 000000000000..9602fb464fa1
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event/ext_emconf.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'This extension defines event listeners for TCA modification.',
+    'description' => 'This extension defines event listeners for TCA modification.',
+    'category' => 'example',
+    'version' => '13.0.0',
+    'state' => 'beta',
+    'author' => 'Nikita Hovratov',
+    'author_email' => 'info@nikita-hovratov.de',
+    'author_company' => '',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '13.0.0',
+        ],
+        'conflicts' => [],
+        'suggests' => [],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/Tca/AfterTcaCompilationEventTest.php b/typo3/sysext/core/Tests/Functional/Tca/AfterTcaCompilationEventTest.php
new file mode 100644
index 000000000000..d1d384621d54
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Tca/AfterTcaCompilationEventTest.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Functional\Tca;
+
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+final class AfterTcaCompilationEventTest extends FunctionalTestCase
+{
+    protected array $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event',
+    ];
+
+    /**
+     * @test
+     */
+    public function addedTcaOverridesAnythingElse(): void
+    {
+        self::assertSame('Vegetable', $GLOBALS['TCA']['fruit']['ctrl']['title']);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/Tca/BeforeTcaOverridesEventTest.php b/typo3/sysext/core/Tests/Functional/Tca/BeforeTcaOverridesEventTest.php
new file mode 100644
index 000000000000..484f4977422a
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Tca/BeforeTcaOverridesEventTest.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Functional\Tca;
+
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+final class BeforeTcaOverridesEventTest extends FunctionalTestCase
+{
+    protected array $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_tca_event',
+    ];
+
+    /**
+     * @test
+     */
+    public function cannotOverrideTcaOverrides(): void
+    {
+        self::assertSame('text', $GLOBALS['TCA']['fruit']['columns']['name']['config']['type']);
+    }
+
+    /**
+     * @test
+     */
+    public function canOverrideBaseTca(): void
+    {
+        self::assertSame('Monstera', $GLOBALS['TCA']['fruit']['columns']['name']['config']['label']);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Utility/AccessibleProxies/ExtensionManagementUtilityAccessibleProxy.php b/typo3/sysext/core/Tests/Unit/Utility/AccessibleProxies/ExtensionManagementUtilityAccessibleProxy.php
index 991c87099bb7..070e0fde1818 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/AccessibleProxies/ExtensionManagementUtilityAccessibleProxy.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/AccessibleProxies/ExtensionManagementUtilityAccessibleProxy.php
@@ -77,6 +77,10 @@ class ExtensionManagementUtilityAccessibleProxy extends ExtensionManagementUtili
         $GLOBALS['TCA'] = [];
     }
 
+    public static function dispatchBaseTcaIsBeingBuiltEvent(array $tca): void
+    {
+    }
+
     public static function dispatchTcaIsBeingBuiltEvent(array $tca): void
     {
     }
-- 
GitLab