diff --git a/generator/src/Commands/GenerateCommand.php b/generator/src/Commands/GenerateCommand.php index 0e7efba7..0ce02359 100644 --- a/generator/src/Commands/GenerateCommand.php +++ b/generator/src/Commands/GenerateCommand.php @@ -83,7 +83,7 @@ protected function execute( $modules[$function->getModuleName()] = true; } - $genDir = FileCreator::getSafeRootDir() . "/generated/$version"; + $genDir = Filesystem::outputDir() . "/$version"; $fileCreator = new FileCreator(); $fileCreator->generatePhpFile($res->methods, "$genDir/"); $fileCreator->generateFunctionsList($res->methods, "$genDir/functionsList.php"); @@ -91,10 +91,11 @@ protected function execute( } foreach (\array_keys($modules) as $moduleName) { - $fileCreator->generateVersionSplitters($moduleName, FileCreator::getSafeRootDir() . "/generated/", \array_keys($versions)); + $fileCreator->generateVersionSplitters($moduleName, Filesystem::outputDir() . "/", \array_keys($versions)); $fileCreator->createExceptionFile((string) $moduleName); } - $fileCreator->generateVersionSplitters("functionsList", FileCreator::getSafeRootDir() . "/generated/", \array_keys($versions), true); + + $fileCreator->generateVersionSplitters("functionsList", Filesystem::outputDir() . "/", \array_keys($versions), true); $this->runCsFix($output); diff --git a/generator/src/Generator/ComposerJsonEditor.php b/generator/src/Generator/ComposerJsonEditor.php index 94ee9a04..7721bd07 100644 --- a/generator/src/Generator/ComposerJsonEditor.php +++ b/generator/src/Generator/ComposerJsonEditor.php @@ -4,6 +4,8 @@ namespace Safe\Generator; +use Safe\Templating\Filesystem; + /** * This class will edit the main composer.json file to add the list of files generated from modules. */ @@ -15,7 +17,7 @@ class ComposerJsonEditor public static function editComposerFileForGeneration(array $modules): void { - $composerContent = file_get_contents(FileCreator::getSafeRootDir() . '/composer.json'); + $composerContent = file_get_contents(Filesystem::projectRootDir() . '/composer.json'); if ($composerContent === false) { throw new \RuntimeException('Error while loading composer.json file for edition.'); } @@ -24,7 +26,7 @@ public static function editComposerFileForGeneration(array $modules): void $composerJson['autoload']['files'] = self::editFilesListForGeneration($composerJson['autoload']['files'], $modules); $newContent = \json_encode($composerJson, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES) . "\n"; - \file_put_contents(FileCreator::getSafeRootDir() . '/composer.json', $newContent); + \file_put_contents(Filesystem::projectRootDir() . '/composer.json', $newContent); } /** diff --git a/generator/src/Generator/FileCreator.php b/generator/src/Generator/FileCreator.php index 47935d0a..c5c8da27 100644 --- a/generator/src/Generator/FileCreator.php +++ b/generator/src/Generator/FileCreator.php @@ -4,6 +4,8 @@ namespace Safe\Generator; +use Safe\Templating\Engine; +use Safe\Templating\Filesystem; use Safe\XmlDocParser\ErrorType; use Safe\XmlDocParser\Scanner; use Safe\XmlDocParser\Method; @@ -13,6 +15,12 @@ class FileCreator { + private Engine $engine; + + public function __construct(?Engine $engine = null) + { + $this->engine = $engine ?? new Engine(); + } /** * This function generate an improved php lib function in a php file @@ -34,26 +42,14 @@ public function generatePhpFile( foreach ($phpFunctionsByModule as $module => $phpFunctions) { $lcModule = \lcfirst($module); - if (!is_dir($path)) { + if (!\is_dir($path)) { \mkdir($path); } - $stream = \fopen($path.$lcModule.'.php', 'w'); - if ($stream === false) { - throw new \RuntimeException('Unable to write to '.$path); - } - - // Write file header - \fwrite($stream, "<?php\n -namespace Safe; - -use Safe\\Exceptions\\".self::toExceptionName($module). ';'); - - // Write safe wrappers for non-safe functions - foreach ($phpFunctions as $phpFunction) { - \fwrite($stream, "\n".$phpFunction); - } - \fclose($stream); + Filesystem::dumpFile($path . $lcModule . '.php', $this->engine->generate('Module.php.tpl', [ + '{{exceptionName}}' => self::toExceptionName($module), + '{{functions}}' => \implode(PHP_EOL, $phpFunctions), + ])); } } @@ -107,18 +103,9 @@ private function getFunctionsNameList(array $functions): array */ public function generateFunctionsList(array $functions, string $path): void { - $functionNames = $this->getFunctionsNameList($functions); - $stream = fopen($path, 'w'); - if ($stream === false) { - throw new \RuntimeException('Unable to write to '.$path); - } - fwrite($stream, "<?php\n -return [\n"); - foreach ($functionNames as $functionName) { - fwrite($stream, ' '.\var_export($functionName, true).",\n"); - } - fwrite($stream, "];\n"); - fclose($stream); + Filesystem::dumpFile($path, $this->engine->generate('FunctionList.php.tpl', [ + '{{functionNames}}' => \implode(PHP_EOL, \array_map(static fn(string $name): string => \sprintf('\'%s\',', $name), $this->getFunctionsNameList($functions))), + ])); } /** @@ -128,66 +115,29 @@ public function generateFunctionsList(array $functions, string $path): void */ public function generateRectorFile(array $functions, string $path): void { - $functionNames = $this->getFunctionsNameList($functions); - - $stream = fopen($path, 'w'); - - if ($stream === false) { - throw new \RuntimeException('Unable to write to '.$path); - } - - $header = <<<'TXT' -<?php - -declare(strict_types=1); - -use Rector\Config\RectorConfig; -use Rector\Renaming\Rector\FuncCall\RenameFunctionRector; - -// This file configures rector/rector to replace all PHP functions with their equivalent "safe" functions. -return static function (RectorConfig $rectorConfig): void { - $rectorConfig->ruleWithConfiguration( - RenameFunctionRector::class, - [ -TXT; - - fwrite($stream, $header); - - foreach ($functionNames as $functionName) { - fwrite($stream, " '$functionName' => 'Safe\\$functionName',\n"); - } - - fwrite($stream, " ]\n );\n};\n"); - fclose($stream); + Filesystem::dumpFile($path, $this->engine->generate('RectorConfig.php.tpl', [ + '{{functionNames}}' => \implode(PHP_EOL, \array_map(static fn(string $name): string => \sprintf('\'%1$s\' => \'Safe\\%1$s\',', $name), $this->getFunctionsNameList($functions))), + ])); } public function createExceptionFile(string $moduleName): void { $exceptionName = self::toExceptionName($moduleName); - if (!file_exists(FileCreator::getSafeRootDir() . '/lib/Exceptions/'.$exceptionName.'.php')) { - \file_put_contents( - FileCreator::getSafeRootDir() . '/generated/Exceptions/'.$exceptionName.'.php', - <<<EOF -<?php -namespace Safe\Exceptions; -class {$exceptionName} extends \ErrorException implements SafeExceptionInterface -{ - public static function createFromPhpError(): self - { - \$error = error_get_last(); - return new self(\$error['message'] ?? 'An error occurred', 0, \$error['type'] ?? 1); - } -} - -EOF - ); - } + Filesystem::dumpFile(Filesystem::outputDir() . '/Exceptions/'.$exceptionName.'.php', $this->engine->generate('Exception.php.tpl', [ + '{{exceptionName}}' => $exceptionName, + ])); } public static function getSafeRootDir(): string { - return __DIR__ . '/../../..'; + $path = realpath(__DIR__ . '/../../..'); + + if (false === $path) { + throw new \RuntimeException('Unable to locate root directory'); + } + + return $path; } /** diff --git a/generator/src/PhpStanFunctions/PhpStanFunctionMapReader.php b/generator/src/PhpStanFunctions/PhpStanFunctionMapReader.php index f4471b07..9882614f 100644 --- a/generator/src/PhpStanFunctions/PhpStanFunctionMapReader.php +++ b/generator/src/PhpStanFunctions/PhpStanFunctionMapReader.php @@ -5,6 +5,7 @@ namespace Safe\PhpStanFunctions; use Safe\Generator\FileCreator; +use Safe\Templating\Filesystem; class PhpStanFunctionMapReader { @@ -20,8 +21,8 @@ class PhpStanFunctionMapReader public function __construct() { - $this->functionMap = require 'phar://' . FileCreator::getSafeRootDir() . '/generator/vendor/phpstan/phpstan/phpstan.phar/resources/functionMap.php'; - $this->customFunctionMap = require FileCreator::getSafeRootDir() . '/generator/config/CustomPhpStanFunctionMap.php'; + $this->functionMap = require 'phar://' . Filesystem::generatorDir() . '/vendor/phpstan/phpstan/phpstan.phar/resources/functionMap.php'; + $this->customFunctionMap = require Filesystem::generatorDir() . '/config/CustomPhpStanFunctionMap.php'; } public function getFunction(string $functionName): ?PhpStanFunction diff --git a/generator/src/Templating/Engine.php b/generator/src/Templating/Engine.php new file mode 100644 index 00000000..f990f755 --- /dev/null +++ b/generator/src/Templating/Engine.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace Safe\Templating; + +use Safe\Templating\Exception\InvalidPlaceholderException; +use Safe\Templating\Exception\TemplateNotFoundException; +use Safe\Templating\Exception\TemplatingException; +use Safe\Templating\Exception\UnreadableTemplateException; +use Symfony\Component\Finder\Finder; + +final class Engine +{ + /** + * @var string[] $templates + */ + private array $templates; + + public function __construct() + { + $finder = Finder::create()->files()->name('*.php.tpl')->in(self::basePath()); + $this->templates = array_map(fn(\SplFileInfo $file) => str_replace(self::basePath() . '/', '', $file->getRealPath()), \iterator_to_array($finder->getIterator())); + } + + /** + * @param array<string, string> $context + * + * @throws TemplatingException + */ + public function generate(string $template, array $context = []): string + { + if (!$this->hasTemplate($template)) { + throw new TemplateNotFoundException(\sprintf('Template "%s" not found.', $template)); + } + + if (false === $content = file_get_contents(self::basePath() . '/' . $template)) { + throw new UnreadableTemplateException(\sprintf('Could not read template "%s".', $template)); + } + + foreach ($context as $placeholder => $replacement) { + if (!\is_string($replacement)) { + throw new InvalidPlaceholderException(\sprintf('Placeholder "%s" must be a string.', $placeholder)); + } + + $content = str_replace($placeholder, $replacement, $content); + } + + return $content; + } + + private function hasTemplate(string $name): bool + { + return 0 < \count(\array_filter($this->templates, static fn(string $template) => $template === $name)); + } + + private static function basePath(): string + { + return Filesystem::generatorDir().'/templates'; + } +} diff --git a/generator/src/Templating/Exception/InvalidPlaceholderException.php b/generator/src/Templating/Exception/InvalidPlaceholderException.php new file mode 100644 index 00000000..f5c7d089 --- /dev/null +++ b/generator/src/Templating/Exception/InvalidPlaceholderException.php @@ -0,0 +1,9 @@ +<?php + +declare(strict_types=1); + +namespace Safe\Templating\Exception; + +final class InvalidPlaceholderException extends TemplatingException +{ +} diff --git a/generator/src/Templating/Exception/TemplateNotFoundException.php b/generator/src/Templating/Exception/TemplateNotFoundException.php new file mode 100644 index 00000000..d91c11cc --- /dev/null +++ b/generator/src/Templating/Exception/TemplateNotFoundException.php @@ -0,0 +1,9 @@ +<?php + +declare(strict_types=1); + +namespace Safe\Templating\Exception; + +final class TemplateNotFoundException extends TemplatingException +{ +} diff --git a/generator/src/Templating/Exception/TemplatingException.php b/generator/src/Templating/Exception/TemplatingException.php new file mode 100644 index 00000000..c560fa40 --- /dev/null +++ b/generator/src/Templating/Exception/TemplatingException.php @@ -0,0 +1,7 @@ +<?php + +namespace Safe\Templating\Exception; + +abstract class TemplatingException extends \InvalidArgumentException +{ +} diff --git a/generator/src/Templating/Exception/UnreadableTemplateException.php b/generator/src/Templating/Exception/UnreadableTemplateException.php new file mode 100644 index 00000000..de550d33 --- /dev/null +++ b/generator/src/Templating/Exception/UnreadableTemplateException.php @@ -0,0 +1,9 @@ +<?php + +declare(strict_types=1); + +namespace Safe\Templating\Exception; + +final class UnreadableTemplateException extends TemplatingException +{ +} diff --git a/generator/src/Templating/Filesystem.php b/generator/src/Templating/Filesystem.php new file mode 100644 index 00000000..3d69a178 --- /dev/null +++ b/generator/src/Templating/Filesystem.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace Safe\Templating; + +final class Filesystem +{ + private function __construct() + { + } + + public static function projectRootDir(): string + { + $path = realpath(__DIR__ . '/../../..'); + + if (false === $path) { + throw new \RuntimeException('Unable to locate root directory'); + } + + return $path; + } + + public static function generatorDir(): string + { + return \sprintf(self::projectRootDir().'/generator'); + } + + public static function outputDir(): string + { + return \sprintf(self::projectRootDir().'/generated'); + } + + public static function dumpFile(string $targetPath, string $content): void + { + $result = file_put_contents($targetPath, $content); + + if (false === $result) { + throw new \RuntimeException(\sprintf('Could not write to "%s".', $targetPath)); + } + } +} diff --git a/generator/src/XmlDocParser/DocPage.php b/generator/src/XmlDocParser/DocPage.php index a5acc0d9..2fde0b79 100644 --- a/generator/src/XmlDocParser/DocPage.php +++ b/generator/src/XmlDocParser/DocPage.php @@ -6,6 +6,7 @@ use Safe\Generator\FileCreator; +use Safe\Templating\Filesystem; use function explode; use function strpos; @@ -61,7 +62,7 @@ public function getErrorType(): ErrorType // This minimizes 'false positives', where text such as "returns false when ..." could be matched outside // the function's dedicated Return Values section. $returnDocs = $this->extractSection('returnvalues', $file); - $detectErrorType = require FileCreator::getSafeRootDir() . '/generator/config/detectErrorType.php'; + $detectErrorType = require Filesystem::generatorDir() . '/config/detectErrorType.php'; return $detectErrorType($returnDocs); } diff --git a/generator/src/XmlDocParser/Scanner.php b/generator/src/XmlDocParser/Scanner.php index 3ac8438f..61a4aaa7 100644 --- a/generator/src/XmlDocParser/Scanner.php +++ b/generator/src/XmlDocParser/Scanner.php @@ -4,6 +4,8 @@ namespace Safe\XmlDocParser; +use Safe\Templating\Filesystem; +use Symfony\Component\Console\Style\SymfonyStyle; use function array_merge; use function iterator_to_array; use Safe\PhpStanFunctions\PhpStanFunctionMapReader; @@ -58,7 +60,7 @@ public function getMethodsPaths(): array private function getIgnoredFunctions(): array { if ($this->ignoredFunctions === null) { - $ignoredFunctions = require FileCreator::getSafeRootDir() . '/generator/config/ignoredFunctions.php'; + $ignoredFunctions = require Filesystem::generatorDir() . '/config/ignoredFunctions.php'; $specialCaseFunctions = $this->getSpecialCases(); $this->ignoredFunctions = array_merge($ignoredFunctions, $specialCaseFunctions); @@ -73,7 +75,7 @@ private function getIgnoredFunctions(): array private function getIgnoredModules(): array { if ($this->ignoredModules === null) { - $this->ignoredModules = require FileCreator::getSafeRootDir() . '/generator/config/ignoredModules.php'; + $this->ignoredModules = require Filesystem::generatorDir() . '/config/ignoredModules.php'; assert(!is_null($this->ignoredModules)); } return $this->ignoredModules; @@ -87,7 +89,7 @@ private function getIgnoredModules(): array */ public static function getSpecialCases(): array { - $data = file_get_contents(FileCreator::getSafeRootDir() . '/lib/special_cases.php'); + $data = file_get_contents(Filesystem::projectRootDir() . '/lib/special_cases.php'); if ($data === false) { throw new \RuntimeException('Unable to read special cases'); } @@ -104,14 +106,14 @@ public static function getSpecialCases(): array */ public static function getHiddenFunctions(): array { - return require FileCreator::getSafeRootDir() . '/generator/config/hiddenFunctions.php'; + return require Filesystem::generatorDir() . '/config/hiddenFunctions.php'; } /** * @param SplFileInfo[] $paths * @param string[] $pastFunctionNames */ - public function getMethods(array $paths, array $pastFunctionNames, OutputInterface $output): ScannerResponse + public function getMethods(array $paths, array $pastFunctionNames, SymfonyStyle $io): ScannerResponse { /** @var Method[] $functions */ $functions = []; @@ -124,9 +126,7 @@ public function getMethods(array $paths, array $pastFunctionNames, OutputInterfa $ignoredModules = $this->getIgnoredModules(); $ignoredModules = \array_combine($ignoredModules, $ignoredModules); - ProgressBar::setFormatDefinition('custom', ' %current%/%max% [%bar%] %message%'); - $progressBar = new ProgressBar($output, count($paths)); - $progressBar->setFormat("custom"); + $progressBar = $io->createProgressBar(\count($paths)); foreach ($paths as $path) { $module = \basename(\dirname($path->getPath())); $progressBar->setMessage($path->getFilename()); @@ -167,8 +167,8 @@ public function getMethods(array $paths, array $pastFunctionNames, OutputInterfa } } } + $progressBar->finish(); - $output->writeln(""); return new ScannerResponse($functions, $overloadedFunctions); } diff --git a/generator/src/XmlDocParser/ScannerResponse.php b/generator/src/XmlDocParser/ScannerResponse.php index e82af9c4..3c8ce42c 100644 --- a/generator/src/XmlDocParser/ScannerResponse.php +++ b/generator/src/XmlDocParser/ScannerResponse.php @@ -15,4 +15,9 @@ public function __construct( public readonly array $overloadedFunctions ) { } + + public function hasOverloadedFunctions(): bool + { + return \count($this->overloadedFunctions) > 0; + } } diff --git a/generator/templates/Exception.php.tpl b/generator/templates/Exception.php.tpl new file mode 100644 index 00000000..d664d65a --- /dev/null +++ b/generator/templates/Exception.php.tpl @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +namespace Safe\Exceptions; + +class {{exceptionName}} extends \ErrorException implements SafeExceptionInterface +{ + public static function createFromPhpError(): self + { + $error = error_get_last(); + + return new self($error['message'] ?? 'An error occurred', 0, $error['type'] ?? 1); + } +} diff --git a/generator/templates/FunctionList.php.tpl b/generator/templates/FunctionList.php.tpl new file mode 100644 index 00000000..eef6f074 --- /dev/null +++ b/generator/templates/FunctionList.php.tpl @@ -0,0 +1,5 @@ +<?php + +return [ + {{functionNames}} +]; \ No newline at end of file diff --git a/generator/templates/Module.php.tpl b/generator/templates/Module.php.tpl new file mode 100644 index 00000000..be3e928f --- /dev/null +++ b/generator/templates/Module.php.tpl @@ -0,0 +1,7 @@ +<?php + +namespace Safe; + +use Safe\Exceptions\{{exceptionName}}; + +{{functions}} diff --git a/generator/templates/RectorConfig.php.tpl b/generator/templates/RectorConfig.php.tpl new file mode 100644 index 00000000..b30224bd --- /dev/null +++ b/generator/templates/RectorConfig.php.tpl @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +use Rector\Config\RectorConfig; +use Rector\Renaming\Rector\FuncCall\RenameFunctionRector; + +// This file configures rector/rector to replace all PHP functions with their equivalent "safe" functions. +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->ruleWithConfiguration( + RenameFunctionRector::class, + [ + {{functionNames}} + ], + ); +}; \ No newline at end of file