diff --git a/config/php-fpm/Dockerfile b/config/php-fpm/Dockerfile index 5932aea..ea0b7c7 100644 --- a/config/php-fpm/Dockerfile +++ b/config/php-fpm/Dockerfile @@ -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 diff --git a/config/php-fpm/Node.Dockerfile b/config/php-fpm/Node.Dockerfile new file mode 100644 index 0000000..41371ca --- /dev/null +++ b/config/php-fpm/Node.Dockerfile @@ -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 \ No newline at end of file diff --git a/config/spinner.yaml b/config/spinner.yaml index 079f3c6..f6e8320 100644 --- a/config/spinner.yaml +++ b/config/spinner.yaml @@ -1,4 +1,7 @@ options: environment: php: - version: 8.4 \ No newline at end of file + version: 8.4 + node: + enabled: true + version: 20 \ No newline at end of file diff --git a/src/Classes/Collection/FilePathCollection.php b/src/Classes/Collection/FilePathCollection.php index 335b06c..98fe025 100644 --- a/src/Classes/Collection/FilePathCollection.php +++ b/src/Classes/Collection/FilePathCollection.php @@ -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; } diff --git a/src/Classes/Config/Config.php b/src/Classes/Config/Config.php new file mode 100644 index 0000000..5fde43f --- /dev/null +++ b/src/Classes/Config/Config.php @@ -0,0 +1,65 @@ +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'); + } +} \ No newline at end of file diff --git a/src/Classes/File/AbstractFileBuilder.php b/src/Classes/File/AbstractFileBuilder.php new file mode 100644 index 0000000..d7ac42b --- /dev/null +++ b/src/Classes/File/AbstractFileBuilder.php @@ -0,0 +1,31 @@ +path->getAbsolutePath(), $this->content); + } + + + protected function addNewLine(): void + { + $this->content .= "\r\n\r\n"; + } +} \ No newline at end of file diff --git a/src/Classes/File/DockerFileBuilder.php b/src/Classes/File/DockerFileBuilder.php new file mode 100644 index 0000000..69bdd24 --- /dev/null +++ b/src/Classes/File/DockerFileBuilder.php @@ -0,0 +1,28 @@ +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/FileBuilderInterface.php b/src/Classes/File/Interface/FileBuilderInterface.php new file mode 100644 index 0000000..bf7645b --- /dev/null +++ b/src/Classes/File/Interface/FileBuilderInterface.php @@ -0,0 +1,13 @@ +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'), ]); } } \ No newline at end of file diff --git a/src/Command/SpinCommand.php b/src/Command/SpinCommand.php index b56fec4..a389c99 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\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 ); }