Skip to content

Commit

Permalink
[FEATURE] Add Rule generator
Browse files Browse the repository at this point in the history
Resolves: #108
  • Loading branch information
simonschaufi committed Sep 5, 2024
1 parent 068253d commit f0c1af5
Show file tree
Hide file tree
Showing 34 changed files with 1,433 additions and 3 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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/",
Expand All @@ -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/",
Expand Down
5 changes: 4 additions & 1 deletion ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
3 changes: 3 additions & 0 deletions packages/fractor-rule-generator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/vendor/
/composer.lock
.phpunit.cache
49 changes: 49 additions & 0 deletions packages/fractor-rule-generator/composer.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
22 changes: 22 additions & 0 deletions packages/fractor-rule-generator/config/application.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void {
$services = $containerConfigurator->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);
};
13 changes: 13 additions & 0 deletions packages/fractor-rule-generator/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<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">
<testsuites>
<testsuite name="fractor-rule-generator">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>./src</directory>
</include>
</source>
</phpunit>
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<?php

declare(strict_types=1);

namespace a9f\FractorRuleGenerator\Console\Command;

use a9f\Fractor\Exception\ShouldNotHappenException;
use a9f\Fractor\FileSystem\FileInfoFactory;
use a9f\FractorRuleGenerator\Factory\Typo3FractorTypeFactory;
use a9f\FractorRuleGenerator\FileSystem\ConfigFilesystem;
use a9f\FractorRuleGenerator\Finder\TemplateFinder;
use a9f\FractorRuleGenerator\Generator\FileGenerator;
use a9f\FractorRuleGenerator\ValueObject\Typo3FractorRecipe;
use a9f\FractorRuleGenerator\ValueObject\Typo3Version;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Webmozart\Assert\Assert;

#[AsCommand(name: 'generate-rule', description: 'Generate a new Fractor rule in a proper location, with tests')]
final class GenerateRuleCommand extends Command
{
/**
* @var string
*/
private const FRACTOR_FQN_NAME_PATTERN = 'a9f\Typo3Fractor\TYPO3__MajorPrefixed__\__Type__\__Name__';

public function __construct(
private readonly TemplateFinder $templateFinder,
private readonly FileGenerator $fileGenerator,
private readonly OutputInterface $outputStyle,
private readonly ConfigFilesystem $configFilesystem,
private readonly FileInfoFactory $fileInfoFactory
) {
parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var QuestionHelper $helper */
$helper = $this->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('<info>New files generated for "%s":</info>', $name);
$this->outputStyle->writeln($message);

sort($generatedFilePaths);

foreach ($generatedFilePaths as $generatedFilePath) {
$fileInfo = $this->fileInfoFactory->createFileInfoFromPath($generatedFilePath);
$this->outputStyle->writeln(' * ' . $fileInfo->getRelativePathname());
}

$message = sprintf(
'<info>Run tests for this fractor:</info>%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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace a9f\FractorRuleGenerator\Contract;

interface Typo3FractorTypeInterface extends \Stringable
{
public function getFolderName(): string;

public function getUseImports(): string;

public function getExtendsImplements(): string;

public function getTraits(): string;

public function getFractorFixtureFileExtension(): string;

public function getFractorBodyTemplate(): string;
}
Loading

0 comments on commit f0c1af5

Please sign in to comment.