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