, * rewarmed: list, * warnings: list, * ok: bool, * } */ public function run(?OutputStyle $output = null): array { $running = (string) config('projectsend.version'); $result = [ 'from' => '', 'to' => $running, 'migrated' => false, 'cleared' => [], 'rewarmed' => [], 'warnings' => [], 'ok' => false, ]; // Caught rather than left to surface as a stack trace: the most // likely failure here is a database that is unreachable or refusing // the credentials, and forty frames of Laravel internals is a worse // answer to that than one sentence naming it. try { $migrated = $this->artisan('migrate', ['--force' => true], $output) === 0; } catch (Throwable $exception) { $result['warnings'][] = 'The database migration failed: '.$exception->getMessage(); $result['warnings'][] = 'Nothing else was changed.'; return $result; } if (! $migrated) { $result['warnings'][] = 'The database migration failed. Nothing else was changed.'; return $result; } $result['migrated'] = true; $result['from'] = $this->previouslyApplied(); $this->roles->ensure(); // Not parity with the old entrypoint line — a fix. The release zip // ships no public/storage symlink (the build refuses symlinks // outright, they do not survive zipping), so an installation that // followed UPDATE.md's "unpack beside it and swap the directories" // advice loses the link entirely, and nothing in the documented // sequence ever put it back. $this->artisan('storage:link', ['--force' => true], $output); // Read before anything is cleared: this is the only moment the // question "was this installation using the optional caches?" can // still be answered. $warm = $this->warmCaches(); foreach (['config:clear', 'clear-compiled', 'event:clear', 'route:clear', 'view:clear'] as $command) { if ($this->artisan($command, [], $output) === 0) { $result['cleared'][] = $command; } } if ($warm['config']) { $result['warnings'][] = 'A cached configuration was found and cleared. Do not run config:cache on this' .' application — it stops TRUSTED_PROXIES from being read at all. See INSTALL.md.'; } foreach ($this->cachesToRewarm($warm) as $command) { // A route table that will not compile is a slower site; a // failed update is a broken one. Never fatal. if ($this->artisan($command, [], $output) === 0) { $result['rewarmed'][] = $command; continue; } $result['warnings'][] = "{$command} failed, so that cache is not in place. The site runs without it."; } $this->settings->set(Setting::AppliedVersion, $running); $this->settings->set(Setting::AppliedVersionAt, now()->toIso8601String()); // Last, and after every cache operation above — see the class // docblock. A worker only learns to exit by reading this signal. $this->artisan('queue:restart', [], $output); $result['ok'] = true; return $result; } /** * What the previous run of this recorded, or '' when there was none. * * Swallows failures on purpose: by this point the schema is current, * but a cache store that is momentarily unreachable must not fail a * container boot over a line of reporting. */ private function previouslyApplied(): string { try { $applied = $this->settings->get(Setting::AppliedVersion); return is_string($applied) ? $applied : ''; } catch (Throwable) { return ''; } } /** * Which optional caches this installation had in place. * * Asked through the framework's own path accessors rather than the * filenames: `routes-v7.php` is an internal that changes with major * versions. And by file_exists() rather than $app->routesAreCached(), * which memoises at bootstrap and would still answer true after * route:clear ran in this same process. * * @return array{route: bool, event: bool, config: bool} */ protected function warmCaches(): array { return [ 'route' => file_exists($this->app->getCachedRoutesPath()), 'event' => file_exists($this->app->getCachedEventsPath()), 'config' => file_exists($this->app->getCachedConfigPath()), ]; } /** * Views have no honest signal of their own — storage/framework/views * fills up from ordinary traffic, cached deliberately or not. So the * route and event caches stand in for the set: their presence means * this installation followed INSTALL.md's "Making it faster", which * lists all three together. A container caches none of them and so * rebuilds nothing, which keeps boot as fast as it is today. * * config:cache is never rebuilt, at any time, for any installation. * * @param array{route: bool, event: bool, config: bool} $warm * @return list */ private function cachesToRewarm(array $warm): array { if (! $warm['route'] && ! $warm['event']) { return []; } return ['route:cache', 'event:cache', 'view:cache']; } /** * Protected so a test can watch the sequence without running it. The * ordering constraints in run() are invisible in its result and * catastrophic when wrong, and an ordered list of calls is the only * thing that can assert them. * * @param array $parameters */ protected function artisan(string $command, array $parameters = [], ?OutputStyle $output = null): int { return Artisan::call($command, $parameters, $output); } }