diff --git a/composer.json b/composer.json
index 46a80ad..11689d1 100644
--- a/composer.json
+++ b/composer.json
@@ -47,6 +47,7 @@
"a9f/fractor-extension-installer": "self.version",
"a9f/fractor-fluid": "self.version",
"a9f/fractor-phpstan-rules": "self.version",
+ "a9f/fractor-rule-generator": "self.version",
"a9f/fractor-typoscript": "self.version",
"a9f/fractor-xml": "self.version",
"a9f/fractor-yaml": "self.version",
@@ -64,6 +65,7 @@
"a9f\\FractorFluid\\": "packages/fractor-fluid/src/",
"a9f\\FractorMonorepo\\": "src/",
"a9f\\FractorPhpStanRules\\": "packages/fractor-phpstan-rules/src/",
+ "a9f\\FractorRuleGenerator\\": "packages/fractor-rule-generator/src/",
"a9f\\FractorTypoScript\\": "packages/fractor-typoscript/src/",
"a9f\\FractorXml\\": "packages/fractor-xml/src/",
"a9f\\FractorYaml\\": "packages/fractor-yaml/src/",
@@ -83,6 +85,7 @@
"a9f\\FractorDocGenerator\\Tests\\": "packages/fractor-doc-generator/tests/",
"a9f\\FractorFluid\\Tests\\": "packages/fractor-fluid/tests/",
"a9f\\FractorPhpStanRules\\Tests\\": "packages/fractor-phpstan-rules/tests/",
+ "a9f\\FractorRuleGenerator\\Tests\\": "packages/fractor-rule-generator/tests/",
"a9f\\FractorTypoScript\\Tests\\": "packages/fractor-typoscript/tests/",
"a9f\\FractorXml\\Tests\\": "packages/fractor-xml/tests/",
"a9f\\FractorYaml\\Tests\\": "packages/fractor-yaml/tests/",
diff --git a/ecs.php b/ecs.php
index 0b932a3..7d75b4b 100644
--- a/ecs.php
+++ b/ecs.php
@@ -31,7 +31,10 @@
YodaStyleFixer::class,
OperatorLinebreakFixer::class,
])
- ->withSkip([__DIR__ . '/packages/extension-installer/generated'])
+ ->withSkip([
+ __DIR__ . '/packages/extension-installer/generated',
+ __DIR__ . '/packages/fractor-rule-generator/templates',
+ ])
->withPreparedSets(psr12: true, common: true, symplify: true, cleanCode: true)
->withPaths([__DIR__ . '/e2e', __DIR__ . '/src', __DIR__ . '/packages'])
->withRootFiles();
diff --git a/packages/fractor-rule-generator/.gitignore b/packages/fractor-rule-generator/.gitignore
new file mode 100644
index 0000000..a89e45e
--- /dev/null
+++ b/packages/fractor-rule-generator/.gitignore
@@ -0,0 +1,3 @@
+/vendor/
+/composer.lock
+.phpunit.cache
\ No newline at end of file
diff --git a/packages/fractor-rule-generator/composer.json b/packages/fractor-rule-generator/composer.json
new file mode 100644
index 0000000..9339629
--- /dev/null
+++ b/packages/fractor-rule-generator/composer.json
@@ -0,0 +1,49 @@
+{
+ "name": "a9f/fractor-rule-generator",
+ "description": "Fractor rule generator",
+ "license": "MIT",
+ "type": "fractor-extension",
+ "authors": [
+ {
+ "name": "Andreas Wolf",
+ "email": "dev@a-w.io",
+ "role": "Lead Developer"
+ }
+ ],
+ "require": {
+ "php": "^8.2",
+ "a9f/fractor": "^0.3",
+ "a9f/fractor-extension-installer": "^0.3",
+ "nette/utils": "^4.0",
+ "symfony/config": "^5.4 || ^6.4 || ^7.0",
+ "symfony/console": "^5.4 || ^6.4 || ^7.0",
+ "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0",
+ "symfony/filesystem": "^5.4 || ^6.4 || ^7.0",
+ "symfony/finder": "^5.4 || ^6.4 || ^7.0",
+ "symplify/rule-doc-generator-contracts": "^11.2",
+ "webmozart/assert": "^1.11"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "autoload": {
+ "psr-4": {
+ "a9f\\FractorRuleGenerator\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "a9f\\FractorRuleGenerator\\Tests\\": "tests/"
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "a9f/fractor-extension-installer": true
+ },
+ "sort-packages": true
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-main": "0.3-dev"
+ }
+ }
+}
diff --git a/packages/fractor-rule-generator/config/application.php b/packages/fractor-rule-generator/config/application.php
new file mode 100644
index 0000000..7f88895
--- /dev/null
+++ b/packages/fractor-rule-generator/config/application.php
@@ -0,0 +1,22 @@
+services();
+ $services->defaults()
+ ->autowire()
+ ->private()
+ ->autoconfigure();
+
+ $services->load('a9f\\FractorRuleGenerator\\', __DIR__ . '/../../fractor-rule-generator/src')
+ ->exclude([__DIR__ . '/../src/ValueObject', __DIR__ . '/../src/**/ValueObject']);
+
+ $services->set(ConsoleOutput::class);
+ $services->alias(OutputInterface::class, ConsoleOutput::class);
+};
diff --git a/packages/fractor-rule-generator/phpunit.xml b/packages/fractor-rule-generator/phpunit.xml
new file mode 100644
index 0000000..30100d0
--- /dev/null
+++ b/packages/fractor-rule-generator/phpunit.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ tests
+
+
+
+
diff --git a/packages/fractor-rule-generator/src/Console/Command/GenerateRuleCommand.php b/packages/fractor-rule-generator/src/Console/Command/GenerateRuleCommand.php
new file mode 100644
index 0000000..cdc059f
--- /dev/null
+++ b/packages/fractor-rule-generator/src/Console/Command/GenerateRuleCommand.php
@@ -0,0 +1,242 @@
+getHelper('question');
+
+ /** @var Typo3Version $typo3Version */
+ $typo3Version = $helper->ask($input, $output, $this->askForTypo3Version());
+ $changelogUrl = $helper->ask($input, $output, $this->askForChangelogUrl());
+ $name = $helper->ask($input, $output, $this->askForName());
+ $description = $helper->ask($input, $output, $this->askForDescription());
+ $type = $helper->ask($input, $output, $this->askForType());
+
+ $recipe = new Typo3FractorRecipe(
+ $typo3Version,
+ $changelogUrl,
+ $name,
+ $description,
+ Typo3FractorTypeFactory::fromString($type)
+ );
+
+ $templateFileInfos = $this->templateFinder->find($recipe->getFractorFixtureFileExtension());
+
+ $templateVariables = [
+ '__MajorPrefixed__' => $recipe->getMajorVersionPrefixed(),
+ '__Major__' => $recipe->getMajorVersion(),
+ '__MinorPrefixed__' => $recipe->getMinorVersionPrefixed(),
+ '__Type__' => $recipe->getFractorTypeFolderName(),
+ '__FixtureFileExtension__' => $recipe->getFractorFixtureFileExtension(),
+ '__Name__' => $recipe->getFractorName(),
+ '__Test_Directory__' => $recipe->getTestDirectory(),
+ '__Changelog_Annotation__' => $recipe->getChangelogAnnotation(),
+ '__Description__' => addslashes($recipe->getDescription()),
+ '__Use__' => $recipe->getUseImports(),
+ '__Traits__' => $recipe->getTraits(),
+ '__ExtendsImplements__' => $recipe->getExtendsImplements(),
+ '__Base_Fractor_Body_Template__' => $recipe->getFractorBodyTemplate(),
+ ];
+
+ $targetDirectory = __DIR__ . '/../../../../typo3-fractor';
+
+ $generatedFilePaths = $this->fileGenerator->generateFiles(
+ $templateFileInfos,
+ $templateVariables,
+ $targetDirectory
+ );
+
+ $this->configFilesystem->addRuleToConfigurationFile(
+ $recipe->getSet(),
+ $templateVariables,
+ self::FRACTOR_FQN_NAME_PATTERN
+ );
+
+ $testCaseDirectoryPath = $this->resolveTestCaseDirectoryPath($generatedFilePaths);
+ $this->printSuccess($recipe->getFractorName(), $generatedFilePaths, $testCaseDirectoryPath);
+
+ return Command::SUCCESS;
+ }
+
+ private function askForTypo3Version(): Question
+ {
+ $whatTypo3Version = new Question('TYPO3-Version (i.e. 12.0): ');
+ $whatTypo3Version->setNormalizer(
+ static fn ($version): Typo3Version => Typo3Version::createFromString(trim((string) $version))
+ );
+ $whatTypo3Version->setMaxAttempts(2);
+ $whatTypo3Version->setValidator(
+ static function (Typo3Version $version): Typo3Version {
+ Assert::greaterThanEq($version->getMajor(), 7);
+ Assert::greaterThanEq($version->getMinor(), 0);
+
+ return $version;
+ }
+ );
+
+ return $whatTypo3Version;
+ }
+
+ private function askForChangelogUrl(): Question
+ {
+ $whatIsTheUrlToChangelog = new Question(
+ 'Url to changelog (i.e. https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/...) or "x" for none: '
+ );
+ $whatIsTheUrlToChangelog->setMaxAttempts(3);
+ $whatIsTheUrlToChangelog->setValidator(
+ static function (?string $url): string {
+ Assert::notNull($url);
+
+ if (strtolower($url) === 'x') {
+ return '';
+ }
+
+ if (! filter_var($url, FILTER_VALIDATE_URL)) {
+ throw new RuntimeException('Please enter a valid Url');
+ }
+
+ Assert::startsWith($url, 'https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/');
+
+ return $url;
+ }
+ );
+
+ return $whatIsTheUrlToChangelog;
+ }
+
+ private function askForName(): Question
+ {
+ $giveMeYourName = new Question('Name (i.e MigrateRequiredFlag): ');
+ $giveMeYourName->setNormalizer(
+ static fn ($name): ?string => preg_replace('/Fractor$/', '', ucfirst((string) $name))
+ );
+ $giveMeYourName->setMaxAttempts(3);
+ $giveMeYourName->setValidator(static function (string $name): string {
+ Assert::minLength($name, 5);
+ Assert::maxLength($name, 60);
+ Assert::notContains($name, ' ', 'The name must not contain spaces');
+ // Pattern from: https://www.php.net/manual/en/language.oop5.basic.php
+ Assert::regex(
+ $name,
+ '/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/',
+ 'The name must be a valid PHP class name. A valid class name starts with a letter or underscore, followed by any number of letters, numbers, or underscores.'
+ );
+
+ return $name;
+ });
+
+ return $giveMeYourName;
+ }
+
+ private function askForDescription(): Question
+ {
+ $description = new Question('Description (i.e. Migrate required flag): ');
+ $description->setMaxAttempts(3);
+ $description->setValidator(static function (?string $description): string {
+ Assert::notNull($description, 'Please enter a description');
+ Assert::minLength($description, 5);
+ Assert::maxLength($description, 120);
+
+ return $description;
+ });
+
+ return $description;
+ }
+
+ private function askForType(): Question
+ {
+ $question = new ChoiceQuestion('Please select the Fractor type', [
+ 'flexform',
+ 'fluid',
+ 'typoscript',
+ 'yaml',
+ 'composer',
+ ]);
+ $question->setMaxAttempts(3);
+ $question->setErrorMessage('Type %s is invalid.');
+
+ return $question;
+ }
+
+ /**
+ * @param string[] $generatedFilePaths
+ */
+ private function printSuccess(string $name, array $generatedFilePaths, string $testCaseFilePath): void
+ {
+ $message = sprintf('New files generated for "%s":', $name);
+ $this->outputStyle->writeln($message);
+
+ sort($generatedFilePaths);
+
+ foreach ($generatedFilePaths as $generatedFilePath) {
+ $fileInfo = $this->fileInfoFactory->createFileInfoFromPath($generatedFilePath);
+ $this->outputStyle->writeln(' * ' . $fileInfo->getRelativePathname());
+ }
+
+ $message = sprintf(
+ 'Run tests for this fractor:%svendor/bin/phpunit %s',
+ PHP_EOL . PHP_EOL,
+ $testCaseFilePath . PHP_EOL
+ );
+ $this->outputStyle->writeln($message);
+ }
+
+ /**
+ * @param string[] $generatedFilePaths
+ */
+ private function resolveTestCaseDirectoryPath(array $generatedFilePaths): string
+ {
+ foreach ($generatedFilePaths as $generatedFilePath) {
+ if (! \str_ends_with($generatedFilePath, 'Test.php')
+ && ! \str_ends_with($generatedFilePath, 'Test.php.inc')
+ ) {
+ continue;
+ }
+
+ $generatedFileInfo = $this->fileInfoFactory->createFileInfoFromPath($generatedFilePath);
+ return $generatedFileInfo->getRelativePath();
+ }
+
+ throw new ShouldNotHappenException();
+ }
+}
diff --git a/packages/fractor-rule-generator/src/Contract/Typo3FractorTypeInterface.php b/packages/fractor-rule-generator/src/Contract/Typo3FractorTypeInterface.php
new file mode 100644
index 0000000..b691a71
--- /dev/null
+++ b/packages/fractor-rule-generator/src/Contract/Typo3FractorTypeInterface.php
@@ -0,0 +1,20 @@
+ $variables
+ */
+ public function create(string $content, array $variables): string
+ {
+ $variableKeys = array_keys($variables);
+ $variableValues = array_values($variables);
+
+ return str_replace($variableKeys, $variableValues, $content);
+ }
+}
diff --git a/packages/fractor-rule-generator/src/Factory/Typo3FractorTypeFactory.php b/packages/fractor-rule-generator/src/Factory/Typo3FractorTypeFactory.php
new file mode 100644
index 0000000..d501132
--- /dev/null
+++ b/packages/fractor-rule-generator/src/Factory/Typo3FractorTypeFactory.php
@@ -0,0 +1,27 @@
+ new ComposerJsonFractorType(),
+ 'flexform' => new FlexFormFractorType(),
+ 'fluid' => new FluidFractorType(),
+ 'typoscript' => new TypoScriptFractorType(),
+ 'yaml' => new YamlFractorType(),
+ default => throw new \Exception('Invalid type given'),
+ };
+ }
+}
diff --git a/packages/fractor-rule-generator/src/FileSystem/ConfigFilesystem.php b/packages/fractor-rule-generator/src/FileSystem/ConfigFilesystem.php
new file mode 100644
index 0000000..8ddaf54
--- /dev/null
+++ b/packages/fractor-rule-generator/src/FileSystem/ConfigFilesystem.php
@@ -0,0 +1,95 @@
+ $templateVariables
+ */
+ public function addRuleToConfigurationFile(
+ string $configFilePath,
+ array $templateVariables,
+ string $rectorFqnNamePattern
+ ): void {
+ $this->createConfigurationFileIfNotExists($configFilePath);
+
+ $configFileContents = (string) file_get_contents($configFilePath);
+
+ $this->ensureRequiredKeysAreSet($templateVariables);
+
+ // already added?
+ $servicesFullyQualifiedName = $this->templateFactory->create($rectorFqnNamePattern, $templateVariables);
+ if (\str_contains($configFileContents, $servicesFullyQualifiedName)) {
+ return;
+ }
+
+ $rule = sprintf('$services->set(\\%s::class);', $servicesFullyQualifiedName);
+ // Add new rule to existing ones or add as first rule of new configuration file.
+ if (Strings::match($configFileContents, self::LAST_ITEM_REGEX) !== null
+ && Strings::match($configFileContents, self::LAST_ITEM_REGEX) !== []
+ ) {
+ $registerServiceLine = sprintf(';' . PHP_EOL . ' %s' . PHP_EOL . '};', $rule);
+ $configFileContents = Strings::replace($configFileContents, self::LAST_ITEM_REGEX, $registerServiceLine);
+ } else {
+ $configFileContents = str_replace('###FIRST_RULE###', $rule, $configFileContents);
+ }
+
+ // Print the content back to file
+ $this->filesystem->dumpFile($configFilePath, $configFileContents);
+ }
+
+ /**
+ * @param array $templateVariables
+ */
+ private function ensureRequiredKeysAreSet(array $templateVariables): void
+ {
+ $missingKeys = array_diff(self::REQUIRED_KEYS, array_keys($templateVariables));
+ if ($missingKeys === []) {
+ return;
+ }
+
+ $message = sprintf('Template variables for "%s" keys are missing', implode('", "', $missingKeys));
+ throw new ShouldNotHappenException($message);
+ }
+
+ private function createConfigurationFileIfNotExists(string $configFilePath): void
+ {
+ if ($this->filesystem->exists($configFilePath)) {
+ return;
+ }
+
+ $parentDirectory = dirname($configFilePath);
+ $this->filesystem->mkdir($parentDirectory);
+ $this->filesystem->touch($configFilePath);
+ $this->filesystem->appendToFile(
+ $configFilePath,
+ (string) file_get_contents(__DIR__ . '/../../templates/config/config.php'),
+ true
+ );
+ }
+}
diff --git a/packages/fractor-rule-generator/src/FileSystem/TemplateFileSystem.php b/packages/fractor-rule-generator/src/FileSystem/TemplateFileSystem.php
new file mode 100644
index 0000000..25888ed
--- /dev/null
+++ b/packages/fractor-rule-generator/src/FileSystem/TemplateFileSystem.php
@@ -0,0 +1,97 @@
+getRelativeFilePathFromDirectory($smartFileInfo, TemplateFinder::TEMPLATES_DIRECTORY);
+ $destination = $this->applyVariables($destination, $templateVariables);
+
+ // remove ".inc" protection from PHPUnit if not a test case
+ if ($this->isNonFixtureFileWithIncSuffix($destination)) {
+ $destination = Strings::before($destination, '.inc');
+ }
+
+ // special hack for tests, to PHPUnit doesn't load the generated file as test case
+ /** @var string $destination */
+ if (\str_ends_with($destination, 'Test.php') && StaticPHPUnitEnvironment::isPHPUnitRun()) {
+ $destination .= '.inc';
+ }
+
+ return $targetDirectory . DIRECTORY_SEPARATOR . $destination;
+ }
+
+ /**
+ * @param mixed[] $variables
+ */
+ private function applyVariables(string $content, array $variables): string
+ {
+ return str_replace(array_keys($variables), array_values($variables), $content);
+ }
+
+ private function isNonFixtureFileWithIncSuffix(string $filePath): bool
+ {
+ if (Strings::match($filePath, self::FIXTURE_SHORT_REGEX) !== null
+ && Strings::match($filePath, self::FIXTURE_SHORT_REGEX) !== []
+ ) {
+ return false;
+ }
+
+ return \str_ends_with($filePath, '.inc');
+ }
+
+ private function getRelativeFilePathFromDirectory(SplFileInfo $fileInfo, string $directory): string
+ {
+ if (! file_exists($directory)) {
+ throw new ShouldNotHappenException(sprintf(
+ 'Directory "%s" was not found in %s.',
+ $directory,
+ self::class
+ ));
+ }
+
+ $relativeFilePath = $this->filesystem->makePathRelative(
+ $this->getNormalizedRealPath($fileInfo),
+ (string) realpath($directory)
+ );
+ return rtrim($relativeFilePath, '/');
+ }
+
+ private function getNormalizedRealPath(SplFileInfo $fileInfo): string
+ {
+ return $this->normalizePath($fileInfo->getRealPath());
+ }
+
+ private function normalizePath(string $path): string
+ {
+ return str_replace('\\', '/', $path);
+ }
+}
diff --git a/packages/fractor-rule-generator/src/Finder/TemplateFinder.php b/packages/fractor-rule-generator/src/Finder/TemplateFinder.php
new file mode 100644
index 0000000..d029710
--- /dev/null
+++ b/packages/fractor-rule-generator/src/Finder/TemplateFinder.php
@@ -0,0 +1,49 @@
+addRuleAndTestCase($fixtureFileExtension);
+
+ $smartFileInfos = [];
+ foreach ($filePaths as $filePath) {
+ $smartFileInfos[] = $this->fileInfoFactory->createFileInfoFromPath($filePath);
+ }
+
+ return $smartFileInfos;
+ }
+
+ /**
+ * @return array
+ */
+ private function addRuleAndTestCase(string $fixtureFileExtension): array
+ {
+ return [
+ __DIR__ . '/../../templates/rules/TYPO3__MajorPrefixed__/__Type__/__Name__.php',
+ __DIR__ . '/../../templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/__Name__Test.php.inc',
+ __DIR__ . '/../../templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.' . $fixtureFileExtension,
+ __DIR__ . '/../../templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/config/fractor.php.inc',
+ ];
+ }
+}
diff --git a/packages/fractor-rule-generator/src/Generator/FileGenerator.php b/packages/fractor-rule-generator/src/Generator/FileGenerator.php
new file mode 100644
index 0000000..86231c8
--- /dev/null
+++ b/packages/fractor-rule-generator/src/Generator/FileGenerator.php
@@ -0,0 +1,64 @@
+generateFileInfoWithTemplateVariables(
+ $fileInfo,
+ $templateVariables,
+ $destinationDirectory
+ );
+ }
+
+ return $generatedFilePaths;
+ }
+
+ /**
+ * @param array $templateVariables
+ */
+ private function generateFileInfoWithTemplateVariables(
+ SplFileInfo $smartFileInfo,
+ array $templateVariables,
+ string $targetDirectory
+ ): string {
+ $targetFilePath = $this->templateFileSystem->resolveDestination(
+ $smartFileInfo,
+ $templateVariables,
+ $targetDirectory
+ );
+
+ $content = $this->templateFactory->create($smartFileInfo->getContents(), $templateVariables);
+
+ $this->filesystem->dumpFile($targetFilePath, $content);
+
+ return $targetFilePath;
+ }
+}
diff --git a/packages/fractor-rule-generator/src/ValueObject/FractorType/ComposerJsonFractorType.php b/packages/fractor-rule-generator/src/ValueObject/FractorType/ComposerJsonFractorType.php
new file mode 100644
index 0000000..bcd67a0
--- /dev/null
+++ b/packages/fractor-rule-generator/src/ValueObject/FractorType/ComposerJsonFractorType.php
@@ -0,0 +1,56 @@
+url === '') {
+ return '';
+ }
+
+ $url = $this->url;
+ return <<typo3Version->getMajor());
+ }
+
+ public function getMajorVersion(): string
+ {
+ return (string) $this->typo3Version->getMajor();
+ }
+
+ public function getMinorVersionPrefixed(): string
+ {
+ return sprintf('v%d', $this->typo3Version->getMinor());
+ }
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ public function getFractorName(): string
+ {
+ return $this->name . 'Fractor';
+ }
+
+ public function getTestDirectory(): string
+ {
+ return $this->name . 'Fractor';
+ }
+
+ public function getSet(): string
+ {
+ return sprintf(__DIR__ . '/../../../typo3-fractor/config/typo3-%d.php', $this->getMajorVersion());
+ }
+
+ public function getUseImports(): string
+ {
+ $useImports = '';
+ if ($this->url === '') {
+ $useImports .= <<type->getUseImports();
+ }
+
+ public function getTraits(): string
+ {
+ return $this->type->getTraits();
+ }
+
+ public function getExtendsImplements(): string
+ {
+ $extendsImplements = $this->type->getExtendsImplements();
+ if ($this->url === '') {
+ if (str_contains($extendsImplements, 'implements')) {
+ $extendsImplements .= ', NoChangelogRequired';
+ } else {
+ $extendsImplements .= ' implements NoChangelogRequired';
+ }
+ }
+ return $extendsImplements;
+ }
+
+ public function getFractorBodyTemplate(): string
+ {
+ return $this->type->getFractorBodyTemplate();
+ }
+
+ public function getFractorTypeFolderName(): string
+ {
+ return $this->type->getFolderName();
+ }
+
+ public function getFractorFixtureFileExtension(): string
+ {
+ return $this->type->getFractorFixtureFileExtension();
+ }
+}
diff --git a/packages/fractor-rule-generator/src/ValueObject/Typo3Version.php b/packages/fractor-rule-generator/src/ValueObject/Typo3Version.php
new file mode 100644
index 0000000..6457eb0
--- /dev/null
+++ b/packages/fractor-rule-generator/src/ValueObject/Typo3Version.php
@@ -0,0 +1,40 @@
+major;
+ }
+
+ public function getMinor(): int
+ {
+ return $this->minor;
+ }
+
+ public static function createFromString(string $version): self
+ {
+ if (! str_contains($version, '.')) {
+ $version .= '.0';
+ }
+
+ [$major, $minor] = explode('.', $version, 2);
+
+ return new self((int) $major, (int) $minor);
+ }
+
+ public function getFullVersion(): string
+ {
+ return sprintf('%d%d', $this->major, $this->minor);
+ }
+}
diff --git a/packages/fractor-rule-generator/templates/config/config.php b/packages/fractor-rule-generator/templates/config/config.php
new file mode 100644
index 0000000..fb2029a
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/config/config.php
@@ -0,0 +1,14 @@
+services();
+ $services->defaults()
+ ->autoconfigure()
+ ->autowire();
+
+ ###FIRST_RULE###
+};
diff --git a/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.html b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.html
new file mode 100644
index 0000000..31cd93a
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.html
@@ -0,0 +1,7 @@
+
+
+
+-----
+
+
+
diff --git a/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.json b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.json
new file mode 100644
index 0000000..9b7e38c
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.json
@@ -0,0 +1,7 @@
+{
+ "key": "value"
+}
+-----
+{
+ "key": "value"
+}
diff --git a/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.typoscript b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.typoscript
new file mode 100644
index 0000000..1cbe1d2
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.typoscript
@@ -0,0 +1,7 @@
+config {
+ a = 1
+}
+-----
+config {
+ a = 1
+}
diff --git a/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.xml b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.xml
new file mode 100644
index 0000000..fe07f5b
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ sheetTitle
+ array
+
+
+
+ input
+
+
+
+
+
+
+
+-----
+
+
+
+
+
+ sheetTitle
+ array
+
+
+
+ input
+
+
+
+
+
+
+
diff --git a/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.yaml b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.yaml
new file mode 100644
index 0000000..367772f
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.yaml
@@ -0,0 +1,15 @@
+TYPO3:
+ CMS:
+ Form:
+ prototypes:
+ standard:
+ formElementsDefinition:
+ Form:
+-----
+TYPO3:
+ CMS:
+ Form:
+ prototypes:
+ standard:
+ formElementsDefinition:
+ Form:
diff --git a/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/__Name__Test.php.inc b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/__Name__Test.php.inc
new file mode 100644
index 0000000..8b1523f
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/__Name__Test.php.inc
@@ -0,0 +1,27 @@
+doTestFile($filePath);
+ }
+
+ public static function provideData(): \Iterator
+ {
+ return self::yieldFilesFromDirectory(__DIR__ . '/Fixtures', '*.__FixtureFileExtension__');
+ }
+
+ public function provideConfigFilePath(): ?string
+ {
+ return __DIR__ . '/config/fractor.php';
+ }
+}
diff --git a/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/config/fractor.php.inc b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/config/fractor.php.inc
new file mode 100644
index 0000000..a61a94d
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/config/fractor.php.inc
@@ -0,0 +1,15 @@
+withOptions([
+ XmlProcessorOption::INDENT_CHARACTER => Indent::STYLE_TAB,
+ XmlProcessorOption::INDENT_SIZE => 1,
+ ])
+ ->withRules([__Name__::class]);
diff --git a/packages/fractor-rule-generator/templates/rules/TYPO3__MajorPrefixed__/__Type__/__Name__.php b/packages/fractor-rule-generator/templates/rules/TYPO3__MajorPrefixed__/__Type__/__Name__.php
new file mode 100644
index 0000000..856933a
--- /dev/null
+++ b/packages/fractor-rule-generator/templates/rules/TYPO3__MajorPrefixed__/__Type__/__Name__.php
@@ -0,0 +1,28 @@
+add($createdCommand);
+
+ $foundCommand = $application->find($commandName);
+
+ // Create a CommandTester with your Command class processed by your Application.
+ $tester = new CommandTester($foundCommand);
+
+ // Respond "y" to the first prompt (question) when the command is invoked.
+ $tester->setInputs(['7', 'x', 'MigrateTypoScript', 'Migrate TypoScript Setting', '2']);
+
+ // Execute the command. This example would be the equivalent of
+ // 'bin/console example 127.0.0.1 --ipv6=true'
+ $tester->execute([
+ 'command' => $commandName,
+ // Arguments as needed.
+ //'generate-rule' => 'generate-rule',
+ // Options as needed.
+ //'--ipv6' => true,
+ ]);
+
+ //self::assertSame('Example output', $tester->getDisplay());
+ self::assertSame(0, $tester->getStatusCode());
+ self::assertFileExists(__DIR__ . '/../../../../../typo3-fractor/config/typo3-7.php');
+ self::assertFileExists(
+ __DIR__ . '/../../../../../typo3-fractor/rules/TYPO3v7/TypoScript/MigrateTypoScriptFractor.php'
+ );
+ self::assertFileExists(
+ __DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor/config/fractor.php'
+ );
+ self::assertFileExists(
+ __DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor/Fixture/fixture.typoscript'
+ );
+ self::assertFileExists(
+ __DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor/MigrateTypoScriptFractorTest.php.inc'
+ );
+
+ unlink(__DIR__ . '/../../../../../typo3-fractor/config/typo3-7.php');
+ unlink(__DIR__ . '/../../../../../typo3-fractor/rules/TYPO3v7/TypoScript/MigrateTypoScriptFractor.php');
+ rmdir(__DIR__ . '/../../../../../typo3-fractor/rules/TYPO3v7/TypoScript');
+ rmdir(__DIR__ . '/../../../../../typo3-fractor/rules/TYPO3v7');
+
+ unlink(
+ __DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor/config/fractor.php'
+ );
+ rmdir(__DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor/config');
+
+ unlink(
+ __DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor/Fixture/fixture.typoscript'
+ );
+ rmdir(
+ __DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor/Fixture'
+ );
+
+ unlink(
+ __DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor/MigrateTypoScriptFractorTest.php.inc'
+ );
+ rmdir(__DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript/MigrateTypoScriptFractor');
+ rmdir(__DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7/TypoScript');
+ rmdir(__DIR__ . '/../../../../../typo3-fractor/rules-tests/TYPO3v7');
+ }
+}
diff --git a/packages/fractor/src/FileSystem/FileInfoFactory.php b/packages/fractor/src/FileSystem/FileInfoFactory.php
new file mode 100644
index 0000000..23f8190
--- /dev/null
+++ b/packages/fractor/src/FileSystem/FileInfoFactory.php
@@ -0,0 +1,36 @@
+filesystem->makePathRelative($realPath, $currentWorkingDirectory), '/');
+ $relativeDirectoryPath = dirname($relativeFilePath);
+
+ return new SplFileInfo($filePath, $relativeDirectoryPath, $relativeFilePath);
+ }
+}
diff --git a/phpstan.neon b/phpstan.neon
index d85e1ea..2f46bc3 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -8,10 +8,10 @@ parameters:
paths:
- src/
- packages/
- - ecs.php
- rector.php
excludePaths:
- packages/extension-installer/generated
- packages/**/tests/**/Fixtures/*
- packages/**/tests/**/Fixture/*
- packages/**/tests/Fixtures/*
+ - packages/fractor-rule-generator/templates
diff --git a/rector.php b/rector.php
index 7d74a38..bd42b7d 100644
--- a/rector.php
+++ b/rector.php
@@ -8,7 +8,10 @@
->withPhpSets(php82: true)
->withPreparedSets(deadCode: true, typeDeclarations: true, earlyReturn: true, strictBooleans: true)
->withImportNames(true, true, false, true)
- ->withSkip([__DIR__ . '/packages/extension-installer/generated'])
+ ->withSkip([
+ __DIR__ . '/packages/extension-installer/generated',
+ __DIR__ . '/packages/fractor-rule-generator/templates',
+ ])
->withPaths([
__DIR__ . '/ecs.php',
__DIR__ . '/packages',