Update SpinCommand and Abstract to remove messy directory array in favour of cleaner methods

This commit is contained in:
Daniel Winning 2025-04-23 23:30:51 +01:00
parent 3fe612cb36
commit df67dafb8e
10 changed files with 146 additions and 250 deletions

View file

@ -1,6 +1,7 @@
#!/usr/bin/env php
<?php
use Loom\Spinner\Command\DestroyCommand;
use Loom\Spinner\Command\SpinCommand;
use Symfony\Component\Console\Application;
@ -9,6 +10,7 @@ require dirname(__DIR__) . '/vendor/autoload.php';
$application = new Application('Loom Spinner');
$application->add(new SpinCommand());
$application->add(new DestroyCommand());
try {
$application->run();

View file

@ -4,40 +4,52 @@ declare(strict_types=1);
namespace Loom\Spinner\Classes\Config;
use Loom\Spinner\Classes\Collection\FilePathCollection;
use Loom\Spinner\Classes\File\Interface\DataPathInterface;
use Loom\Utility\FilePath\FilePath;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Yaml\Yaml;
class Config
{
private FilePathCollection $filePaths;
private string $spinnerRootPath;
private string $configDirectory;
private string $dataDirectory;
private ?string $projectWorkPath = null;
public function __construct()
public function __construct(string $projectName, ?string $projectWorkPath = null)
{
$this->setFilePaths();
$this->spinnerRootPath = dirname(__DIR__, 3);
$this->configDirectory = $this->spinnerRootPath . '/config';
$this->dataDirectory = $this->spinnerRootPath . '/data/environments/' . $projectName;
if ($projectWorkPath) {
$this->projectWorkPath = $projectWorkPath;
}
}
public function getFilePaths(): FilePathCollection
public function getDataDirectory(): string
{
return $this->filePaths;
return $this->dataDirectory;
}
public function getFilePath(string $key): ?FilePath
public function getConfigFilePath(string $fileName): string
{
return $this->filePaths->get($key);
return $this->configDirectory . '/' . $fileName;
}
public function addFilePath(FilePath $filePath, string $key): void
public function getConfigFileContents(string $fileName): string|null
{
$this->filePaths->add($filePath, $key);
if (file_exists($this->configDirectory . '/' . $fileName)) {
return file_get_contents($this->configDirectory . '/' . $fileName);
}
return null;
}
/**
* @throws \Exception
*/
public function getPhpVersion(InputInterface $input): ?float
public function projectDataExists(string $projectName): bool
{
return file_exists($this->dataDirectory . '/environments/' . $projectName);
}
public function getPhpVersion(InputInterface $input): float
{
if ($input->getOption('php')) {
return (float) $input->getOption('php');
@ -91,7 +103,7 @@ class Config
return false;
}
return $this->getEnvironmentOption('database', 'enabled');
return (bool) $this->getEnvironmentOption('database', 'enabled');
}
/**
@ -118,52 +130,29 @@ class Config
return (int) $this->getEnvironmentOption('node','version');
}
/**
* @throws \Exception
*/
public function getEnvironmentOption(string $service, string $option): mixed
{
$projectCustomConfig = $this->filePaths->get('projectCustomConfig');
$projectCustomConfig = $this->getProjectCustomConfig();
if ($projectCustomConfig->exists()) {
$customConfig = Yaml::parseFile($projectCustomConfig->getAbsolutePath())['options']['environment']
?? null;
if ($customConfig) {
if (isset($customConfig[$service][$option])) {
return $customConfig[$service][$option];
}
}
if ($projectCustomConfig) {
return $projectCustomConfig[$service][$option] ?? $this->getDefaultConfig()[$service][$option] ?? null;
}
return $this->getDefaultConfig()['environment'][$service][$option] ?? null;
return $this->getDefaultConfig()[$service][$option] ?? null;
}
public function getProjectCustomConfig(): ?array
{
if ($this->projectWorkPath && file_exists($this->projectWorkPath . '/spinner.yaml')) {
return Yaml::parseFile($this->projectWorkPath . '/spinner.yaml')['options']['environment'];
}
return null;
}
/**
* @throws \Exception
*/
protected function getDefaultConfig(): ?array
{
return Yaml::parseFile($this->filePaths->get('defaultSpinnerConfig')?->getAbsolutePath())['options']
return Yaml::parseFile($this->configDirectory . '/spinner.yaml')['options']['environment']
?? null;
}
private function setFilePaths(): void
{
$this->filePaths = new FilePathCollection([
'config' => new FilePath('config'),
'defaultSpinnerConfig' => new FilePath('config/spinner.yaml'),
'envTemplate' => new FilePath('config/.template.env'),
'data' => new FilePath('data'),
'phpYamlTemplate' => new FilePath('config/php.yaml'),
'nginxYamlTemplate' => new FilePath('config/nginx.yaml'),
'phpFpmDataDirectory' => new FilePath('config/php-fpm'),
DataPathInterface::CONFIG_PHP_FPM_DOCKERFILE => new FilePath(DataPathInterface::CONFIG_PHP_FPM_DOCKERFILE),
DataPathInterface::CONFIG_NGINX_DOCKERFILE => new FilePath(DataPathInterface::CONFIG_NGINX_DOCKERFILE),
'nodeDockerfileTemplate' => new FilePath('config/php-fpm/Node.Dockerfile'),
'xdebugIniTemplate' => new FilePath('config/php-fpm/xdebug.ini'),
'opcacheIniTemplate' => new FilePath('config/php-fpm/opcache.ini'),
'xdebugDockerfileTemplate' => new FilePath('config/php-fpm/XDebug.Dockerfile'),
]);
}
}

View file

@ -6,14 +6,13 @@ namespace Loom\Spinner\Classes\File;
use Loom\Spinner\Classes\Config\Config;
use Loom\Spinner\Classes\File\Interface\FileBuilderInterface;
use Loom\Utility\FilePath\FilePath;
use Symfony\Component\Console\Input\InputInterface;
abstract class AbstractFileBuilder implements FileBuilderInterface
{
protected string $content;
public function __construct(protected FilePath $path, protected Config $config)
public function __construct(protected string $path, protected Config $config)
{
return $this;
}
@ -22,7 +21,7 @@ abstract class AbstractFileBuilder implements FileBuilderInterface
public function save(): void
{
file_put_contents($this->path->getProvidedPath(), $this->content);
file_put_contents($this->path, $this->content);
}

View file

@ -15,13 +15,7 @@ class DockerComposeFileBuilder extends AbstractFileBuilder
*/
public function __construct(Config $config)
{
$projectDockerCompose = $config->getFilePaths()->get('projectDockerCompose');
if (!$projectDockerCompose instanceof FilePath) {
throw new \Exception('Project Docker Compose file path not found.');
}
return parent::__construct($projectDockerCompose, $config);
return parent::__construct($config->getDataDirectory() . '/docker-compose.yaml', $config);
}
/**
@ -29,9 +23,7 @@ class DockerComposeFileBuilder extends AbstractFileBuilder
*/
public function build(InputInterface $input): DockerComposeFileBuilder
{
$this->content = file_get_contents(
$this->config->getFilePaths()->get('phpYamlTemplate')->getAbsolutePath()
);
$this->content = $this->config->getConfigFileContents('php.yaml');
if ($this->config->isDatabaseEnabled($input) && in_array($this->config->getDatabaseDriver($input), ['sqlite3', 'sqlite'])) {
$this->addSqliteDatabaseConfig();
@ -49,20 +41,18 @@ class DockerComposeFileBuilder extends AbstractFileBuilder
$this->content .= str_replace(
'services:',
'',
file_get_contents(
$this->config->getFilePaths()->get('nginxYamlTemplate')->getAbsolutePath()
)
$this->config->getConfigFileContents('nginx.yaml')
);
$this->content = str_replace(
'./nginx/conf.d',
(new FilePath('config/nginx/conf.d'))->getAbsolutePath(),
$this->config->getConfigFilePath('nginx/conf.d'),
$this->content
);
}
private function addSqliteDatabaseConfig(): void
{
$sqlLiteConfig = file_get_contents((new FilePath('config/sqlite.yaml'))->getAbsolutePath());
$sqlLiteConfig = $this->config->getConfigFileContents('sqlite.yaml');
$sqlLiteConfig = str_replace('volumes:', '', $sqlLiteConfig);
$this->content .= $sqlLiteConfig;
}

View file

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace Loom\Spinner\Classes\File\Interface;
interface DataPathInterface
{
public const string CONFIG_PHP_FPM_DOCKERFILE = 'config/php-fpm/Dockerfile';
public const string CONFIG_NGINX_DOCKERFILE = 'config/nginx/Dockerfile';
}

View file

@ -5,8 +5,6 @@ declare(strict_types=1);
namespace Loom\Spinner\Classes\File;
use Loom\Spinner\Classes\Config\Config;
use Loom\Spinner\Classes\File\Interface\DataPathInterface;
use Loom\Utility\FilePath\FilePath;
use Symfony\Component\Console\Input\InputInterface;
class NginxDockerFileBuilder extends AbstractFileBuilder
@ -16,22 +14,12 @@ class NginxDockerFileBuilder extends AbstractFileBuilder
*/
public function __construct(Config $config)
{
$projectNginxDockerfilePath = $config->getFilePath('projectNginxDockerfile');
if (!$projectNginxDockerfilePath instanceof FilePath) {
throw new \Exception('Project PHP-FPM Dockerfile not found');
}
return parent::__construct($projectNginxDockerfilePath, $config);
return parent::__construct($config->getDataDirectory() . '/nginx/Dockerfile', $config);
}
public function build(InputInterface $input): AbstractFileBuilder
{
$this->content = file_get_contents(
$this->config->getFilePaths()
->get(DataPathInterface::CONFIG_NGINX_DOCKERFILE)
->getAbsolutePath()
);
$this->content = $this->config->getConfigFileContents('nginx/Dockerfile');
return $this;
}

View file

@ -5,8 +5,6 @@ declare(strict_types=1);
namespace Loom\Spinner\Classes\File;
use Loom\Spinner\Classes\Config\Config;
use Loom\Spinner\Classes\File\Interface\DataPathInterface;
use Loom\Utility\FilePath\FilePath;
use Symfony\Component\Console\Input\InputInterface;
class PHPDockerFileBuilder extends AbstractFileBuilder
@ -16,13 +14,7 @@ class PHPDockerFileBuilder extends AbstractFileBuilder
*/
public function __construct(Config $config)
{
$projectPhpFpmDockerfile = $config->getFilePath('projectPhpFpmDockerfile');
if (!$projectPhpFpmDockerfile instanceof FilePath) {
throw new \Exception('Project PHP-FPM Dockerfile not found');
}
return parent::__construct($projectPhpFpmDockerfile, $config);
return parent::__construct($config->getDataDirectory() . '/php-fpm/Dockerfile', $config);
}
/**
@ -35,27 +27,27 @@ class PHPDockerFileBuilder extends AbstractFileBuilder
$this->content = str_replace('${PHP_VERSION}', (string) $this->config->getPhpVersion($input), $this->content);
file_put_contents(
(new FilePath(sprintf('data/environments/%s/php-fpm/opcache.ini', $input->getArgument('name'))))->getProvidedPath(),
file_get_contents($this->config->getFilePaths()->get('opcacheIniTemplate')->getAbsolutePath())
$this->config->getDataDirectory() . '/php-fpm/opcache.ini',
$this->config->getConfigFileContents('php-fpm/opcache.ini')
);
if ($this->config->isDatabaseEnabled($input) && in_array($this->config->getDatabaseDriver($input), ['sqlite3', 'sqlite'])) {
$this->addNewLine();
$this->content .= file_get_contents((new FilePath('config/php-fpm/Sqlite.Dockerfile'))->getAbsolutePath());
$this->content .= $this->config->getConfigFileContents('php-fpm/Sqlite.Dockerfile');
}
if ($this->config->isNodeEnabled($input)) {
$this->addNewLine();
$this->content .= file_get_contents($this->config->getFilePaths()->get('nodeDockerfileTemplate')->getAbsolutePath());
$this->content .= $this->config->getConfigFileContents('php-fpm/Node.Dockerfile');
$this->content = str_replace('${NODE_VERSION}', (string) $this->config->getNodeVersion($input), $this->content);
}
if ($this->config->isXdebugEnabled($input)) {
$this->addNewLine();
$this->content .= file_get_contents($this->config->getFilePaths()->get('xdebugDockerfileTemplate')->getAbsolutePath());
$this->content .= $this->config->getConfigFileContents('php-fpm/Xdebug.Dockerfile');
file_put_contents(
(new FilePath(sprintf('data/environments/%s/php-fpm/xdebug.ini', $input->getArgument('name'))))->getProvidedPath(),
file_get_contents($this->config->getFilePaths()->get('xdebugIniTemplate')->getAbsolutePath())
$this->config->getDataDirectory() . '/php-fpm/xdebug.ini',
$this->config->getConfigFileContents('php-fpm/xdebug.ini')
);
}
@ -64,8 +56,6 @@ class PHPDockerFileBuilder extends AbstractFileBuilder
private function setInitialContent(): void
{
$this->content = file_get_contents(
$this->config->getFilePaths()->get(DataPathInterface::CONFIG_PHP_FPM_DOCKERFILE)->getAbsolutePath()
);
$this->content = $this->config->getConfigFileContents('php-fpm/Dockerfile');
}
}

View file

@ -4,16 +4,14 @@ declare(strict_types=1);
namespace Loom\Spinner\Command;
use Loom\Spinner\Classes\Collection\FilePathCollection;
use Loom\Spinner\Classes\Config\Config;
use Loom\Spinner\Classes\OS\System;
use Loom\Spinner\Command\Interface\ConsoleCommandInterface;
use Loom\Utility\FilePath\FilePath;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Yaml\Yaml;
class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
{
@ -24,7 +22,6 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
public function __construct()
{
$this->system = new System();
$this->config = new Config();
parent::__construct();
}
@ -41,12 +38,6 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
return Command::FAILURE;
}
if (!$this->validatePathArgument($input)) {
return Command::FAILURE;
}
$this->validateNameArgument($input);
return Command::SUCCESS;
}
@ -54,61 +45,13 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
{
return sprintf(
'cd %s && docker compose --env-file=%s %s%s',
$this->config->getFilePaths()->get('projectData')->getAbsolutePath(),
$this->config->getFilePaths()->get('projectEnv')->getAbsolutePath(),
$this->config->getDataDirectory(),
$this->config->getDataDirectory() . '/.env',
$command,
$daemon ? ' -d' : ''
);
}
private function validatePathArgument(InputInterface $input): bool
{
if ($input->hasArgument('path')) {
$projectDirectory = new FilePath($input->getArgument('path'));
if (!$projectDirectory->exists() || !$projectDirectory->isDirectory()) {
$this->style->error('The provided path is not a valid directory.');
return false;
}
$this->config->addFilePath($projectDirectory, 'project');
}
return true;
}
private function validateNameArgument(InputInterface $input): void
{
var_dump('Validating name argument');
if ($input->hasArgument('name')) {
$this->config->addFilePath(
new FilePath(sprintf('data/environments/%s', $input->getArgument('name'))),
'projectData'
);
$this->config->addFilePath(
new FilePath(sprintf('data/environments/%s/.env', $input->getArgument('name'))),
'projectEnv'
);
$this->config->addFilePath(
new FilePath(sprintf('data/environments/%s/docker-compose.yml', $input->getArgument('name'))),
'projectDockerCompose'
);
$this->config->addFilePath(
new FilePath(sprintf('data/environments/%s/php-fpm/Dockerfile', $input->getArgument('name'))),
'projectPhpFpmDockerfile'
);
$this->config->addFilePath(
new FilePath(sprintf('data/environments/%s/nginx/Dockerfile', $input->getArgument('name'))),
'projectNginxDockerfile'
);
$this->config->addFilePath(
new FilePath($this->config->getFilePaths()->get('project')->getAbsolutePath() . DIRECTORY_SEPARATOR . 'spinner.yaml'),
'projectCustomConfig'
);
}
}
private function setStyle(InputInterface $input, OutputInterface $output): void
{
$this->style = new SymfonyStyle($input, $output);

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Loom\Spinner\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: 'spin:down', description: 'Destroy a development environment')]
class DestroyCommand extends AbstractSpinnerCommand
{
protected function configure(): void
{
$this->addArgument(
'name',
InputArgument::REQUIRED,
'The name of the project (as used when running spin:up).'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (parent::execute($input, $output)) {
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

View file

@ -4,11 +4,11 @@ declare(strict_types=1);
namespace Loom\Spinner\Command;
use Loom\Spinner\Classes\Config\Config;
use Loom\Spinner\Classes\File\DockerComposeFileBuilder;
use Loom\Spinner\Classes\File\NginxDockerFileBuilder;
use Loom\Spinner\Classes\File\PHPDockerFileBuilder;
use Loom\Spinner\Classes\OS\PortGenerator;
use Loom\Utility\FilePath\FilePath;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@ -21,6 +21,7 @@ class SpinCommand extends AbstractSpinnerCommand
{
private PortGenerator $portGenerator;
private array $ports;
private string $projectWorkPath = '';
public function __construct()
{
@ -84,14 +85,35 @@ class SpinCommand extends AbstractSpinnerCommand
return Command::FAILURE;
}
if ($this->projectDataExists()) {
return Command::SUCCESS;
$this->projectWorkPath = $input->getArgument('path') === '.'
? getcwd()
: $input->getArgument('path');
if (!file_exists($this->projectWorkPath)) {
$this->style->error('The provided project path does not exist.');
return Command::FAILURE;
}
$this->config = new Config($input->getArgument('name'), $this->projectWorkPath);
if ($this->config->projectDataExists($input->getArgument('name'))) {
$this->style->error('A project with the same name already exists.');
return Command::FAILURE;
}
$this->style->success("Spinning up a new development environment...");
$this->style->text('Creating project data...');
$this->createProjectData($input);
try {
$this->createProjectData($input);
} catch (\Exception $exception) {
$this->style->error('Failed to create project data: '. $exception->getMessage());
return Command::FAILURE;
}
$this->style->success('Project data created.');
$this->style->text('Building Docker images...');
@ -102,23 +124,12 @@ class SpinCommand extends AbstractSpinnerCommand
return Command::SUCCESS;
}
protected function projectDataExists(): bool
{
if ($this->config->getFilePaths()->get('projectData')->exists()) {
$this->style->warning('Project already exists. Skipping new build.');
return true;
}
return false;
}
/**
* @throws \Exception
*/
private function createProjectData(InputInterface $input): void
{
$this->createProjectDataDirectory();
$this->createProjectDataDirectory($input);
$this->createEnvironmentFile($input);
$this->buildDockerComposeFile($input);
$this->buildDockerfiles($input);
@ -127,15 +138,13 @@ class SpinCommand extends AbstractSpinnerCommand
/**
* @throws \Exception
*/
private function createProjectDataDirectory(): void
private function createProjectDataDirectory(InputInterface $input): void
{
$projectData = $this->config->getFilePaths()->get('projectData');
if (!$projectData instanceof FilePath) {
throw new \Exception('Invalid project data directory provided.');
}
mkdir($projectData->getProvidedPath(), 0777, true);
mkdir(
$this->config->getDataDirectory(),
0777,
true
);
}
/**
@ -143,21 +152,15 @@ class SpinCommand extends AbstractSpinnerCommand
*/
private function createEnvironmentFile(InputInterface $input): void
{
$projectEnv = $this->config->getFilePaths()->get('projectEnv');
if (!$projectEnv instanceof FilePath) {
throw new \Exception('Invalid project environment file provided.');
}
file_put_contents(
$projectEnv->getProvidedPath(),
$this->config->getDataDirectory() . '/.env',
sprintf(
file_get_contents($this->config->getFilePaths()->get('envTemplate')->getAbsolutePath()),
$this->config->getFilePaths()->get('project')->getAbsolutePath(),
$this->config->getConfigFileContents('.template.env'),
$this->projectWorkPath,
$input->getArgument('name'),
$this->config->getPhpVersion($input),
$this->getPort('php'),
$this->getPort('nginx'),
$this->ports['php'],
$this->ports['nginx'],
)
);
}
@ -167,12 +170,7 @@ class SpinCommand extends AbstractSpinnerCommand
*/
private function buildDockerComposeFile(InputInterface $input): void
{
$this->createProjectPhpFpmDirectory();
if ($this->config->isServerEnabled($input)) {
$this->createProjectNginxDirectory();
(new NginxDockerFileBuilder($this->config))->build($input)->save();
}
$this->createProjectDataSubDirectory('php-fpm');
(new DockerComposeFileBuilder($this->config))->build($input)->save();
}
@ -182,43 +180,18 @@ class SpinCommand extends AbstractSpinnerCommand
*/
private function buildDockerfiles(InputInterface $input): void
{
if ($this->config->isServerEnabled($input)) {
$this->createProjectDataSubDirectory('nginx');
(new NginxDockerFileBuilder($this->config))->build($input)->save();
}
(new PHPDockerFileBuilder($this->config))->build($input)->save();
}
/**
* @throws \Exception
*/
private function createProjectPhpFpmDirectory(): void
private function createProjectDataSubDirectory(string $directory): void
{
$projectData = $this->config->getFilePaths()->get('projectData');
if (!$projectData instanceof FilePath) {
throw new \Exception('Invalid project data directory provided.');
if (!file_exists($this->config->getDataDirectory() . '/' . $directory)) {
mkdir($this->config->getDataDirectory() . '/' . $directory, 0777, true);
}
if (!file_exists($projectData->getProvidedPath() . '/php-fpm')) {
mkdir($projectData->getProvidedPath() . '/php-fpm', 0777, true);
}
}
/**
* @throws \Exception
*/
private function createProjectNginxDirectory(): void
{
$projectData = $this->config->getFilePaths()->get('projectData');
if (!$projectData instanceof FilePath) {
throw new \Exception('Invalid project data directory provided.');
}
if (!file_exists($projectData->getProvidedPath() . '/nginx')) {
mkdir($projectData->getProvidedPath() . '/nginx', 0777, true);
}
}
private function getPort(string $service): ?int
{
return $this->ports[$service];
}
}