From b8ad06cda32a48460ccb3cf236fbfcddc522d2bd Mon Sep 17 00:00:00 2001 From: Daniel Winning Date: Sat, 12 Apr 2025 17:28:57 +0100 Subject: [PATCH] Create basic project data structure from env template --- composer.json | 3 +- config/.template.env | 3 ++ config/docker-compose.sample.yaml | 26 ++++++++++++ config/docker-compose.yml | 38 ----------------- config/php-fpm/Dockerfile | 41 ++++++++++++++++++ config/php.yaml | 10 +++++ config/spinner.yaml | 4 ++ data/.gitignore | 2 + src/Classes/File/SpinnerFilePath.php | 9 +++- src/Command/AbstractSpinnerCommand.php | 58 +++++++++++++++++++++++++- src/Command/SpinCommand.php | 31 ++++++++++++-- 11 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 config/.template.env create mode 100644 config/docker-compose.sample.yaml delete mode 100644 config/docker-compose.yml create mode 100644 config/php-fpm/Dockerfile create mode 100644 config/php.yaml create mode 100644 config/spinner.yaml create mode 100644 data/.gitignore diff --git a/composer.json b/composer.json index be6f07a..92eee0d 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "require": { "loomlabs/utility.collection": "^1.0", "loomlabs/utility.filepath": "^1.1", - "symfony/console": "^7.2" + "symfony/console": "^7.2", + "symfony/yaml": "^7.2" }, "require-dev": { "phpunit/phpunit": "^12.1" diff --git a/config/.template.env b/config/.template.env new file mode 100644 index 0000000..60fecf1 --- /dev/null +++ b/config/.template.env @@ -0,0 +1,3 @@ +PROJECT_DIRECTORY=%s +PROJECT_NAME=%s +PHP_VERSION=%s \ No newline at end of file diff --git a/config/docker-compose.sample.yaml b/config/docker-compose.sample.yaml new file mode 100644 index 0000000..f8ed8ff --- /dev/null +++ b/config/docker-compose.sample.yaml @@ -0,0 +1,26 @@ +services: +# nginx: +# build: +# context: ./nginx +# ports: +# - ${NGINX_PORT}:80 +# volumes: +# - ${PROJECT_DIRECTORY}:/var/www/html:cached +# - ./nginx/conf.d:/etc/nginx/conf.d +# database: +# image: mysql:8.0 +# command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci +# ports: +# - ${MYSQL_PORT}:3306 +# expose: +# - "3306" +# environment: +# MYSQL_ROOT_PASSWORD: docker +# volumes: +# - ./data/${PROJECT_NAME}/mysql:/var/lib/mysql:cached +# cache: +# image: redis:latest +# ports: +# - ${REDIS_PORT}:6379 +# volumes: +# - ./data/${PROJECT_NAME}/redis:/data:cached \ No newline at end of file diff --git a/config/docker-compose.yml b/config/docker-compose.yml deleted file mode 100644 index 7cc0ae3..0000000 --- a/config/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: "3.9" - -services: - nginx: - build: - context: ./nginx - ports: - - ${NGINX_PORT}:80 - volumes: - - ${PROJECT_DIRECTORY}:/var/www/html:cached - - ./nginx/conf.d:/etc/nginx/conf.d - php: - build: - context: ./php-fpm - working_dir: /var/www/html - extra_hosts: - - host.docker.internal:host-gateway - volumes: - - ${PROJECT_DIRECTORY}:/var/www/html:cached - ports: - - ${PHP_PORT}:9003 - database: - image: mysql:8.0 - command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci - ports: - - ${MYSQL_PORT}:3306 - expose: - - "3306" - environment: - MYSQL_ROOT_PASSWORD: docker - volumes: - - ./data/${PROJECT_NAME}/mysql:/var/lib/mysql:cached - cache: - image: redis:latest - ports: - - ${REDIS_PORT}:6379 - volumes: - - ./data/${PROJECT_NAME}/redis:/data:cached \ No newline at end of file diff --git a/config/php-fpm/Dockerfile b/config/php-fpm/Dockerfile new file mode 100644 index 0000000..b4a1759 --- /dev/null +++ b/config/php-fpm/Dockerfile @@ -0,0 +1,41 @@ +ARG PHP_VERSION + +ENV PHP_VERSION ${PHP_VERSION} + +FROM php:${PHP_VERSION}-fpm + +RUN mkdir -p /var/www/html + +RUN apt-get -qq update && apt-get -qq install -y \ + apt-transport-https \ + git \ + curl \ + zip \ + unzip \ + libicu-dev \ + imagick \ + nano \ + 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 opcache > /dev/null + +RUN pecl install xdebug redis \ + && docker-php-ext-enable redis + +COPY ./xdebug.ini.tmp "${PHP_INI_DIR}/conf.d/xdebug.ini" +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.yaml b/config/php.yaml new file mode 100644 index 0000000..b99d0bd --- /dev/null +++ b/config/php.yaml @@ -0,0 +1,10 @@ +php: + build: + context: ./php-fpm + working_dir: /var/www/html + extra_hosts: + - host.docker.internal:host-gateway + volumes: + - ${PROJECT_DIRECTORY}:/var/www/html:cached + ports: + - ${PHP_PORT}:9003 \ No newline at end of file diff --git a/config/spinner.yaml b/config/spinner.yaml new file mode 100644 index 0000000..079f3c6 --- /dev/null +++ b/config/spinner.yaml @@ -0,0 +1,4 @@ +options: + environment: + php: + version: 8.4 \ No newline at end of file diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/src/Classes/File/SpinnerFilePath.php b/src/Classes/File/SpinnerFilePath.php index c148cf8..a95e8a7 100644 --- a/src/Classes/File/SpinnerFilePath.php +++ b/src/Classes/File/SpinnerFilePath.php @@ -8,8 +8,13 @@ use Loom\Utility\FilePath\FilePath; class SpinnerFilePath extends FilePath { - public function __construct(string $projectPath) + public function __construct(string $path) { - parent::__construct(sprintf('%s%s%s', dirname(__DIR__, 3), DIRECTORY_SEPARATOR, $projectPath)); + parent::__construct(sprintf('%s%s%s', dirname(__DIR__, 3), DIRECTORY_SEPARATOR, $path)); + } + + public function getProvidedPath(): string + { + return $this->path; } } \ No newline at end of file diff --git a/src/Command/AbstractSpinnerCommand.php b/src/Command/AbstractSpinnerCommand.php index 27a654c..460395b 100644 --- a/src/Command/AbstractSpinnerCommand.php +++ b/src/Command/AbstractSpinnerCommand.php @@ -8,11 +8,11 @@ use Loom\Spinner\Classes\Collection\FilePathCollection; 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; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Yaml\Yaml; class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface { @@ -34,6 +34,8 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface { $this->setStyle($input, $output); + $this->style->title('Loom Spinner'); + if (!$this->system->isDockerEngineRunning()) { $this->style->error('It looks like the Docker Engine is not running. Please start it and try again.'); @@ -41,7 +43,7 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface } if ($input->hasArgument('path')) { - $projectDirectory = new FilePath($input->getArgument('path')); + $projectDirectory = new SpinnerFilePath($input->getArgument('path')); if (!$projectDirectory->exists() || !$projectDirectory->isDirectory()) { $this->style->error('The provided path is not a valid directory.'); @@ -52,9 +54,58 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface $this->filePaths->add($projectDirectory, 'project'); } + 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' + ); + } + 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 %s --env-file=%s%s', + $this->filePaths->get('config')->getAbsolutePath(), + $command, + $this->filePaths->get('projectEnv')->getAbsolutePath() + ?? $this->filePaths->get('projectEnv')->getProvidedPath(), + $daemon ? ' -d' : '' + ); + } + + /** + * @throws \Exception + */ + protected function getDefaultPhpVersion(): ?float + { + if (!$this->filePaths->get('defaultSpinnerConfig')?->exists()) { + throw new \Exception('Default spinner configuration file not found.'); + } + + $config = $this->getDefaultConfig(); + + return $config['environment']['php']['version'] ?? null; + } + private function setStyle(InputInterface $input, OutputInterface $output): void { $this->style = new SymfonyStyle($input, $output); @@ -64,6 +115,9 @@ class AbstractSpinnerCommand extends Command implements ConsoleCommandInterface { $this->filePaths = new FilePathCollection([ 'config' => new SpinnerFilePath('config'), + 'defaultSpinnerConfig' => new SpinnerFilePath('config/spinner.yaml'), + 'envTemplate' => new SpinnerFilePath('config/.template.env'), + 'data' => new SpinnerFilePath('data'), ]); } } \ No newline at end of file diff --git a/src/Command/SpinCommand.php b/src/Command/SpinCommand.php index b19234a..995934a 100644 --- a/src/Command/SpinCommand.php +++ b/src/Command/SpinCommand.php @@ -8,24 +8,49 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -#[AsCommand(name: 'spin', description: 'Spin up a new development environment')] +#[AsCommand(name: 'spin:up', description: 'Spin up a new development environment')] class SpinCommand extends AbstractSpinnerCommand { + /** + * @throws \Exception + */ protected function configure(): void { $this ->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()); } protected function execute(InputInterface $input, OutputInterface $output): int { - if (!parent::execute($input, $output)) { + if (parent::execute($input, $output)) { return Command::FAILURE; } + if ($this->filePaths->get('projectData')->exists()) { + $this->style->warning('Project data already exists. Skipping new build.'); + + return Command::SUCCESS; + } + + $this->style->success("Spinning up a new development environment..."); + + mkdir($this->filePaths->get('projectData')->getProvidedPath(), 0777, true); + + file_put_contents( + $this->filePaths->get('projectEnv')->getProvidedPath(), + sprintf( + file_get_contents($this->filePaths->get('envTemplate')->getAbsolutePath()), + $this->filePaths->get('project')->getAbsolutePath(), + $input->getArgument('name'), + $input->getOption('php') + ) + ); + return Command::SUCCESS; } } \ No newline at end of file