Skip to content

Commit

Permalink
[FEATURE] Add MigrateRequiredFlagSiteConfigRector
Browse files Browse the repository at this point in the history
Resolves: #2944
Releases: 3, 2
  • Loading branch information
simonschaufi committed Dec 20, 2024
1 parent c3ec82f commit 9269cde
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 199 deletions.
128 changes: 128 additions & 0 deletions rules/TYPO312/v0/MigrateRequiredFlagSiteConfigRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

namespace Ssch\TYPO3Rector\TYPO312\v0;

use PhpParser\Node;
use PhpParser\Node\ArrayItem;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use Ssch\TYPO3Rector\Helper\ArrayUtility;
use Ssch\TYPO3Rector\Helper\StringUtility;
use Ssch\TYPO3Rector\Rector\AbstractArrayDimFetchTcaRector;
use Symplify\RuleDocGenerator\Contract\DocumentedRuleInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @changelog https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/12.0/Deprecation-97035-RequiredOptionInEvalKeyword.html
* @changelog https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/12.0/Feature-97035-UtilizeRequiredDirectlyInTCAFieldConfiguration.html
* @see \Ssch\TYPO3Rector\Tests\Rector\v12\v0\MigrateRequiredFlagSiteConfigRector\MigrateRequiredFlagSiteConfigRectorTest
*/
final class MigrateRequiredFlagSiteConfigRector extends AbstractArrayDimFetchTcaRector implements DocumentedRuleInterface
{
/**
* @var string
*/
private const REQUIRED = 'required';

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Migrate required flag', [new CodeSample(
<<<'CODE_SAMPLE'
$GLOBALS['SiteConfiguration']['site']['columns']['required_column1'] = [
'required_column' => [
'config' => [
'eval' => 'trim,required',
],
],
];
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$GLOBALS['SiteConfiguration']['site']['columns']['required_column1'] = [
'required_column' => [
'config' => [
'eval' => 'trim',
'required' = true,
],
],
];
CODE_SAMPLE
)]);
}

/**
* @param Assign $node
*/
public function refactor(Node $node): ?Node
{
$columnName = $node->var;
if (! $columnName instanceof ArrayDimFetch) {
return null;
}

if (! $columnName->dim instanceof String_ && ! $columnName->dim instanceof Variable) {
return null;
}

$rootLine = ['SiteConfiguration', 'site', 'columns'];
$result = $this->isInRootLine($columnName, $rootLine);
if (! $result) {
return null;
}

$columnTca = $node->expr;

$configArray = $this->extractSubArrayByKey($columnTca, self::CONFIG);
if (! $configArray instanceof Array_) {
return null;
}

if (! $this->hasKey($configArray, 'eval')) {
return null;
}

$evalArrayItem = $this->extractArrayItemByKey($configArray, 'eval');
if (! $evalArrayItem instanceof ArrayItem) {
return null;
}

$value = $this->valueResolver->getValue($evalArrayItem->value);
if (! is_string($value)) {
return null;
}

if (! StringUtility::inList($value, self::REQUIRED)) {
return null;
}

$evalList = ArrayUtility::trimExplode(',', $value, true);

// Remove "required" from $evalList
$evalList = array_filter($evalList, static fn (string $eval) => $eval !== self::REQUIRED);

if ($evalList !== []) {
// Write back filtered 'eval'
$evalArrayItem->value = new String_(implode(',', $evalList));
} else {
$this->removeArrayItemFromArrayByKey($configArray, 'eval');
}

// If required config exists already do not add one again
$requiredItemToRemove = $this->extractArrayItemByKey($configArray, self::REQUIRED);
if ($requiredItemToRemove instanceof ArrayItem) {
return null;
}

$configArray->items[] = new ArrayItem(new ConstFetch(new Name('true')), new String_(self::REQUIRED));

return $node;
}
}
213 changes: 213 additions & 0 deletions src/Helper/TcaHelperTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<?php

declare(strict_types=1);

namespace Ssch\TYPO3Rector\Helper;

use PhpParser\Node;
use PhpParser\Node\ArrayItem;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Scalar\String_;

trait TcaHelperTrait
{
protected function isConfigType(Array_ $columnItemConfigurationArray, string $type): bool
{
return $this->hasKeyValuePair($columnItemConfigurationArray, 'type', $type);
}

protected function configIsOfRenderType(Array_ $configValueArray, string $expectedRenderType): bool
{
return $this->hasKeyValuePair($configValueArray, 'renderType', $expectedRenderType);
}

protected function changeTcaType(Array_ $configArray, string $type): void
{
$toChangeArrayItem = $this->extractArrayItemByKey($configArray, 'type');
if ($toChangeArrayItem instanceof ArrayItem) {
$toChangeArrayItem->value = new String_($type);
}
}

protected function hasRenderType(Array_ $columnItemConfigurationArray): bool
{
$renderTypeItem = $this->extractArrayItemByKey($columnItemConfigurationArray, 'renderType');
return $renderTypeItem instanceof ArrayItem;
}

protected function hasInternalType(Array_ $columnItemConfigurationArray): bool
{
$internalType = $this->extractArrayItemByKey($columnItemConfigurationArray, 'internal_type');
return $internalType instanceof ArrayItem;
}

protected function configIsOfInternalType(Array_ $configValueArray, string $expectedType): bool
{
return $this->hasKeyValuePair($configValueArray, 'internal_type', $expectedType);
}

/**
* @param string|int $key
*/
protected function extractArrayValueByKey(?Node $node, $key): ?Expr
{
return (($extractArrayItemByKey = $this->extractArrayItemByKey(
$node,
$key
)) instanceof ArrayItem) ? $extractArrayItemByKey->value : null;
}

/**
* @param string|int $key
*/
protected function extractSubArrayByKey(?Node $node, $key): ?Array_
{
if (! $node instanceof Node) {
return null;
}

$arrayItem = $this->extractArrayItemByKey($node, $key);
if (! $arrayItem instanceof ArrayItem) {
return null;
}

$columnItems = $arrayItem->value;
if (! $columnItems instanceof Array_) {
return null;
}

return $columnItems;
}

/**
* @param string|int $key
*/
protected function extractArrayItemByKey(?Node $node, $key): ?ArrayItem
{
if (! $node instanceof Node) {
return null;
}

if (! $node instanceof Array_) {
return null;
}

foreach ($node->items as $item) {
if (! $item instanceof ArrayItem) {
continue;
}

if (! $item->key instanceof Expr) {
continue;
}

$itemKey = $this->getValue($item->key);
if ($key === $itemKey) {
return $item;
}
}

return null;
}

/**
* Removes an array key directly from the first level of an array.
*
* ```
* $this->removeArrayItemFromArrayByKey($configArray, 'myKeyToBeRemoved');
* ```
*
* If the key to be removed is in a sub array of the current one
* use `extractSubArrayByKey` to extract the sub array first:
*
* ```
* $appearanceArray = $this->extractSubArrayByKey($configArray, 'appearance');
* if (! $appearanceArray instanceof Array_) {
* return;
* }
* $this->removeArrayItemFromArrayByKey($appearanceArray, 'showRemovedLocalizationRecords');
* ```
*
* Attention: Strict comparison is used for the key. key with int 42 will
* not remove string "42"!
*
* @param string|int $key
*/
protected function removeArrayItemFromArrayByKey(Array_ $array, $key): void
{
$arrayItemToRemove = $this->extractArrayItemByKey($array, $key);
if (! $arrayItemToRemove instanceof ArrayItem) {
return;
}

foreach ($array->items as $arrayItemKey => $arrayItem) {
if ($arrayItem === $arrayItemToRemove) {
unset($array->items[$arrayItemKey]);
$this->hasAstBeenChanged = true;
}
}
}

/**
* @param string|int $configKey
*/
protected function hasKey(Array_ $configValuesArray, $configKey): bool
{
foreach ($configValuesArray->items as $configItemValue) {
if (! $configItemValue instanceof ArrayItem) {
continue;
}

if (! $configItemValue->key instanceof Expr) {
continue;
}

if ($this->isValue($configItemValue->key, $configKey)) {
return true;
}
}

return false;
}

/**
* @param mixed $expectedValue
*/
protected function hasKeyValuePair(Array_ $configValueArray, string $configKey, $expectedValue): bool
{
foreach ($configValueArray->items as $configItemValue) {
if (! $configItemValue instanceof ArrayItem) {
continue;
}

if (! $configItemValue->key instanceof Expr) {
continue;
}

if ($this->isValue($configItemValue->key, $configKey)
&& $this->isValue($configItemValue->value, $expectedValue)
) {
return true;
}
}

return false;
}

/**
* @param mixed $value
*/
private function isValue(Expr $expr, $value): bool
{
return $this->valueResolver->isValue($expr, $value);
}

/**
* @return mixed|null
*/
private function getValue(Expr $expr)
{
return $this->valueResolver->getValue($expr);
}
}
7 changes: 7 additions & 0 deletions src/Rector/AbstractArrayDimFetchTcaRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@
use PhpParser\Node\Scalar\String_;
use Rector\PhpParser\Node\Value\ValueResolver;
use Rector\Rector\AbstractRector;
use Ssch\TYPO3Rector\Helper\TcaHelperTrait;

/**
* Base rector that detects Assignments containing TCA definitions and allows to refactor them
*/
abstract class AbstractArrayDimFetchTcaRector extends AbstractRector
{
use TcaHelperTrait;

protected const CONFIG = 'config';

protected bool $hasAstBeenChanged = false;

protected ValueResolver $valueResolver;

public function __construct(ValueResolver $valueResolver)
Expand Down
Loading

0 comments on commit 9269cde

Please sign in to comment.