Skip to content

Commit bcacfb6

Browse files
authored
Merge pull request #421 from FriendsOfTYPO3/harden-simpletcaschema
[TASK] Harden SimpleTcaSchemaFactory initialization
2 parents a88f6bb + 3cd0014 commit bcacfb6

File tree

8 files changed

+187
-56
lines changed

8 files changed

+187
-56
lines changed

Classes/Schema/SimpleTcaSchemaFactory.php

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919

2020
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
2121
use Symfony\Component\DependencyInjection\Attribute\Autowire;
22+
use Symfony\Component\Finder\Finder;
2223
use Symfony\Component\VarExporter\VarExporter;
2324
use TYPO3\CMS\ContentBlocks\Schema\Exception\UndefinedSchemaException;
2425
use TYPO3\CMS\ContentBlocks\Schema\Field\FieldCollection;
2526
use TYPO3\CMS\ContentBlocks\Schema\Field\TcaField;
2627
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
28+
use TYPO3\CMS\Core\Package\PackageManager;
2729

2830
/**
2931
* @todo This class is a factory and Root Schema at the same time.
@@ -40,12 +42,14 @@ public function __construct(
4042
#[Autowire(service: 'cache.core')]
4143
protected readonly PhpFrontend $cache,
4244
protected FieldTypeResolver $typeResolver,
45+
protected PackageManager $packageManager,
4346
) {
44-
// The schema must only be hydrated from previous caches,
45-
// which were built in BeforeTcaOverridesEvent.
4647
if (($schemas = $this->getFromCache()) !== false) {
4748
$this->schemas = $schemas;
49+
return;
4850
}
51+
$baseTca = $this->loadConfigurationTcaFiles();
52+
$this->initialize($baseTca);
4953
}
5054

5155
/**
@@ -104,6 +108,52 @@ protected function build(string $schemaName, array $schemaDefinition): SimpleTca
104108
return $schema;
105109
}
106110

111+
/**
112+
* @todo You may wonder, why we copy this code from the Core TcaFactory.
113+
* @todo We used to fill this schema by using the BeforeTcaOverridesEvent.
114+
* @todo The reason we removed this dependency was that many deployment
115+
* @todo processes use `typo3 cache:flush` in their pipeline. This works
116+
* @todo well in local / staging environments, but in frequently visited
117+
* @todo production environments a concurrent hit can happen, which may
118+
* @todo produce a compiled Content Blocks cache entry, while the flush
119+
* @todo command erased the SimpleTcaSchema cache entry at the same time.
120+
* @todo The problem is, this cache can't recover itself, if TCA is
121+
* @todo already cached and the event won't fire again, leaving the system
122+
* @todo in a broken state.
123+
* @todo Example: "The field "pages" is missing the required "type" in Content Block".
124+
* @todo To circumvent this error, the functionality to create base TCA
125+
* @todo is added to this class. Now, the cache can rebuild itself.
126+
*/
127+
private function loadConfigurationTcaFiles(): array
128+
{
129+
// To require TCA in a safe scoped environment avoiding local variable clashes.
130+
// Note: Return type 'mixed' is intended, otherwise broken TCA files with missing "return [];" statement would
131+
// emit a "return value must be of type array, int returned" PHP TypeError. This is mitigated by an array
132+
// check below.
133+
$scopedReturnRequire = static function (string $filename): mixed {
134+
return require $filename;
135+
};
136+
// First load "full table" files from Configuration/TCA
137+
$tca = [];
138+
$activePackages = $this->packageManager->getActivePackages();
139+
foreach ($activePackages as $package) {
140+
try {
141+
$finder = Finder::create()->files()->sortByName()->depth(0)->name('*.php')->in($package->getPackagePath() . 'Configuration/TCA');
142+
} catch (\InvalidArgumentException) {
143+
// No such directory in this package
144+
continue;
145+
}
146+
foreach ($finder as $fileInfo) {
147+
$tcaOfTable = $scopedReturnRequire($fileInfo->getPathname());
148+
if (is_array($tcaOfTable)) {
149+
$tcaTableName = substr($fileInfo->getBasename(), 0, -4);
150+
$tca[$tcaTableName] = $tcaOfTable;
151+
}
152+
}
153+
}
154+
return $tca;
155+
}
156+
107157
protected function getFromCache(): false|array
108158
{
109159
return $this->cache->require('ContentBlocks_SimpleTcaSchema');

Classes/ServiceProvider.php

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
use TYPO3\CMS\ContentBlocks\Definition\TableDefinitionCollection;
2828
use TYPO3\CMS\ContentBlocks\Generator\TcaGenerator;
2929
use TYPO3\CMS\ContentBlocks\Registry\ContentBlockRegistry;
30-
use TYPO3\CMS\ContentBlocks\Schema\SimpleTcaSchemaFactory;
3130
use TYPO3\CMS\ContentBlocks\UserFunction\ContentWhere;
3231
use TYPO3\CMS\ContentBlocks\Utility\ContentBlockPathUtility;
3332
use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
@@ -74,7 +73,6 @@ public function getFactories(): array
7473
'content-blocks.hide-content-element-children' => static::hideContentElementChildren(...),
7574
'content-blocks.hide-content-element-children-page-content-fetching' => static::hideContentElementChildrenPageContentFetching(...),
7675
'content-blocks.record-summary-for-localization' => static::recordSummaryForLocalization(...),
77-
'content-blocks.base-simple-tca-schema' => static::baseSimpleTcaSchema(...),
7876
'content-blocks.tca' => static::tca(...),
7977
];
8078
}
@@ -281,14 +279,6 @@ public static function addUserTsConfig(ContainerInterface $container): \Closure
281279
};
282280
}
283281

284-
public static function baseSimpleTcaSchema(ContainerInterface $container): \Closure
285-
{
286-
return static function (BeforeTcaOverridesEvent $event) use ($container) {
287-
$simpleTcaSchemaFactory = $container->get(SimpleTcaSchemaFactory::class);
288-
$simpleTcaSchemaFactory->initialize($event->getTca());
289-
};
290-
}
291-
292282
public static function tca(ContainerInterface $container): \Closure
293283
{
294284
return static function (BeforeTcaOverridesEvent $event) use ($container) {
@@ -425,7 +415,6 @@ public static function addEventListeners(ContainerInterface $container, Listener
425415
$listenerProvider->addListener(ModifyDatabaseQueryForContentEvent::class, 'content-blocks.hide-content-element-children');
426416
$listenerProvider->addListener(AfterContentHasBeenFetchedEvent::class, 'content-blocks.hide-content-element-children-page-content-fetching');
427417
$listenerProvider->addListener(AfterRecordSummaryForLocalizationEvent::class, 'content-blocks.record-summary-for-localization');
428-
$listenerProvider->addListener(BeforeTcaOverridesEvent::class, 'content-blocks.base-simple-tca-schema');
429418
$listenerProvider->addListener(BeforeTcaOverridesEvent::class, 'content-blocks.tca');
430419
return $listenerProvider;
431420
}

0 commit comments

Comments
 (0)