1 <?php declare(strict_types=1);
3 namespace Cli\Commands;
5 use Cli\Services\AppLocator;
6 use Cli\Services\ArtisanRunner;
7 use Cli\Services\ComposerLocator;
8 use Cli\Services\Paths;
9 use Cli\Services\ProgramRunner;
10 use Cli\Services\RequirementsValidator;
11 use Symfony\Component\Console\Command\Command;
12 use Symfony\Component\Console\Input\InputInterface;
13 use Symfony\Component\Console\Input\InputOption;
14 use Symfony\Component\Console\Output\OutputInterface;
16 class UpdateCommand extends Command
18 protected function configure(): void
20 $this->setName('update');
21 $this->setDescription('Update an existing BookStack instance.');
22 $this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to update', '');
26 * @throws CommandError
28 protected function execute(InputInterface $input, OutputInterface $output): int
30 $appDir = AppLocator::require($input->getOption('app-directory'));
31 $output->writeln("<info>Checking system requirements...</info>");
32 RequirementsValidator::validate();
34 $output->writeln("<info>Checking local Git repository is active...</info>");
35 $this->ensureGitRepoExists($appDir);
37 $output->writeln("<info>Checking composer exists...</info>");
38 $composerLocator = new ComposerLocator($appDir);
39 $composer = $composerLocator->getProgram();
40 if (!$composer->isFound()) {
41 $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
42 $composerLocator->download();
45 $output->writeln("<info>Fetching latest code via Git...</info>");
46 $this->updateCodeUsingGit($appDir);
48 $output->writeln("<info>Installing PHP dependencies via composer...</info>");
49 $this->installComposerDependencies($composer, $appDir);
51 $output->writeln("<info>Running database migrations...</info>");
52 $artisan = (new ArtisanRunner($appDir));
53 $artisan->run(['migrate', '--force']);
55 $output->writeln("<info>Clearing app caches...</info>");
56 $artisan->run(['cache:clear']);
57 $artisan->run(['config:clear']);
58 $artisan->run(['view:clear']);
60 $output->writeln("<success>Your BookStack instance at [{$appDir}] has been updated!</success>");
62 return Command::SUCCESS;
66 * @throws CommandError
68 protected function updateCodeUsingGit(string $appDir): void
70 $errors = (new ProgramRunner('git', '/usr/bin/git'))
73 ->runCapturingStdErr([
75 'pull', '-q', 'origin', 'release',
79 throw new CommandError("Failed git pull with errors:\n" . $errors);
84 * @throws CommandError
86 protected function installComposerDependencies(ProgramRunner $composer, string $appDir): void
88 $errors = $composer->runCapturingStdErr([
90 '--no-dev', '-n', '-q', '--no-progress',
95 throw new CommandError("Failed composer install with errors:\n" . $errors);
99 protected function ensureGitRepoExists(string $appDir): void
101 $expectedPath = Paths::join($appDir, '.git');
102 if (!is_dir($expectedPath)) {
103 $message = "Could not find a local git repository, it does not look like this instance is managed via common means.\n";
104 $message .= "If you are running BookStack via a docker container, you should update following the advised process for the docker container image in use.\n";
105 $message .= "This typically involves pulling and using an updated docker container image.";
107 throw new CommandError($message);