Began clean up effort, refactoring to move config related code to separate class and add classes for file building - WIP

This commit is contained in:
Daniel Winning 2025-04-13 10:43:30 +01:00
parent 00b593d6d3
commit be41aeb81d
10 changed files with 308 additions and 61 deletions

View file

@ -13,16 +13,6 @@ RUN apt-get -qq update && apt-get -qq install -y \
bash \ bash \
dnsutils dnsutils
#ENV NVM_DIR /root/.nvm
#ENV NODE_VERSION 20
#
#RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \
# && . "$NVM_DIR/nvm.sh" \
# && nvm install $NODE_VERSION \
# && nvm use $NODE_VERSION
#
#COPY --from=node:20 /usr/local/bin/npx /usr/local/bin/npx
RUN docker-php-ext-configure intl > /dev/null RUN docker-php-ext-configure intl > /dev/null
RUN docker-php-ext-install mysqli pdo pdo_mysql sockets intl exif bcmath > /dev/null RUN docker-php-ext-install mysqli pdo pdo_mysql sockets intl exif bcmath > /dev/null

View file

@ -0,0 +1,8 @@
ENV NVM_DIR /root/.nvm
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \
&& . "$NVM_DIR/nvm.sh" \
&& nvm install ${NODE_VERSION} \
&& nvm use ${NODE_VERSION}
COPY --from=node:${NODE_VERSION} /usr/local/bin/npx /usr/local/bin/npx

View file

@ -2,3 +2,6 @@ options:
environment: environment:
php: php:
version: 8.4 version: 8.4
node:
enabled: true
version: 20

View file

@ -4,21 +4,21 @@ declare(strict_types=1);
namespace Loom\Spinner\Classes\Collection; namespace Loom\Spinner\Classes\Collection;
use Loom\Spinner\Classes\File\SpinnerFilePath;
use Loom\Utility\Collection\AbstractCollection; use Loom\Utility\Collection\AbstractCollection;
use Loom\Utility\FilePath\FilePath;
class FilePathCollection extends AbstractCollection class FilePathCollection extends AbstractCollection
{ {
public function __construct(array $items = []) public function __construct(array $items = [])
{ {
$items = array_filter($items, function ($item) { $items = array_filter($items, function ($item) {
return $item instanceof SpinnerFilePath; return $item instanceof FilePath;
}); });
parent::__construct($items); parent::__construct($items);
} }
public function get(string $key): ?SpinnerFilePath public function get(string $key): ?FilePath
{ {
return $this->items[$key] ?? null; return $this->items[$key] ?? null;
} }

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Loom\Spinner\Classes\Config;
use Loom\Spinner\Classes\Collection\FilePathCollection;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Yaml\Yaml;
class Config
{
public function __construct(private readonly FilePathCollection $filePaths)
{
}
/**
* @throws \Exception
*/
protected function getEnvironmentOption(string $service, string $option): mixed
{
if ($this->filePaths->get('projectCustomConfig')?->exists()) {
$config = Yaml::parseFile(
$this->filePaths->get('projectCustomConfig')->getAbsolutePath()
)['options']['environment'] ?? null;
if ($config) {
if (isset($config[$service][$option])) {
return $config[$service][$option];
}
}
}
return $this->getDefaultConfig()['environment'][$service][$option] ?? null;
}
/**
* @throws \Exception
*/
protected function getDefaultConfig()
{
return Yaml::parseFile($this->filePaths->get('defaultSpinnerConfig')?->getAbsolutePath())['options']
?? null;
}
/**
* @throws \Exception
*/
public function getDefaultPhpVersionArgument(): ?float
{
return $this->getEnvironmentOption('php', 'version');
}
/**
* @throws \Exception
*/
protected function isNodeEnabled(InputInterface $input): bool
{
if ($input->getOption('node-disabled')) {
return true;
}
return $this->getEnvironmentOption('node', 'enabled');
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Loom\Spinner\Classes\File;
use Loom\Spinner\Classes\Collection\FilePathCollection;
use Loom\Spinner\Classes\File\Interface\FileBuilderInterface;
use Symfony\Component\Console\Input\InputInterface;
abstract class AbstractFileBuilder implements FileBuilderInterface
{
protected string $content;
public function __construct(private readonly SpinnerFilePath $path, protected readonly FilePathCollection $filePaths)
{
}
abstract public function build(InputInterface $input): void;
public function save(): void
{
file_put_contents($this->path->getAbsolutePath(), $this->content);
}
protected function addNewLine(): void
{
$this->content .= "\r\n\r\n";
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Loom\Spinner\Classes\File;
use Loom\Spinner\Classes\Collection\FilePathCollection;
use Symfony\Component\Console\Input\InputInterface;
class DockerFileBuilder extends AbstractFileBuilder
{
public function __construct(FilePathCollection $filePaths)
{
$projectDataDir = $filePaths->get('projectData');
if (!$projectDataDir instanceof SpinnerFilePath) {
return;
}
parent::__construct($projectDataDir, $filePaths);
}
public function build(InputInterface $input): void
{
$this->content = file_get_contents($this->filePaths->get('phpFpmDockerfileTemplate')->getAbsolutePath());
$this->content = str_replace('${PHP_VERSION}', $input->getOption('php'), $this->content);
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Loom\Spinner\Classes\File\Interface;
use Symfony\Component\Console\Input\InputInterface;
interface FileBuilderInterface
{
public function build(InputInterface $input): void;
public function save(): void;
}

View file

@ -5,9 +5,11 @@ declare(strict_types=1);
namespace Loom\Spinner\Command; namespace Loom\Spinner\Command;
use Loom\Spinner\Classes\Collection\FilePathCollection; use Loom\Spinner\Classes\Collection\FilePathCollection;
use Loom\Spinner\Classes\Config\Config;
use Loom\Spinner\Classes\File\SpinnerFilePath; use Loom\Spinner\Classes\File\SpinnerFilePath;
use Loom\Spinner\Classes\OS\System; use Loom\Spinner\Classes\OS\System;
use Loom\Spinner\Command\Interface\ConsoleCommandInterface; use Loom\Spinner\Command\Interface\ConsoleCommandInterface;
use Loom\Utility\FilePath\FilePath;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -16,16 +18,18 @@ use Symfony\Component\Yaml\Yaml;
class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
{ {
protected SymfonyStyle $style; protected Config $config;
protected System $system;
protected FilePathCollection $filePaths; protected FilePathCollection $filePaths;
protected string $rootDirectory; protected string $rootDirectory;
protected SymfonyStyle $style;
protected System $system;
public function __construct() public function __construct()
{ {
$this->rootDirectory = dirname(__DIR__, 2); $this->rootDirectory = dirname(__DIR__, 2);
$this->system = new System(); $this->system = new System();
$this->setFilePaths(); $this->setFilePaths();
$this->config = new Config($this->filePaths);
parent::__construct(); parent::__construct();
} }
@ -42,32 +46,11 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
return Command::FAILURE; return Command::FAILURE;
} }
if ($input->hasArgument('path')) { if (!$this->validatePathArgument($input)) {
$projectDirectory = new SpinnerFilePath($input->getArgument('path'));
if (!$projectDirectory->exists() || !$projectDirectory->isDirectory()) {
$this->style->error('The provided path is not a valid directory.');
return Command::FAILURE; return Command::FAILURE;
} }
$this->filePaths->add($projectDirectory, 'project'); $this->validateNameArgument($input);
}
if ($input->hasArgument('name')) {
$this->filePaths->add(
new SpinnerFilePath(sprintf('data/environments/%s', $input->getArgument('name'))),
'projectData'
);
$this->filePaths->add(
new SpinnerFilePath(sprintf('data/environments/%s/.env', $input->getArgument('name'))),
'projectEnv'
);
$this->filePaths->add(
new SpinnerFilePath(sprintf('data/environments/%s/docker-compose.yml', $input->getArgument('name'))),
'projectDockerCompose'
);
}
return Command::SUCCESS; return Command::SUCCESS;
} }
@ -98,15 +81,72 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
/** /**
* @throws \Exception * @throws \Exception
*/ */
protected function getDefaultPhpVersion(): ?float protected function getEnvironmentOption(string $service, string $option): mixed
{ {
if (!$this->filePaths->get('defaultSpinnerConfig')?->exists()) { if ($this->filePaths->get('projectCustomConfig')?->exists()) {
throw new \Exception('Default spinner configuration file not found.'); $config = Yaml::parseFile(
$this->filePaths->get('projectCustomConfig')->getAbsolutePath()
)['options']['environment'] ?? null;
if ($config) {
if (isset($config[$service][$option])) {
return $config[$service][$option];
}
}
} }
$config = $this->getDefaultConfig(); return $this->getDefaultConfig()['environment'][$service][$option] ?? null;
}
return $config['environment']['php']['version'] ?? null; /**
* @throws \Exception
*/
protected function isNodeEnabled(InputInterface $input): bool
{
if ($input->getOption('node-disabled')) {
return true;
}
return $this->getEnvironmentOption('node', 'enabled');
}
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->filePaths->add($projectDirectory, 'project');
}
return true;
}
private function validateNameArgument(InputInterface $input): void
{
if ($input->hasArgument('name')) {
$this->filePaths->add(
new SpinnerFilePath(sprintf('data/environments/%s', $input->getArgument('name'))),
'projectData'
);
$this->filePaths->add(
new SpinnerFilePath(sprintf('data/environments/%s/.env', $input->getArgument('name'))),
'projectEnv'
);
$this->filePaths->add(
new SpinnerFilePath(sprintf('data/environments/%s/docker-compose.yml', $input->getArgument('name'))),
'projectDockerCompose'
);
$this->filePaths->add(
new FilePath($this->filePaths->get('project')->getAbsolutePath() . DIRECTORY_SEPARATOR . 'spinner.yaml'),
'projectCustomConfig'
);
}
} }
private function setStyle(InputInterface $input, OutputInterface $output): void private function setStyle(InputInterface $input, OutputInterface $output): void
@ -122,8 +162,9 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
'envTemplate' => new SpinnerFilePath('config/.template.env'), 'envTemplate' => new SpinnerFilePath('config/.template.env'),
'data' => new SpinnerFilePath('data'), 'data' => new SpinnerFilePath('data'),
'phpYamlTemplate' => new SpinnerFilePath('config/php.yaml'), 'phpYamlTemplate' => new SpinnerFilePath('config/php.yaml'),
'phpFpmDockerfileTemplate' => new SpinnerFilePath('config/php-fpm/Dockerfile'),
'phpFpmDataDirectory' => new SpinnerFilePath('config/php-fpm'), 'phpFpmDataDirectory' => new SpinnerFilePath('config/php-fpm'),
'phpFpmDockerfileTemplate' => new SpinnerFilePath('config/php-fpm/Dockerfile'),
'nodeDockerfileTemplate' => new SpinnerFilePath('config/php-fpm/Node.Dockerfile'),
]); ]);
} }
} }

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Loom\Spinner\Command; namespace Loom\Spinner\Command;
use Loom\Spinner\Classes\File\SpinnerFilePath;
use Loom\Spinner\Classes\OS\PortGenerator; use Loom\Spinner\Classes\OS\PortGenerator;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -11,7 +12,6 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
#[AsCommand(name: 'spin:up', description: 'Spin up a new development environment')] #[AsCommand(name: 'spin:up', description: 'Spin up a new development environment')]
class SpinCommand extends AbstractSpinnerCommand class SpinCommand extends AbstractSpinnerCommand
@ -37,18 +37,20 @@ class SpinCommand extends AbstractSpinnerCommand
$this $this
->addArgument('name', InputArgument::REQUIRED, 'The name for your Docker container.') ->addArgument('name', InputArgument::REQUIRED, 'The name for your Docker container.')
->addArgument('path', InputArgument::REQUIRED, 'The absolute path to your projects root directory.') ->addArgument('path', InputArgument::REQUIRED, 'The absolute path to your projects root directory.')
->addOption('php', null, InputOption::VALUE_REQUIRED, 'The PHP version to use (e.g., 8.0).', $this->getDefaultPhpVersion()); ->addOption('php', null, InputOption::VALUE_REQUIRED, 'The PHP version to use (e.g., 8.0).', $this->config->getDefaultPhpVersionArgument())
->addOption('node-disabled', null, InputOption::VALUE_NONE, 'Set this flag to disable Node.js for your environment.');
} }
/**
* @throws \Exception
*/
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {
if (parent::execute($input, $output)) { if (parent::execute($input, $output)) {
return Command::FAILURE; return Command::FAILURE;
} }
if ($this->filePaths->get('projectData')->exists()) { if ($this->projectDataExists()) {
$this->style->warning('Project data already exists. Skipping new build.');
return Command::SUCCESS; return Command::SUCCESS;
} }
@ -63,18 +65,55 @@ class SpinCommand extends AbstractSpinnerCommand
return Command::SUCCESS; return Command::SUCCESS;
} }
private function createProjectData(InputInterface $input): void protected function projectDataExists(): bool
{ {
mkdir($this->filePaths->get('projectData')->getProvidedPath(), 0777, true); if (!$this->filePaths->get('projectData')->exists()) {
$this->style->warning('Project data already exists. Skipping new build.');
$this->createEnvironmentFile($input); return false;
$this->buildDockerComposeFile($input);
} }
return true;
}
/**
* @throws \Exception
*/
private function createProjectData(InputInterface $input): void
{
$this->createProjectDataDirectory();
$this->createEnvironmentFile($input);
$this->buildDockerComposeFile();
$this->buildDockerfile($input);
}
/**
* @throws \Exception
*/
private function createProjectDataDirectory(): void
{
$projectData = $this->filePaths->get('projectData');
if (!$projectData instanceof SpinnerFilePath) {
throw new \Exception('Invalid project data directory provided.');
}
mkdir($projectData->getProvidedPath(), 0777, true);
}
/**
* @throws \Exception
*/
private function createEnvironmentFile(InputInterface $input): void private function createEnvironmentFile(InputInterface $input): void
{ {
$projectEnv = $this->filePaths->get('projectEnv');
if (!$projectEnv instanceof SpinnerFilePath) {
throw new \Exception('Invalid project environment file provided.');
}
file_put_contents( file_put_contents(
$this->filePaths->get('projectEnv')->getProvidedPath(), $projectEnv->getProvidedPath(),
sprintf( sprintf(
file_get_contents($this->filePaths->get('envTemplate')->getAbsolutePath()), file_get_contents($this->filePaths->get('envTemplate')->getAbsolutePath()),
$this->filePaths->get('project')->getAbsolutePath(), $this->filePaths->get('project')->getAbsolutePath(),
@ -85,20 +124,49 @@ class SpinCommand extends AbstractSpinnerCommand
); );
} }
private function buildDockerComposeFile(InputInterface $input): void /**
* @throws \Exception
*/
private function buildDockerComposeFile(): void
{ {
if (!file_exists($this->filePaths->get('projectData')->getProvidedPath() . '/php-fpm')) { $projectData = $this->filePaths->get('projectData');
mkdir($this->filePaths->get('projectData')->getProvidedPath() . '/php-fpm', 0777, true); $projectDockerCompose = $this->filePaths->get('projectDockerCompose');
if (!$projectData instanceof SpinnerFilePath || !$projectDockerCompose instanceof SpinnerFilePath) {
throw new \Exception('Invalid project data directory provided.');
}
if (!file_exists($projectData->getProvidedPath() . '/php-fpm')) {
mkdir($projectData->getProvidedPath() . '/php-fpm', 0777, true);
} }
file_put_contents( file_put_contents(
$this->filePaths->get('projectDockerCompose')->getProvidedPath(), $projectDockerCompose->getProvidedPath(),
file_get_contents($this->filePaths->get('phpYamlTemplate')->getAbsolutePath()) file_get_contents($this->filePaths->get('phpYamlTemplate')->getAbsolutePath())
); );
}
/**
* @throws \Exception
*/
private function buildDockerfile(InputInterface $input): void
{
$phpFpmDockerfileTemplate = file_get_contents($this->filePaths->get('phpFpmDockerfileTemplate')->getAbsolutePath()); $phpFpmDockerfileTemplate = file_get_contents($this->filePaths->get('phpFpmDockerfileTemplate')->getAbsolutePath());
$phpFpmDockerfileTemplate = str_replace('${PHP_VERSION}', $input->getOption('php'), $phpFpmDockerfileTemplate); $phpFpmDockerfileTemplate = str_replace('${PHP_VERSION}', $input->getOption('php'), $phpFpmDockerfileTemplate);
if ($this->isNodeEnabled($input)) {
// Add contents of Node.Dockerfile from /config/php-fpm/Node.Dockerfile
$phpFpmDockerfileTemplate .= "\r\n\r\n" . file_get_contents($this->filePaths->get('nodeDockerfileTemplate')->getAbsolutePath());
$phpFpmDockerfileTemplate = str_replace('${NODE_VERSION}', (string) $this->getEnvironmentOption('node', 'version'), $phpFpmDockerfileTemplate);
}
$projectDataPath = $this->filePaths->get('projectData');
if (!$projectDataPath instanceof SpinnerFilePath) {
return;
}
file_put_contents( file_put_contents(
$this->filePaths->get('projectData')->getProvidedPath() . '/php-fpm/Dockerfile', $projectDataPath->getProvidedPath() . '/php-fpm/Dockerfile',
$phpFpmDockerfileTemplate $phpFpmDockerfileTemplate
); );
} }