diff --git a/src/Console/Commands/Concerns/RunsArtisanCommand.php b/src/Console/Commands/Concerns/RunsArtisanCommand.php new file mode 100644 index 0000000000..e09e5e8ccb --- /dev/null +++ b/src/Console/Commands/Concerns/RunsArtisanCommand.php @@ -0,0 +1,44 @@ +find(false) ?: 'php', + defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan', + ], + explode(' ', $command) + ); + + $result = Process::forever()->run($components, function ($type, $line) use ($writeOutput) { + if ($writeOutput) { + $this->output->write($line); + } + }); + + // We're doing this instead of ->throw() so we can control the output of errors. + if ($result->failed()) { + if (Str::of($result->output())->contains('Unknown database')) { + error('The database does not exist. Please create it before running this command.'); + exit(1); + } + + error('Failed to run command: '.$command); + $this->output->write($result->output()); + exit(1); + } + + return $result; + } +} diff --git a/src/Console/Commands/Eloquent/InstallEloquentCollectionTrees.php b/src/Console/Commands/Eloquent/InstallEloquentCollectionTrees.php new file mode 100644 index 0000000000..7e49f50899 --- /dev/null +++ b/src/Console/Commands/Eloquent/InstallEloquentCollectionTrees.php @@ -0,0 +1,43 @@ +runArtisanCommand('vendor:publish --tag=statamic-eloquent-navigation-tree-migrations'); + $this->runArtisanCommand('migrate'); + + $this->switchToEloquentDriver('collection_trees'); + }, + message: 'Migrating collection trees...' + ); + + $this->infoMessage('Configured collection trees'); + + if ($this->shouldImport('collection trees')) { + spin( + callback: fn () => $this->runArtisanCommand('statamic:eloquent:import-collections --force --only-collection-trees'), + message: 'Importing existing collections...' + ); + + $this->infoMessage('Imported existing collection trees'); + } + } + + public function hasBeenMigrated(): bool + { + return config('statamic.eloquent-driver.collection_trees.driver') === 'eloquent'; + } +} diff --git a/src/Console/Commands/Eloquent/InstallEloquentCollections.php b/src/Console/Commands/Eloquent/InstallEloquentCollections.php new file mode 100644 index 0000000000..63299df29b --- /dev/null +++ b/src/Console/Commands/Eloquent/InstallEloquentCollections.php @@ -0,0 +1,43 @@ +runArtisanCommand('vendor:publish --tag=statamic-eloquent-collection-migrations'); + $this->runArtisanCommand('migrate'); + + $this->switchToEloquentDriver('collections'); + }, + message: 'Migrating collections...' + ); + + $this->infoMessage('Configured collections'); + + if ($this->shouldImport('collections')) { + spin( + callback: fn () => $this->runArtisanCommand('statamic:eloquent:import-collections --force --only-collections'), + message: 'Importing existing collections...' + ); + + $this->infoMessage('Imported existing collections'); + } + } + + public function hasBeenMigrated(): bool + { + return config('statamic.eloquent-driver.collections.driver') === 'eloquent'; + } +} diff --git a/src/Console/Commands/Eloquent/InstallEloquentEntries.php b/src/Console/Commands/Eloquent/InstallEloquentEntries.php new file mode 100644 index 0000000000..659d858a40 --- /dev/null +++ b/src/Console/Commands/Eloquent/InstallEloquentEntries.php @@ -0,0 +1,69 @@ +shouldImport('entries'); + + spin( + callback: function () use ($shouldImportEntries) { + $this->switchToEloquentDriver('entries'); + + if ($shouldImportEntries) { + File::put( + config_path('statamic/eloquent-driver.php'), + Str::of(File::get(config_path('statamic/eloquent-driver.php'))) + ->replace("'model' => \Statamic\Eloquent\Entries\EntryModel::class", "'model' => \Statamic\Eloquent\Entries\UuidEntryModel::class") + ->__toString() + ); + + $this->runArtisanCommand('vendor:publish --tag=statamic-eloquent-entries-table-with-string-ids'); + $this->runArtisanCommand('migrate'); + + $this->runArtisanCommand('statamic:eloquent:import-entries'); + + return; + } + + if (File::exists(base_path('content/collections/pages/home.md'))) { + File::delete(base_path('content/collections/pages/home.md')); + } + + if (File::exists(base_path('content/trees/collections/pages.yaml'))) { + File::put(base_path('content/trees/collections/pages.yaml'), 'tree: {}'); + } + + $this->runArtisanCommand('vendor:publish --tag=statamic-eloquent-entries-table'); + $this->runArtisanCommand('migrate'); + }, + message: $shouldImportEntries + ? 'Migrating entries...' + : 'Migrating and importing entries...' + ); + + $this->infoMessage( + $shouldImportEntries + ? 'Configured & imported existing entries' + : 'Configured entries' + ); + } + + public function hasBeenMigrated(): bool + { + return config('statamic.eloquent-driver.entries.driver') === 'eloquent'; + } +} diff --git a/src/Console/Commands/Eloquent/InstallEloquentRepository.php b/src/Console/Commands/Eloquent/InstallEloquentRepository.php new file mode 100644 index 0000000000..f2c40951b6 --- /dev/null +++ b/src/Console/Commands/Eloquent/InstallEloquentRepository.php @@ -0,0 +1,56 @@ +handle; + } + + public function repoTitle(): string + { + return $this->title ?? Str::of($this->repoHandle())->replace('_', ' ')->title(); + } + + protected function switchToEloquentDriver(): void + { + File::put( + config_path('statamic/eloquent-driver.php'), + Str::of(File::get(config_path('statamic/eloquent-driver.php'))) + ->replace( + "'{$this->repoHandle()}' => [\n 'driver' => 'file'", + "'{$this->repoHandle()}' => [\n 'driver' => 'eloquent'" + ) + ->__toString() + ); + } + + protected function shouldImport(string $repository): bool + { + return $this->option('import') || confirm("Would you like to import existing {$repository}?"); + } + + protected function infoMessage(string $message): void + { + if ($this->option('without-messages')) { + return; + } + + $this->components->info($message); + } +} diff --git a/src/Console/Commands/InstallEloquent.php b/src/Console/Commands/InstallEloquent.php new file mode 100644 index 0000000000..f41898385f --- /dev/null +++ b/src/Console/Commands/InstallEloquent.php @@ -0,0 +1,175 @@ +installEloquentDriver(); + $this->setupDatabase(); + $this->migrateRepositories(); + } + + protected function installEloquentDriver() + { + if (! Composer::isInstalled('statamic/eloquent-driver')) { + spin( + callback: fn () => Composer::withoutQueue()->throwOnFailure()->require('statamic/eloquent-driver'), + message: 'Installing the statamic/eloquent-driver package...' + ); + + $this->infoMessage('Installed statamic/eloquent-driver package'); + } + + if (! File::exists(config_path('statamic/eloquent-driver.php'))) { + $this->runArtisanCommand('vendor:publish --tag=statamic-eloquent-config'); + $this->infoMessage('Config file [config/statamic/eloquent-driver.php] published successfully.'); + } + } + + protected function setupDatabase() + { + try { + DB::connection()->getPDO(); + DB::connection()->getDatabaseName(); + } catch (\PDOException $e) { + $this->components->error('Failed to connect to the configured database. Please check your database configuration and try again.'); + exit(1); + } + } + + protected function migrateRepositories() + { + if ($this->repositories()->reject(fn ($repository) => $repository->hasBeenMigrated())->isEmpty()) { + $this->components->warn("No repositories left to migrate. You're already using the Eloquent Driver for all repositories."); + exit(1); + } + + $this->repositories() + ->only($this->selectedRepositories()) + ->each(function ($repository) { + if ($repository->hasBeenMigrated()) { + return $this->components->warn("Skipping. The {$repository->repoTitle()} repository is already using the Eloquent Driver."); + } + + // Pass valid options from this command down to the individual repository command + $commandOptions = collect($this->options()) + ->intersectByKeys($repository->getDefinition()->getOptions()) + ->mapWithKeys(fn ($value, $key) => ["--{$key}" => $value]) + ->all(); + + $this->call($repository->getName(), $commandOptions); + }); + } + + protected function selectedRepositories(): array + { + if ($this->option('all')) { + return $this->repositories()->map->repoHandle()->all(); + } + + if ($repositories = $this->option('repositories')) { + $repositories = collect(explode(',', $repositories)) + ->map(fn ($repo) => trim(strtolower($repo))) + ->unique(); + + $invalidRepositories = $repositories->reject(fn ($repo) => $this->repositories()->has($repo)); + + if ($invalidRepositories->isNotEmpty()) { + $this->components->warn("Some of the repositories you provided are invalid: {$invalidRepositories->implode(', ')}"); + } + + return $repositories + ->filter(fn ($repository) => ! $invalidRepositories->contains($repository)) + ->values() + ->all(); + } + + return multiselect( + label: 'Which repositories would you like to migrate?', + options: $this->repositories() + ->reject(fn ($repository) => $repository->hasBeenMigrated()) + ->map->repoTitle(), + validate: fn (array $values) => count($values) === 0 + ? 'You must select at least one repository to migrate.' + : null, + hint: 'You can always import other repositories later.' + ); + } + + public static function register(string $repository): void + { + self::$repositories[] = $repository; + } + + protected function repositories(): Collection + { + return Blink::once('install-eloquent-driver-repositories', function () { + return collect(self::$repositories) + ->sort() + ->map(fn (string $repo) => app($repo)) + ->mapWithKeys(fn (InstallEloquentRepository $repo) => [$repo->repoHandle() => $repo]); + }); + } + + private function infoMessage(string $message): void + { + if ($this->option('without-messages')) { + return; + } + + $this->components->info($message); + } +} diff --git a/src/Providers/ConsoleServiceProvider.php b/src/Providers/ConsoleServiceProvider.php index 4a043e0ac4..80e159603d 100644 --- a/src/Providers/ConsoleServiceProvider.php +++ b/src/Providers/ConsoleServiceProvider.php @@ -17,6 +17,10 @@ class ConsoleServiceProvider extends ServiceProvider Commands\Install::class, Commands\InstallCollaboration::class, Commands\InstallEloquentDriver::class, + Commands\InstallEloquent::class, + Commands\Eloquent\InstallEloquentCollections::class, + Commands\Eloquent\InstallEloquentCollectionTrees::class, + Commands\Eloquent\InstallEloquentEntries::class, Commands\InstallSsg::class, Commands\FlatCamp::class, Commands\LicenseSet::class,