TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:20 +02:00
commit c7a46689ff
115 changed files with 11736 additions and 0 deletions
@@ -0,0 +1,200 @@
<?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\Fluid\Core\Component;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\Event\ModifyComponentDefinitionEvent;
use TYPO3\CMS\Fluid\Event\ProvideStaticVariablesToComponentEvent;
use TYPO3\CMS\Fluid\View\TemplatePaths;
use TYPO3Fluid\Fluid\Core\Component\ComponentAdapter;
use TYPO3Fluid\Fluid\Core\Component\ComponentDefinition;
use TYPO3Fluid\Fluid\Core\Component\ComponentDefinitionProviderInterface;
use TYPO3Fluid\Fluid\Core\Component\ComponentListProviderInterface;
use TYPO3Fluid\Fluid\Core\Component\ComponentRendererInterface;
use TYPO3Fluid\Fluid\Core\Component\ComponentTemplateResolverInterface;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext;
use TYPO3Fluid\Fluid\Core\ViewHelper\TemplateStructureViewHelperResolver;
use TYPO3Fluid\Fluid\Core\ViewHelper\UnresolvableViewHelperException;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolverDelegateInterface;
use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;
/**
* @internal
*/
#[Autoconfigure(autowire: false)]
final readonly class DeclarativeComponentCollection implements ViewHelperResolverDelegateInterface, ComponentDefinitionProviderInterface, ComponentTemplateResolverInterface, ComponentListProviderInterface
{
private string $templateNamePattern;
public function __construct(
private FrontendInterface $cache,
private EventDispatcherInterface $eventDispatcher,
private string $namespace,
private array $templatePaths,
string $templateNamePattern = '{path}/{name}/{name}',
private bool $additionalArgumentsAllowed = false,
) {
$this->templateNamePattern = trim($templateNamePattern, '/');
}
public function withTemplateNamePattern(string $templateNamePattern): static
{
return new static($this->cache, $this->eventDispatcher, $this->namespace, $this->templatePaths, $templateNamePattern, $this->additionalArgumentsAllowed);
}
public function withAdditionalArgumentsAllowed(bool $additionalArgumentsAllowed): static
{
return new static($this->cache, $this->eventDispatcher, $this->namespace, $this->templatePaths, $this->templateNamePattern, $additionalArgumentsAllowed);
}
public function resolveTemplateName(string $viewHelperName): string
{
$fragments = array_map(ucfirst(...), explode('.', $viewHelperName));
$name = array_pop($fragments);
$path = implode('/', $fragments);
return ltrim(str_replace(['{path}', '{name}'], [$path, $name], $this->templateNamePattern), '/');
}
public function getAvailableComponents(): array
{
$availableTemplates = $this->getTemplatePaths()->resolveAvailableTemplateFiles(null, null, true);
$templateNamePattern = self::convertTemplatePatternToRegularExpression($this->templateNamePattern);
$availableComponents = [];
foreach ($availableTemplates as $templatePath) {
// Remove template root path
foreach ($this->getTemplatePaths()->getTemplateRootPaths() as $rootPath) {
if (str_starts_with($templatePath, $rootPath)) {
$templatePath = substr($templatePath, strlen($rootPath));
break;
}
}
// Convert template name into ViewHelper name and validate directory structure
// (resolveTemplateName() in reverse)
if (!preg_match($templateNamePattern, $templatePath, $matches)) {
continue;
}
$fragments = $matches['path'] ? GeneralUtility::trimExplode('/', $matches['path'], true) : [];
$fragments[] = $matches['name'];
$availableComponents[] = implode('.', array_map(lcfirst(...), $fragments));
}
return array_values(array_unique($availableComponents));
}
public function getTemplatePaths(): TemplatePaths
{
$templatePaths = new TemplatePaths();
$templatePaths->setTemplateRootPaths($this->templatePaths);
return $templatePaths;
}
public function getAdditionalVariables(string $viewHelperName): array
{
// Allow to provide additional variables to the component template.
// Note that this deliberately cannot depend on runtime characteristics,
// such as the request, as this should be done in the renderer.
$event = $this->eventDispatcher->dispatch(
new ProvideStaticVariablesToComponentEvent($this, $viewHelperName)
);
return $event->getStaticVariables();
}
public function getComponentDefinition(string $viewHelperName): ComponentDefinition
{
$cacheIdentifier = hash('xxh3', $this->namespace . '--' . $viewHelperName);
$componentDefinition = $this->cache->get($cacheIdentifier);
if ($componentDefinition instanceof ComponentDefinition) {
return $componentDefinition;
}
$templateName = $this->resolveTemplateName($viewHelperName);
/**
* Extract component definition from component template
* This part is an ugly workaround because of shortcomings in the Fluid parser.
* Once this has been resolved on the Fluid side, there will most likely be a better API.
* @see \TYPO3Fluid\Fluid\Core\Component\AbstractComponentCollection
*/
$renderingContext = new RenderingContext();
$renderingContext->setViewHelperResolver(new TemplateStructureViewHelperResolver());
$parsedTemplate = $renderingContext->getTemplateParser()->parse(
$this->getTemplatePaths()->getTemplateSource('Default', $templateName),
$this->getTemplatePaths()->getTemplateIdentifier('Default', $templateName),
$this->getTemplatePaths()->resolveTemplateFileForControllerAndActionAndFormat('Default', $templateName),
);
$componentDefinition = new ComponentDefinition(
$viewHelperName,
$parsedTemplate->getArgumentDefinitions(),
$this->additionalArgumentsAllowed,
$parsedTemplate->getAvailableSlots(),
);
// Allow modification of component definition before it's written to cache.
// Note that this deliberately cannot depend on runtime characteristics,
// such as the request, as this should be done during rendering (e. g. by allowing
// arbitrary arguments)
$event = $this->eventDispatcher->dispatch(
new ModifyComponentDefinitionEvent($this->namespace, $componentDefinition)
);
$componentDefinition = $event->getComponentDefinition();
$this->cache->set($cacheIdentifier, $componentDefinition);
return $componentDefinition;
}
public function getComponentRenderer(): ComponentRendererInterface
{
return new EventBasedComponentRenderer($this->eventDispatcher, $this);
}
public function resolveViewHelperClassName(string $name): string
{
$expectedTemplateName = $this->resolveTemplateName($name);
try {
$this->getTemplatePaths()->resolveTemplateFileForControllerAndActionAndFormat('Default', $expectedTemplateName, null, true);
} catch (InvalidTemplateResourceException $e) {
throw new UnresolvableViewHelperException(sprintf(
'The component template "%s" in format ".%s" could not be found in the configured template paths. %s',
$expectedTemplateName,
$this->getTemplatePaths()->getFormat(),
$e->evaluatedTemplatePaths !== [] ? 'The following file paths were evaluated: "' . implode('", "', $e->evaluatedTemplatePaths) . '"' : 'No paths configured.',
), 1765711586);
}
return ComponentAdapter::class;
}
public function getNamespace(): string
{
return $this->namespace;
}
private static function convertTemplatePatternToRegularExpression(string $templateNamePattern): string
{
$delimiter = '~';
$pathMarker = preg_quote('{path}/', $delimiter);
$nameMarker = preg_quote('{name}', $delimiter);
$templateNamePattern = preg_quote($templateNamePattern, $delimiter);
if (str_contains($templateNamePattern, $pathMarker)) {
[$beforePath, $afterPath] = explode($pathMarker, $templateNamePattern, 2);
$templateNamePattern = $beforePath . '(?<path>(?:.+?/)?)' . str_replace($pathMarker, '(?P=path)', $afterPath);
}
if (str_contains($templateNamePattern, $nameMarker)) {
[$beforeName, $afterName] = explode($nameMarker, $templateNamePattern, 2);
$templateNamePattern = $beforeName . '(?<name>[^/]+?)' . str_replace($nameMarker, '(?P=name)', $afterName);
}
return $delimiter . '^' . $templateNamePattern . '$' . $delimiter;
}
}