From d19fcbdedef9154fae2cda7132292b533e81949e Mon Sep 17 00:00:00 2001 From: Oliver Davies Date: Tue, 25 Apr 2023 00:16:21 +0100 Subject: [PATCH] refactor: use Illuminate Pipelines --- composer.json | 1 + composer.lock | 50 +++- src/Action/CreateFinalConfigurationData.php | 26 ++ src/Action/CreateListOfFilesToGenerate.php | 134 ++++++++++ src/Action/GenerateConfigurationFiles.php | 58 +++++ src/Action/ValidateBuildConfigurationData.php | 40 +++ src/Command/GenerateCommand.php | 245 +++--------------- 7 files changed, 343 insertions(+), 211 deletions(-) create mode 100644 src/Action/CreateFinalConfigurationData.php create mode 100644 src/Action/CreateListOfFilesToGenerate.php create mode 100644 src/Action/GenerateConfigurationFiles.php create mode 100644 src/Action/ValidateBuildConfigurationData.php diff --git a/composer.json b/composer.json index 1cb75a6..1015433 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "ext-iconv": "*", "doctrine/annotations": "^2.0", "illuminate/collections": "*", + "illuminate/pipeline": "^10.8", "illuminate/support": "^10.8", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.20", diff --git a/composer.lock b/composer.lock index 802a490..b833ee0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e6d53e1562c0a12390e51255801e7799", + "content-hash": "b98fa3983e5852c95b6d69ad0c556e29", "packages": [ { "name": "doctrine/annotations", @@ -488,6 +488,54 @@ }, "time": "2023-03-17T13:33:11+00:00" }, + { + "name": "illuminate/pipeline", + "version": "v10.8.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pipeline.git", + "reference": "f2119ae9a26e420bf0ed9d5e7e7aa4548547e7b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/f2119ae9a26e420bf0ed9d5e7e7aa4548547e7b1", + "reference": "f2119ae9a26e420bf0ed9d5e7e7aa4548547e7b1", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pipeline\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pipeline package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-03-03T15:55:44+00:00" + }, { "name": "illuminate/support", "version": "v10.8.0", diff --git a/src/Action/CreateFinalConfigurationData.php b/src/Action/CreateFinalConfigurationData.php new file mode 100644 index 0000000..50b4edb --- /dev/null +++ b/src/Action/CreateFinalConfigurationData.php @@ -0,0 +1,26 @@ + */ + $filesToGenerate = collect([ + new TemplateFile(data: 'common/.dockerignore', name: '.dockerignore'), + new TemplateFile(data: 'common/.hadolint.yaml', name: '.hadolint.yaml'), + new TemplateFile(data: 'env.example', name: '.env.example'), + ]); + + $extraDatabases = Arr::get($configurationData, 'database.extra_databases', []); + if (count($extraDatabases) > 0) { + $filesToGenerate[] = new TemplateFile( + data: 'extra-databases.sql', + name: 'extra-databases.sql', + path: 'tools/docker/images/database/root/docker-entrypoint-initdb.d', + ); + } + + if (false !== Arr::get($configurationData, "justfile", true)) { + $filesToGenerate[] = new TemplateFile(data: 'justfile', name: 'justfile'); + } + + if (isset($configurationData['dockerCompose']) && $configurationData['dockerCompose'] !== null) { + $filesToGenerate[] = new TemplateFile(data: 'docker-compose.yaml', name: 'docker-compose.yaml'); + } + + if (static::isPhp(Arr::get($configurationData, 'language'))) { + $filesToGenerate[] = new TemplateFile(data: 'php/Dockerfile', name: 'Dockerfile'); + $filesToGenerate[] = new TemplateFile(data: 'php/phpcs.xml', name: 'phpcs.xml.dist'); + $filesToGenerate[] = new TemplateFile(data: 'php/phpunit.xml', name: 'phpunit.xml.dist'); + $filesToGenerate[] = new TemplateFile( + data: 'php/docker-entrypoint-php', + name: 'docker-entrypoint-php', + path: 'tools/docker/images/php/root/usr/local/bin', + ); + + if (Arr::has(array: $configurationData, keys: 'php.phpstan')) { + $filesToGenerate[] = new TemplateFile(data: 'php/phpstan.neon', name: 'phpstan.neon.dist'); + } + } + + if (static::isNode(Arr::get($configurationData, 'language'))) { + $filesToGenerate[] = new TemplateFile(data: 'node/.yarnrc', name: '.yarnrc'); + $filesToGenerate[] = new TemplateFile(data: 'node/Dockerfile', name: 'Dockerfile'); + } + + if (static::isCaddy(Arr::get($configurationData, 'web.type'))) { + $filesToGenerate[] = new TemplateFile( + data: 'web/caddy/Caddyfile', + name: 'Caddyfile', + path: 'tools/docker/images/web/root/etc/caddy', + ); + } + + if (static::isNginx(Arr::get($configurationData, 'web.type'))) { + $filesToGenerate[] = new TemplateFile( + data: 'web/nginx/default.conf', + name: 'default.conf', + path: 'tools/docker/images/web/root/etc/nginx/conf.d', + ); + } + + if ('drupal-project' === Arr::get($configurationData, 'type')) { + // Add a Drupal version of phpunit.xml.dist. + $filesToGenerate[] = new TemplateFile(data: 'drupal-project/phpunit.xml.dist', name: 'phpunit.xml.dist'); + } + + if (Arr::get($configurationData, 'experimental.createGitHubActionsConfiguration', false) === true) { + $filesToGenerate[] = new TemplateFile( + data: 'ci/github-actions/ci.yml', + name: 'ci.yml', + path: '.github/workflows', + ); + } + + if (Arr::get($configurationData, 'experimental.runGitHooksBeforePush', false) === true) { + $filesToGenerate[] = new TemplateFile( + data: 'git-hooks/pre-push', + name: 'pre-push', + path: '.githooks', + ); + } + + return $next([$configurationData, $filesToGenerate]); + } + + private static function isCaddy(?string $webServer): bool + { + if (is_null($webServer)) { + return false; + } + + return strtoupper($webServer) === WebServer::CADDY->name; + } + + private static function isNginx(?string $webServer): bool + { + if (is_null($webServer)) { + return false; + } + + return strtoupper($webServer) === WebServer::NGINX->name; + } + + private static function isNode(?string $language): bool + { + if (is_null($language)) { + return false; + } + + return strtoupper($language) === Language::NODE->name; + } + + private static function isPhp(?string $language): bool + { + if (is_null($language)) { + return false; + } + + return strtoupper($language) === Language::PHP->name; + } +} diff --git a/src/Action/GenerateConfigurationFiles.php b/src/Action/GenerateConfigurationFiles.php new file mode 100644 index 0000000..1550605 --- /dev/null +++ b/src/Action/GenerateConfigurationFiles.php @@ -0,0 +1,58 @@ + $filesToGenerate + * @var array $configurationData + */ + [$configurationData, $filesToGenerate] = $filesToGenerateAndConfigurationData; + + $filesToGenerate->each(function(TemplateFile $templateFile) use ($configurationData): void { + if ($templateFile->path !== null) { + if (!$this->filesystem->exists($templateFile->path)) { + $this->filesystem->mkdir("{$this->outputDir}/{$templateFile->path}"); + } + } + + $sourceFile = "{$templateFile->data}.twig"; + + $outputFile = collect([ + $this->outputDir, + $templateFile->path, + $templateFile->name, + ])->filter()->implode('/'); + + $this->filesystem->dumpFile($outputFile, $this->twig->render($sourceFile, $configurationData)); + }); + + // If the Docker entrypoint file is generated, ensure it is executable. + if ($this->filesystem->exists("{$outputDir}/tools/docker/images/php/root/usr/local/bin/docker-entrypoint-php")) { + $filesystem->chmod("{$outputDir}/tools/docker/images/php/root/usr/local/bin/docker-entrypoint-php", 0755); + } + + if ($this->filesystem->exists("{$outputDir}/.githooks/pre-push")) { + $this->filesystem->chmod("{$outputDir}/.githooks/pre-push", 0755); + } + + return $next([$configurationData, $filesToGenerate]); + } +} diff --git a/src/Action/ValidateBuildConfigurationData.php b/src/Action/ValidateBuildConfigurationData.php new file mode 100644 index 0000000..6bc0cce --- /dev/null +++ b/src/Action/ValidateBuildConfigurationData.php @@ -0,0 +1,40 @@ +deserialize(json_encode($configurationData), Config::class, 'json'); + + $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator(); + $violations = $validator->validate($configurationDataObject); + + if (0 < $violations->count()) { + $io->error('Configuration is invalid.'); + + $io->listing( + collect($violations) + ->map(fn (ConstraintViolationInterface $v) => "{$v->getPropertyPath()} - {$v->getMessage()}") + ->toArray() + ); + + return; + } + + return $next($configurationData); + } +} diff --git a/src/Command/GenerateCommand.php b/src/Command/GenerateCommand.php index 1ca9bfa..7c2c85d 100644 --- a/src/Command/GenerateCommand.php +++ b/src/Command/GenerateCommand.php @@ -4,11 +4,12 @@ declare(strict_types=1); namespace App\Command; -use App\DataTransferObject\Config; +use App\Action\CreateFinalConfigurationData; +use App\Action\CreateListOfFilesToGenerate; +use App\Action\GenerateConfigurationFiles; +use App\Action\ValidateBuildConfigurationData; use App\DataTransferObject\TemplateFile; -use App\Enum\Language; -use App\Enum\WebServer; -use Illuminate\Support\Arr; +use Illuminate\Pipeline\Pipeline; use Illuminate\Support\Collection; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -17,15 +18,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\Serializer\Encoder\JsonEncoder; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; -use Symfony\Component\Serializer\Serializer; -use Symfony\Component\Validator\ConstraintViolationInterface; -use Symfony\Component\Validator\Validation; -use Symfony\Component\Yaml\Yaml; use Twig\Environment; -use Twig\Loader\FilesystemLoader; #[AsCommand( name: 'app:generate', @@ -33,6 +26,13 @@ use Twig\Loader\FilesystemLoader; )] class GenerateCommand extends Command { + public function __construct( + private Filesystem $filesystem, + private Environment $twig, + ) { + parent::__construct(); + } + protected function configure(): void { $this @@ -60,222 +60,47 @@ class GenerateCommand extends Command $configFile = $input->getOption(name: 'config-file'); $outputDir = $input->getOption(name: 'output-dir'); - $configurationData = array_merge( - Yaml::parseFile(filename: __DIR__ . '/../../resources/build.defaults.yaml'), - Yaml::parseFile(filename: $configFile), - ); + $pipes = [ + new CreateFinalConfigurationData(), - // Convert the input to a configuration data object. - $normalizer = new ObjectNormalizer(null, new CamelCaseToSnakeCaseNameConverter()); - $serializer = new Serializer([$normalizer], [new JsonEncoder()]); - $configurationDataObject = $serializer->deserialize(json_encode($configurationData), Config::class, 'json'); + new ValidateBuildConfigurationData(), - $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator(); - $violations = $validator->validate($configurationDataObject); + new CreateListOfFilesToGenerate(), - if (0 < $violations->count()) { - $io->error('Configuration is invalid.'); + new GenerateConfigurationFiles( + $this->filesystem, + $this->twig, + $outputDir, + ), + ]; - $io->listing( - collect($violations) - ->map(fn (ConstraintViolationInterface $v) => "{$v->getPropertyPath()} - {$v->getMessage()}") - ->toArray() - ); - - return Command::FAILURE; - } - - if (isset($configurationData['docker-compose'])) { - $configurationData['dockerCompose'] = $configurationData['docker-compose']; - $configurationData['docker-compose'] = null; - } - - $configurationData['managedText'] = 'Do not edit this file. It is automatically generated by https://www.oliverdavies.uk/build-configs.'; - - $filesToGenerate = $this->getFiles(configurationData: $configurationData); + /** + * @var Collection $generatedFiles + * @var array $configurationData + */ + [$configurationData, $generatedFiles] = (new Pipeline()) + ->send($configFile) + ->through($pipes) + ->thenReturn(); $io->info("Building configuration for {$configurationData['name']}."); $io->write('Generated files:'); - $io->listing($this->getListOfFiles(filesToGenerate: $filesToGenerate)->toArray()); + $io->listing(static::getListOfFiles(filesToGenerate: $generatedFiles)->toArray()); - $this->generateFiles( - configurationData: $configurationData, - filesToGenerate: $filesToGenerate, - outputDir: $outputDir, - ); - // return Command::SUCCESS; } - /** - * @param array $configurationData - * @param Collection $filesToGenerate - */ - function generateFiles( - Collection $filesToGenerate, - string $outputDir, - array $configurationData, - ): void + private static function buildFilePath(TemplateFile $templateFile): string { - $filesystem = new Filesystem(); - $twig = new Environment(new FilesystemLoader([__DIR__ . '/../../templates'])); - - $filesToGenerate->each(function(TemplateFile $templateFile) use ($configurationData, $filesystem, $outputDir, $twig): void { - if ($templateFile->path !== null) { - if (!$filesystem->exists($templateFile->path)) { - $filesystem->mkdir("{$outputDir}/{$templateFile->path}"); - } - } - - $sourceFile = "{$templateFile->data}.twig"; - $outputFile = collect([ - $outputDir, - $templateFile->path, - $templateFile->name, - ])->filter()->implode('/'); - - $filesystem->dumpFile($outputFile, $twig->render($sourceFile, $configurationData)); - }); - - // If the Docker entrypoint file is generated, ensure it is executable. - if ($filesystem->exists("{$outputDir}/tools/docker/images/php/root/usr/local/bin/docker-entrypoint-php")) { - $filesystem->chmod("{$outputDir}/tools/docker/images/php/root/usr/local/bin/docker-entrypoint-php", 0755); - } - - if ($filesystem->exists("{$outputDir}/.githooks/pre-push")) { - $filesystem->chmod("{$outputDir}/.githooks/pre-push", 0755); - } + return collect([$templateFile->path, $templateFile->name])->filter()->implode('/'); } - function getFiles(array $configurationData): Collection - { - /** @var Collection */ - $filesToGenerate = collect([ - new TemplateFile(data: 'common/.dockerignore', name: '.dockerignore'), - new TemplateFile(data: 'common/.hadolint.yaml', name: '.hadolint.yaml'), - new TemplateFile(data: 'env.example', name: '.env.example'), - ]); - - $extraDatabases = Arr::get($configurationData, 'database.extra_databases', []); - if (count($extraDatabases) > 0) { - $filesToGenerate[] = new TemplateFile( - data: 'extra-databases.sql', - name: 'extra-databases.sql', - path: 'tools/docker/images/database/root/docker-entrypoint-initdb.d', - ); - } - - if (false !== Arr::get($configurationData, "justfile", true)) { - $filesToGenerate[] = new TemplateFile(data: 'justfile', name: 'justfile'); - } - - if (isset($configurationData['dockerCompose']) && $configurationData['dockerCompose'] !== null) { - $filesToGenerate[] = new TemplateFile(data: 'docker-compose.yaml', name: 'docker-compose.yaml'); - } - - if (static::isPhp(Arr::get($configurationData, 'language'))) { - $filesToGenerate[] = new TemplateFile(data: 'php/Dockerfile', name: 'Dockerfile'); - $filesToGenerate[] = new TemplateFile(data: 'php/phpcs.xml', name: 'phpcs.xml.dist'); - $filesToGenerate[] = new TemplateFile(data: 'php/phpunit.xml', name: 'phpunit.xml.dist'); - $filesToGenerate[] = new TemplateFile( - data: 'php/docker-entrypoint-php', - name: 'docker-entrypoint-php', - path: 'tools/docker/images/php/root/usr/local/bin', - ); - - if (Arr::has(array: $configurationData, keys: 'php.phpstan')) { - $filesToGenerate[] = new TemplateFile(data: 'php/phpstan.neon', name: 'phpstan.neon.dist'); - } - } - - if (static::isNode(Arr::get($configurationData, 'language'))) { - $filesToGenerate[] = new TemplateFile(data: 'node/.yarnrc', name: '.yarnrc'); - $filesToGenerate[] = new TemplateFile(data: 'node/Dockerfile', name: 'Dockerfile'); - } - - if (static::isCaddy(Arr::get($configurationData, 'web.type'))) { - $filesToGenerate[] = new TemplateFile( - data: 'web/caddy/Caddyfile', - name: 'Caddyfile', - path: 'tools/docker/images/web/root/etc/caddy', - ); - } - - if (static::isNginx(Arr::get($configurationData, 'web.type'))) { - $filesToGenerate[] = new TemplateFile( - data: 'web/nginx/default.conf', - name: 'default.conf', - path: 'tools/docker/images/web/root/etc/nginx/conf.d', - ); - } - - if ('drupal-project' === Arr::get($configurationData, 'type')) { - // Add a Drupal version of phpunit.xml.dist. - $filesToGenerate[] = new TemplateFile(data: 'drupal-project/phpunit.xml.dist', name: 'phpunit.xml.dist'); - } - - if (Arr::get($configurationData, 'experimental.createGitHubActionsConfiguration', false) === true) { - $filesToGenerate[] = new TemplateFile( - data: 'ci/github-actions/ci.yml', - name: 'ci.yml', - path: '.github/workflows', - ); - } - - if (Arr::get($configurationData, 'experimental.runGitHooksBeforePush', false) === true) { - $filesToGenerate[] = new TemplateFile( - data: 'git-hooks/pre-push', - name: 'pre-push', - path: '.githooks', - ); - } - - return $filesToGenerate; - } - - function getListOfFiles(Collection $filesToGenerate): Collection + private static function getListOfFiles(Collection $filesToGenerate): Collection { return $filesToGenerate - ->map(fn (TemplateFile $templateFile): string => - collect([$templateFile->path, $templateFile->name])->filter()->implode('/')) + ->map(fn (TemplateFile $templateFile): string => static::buildFilePath($templateFile)) ->unique() ->sort(); } - - function isCaddy(?string $webServer): bool - { - if (is_null($webServer)) { - return false; - } - - return strtoupper($webServer) === WebServer::CADDY->name; - } - - function isNginx(?string $webServer): bool - { - if (is_null($webServer)) { - return false; - } - - return strtoupper($webServer) === WebServer::NGINX->name; - } - - function isNode(?string $language): bool - { - if (is_null($language)) { - return false; - } - - return strtoupper($language) === Language::NODE->name; - } - - function isPhp(?string $language): bool - { - if (is_null($language)) { - return false; - } - - return strtoupper($language) === Language::PHP->name; - } }