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 \
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-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:
php:
version: 8.4
node:
enabled: true
version: 20

View file

@ -4,21 +4,21 @@ declare(strict_types=1);
namespace Loom\Spinner\Classes\Collection;
use Loom\Spinner\Classes\File\SpinnerFilePath;
use Loom\Utility\Collection\AbstractCollection;
use Loom\Utility\FilePath\FilePath;
class FilePathCollection extends AbstractCollection
{
public function __construct(array $items = [])
{
$items = array_filter($items, function ($item) {
return $item instanceof SpinnerFilePath;
return $item instanceof FilePath;
});
parent::__construct($items);
}
public function get(string $key): ?SpinnerFilePath
public function get(string $key): ?FilePath
{
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;
use Loom\Spinner\Classes\Collection\FilePathCollection;
use Loom\Spinner\Classes\Config\Config;
use Loom\Spinner\Classes\File\SpinnerFilePath;
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\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@ -16,16 +18,18 @@ use Symfony\Component\Yaml\Yaml;
class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
{
protected SymfonyStyle $style;
protected System $system;
protected Config $config;
protected FilePathCollection $filePaths;
protected string $rootDirectory;
protected SymfonyStyle $style;
protected System $system;
public function __construct()
{
$this->rootDirectory = dirname(__DIR__, 2);
$this->system = new System();
$this->setFilePaths();
$this->config = new Config($this->filePaths);
parent::__construct();
}
@ -42,32 +46,11 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
return Command::FAILURE;
}
if ($input->hasArgument('path')) {
$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;
}
$this->filePaths->add($projectDirectory, 'project');
if (!$this->validatePathArgument($input)) {
return Command::FAILURE;
}
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->validateNameArgument($input);
return Command::SUCCESS;
}
@ -98,15 +81,72 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
/**
* @throws \Exception
*/
protected function getDefaultPhpVersion(): ?float
protected function getEnvironmentOption(string $service, string $option): mixed
{
if (!$this->filePaths->get('defaultSpinnerConfig')?->exists()) {
throw new \Exception('Default spinner configuration file not found.');
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];
}
}
}
$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
@ -122,8 +162,9 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface
'envTemplate' => new SpinnerFilePath('config/.template.env'),
'data' => new SpinnerFilePath('data'),
'phpYamlTemplate' => new SpinnerFilePath('config/php.yaml'),
'phpFpmDockerfileTemplate' => new SpinnerFilePath('config/php-fpm/Dockerfile'),
'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;
use Loom\Spinner\Classes\File\SpinnerFilePath;
use Loom\Spinner\Classes\OS\PortGenerator;
use Symfony\Component\Console\Attribute\AsCommand;
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\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
#[AsCommand(name: 'spin:up', description: 'Spin up a new development environment')]
class SpinCommand extends AbstractSpinnerCommand
@ -37,18 +37,20 @@ class SpinCommand extends AbstractSpinnerCommand
$this
->addArgument('name', InputArgument::REQUIRED, 'The name for your Docker container.')
->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
{
if (parent::execute($input, $output)) {
return Command::FAILURE;
}
if ($this->filePaths->get('projectData')->exists()) {
$this->style->warning('Project data already exists. Skipping new build.');
if ($this->projectDataExists()) {
return Command::SUCCESS;
}
@ -63,18 +65,55 @@ class SpinCommand extends AbstractSpinnerCommand
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);
$this->buildDockerComposeFile($input);
return false;
}
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
{
$projectEnv = $this->filePaths->get('projectEnv');
if (!$projectEnv instanceof SpinnerFilePath) {
throw new \Exception('Invalid project environment file provided.');
}
file_put_contents(
$this->filePaths->get('projectEnv')->getProvidedPath(),
$projectEnv->getProvidedPath(),
sprintf(
file_get_contents($this->filePaths->get('envTemplate')->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')) {
mkdir($this->filePaths->get('projectData')->getProvidedPath() . '/php-fpm', 0777, true);
$projectData = $this->filePaths->get('projectData');
$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(
$this->filePaths->get('projectDockerCompose')->getProvidedPath(),
$projectDockerCompose->getProvidedPath(),
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 = 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(
$this->filePaths->get('projectData')->getProvidedPath() . '/php-fpm/Dockerfile',
$projectDataPath->getProvidedPath() . '/php-fpm/Dockerfile',
$phpFpmDockerfileTemplate
);
}