Skip to content

Commit

Permalink
[FEATURE] Add MigrateViewHelperRenderStaticRector
Browse files Browse the repository at this point in the history
This rector rule covers the majority of use cases where the now
deprecated `renderStatic()` is used in Fluid ViewHelper classes.
It's the same rule that has been used in the TYPO3 Core patch that
migrated all existing ViewHelpers to `render()`.
There were some edge cases in the TYPO3 Core that weren't covered,
but those are rather old and don't follow recent best practices.

Core Patch: https://review.typo3.org/c/Packages/TYPO3.CMS/+/85834
  • Loading branch information
s2b authored and simonschaufi committed Sep 27, 2024
1 parent 10a7683 commit 60a4ce1
Show file tree
Hide file tree
Showing 16 changed files with 754 additions and 0 deletions.
1 change: 1 addition & 0 deletions config/typo3-13.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
$rectorConfig->import(__DIR__ . '/v13/typo3-130.php');
$rectorConfig->import(__DIR__ . '/v13/typo3-130-extbase-hash-service-core-hash-service.php');
$rectorConfig->import(__DIR__ . '/v13/typo3-131.php');
$rectorConfig->import(__DIR__ . '/v13/typo3-133.php');
};
11 changes: 11 additions & 0 deletions config/v13/typo3-133.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Ssch\TYPO3Rector\TYPO313\v3\MigrateViewHelperRenderStaticRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->import(__DIR__ . '/../config.php');
$rectorConfig->rule(MigrateViewHelperRenderStaticRector::class);
};
235 changes: 235 additions & 0 deletions rules/TYPO313/v3/MigrateViewHelperRenderStaticRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<?php

declare(strict_types=1);

namespace Ssch\TYPO3Rector\TYPO313\v3;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\PropertyProperty;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\TraitUse;
use PhpParser\Node\VariadicPlaceholder;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;

/**
* @changelog https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/13.3/Deprecation-104789-RenderStaticForFluidViewHelpers.html
* @see \Ssch\TYPO3Rector\Tests\Rector\v13\v3\MigrateViewHelperRenderStaticRector\MigrateViewHelperRenderStaticRectorTest
*/
final class MigrateViewHelperRenderStaticRector extends AbstractRector
{
/**
* This method helps other to understand the rule
* and to generate documentation.
*/
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Migrate static ViewHelpers to object-based ViewHelpers',
[
new CodeSample(
<<<'CODE_SAMPLE'
class MyViewHelper extends AbstractViewHelper
{
use CompileWithRenderStatic;
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
{
return $renderChildrenClosure();
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class MyViewHelper extends AbstractViewHelper
{
public function render(): string
{
return $this->renderChildren();
}
}
CODE_SAMPLE
),
]
);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $classNode
*/
public function refactor(Node $classNode): ?Node
{
// Only refactor ViewHelper classes that implement RenderStatic traits with
// explicit definition of the contentArgumentName
if (! $this->classCanBeMigrated($classNode)) {
return null;
}

$staticMethodNode = $classNode->getMethod('renderStatic');
if ($staticMethodNode === null) {
return null;
}

// Replacements for renderStatic() method arguments
$argumentsParamName = $this->nodeNameResolver->getName($staticMethodNode->params[0]->var);
$renderClosureParamName = $this->nodeNameResolver->getName($staticMethodNode->params[1]->var);
$renderingContextParamName = $this->nodeNameResolver->getName($staticMethodNode->params[2]->var);

// Replace local variables in render function with object properties
$this->traverseNodesWithCallable(
$staticMethodNode->stmts ?? [],
function (Node $node) use (
$argumentsParamName,
$renderClosureParamName,
$renderingContextParamName
): ?Node {
$renderingContextReplacement = new PropertyFetch(new Variable('this'), new Identifier(
'renderingContext'
));
$childrenClosureCallReplacement = new MethodCall(new Variable('this'), new Identifier(
'renderChildren'
));
$childrenClosureReplacement = new MethodCall(new Variable('this'), new Identifier('renderChildren'), [
new VariadicPlaceholder(),
]);
$argumentsReplacement = new PropertyFetch(new Variable('this'), new Identifier('arguments'));
// If the renderChildren closure is called directly, the whole function call needs to be replaced
if (
$node instanceof FuncCall
&& $node->name instanceof Variable
&& $this->nodeNameResolver->getName($node->name) === $renderClosureParamName
) {
return $childrenClosureCallReplacement;
}

// Replace usages of variables
if ($node instanceof Variable) {
switch ($this->nodeNameResolver->getName($node)) {
case $argumentsParamName:
return $argumentsReplacement;

case $renderingContextParamName:
return $renderingContextReplacement;

case $renderClosureParamName:
return $childrenClosureReplacement;
}
}

return null;
}
);

// Rename method and make it non-static
$staticMethodNode->params = [];
$staticMethodNode->name = new Identifier('render');
$staticMethodNode->flags ^= Class_::MODIFIER_STATIC;

// Use new API to set content argument
$resolveContentArgumentNameMethod = $classNode->getMethod('resolveContentArgumentName');
$contentArgumentNameProperty = $classNode->getProperty('contentArgumentName');
if ($resolveContentArgumentNameMethod instanceof ClassMethod) {
// Rename content argument method
$resolveContentArgumentNameMethod->name = new Identifier('getContentArgumentName');
} elseif ($contentArgumentNameProperty instanceof Property) {
// Remove property and extract its default value
$defaultValueExpression = null;
foreach ($classNode->stmts as $stmtKey => $stmt) {
if (! $stmt instanceof Property) {
continue;
}

foreach ($stmt->props as $propKey => $prop) {
if ($prop instanceof PropertyProperty && $prop->name->toString() === 'contentArgumentName') {
$defaultValueExpression = $prop->default;
unset($stmt->props[$propKey]);
}
}

if ($stmt->props === []) {
unset($classNode->stmts[$stmtKey]);
}
}

$getContentArgumentNameMethod = new ClassMethod('getContentArgumentName');
$getContentArgumentNameMethod->flags = Class_::MODIFIER_PUBLIC;
$getContentArgumentNameMethod->stmts[] = new Return_($defaultValueExpression);
$getContentArgumentNameMethod->returnType = new Identifier('string');

$classNode->stmts[] = $getContentArgumentNameMethod;
}

// Remove traits
foreach ($classNode->stmts as $stmtKey => $stmt) {
if (! $stmt instanceof TraitUse) {
continue;
}

foreach ($stmt->traits as $traitKey => $trait) {
if ($this->isNames(
$trait,
[CompileWithRenderStatic::class, CompileWithContentArgumentAndRenderStatic::class]
)) {
unset($stmt->traits[$traitKey]);
}
}

if ($stmt->traits === []) {
unset($classNode->stmts[$stmtKey]);
}
}

return $classNode;
}

private function classCanBeMigrated(Class_ $classNode): bool
{
// Skip ViewHelpers without renderStatic() method (this shouldn't happen)
$staticMethodNode = $classNode->getMethod('renderStatic');
if (! $staticMethodNode instanceof ClassMethod || ! $staticMethodNode->isStatic()) {
return false;
}

foreach ($classNode->getTraitUses() as $traitUse) {
foreach ($traitUse->traits as $trait) {
// Skip ViewHelpers where content argument is determined automatically
if ($this->isName($trait, CompileWithContentArgumentAndRenderStatic::class)) {
$contentArgumentNameProperty = $classNode->getProperty('contentArgumentName');
if ($contentArgumentNameProperty && $contentArgumentNameProperty->props !== []) {
return true;
}

$resolveContentArgumentNameMethod = $classNode->getMethod('resolveContentArgumentName');
if ($resolveContentArgumentNameMethod) {
return true;
}
}

if ($this->isName($trait, CompileWithRenderStatic::class)) {
return true;
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace TYPO3Fluid\Fluid\Core\ViewHelper\Traits;

if (trait_exists('TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic')) {
return;
}

trait CompileWithContentArgumentAndRenderStatic
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace TYPO3Fluid\Fluid\Core\ViewHelper\Traits;

if (trait_exists('TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic')) {
return;
}

trait CompileWithRenderStatic
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Ssch\TYPO3Rector\Tests\Rector\v13\v3\MigrateViewHelperRenderStaticRector\Fixture;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;

class CallRenderChildrenClosureViewHelper extends AbstractViewHelper
{
use CompileWithRenderStatic;

public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
{
return $renderChildrenClosure();
}
}

?>
-----
<?php

namespace Ssch\TYPO3Rector\Tests\Rector\v13\v3\MigrateViewHelperRenderStaticRector\Fixture;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;

class CallRenderChildrenClosureViewHelper extends AbstractViewHelper
{
public function render(): string
{
return $this->renderChildren();
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Ssch\TYPO3Rector\Tests\Rector\v13\v3\MigrateViewHelperRenderStaticRector\Fixture;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;

class ContentArgumentNamePropertyViewHelper extends AbstractViewHelper
{
use CompileWithContentArgumentAndRenderStatic;

protected $contentArgumentName = 'value';

public function initializeArguments(): void
{
$this->registerArgument('value', 'string', 'a value');
}

public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
{
return $renderChildrenClosure();
}
}

?>
-----
<?php

namespace Ssch\TYPO3Rector\Tests\Rector\v13\v3\MigrateViewHelperRenderStaticRector\Fixture;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;

class ContentArgumentNamePropertyViewHelper extends AbstractViewHelper
{
public function initializeArguments(): void
{
$this->registerArgument('value', 'string', 'a value');
}

public function render(): string
{
return $this->renderChildren();
}
public function getContentArgumentName(): string
{
return 'value';
}
}

?>
Loading

0 comments on commit 60a4ce1

Please sign in to comment.