diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a59195..8e5ebe48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ All notable changes to this project will be documented in this file. * Removed propTypes. * Upgraded redux-toolkit and how api slices are generated. * Fixed redux-toolkit cache handling. -* Add Taskfile +* Added Taskfile +* Added update command. * Added (Client) online-check to public. * Updated developer documentation. diff --git a/README.md b/README.md index bbfd47e5..14719d35 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,12 @@ To get started with the development setup, run the following task command: ```shell task site-install + +# or if you want to load fixtures as well +task site-install-with-fixtures ``` -If you want to load fixtures, use the command (use the option `--yes` for auto-confirming). +If you want to load fixtures manually, use the command (`--yes` for auto-confirming): ```shell task fixtures:load --yes @@ -92,7 +95,7 @@ The fixtures have an admin user: with the password: "apasswo The fixtures have an editor user: with the password: "apassword". -The fixtures have the image-text template, and two screen layouts: full screen and "two boxes". +The fixtures have the image-text template, and two screen layouts: "full screen" and "two boxes". ## Production setup @@ -107,6 +110,12 @@ APP_SECRET= TODO: Add further production instructions: Build steps, release.json, etc. +Use the `app:update` command to migrate and update templates to latest version: + +```shell +docker compose exec phpfpm bin/console app:update --no-interaction +``` + ## Coding standards Before a PR can be merged it has to pass the GitHub Actions checks. See `.github/workflows` for workflows that should diff --git a/Taskfile.yml b/Taskfile.yml index bdb7e896..1289339a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -216,3 +216,8 @@ tasks: desc: "Generate the RTK Query api slices (in assets/shared/redux/)." cmds: - task compose -- exec node npx @rtk-query/codegen-openapi /app/assets/shared/redux/openapi-config.js + + app:update: + desc: "Migrate to latest database schema and update installed templates" + cmds: + - task compose -- exec phpfpm bin/console app:update --no-interaction diff --git a/src/Command/StatusCommand.php b/src/Command/StatusCommand.php new file mode 100644 index 00000000..73e82847 --- /dev/null +++ b/src/Command/StatusCommand.php @@ -0,0 +1,56 @@ +getApplication(); + + if (null === $application) { + $io->error('Application not initialized.'); + + return Command::FAILURE; + } + + $io->title('Migrations status'); + + // Check status for migrations. + $command = new ArrayInput([ + 'command' => 'doctrine:migrations:up-to-date', + ]); + $application->doRun($command, $output); + + $io->writeln(''); + $io->writeln(''); + $io->writeln(''); + $io->title('Templates status'); + + // List status for templates. + $command = new ArrayInput([ + 'command' => 'app:templates:list', + '--status' => true, + ]); + $application->doRun($command, $output); + + $io->info('Run app:update to update migrations and templates.'); + + return Command::SUCCESS; + } +} diff --git a/src/Command/TemplatesListCommand.php b/src/Command/TemplatesListCommand.php index 4466c9d2..849758fa 100644 --- a/src/Command/TemplatesListCommand.php +++ b/src/Command/TemplatesListCommand.php @@ -9,6 +9,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -24,10 +25,17 @@ public function __construct( parent::__construct(); } + protected function configure(): void + { + $this->addOption('status', 's', InputOption::VALUE_NONE, 'Get status of installed templates.'); + } + final protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); + $status = $input->getOption('status'); + try { $templates = $this->templateService->getCoreTemplates(); @@ -39,12 +47,22 @@ final protected function execute(InputInterface $input, OutputInterface $output) $customTemplates = $this->templateService->getCustomTemplates(); - $io->table(['ID', 'Title', 'Status', 'Type'], array_map(fn (TemplateData $templateData) => [ - $templateData->id, - $templateData->title, - $templateData->installed ? 'Installed' : 'Not Installed', - $templateData->type, - ], array_merge($templates, $customTemplates))); + $allTemplates = array_merge($templates, $customTemplates); + + if ($status) { + $numberOfTemplates = count($allTemplates); + $numberOfInstallledTemplates = count(array_filter($allTemplates, fn ($entry): bool => $entry->installed)); + $text = $numberOfInstallledTemplates.' / '.$numberOfTemplates.' templates installed.'; + + $io->success($text); + } else { + $io->table(['ID', 'Title', 'Status', 'Type'], array_map(fn (TemplateData $templateData) => [ + $templateData->id, + $templateData->title, + $templateData->installed ? 'Installed' : 'Not Installed', + $templateData->type, + ], $allTemplates)); + } return Command::SUCCESS; } catch (\Exception $e) { diff --git a/src/Command/TemplatesUpdateCommand.php b/src/Command/TemplatesUpdateCommand.php new file mode 100644 index 00000000..eb520741 --- /dev/null +++ b/src/Command/TemplatesUpdateCommand.php @@ -0,0 +1,40 @@ +templateService->getAllTemplates(); + + foreach ($templates as $templateToUpdate) { + $this->templateService->updateTemplate($templateToUpdate); + } + + $io->success('Updated all installed templates'); + + return Command::SUCCESS; + } +} diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index 9e85424c..ad52ca0d 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -4,9 +4,78 @@ namespace App\Command; -class UpdateCommand +use App\Service\TemplateService; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; + +#[AsCommand( + name: 'app:update', + description: 'Run required updates.', +)] +class UpdateCommand extends Command { - // TODO: Test that migrations have been run. - // TODO: Run test of status for templates. No templates = clean install. Install all? - // TODO: Update existing templates. + private TemplateService $templateService; + + public function __construct(TemplateService $templateService, ?string $name = null) + { + parent::__construct($name); + $this->templateService = $templateService; + } + + final protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $isInteractive = $input->isInteractive(); + + $application = $this->getApplication(); + + if (null === $application) { + $io->error('Application not initialized.'); + + return Command::FAILURE; + } + + $command = new ArrayInput([ + 'command' => 'doctrine:migrations:migrate', + ]); + $command->setInteractive($isInteractive); + $result = $application->doRun($command, $output); + + if (0 !== $result) { + $io->info('Update aborted. Migrations need to run for the system to work. Run doctrine:migrations:migrate or rerun app:update to migrate.'); + + return Command::FAILURE; + } + + $allTemplates = $this->templateService->getAllTemplates(); + $installedTemplates = array_filter($allTemplates, fn ($entry): bool => $entry->installed); + + // If no installed templates, we assume that this is a new installation and offer to install all templates. + if ($isInteractive && 0 === count($installedTemplates)) { + $question = new ConfirmationQuestion('No templates are installed. Install all '.count($allTemplates).'?'); + $installAll = $io->askQuestion($question); + + if ('yes' === $installAll) { + $io->info('Installing all templates...'); + $command = new ArrayInput([ + 'command' => 'app:templates:install', + '--all' => true, + ]); + $application->doRun($command, $output); + } + } else { + $io->info('Updating existing template...'); + $command = new ArrayInput([ + 'command' => 'app:templates:update', + ]); + $application->doRun($command, $output); + } + + return Command::SUCCESS; + } } diff --git a/src/Service/TemplateService.php b/src/Service/TemplateService.php index c19b7162..d99286dd 100644 --- a/src/Service/TemplateService.php +++ b/src/Service/TemplateService.php @@ -43,6 +43,20 @@ public function installTemplate(TemplateData $templateData, bool $update = false $this->entityManager->flush(); } + public function updateTemplate(TemplateData $templateData): void + { + $template = $templateData->templateEntity; + + // Ignore templates that do not exist in the database. + if (null === $template) { + return; + } + + $template->setTitle($templateData->title); + + $this->entityManager->flush(); + } + public function getAllTemplates(): array { return array_merge($this->getCoreTemplates(), $this->getCustomTemplates());