Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f35a605

Browse files
committedSep 5, 2024·
[FEATURE] Add Rule generator
Resolves: #108
1 parent 068253d commit f35a605

34 files changed

+1603
-3
lines changed
 

‎composer.json

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"a9f/fractor-extension-installer": "self.version",
4848
"a9f/fractor-fluid": "self.version",
4949
"a9f/fractor-phpstan-rules": "self.version",
50+
"a9f/fractor-rule-generator": "self.version",
5051
"a9f/fractor-typoscript": "self.version",
5152
"a9f/fractor-xml": "self.version",
5253
"a9f/fractor-yaml": "self.version",
@@ -64,6 +65,7 @@
6465
"a9f\\FractorFluid\\": "packages/fractor-fluid/src/",
6566
"a9f\\FractorMonorepo\\": "src/",
6667
"a9f\\FractorPhpStanRules\\": "packages/fractor-phpstan-rules/src/",
68+
"a9f\\FractorRuleGenerator\\": "packages/fractor-rule-generator/src/",
6769
"a9f\\FractorTypoScript\\": "packages/fractor-typoscript/src/",
6870
"a9f\\FractorXml\\": "packages/fractor-xml/src/",
6971
"a9f\\FractorYaml\\": "packages/fractor-yaml/src/",
@@ -83,6 +85,7 @@
8385
"a9f\\FractorDocGenerator\\Tests\\": "packages/fractor-doc-generator/tests/",
8486
"a9f\\FractorFluid\\Tests\\": "packages/fractor-fluid/tests/",
8587
"a9f\\FractorPhpStanRules\\Tests\\": "packages/fractor-phpstan-rules/tests/",
88+
"a9f\\FractorRuleGenerator\\Tests\\": "packages/fractor-rule-generator/tests/",
8689
"a9f\\FractorTypoScript\\Tests\\": "packages/fractor-typoscript/tests/",
8790
"a9f\\FractorXml\\Tests\\": "packages/fractor-xml/tests/",
8891
"a9f\\FractorYaml\\Tests\\": "packages/fractor-yaml/tests/",

‎ecs.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
YodaStyleFixer::class,
3232
OperatorLinebreakFixer::class,
3333
])
34-
->withSkip([__DIR__ . '/packages/extension-installer/generated'])
34+
->withSkip([
35+
__DIR__ . '/packages/extension-installer/generated',
36+
__DIR__ . '/packages/fractor-rule-generator/templates',
37+
])
3538
->withPreparedSets(psr12: true, common: true, symplify: true, cleanCode: true)
3639
->withPaths([__DIR__ . '/e2e', __DIR__ . '/src', __DIR__ . '/packages'])
3740
->withRootFiles();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/vendor/
2+
/composer.lock
3+
.phpunit.cache
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "a9f/fractor-rule-generator",
3+
"description": "Fractor rule generator",
4+
"license": "MIT",
5+
"type": "fractor-extension",
6+
"authors": [
7+
{
8+
"name": "Andreas Wolf",
9+
"email": "dev@a-w.io",
10+
"role": "Lead Developer"
11+
}
12+
],
13+
"require": {
14+
"php": "^8.2",
15+
"a9f/fractor": "^0.3",
16+
"a9f/fractor-extension-installer": "^0.3",
17+
"nette/utils": "^4.0",
18+
"symfony/config": "^5.4 || ^6.4 || ^7.0",
19+
"symfony/console": "^5.4 || ^6.4 || ^7.0",
20+
"symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0",
21+
"symfony/filesystem": "^5.4 || ^6.4 || ^7.0",
22+
"symfony/finder": "^5.4 || ^6.4 || ^7.0",
23+
"symplify/rule-doc-generator-contracts": "^11.2",
24+
"webmozart/assert": "^1.11"
25+
},
26+
"minimum-stability": "dev",
27+
"prefer-stable": true,
28+
"autoload": {
29+
"psr-4": {
30+
"a9f\\FractorRuleGenerator\\": "src/"
31+
}
32+
},
33+
"autoload-dev": {
34+
"psr-4": {
35+
"a9f\\FractorRuleGenerator\\Tests\\": "tests/"
36+
}
37+
},
38+
"config": {
39+
"allow-plugins": {
40+
"a9f/fractor-extension-installer": true
41+
},
42+
"sort-packages": true
43+
},
44+
"extra": {
45+
"branch-alias": {
46+
"dev-main": "0.3-dev"
47+
}
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Symfony\Component\Console\Output\ConsoleOutput;
6+
use Symfony\Component\Console\Output\OutputInterface;
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
9+
10+
return static function (ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void {
11+
$services = $containerConfigurator->services();
12+
$services->defaults()
13+
->autowire()
14+
->private()
15+
->autoconfigure();
16+
17+
$services->load('a9f\\FractorRuleGenerator\\', __DIR__ . '/../../fractor-rule-generator/src')
18+
->exclude([__DIR__ . '/../src/ValueObject', __DIR__ . '/../src/**/ValueObject']);
19+
20+
$services->set(ConsoleOutput::class);
21+
$services->alias(OutputInterface::class, ConsoleOutput::class);
22+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache">
3+
<testsuites>
4+
<testsuite name="fractor-rule-generator">
5+
<directory>tests</directory>
6+
</testsuite>
7+
</testsuites>
8+
<source>
9+
<include>
10+
<directory>./src</directory>
11+
</include>
12+
</source>
13+
</phpunit>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\Console\Command;
6+
7+
use a9f\Fractor\Exception\ShouldNotHappenException;
8+
use a9f\Fractor\FileSystem\FileInfoFactory;
9+
use a9f\FractorRuleGenerator\Factory\Typo3FractorTypeFactory;
10+
use a9f\FractorRuleGenerator\FileSystem\ConfigFilesystem;
11+
use a9f\FractorRuleGenerator\Finder\TemplateFinder;
12+
use a9f\FractorRuleGenerator\Generator\FileGenerator;
13+
use a9f\FractorRuleGenerator\ValueObject\Typo3FractorRecipe;
14+
use a9f\FractorRuleGenerator\ValueObject\Typo3Version;
15+
use RuntimeException;
16+
use Symfony\Component\Console\Attribute\AsCommand;
17+
use Symfony\Component\Console\Command\Command;
18+
use Symfony\Component\Console\Helper\QuestionHelper;
19+
use Symfony\Component\Console\Input\InputInterface;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\Console\Question\ChoiceQuestion;
22+
use Symfony\Component\Console\Question\Question;
23+
use Webmozart\Assert\Assert;
24+
25+
#[AsCommand(name: 'generate-rule', description: 'Generate a new Fractor rule in a proper location, with tests')]
26+
final class GenerateRuleCommand extends Command
27+
{
28+
/**
29+
* @var string
30+
*/
31+
private const FRACTOR_FQN_NAME_PATTERN = 'a9f\Typo3Fractor\TYPO3__MajorPrefixed__\__Type__\__Name__';
32+
33+
public function __construct(
34+
private readonly TemplateFinder $templateFinder,
35+
private readonly FileGenerator $fileGenerator,
36+
private readonly OutputInterface $outputStyle,
37+
private readonly ConfigFilesystem $configFilesystem,
38+
private readonly FileInfoFactory $fileInfoFactory
39+
) {
40+
parent::__construct();
41+
}
42+
43+
protected function execute(InputInterface $input, OutputInterface $output): int
44+
{
45+
/** @var QuestionHelper $helper */
46+
$helper = $this->getHelper('question');
47+
48+
/** @var Typo3Version $typo3Version */
49+
$typo3Version = $helper->ask($input, $output, $this->askForTypo3Version());
50+
$changelogUrl = $helper->ask($input, $output, $this->askForChangelogUrl());
51+
$name = $helper->ask($input, $output, $this->askForName());
52+
$description = $helper->ask($input, $output, $this->askForDescription());
53+
$type = $helper->ask($input, $output, $this->askForType());
54+
55+
$recipe = new Typo3FractorRecipe(
56+
$typo3Version,
57+
$changelogUrl,
58+
$name,
59+
$description,
60+
Typo3FractorTypeFactory::fromString($type)
61+
);
62+
63+
$templateFileInfos = $this->templateFinder->find($recipe->getFractorFixtureFileExtension());
64+
65+
$templateVariables = [
66+
'__MajorPrefixed__' => $recipe->getMajorVersionPrefixed(),
67+
'__Major__' => $recipe->getMajorVersion(),
68+
'__MinorPrefixed__' => $recipe->getMinorVersionPrefixed(),
69+
'__Type__' => $recipe->getFractorTypeFolderName(),
70+
'__FixtureFileExtension__' => $recipe->getFractorFixtureFileExtension(),
71+
'__Name__' => $recipe->getFractorName(),
72+
'__Test_Directory__' => $recipe->getTestDirectory(),
73+
'__Changelog_Annotation__' => $recipe->getChangelogAnnotation(),
74+
'__Description__' => addslashes($recipe->getDescription()),
75+
'__Use__' => $recipe->getUseImports(),
76+
'__Traits__' => $recipe->getTraits(),
77+
'__ExtendsImplements__' => $recipe->getExtendsImplements(),
78+
'__Base_Fractor_Body_Template__' => $recipe->getFractorBodyTemplate(),
79+
];
80+
81+
$targetDirectory = __DIR__ . '/../../../../typo3-fractor';
82+
83+
$generatedFilePaths = $this->fileGenerator->generateFiles(
84+
$templateFileInfos,
85+
$templateVariables,
86+
$targetDirectory
87+
);
88+
89+
$this->configFilesystem->addRuleToConfigurationFile(
90+
$recipe->getSet(),
91+
$templateVariables,
92+
self::FRACTOR_FQN_NAME_PATTERN
93+
);
94+
95+
$testCaseDirectoryPath = $this->resolveTestCaseDirectoryPath($generatedFilePaths);
96+
$this->printSuccess($recipe->getFractorName(), $generatedFilePaths, $testCaseDirectoryPath);
97+
98+
return Command::SUCCESS;
99+
}
100+
101+
private function askForTypo3Version(): Question
102+
{
103+
$whatTypo3Version = new Question('TYPO3-Version (i.e. 12.0): ');
104+
$whatTypo3Version->setNormalizer(
105+
static fn ($version): Typo3Version => Typo3Version::createFromString(trim((string) $version))
106+
);
107+
$whatTypo3Version->setMaxAttempts(2);
108+
$whatTypo3Version->setValidator(
109+
static function (Typo3Version $version): Typo3Version {
110+
Assert::greaterThanEq($version->getMajor(), 7);
111+
Assert::greaterThanEq($version->getMinor(), 0);
112+
113+
return $version;
114+
}
115+
);
116+
117+
return $whatTypo3Version;
118+
}
119+
120+
private function askForChangelogUrl(): Question
121+
{
122+
$whatIsTheUrlToChangelog = new Question(
123+
'Url to changelog (i.e. https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/...) or "x" for none: '
124+
);
125+
$whatIsTheUrlToChangelog->setMaxAttempts(3);
126+
$whatIsTheUrlToChangelog->setValidator(
127+
static function (?string $url): string {
128+
Assert::notNull($url);
129+
130+
if (strtolower($url) === 'x') {
131+
return '';
132+
}
133+
134+
if (! filter_var($url, FILTER_VALIDATE_URL)) {
135+
throw new RuntimeException('Please enter a valid Url');
136+
}
137+
138+
Assert::startsWith($url, 'https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/');
139+
140+
return $url;
141+
}
142+
);
143+
144+
return $whatIsTheUrlToChangelog;
145+
}
146+
147+
private function askForName(): Question
148+
{
149+
$giveMeYourName = new Question('Name (i.e MigrateRequiredFlag): ');
150+
$giveMeYourName->setNormalizer(
151+
static fn ($name): ?string => preg_replace('/Fractor$/', '', ucfirst((string) $name))
152+
);
153+
$giveMeYourName->setMaxAttempts(3);
154+
$giveMeYourName->setValidator(static function (string $name): string {
155+
Assert::minLength($name, 5);
156+
Assert::maxLength($name, 60);
157+
Assert::notContains($name, ' ', 'The name must not contain spaces');
158+
// Pattern from: https://www.php.net/manual/en/language.oop5.basic.php
159+
Assert::regex(
160+
$name,
161+
'/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/',
162+
'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.'
163+
);
164+
165+
return $name;
166+
});
167+
168+
return $giveMeYourName;
169+
}
170+
171+
private function askForDescription(): Question
172+
{
173+
$description = new Question('Description (i.e. Migrate required flag): ');
174+
$description->setMaxAttempts(3);
175+
$description->setValidator(static function (?string $description): string {
176+
Assert::notNull($description, 'Please enter a description');
177+
Assert::minLength($description, 5);
178+
Assert::maxLength($description, 120);
179+
180+
return $description;
181+
});
182+
183+
return $description;
184+
}
185+
186+
private function askForType(): Question
187+
{
188+
$question = new ChoiceQuestion('Please select the Fractor type', [
189+
'flexform',
190+
'fluid',
191+
'typoscript',
192+
'yaml',
193+
'composer',
194+
]);
195+
$question->setMaxAttempts(3);
196+
$question->setErrorMessage('Type %s is invalid.');
197+
198+
return $question;
199+
}
200+
201+
/**
202+
* @param string[] $generatedFilePaths
203+
*/
204+
private function printSuccess(string $name, array $generatedFilePaths, string $testCaseFilePath): void
205+
{
206+
$message = sprintf('<info>New files generated for "%s":</info>', $name);
207+
$this->outputStyle->writeln($message);
208+
209+
sort($generatedFilePaths);
210+
211+
foreach ($generatedFilePaths as $generatedFilePath) {
212+
$fileInfo = $this->fileInfoFactory->createFileInfoFromPath($generatedFilePath);
213+
$this->outputStyle->writeln(' * ' . $fileInfo->getRelativePathname());
214+
}
215+
216+
$message = sprintf(
217+
'<info>Run tests for this fractor:</info>%svendor/bin/phpunit %s',
218+
PHP_EOL . PHP_EOL,
219+
$testCaseFilePath . PHP_EOL
220+
);
221+
$this->outputStyle->writeln($message);
222+
}
223+
224+
/**
225+
* @param string[] $generatedFilePaths
226+
*/
227+
private function resolveTestCaseDirectoryPath(array $generatedFilePaths): string
228+
{
229+
foreach ($generatedFilePaths as $generatedFilePath) {
230+
if (! \str_ends_with($generatedFilePath, 'Test.php')
231+
&& ! \str_ends_with($generatedFilePath, 'Test.php.inc')
232+
) {
233+
continue;
234+
}
235+
236+
$generatedFileInfo = $this->fileInfoFactory->createFileInfoFromPath($generatedFilePath);
237+
return $generatedFileInfo->getRelativePath();
238+
}
239+
240+
throw new ShouldNotHappenException();
241+
}
242+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\Contract;
6+
7+
interface Typo3FractorTypeInterface extends \Stringable
8+
{
9+
public function getFolderName(): string;
10+
11+
public function getUseImports(): string;
12+
13+
public function getExtendsImplements(): string;
14+
15+
public function getTraits(): string;
16+
17+
public function getFractorFixtureFileExtension(): string;
18+
19+
public function getFractorBodyTemplate(): string;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\Factory;
6+
7+
class TemplateFactory
8+
{
9+
/**
10+
* @param array<string, string> $variables
11+
*/
12+
public function create(string $content, array $variables): string
13+
{
14+
$variableKeys = array_keys($variables);
15+
$variableValues = array_values($variables);
16+
17+
return str_replace($variableKeys, $variableValues, $content);
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\Factory;
6+
7+
use a9f\FractorRuleGenerator\Contract\Typo3FractorTypeInterface;
8+
use a9f\FractorRuleGenerator\ValueObject\FractorType\ComposerJsonFractorType;
9+
use a9f\FractorRuleGenerator\ValueObject\FractorType\FlexFormFractorType;
10+
use a9f\FractorRuleGenerator\ValueObject\FractorType\FluidFractorType;
11+
use a9f\FractorRuleGenerator\ValueObject\FractorType\TypoScriptFractorType;
12+
use a9f\FractorRuleGenerator\ValueObject\FractorType\YamlFractorType;
13+
14+
final class Typo3FractorTypeFactory
15+
{
16+
public static function fromString(string $type): Typo3FractorTypeInterface
17+
{
18+
return match ($type) {
19+
'composer' => new ComposerJsonFractorType(),
20+
'flexform' => new FlexFormFractorType(),
21+
'fluid' => new FluidFractorType(),
22+
'typoscript' => new TypoScriptFractorType(),
23+
'yaml' => new YamlFractorType(),
24+
default => throw new \Exception('Invalid type given'),
25+
};
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\FileSystem;
6+
7+
use a9f\Fractor\Exception\ShouldNotHappenException;
8+
use a9f\FractorRuleGenerator\Factory\TemplateFactory;
9+
use Nette\Utils\Strings;
10+
use Symfony\Component\Filesystem\Filesystem;
11+
12+
final readonly class ConfigFilesystem
13+
{
14+
/**
15+
* @var string[]
16+
*/
17+
private const REQUIRED_KEYS = ['__MajorPrefixed__', '__Type__', '__Name__'];
18+
19+
/**
20+
* @see https://regex101.com/r/gJ0bHJ/1
21+
* @var string
22+
*/
23+
private const LAST_ITEM_REGEX = '#;\n};#';
24+
25+
public function __construct(
26+
private Filesystem $filesystem,
27+
private TemplateFactory $templateFactory
28+
) {
29+
}
30+
31+
/**
32+
* @param array<string, string> $templateVariables
33+
*/
34+
public function addRuleToConfigurationFile(
35+
string $configFilePath,
36+
array $templateVariables,
37+
string $rectorFqnNamePattern
38+
): void {
39+
$this->createConfigurationFileIfNotExists($configFilePath);
40+
41+
$configFileContents = (string) file_get_contents($configFilePath);
42+
43+
$this->ensureRequiredKeysAreSet($templateVariables);
44+
45+
// already added?
46+
$servicesFullyQualifiedName = $this->templateFactory->create($rectorFqnNamePattern, $templateVariables);
47+
if (\str_contains($configFileContents, $servicesFullyQualifiedName)) {
48+
return;
49+
}
50+
51+
$rule = sprintf('$services->set(\\%s::class);', $servicesFullyQualifiedName);
52+
// Add new rule to existing ones or add as first rule of new configuration file.
53+
if (Strings::match($configFileContents, self::LAST_ITEM_REGEX) !== null
54+
&& Strings::match($configFileContents, self::LAST_ITEM_REGEX) !== []
55+
) {
56+
$registerServiceLine = sprintf(';' . PHP_EOL . ' %s' . PHP_EOL . '};', $rule);
57+
$configFileContents = Strings::replace($configFileContents, self::LAST_ITEM_REGEX, $registerServiceLine);
58+
} else {
59+
$configFileContents = str_replace('###FIRST_RULE###', $rule, $configFileContents);
60+
}
61+
62+
// Print the content back to file
63+
$this->filesystem->dumpFile($configFilePath, $configFileContents);
64+
}
65+
66+
/**
67+
* @param array<string, string> $templateVariables
68+
*/
69+
private function ensureRequiredKeysAreSet(array $templateVariables): void
70+
{
71+
$missingKeys = array_diff(self::REQUIRED_KEYS, array_keys($templateVariables));
72+
if ($missingKeys === []) {
73+
return;
74+
}
75+
76+
$message = sprintf('Template variables for "%s" keys are missing', implode('", "', $missingKeys));
77+
throw new ShouldNotHappenException($message);
78+
}
79+
80+
private function createConfigurationFileIfNotExists(string $configFilePath): void
81+
{
82+
if ($this->filesystem->exists($configFilePath)) {
83+
return;
84+
}
85+
86+
$parentDirectory = dirname($configFilePath);
87+
$this->filesystem->mkdir($parentDirectory);
88+
$this->filesystem->touch($configFilePath);
89+
$this->filesystem->appendToFile(
90+
$configFilePath,
91+
(string) file_get_contents(__DIR__ . '/../../templates/config/config.php'),
92+
true
93+
);
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\FileSystem;
6+
7+
use a9f\Fractor\Exception\ShouldNotHappenException;
8+
use a9f\FractorRuleGenerator\Finder\TemplateFinder;
9+
use Nette\Utils\Strings;
10+
use Rector\Testing\PHPUnit\StaticPHPUnitEnvironment;
11+
use Symfony\Component\Filesystem\Filesystem;
12+
use Symfony\Component\Finder\SplFileInfo;
13+
14+
final readonly class TemplateFileSystem
15+
{
16+
/**
17+
* @var string
18+
* @see https://regex101.com/r/fw3jBe/1
19+
*/
20+
private const FIXTURE_SHORT_REGEX = '#/Fixture/#';
21+
22+
public function __construct(
23+
private Filesystem $filesystem
24+
) {
25+
}
26+
27+
/**
28+
* @param string[] $templateVariables
29+
*/
30+
public function resolveDestination(
31+
SplFileInfo $smartFileInfo,
32+
array $templateVariables,
33+
string $targetDirectory
34+
): string {
35+
$destination = $this->getRelativeFilePathFromDirectory($smartFileInfo, TemplateFinder::TEMPLATES_DIRECTORY);
36+
$destination = $this->applyVariables($destination, $templateVariables);
37+
38+
// remove ".inc" protection from PHPUnit if not a test case
39+
if ($this->isNonFixtureFileWithIncSuffix($destination)) {
40+
$destination = Strings::before($destination, '.inc');
41+
}
42+
43+
// special hack for tests, to PHPUnit doesn't load the generated file as test case
44+
/** @var string $destination */
45+
if (\str_ends_with($destination, 'Test.php') && StaticPHPUnitEnvironment::isPHPUnitRun()) {
46+
$destination .= '.inc';
47+
}
48+
49+
return $targetDirectory . DIRECTORY_SEPARATOR . $destination;
50+
}
51+
52+
/**
53+
* @param mixed[] $variables
54+
*/
55+
private function applyVariables(string $content, array $variables): string
56+
{
57+
return str_replace(array_keys($variables), array_values($variables), $content);
58+
}
59+
60+
private function isNonFixtureFileWithIncSuffix(string $filePath): bool
61+
{
62+
if (Strings::match($filePath, self::FIXTURE_SHORT_REGEX) !== null
63+
&& Strings::match($filePath, self::FIXTURE_SHORT_REGEX) !== []
64+
) {
65+
return false;
66+
}
67+
68+
return \str_ends_with($filePath, '.inc');
69+
}
70+
71+
private function getRelativeFilePathFromDirectory(SplFileInfo $fileInfo, string $directory): string
72+
{
73+
if (! file_exists($directory)) {
74+
throw new ShouldNotHappenException(sprintf(
75+
'Directory "%s" was not found in %s.',
76+
$directory,
77+
self::class
78+
));
79+
}
80+
81+
$relativeFilePath = $this->filesystem->makePathRelative(
82+
$this->getNormalizedRealPath($fileInfo),
83+
(string) realpath($directory)
84+
);
85+
return rtrim($relativeFilePath, '/');
86+
}
87+
88+
private function getNormalizedRealPath(SplFileInfo $fileInfo): string
89+
{
90+
return $this->normalizePath($fileInfo->getRealPath());
91+
}
92+
93+
private function normalizePath(string $path): string
94+
{
95+
return str_replace('\\', '/', $path);
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\Finder;
6+
7+
use a9f\Fractor\FileSystem\FileInfoFactory;
8+
use Symfony\Component\Finder\SplFileInfo;
9+
10+
final readonly class TemplateFinder
11+
{
12+
/**
13+
* @var string
14+
*/
15+
public const TEMPLATES_DIRECTORY = __DIR__ . '/../../templates';
16+
17+
public function __construct(
18+
private FileInfoFactory $fileInfoFactory
19+
) {
20+
}
21+
22+
/**
23+
* @return SplFileInfo[]
24+
*/
25+
public function find(string $fixtureFileExtension): array
26+
{
27+
$filePaths = $this->addRuleAndTestCase($fixtureFileExtension);
28+
29+
$smartFileInfos = [];
30+
foreach ($filePaths as $filePath) {
31+
$smartFileInfos[] = $this->fileInfoFactory->createFileInfoFromPath($filePath);
32+
}
33+
34+
return $smartFileInfos;
35+
}
36+
37+
/**
38+
* @return array<int, string>
39+
*/
40+
private function addRuleAndTestCase(string $fixtureFileExtension): array
41+
{
42+
return [
43+
__DIR__ . '/../../templates/rules/TYPO3__MajorPrefixed__/__Type__/__Name__.php',
44+
__DIR__ . '/../../templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/__Name__Test.php.inc',
45+
__DIR__ . '/../../templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/Fixture/fixture.' . $fixtureFileExtension,
46+
__DIR__ . '/../../templates/rules-tests/TYPO3__MajorPrefixed__/__Type__/__Test_Directory__/config/fractor.php.inc',
47+
];
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\Generator;
6+
7+
use a9f\FractorRuleGenerator\Factory\TemplateFactory;
8+
use a9f\FractorRuleGenerator\FileSystem\TemplateFileSystem;
9+
use Symfony\Component\Filesystem\Filesystem;
10+
use Symfony\Component\Finder\SplFileInfo;
11+
12+
final readonly class FileGenerator
13+
{
14+
public function __construct(
15+
private Filesystem $filesystem,
16+
private TemplateFactory $templateFactory,
17+
private TemplateFileSystem $templateFileSystem
18+
) {
19+
}
20+
21+
/**
22+
* @param SplFileInfo[] $templateFileInfos
23+
* @param string[] $templateVariables
24+
* @return string[]
25+
*/
26+
public function generateFiles(
27+
array $templateFileInfos,
28+
array $templateVariables,
29+
string $destinationDirectory
30+
): array {
31+
$generatedFilePaths = [];
32+
33+
foreach ($templateFileInfos as $fileInfo) {
34+
$generatedFilePaths[] = $this->generateFileInfoWithTemplateVariables(
35+
$fileInfo,
36+
$templateVariables,
37+
$destinationDirectory
38+
);
39+
}
40+
41+
return $generatedFilePaths;
42+
}
43+
44+
/**
45+
* @param array<string, mixed> $templateVariables
46+
*/
47+
private function generateFileInfoWithTemplateVariables(
48+
SplFileInfo $smartFileInfo,
49+
array $templateVariables,
50+
string $targetDirectory
51+
): string {
52+
$targetFilePath = $this->templateFileSystem->resolveDestination(
53+
$smartFileInfo,
54+
$templateVariables,
55+
$targetDirectory
56+
);
57+
58+
$content = $this->templateFactory->create($smartFileInfo->getContents(), $templateVariables);
59+
60+
$this->filesystem->dumpFile($targetFilePath, $content);
61+
62+
return $targetFilePath;
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\ValueObject\FractorType;
6+
7+
use a9f\FractorRuleGenerator\Contract\Typo3FractorTypeInterface;
8+
9+
class ComposerJsonFractorType implements Typo3FractorTypeInterface
10+
{
11+
public function __toString(): string
12+
{
13+
return 'composer';
14+
}
15+
16+
public function getFolderName(): string
17+
{
18+
return 'Composer';
19+
}
20+
21+
public function getUseImports(): string
22+
{
23+
return <<<'EOF'
24+
use a9f\FractorComposerJson\Contract\ComposerJson;
25+
use a9f\FractorComposerJson\Contract\ComposerJsonFractorRule;
26+
EOF;
27+
}
28+
29+
public function getExtendsImplements(): string
30+
{
31+
return 'implements ComposerJsonFractorRule';
32+
}
33+
34+
public function getTraits(): string
35+
{
36+
return '';
37+
}
38+
39+
public function getFractorBodyTemplate(): string
40+
{
41+
return <<<'EOF'
42+
public function refactor(ComposerJson $composerJson): void
43+
{
44+
}
45+
46+
public function configure(array $configuration): void
47+
{
48+
}
49+
EOF;
50+
}
51+
52+
public function getFractorFixtureFileExtension(): string
53+
{
54+
return 'json';
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\ValueObject\FractorType;
6+
7+
use a9f\FractorRuleGenerator\Contract\Typo3FractorTypeInterface;
8+
9+
final class FlexFormFractorType implements Typo3FractorTypeInterface
10+
{
11+
public function __toString(): string
12+
{
13+
return 'flexform';
14+
}
15+
16+
public function getFolderName(): string
17+
{
18+
return 'FlexForm';
19+
}
20+
21+
public function getUseImports(): string
22+
{
23+
return <<<'EOF'
24+
use a9f\Typo3Fractor\AbstractFlexformFractor;
25+
use a9f\Typo3Fractor\Helper\FlexFormHelperTrait;
26+
EOF;
27+
}
28+
29+
public function getExtendsImplements(): string
30+
{
31+
return 'extends AbstractFlexformFractor';
32+
}
33+
34+
public function getTraits(): string
35+
{
36+
return <<<'EOF'
37+
38+
use FlexFormHelperTrait;
39+
40+
EOF;
41+
}
42+
43+
public function getFractorBodyTemplate(): string
44+
{
45+
return <<<'EOF'
46+
public function refactor(\DOMNode $node): \DOMNode|int|null
47+
{
48+
}
49+
EOF;
50+
}
51+
52+
public function getFractorFixtureFileExtension(): string
53+
{
54+
return 'xml';
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\ValueObject\FractorType;
6+
7+
use a9f\FractorRuleGenerator\Contract\Typo3FractorTypeInterface;
8+
9+
class FluidFractorType implements Typo3FractorTypeInterface
10+
{
11+
public function __toString(): string
12+
{
13+
return 'fluid';
14+
}
15+
16+
public function getFolderName(): string
17+
{
18+
return 'Fluid';
19+
}
20+
21+
public function getUseImports(): string
22+
{
23+
return <<<'EOF'
24+
use a9f\FractorFluid\Contract\FluidFractorRule;
25+
EOF;
26+
}
27+
28+
public function getExtendsImplements(): string
29+
{
30+
return 'implements FluidFractorRule';
31+
}
32+
33+
public function getTraits(): string
34+
{
35+
return '';
36+
}
37+
38+
public function getFractorBodyTemplate(): string
39+
{
40+
return <<<'EOF'
41+
public function refactor(string $fluid): string
42+
{
43+
return 'TODO';
44+
}
45+
EOF;
46+
}
47+
48+
public function getFractorFixtureFileExtension(): string
49+
{
50+
return 'html';
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\ValueObject\FractorType;
6+
7+
use a9f\FractorRuleGenerator\Contract\Typo3FractorTypeInterface;
8+
9+
final class TypoScriptFractorType implements Typo3FractorTypeInterface
10+
{
11+
public function __toString(): string
12+
{
13+
return 'typoscript';
14+
}
15+
16+
public function getFolderName(): string
17+
{
18+
return 'TypoScript';
19+
}
20+
21+
public function getUseImports(): string
22+
{
23+
return <<<'EOF'
24+
use a9f\FractorTypoScript\AbstractTypoScriptFractor;
25+
use Helmich\TypoScriptParser\Parser\AST\Statement;
26+
EOF;
27+
}
28+
29+
public function getExtendsImplements(): string
30+
{
31+
return 'extends AbstractTypoScriptFractor';
32+
}
33+
34+
public function getTraits(): string
35+
{
36+
return '';
37+
}
38+
39+
public function getFractorBodyTemplate(): string
40+
{
41+
return <<<'EOF'
42+
public function refactor(Statement $statement): null|Statement|int
43+
{
44+
return $statement;
45+
}
46+
EOF;
47+
}
48+
49+
public function getFractorFixtureFileExtension(): string
50+
{
51+
return 'typoscript';
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\ValueObject\FractorType;
6+
7+
use a9f\FractorRuleGenerator\Contract\Typo3FractorTypeInterface;
8+
9+
class YamlFractorType implements Typo3FractorTypeInterface
10+
{
11+
public function __toString(): string
12+
{
13+
return 'yaml';
14+
}
15+
16+
public function getFolderName(): string
17+
{
18+
return 'Yaml';
19+
}
20+
21+
public function getUseImports(): string
22+
{
23+
return <<<'EOF'
24+
use a9f\FractorYaml\Contract\YamlFractorRule;
25+
EOF;
26+
}
27+
28+
public function getExtendsImplements(): string
29+
{
30+
return 'implements YamlFractorRule';
31+
}
32+
33+
public function getTraits(): string
34+
{
35+
return '';
36+
}
37+
38+
public function getFractorBodyTemplate(): string
39+
{
40+
return <<<'EOF'
41+
public function refactor(array $yaml): array
42+
{
43+
return $yaml;
44+
}
45+
EOF;
46+
}
47+
48+
public function getFractorFixtureFileExtension(): string
49+
{
50+
return 'yaml';
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\ValueObject;
6+
7+
use a9f\FractorRuleGenerator\Contract\Typo3FractorTypeInterface;
8+
9+
final readonly class Typo3FractorRecipe
10+
{
11+
public function __construct(
12+
private Typo3Version $typo3Version,
13+
private string $url,
14+
private string $name,
15+
private string $description,
16+
private Typo3FractorTypeInterface $type
17+
) {
18+
}
19+
20+
public function getChangelogAnnotation(): string
21+
{
22+
if ($this->url === '') {
23+
return '';
24+
}
25+
26+
$url = $this->url;
27+
return <<<EOF
28+
29+
* @changelog {$url}
30+
EOF;
31+
}
32+
33+
public function getMajorVersionPrefixed(): string
34+
{
35+
return sprintf('v%d', $this->typo3Version->getMajor());
36+
}
37+
38+
public function getMajorVersion(): string
39+
{
40+
return (string) $this->typo3Version->getMajor();
41+
}
42+
43+
public function getMinorVersionPrefixed(): string
44+
{
45+
return sprintf('v%d', $this->typo3Version->getMinor());
46+
}
47+
48+
public function getDescription(): string
49+
{
50+
return $this->description;
51+
}
52+
53+
public function getFractorName(): string
54+
{
55+
return $this->name . 'Fractor';
56+
}
57+
58+
public function getTestDirectory(): string
59+
{
60+
return $this->name . 'Fractor';
61+
}
62+
63+
public function getSet(): string
64+
{
65+
return sprintf(__DIR__ . '/../../../typo3-fractor/config/typo3-%d.php', $this->getMajorVersion());
66+
}
67+
68+
public function getUseImports(): string
69+
{
70+
$useImports = '';
71+
if ($this->url === '') {
72+
$useImports .= <<<EOF
73+
use a9f\Fractor\Contract\NoChangelogRequired;
74+
75+
EOF;
76+
}
77+
return $useImports . $this->type->getUseImports();
78+
}
79+
80+
public function getTraits(): string
81+
{
82+
return $this->type->getTraits();
83+
}
84+
85+
public function getExtendsImplements(): string
86+
{
87+
$extendsImplements = $this->type->getExtendsImplements();
88+
if ($this->url === '') {
89+
if (str_contains($extendsImplements, 'implements')) {
90+
$extendsImplements .= ', NoChangelogRequired';
91+
} else {
92+
$extendsImplements .= ' implements NoChangelogRequired';
93+
}
94+
}
95+
return $extendsImplements;
96+
}
97+
98+
public function getFractorBodyTemplate(): string
99+
{
100+
return $this->type->getFractorBodyTemplate();
101+
}
102+
103+
public function getFractorTypeFolderName(): string
104+
{
105+
return $this->type->getFolderName();
106+
}
107+
108+
public function getFractorFixtureFileExtension(): string
109+
{
110+
return $this->type->getFractorFixtureFileExtension();
111+
}
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\ValueObject;
6+
7+
final readonly class Typo3Version
8+
{
9+
private function __construct(
10+
private int $major,
11+
private int $minor
12+
) {
13+
}
14+
15+
public function getMajor(): int
16+
{
17+
return $this->major;
18+
}
19+
20+
public function getMinor(): int
21+
{
22+
return $this->minor;
23+
}
24+
25+
public static function createFromString(string $version): self
26+
{
27+
if (! str_contains($version, '.')) {
28+
$version .= '.0';
29+
}
30+
31+
[$major, $minor] = explode('.', $version, 2);
32+
33+
return new self((int) $major, (int) $minor);
34+
}
35+
36+
public function getFullVersion(): string
37+
{
38+
return sprintf('%d%d', $this->major, $this->minor);
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
7+
return static function (ContainerConfigurator $containerConfigurator): void {
8+
$services = $containerConfigurator->services();
9+
$services->defaults()
10+
->autoconfigure()
11+
->autowire();
12+
13+
###FIRST_RULE###
14+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div>
2+
<!-- stuff happens here -->
3+
</div>
4+
-----
5+
<div>
6+
<!-- stuff happens here -->
7+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"key": "value"
3+
}
4+
-----
5+
{
6+
"key": "value"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
config {
2+
a = 1
3+
}
4+
-----
5+
config {
6+
a = 1
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<T3DataStructure>
3+
<sheets>
4+
<sDEF>
5+
<ROOT>
6+
<sheetTitle>sheetTitle</sheetTitle>
7+
<type>array</type>
8+
<el>
9+
<aColumn>
10+
<config>
11+
<type>input</type>
12+
</config>
13+
</aColumn>
14+
</el>
15+
</ROOT>
16+
</sDEF>
17+
</sheets>
18+
</T3DataStructure>
19+
-----
20+
<?xml version="1.0" encoding="UTF-8"?>
21+
<T3DataStructure>
22+
<sheets>
23+
<sDEF>
24+
<ROOT>
25+
<sheetTitle>sheetTitle</sheetTitle>
26+
<type>array</type>
27+
<el>
28+
<aColumn>
29+
<config>
30+
<type>input</type>
31+
</config>
32+
</aColumn>
33+
</el>
34+
</ROOT>
35+
</sDEF>
36+
</sheets>
37+
</T3DataStructure>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
TYPO3:
2+
CMS:
3+
Form:
4+
prototypes:
5+
standard:
6+
formElementsDefinition:
7+
Form:
8+
-----
9+
TYPO3:
10+
CMS:
11+
Form:
12+
prototypes:
13+
standard:
14+
formElementsDefinition:
15+
Form:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\Typo3Fractor\Tests\TYPO3__MajorPrefixed__\__Type__\__Test_Directory__;
6+
7+
use a9f\Fractor\Testing\PHPUnit\AbstractFractorTestCase;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
10+
final class __Name__Test extends AbstractFractorTestCase
11+
{
12+
#[DataProvider('provideData')]
13+
public function test(string $filePath): void
14+
{
15+
$this->doTestFile($filePath);
16+
}
17+
18+
public static function provideData(): \Iterator
19+
{
20+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixtures', '*.__FixtureFileExtension__');
21+
}
22+
23+
public function provideConfigFilePath(): ?string
24+
{
25+
return __DIR__ . '/config/fractor.php';
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use a9f\Fractor\Configuration\FractorConfiguration;
6+
use a9f\Fractor\ValueObject\Indent;
7+
use a9f\FractorXml\Configuration\XmlProcessorOption;
8+
use a9f\Typo3Fractor\TYPO3__MajorPrefixed__\__Type__\__Name__;
9+
10+
return FractorConfiguration::configure()
11+
->withOptions([
12+
XmlProcessorOption::INDENT_CHARACTER => Indent::STYLE_TAB,
13+
XmlProcessorOption::INDENT_SIZE => 1,
14+
])
15+
->withRules([__Name__::class]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\Typo3Fractor\TYPO3__MajorPrefixed__\__Type__;
6+
7+
__Use__
8+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
9+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
10+
11+
/**__Changelog_Annotation__
12+
* @see \a9f\Typo3Fractor\Tests\TYPO3__MajorPrefixed__\__Type__\__Test_Directory__\__Name__Test
13+
*/
14+
final class __Name__ __ExtendsImplements__
15+
{__Traits__
16+
public function getRuleDefinition(): RuleDefinition
17+
{
18+
return new RuleDefinition('__Description__', [new CodeSample(
19+
<<<'CODE_SAMPLE'
20+
CODE_SAMPLE
21+
,
22+
<<<'CODE_SAMPLE'
23+
CODE_SAMPLE
24+
)]);
25+
}
26+
27+
__Base_Fractor_Body_Template__
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorRuleGenerator\Tests\Functional\Console\Command;
6+
7+
use a9f\Fractor\FileSystem\FileInfoFactory;
8+
use a9f\FractorRuleGenerator\Console\Command\GenerateRuleCommand;
9+
use a9f\FractorRuleGenerator\Factory\TemplateFactory;
10+
use a9f\FractorRuleGenerator\FileSystem\ConfigFilesystem;
11+
use a9f\FractorRuleGenerator\FileSystem\TemplateFileSystem;
12+
use a9f\FractorRuleGenerator\Finder\TemplateFinder;
13+
use a9f\FractorRuleGenerator\Generator\FileGenerator;
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\Application;
16+
use Symfony\Component\Console\Output\ConsoleOutput;
17+
use Symfony\Component\Console\Tester\CommandTester;
18+
use Symfony\Component\Filesystem\Filesystem;
19+
20+
final class GenerateRuleCommandTest extends TestCase
21+
{
22+
/**
23+
* @var array<int, string>
24+
*/
25+
private array $testFilesToDelete = [];
26+
27+
/**
28+
* @var array<int, string>
29+
*/
30+
private array $testDirsToDelete = [];
31+
32+
private CommandTester $commandTester;
33+
34+
protected function setUp(): void
35+
{
36+
parent::setUp();
37+
38+
$fileSystem = new Filesystem();
39+
$templateFactory = new TemplateFactory();
40+
$templateFileSystem = new TemplateFileSystem($fileSystem);
41+
42+
$fileInfoFactory = new FileInfoFactory($fileSystem);
43+
$templateFinder = new TemplateFinder($fileInfoFactory);
44+
$fileGenerator = new FileGenerator($fileSystem, $templateFactory, $templateFileSystem);
45+
46+
$outputStyle = new ConsoleOutput();
47+
48+
$configFilesystem = new ConfigFilesystem($fileSystem, $templateFactory);
49+
50+
$createdCommand = new GenerateRuleCommand(
51+
$templateFinder,
52+
$fileGenerator,
53+
$outputStyle,
54+
$configFilesystem,
55+
$fileInfoFactory
56+
);
57+
58+
$application = new Application();
59+
$application->add($createdCommand);
60+
61+
$foundCommand = $application->find('generate-rule');
62+
63+
$this->commandTester = new CommandTester($foundCommand);
64+
}
65+
66+
/**
67+
* Tear down for remove of the test files
68+
*/
69+
protected function tearDown(): void
70+
{
71+
foreach ($this->testFilesToDelete as $absoluteFileName) {
72+
if (@is_file($absoluteFileName)) {
73+
unlink($absoluteFileName);
74+
}
75+
}
76+
foreach ($this->testDirsToDelete as $absoluteDirName) {
77+
if (@is_dir($absoluteDirName)) {
78+
self::rmdir($absoluteDirName, true);
79+
}
80+
}
81+
parent::tearDown();
82+
}
83+
84+
public function testCreateRuleForFlexForm(): void
85+
{
86+
$this->commandTester->setInputs(['7', 'x', 'MigrateFlexForm', 'Migrate FlexForm field', '0']);
87+
88+
$this->commandTester->execute([
89+
'command' => 'generate-rule',
90+
]);
91+
92+
self::assertSame(0, $this->commandTester->getStatusCode());
93+
94+
$basePathConfig = __DIR__ . '/../../../../../typo3-fractor/config';
95+
$basePathRules = __DIR__ . '/../../../../../typo3-fractor/rules';
96+
$basePathRuleTests = __DIR__ . '/../../../../../typo3-fractor/rules-tests';
97+
98+
$this->testFilesToDelete[] = $basePathConfig . '/typo3-7.php';
99+
self::assertFileExists($basePathConfig . '/typo3-7.php');
100+
self::assertFileExists($basePathRules . '/TYPO3v7/FlexForm/MigrateFlexFormFractor.php');
101+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/FlexForm/MigrateFlexFormFractor/config/fractor.php');
102+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/FlexForm/MigrateFlexFormFractor/Fixture/fixture.xml');
103+
self::assertFileExists(
104+
$basePathRuleTests . '/TYPO3v7/FlexForm/MigrateFlexFormFractor/MigrateFlexFormFractorTest.php.inc'
105+
);
106+
107+
$this->testDirsToDelete[] = $basePathRules . '/TYPO3v7';
108+
$this->testDirsToDelete[] = $basePathRuleTests . '/TYPO3v7';
109+
}
110+
111+
public function testCreateRuleForFluid(): void
112+
{
113+
$this->commandTester->setInputs(['7', 'x', 'MigrateFluid', 'Migrate Fluid field', '1']);
114+
115+
$this->commandTester->execute([
116+
'command' => 'generate-rule',
117+
]);
118+
119+
self::assertSame(0, $this->commandTester->getStatusCode());
120+
121+
$basePathConfig = __DIR__ . '/../../../../../typo3-fractor/config';
122+
$basePathRules = __DIR__ . '/../../../../../typo3-fractor/rules';
123+
$basePathRuleTests = __DIR__ . '/../../../../../typo3-fractor/rules-tests';
124+
125+
$this->testFilesToDelete[] = $basePathConfig . '/typo3-7.php';
126+
self::assertFileExists($basePathConfig . '/typo3-7.php');
127+
self::assertFileExists($basePathRules . '/TYPO3v7/Fluid/MigrateFluidFractor.php');
128+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/Fluid/MigrateFluidFractor/config/fractor.php');
129+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/Fluid/MigrateFluidFractor/Fixture/fixture.html');
130+
self::assertFileExists(
131+
$basePathRuleTests . '/TYPO3v7/Fluid/MigrateFluidFractor/MigrateFluidFractorTest.php.inc'
132+
);
133+
134+
$this->testDirsToDelete[] = $basePathRules . '/TYPO3v7';
135+
$this->testDirsToDelete[] = $basePathRuleTests . '/TYPO3v7';
136+
}
137+
138+
public function testCreateRuleForTypoScript(): void
139+
{
140+
$this->commandTester->setInputs(['7', 'x', 'MigrateTypoScript', 'Migrate TypoScript setting', '2']);
141+
142+
$this->commandTester->execute([
143+
'command' => 'generate-rule',
144+
]);
145+
146+
self::assertSame(0, $this->commandTester->getStatusCode());
147+
148+
$basePathConfig = __DIR__ . '/../../../../../typo3-fractor/config';
149+
$basePathRules = __DIR__ . '/../../../../../typo3-fractor/rules';
150+
$basePathRuleTests = __DIR__ . '/../../../../../typo3-fractor/rules-tests';
151+
152+
$this->testFilesToDelete[] = $basePathConfig . '/typo3-7.php';
153+
self::assertFileExists($basePathConfig . '/typo3-7.php');
154+
self::assertFileExists($basePathRules . '/TYPO3v7/TypoScript/MigrateTypoScriptFractor.php');
155+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/TypoScript/MigrateTypoScriptFractor/config/fractor.php');
156+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/TypoScript/MigrateTypoScriptFractor/Fixture/fixture.typoscript');
157+
self::assertFileExists(
158+
$basePathRuleTests . '/TYPO3v7/TypoScript/MigrateTypoScriptFractor/MigrateTypoScriptFractorTest.php.inc'
159+
);
160+
161+
$this->testDirsToDelete[] = $basePathRules . '/TYPO3v7';
162+
$this->testDirsToDelete[] = $basePathRuleTests . '/TYPO3v7';
163+
}
164+
165+
public function testCreateRuleForYaml(): void
166+
{
167+
$this->commandTester->setInputs(['7', 'x', 'MigrateYaml', 'Migrate Yaml setting', '3']);
168+
169+
$this->commandTester->execute([
170+
'command' => 'generate-rule',
171+
]);
172+
173+
self::assertSame(0, $this->commandTester->getStatusCode());
174+
175+
$basePathConfig = __DIR__ . '/../../../../../typo3-fractor/config';
176+
$basePathRules = __DIR__ . '/../../../../../typo3-fractor/rules';
177+
$basePathRuleTests = __DIR__ . '/../../../../../typo3-fractor/rules-tests';
178+
179+
$this->testFilesToDelete[] = $basePathConfig . '/typo3-7.php';
180+
self::assertFileExists($basePathConfig . '/typo3-7.php');
181+
self::assertFileExists($basePathRules . '/TYPO3v7/Yaml/MigrateYamlFractor.php');
182+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/Yaml/MigrateYamlFractor/config/fractor.php');
183+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/Yaml/MigrateYamlFractor/Fixture/fixture.yaml');
184+
self::assertFileExists(
185+
$basePathRuleTests . '/TYPO3v7/Yaml/MigrateYamlFractor/MigrateYamlFractorTest.php.inc'
186+
);
187+
188+
$this->testDirsToDelete[] = $basePathRules . '/TYPO3v7';
189+
$this->testDirsToDelete[] = $basePathRuleTests . '/TYPO3v7';
190+
}
191+
192+
public function testCreateRuleForComposer(): void
193+
{
194+
$this->commandTester->setInputs(['7', 'x', 'MigrateComposer', 'Migrate Composer setting', '4']);
195+
196+
$this->commandTester->execute([
197+
'command' => 'generate-rule',
198+
]);
199+
200+
self::assertSame(0, $this->commandTester->getStatusCode());
201+
202+
$basePathConfig = __DIR__ . '/../../../../../typo3-fractor/config';
203+
$basePathRules = __DIR__ . '/../../../../../typo3-fractor/rules';
204+
$basePathRuleTests = __DIR__ . '/../../../../../typo3-fractor/rules-tests';
205+
206+
$this->testFilesToDelete[] = $basePathConfig . '/typo3-7.php';
207+
self::assertFileExists($basePathConfig . '/typo3-7.php');
208+
self::assertFileExists($basePathRules . '/TYPO3v7/Composer/MigrateComposerFractor.php');
209+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/Composer/MigrateComposerFractor/config/fractor.php');
210+
self::assertFileExists($basePathRuleTests . '/TYPO3v7/Composer/MigrateComposerFractor/Fixture/fixture.json');
211+
self::assertFileExists(
212+
$basePathRuleTests . '/TYPO3v7/Composer/MigrateComposerFractor/MigrateComposerFractorTest.php.inc'
213+
);
214+
215+
$this->testDirsToDelete[] = $basePathRules . '/TYPO3v7';
216+
$this->testDirsToDelete[] = $basePathRuleTests . '/TYPO3v7';
217+
}
218+
219+
/**
220+
* Wrapper function for rmdir, allowing recursive deletion of folders and files
221+
*
222+
* @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
223+
* @param bool $removeNonEmpty Allow deletion of non-empty directories
224+
* @return bool TRUE if operation was successful
225+
* @source TYPO3: \TYPO3\CMS\Core\Utility\GeneralUtility::rmdir
226+
*/
227+
private static function rmdir(string $path, bool $removeNonEmpty = false): bool
228+
{
229+
$OK = false;
230+
// Remove trailing slash
231+
$path = preg_replace('|/$|', '', $path) ?? '';
232+
$isWindows = DIRECTORY_SEPARATOR === '\\';
233+
if (file_exists($path)) {
234+
$OK = true;
235+
if (! is_link($path) && is_dir($path)) {
236+
if ($removeNonEmpty === true && ($handle = @opendir($path))) {
237+
$entries = [];
238+
239+
while (false !== ($file = readdir($handle))) {
240+
if ($file === '.') {
241+
continue;
242+
}
243+
if ($file === '..') {
244+
continue;
245+
}
246+
$entries[] = $path . '/' . $file;
247+
}
248+
249+
closedir($handle);
250+
251+
foreach ($entries as $entry) {
252+
if (! static::rmdir($entry, true)) {
253+
$OK = false;
254+
}
255+
}
256+
}
257+
if ($OK) {
258+
$OK = @rmdir($path);
259+
}
260+
} elseif (is_link($path) && is_dir($path) && $isWindows) {
261+
$OK = @rmdir($path);
262+
} else {
263+
// If $path is a file, simply remove it
264+
$OK = @unlink($path);
265+
}
266+
clearstatcache();
267+
} elseif (is_link($path)) {
268+
$OK = @unlink($path);
269+
if (! $OK && $isWindows) {
270+
// Try to delete dead folder links on Windows systems
271+
$OK = @rmdir($path);
272+
}
273+
clearstatcache();
274+
}
275+
return $OK;
276+
}
277+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\Fractor\FileSystem;
6+
7+
use a9f\Fractor\Exception\ShouldNotHappenException;
8+
use Symfony\Component\Filesystem\Filesystem;
9+
use Symfony\Component\Finder\SplFileInfo;
10+
11+
readonly class FileInfoFactory
12+
{
13+
public function __construct(
14+
private Filesystem $filesystem
15+
) {
16+
}
17+
18+
public function createFileInfoFromPath(string $filePath): SplFileInfo
19+
{
20+
$currentWorkingDirectory = getcwd();
21+
if ($currentWorkingDirectory === false) {
22+
throw new ShouldNotHappenException('Could not get current working directory');
23+
}
24+
25+
$realPath = realpath($filePath);
26+
27+
if ($realPath === false) {
28+
throw new ShouldNotHappenException(sprintf('Could not get realpath for file "%s"', $filePath));
29+
}
30+
31+
$relativeFilePath = rtrim($this->filesystem->makePathRelative($realPath, $currentWorkingDirectory), '/');
32+
$relativeDirectoryPath = dirname($relativeFilePath);
33+
34+
return new SplFileInfo($filePath, $relativeDirectoryPath, $relativeFilePath);
35+
}
36+
}

‎phpstan.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ parameters:
88
paths:
99
- src/
1010
- packages/
11-
- ecs.php
1211
- rector.php
1312
excludePaths:
1413
- packages/extension-installer/generated
1514
- packages/**/tests/**/Fixtures/*
1615
- packages/**/tests/**/Fixture/*
1716
- packages/**/tests/Fixtures/*
17+
- packages/fractor-rule-generator/templates

‎rector.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
->withPhpSets(php82: true)
99
->withPreparedSets(deadCode: true, typeDeclarations: true, earlyReturn: true, strictBooleans: true)
1010
->withImportNames(true, true, false, true)
11-
->withSkip([__DIR__ . '/packages/extension-installer/generated'])
11+
->withSkip([
12+
__DIR__ . '/packages/extension-installer/generated',
13+
__DIR__ . '/packages/fractor-rule-generator/templates',
14+
])
1215
->withPaths([
1316
__DIR__ . '/ecs.php',
1417
__DIR__ . '/packages',

0 commit comments

Comments
 (0)
Please sign in to comment.