From 7d8fcac15eb13097f93b528ec5c55592595ac915 Mon Sep 17 00:00:00 2001 From: Daniel Winning Date: Sun, 13 Apr 2025 12:59:44 +0100 Subject: [PATCH] Cleaner config and path usage, added dev notes --- README.md | 26 ++++- config/php-fpm/Dockerfile | 6 - config/php-fpm/Node.Dockerfile | 2 +- config/php-fpm/XDebug.Dockerfile | 6 + src/Classes/Config/Config.php | 106 +++++++++++++----- src/Classes/File/AbstractFileBuilder.php | 9 +- src/Classes/File/DockerFileBuilder.php | 28 ----- .../File/Interface/DataPathInterface.php | 10 ++ .../File/Interface/FileBuilderInterface.php | 2 +- src/Classes/File/PHPDockerFileBuilder.php | 50 +++++++++ src/Command/AbstractSpinnerCommand.php | 92 ++++----------- src/Command/SpinCommand.php | 58 +++++----- 12 files changed, 224 insertions(+), 171 deletions(-) create mode 100644 config/php-fpm/XDebug.Dockerfile delete mode 100644 src/Classes/File/DockerFileBuilder.php create mode 100644 src/Classes/File/Interface/DataPathInterface.php create mode 100644 src/Classes/File/PHPDockerFileBuilder.php diff --git a/README.md b/README.md index 886ff16..eede6a1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ # Loom | Spinner -An environment management application for PHP developers. \ No newline at end of file +An environment management application for PHP developers. + +## Dev Notes + +**Argument priority:** + +- Those passed explicitly in the CLI commands +- Any set within `{projectDirectory}/spinner.yaml` +- Fall back to `/config/spinner.yaml` + +## Commands + +### `spin:up` + +#### Arguments + +- `name` - **Required**: The name for your Docker containers. +- `path` - **Required**: The **absolute path** to your project root directory. +- `php` - **Optional**: If passed, sets the PHP version used by your container. Can be overridden +by creating a `spinner.yaml` file in your project root directory and defining the key `options.environment.php.version` + +#### Options + +- `node-disabled` - **Optional**: Disables Node. Can also define the key `options.environment.node.enabled` in your +`spinner.yaml` file. \ No newline at end of file diff --git a/config/php-fpm/Dockerfile b/config/php-fpm/Dockerfile index ea0b7c7..bc84266 100644 --- a/config/php-fpm/Dockerfile +++ b/config/php-fpm/Dockerfile @@ -16,12 +16,6 @@ RUN apt-get -qq update && apt-get -qq install -y \ RUN docker-php-ext-configure intl > /dev/null RUN docker-php-ext-install mysqli pdo pdo_mysql sockets intl exif bcmath > /dev/null -#RUN pecl install xdebug redis \ -# && docker-php-ext-enable redis - -#COPY ./xdebug.ini.tmp "${PHP_INI_DIR}/conf.d/xdebug.ini" -#RUN docker-php-ext-install opcache > /dev/null -#COPY ./opcache.ini "${PHP_INI_DIR}/conf.d" COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer WORKDIR /var/www/html \ No newline at end of file diff --git a/config/php-fpm/Node.Dockerfile b/config/php-fpm/Node.Dockerfile index 41371ca..6c63e67 100644 --- a/config/php-fpm/Node.Dockerfile +++ b/config/php-fpm/Node.Dockerfile @@ -1,6 +1,6 @@ ENV NVM_DIR /root/.nvm -RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \ +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash \ && . "$NVM_DIR/nvm.sh" \ && nvm install ${NODE_VERSION} \ && nvm use ${NODE_VERSION} diff --git a/config/php-fpm/XDebug.Dockerfile b/config/php-fpm/XDebug.Dockerfile new file mode 100644 index 0000000..cd28869 --- /dev/null +++ b/config/php-fpm/XDebug.Dockerfile @@ -0,0 +1,6 @@ +RUN pecl install xdebug redis \ + && docker-php-ext-enable redis + +COPY ./xdebug.ini.tmp "${PHP_INI_DIR}/conf.d/xdebug.ini" +RUN docker-php-ext-install opcache > /dev/null +COPY ./opcache.ini "${PHP_INI_DIR}/conf.d" \ No newline at end of file diff --git a/src/Classes/Config/Config.php b/src/Classes/Config/Config.php index 5fde43f..45fb06c 100644 --- a/src/Classes/Config/Config.php +++ b/src/Classes/Config/Config.php @@ -5,28 +5,86 @@ declare(strict_types=1); namespace Loom\Spinner\Classes\Config; use Loom\Spinner\Classes\Collection\FilePathCollection; +use Loom\Spinner\Classes\File\Interface\DataPathInterface; +use Loom\Spinner\Classes\File\SpinnerFilePath; +use Loom\Utility\FilePath\FilePath; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Yaml\Yaml; class Config { - public function __construct(private readonly FilePathCollection $filePaths) + private FilePathCollection $filePaths; + + public function __construct() { + $this->setFilePaths(); + } + + public function getFilePaths(): FilePathCollection + { + return $this->filePaths; + } + + public function getFilePath(string $key): ?FilePath + { + return $this->filePaths->get($key); + } + + public function addFilePath(FilePath $filePath, string $key): void + { + $this->filePaths->add($filePath, $key); } /** * @throws \Exception */ - protected function getEnvironmentOption(string $service, string $option): mixed + public function getPhpVersion(InputInterface $input): ?float { - if ($this->filePaths->get('projectCustomConfig')?->exists()) { - $config = Yaml::parseFile( - $this->filePaths->get('projectCustomConfig')->getAbsolutePath() - )['options']['environment'] ?? null; + if ($input->getOption('php')) { + return (float) $input->getOption('php'); + } - if ($config) { - if (isset($config[$service][$option])) { - return $config[$service][$option]; + return (float) $this->getEnvironmentOption('php', 'version'); + } + + /** + * @throws \Exception + */ + public function isNodeEnabled(InputInterface $input): bool + { + if ($input->getOption('node-disabled')) { + return false; + } + + return $this->getEnvironmentOption('node', 'enabled'); + } + + /** + * @throws \Exception + */ + public function getNodeVersion(InputInterface $input): ?int + { + if ($input->getOption('node')) { + return (int) $input->getOption('node'); + } + + return (int) $this->getEnvironmentOption('node','version'); + } + + /** + * @throws \Exception + */ + public function getEnvironmentOption(string $service, string $option): mixed + { + $projectCustomConfig = $this->filePaths->get('projectCustomConfig'); + + if ($projectCustomConfig->exists()) { + $customConfig = Yaml::parseFile($projectCustomConfig->getAbsolutePath())['options']['environment'] + ?? null; + + if ($customConfig) { + if (isset($customConfig[$service][$option])) { + return $customConfig[$service][$option]; } } } @@ -37,29 +95,23 @@ class Config /** * @throws \Exception */ - protected function getDefaultConfig() + protected function getDefaultConfig(): ?array { return Yaml::parseFile($this->filePaths->get('defaultSpinnerConfig')?->getAbsolutePath())['options'] ?? null; } - /** - * @throws \Exception - */ - public function getDefaultPhpVersionArgument(): ?float + private function setFilePaths(): void { - 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'); + $this->filePaths = new FilePathCollection([ + 'config' => new SpinnerFilePath('config'), + 'defaultSpinnerConfig' => new SpinnerFilePath('config/spinner.yaml'), + 'envTemplate' => new SpinnerFilePath('config/.template.env'), + 'data' => new SpinnerFilePath('data'), + 'phpYamlTemplate' => new SpinnerFilePath('config/php.yaml'), + 'phpFpmDataDirectory' => new SpinnerFilePath('config/php-fpm'), + DataPathInterface::CONFIG_PHP_FPM_DIRECTORY => new SpinnerFilePath(DataPathInterface::CONFIG_PHP_FPM_DIRECTORY), + 'nodeDockerfileTemplate' => new SpinnerFilePath('config/php-fpm/Node.Dockerfile'), + ]); } } \ No newline at end of file diff --git a/src/Classes/File/AbstractFileBuilder.php b/src/Classes/File/AbstractFileBuilder.php index d7ac42b..26deb87 100644 --- a/src/Classes/File/AbstractFileBuilder.php +++ b/src/Classes/File/AbstractFileBuilder.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Loom\Spinner\Classes\File; -use Loom\Spinner\Classes\Collection\FilePathCollection; +use Loom\Spinner\Classes\Config\Config; use Loom\Spinner\Classes\File\Interface\FileBuilderInterface; use Symfony\Component\Console\Input\InputInterface; @@ -12,15 +12,16 @@ abstract class AbstractFileBuilder implements FileBuilderInterface { protected string $content; - public function __construct(private readonly SpinnerFilePath $path, protected readonly FilePathCollection $filePaths) + public function __construct(protected SpinnerFilePath $path, protected Config $config) { + return $this; } - abstract public function build(InputInterface $input): void; + abstract public function build(InputInterface $input): AbstractFileBuilder; public function save(): void { - file_put_contents($this->path->getAbsolutePath(), $this->content); + file_put_contents($this->path->getProvidedPath(), $this->content); } diff --git a/src/Classes/File/DockerFileBuilder.php b/src/Classes/File/DockerFileBuilder.php deleted file mode 100644 index 69bdd24..0000000 --- a/src/Classes/File/DockerFileBuilder.php +++ /dev/null @@ -1,28 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/src/Classes/File/Interface/DataPathInterface.php b/src/Classes/File/Interface/DataPathInterface.php new file mode 100644 index 0000000..1f71075 --- /dev/null +++ b/src/Classes/File/Interface/DataPathInterface.php @@ -0,0 +1,10 @@ +getFilePath('projectPhpFpmDockerfile'); + + if (!$projectPhpFpmDockerfile instanceof SpinnerFilePath) { + throw new \Exception('Project PHP-FPM Dockerfile not found'); + } + + return parent::__construct($projectPhpFpmDockerfile, $config); + } + + /** + * @throws \Exception + */ + public function build(InputInterface $input): PHPDockerFileBuilder + { + $this->setInitialContent(); + + $this->content = str_replace('${PHP_VERSION}', (string) $this->config->getPhpVersion($input), $this->content); + + if ($this->config->isNodeEnabled($input)) { + $this->content .= "\r\n\r\n" . file_get_contents($this->config->getFilePaths()->get('nodeDockerfileTemplate')->getAbsolutePath()); + $this->content = str_replace('${NODE_VERSION}', (string) $this->config->getNodeVersion($input), $this->content); + } + + return $this; + } + + private function setInitialContent(): void + { + $this->content = file_get_contents( + $this->config->getFilePaths()->get(DataPathInterface::CONFIG_PHP_FPM_DIRECTORY)->getAbsolutePath() + ); + } +} \ No newline at end of file diff --git a/src/Command/AbstractSpinnerCommand.php b/src/Command/AbstractSpinnerCommand.php index 8b553ae..32603d3 100644 --- a/src/Command/AbstractSpinnerCommand.php +++ b/src/Command/AbstractSpinnerCommand.php @@ -19,17 +19,13 @@ use Symfony\Component\Yaml\Yaml; class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface { 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); + $this->config = new Config(); parent::__construct(); } @@ -55,61 +51,17 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface return Command::SUCCESS; } - /** - * @throws \Exception - */ - protected function getDefaultConfig() - { - if (!$this->filePaths->get('defaultSpinnerConfig')?->exists()) { - throw new \Exception('Default spinner configuration file not found.'); - } - - return Yaml::parseFile($this->filePaths->get('defaultSpinnerConfig')->getAbsolutePath())['options'] ?? null; - } - protected function buildDockerComposeCommand(string $command, bool $daemon = true): string { return sprintf( 'cd %s && docker-compose --env-file=%s %s%s', - $this->filePaths->get('projectData')->getAbsolutePath(), - $this->filePaths->get('projectEnv')->getAbsolutePath(), + $this->config->getFilePaths()->get('projectData')->getAbsolutePath(), + $this->config->getFilePaths()->get('projectEnv')->getAbsolutePath(), $command, $daemon ? ' -d' : '' ); } - /** - * @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 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')) { @@ -121,7 +73,7 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface return false; } - $this->filePaths->add($projectDirectory, 'project'); + $this->config->addFilePath($projectDirectory, 'project'); } return true; @@ -130,20 +82,32 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface private function validateNameArgument(InputInterface $input): void { if ($input->hasArgument('name')) { - $this->filePaths->add( + $this->config->addFilePath( new SpinnerFilePath(sprintf('data/environments/%s', $input->getArgument('name'))), 'projectData' ); - $this->filePaths->add( + $this->config->addFilePath( new SpinnerFilePath(sprintf('data/environments/%s/.env', $input->getArgument('name'))), 'projectEnv' ); - $this->filePaths->add( + $this->config->addFilePath( 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'), + $this->config->addFilePath( + new SpinnerFilePath(sprintf('data/environments/%s/php-fpm', $input->getArgument('name'))), + 'projectPhpFpmDirectory' + ); + $this->config->addFilePath( + new SpinnerFilePath(sprintf('data/environments/%s/php-fpm/Dockerfile', $input->getArgument('name'))), + 'projectPhpFpmDockerfile' + ); + $this->config->addFilePath( + new SpinnerFilePath(sprintf('data/environments/%s/php-fpm/Node.Dockerfile', $input->getArgument('name'))), + 'projectPhpFpmNodeDockerfile' + ); + $this->config->addFilePath( + new FilePath($this->config->getFilePaths()->get('project')->getAbsolutePath() . DIRECTORY_SEPARATOR . 'spinner.yaml'), 'projectCustomConfig' ); } @@ -153,18 +117,4 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface { $this->style = new SymfonyStyle($input, $output); } - - private function setFilePaths(): void - { - $this->filePaths = new FilePathCollection([ - 'config' => new SpinnerFilePath('config'), - 'defaultSpinnerConfig' => new SpinnerFilePath('config/spinner.yaml'), - 'envTemplate' => new SpinnerFilePath('config/.template.env'), - 'data' => new SpinnerFilePath('data'), - 'phpYamlTemplate' => new SpinnerFilePath('config/php.yaml'), - 'phpFpmDataDirectory' => new SpinnerFilePath('config/php-fpm'), - 'phpFpmDockerfileTemplate' => new SpinnerFilePath('config/php-fpm/Dockerfile'), - 'nodeDockerfileTemplate' => new SpinnerFilePath('config/php-fpm/Node.Dockerfile'), - ]); - } } \ No newline at end of file diff --git a/src/Command/SpinCommand.php b/src/Command/SpinCommand.php index a389c99..3f6bbd8 100644 --- a/src/Command/SpinCommand.php +++ b/src/Command/SpinCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Loom\Spinner\Command; +use Loom\Spinner\Classes\File\PHPDockerFileBuilder; use Loom\Spinner\Classes\File\SpinnerFilePath; use Loom\Spinner\Classes\OS\PortGenerator; use Symfony\Component\Console\Attribute\AsCommand; @@ -37,8 +38,19 @@ 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->config->getDefaultPhpVersionArgument()) - ->addOption('node-disabled', null, InputOption::VALUE_NONE, 'Set this flag to disable Node.js for your environment.'); + ->addOption( + 'php', + null, + InputOption::VALUE_OPTIONAL, + 'The PHP version to use (e.g., 8.0).' + ) + ->addOption( + 'node-disabled', + null, + InputOption::VALUE_NONE, + 'Set this flag to disable Node.js for your environment.' + ) + ->addOption('node', null, InputOption::VALUE_OPTIONAL, 'The Node.js version to use (e.g. 20).'); } /** @@ -55,7 +67,7 @@ class SpinCommand extends AbstractSpinnerCommand } $this->style->success("Spinning up a new development environment..."); - + $this->style->text('Creating project data...'); $this->createProjectData($input); $command = $this->buildDockerComposeCommand(sprintf('-p %s up', $input->getArgument('name'))); @@ -67,13 +79,13 @@ class SpinCommand extends AbstractSpinnerCommand protected function projectDataExists(): bool { - if (!$this->filePaths->get('projectData')->exists()) { + if ($this->config->getFilePaths()->get('projectData')->exists()) { $this->style->warning('Project data already exists. Skipping new build.'); - return false; + return true; } - return true; + return false; } /** @@ -92,7 +104,7 @@ class SpinCommand extends AbstractSpinnerCommand */ private function createProjectDataDirectory(): void { - $projectData = $this->filePaths->get('projectData'); + $projectData = $this->config->getFilePaths()->get('projectData'); if (!$projectData instanceof SpinnerFilePath) { throw new \Exception('Invalid project data directory provided.'); @@ -106,7 +118,7 @@ class SpinCommand extends AbstractSpinnerCommand */ private function createEnvironmentFile(InputInterface $input): void { - $projectEnv = $this->filePaths->get('projectEnv'); + $projectEnv = $this->config->getFilePaths()->get('projectEnv'); if (!$projectEnv instanceof SpinnerFilePath) { throw new \Exception('Invalid project environment file provided.'); @@ -115,8 +127,8 @@ class SpinCommand extends AbstractSpinnerCommand file_put_contents( $projectEnv->getProvidedPath(), sprintf( - file_get_contents($this->filePaths->get('envTemplate')->getAbsolutePath()), - $this->filePaths->get('project')->getAbsolutePath(), + file_get_contents($this->config->getFilePaths()->get('envTemplate')->getAbsolutePath()), + $this->config->getFilePaths()->get('project')->getAbsolutePath(), $input->getArgument('name'), $input->getOption('php'), $this->getPort('php'), @@ -129,8 +141,8 @@ class SpinCommand extends AbstractSpinnerCommand */ private function buildDockerComposeFile(): void { - $projectData = $this->filePaths->get('projectData'); - $projectDockerCompose = $this->filePaths->get('projectDockerCompose'); + $projectData = $this->config->getFilePaths()->get('projectData'); + $projectDockerCompose = $this->config->getFilePaths()->get('projectDockerCompose'); if (!$projectData instanceof SpinnerFilePath || !$projectDockerCompose instanceof SpinnerFilePath) { throw new \Exception('Invalid project data directory provided.'); @@ -141,7 +153,7 @@ class SpinCommand extends AbstractSpinnerCommand } file_put_contents( $projectDockerCompose->getProvidedPath(), - file_get_contents($this->filePaths->get('phpYamlTemplate')->getAbsolutePath()) + file_get_contents($this->config->getFilePaths()->get('phpYamlTemplate')->getAbsolutePath()) ); } @@ -150,25 +162,7 @@ class SpinCommand extends AbstractSpinnerCommand */ 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( - $projectDataPath->getProvidedPath() . '/php-fpm/Dockerfile', - $phpFpmDockerfileTemplate - ); + (new PHPDockerFileBuilder($this->config))->build($input)->save(); } private function getPort(string $service): int