mirror of
https://github.com/opdavies/build-configs.git
synced 2025-09-06 11:25:33 +01:00
refactor(*): change to a Symfony Console app
This commit is contained in:
parent
4af661bad4
commit
8db64458b1
41 changed files with 5938 additions and 4285 deletions
269
src/Command/GenerateCommand.php
Normal file
269
src/Command/GenerateCommand.php
Normal file
|
@ -0,0 +1,269 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\DataTransferObject\Config;
|
||||
use App\DataTransferObject\TemplateFile;
|
||||
use App\Enum\Language;
|
||||
use App\Enum\WebServer;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
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',
|
||||
description: 'Generate project-specific configuration files',
|
||||
)]
|
||||
class GenerateCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
name: 'config-file',
|
||||
shortcut: ['c'],
|
||||
mode: InputOption::VALUE_REQUIRED,
|
||||
description: 'The path to the project\'s build.yaml file',
|
||||
default: 'build.yaml',
|
||||
)
|
||||
->addOption(
|
||||
name: 'output-dir',
|
||||
shortcut: ['o'],
|
||||
mode: InputOption::VALUE_REQUIRED,
|
||||
description: 'The directory to create files in',
|
||||
default: '.',
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$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),
|
||||
);
|
||||
|
||||
// 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');
|
||||
|
||||
$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 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);
|
||||
|
||||
$io->info("Building configuration for {$configurationData['name']}.");
|
||||
|
||||
$io->write('Generated files:');
|
||||
$io->listing($this->getListOfFiles(filesToGenerate: $filesToGenerate)->toArray());
|
||||
|
||||
$this->generateFiles(
|
||||
configurationData: $configurationData,
|
||||
filesToGenerate: $filesToGenerate,
|
||||
outputDir: $outputDir,
|
||||
);
|
||||
//
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $configurationData
|
||||
* @param Collection<int, TemplateFile> $filesToGenerate
|
||||
*/
|
||||
function generateFiles(
|
||||
Collection $filesToGenerate,
|
||||
string $outputDir,
|
||||
array $configurationData,
|
||||
): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
function getFiles(array $configurationData): Collection
|
||||
{
|
||||
/** @var Collection<int, TemplateFile> */
|
||||
$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',
|
||||
);
|
||||
}
|
||||
|
||||
return $filesToGenerate;
|
||||
}
|
||||
|
||||
function getListOfFiles(Collection $filesToGenerate): Collection
|
||||
{
|
||||
return $filesToGenerate
|
||||
->map(fn (TemplateFile $templateFile): string =>
|
||||
collect([$templateFile->path, $templateFile->name])->filter()->implode('/'))
|
||||
->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;
|
||||
}
|
||||
}
|
49
src/Command/InitCommand.php
Normal file
49
src/Command/InitCommand.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:init',
|
||||
description: 'Add a short description for your command',
|
||||
)]
|
||||
class InitCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('projectName', InputArgument::REQUIRED, 'The name of the project')
|
||||
->addArgument('language', InputArgument::REQUIRED, 'The language the project uses')
|
||||
->addArgument('type', InputArgument::REQUIRED, 'The type of project')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
[
|
||||
'language' => $language,
|
||||
'projectName' => $projectName,
|
||||
'type' => $type,
|
||||
] = $input->getArguments();
|
||||
|
||||
$projectName = str_replace('.', '-', $projectName);
|
||||
|
||||
// TODO: validate the project type.
|
||||
$output = <<<EOF
|
||||
name: $projectName
|
||||
language: $language
|
||||
type: $type
|
||||
EOF;
|
||||
|
||||
file_put_contents('build.yaml', $output);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue