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;
12 use Symfony\Component\Console\Command\Command;
13 use Symfony\Component\Console\Input\InputInterface;
14 use Symfony\Component\Console\Input\InputOption;
15 use Symfony\Component\Console\Output\OutputInterface;
17 class UpdateCommand extends Command
19 protected function configure(): void
21 $this->setName('update');
22 $this->setDescription('Update an existing BookStack instance.');
23 $this->addOption('app-directory', null, InputOption::VALUE_OPTIONAL, 'BookStack install directory to update', '');
27 * @throws CommandError
29 protected function execute(InputInterface $input, OutputInterface $output): int
31 $appDir = AppLocator::require($input->getOption('app-directory'));
32 $output->writeln("<info>Checking system requirements...</info>");
33 RequirementsValidator::validate();
35 $output->writeln("<info>Checking local Git repository is active...</info>");
36 $this->ensureGitRepoExists($appDir);
38 $output->writeln("<info>Checking composer exists...</info>");
39 $composerLocator = new ComposerLocator($appDir);
40 $composer = $composerLocator->getProgram();
41 if (!$composer->isFound()) {
42 $output->writeln("<info>Composer does not exist, downloading a local copy...</info>");
43 $composerLocator->download();
46 $cliPath = Phar::running(false);
47 $cliPreUpdateHash = $cliPath ? hash_file('sha256', $cliPath) : '';
49 $output->writeln("<info>Fetching latest code via Git...</info>");
50 $this->updateCodeUsingGit($appDir);
52 $cliPostUpdateHash = $cliPath ? hash_file('sha256', $cliPath) : '';
53 if ($cliPostUpdateHash !== $cliPreUpdateHash) {
54 $output->writeln("<error>System CLI file changed during update!\nRe-run the update command to complete the update process.</error>");
55 return Command::FAILURE;
58 $output->writeln("<info>Installing PHP dependencies via composer...</info>");
59 $this->installComposerDependencies($composer, $appDir);
61 $output->writeln("<info>Running database migrations...</info>");
62 $artisan = (new ArtisanRunner($appDir));
63 $artisan->run(['migrate', '--force']);
65 $output->writeln("<info>Clearing app caches...</info>");
66 $artisan->run(['cache:clear']);
67 $artisan->run(['config:clear']);
68 $artisan->run(['view:clear']);
70 $output->writeln("<success>Your BookStack instance at [{$appDir}] has been updated!</success>");
72 return Command::SUCCESS;
76 * @throws CommandError
78 protected function updateCodeUsingGit(string $appDir): void
80 $errors = (new ProgramRunner('git', '/usr/bin/git'))
83 ->runCapturingStdErr([
85 'pull', '-q', 'origin', 'release',
89 throw new CommandError("Failed git pull with errors:\n" . $errors);
94 * @throws CommandError
96 protected function installComposerDependencies(ProgramRunner $composer, string $appDir): void
98 $errors = $composer->runCapturingStdErr([
100 '--no-dev', '-n', '-q', '--no-progress',
105 throw new CommandError("Failed composer install with errors:\n" . $errors);
109 protected function ensureGitRepoExists(string $appDir): void
111 $expectedPath = Paths::join($appDir, '.git');
112 if (!is_dir($expectedPath)) {
113 $message = "Could not find a local git repository, it does not look like this instance is managed via common means.\n";
114 $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";
115 $message .= "This typically involves pulling and using an updated docker container image.";
117 throw new CommandError($message);