From 8f0af57b8a32e7f317771b54f9629862e6f09c9e Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 6 Feb 2025 16:03:12 -0500 Subject: [PATCH 1/4] Allow developers to add repositories --- src/Console/Commands/InstallEloquentDriver.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Console/Commands/InstallEloquentDriver.php b/src/Console/Commands/InstallEloquentDriver.php index ae59e76e15..da6bea0dab 100644 --- a/src/Console/Commands/InstallEloquentDriver.php +++ b/src/Console/Commands/InstallEloquentDriver.php @@ -41,6 +41,8 @@ class InstallEloquentDriver extends Command */ protected $description = "Install & configure Statamic's Eloquent Driver package"; + protected static array $additionalRepositories = []; + /** * Execute the console command. * @@ -124,6 +126,11 @@ protected function repositories(): array ); } + public static function addRepository(string $key, array $config): void + { + self::$additionalRepositories[$key] = $config; + } + protected function allRepositories(): Collection { return collect([ @@ -145,7 +152,9 @@ protected function allRepositories(): Collection 'taxonomies' => 'Taxonomies', 'terms' => 'Terms', 'tokens' => 'Tokens', - ]); + ]) + ->merge(Arr::map(self::$additionalRepositories, fn ($value) => $value['title'])) + ->sort(); } protected function repositoryHasBeenMigrated(string $repository): bool @@ -205,6 +214,9 @@ protected function repositoryHasBeenMigrated(string $repository): bool case 'tokens': return config('statamic.eloquent-driver.tokens.driver') === 'eloquent'; + + case $repository: + return Arr::get(self::$additionalRepositories, "{$repository}.hasBeenMigrated"); } } From c00010e62e587a685e4af096adcd0b1fa15a4d39 Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 13 Feb 2025 19:10:31 -0500 Subject: [PATCH 2/4] wip proof of concept --- .../Commands/Concerns/RunsArtisanCommand.php | 44 +++++ .../InstallEloquentCollectionTrees.php | 43 +++++ .../Eloquent/InstallEloquentCollections.php | 43 +++++ .../Eloquent/InstallEloquentEntries.php | 69 +++++++ .../Eloquent/InstallEloquentRepository.php | 55 ++++++ src/Console/Commands/InstallEloquent.php | 174 ++++++++++++++++++ src/Providers/ConsoleServiceProvider.php | 4 + 7 files changed, 432 insertions(+) create mode 100644 src/Console/Commands/Concerns/RunsArtisanCommand.php create mode 100644 src/Console/Commands/Eloquent/InstallEloquentCollectionTrees.php create mode 100644 src/Console/Commands/Eloquent/InstallEloquentCollections.php create mode 100644 src/Console/Commands/Eloquent/InstallEloquentEntries.php create mode 100644 src/Console/Commands/Eloquent/InstallEloquentRepository.php create mode 100644 src/Console/Commands/InstallEloquent.php 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..492a8374f5 --- /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..8c4e4fcd99 --- /dev/null +++ b/src/Console/Commands/Eloquent/InstallEloquentRepository.php @@ -0,0 +1,55 @@ +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..0dd67c10b7 --- /dev/null +++ b/src/Console/Commands/InstallEloquent.php @@ -0,0 +1,174 @@ +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, From 71989f5dad1e2e8df2ca1da6e6e9e1ba890c196a Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 13 Feb 2025 19:11:28 -0500 Subject: [PATCH 3/4] Restore original command This reverts commit 8f0af57b8a32e7f317771b54f9629862e6f09c9e. --- src/Console/Commands/InstallEloquentDriver.php | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/Console/Commands/InstallEloquentDriver.php b/src/Console/Commands/InstallEloquentDriver.php index da6bea0dab..ae59e76e15 100644 --- a/src/Console/Commands/InstallEloquentDriver.php +++ b/src/Console/Commands/InstallEloquentDriver.php @@ -41,8 +41,6 @@ class InstallEloquentDriver extends Command */ protected $description = "Install & configure Statamic's Eloquent Driver package"; - protected static array $additionalRepositories = []; - /** * Execute the console command. * @@ -126,11 +124,6 @@ protected function repositories(): array ); } - public static function addRepository(string $key, array $config): void - { - self::$additionalRepositories[$key] = $config; - } - protected function allRepositories(): Collection { return collect([ @@ -152,9 +145,7 @@ protected function allRepositories(): Collection 'taxonomies' => 'Taxonomies', 'terms' => 'Terms', 'tokens' => 'Tokens', - ]) - ->merge(Arr::map(self::$additionalRepositories, fn ($value) => $value['title'])) - ->sort(); + ]); } protected function repositoryHasBeenMigrated(string $repository): bool @@ -214,9 +205,6 @@ protected function repositoryHasBeenMigrated(string $repository): bool case 'tokens': return config('statamic.eloquent-driver.tokens.driver') === 'eloquent'; - - case $repository: - return Arr::get(self::$additionalRepositories, "{$repository}.hasBeenMigrated"); } } From dc5cdb774c931f0819d4f46962a9ffc2d7baeb1c Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Thu, 13 Feb 2025 19:26:55 -0500 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=8D=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Eloquent/InstallEloquentEntries.php | 2 +- .../Eloquent/InstallEloquentRepository.php | 5 +++-- src/Console/Commands/InstallEloquent.php | 21 ++++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Console/Commands/Eloquent/InstallEloquentEntries.php b/src/Console/Commands/Eloquent/InstallEloquentEntries.php index 492a8374f5..659d858a40 100644 --- a/src/Console/Commands/Eloquent/InstallEloquentEntries.php +++ b/src/Console/Commands/Eloquent/InstallEloquentEntries.php @@ -2,8 +2,8 @@ namespace Statamic\Console\Commands\Eloquent; -use Statamic\Support\Str; use Statamic\Facades\File; +use Statamic\Support\Str; use function Laravel\Prompts\spin; diff --git a/src/Console/Commands/Eloquent/InstallEloquentRepository.php b/src/Console/Commands/Eloquent/InstallEloquentRepository.php index 8c4e4fcd99..f2c40951b6 100644 --- a/src/Console/Commands/Eloquent/InstallEloquentRepository.php +++ b/src/Console/Commands/Eloquent/InstallEloquentRepository.php @@ -3,16 +3,17 @@ namespace Statamic\Console\Commands\Eloquent; use Illuminate\Console\Command; -use function Laravel\Prompts\confirm; use Statamic\Console\Commands\Concerns\RunsArtisanCommand; use Statamic\Console\EnhancesCommands; use Statamic\Console\RunsInPlease; use Statamic\Facades\File; use Statamic\Support\Str; +use function Laravel\Prompts\confirm; + abstract class InstallEloquentRepository extends Command { - use EnhancesCommands, RunsInPlease, RunsArtisanCommand; + use EnhancesCommands, RunsArtisanCommand, RunsInPlease; abstract public function hasBeenMigrated(): bool; diff --git a/src/Console/Commands/InstallEloquent.php b/src/Console/Commands/InstallEloquent.php index 0dd67c10b7..f41898385f 100644 --- a/src/Console/Commands/InstallEloquent.php +++ b/src/Console/Commands/InstallEloquent.php @@ -2,25 +2,26 @@ namespace Statamic\Console\Commands; -use Statamic\Facades\File; -use Statamic\Facades\Blink; +use Facades\Statamic\Console\Processes\Composer; use Illuminate\Console\Command; -use function Laravel\Prompts\spin; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; -use Statamic\Console\RunsInPlease; -use Statamic\Console\EnhancesCommands; -use function Laravel\Prompts\multiselect; -use Facades\Statamic\Console\Processes\Composer; use Statamic\Console\Commands\Concerns\RunsArtisanCommand; -use Statamic\Console\Commands\Eloquent\InstallEloquentEntries; -use Statamic\Console\Commands\Eloquent\InstallEloquentRepository; use Statamic\Console\Commands\Eloquent\InstallEloquentCollections; use Statamic\Console\Commands\Eloquent\InstallEloquentCollectionTrees; +use Statamic\Console\Commands\Eloquent\InstallEloquentEntries; +use Statamic\Console\Commands\Eloquent\InstallEloquentRepository; +use Statamic\Console\EnhancesCommands; +use Statamic\Console\RunsInPlease; +use Statamic\Facades\Blink; +use Statamic\Facades\File; + +use function Laravel\Prompts\multiselect; +use function Laravel\Prompts\spin; class InstallEloquent extends Command { - use EnhancesCommands, RunsInPlease, RunsArtisanCommand; + use EnhancesCommands, RunsArtisanCommand, RunsInPlease; /** * The name and signature of the console command.