Skip to content

Commit

Permalink
[FEATURE] Add MigratePluginContentElementAndPluginSubtypesRector
Browse files Browse the repository at this point in the history
Closes: #4323
  • Loading branch information
simonschaufi committed Nov 11, 2024
1 parent 2f5ab4b commit 4eb57db
Show file tree
Hide file tree
Showing 15 changed files with 553 additions and 1 deletion.
1 change: 1 addition & 0 deletions config/typo3-13.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
$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');
$rectorConfig->import(__DIR__ . '/v13/typo3-134.php');
};
11 changes: 11 additions & 0 deletions config/v13/typo3-134.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\v4\MigratePluginContentElementAndPluginSubtypesRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->import(__DIR__ . '/../config.php');
$rectorConfig->rule(MigratePluginContentElementAndPluginSubtypesRector::class);
};
4 changes: 4 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\PostRector\Rector\UnusedImportRemovingPostRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;

Expand All @@ -31,6 +32,9 @@
__DIR__ . '/rules/TYPO312/v0/MigrateFetchAllToFetchAllAssociativeRector.php', // Don't replace Doctrine Constants
__DIR__ . '/rules/TYPO312/v0/MigrateFetchToFetchAssociativeRector.php', // Don't replace Doctrine Constants
],
UnusedImportRemovingPostRector::class => [
__DIR__ . '/tests/Rector/v13/v4/MigratePluginContentElementAndPluginSubtypesRector/Assertions/extension1/Classes/Updates/TYPO3RectorCTypeMigration.php', // Don't remove PHP8 Attribute
],
// tests
'*/Fixture/*',
'*/Fixture*',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?php

declare(strict_types=1);

namespace Ssch\TYPO3Rector\TYPO313\v4;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
use PHPStan\Type\ObjectType;
use Rector\PhpParser\Node\Value\ValueResolver;
use Rector\Rector\AbstractRector;
use Ssch\TYPO3Rector\ComposerPsr4Resolver;
use Ssch\TYPO3Rector\Contract\FilesystemInterface;
use Ssch\TYPO3Rector\Filesystem\FilesFinder;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @changelog https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/13.4/Deprecation-105076-PluginContentElementAndPluginSubTypes.html
* @see \Ssch\TYPO3Rector\Tests\Rector\v13\v4\MigratePluginContentElementAndPluginSubtypesRector\MigratePluginContentElementAndPluginSubtypesRectorTest
*/
final class MigratePluginContentElementAndPluginSubtypesRector extends AbstractRector
{
/**
* @readonly
*/
private FilesFinder $filesFinder;

/**
* @readonly
*/
private FilesystemInterface $filesystem;

/**
* @readonly
*/
private ValueResolver $valueResolver;

/**
* @readonly
*/
private ComposerPsr4Resolver $composerPsr4Resolver;

public function __construct(
FilesFinder $filesFinder,
FilesystemInterface $filesystem,
ValueResolver $valueResolver,
ComposerPsr4Resolver $composerPsr4Resolver
) {
$this->filesFinder = $filesFinder;
$this->filesystem = $filesystem;
$this->valueResolver = $valueResolver;
$this->composerPsr4Resolver = $composerPsr4Resolver;
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Migrate plugin content element and plugin subtypes (list_type)', [new CodeSample(
<<<'CODE_SAMPLE'
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin([], 'list_type', 'extension_key');
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin('ExtensionName', 'PluginName', [], [], 'list_type');
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin([], 'CType', 'extension_key');
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin('ExtensionName', 'PluginName', [], [], 'CType');
CODE_SAMPLE
)]);
}

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

/**
* @param StaticCall $node
*/
public function refactor(Node $node): ?Node
{
if ($this->shouldSkip($node)) {
return null;
}

$type = 'list_type';
if ($this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType(
$node,
new ObjectType('TYPO3\CMS\Core\Utility\ExtensionManagementUtility')
)) {
if (isset($node->args[1])) {
$type = $this->valueResolver->getValue($node->args[1]->value);
}

if ($type === 'list_type') {
$node->args[1] = new Arg(new ClassConstFetch(new FullyQualified(
'TYPO3\CMS\Extbase\Utility\ExtensionUtility'
), new Identifier('PLUGIN_TYPE_CONTENT_ELEMENT')));
}
} else {
if (isset($node->args[4])) {
$type = $this->valueResolver->getValue($node->args[4]->value);
} elseif (! isset($node->args[3])) {
$node->args[3] = new Arg(new Array_());
}

if ($type === 'list_type') {
$node->args[4] = new Arg(new ClassConstFetch(new FullyQualified(
'TYPO3\CMS\Extbase\Utility\ExtensionUtility'
), new Identifier('PLUGIN_TYPE_CONTENT_ELEMENT')));
}
}

$psr4 = $this->composerPsr4Resolver->resolve($this->file);
if ($psr4 !== null) {
$filePath = $this->file->getFilePath();
$directoryName = $this->filesFinder->isInTCAOverridesFolder($filePath)
? dirname($filePath, 4)
: dirname($filePath);

$namespaceParts = explode('\\', $psr4);
$vendor = $namespaceParts[0];
$lowerCasedVendor = mb_strtolower($vendor);
$extensionName = $namespaceParts[1];

$migrationFile = $directoryName . '/Classes/Updates/' . $vendor . $extensionName . 'CTypeMigration.php';
if (! $this->filesystem->fileExists($migrationFile)) {
$content = <<<CODE
<?php
declare(strict_types=1);
namespace {$psr4}Updates;
use TYPO3\CMS\Install\Attribute\UpgradeWizard;
use TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate;
#[UpgradeWizard('{$lowerCasedVendor}{$extensionName}CTypeMigration')]
final class {$vendor}{$extensionName}CTypeMigration extends AbstractListTypeToCTypeUpdate
{
public function getTitle(): string
{
return 'Migrate "{$vendor} {$extensionName}" plugins to content elements.';
}
public function getDescription(): string
{
return 'The "{$vendor} {$extensionName}" plugins are now registered as content element. Update migrates existing records and backend user permissions.';
}
/**
* This must return an array containing the "list_type" to "CType" mapping
*
* Example:
*
* [
* 'pi_plugin1' => 'pi_plugin1',
* 'pi_plugin2' => 'new_content_element',
* ]
*
* @return array<string, string>
*/
protected function getListTypeToCTypeMapping(): array
{
return [
// TODO: Add this mapping yourself!
];
}
}
CODE;
$this->filesystem->write($migrationFile, $content);
}
}

return $node;
}

private function shouldSkip(StaticCall $staticCall): bool
{
if (! $this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType(
$staticCall,
new ObjectType('TYPO3\CMS\Core\Utility\ExtensionManagementUtility')
) && ! $this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType(
$staticCall,
new ObjectType('TYPO3\CMS\Extbase\Utility\ExtensionUtility')
)) {
return true;
}

return ! $this->isNames($staticCall->name, ['addPlugin', 'configurePlugin']);
}
}
2 changes: 1 addition & 1 deletion src/ComposerExtensionKeyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function resolveExtensionKey(File $file): ?string

$composerJsonContent = $this->filesystem->read($composerJsonFile);

$composerJsonContentDecoded = json_decode($composerJsonContent, true);
$composerJsonContentDecoded = json_decode($composerJsonContent, true, 512, JSON_THROW_ON_ERROR);

return $composerJsonContentDecoded['extra']['typo3/cms']['extension-key'] ?? null;
}
Expand Down
61 changes: 61 additions & 0 deletions src/ComposerPsr4Resolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Ssch\TYPO3Rector;

use Rector\ValueObject\Application\File;
use Ssch\TYPO3Rector\Contract\FilesystemInterface;
use Ssch\TYPO3Rector\Filesystem\FilesFinder;

final class ComposerPsr4Resolver
{
/**
* @readonly
*/
private FilesystemInterface $filesystem;

/**
* @readonly
*/
private FilesFinder $filesFinder;

public function __construct(FilesystemInterface $filesystem, FilesFinder $filesFinder)
{
$this->filesystem = $filesystem;
$this->filesFinder = $filesFinder;
}

public function resolve(File $file): ?string
{
$filePath = $file->getFilePath();
if (! $this->filesFinder->isExtLocalConf($filePath)
&& ! $this->filesFinder->isInTCAOverridesFolder($filePath)
) {
return null;
}

$directoryName = $this->filesFinder->isInTCAOverridesFolder($filePath)
? dirname($filePath, 4)
: dirname($filePath);

$composerJsonFile = $directoryName . '/composer.json';

if (! $this->filesystem->fileExists($composerJsonFile)) {
return null;
}

$composerJsonContent = $this->filesystem->read($composerJsonFile);

$composerJsonContentDecoded = json_decode($composerJsonContent, true, 512, JSON_THROW_ON_ERROR);

$autoLoadings = $composerJsonContentDecoded['autoload']['psr-4'] ?? [];
foreach ($autoLoadings as $namespace => $path) {
if (str_starts_with($path, 'Classes')) {
return $namespace;
}
}

return null;
}
}
5 changes: 5 additions & 0 deletions src/Filesystem/FilesFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public function isExtTables(string $filePath): bool
return $this->fileEqualsName($filePath, 'ext_tables.php');
}

public function isInTCAOverridesFolder(string $filePath): bool
{
return preg_match('#Configuration/TCA/Overrides/#', $filePath) === 1;
}

private function fileEqualsName(string $filePath, string $fileName): bool
{
return basename($filePath) === $fileName;
Expand Down
4 changes: 4 additions & 0 deletions stubs/TYPO3/CMS/Core/Utility/ExtensionManagementUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,8 @@ public static function addPageTSConfig(string $content): void
public static function addUserTSConfig(string $content): void
{
}

public static function addPlugin($itemArray, string $type = 'list_type', ?string $extensionKey = null): void
{
}
}
11 changes: 11 additions & 0 deletions stubs/TYPO3/CMS/Install/Classes/Attribute/UpgradeWizard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace TYPO3\CMS\Install\Attribute;

if (class_exists('TYPO3\CMS\Install\Attribute\UpgradeWizard')) {
return;
}

class UpgradeWizard
{
}
28 changes: 28 additions & 0 deletions stubs/TYPO3/CMS/Install/Updates/AbstractListTypeToCTypeUpdate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace TYPO3\CMS\Install\Updates;

if (class_exists('TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate')) {
return;
}

abstract class AbstractListTypeToCTypeUpdate
{
/**
* This must return an array containing the "list_type" to "CType" mapping
*
* Example:
*
* [
* 'pi_plugin1' => 'pi_plugin1',
* 'pi_plugin2' => 'new_content_element',
* ]
*
* @return array<string, string>
*/
abstract protected function getListTypeToCTypeMapping(): array;

abstract public function getTitle(): string;

abstract public function getDescription(): string;
}
Loading

0 comments on commit 4eb57db

Please sign in to comment.