From 458896e4d35daa225d383fa8725a9f084f5dae5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Wed, 11 Jun 2025 10:45:42 +0200 Subject: [PATCH 1/4] !!!FEATURE: Fluid 4.2 update This updates the external Fluid dependency to version 4.2 making it an upgrade of two major versions. This includes some deprecations and major changes. Much of which we cover up in our API but if you extended Fluid or used internals your code might be affected. --- .../Classes/Core/Cache/CacheAdaptor.php | 45 +---- .../Interceptor/ResourceInterceptor.php | 34 ++-- .../Parser/SyntaxTree/ResourceUriNode.php | 78 +++++--- .../Classes/Core/Parser/TemplateParser.php | 4 +- .../NamespaceDetectionTemplateProcessor.php | 5 +- .../Core/Rendering/RenderingContext.php | 6 +- .../ViewHelper/TemplateVariableContainer.php | 5 +- .../Core/ViewHelper/ViewHelperResolver.php | 11 +- .../Core/Widget/AbstractWidgetViewHelper.php | 2 +- .../Classes/View/AbstractTemplateView.php | 30 +-- .../Classes/View/StandaloneView.php | 8 +- .../Classes/View/TemplatePaths.php | 53 +++-- .../Classes/View/TemplateView.php | 4 +- .../ViewHelpers/Form/CheckboxViewHelper.php | 2 +- .../ViewHelpers/Form/RadioViewHelper.php | 2 +- .../Interceptor/ResourceInterceptorTest.php | 10 +- .../ViewHelper/AbstractViewHelperTest.php | 46 +---- .../Widget/AbstractWidgetViewHelperTest.php | 77 +++++--- .../Fixtures/TemplatePathsTestAccessor.php | 25 +++ .../Tests/Unit/View/StandaloneViewTest.php | 17 +- .../Tests/Unit/View/TemplatePathsTest.php | 186 ++++++------------ .../Tests/Unit/View/TemplateViewTest.php | 6 +- Neos.FluidAdaptor/composer.json | 2 +- .../Tests/Unit/ObjectAccessTest.php | 4 +- composer.json | 2 +- 25 files changed, 295 insertions(+), 369 deletions(-) create mode 100644 Neos.FluidAdaptor/Tests/Unit/View/Fixtures/TemplatePathsTestAccessor.php diff --git a/Neos.FluidAdaptor/Classes/Core/Cache/CacheAdaptor.php b/Neos.FluidAdaptor/Classes/Core/Cache/CacheAdaptor.php index 43bb16e5a4..aaa4dad17f 100644 --- a/Neos.FluidAdaptor/Classes/Core/Cache/CacheAdaptor.php +++ b/Neos.FluidAdaptor/Classes/Core/Cache/CacheAdaptor.php @@ -27,14 +27,7 @@ class CacheAdaptor implements FluidCacheInterface */ protected $flowCache; - /** - * Gets an entry from the cache or NULL if the - * entry does not exist. - * - * @param string $name - * @return string - */ - public function get($name) + public function get(string $name): mixed { if ($this->flowCache->has($name)) { $this->flowCache->requireOnce($name); @@ -43,45 +36,21 @@ public function get($name) return $this->flowCache->getWrapped($name); } - /** - * Set or updates an entry identified by $name - * into the cache. - * - * @param string $name - * @param string $value - */ - public function set($name, $value) + public function set(string $name, mixed $value): void { - // we need to strip the first line with the php header as the flow cache adds that again. $this->flowCache->set($name, substr($value, strpos($value, "\n") + 1)); } - /** - * Flushes the cache either by entry or flushes - * the entire cache if no entry is provided. - * - * @param string|null $name - * @return bool|null - */ - public function flush($name = null) + public function flush(?string $name = null): void { - if ($name !== null) { - return $this->flowCache->remove($name); - } else { + if ($name === null) { $this->flowCache->flush(); - return null; + return; } + $this->flowCache->remove($name); } - /** - * Get an instance of FluidCacheWarmerInterface which - * can warm up template files that would normally be - * cached on-the-fly to this FluidCacheInterface - * implementaion. - * - * @return FluidCacheWarmerInterface - */ - public function getCacheWarmer() + public function getCacheWarmer(): FluidCacheWarmerInterface { return new StandardCacheWarmer(); } diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php b/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php index 286870a6ac..a8ca4bb96a 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php @@ -11,6 +11,8 @@ * source code. */ +use Neos\Flow\Annotations as Flow; +use Neos\Flow\Mvc\Routing\Router; use Neos\Flow\Package\FlowPackageKey; use Neos\FluidAdaptor\Core\Parser\SyntaxTree\ResourceUriNode; use TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface; @@ -40,7 +42,7 @@ class ResourceInterceptor implements InterceptorInterface * * @var string */ - const PATTERN_SPLIT_AT_RESOURCE_URIS = '! + private const PATTERN_SPLIT_AT_RESOURCE_URIS = '! ( (?:[^"\'(\s]+/ # URL part: A string with no quotes, no opening parentheses and no whitespace )* # a URL consists of multiple URL parts @@ -55,15 +57,21 @@ class ResourceInterceptor implements InterceptorInterface * @var string * @see \Neos\Flow\Package\FlowPackageKey::PATTERN */ - const PATTERN_MATCH_RESOURCE_URI = '!(?:../)*(?:(?P[A-Za-z0-9]+\.(?:[A-Za-z0-9][\.a-z0-9]*)+)/Resources/)?Public/(?P[^"]+)!'; + private const PATTERN_MATCH_RESOURCE_URI = '!(?:../)*(?:(?P[A-Za-z0-9]+\.(?:[A-Za-z0-9][\.a-z0-9]*)+)/Resources/)?Public/(?P[^"]+)!'; /** * The default package key to use when rendering resource links without a * package key in the source URL. * - * @var string + * @var string|null + */ + protected ?string $defaultPackageKey = null; + + /** + * @Flow\Inject + * @var Router */ - protected $defaultPackageKey; + protected Router $router; /** * Set the default package key to use for resource URIs. @@ -72,7 +80,7 @@ class ResourceInterceptor implements InterceptorInterface * @return void * @throws \InvalidArgumentException */ - public function setDefaultPackageKey($defaultPackageKey) + public function setDefaultPackageKey(string $defaultPackageKey): void { if (!FlowPackageKey::isPackageKeyValid($defaultPackageKey)) { throw new \InvalidArgumentException('The given argument was not a valid package key.', 1277287099); @@ -89,12 +97,9 @@ public function setDefaultPackageKey($defaultPackageKey) * @param ParsingState $parsingState the current parsing state. Not needed in this interceptor. * @return NodeInterface the modified node */ - public function process(NodeInterface $node, $interceptorPosition, ParsingState $parsingState) + public function process(NodeInterface $node, $interceptorPosition, ParsingState $parsingState): NodeInterface { - if (!$node instanceof TextNode) { - return $node; - } - if (strpos($node->getText(), 'Public/') === false) { + if (!$node instanceof TextNode || !str_contains($node->getText(), 'Public/')) { return $node; } $textParts = preg_split(self::PATTERN_SPLIT_AT_RESOURCE_URIS, $node->getText(), -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); @@ -106,14 +111,13 @@ public function process(NodeInterface $node, $interceptorPosition, ParsingState 'path' => new TextNode($matches['Path']) ]; - if ($this->defaultPackageKey !== null) { - $arguments['package'] = new TextNode($this->defaultPackageKey); - } + $packageKey = $this->defaultPackageKey; + if (isset($matches['Package']) && FlowPackageKey::isPackageKeyValid($matches['Package'])) { - $arguments['package'] = new TextNode($matches['Package']); + $packageKey = $matches['Package']; } - $resourceUriNode = new ResourceUriNode($arguments); + $resourceUriNode = new ResourceUriNode($matches['Path'], $packageKey); $node->addChildNode($resourceUriNode); } else { $textNode = new TextNode($part); diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php b/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php index db44103330..d2f010c018 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php @@ -11,53 +11,73 @@ * source code. */ +use Neos\Flow\I18n\Service; +use Neos\Flow\ResourceManagement\Exception; +use Neos\Flow\ResourceManagement\ResourceManager; use Neos\FluidAdaptor\Core\Parser\Interceptor\ResourceInterceptor; -use Neos\FluidAdaptor\Core\ViewHelper\ViewHelperResolver; -use Neos\FluidAdaptor\ViewHelpers\Uri\ResourceViewHelper; -use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode; -use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface; +use Neos\FluidAdaptor\Core\Rendering\RenderingContext; +use Neos\FluidAdaptor\Core\ViewHelper\Exception\InvalidVariableException; +use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\AbstractNode; +use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; /** * A special ViewHelperNode that works via injections and is created by the ResourceInterceptor * * @see ResourceInterceptor */ -class ResourceUriNode extends ViewHelperNode +class ResourceUriNode extends AbstractNode { /** - * @var array + * @var ResourceManager|null */ - protected $arguments = []; + protected ?ResourceManager $resourceManager; - /** - * @var ViewHelperResolver - */ - protected $viewHelperResolver; + protected ?Service $i18nService; - /** - * @var string - */ - protected $viewHelperClassName = ResourceViewHelper::class; + public function injectResourceManager(ResourceManager $resourceManager): void + { + $this->resourceManager = $resourceManager; + } - /** - * @param ViewHelperResolver $viewHelperResolver - */ - public function injectViewHelperResolver(ViewHelperResolver $viewHelperResolver) + public function injectService(Service $i18nService): void { - $this->viewHelperResolver = $viewHelperResolver; - $this->uninitializedViewHelper = $this->viewHelperResolver->createViewHelperInstanceFromClassName($this->viewHelperClassName); - /** @phpstan-ignore-next-line we use internal api */ - $this->uninitializedViewHelper->setViewHelperNode($this); - $this->argumentDefinitions = $this->viewHelperResolver->getArgumentDefinitionsForViewHelper($this->uninitializedViewHelper); + $this->i18nService = $i18nService; + } + + public function __construct( + public readonly string $path, + public readonly string|null $package + ) { } /** - * Constructor. - * - * @param NodeInterface[] $arguments Arguments of view helper - each value is a RootNode. + * @param RenderingContextInterface $renderingContext + * @return string + * @throws InvalidVariableException */ - public function __construct(array $arguments) + public function evaluate(RenderingContextInterface $renderingContext): string { - $this->arguments = $arguments; + $package = $this->package; + $path = $this->path; + if ($package === null) { + /** @var RenderingContext $renderingContext */ + $package = $renderingContext->getControllerContext()?->getRequest()?->getControllerPackageKey() ?? null; + } + if (str_starts_with($path, 'resource://')) { + try { + [$package, $path] = $this->resourceManager->getPackageAndPathByPublicPath($path); + } catch (Exception $e) { + throw new InvalidVariableException(sprintf('The specified path "%s" does not point to a public resource.', $path), 1386458851, $e); + } + } + + $resourcePath = 'resource://' . $package . '/Public/' . $this->path; + $localizedResourcePathData = $this->i18nService->getLocalizedFilename($resourcePath); + $matches = []; + if (preg_match('#resource://([^/]+)/Public/(.*)#', current($localizedResourcePathData), $matches) === 1) { + [$_, $package, $path] = $matches; + } + + return $this->resourceManager->getPublicPackageResourceUri($package, $path); } } diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateParser.php b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateParser.php index 1ad1054885..742bf9f1ba 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateParser.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateParser.php @@ -9,7 +9,7 @@ class TemplateParser extends \TYPO3Fluid\Fluid\Core\Parser\TemplateParser /** * @return boolean */ - public function isEscapingEnabled() + public function isEscapingEnabled(): bool { return $this->escapingEnabled; } @@ -17,7 +17,7 @@ public function isEscapingEnabled() /** * @param boolean $escapingEnabled */ - public function setEscapingEnabled($escapingEnabled) + public function setEscapingEnabled($escapingEnabled): void { $this->escapingEnabled = $escapingEnabled; } diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/NamespaceDetectionTemplateProcessor.php b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/NamespaceDetectionTemplateProcessor.php index b6c18f2664..8235604bed 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/NamespaceDetectionTemplateProcessor.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/TemplateProcessor/NamespaceDetectionTemplateProcessor.php @@ -53,11 +53,8 @@ class NamespaceDetectionTemplateProcessor extends FluidNamespaceDetectionTemplat * Pre-process the template source before it is * returned to the TemplateParser or passed to * the next TemplateProcessorInterface instance. - * - * @param string $templateSource - * @return string */ - public function preProcessSource($templateSource) + public function preProcessSource(string $templateSource): string { $templateSource = $this->protectCDataSectionsFromParser($templateSource); $templateSource = $this->registerNamespacesFromTemplateSource($templateSource); diff --git a/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php b/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php index 3c68fb552b..53dc59f976 100644 --- a/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php +++ b/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php @@ -12,7 +12,6 @@ */ use Neos\FluidAdaptor\Core\Cache\CacheAdaptor; -use Neos\FluidAdaptor\Core\Parser\TemplateParser; use Neos\FluidAdaptor\Core\Parser\TemplateProcessor\EscapingFlagProcessor; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\ActionRequest; @@ -28,6 +27,7 @@ use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\CastingExpressionNode; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\MathExpressionNode; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\TernaryExpressionNode; +use TYPO3Fluid\Fluid\Core\Parser\TemplateParser; use TYPO3Fluid\Fluid\Core\Parser\TemplateProcessor\PassthroughSourceModifierTemplateProcessor; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext as FluidRenderingContext; @@ -93,7 +93,9 @@ public function __construct(array $options = []) new PassthroughSourceModifierTemplateProcessor(), new NamespaceDetectionTemplateProcessor() ]); - $this->setTemplatePaths(new TemplatePaths($options)); + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions($options); + $this->setTemplatePaths($templatePaths); $this->setVariableProvider(new TemplateVariableContainer()); } diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/TemplateVariableContainer.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/TemplateVariableContainer.php index ed8daa2103..eba2f07a7d 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/TemplateVariableContainer.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/TemplateVariableContainer.php @@ -28,11 +28,8 @@ class TemplateVariableContainer extends StandardVariableProvider * * This sadly mostly copies the parent method to add handling for * subjects of type TemplateObjectAccessInterface. - * - * @param string $path - * @return mixed */ - public function getByPath($path) + public function getByPath(string $path): mixed { // begin copy of parent method $subject = $this->variables; diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php index 8351bfa4f1..20337b6807 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php @@ -15,6 +15,7 @@ use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Package\Package; use Neos\Flow\Package\PackageManager; +use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface; /** * Class ViewHelperResolver @@ -51,7 +52,7 @@ class ViewHelperResolver extends \TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperRes * * @var array */ - protected $namespaces = []; + protected array $namespaces = []; /** * @Flow\InjectConfiguration(path="namespaces") @@ -86,7 +87,7 @@ public function initializeObject($reason) * @param string $viewHelperClassName * @return \TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface */ - public function createViewHelperInstanceFromClassName($viewHelperClassName) + public function createViewHelperInstanceFromClassName(string $viewHelperClassName): ViewHelperInterface { return $this->objectManager->get($viewHelperClassName); } @@ -123,12 +124,8 @@ public function createViewHelperInstanceFromClassName($viewHelperClassName) * you need to remove or replace previously added namespaces. Be aware * that setNamespaces() also removes the default "f" namespace, so * when you use this method you should always include the "f" namespace. - * - * @param string $identifier - * @param string|array $phpNamespace - * @return void */ - public function addNamespace($identifier, $phpNamespace) + public function addNamespace(string $identifier, array|string|null $phpNamespace): void { if ($phpNamespace === null) { $this->namespaces[$identifier] = null; diff --git a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php index e390a32db6..81e7a193ca 100644 --- a/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/Widget/AbstractWidgetViewHelper.php @@ -111,7 +111,7 @@ public function initializeArgumentsAndRender() $this->initialize(); $this->initializeWidgetContext(); - return $this->callRenderMethod(); + return $this->render(); } /** diff --git a/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php b/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php index 37a64bfa17..e56621724d 100644 --- a/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php +++ b/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php @@ -85,12 +85,12 @@ abstract class AbstractTemplateView extends \TYPO3Fluid\Fluid\View\AbstractTempl 'string' ], 'templatePathAndFilename' => [ - null, + '', 'Path and filename of the template file. If set, overrides the templatePathAndFilenamePattern', 'string' ], 'layoutPathAndFilename' => [ - null, + '', 'Path and filename of the layout file. If set, overrides the layoutPathAndFilenamePattern', 'string' ] @@ -147,12 +147,8 @@ public static function createWithOptions(array $options): self * @param array $options * @throws Exception */ - public function __construct(?array $options = null) + public function __construct(array $options = []) { - if ($options === null) { - $options = []; - } - $this->validateOptions($options); $this->setOptions($options); @@ -168,7 +164,7 @@ public function __construct(?array $options = null) */ public function setTemplatePathAndFilename($templatePathAndFilename) { - $this->getTemplatePaths()->setTemplatePathAndFilename($templatePathAndFilename); + $this->baseRenderingContext->getTemplatePaths()->setTemplatePathAndFilename($templatePathAndFilename); } /** @@ -184,8 +180,7 @@ public function setControllerContext(ControllerContext $controllerContext) $renderingContext->setControllerContext($controllerContext); } - - $paths = $this->getTemplatePaths(); + $paths = $this->baseRenderingContext->getTemplatePaths(); $request = $controllerContext->getRequest(); if (!$request instanceof ActionRequest) { @@ -210,11 +205,11 @@ public function setControllerContext(ControllerContext $controllerContext) * @return string rendered template for the section * @throws \Neos\FluidAdaptor\View\Exception\InvalidSectionException */ - public function renderSection($sectionName, array $variables = [], $ignoreUnknown = false) + public function renderSection($sectionName, array|\ArrayAccess $variables = [], $ignoreUnknown = false) { // FIXME: We should probably give variables explicitly to this method. - if ($variables === []) { - $variables = $this->getRenderingContext()->getVariableProvider()->getAll(); + if (empty($variables)) { + $variables = $this->getCurrentRenderingContext()->getVariableProvider()->getAll(); } return parent::renderSection($sectionName, $variables, $ignoreUnknown); @@ -301,4 +296,13 @@ public function setOption($optionName, $value) $this->baseRenderingContext->setOption($optionName, $value); } } + + /** + * @deprecated Use $this->getRenderingContext()->getTemplatePaths() + * @see RenderingContext::getTemplatePaths() + */ + public function getTemplatePaths(): \TYPO3Fluid\Fluid\View\TemplatePaths + { + return $this->baseRenderingContext->getTemplatePaths(); + } } diff --git a/Neos.FluidAdaptor/Classes/View/StandaloneView.php b/Neos.FluidAdaptor/Classes/View/StandaloneView.php index 6fdb79413a..2c37d85ab6 100644 --- a/Neos.FluidAdaptor/Classes/View/StandaloneView.php +++ b/Neos.FluidAdaptor/Classes/View/StandaloneView.php @@ -75,7 +75,7 @@ public static function createWithOptions(array $options): self /** * Constructor * - * @param ActionRequest $request The current action request. If none is specified it will be created from the environment. + * @param ActionRequest|null $request The current action request. If none is specified it will be created from the environment. * @param array $options * @throws \Neos\FluidAdaptor\Exception */ @@ -90,7 +90,7 @@ public function __construct(?ActionRequest $request = null, array $options = []) * * @return void */ - public function initializeObject() + public function initializeObject(): void { if ($this->request === null) { $requestHandler = $this->bootstrap->getActiveRequestHandler(); @@ -116,7 +116,7 @@ public function initializeObject() /** * @param string $templateName */ - public function setTemplate($templateName) + public function setTemplate($templateName): void { $this->baseRenderingContext->setControllerAction($templateName); } @@ -128,7 +128,7 @@ public function setTemplate($templateName) * @return void * @api */ - public function setFormat($format) + public function setFormat($format): void { $this->request->setFormat($format); $this->baseRenderingContext->getTemplatePaths()->setFormat($format); diff --git a/Neos.FluidAdaptor/Classes/View/TemplatePaths.php b/Neos.FluidAdaptor/Classes/View/TemplatePaths.php index ee45bae7eb..bdbcd4d385 100644 --- a/Neos.FluidAdaptor/Classes/View/TemplatePaths.php +++ b/Neos.FluidAdaptor/Classes/View/TemplatePaths.php @@ -61,13 +61,6 @@ class TemplatePaths extends \TYPO3Fluid\Fluid\View\TemplatePaths */ protected $packageManager; - public function __construct(array $options = []) - { - foreach ($options as $optionName => $optionValue) { - $this->setOption($optionName, $optionValue); - } - } - /** * @param PackageManager $packageManager */ @@ -119,9 +112,9 @@ public function setTemplateRootPath($templateRootPath) /** * Resolves the template root to be used inside other paths. * - * @return array Path(s) to template root directory + * @return string[] */ - public function getTemplateRootPaths() + public function getTemplateRootPaths(): array { if ($this->templateRootPaths !== []) { return $this->templateRootPaths; @@ -140,9 +133,9 @@ public function getTemplateRootPaths() } /** - * @return array + * @return string[] */ - public function getLayoutRootPaths() + public function getLayoutRootPaths(): array { if ($this->layoutRootPaths !== []) { return $this->layoutRootPaths; @@ -160,7 +153,10 @@ public function getLayoutRootPaths() return [$layoutRootPath]; } - public function getPartialRootPaths() + /** + * @return string[] + */ + public function getPartialRootPaths(): array { if ($this->partialRootPaths !== []) { return $this->partialRootPaths; @@ -217,10 +213,9 @@ public function setPatternReplacementVariables($patternReplacementVariables) * @param string $controller * @param string $action * @param string $format - * @return mixed|string * @throws Exception\InvalidTemplateResourceException */ - public function resolveTemplateFileForControllerAndActionAndFormat($controller, $action, $format = null) + public function resolveTemplateFileForControllerAndActionAndFormat(string $controller, string $action, ?string $format = null): null|string { if ($this->templatePathAndFilename) { return $this->templatePathAndFilename; @@ -229,7 +224,7 @@ public function resolveTemplateFileForControllerAndActionAndFormat($controller, $action = ucfirst($action); $paths = $this->getTemplateRootPaths(); - if (isset($this->options['templatePathAndFilenamePattern'])) { + if (!empty($this->options['templatePathAndFilenamePattern'])) { $paths = $this->expandGenericPathPattern($this->options['templatePathAndFilenamePattern'], array_merge($this->patternReplacementVariables, [ 'controllerName' => $controller, 'action' => $action, @@ -262,15 +257,15 @@ public function resolveTemplateFileForControllerAndActionAndFormat($controller, * @return string Path and filename of layout files * @throws Exception\InvalidTemplateResourceException */ - public function getLayoutPathAndFilename($layoutName = 'Default') + public function getLayoutPathAndFilename(string $layoutName = 'Default'): string { - if (isset($this->options['layoutPathAndFilename'])) { + if (!empty($this->options['layoutPathAndFilename'])) { return $this->options['layoutPathAndFilename']; } $layoutName = ucfirst($layoutName); $paths = $this->getLayoutRootPaths(); - if (isset($this->options['layoutPathAndFilenamePattern'])) { + if (!empty($this->options['layoutPathAndFilenamePattern'])) { $paths = $this->expandGenericPathPattern($this->options['layoutPathAndFilenamePattern'], array_merge($this->patternReplacementVariables, [ 'layout' => $layoutName ]), true, true); @@ -291,14 +286,14 @@ public function getLayoutPathAndFilename($layoutName = 'Default') * @return string the full path which should be used. The path definitely exists. * @throws InvalidTemplateResourceException */ - public function getPartialPathAndFilename($partialName) + public function getPartialPathAndFilename(string $partialName): string { $patternReplacementVariables = array_merge($this->patternReplacementVariables, [ 'partial' => $partialName, ]); - if (strpos($partialName, ':') !== false) { - list($packageKey, $actualPartialName) = explode(':', $partialName); + if (str_contains($partialName, ':')) { + [$packageKey, $actualPartialName] = explode(':', $partialName); /** @var FlowPackageInterface $package */ $package = $this->packageManager->getPackage($packageKey); $patternReplacementVariables['package'] = $packageKey; @@ -321,7 +316,7 @@ public function getPartialPathAndFilename($partialName) * @param string $packageName * @return string */ - protected function getPackagePath($packageName) + protected function getPackagePath(string $packageName): string { if ($this->packageManager === null) { return ''; @@ -344,7 +339,7 @@ protected function getPackagePath($packageName) * @param string $path * @return string */ - protected function sanitizePath($path) + protected function sanitizePath($path): string { if (empty($path)) { return ''; @@ -522,17 +517,21 @@ public function setOption($optionName, $value) } } + public function setOptions(array $options = []): void + { + foreach ($options as $optionName => $optionValue) { + $this->setOption($optionName, $optionValue); + } + } + /** * Returns a unique identifier for the given file in the format * ____ * The SH1 hash is a checksum that is based on the file path and last modification date * - * @param string|null $pathAndFilename - * @param string $prefix - * @return string * @throws InvalidTemplateResourceException */ - protected function createIdentifierForFile($pathAndFilename, $prefix) + protected function createIdentifierForFile(?string $pathAndFilename, string $prefix): string { $pathAndFilename = (string)$pathAndFilename; $templateModifiedTimestamp = 0; diff --git a/Neos.FluidAdaptor/Classes/View/TemplateView.php b/Neos.FluidAdaptor/Classes/View/TemplateView.php index cae651373b..ac2a521333 100644 --- a/Neos.FluidAdaptor/Classes/View/TemplateView.php +++ b/Neos.FluidAdaptor/Classes/View/TemplateView.php @@ -11,11 +11,9 @@ * source code. */ -use Neos\Flow\Mvc\View\ViewInterface; - /** * A standard Flow view based on Fluid. */ -class TemplateView extends AbstractTemplateView implements ViewInterface +class TemplateView extends AbstractTemplateView { } diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/CheckboxViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/CheckboxViewHelper.php index d7d4e3b574..335abc9271 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/CheckboxViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/CheckboxViewHelper.php @@ -63,7 +63,7 @@ public function initializeArguments() $this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this view helper', false, 'f3-form-error'); $this->registerArgument('checked', 'boolean', 'Specifies that the input element should be preselected', false, null); $this->registerArgument('multiple', 'boolean', 'Specifies whether this checkbox belongs to a multivalue (is part of a checkbox group)', false, null); - $this->overrideArgument('value', 'mixed', 'Value of input tag. Required for checkboxes', true); + $this->registerArgument('value', 'mixed', 'Value of input tag. Required for checkboxes', true); $this->registerUniversalTagAttributes(); } diff --git a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/RadioViewHelper.php b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/RadioViewHelper.php index 2e57853d7e..6c6f2d5d89 100644 --- a/Neos.FluidAdaptor/Classes/ViewHelpers/Form/RadioViewHelper.php +++ b/Neos.FluidAdaptor/Classes/ViewHelpers/Form/RadioViewHelper.php @@ -62,7 +62,7 @@ public function initializeArguments() $this->registerTagAttribute('disabled', 'boolean', 'Specifies that the input element should be disabled when the page loads', false, false); $this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this view helper', false, 'f3-form-error'); $this->registerArgument('checked', 'boolean', 'Specifies that the input element should be preselected', false, null); - $this->overrideArgument('value', 'mixed', 'Value of input tag. Required for radio buttons', true); + $this->registerArgument('value', 'mixed', 'Value of input tag. Required for radio buttons', true); $this->registerUniversalTagAttributes(); } diff --git a/Neos.FluidAdaptor/Tests/Unit/Core/Parser/Interceptor/ResourceInterceptorTest.php b/Neos.FluidAdaptor/Tests/Unit/Core/Parser/Interceptor/ResourceInterceptorTest.php index 6f44313956..ec838ff340 100644 --- a/Neos.FluidAdaptor/Tests/Unit/Core/Parser/Interceptor/ResourceInterceptorTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/Core/Parser/Interceptor/ResourceInterceptorTest.php @@ -50,9 +50,7 @@ public function resourcesInCssUrlsAreReplacedCorrectly() self::assertCount(3, $resultingNodeTree->getChildNodes()); foreach ($resultingNodeTree->getChildNodes() as $parserNode) { if ($parserNode instanceof ResourceUriNode) { - self::assertEquals([ - 'path' => $path - ], $parserNode->getArguments()); + self::assertEquals($path, $parserNode->path); } } } @@ -122,10 +120,8 @@ public function supportedUrlsAreDetected($part1, $part2, $part3, $expectedPath, self::assertCount(3, $resultingNodeTree->getChildNodes()); foreach ($resultingNodeTree->getChildNodes() as $parserNode) { if ($parserNode instanceof ResourceUriNode) { - self::assertEquals([ - 'path' => $expectedPath, - 'package' => $expectedPackageKey - ], $parserNode->getArguments()); + self::assertEquals($expectedPath, $parserNode->path); + self::assertEquals($expectedPackageKey, $parserNode->package); } } } diff --git a/Neos.FluidAdaptor/Tests/Unit/Core/ViewHelper/AbstractViewHelperTest.php b/Neos.FluidAdaptor/Tests/Unit/Core/ViewHelper/AbstractViewHelperTest.php index 15b5a8a582..392e26ec0c 100644 --- a/Neos.FluidAdaptor/Tests/Unit/Core/ViewHelper/AbstractViewHelperTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/Core/ViewHelper/AbstractViewHelperTest.php @@ -15,11 +15,11 @@ use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Reflection\ReflectionService; use Neos\Flow\Tests\UnitTestCase; +use Neos\FluidAdaptor\Core\Rendering\RenderingContext; use Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper; use Neos\FluidAdaptor\Core\ViewHelper\TemplateVariableContainer; use Neos\FluidAdaptor\View\TemplateView; use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition; -use TYPO3Fluid\Fluid\Core\ViewHelper\Exception; use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperVariableContainer; require_once(__DIR__ . '/../Fixtures/TestViewHelper.php'); @@ -105,41 +105,6 @@ public function argumentsCanBeRegistered(): void self::assertEquals([$name => $expected], $viewHelper->prepareArguments(), 'Argument definitions not returned correctly.'); } - /** - * @test - */ - public function overrideArgumentOverwritesExistingArgumentDefinition(): void - { - $this->mockReflectionService->expects(self::any())->method('getMethodParameters')->willReturn([]); - - $viewHelper = $this->getAccessibleMock(AbstractViewHelper::class, ['render'], [], '', false); - $viewHelper->injectObjectManager($this->mockObjectManager); - - $name = 'argumentName'; - $description = 'argument description'; - $overriddenDescription = 'overwritten argument description'; - $type = 'string'; - $overriddenType = 'integer'; - $isRequired = true; - $expected = new ArgumentDefinition($name, $overriddenType, $overriddenDescription, $isRequired); - - $viewHelper->_call('registerArgument', $name, $type, $description, $isRequired); - $viewHelper->_call('overrideArgument', $name, $overriddenType, $overriddenDescription, $isRequired); - self::assertEquals($viewHelper->prepareArguments(), [$name => $expected], 'Argument definitions not returned correctly. The original ArgumentDefinition could not be overridden.'); - } - - /** - * @test - */ - public function overrideArgumentThrowsExceptionWhenTryingToOverwriteAnNonexistingArgument(): void - { - $this->expectException(Exception::class); - $viewHelper = $this->getAccessibleMock(AbstractViewHelper::class, ['render'], [], '', false); - $viewHelper->injectObjectManager($this->mockObjectManager); - - $viewHelper->_call('overrideArgument', 'argumentName', 'string', 'description', true); - } - /** * @test */ @@ -221,22 +186,22 @@ public function validateArgumentsCallsTheRightValidatorsAndThrowsExceptionIfVali public function initializeArgumentsAndRenderCallsTheCorrectSequenceOfMethods(): void { $calls = []; - $viewHelper = $this->getAccessibleMock(AbstractViewHelper::class, ['validateArguments', 'initialize', 'callRenderMethod']); + $viewHelper = $this->getAccessibleMock(AbstractViewHelper::class, ['validateArguments', 'initialize', 'render']); $viewHelper->expects(self::atLeastOnce())->method('validateArguments')->willReturnCallback(function () use (&$calls) { $calls[] = 'validateArguments'; }); $viewHelper->expects(self::atLeastOnce())->method('initialize')->willReturnCallback(function () use (&$calls) { $calls[] = 'initialize'; }); - $viewHelper->expects(self::atLeastOnce())->method('callRenderMethod')->willReturnCallback(function () use (&$calls) { - $calls[] = 'callRenderMethod'; + $viewHelper->expects(self::atLeastOnce())->method('render')->willReturnCallback(function () use (&$calls) { + $calls[] = 'render'; return 'Output'; }); $expectedOutput = 'Output'; $actualOutput = $viewHelper->initializeArgumentsAndRender(['argument1' => 'value1']); self::assertEquals($expectedOutput, $actualOutput); - self::assertEquals(['validateArguments', 'initialize', 'callRenderMethod'], $calls); + self::assertEquals(['validateArguments', 'initialize', 'render'], $calls); } /** @@ -249,6 +214,7 @@ public function setRenderingContextShouldSetInnerVariables(): void $controllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); $dummyView = new TemplateView([]); + /** @var RenderingContext $renderingContext */ $renderingContext = $dummyView->getRenderingContext(); $renderingContext->setVariableProvider($templateVariableContainer); $renderingContext->setViewHelperVariableContainer($viewHelperVariableContainer); diff --git a/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AbstractWidgetViewHelperTest.php b/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AbstractWidgetViewHelperTest.php index 273015755e..5edbeb758a 100644 --- a/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AbstractWidgetViewHelperTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/Core/Widget/AbstractWidgetViewHelperTest.php @@ -11,6 +11,8 @@ * source code. */ +use Neos\FluidAdaptor\Core\Widget\AbstractWidgetController; +use Neos\FluidAdaptor\Core\Widget\AbstractWidgetViewHelper; use Neos\FluidAdaptor\Core\Widget\Exception\MissingControllerException; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\AbstractNode; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode; @@ -52,25 +54,51 @@ class AbstractWidgetViewHelperTest extends \Neos\Flow\Tests\UnitTestCase */ protected $request; + /** + * @var AbstractWidgetController|__anonymous@2351 + */ + protected $testWidgetControllerClass; + /** */ protected function setUp(): void { - $this->viewHelper = $this->getAccessibleMock(\Neos\FluidAdaptor\Core\Widget\AbstractWidgetViewHelper::class, ['validateArguments', 'initialize', 'callRenderMethod', 'getWidgetConfiguration', 'getRenderingContext']); - $this->ajaxWidgetContextHolder = $this->createMock(\Neos\FluidAdaptor\Core\Widget\AjaxWidgetContextHolder::class); - $this->viewHelper->injectAjaxWidgetContextHolder($this->ajaxWidgetContextHolder); - $this->widgetContext = $this->createMock(\Neos\FluidAdaptor\Core\Widget\WidgetContext::class); - $this->viewHelper->injectWidgetContext($this->widgetContext); - $this->objectManager = $this->createMock(\Neos\Flow\ObjectManagement\ObjectManagerInterface::class); - $this->objectManager->expects(self::any())->method('get')->with(\Neos\FluidAdaptor\Core\Widget\WidgetContext::class)->will(self::returnValue($this->widgetContext)); - $this->viewHelper->injectObjectManager($this->objectManager); - + $this->objectManager->expects(self::any())->method('get')->with(\Neos\FluidAdaptor\Core\Widget\WidgetContext::class)->willReturn($this->widgetContext); $this->controllerContext = $this->getMockBuilder(\Neos\Flow\Mvc\Controller\ControllerContext::class)->disableOriginalConstructor()->getMock(); - $this->viewHelper->_set('controllerContext', $this->controllerContext); - + $this->testWidgetControllerClass = new class extends AbstractWidgetController { + }; + $testWidgetViewHelperClass = new class extends AbstractWidgetViewHelper { + public function setAjax(bool $ajax): void + { + $this->ajaxWidget = $ajax; + } + public function injectRenderingContext(RenderingContextInterface $renderingContext): void + { + $this->renderingContext = $renderingContext; + } + public function injectController($controller): void + { + $this->controller = $controller; + } + public function render(): string + { + return 'renderedResult'; + } + + public function initiateSubRequest(): void + { + parent::initiateSubRequest(); + } + }; + + $this->viewHelper = $testWidgetViewHelperClass; + $this->viewHelper->injectWidgetContext($this->widgetContext); + $this->viewHelper->injectController($this->testWidgetControllerClass); + $this->viewHelper->injectObjectManager($this->objectManager); + $this->viewHelper->injectAjaxWidgetContextHolder($this->ajaxWidgetContextHolder); $this->request = $this->getMockBuilder(\Neos\Flow\Mvc\ActionRequest::class)->disableOriginalConstructor()->getMock(); } @@ -79,6 +107,7 @@ protected function setUp(): void */ public function initializeArgumentsAndRenderCallsTheRightSequenceOfMethods() { + $this->widgetContext->expects(self::once())->method('setControllerObjectName')->with(get_class($this->testWidgetControllerClass)); $this->callViewHelper(); } @@ -87,10 +116,8 @@ public function initializeArgumentsAndRenderCallsTheRightSequenceOfMethods() */ public function initializeArgumentsAndRenderDoesNotStoreTheWidgetContextForStatelessWidgets() { - $this->viewHelper->_set('ajaxWidget', true); - $this->viewHelper->_set('storeConfigurationInSession', false); $this->ajaxWidgetContextHolder->expects(self::never())->method('store'); - + $this->widgetContext->expects(self::once())->method('setControllerObjectName')->with(get_class($this->testWidgetControllerClass)); $this->callViewHelper(); } @@ -99,9 +126,9 @@ public function initializeArgumentsAndRenderDoesNotStoreTheWidgetContextForState */ public function initializeArgumentsAndRenderStoresTheWidgetContextIfInAjaxMode() { - $this->viewHelper->_set('ajaxWidget', true); + $this->viewHelper->setAjax(true); $this->ajaxWidgetContextHolder->expects(self::once())->method('store')->with($this->widgetContext); - + $this->widgetContext->expects(self::once())->method('setControllerObjectName')->with(get_class($this->testWidgetControllerClass)); $this->callViewHelper(); } @@ -112,17 +139,8 @@ public function initializeArgumentsAndRenderStoresTheWidgetContextIfInAjaxMode() */ public function callViewHelper() { - $this->viewHelper->expects(self::any())->method('getWidgetConfiguration')->will(self::returnValue(['Some Widget Configuration'])); - $this->widgetContext->expects(self::once())->method('setNonAjaxWidgetConfiguration')->with(['Some Widget Configuration']); - + $this->widgetContext->expects(self::once())->method('setNonAjaxWidgetConfiguration')->with([]); $this->widgetContext->expects(self::once())->method('setWidgetIdentifier')->with(strtolower(str_replace('\\', '-', get_class($this->viewHelper)))); - - $this->viewHelper->_set('controller', new \stdClass()); - $this->widgetContext->expects(self::once())->method('setControllerObjectName')->with('stdClass'); - - $this->viewHelper->expects(self::once())->method('validateArguments'); - $this->viewHelper->expects(self::once())->method('initialize'); - $this->viewHelper->expects(self::once())->method('callRenderMethod')->will(self::returnValue('renderedResult')); $output = $this->viewHelper->initializeArgumentsAndRender(['arg1' => 'val1']); self::assertEquals('renderedResult', $output); } @@ -145,8 +163,7 @@ public function setChildNodesAddsChildNodesToWidgetContext() $rootNode->addChildNode($node3); $renderingContext = $this->createMock(RenderingContextInterface::class); - $this->viewHelper->_set('renderingContext', $renderingContext); - + $this->viewHelper->injectRenderingContext($renderingContext); $this->viewHelper->setChildNodes([$node1, $node2, $node3]); self::assertEquals($rootNode, $this->widgetContext->getViewHelperChildNodes()); @@ -159,8 +176,8 @@ public function initiateSubRequestThrowsExceptionIfControllerIsNoWidgetControlle { $this->expectException(MissingControllerException::class); $controller = $this->createMock(\Neos\Flow\Mvc\Controller\ControllerInterface::class); - $this->viewHelper->_set('controller', $controller); + $this->viewHelper->injectController($controller); - $this->viewHelper->_call('initiateSubRequest'); + $this->viewHelper->initiateSubRequest(); } } diff --git a/Neos.FluidAdaptor/Tests/Unit/View/Fixtures/TemplatePathsTestAccessor.php b/Neos.FluidAdaptor/Tests/Unit/View/Fixtures/TemplatePathsTestAccessor.php new file mode 100644 index 0000000000..a573151ec9 --- /dev/null +++ b/Neos.FluidAdaptor/Tests/Unit/View/Fixtures/TemplatePathsTestAccessor.php @@ -0,0 +1,25 @@ +expandGenericPathPattern($pattern, $patternReplacementVariables, $bubbleControllerAndSubpackage, $formatIsOptional); + }; + + $accessor = $accessor->bindTo($this->templatePaths, TemplatePaths::class); + + return $accessor($pattern, $patternReplacementVariables, $bubbleControllerAndSubpackage, $formatIsOptional); + } +} diff --git a/Neos.FluidAdaptor/Tests/Unit/View/StandaloneViewTest.php b/Neos.FluidAdaptor/Tests/Unit/View/StandaloneViewTest.php index f866cf119d..5d02b826d6 100644 --- a/Neos.FluidAdaptor/Tests/Unit/View/StandaloneViewTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/View/StandaloneViewTest.php @@ -41,12 +41,11 @@ class StandaloneViewTest extends UnitTestCase protected function setUp(): void { - $this->standaloneView = $this->getAccessibleMock(\Neos\FluidAdaptor\View\StandaloneView::class, ['dummy']); - $this->mockRequest = $this->getMockBuilder(\Neos\Flow\Mvc\ActionRequest::class)->disableOriginalConstructor()->getMock(); - $this->mockControllerContext = $this->getMockBuilder(\Neos\Flow\Mvc\Controller\ControllerContext::class)->disableOriginalConstructor()->getMock(); - $this->mockControllerContext->expects(self::any())->method('getRequest')->will(self::returnValue($this->mockRequest)); - $this->inject($this->standaloneView, 'controllerContext', $this->mockControllerContext); + $this->standaloneView = new StandaloneView($this->mockRequest); +// $this->mockControllerContext = $this->getMockBuilder(\Neos\Flow\Mvc\Controller\ControllerContext::class)->disableOriginalConstructor()->getMock(); +// $this->mockControllerContext->expects(self::any())->method('getRequest')->will(self::returnValue($this->mockRequest)); +// $this->inject($this->standaloneView, 'controllerContext', $this->mockControllerContext); } /** @@ -59,7 +58,7 @@ public function getLayoutPathAndFilenameThrowsExceptionIfSpecifiedLayoutRootPath mkdir('vfs://MyLayouts'); \file_put_contents('vfs://MyLayouts/NotAFolder', 'foo'); $this->standaloneView->setLayoutRootPath('vfs://MyLayouts/NotAFolder'); - $this->standaloneView->getTemplatePaths()->getLayoutSource(); + $this->standaloneView->getRenderingContext()->getTemplatePaths()->getLayoutSource(); } /** @@ -71,7 +70,7 @@ public function getLayoutPathAndFilenameThrowsExceptionIfLayoutFileIsADirectory( vfsStreamWrapper::register(); mkdir('vfs://MyLayouts/NotAFile'); $this->standaloneView->setLayoutRootPath('vfs://MyLayouts'); - $this->standaloneView->getTemplatePaths()->getLayoutSource('NotAFile'); + $this->standaloneView->getRenderingContext()->getTemplatePaths()->getLayoutSource('NotAFile'); } /** @@ -84,7 +83,7 @@ public function getPartialPathAndFilenameThrowsExceptionIfSpecifiedPartialRootPa mkdir('vfs://MyPartials'); \file_put_contents('vfs://MyPartials/NotAFolder', 'foo'); $this->standaloneView->setPartialRootPath('vfs://MyPartials/NotAFolder'); - $this->standaloneView->getTemplatePaths()->getPartialSource('SomePartial'); + $this->standaloneView->getRenderingContext()->getTemplatePaths()->getPartialSource('SomePartial'); } /** @@ -96,6 +95,6 @@ public function getPartialPathAndFilenameThrowsExceptionIfPartialFileIsADirector vfsStreamWrapper::register(); mkdir('vfs://MyPartials/NotAFile'); $this->standaloneView->setPartialRootPath('vfs://MyPartials'); - $this->standaloneView->getTemplatePaths()->getPartialSource('NotAFile'); + $this->standaloneView->getRenderingContext()->getTemplatePaths()->getPartialSource('NotAFile'); } } diff --git a/Neos.FluidAdaptor/Tests/Unit/View/TemplatePathsTest.php b/Neos.FluidAdaptor/Tests/Unit/View/TemplatePathsTest.php index 25675dcd0c..a2a3284e7a 100644 --- a/Neos.FluidAdaptor/Tests/Unit/View/TemplatePathsTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/View/TemplatePathsTest.php @@ -4,6 +4,7 @@ use GuzzleHttp\Psr7\ServerRequest; use GuzzleHttp\Psr7\Uri; use Neos\FluidAdaptor\View\Exception\InvalidTemplateResourceException; +use Neos\FluidAdaptor\Tests\Unit\View\Fixtures\TemplatePathsTestAccessor; use org\bovigo\vfs\vfsStreamWrapper; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Controller\ControllerContext; @@ -43,7 +44,11 @@ protected function setupMockControllerContextForPathResolving($packageKey, $subP return $mockControllerContext; } - public function expandGenericPathPatternDataProvider() + /** + * Test data provider + * @return array[] + */ + public function expandGenericPathPatternDataProvider(): array { return [ // bubbling controller & subpackage parts and optional format @@ -476,8 +481,8 @@ public function expandGenericPathPatternTests($package, $subPackage, $controller $options['layoutRootPaths'] = $layoutRootPaths; } - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['dummy'], [$options], '', true); + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions($options); $patternReplacementVariables = [ 'packageKey' => $package, 'subPackageKey' => $subPackage, @@ -485,7 +490,8 @@ public function expandGenericPathPatternTests($package, $subPackage, $controller 'format' => $format ]; - $actualResult = $templatePaths->_call('expandGenericPathPattern', $pattern, $patternReplacementVariables, $bubbleControllerAndSubpackage, $formatIsOptional); + $accessor = new TemplatePathsTestAccessor($templatePaths); + $actualResult = $accessor->expandGenericPathPattern($pattern, $patternReplacementVariables, $bubbleControllerAndSubpackage, $formatIsOptional); self::assertEquals($expectedResult, $actualResult); } @@ -498,11 +504,11 @@ public function expandGenericPathPatternWorksWithBubblingDisabledAndFormatNotOpt 'templateRootPaths' => ['Resources/Private/'] ]; - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, null, [$options], '', true); - + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions($options); $expected = ['Resources/Private/Templates/My/@action.html']; - $actual = $templatePaths->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', [ + $accessor = new TemplatePathsTestAccessor($templatePaths); + $actual = $accessor->expandGenericPathPattern('@templateRoot/Templates/@subpackage/@controller/@action.@format', [ 'subPackageKey' => null, 'controllerName' => 'My', 'format' => 'html' @@ -519,10 +525,10 @@ public function expandGenericPathPatternWorksWithSubpackageAndBubblingDisabledAn 'templateRootPaths' => ['Resources/Private/'] ]; - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, null, [$options], '', true); - - $actual = $templatePaths->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', [ + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions($options); + $accessor = new TemplatePathsTestAccessor($templatePaths); + $actual = $accessor->expandGenericPathPattern('@templateRoot/Templates/@subpackage/@controller/@action.@format', [ 'subPackageKey' => 'MySubPackage', 'controllerName' => 'My', 'format' => 'html' @@ -543,10 +549,10 @@ public function expandGenericPathPatternWorksWithSubpackageAndBubblingDisabledAn 'templateRootPaths' => ['Resources/Private/'] ]; - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, null, [$options], '', true); - - $actual = $templatePaths->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', [ + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions($options); + $accessor = new TemplatePathsTestAccessor($templatePaths); + $actual = $accessor->expandGenericPathPattern('@templateRoot/Templates/@subpackage/@controller/@action.@format', [ 'subPackageKey' => 'MySubPackage', 'controllerName' => 'My', 'format' => 'html' @@ -568,10 +574,10 @@ public function expandGenericPathPatternWorksWithSubpackageAndBubblingEnabledAnd 'templateRootPaths' => ['Resources/Private/'] ]; - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, null, [$options], '', true); - - $actual = $templatePaths->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', [ + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions($options); + $accessor = new TemplatePathsTestAccessor($templatePaths); + $actual = $accessor->expandGenericPathPattern('@templateRoot/Templates/@subpackage/@controller/@action.@format', [ 'subPackageKey' => 'MySubPackage', 'controllerName' => 'My', 'format' => 'html' @@ -597,16 +603,9 @@ public function pathToPartialIsResolvedCorrectly() mkdir('vfs://MyPartials'); \file_put_contents('vfs://MyPartials/SomePartial', 'contentsOfSomePartial'); - $paths = [ - 'vfs://NonExistentDir/UnknowFile.html', - 'vfs://MyPartials/SomePartial.html', - 'vfs://MyPartials/SomePartial' - ]; - - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['expandGenericPathPattern'], [[ - 'partialPathAndFilenamePattern' => '@partialRoot/@subpackage/@partial.@format' - ]], '', true); - $templatePaths->expects(self::once())->method('expandGenericPathPattern')->with('@partialRoot/@subpackage/@partial.@format', ['partial' => 'SomePartial', 'format' => 'html'], true, true)->will(self::returnValue($paths)); + $templatePaths = new TemplatePaths(); + $templatePaths->setOption('partialPathAndFilenamePattern', '@partialRoot/@subpackage/@partial.@format'); + $templatePaths->setPatternReplacementVariables(['partialRoot' => 'vfs://MyPartials']); self::assertSame('contentsOfSomePartial', $templatePaths->getPartialSource('SomePartial')); } @@ -620,24 +619,17 @@ public function getTemplateSourceChecksDifferentPathPatternsAndReturnsTheFirstPa mkdir('vfs://MyTemplates'); file_put_contents('vfs://MyTemplates/MyCoolAction.html', 'contentsOfMyCoolAction'); - $paths = [ - 'vfs://NonExistentDir/UnknownFile.html', - 'vfs://MyTemplates/@action.html', - 'vfs://MyTemplates/MyCoolAction.html' - ]; + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions([ + 'templatePathAndFilenamePattern' => '@templateRoot/@subpackage/@controller/@action.@format' + ]); - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['expandGenericPathPattern'], [ - [ - 'templatePathAndFilenamePattern' => '@templateRoot/@subpackage/@controller/@action.@format' - ] - ], '', true); - - $templatePaths->expects(self::once())->method('expandGenericPathPattern')->with('@templateRoot/@subpackage/@controller/@action.@format', [ + $templatePaths->setPatternReplacementVariables([ + 'templateRoot' => 'vfs://MyTemplates', 'controllerName' => '', 'action' => 'MyCoolAction', 'format' => 'html' - ], false, false)->will(self::returnValue($paths)); - + ]); self::assertSame('contentsOfMyCoolAction', $templatePaths->getTemplateSource('', 'myCoolAction')); } @@ -653,18 +645,14 @@ public function getTemplatePathAndFilenameThrowsExceptionIfNoPathCanBeResolved() 'vfs://NonExistentDir/AnotherUnknownFile.html', ]; - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['expandGenericPathPattern'], [ - [ - 'templatePathAndFilenamePattern' => '@templateRoot/@subpackage/@controller/@action.@format' - ] - ], '', true); - - $templatePaths->expects(self::once())->method('expandGenericPathPattern')->with('@templateRoot/@subpackage/@controller/@action.@format', [ + $templatePaths = new TemplatePaths(); + $templatePaths->setOption('templatePathAndFilenamePattern', '@templateRoot/@subpackage/@controller/@action.@format'); + $templatePaths->setPatternReplacementVariables([ + 'templateRoot' => 'vfs://MyTemplates', 'controllerName' => '', 'action' => 'MyCoolAction', 'format' => 'html' - ], false, false)->will(self::returnValue($paths)); - + ]); $templatePaths->getTemplateSource('', 'myCoolAction'); } @@ -681,17 +669,8 @@ public function getTemplatePathAndFilenameThrowsExceptionIfResolvedPathPointsToA 'vfs://MyTemplates/NotAFile' ]; - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['expandGenericPathPattern'], [ - [ - 'templatePathAndFilenamePattern' => '@templateRoot/@subpackage/@controller/@action.@format' - ] - ], '', true); - - $templatePaths->expects(self::once())->method('expandGenericPathPattern')->with('@templateRoot/@subpackage/@controller/@action.@format', [ - 'controllerName' => '', - 'action' => 'MyCoolAction', - 'format' => 'html' - ], false, false)->will(self::returnValue($paths)); + $templatePaths = new TemplatePaths(); + $templatePaths->setOption('templatePathAndFilenamePattern', '@templateRoot/@subpackage/@controller/@action.@format'); $templatePaths->getTemplateSource('', 'myCoolAction'); } @@ -705,9 +684,10 @@ public function resolveTemplatePathAndFilenameReturnsTheExplicitlyConfiguredTemp mkdir('vfs://MyTemplates'); file_put_contents('vfs://MyTemplates/MyCoolAction.html', 'contentsOfMyCoolAction'); - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['dummy'], [['templatePathAndFilename' => 'vfs://MyTemplates/MyCoolAction.html']]); + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions(['templatePathAndFilename' => 'vfs://MyTemplates/MyCoolAction.html']); - self::assertSame('contentsOfMyCoolAction', $templatePaths->_call('getTemplateSource')); + self::assertSame('contentsOfMyCoolAction', $templatePaths->getTemplateSource()); } /** @@ -717,22 +697,9 @@ public function getLayoutPathAndFilenameThrowsExceptionIfNoPathCanBeResolved() { $this->expectException(InvalidTemplateResourceException::class); vfsStreamWrapper::register(); - $paths = [ - 'vfs://NonExistentDir/UnknownFile.html', - 'vfs://NonExistentDir/AnotherUnknownFile.html', - ]; - - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['expandGenericPathPattern'], [ - [ - 'layoutPathAndFilenamePattern' => '@layoutRoot/@layout.@format' - ] - ], '', true); - $templatePaths->expects(self::once())->method('expandGenericPathPattern')->with('@layoutRoot/@layout.@format', [ - 'layout' => 'Default', - 'format' => 'html' - ], true, true)->will(self::returnValue($paths)); + $templatePaths = new TemplatePaths(); + $templatePaths->setOption('templatePathAndFilenamePattern', '@layoutRoot/@layout.@format'); $templatePaths->getLayoutSource(); } @@ -745,22 +712,11 @@ public function getLayoutPathAndFilenameThrowsExceptionIfResolvedPathPointsToADi $this->expectException(InvalidTemplateResourceException::class); vfsStreamWrapper::register(); mkdir('vfs://MyTemplates/NotAFile'); - $paths = [ - 'vfs://NonExistentDir/UnknownFile.html', - 'vfs://MyTemplates/NotAFile' - ]; - - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['expandGenericPathPattern'], [ - [ - 'layoutPathAndFilenamePattern' => '@layoutRoot/@layout.@format' - ] - ], '', true); - $templatePaths->expects(self::once())->method('expandGenericPathPattern')->with('@layoutRoot/@layout.@format', [ - 'layout' => 'SomeLayout', - 'format' => 'html' - ], true, true)->will(self::returnValue($paths)); + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions([ + 'layoutPathAndFilenamePattern' => '@layoutRoot/@layout.@format' + ]); $templatePaths->getLayoutSource('SomeLayout'); } @@ -772,22 +728,11 @@ public function getPartialPathAndFilenameThrowsExceptionIfNoPathCanBeResolved() { $this->expectException(InvalidTemplateResourceException::class); vfsStreamWrapper::register(); - $paths = [ - 'vfs://NonExistentDir/UnknownFile.html', - 'vfs://NonExistentDir/AnotherUnknownFile.html', - ]; - - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['expandGenericPathPattern'], [ - [ - 'partialPathAndFilenamePattern' => '@partialRoot/@subpackage/@partial.@format' - ] - ], '', true); - $templatePaths->expects(self::once())->method('expandGenericPathPattern')->with('@partialRoot/@subpackage/@partial.@format', [ - 'partial' => 'SomePartial', - 'format' => 'html' - ], true, true)->will(self::returnValue($paths)); + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions([ + 'partialPathAndFilenamePattern' => '@partialRoot/@subpackage/@partial.@format' + ]); $templatePaths->getPartialSource('SomePartial'); } @@ -800,22 +745,11 @@ public function getPartialPathAndFilenameThrowsExceptionIfResolvedPathPointsToAD $this->expectException(InvalidTemplateResourceException::class); vfsStreamWrapper::register(); mkdir('vfs://MyTemplates/NotAFile'); - $paths = [ - 'vfs://NonExistentDir/UnknownFile.html', - 'vfs://MyTemplates/NotAFile' - ]; - - /** @var TemplatePaths $templatePaths */ - $templatePaths = $this->getAccessibleMock(TemplatePaths::class, ['expandGenericPathPattern'], [ - [ - 'partialPathAndFilenamePattern' => '@partialRoot/@subpackage/@partial.@format' - ] - ], '', true); - $templatePaths->expects(self::once())->method('expandGenericPathPattern')->with('@partialRoot/@subpackage/@partial.@format', [ - 'partial' => 'SomePartial', - 'format' => 'html' - ], true, true)->will(self::returnValue($paths)); + $templatePaths = new TemplatePaths(); + $templatePaths->setOptions([ + 'partialPathAndFilenamePattern' => '@partialRoot/@subpackage/@partial.@format' + ]); $templatePaths->getPartialSource('SomePartial'); } diff --git a/Neos.FluidAdaptor/Tests/Unit/View/TemplateViewTest.php b/Neos.FluidAdaptor/Tests/Unit/View/TemplateViewTest.php index b8d741b72a..b1b2d64301 100644 --- a/Neos.FluidAdaptor/Tests/Unit/View/TemplateViewTest.php +++ b/Neos.FluidAdaptor/Tests/Unit/View/TemplateViewTest.php @@ -62,7 +62,7 @@ public function getTemplateRootPathsReturnsUserSpecifiedTemplatePaths() $templateRootPaths = ['/foo/bar/', 'baz/']; $templateView->setOption('templateRootPaths', $templateRootPaths); - $actual = $templateView->getTemplatePaths()->getTemplateRootPaths(); + $actual = $templateView->getRenderingContext()->getTemplatePaths()->getTemplateRootPaths(); self::assertEquals($templateRootPaths, $actual, 'A set template root path was not returned correctly.'); } @@ -76,7 +76,7 @@ public function getPartialRootPathsReturnsUserSpecifiedPartialPath() $partialRootPaths = ['/foo/bar/', 'baz/']; $templateView->setOption('partialRootPaths', $partialRootPaths); - $actual = $templateView->getTemplatePaths()->getPartialRootPaths(); + $actual = $templateView->getRenderingContext()->getTemplatePaths()->getPartialRootPaths(); self::assertEquals($partialRootPaths, $actual, 'A set partial root path was not returned correctly.'); } @@ -90,7 +90,7 @@ public function getLayoutRootPathsReturnsUserSpecifiedPartialPaths() $layoutRootPaths = ['/foo/bar/', 'baz/']; $templateView->setOption('layoutRootPaths', $layoutRootPaths); - $actual = $templateView->getTemplatePaths()->getLayoutRootPaths(); + $actual = $templateView->getRenderingContext()->getTemplatePaths()->getLayoutRootPaths(); self::assertEquals($layoutRootPaths, $actual, 'A set layout root path was not returned correctly.'); } } diff --git a/Neos.FluidAdaptor/composer.json b/Neos.FluidAdaptor/composer.json index aa1350940e..e19adeda67 100644 --- a/Neos.FluidAdaptor/composer.json +++ b/Neos.FluidAdaptor/composer.json @@ -11,7 +11,7 @@ "neos/cache": "self.version", "neos/utility-files": "self.version", "neos/utility-objecthandling": "self.version", - "typo3fluid/fluid": "^2.8.0", + "typo3fluid/fluid": "^4.2.0", "psr/log": "^2.0 || ^3.0" }, "autoload": { diff --git a/Neos.Utility.ObjectHandling/Tests/Unit/ObjectAccessTest.php b/Neos.Utility.ObjectHandling/Tests/Unit/ObjectAccessTest.php index 1ed54a7d1f..2140dda444 100644 --- a/Neos.Utility.ObjectHandling/Tests/Unit/ObjectAccessTest.php +++ b/Neos.Utility.ObjectHandling/Tests/Unit/ObjectAccessTest.php @@ -42,7 +42,9 @@ protected function setUp(): void $this->dummyObject = new DummyClassWithGettersAndSetters(); $this->dummyObject->setProperty('string1'); $this->dummyObject->setAnotherProperty(42); + $original = error_reporting(E_ALL & ~E_DEPRECATED); $this->dummyObject->shouldNotBePickedUp = true; + error_reporting($original); } /** @@ -560,7 +562,7 @@ public function getPropertyUsingDirectAccessWorksOnPrivatePropertyOfProxyParent( public function setPropertyUsingDirectAccessWorksOnPrivatePropertyOfProxyParent() { $proxyObject = new ProxiedClassWithPrivateProperty(); - + ObjectAccess::setProperty($proxyObject, 'property', 'changed', true); self::assertEquals('changed', $proxyObject->getProperty()); } diff --git a/composer.json b/composer.json index 9bf75ac0c7..0b6b3fa6af 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "symfony/console": "^5.1||^6.0", "composer/composer": "^2.7.7", "egulias/email-validator": "^3.0||^4.0", - "typo3fluid/fluid": "^2.8.0", + "typo3fluid/fluid": "^4.2.0", "guzzlehttp/psr7": "^1.8.4 || ^2.1.1", "ext-mbstring": "*" }, From 1e50395372d4a6f2ba66e377b88253ce92268f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Mon, 18 Aug 2025 11:57:29 +0200 Subject: [PATCH 2/4] Code cleanup FluidAdaptor and mini fix --- .../Interceptor/ResourceInterceptor.php | 8 +- .../Parser/SyntaxTree/ResourceUriNode.php | 6 +- .../FlowAwareRenderingContextInterface.php | 4 +- .../Core/Rendering/RenderingContext.php | 24 +++--- .../AbstractConditionViewHelper.php | 24 +++--- .../AbstractLocaleAwareViewHelper.php | 2 +- .../ViewHelper/AbstractTagBasedViewHelper.php | 6 +- .../Core/ViewHelper/AbstractViewHelper.php | 17 ++--- ...RenderingContextNotAccessibleException.php | 23 ------ .../ViewHelper/TemplateVariableContainer.php | 14 ++-- .../Core/ViewHelper/ViewHelperResolver.php | 74 ++----------------- 11 files changed, 53 insertions(+), 149 deletions(-) delete mode 100644 Neos.FluidAdaptor/Classes/Core/ViewHelper/Exception/RenderingContextNotAccessibleException.php diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php b/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php index a8ca4bb96a..761f362142 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php @@ -42,7 +42,7 @@ class ResourceInterceptor implements InterceptorInterface * * @var string */ - private const PATTERN_SPLIT_AT_RESOURCE_URIS = '! + private const string PATTERN_SPLIT_AT_RESOURCE_URIS = '! ( (?:[^"\'(\s]+/ # URL part: A string with no quotes, no opening parentheses and no whitespace )* # a URL consists of multiple URL parts @@ -57,15 +57,15 @@ class ResourceInterceptor implements InterceptorInterface * @var string * @see \Neos\Flow\Package\FlowPackageKey::PATTERN */ - private const PATTERN_MATCH_RESOURCE_URI = '!(?:../)*(?:(?P[A-Za-z0-9]+\.(?:[A-Za-z0-9][\.a-z0-9]*)+)/Resources/)?Public/(?P[^"]+)!'; + private const string PATTERN_MATCH_RESOURCE_URI = '!(?:../)*(?:(?P[A-Za-z0-9]+\.(?:[A-Za-z0-9][\.a-z0-9]*)+)/Resources/)?Public/(?P[^"]+)!'; /** * The default package key to use when rendering resource links without a * package key in the source URL. * - * @var string|null + * @var string */ - protected ?string $defaultPackageKey = null; + protected string $defaultPackageKey = ''; /** * @Flow\Inject diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php b/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php index d2f010c018..f05a853f1a 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/SyntaxTree/ResourceUriNode.php @@ -46,7 +46,7 @@ public function injectService(Service $i18nService): void public function __construct( public readonly string $path, - public readonly string|null $package + public readonly string $package ) { } @@ -59,9 +59,9 @@ public function evaluate(RenderingContextInterface $renderingContext): string { $package = $this->package; $path = $this->path; - if ($package === null) { + if ($package === '') { /** @var RenderingContext $renderingContext */ - $package = $renderingContext->getControllerContext()?->getRequest()?->getControllerPackageKey() ?? null; + $package = $renderingContext->getControllerContext()?->getRequest()?->getControllerPackageKey(); } if (str_starts_with($path, 'resource://')) { try { diff --git a/Neos.FluidAdaptor/Classes/Core/Rendering/FlowAwareRenderingContextInterface.php b/Neos.FluidAdaptor/Classes/Core/Rendering/FlowAwareRenderingContextInterface.php index 7bb88d9f20..cf42683c09 100644 --- a/Neos.FluidAdaptor/Classes/Core/Rendering/FlowAwareRenderingContextInterface.php +++ b/Neos.FluidAdaptor/Classes/Core/Rendering/FlowAwareRenderingContextInterface.php @@ -24,10 +24,10 @@ interface FlowAwareRenderingContextInterface /** * @return ObjectManagerInterface */ - public function getObjectManager(); + public function getObjectManager(): ObjectManagerInterface; /** * @return ControllerContext */ - public function getControllerContext(); + public function getControllerContext(): ControllerContext; } diff --git a/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php b/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php index 53dc59f976..5f3693a02d 100644 --- a/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php +++ b/Neos.FluidAdaptor/Classes/Core/Rendering/RenderingContext.php @@ -52,14 +52,14 @@ class RenderingContext extends FluidRenderingContext implements FlowAwareRenderi ]; /** - * @var ControllerContext + * @var ControllerContext|null */ - protected $controllerContext; + protected ControllerContext|null $controllerContext = null; /** - * @var ObjectManagerInterface + * @var ObjectManagerInterface|null */ - protected $objectManager; + protected ObjectManagerInterface|null $objectManager = null; /** * @Flow\Inject @@ -74,9 +74,9 @@ class RenderingContext extends FluidRenderingContext implements FlowAwareRenderi protected $cache; /** - * @var Configuration + * @var Configuration|null */ - protected $parserConfiguration; + protected Configuration|null $parserConfiguration = null; /** * RenderingContext constructor. @@ -102,7 +102,7 @@ public function __construct(array $options = []) /** * @param ObjectManagerInterface $objectManager */ - public function injectObjectManager(ObjectManagerInterface $objectManager) + public function injectObjectManager(ObjectManagerInterface $objectManager): void { $this->objectManager = $objectManager; } @@ -110,7 +110,7 @@ public function injectObjectManager(ObjectManagerInterface $objectManager) /** * @return ControllerContext */ - public function getControllerContext() + public function getControllerContext(): ControllerContext { return $this->controllerContext; } @@ -118,7 +118,7 @@ public function getControllerContext() /** * @param ControllerContext $controllerContext */ - public function setControllerContext($controllerContext) + public function setControllerContext(ControllerContext $controllerContext): void { $this->controllerContext = $controllerContext; $request = $controllerContext->getRequest(); @@ -138,7 +138,7 @@ public function setControllerContext($controllerContext) /** * @return ObjectManagerInterface */ - public function getObjectManager() + public function getObjectManager(): ObjectManagerInterface { return $this->objectManager; } @@ -148,7 +148,7 @@ public function getObjectManager() * * @return Configuration */ - public function buildParserConfiguration() + public function buildParserConfiguration(): Configuration { if ($this->parserConfiguration === null) { $this->parserConfiguration = parent::buildParserConfiguration(); @@ -166,7 +166,7 @@ public function buildParserConfiguration() * @return void * @throws \Neos\Flow\Mvc\Exception */ - public function setOption($optionName, $value) + public function setOption(string $optionName, $value): void { if ($this->templatePaths instanceof TemplatePaths) { $this->templatePaths->setOption($optionName, $value); diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractConditionViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractConditionViewHelper.php index 052ef09e98..9d37dbbab5 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractConditionViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractConditionViewHelper.php @@ -85,21 +85,15 @@ protected static function evaluateCondition($arguments, RenderingContextInterfac public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) { if (static::evaluateCondition($arguments, $renderingContext)) { - if (isset($arguments['then'])) { - return $arguments['then']; - } - if (isset($arguments['__thenClosure'])) { - return $arguments['__thenClosure'](); - } - } elseif (!empty($arguments['__elseClosures'])) { - $elseIfClosures = isset($arguments['__elseifClosures']) ? $arguments['__elseifClosures'] : []; + return ($arguments['then'] ?? $arguments['__thenClosure']) ?? ''; + } + if (!empty($arguments['__elseClosures'])) { + $elseIfClosures = $arguments['__elseifClosures'] ?? []; return static::evaluateElseClosures($arguments['__elseClosures'], $elseIfClosures, $renderingContext); - } elseif (array_key_exists('else', $arguments)) { - return $arguments['else']; } - return ''; + return $arguments['else'] ?? ''; } /** @@ -113,10 +107,10 @@ protected static function evaluateElseClosures(array $closures, array $condition foreach ($closures as $elseNodeIndex => $elseNodeClosure) { if (!isset($conditionClosures[$elseNodeIndex])) { return $elseNodeClosure(); - } else { - if ($conditionClosures[$elseNodeIndex]()) { - return $elseNodeClosure(); - } + } + + if ($conditionClosures[$elseNodeIndex]()) { + return $elseNodeClosure(); } } diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractLocaleAwareViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractLocaleAwareViewHelper.php index 5f33d1e651..884bbb80d3 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractLocaleAwareViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractLocaleAwareViewHelper.php @@ -50,7 +50,7 @@ public function injectLocalizationService(I18n\Service $localizationService) * @throws InvalidVariableException * @return I18n\Locale|null The locale to use or NULL if locale should not be used */ - protected function getLocale() + protected function getLocale(): ?I18n\Locale { if (!$this->hasArgument('forceLocale')) { return null; diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractTagBasedViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractTagBasedViewHelper.php index d4743808a6..9d0f2396ce 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractTagBasedViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractTagBasedViewHelper.php @@ -34,7 +34,7 @@ abstract class AbstractTagBasedViewHelper extends AbstractViewHelper * * @var array */ - private static $tagAttributes = []; + private static array $tagAttributes = []; /** * Tag builder instance @@ -42,7 +42,7 @@ abstract class AbstractTagBasedViewHelper extends AbstractViewHelper * @var TagBuilder * @api */ - protected $tag = null; + protected TagBuilder $tag; /** * Name of the tag to be created by this view helper @@ -68,7 +68,7 @@ public function __construct() * @param TagBuilder $tag * @return void */ - public function injectTagBuilder(TagBuilder $tag) + public function injectTagBuilder(TagBuilder $tag): void { $this->tag = $tag; $this->tag->setTagName($this->tagName); diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php index 6731a63562..3a2f1be68a 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php @@ -28,33 +28,32 @@ abstract class AbstractViewHelper extends FluidAbstractViewHelper { /** - * @var FlowAwareRenderingContextInterface&RenderingContextInterface + * @var FlowAwareRenderingContextInterface|RenderingContextInterface */ protected $renderingContext; /** * Controller Context to use * - * @var ControllerContext - * @api + * @var ControllerContext|null */ - protected $controllerContext; + protected ControllerContext|null $controllerContext = null; /** - * @var ObjectManagerInterface + * @var ObjectManagerInterface|null */ - protected $objectManager; + protected ObjectManagerInterface|null $objectManager = null; /** - * @var LoggerInterface + * @var LoggerInterface|null */ - protected $logger; + protected LoggerInterface|null $logger; /** * @param FlowAwareRenderingContextInterface&RenderingContextInterface $renderingContext * @return void */ - public function setRenderingContext(RenderingContextInterface $renderingContext) + public function setRenderingContext(RenderingContextInterface $renderingContext): void { $this->renderingContext = $renderingContext; $this->templateVariableContainer = $renderingContext->getVariableProvider(); diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/Exception/RenderingContextNotAccessibleException.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/Exception/RenderingContextNotAccessibleException.php deleted file mode 100644 index e20c442856..0000000000 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/Exception/RenderingContextNotAccessibleException.php +++ /dev/null @@ -1,23 +0,0 @@ - true, + (in_array($normalizedPath, ['false', 'off', 'no'])) => false, + default => null + }; } } diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php index 20337b6807..4a486fa313 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php @@ -60,7 +60,7 @@ class ViewHelperResolver extends \TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperRes */ protected $namespacesFromConfiguration; - public function initializeObject($reason) + public function initializeObject($reason): void { if ($reason === ObjectManagerInterface::INITIALIZATIONCAUSE_RECREATED) { return; @@ -70,7 +70,7 @@ public function initializeObject($reason) foreach ($this->packageManager->getAvailablePackages() as $package) { foreach ($package->getNamespaces() as $namespace) { $viewHelperNamespace = $namespace; - if (strpos(strrev($namespace), '\\') !== 0) { + if (str_ends_with($namespace, '\\') === false) { $viewHelperNamespace .= '\\'; } $viewHelperNamespace .= 'ViewHelpers'; @@ -83,75 +83,13 @@ public function initializeObject($reason) } } - /** - * @param string $viewHelperClassName - * @return \TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface - */ public function createViewHelperInstanceFromClassName(string $viewHelperClassName): ViewHelperInterface { - return $this->objectManager->get($viewHelperClassName); - } - - /** - * Add a PHP namespace where ViewHelpers can be found and give - * it an alias/identifier. - * - * The provided namespace can be either a single namespace or - * an array of namespaces, as strings. The identifier/alias is - * always a single, alpha-numeric ASCII string. - * - * Calling this method multiple times with different PHP namespaces - * for the same alias causes that namespace to be *extended*, - * meaning that the PHP namespace you provide second, third etc. - * are also used in lookups and are used *first*, so that if any - * of the namespaces you add contains a class placed and named the - * same way as one that exists in an earlier namespace, then your - * class gets used instead of the earlier one. - * - * Example: - * - * $resolver->addNamespace('my', 'My\Package\ViewHelpers'); - * // Any ViewHelpers under this namespace can now be accessed using for example {my:example()} - * // Now, assuming you also have an ExampleViewHelper class in a different - * // namespace and wish to make that ExampleViewHelper override the other: - * $resolver->addNamespace('my', 'My\OtherPackage\ViewHelpers'); - * // Now, since ExampleViewHelper exists in both places but the - * // My\OtherPackage\ViewHelpers namespace was added *last*, Fluid - * // will find and use My\OtherPackage\ViewHelpers\ExampleViewHelper. - * - * Alternatively, setNamespaces() can be used to reset and redefine - * all previously added namespaces - which is great for cases where - * you need to remove or replace previously added namespaces. Be aware - * that setNamespaces() also removes the default "f" namespace, so - * when you use this method you should always include the "f" namespace. - */ - public function addNamespace(string $identifier, array|string|null $phpNamespace): void - { - if ($phpNamespace === null) { - $this->namespaces[$identifier] = null; - return; - } - - if (!is_array($phpNamespace)) { - $this->addNamespaceInternal($identifier, $phpNamespace); - return; - } - - foreach ($phpNamespace as $namespace) { - $this->addNamespaceInternal($identifier, $namespace); - } - } - - /** - * @param string $identifier - * @param string $phpNamespace - */ - protected function addNamespaceInternal($identifier, $phpNamespace) - { - if (!isset($this->namespaces[$identifier])) { - $this->namespaces[$identifier] = []; + $possibleViewHelper = $this->objectManager->get($viewHelperClassName); + if ($possibleViewHelper instanceof ViewHelperInterface) { + return $possibleViewHelper; } - $this->namespaces[$identifier] = array_unique(array_merge($this->namespaces[$identifier], [$phpNamespace])); + throw new \RuntimeException('Given ViewHelper class "' . $viewHelperClassName . '" does not implement ViewHelperInterface'); } } From a76d09963c5f696d925eb0db1e38a199075c2905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Mon, 18 Aug 2025 12:37:58 +0200 Subject: [PATCH 3/4] Stay PHP 8.2 compatible --- .../Classes/Core/Parser/Interceptor/ResourceInterceptor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php b/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php index 761f362142..65c9a7978e 100644 --- a/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php +++ b/Neos.FluidAdaptor/Classes/Core/Parser/Interceptor/ResourceInterceptor.php @@ -42,7 +42,7 @@ class ResourceInterceptor implements InterceptorInterface * * @var string */ - private const string PATTERN_SPLIT_AT_RESOURCE_URIS = '! + private const PATTERN_SPLIT_AT_RESOURCE_URIS = '! ( (?:[^"\'(\s]+/ # URL part: A string with no quotes, no opening parentheses and no whitespace )* # a URL consists of multiple URL parts @@ -57,7 +57,7 @@ class ResourceInterceptor implements InterceptorInterface * @var string * @see \Neos\Flow\Package\FlowPackageKey::PATTERN */ - private const string PATTERN_MATCH_RESOURCE_URI = '!(?:../)*(?:(?P[A-Za-z0-9]+\.(?:[A-Za-z0-9][\.a-z0-9]*)+)/Resources/)?Public/(?P[^"]+)!'; + private const PATTERN_MATCH_RESOURCE_URI = '!(?:../)*(?:(?P[A-Za-z0-9]+\.(?:[A-Za-z0-9][\.a-z0-9]*)+)/Resources/)?Public/(?P[^"]+)!'; /** * The default package key to use when rendering resource links without a From ea9a9a841825f381197e5560256024aa9ab6c24a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Mon, 18 Aug 2025 13:13:21 +0200 Subject: [PATCH 4/4] Resolve phpstan issues --- .../Classes/Core/ViewHelper/AbstractViewHelper.php | 2 +- .../Classes/Core/ViewHelper/ViewHelperResolver.php | 2 +- Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php index 3a2f1be68a..fe6634f302 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/AbstractViewHelper.php @@ -28,7 +28,7 @@ abstract class AbstractViewHelper extends FluidAbstractViewHelper { /** - * @var FlowAwareRenderingContextInterface|RenderingContextInterface + * @var FlowAwareRenderingContextInterface&RenderingContextInterface */ protected $renderingContext; diff --git a/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php b/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php index 4a486fa313..9cd2f16033 100644 --- a/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php +++ b/Neos.FluidAdaptor/Classes/Core/ViewHelper/ViewHelperResolver.php @@ -50,7 +50,7 @@ class ViewHelperResolver extends \TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperRes * will look for classes in both namespaces starting * from the bottom. * - * @var array + * @var array */ protected array $namespaces = []; diff --git a/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php b/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php index e56621724d..280df36ee1 100644 --- a/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php +++ b/Neos.FluidAdaptor/Classes/View/AbstractTemplateView.php @@ -110,9 +110,6 @@ abstract class AbstractTemplateView extends \TYPO3Fluid\Fluid\View\AbstractTempl */ protected $controllerContext; - /** - * @phpstan-ignore-next-line we are incompatible with the fluid view and should use composition instead - */ public function render($actionName = null): StreamInterface { return $this->createStream(parent::render($actionName));