diff --git a/bin/spinner b/bin/spinner index d80180e..7a5a3a9 100755 --- a/bin/spinner +++ b/bin/spinner @@ -1,6 +1,7 @@ #!/usr/bin/env php add(new SpinCommand()); +$application->add(new DestroyCommand()); try { $application->run(); diff --git a/src/Classes/Config/Config.php b/src/Classes/Config/Config.php index b992ecf..6b39f00 100644 --- a/src/Classes/Config/Config.php +++ b/src/Classes/Config/Config.php @@ -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'), - ]); - } } \ No newline at end of file diff --git a/src/Classes/File/AbstractFileBuilder.php b/src/Classes/File/AbstractFileBuilder.php index 5ec53c5..deb504f 100644 --- a/src/Classes/File/AbstractFileBuilder.php +++ b/src/Classes/File/AbstractFileBuilder.php @@ -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); } diff --git a/src/Classes/File/DockerComposeFileBuilder.php b/src/Classes/File/DockerComposeFileBuilder.php index 96d785a..4a9300b 100644 --- a/src/Classes/File/DockerComposeFileBuilder.php +++ b/src/Classes/File/DockerComposeFileBuilder.php @@ -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; } diff --git a/src/Classes/File/Interface/DataPathInterface.php b/src/Classes/File/Interface/DataPathInterface.php deleted file mode 100644 index 16e201f..0000000 --- a/src/Classes/File/Interface/DataPathInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -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; } diff --git a/src/Classes/File/PHPDockerFileBuilder.php b/src/Classes/File/PHPDockerFileBuilder.php index 53605e8..4929781 100644 --- a/src/Classes/File/PHPDockerFileBuilder.php +++ b/src/Classes/File/PHPDockerFileBuilder.php @@ -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'); } } \ No newline at end of file diff --git a/src/Command/AbstractSpinnerCommand.php b/src/Command/AbstractSpinnerCommand.php index 9b8606c..1c031dc 100644 --- a/src/Command/AbstractSpinnerCommand.php +++ b/src/Command/AbstractSpinnerCommand.php @@ -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); diff --git a/src/Command/DestroyCommand.php b/src/Command/DestroyCommand.php new file mode 100644 index 0000000..250d894 --- /dev/null +++ b/src/Command/DestroyCommand.php @@ -0,0 +1,33 @@ +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; + } +} \ No newline at end of file diff --git a/src/Command/SpinCommand.php b/src/Command/SpinCommand.php index f3d38e3..bc97dde 100644 --- a/src/Command/SpinCommand.php +++ b/src/Command/SpinCommand.php @@ -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]; } } \ No newline at end of file