From 41a2c877fbe3242954e2ef892f6bd6eb61d84b79 Mon Sep 17 00:00:00 2001 From: Adam Patterson Date: Sat, 4 Jan 2025 13:11:07 -0700 Subject: [PATCH 1/9] Adds Taxonomy export options --- src/Commands/ExportTaxonomies.php | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Commands/ExportTaxonomies.php b/src/Commands/ExportTaxonomies.php index 5368c08e..8ef9e179 100644 --- a/src/Commands/ExportTaxonomies.php +++ b/src/Commands/ExportTaxonomies.php @@ -29,7 +29,10 @@ class ExportTaxonomies extends Command * * @var string */ - protected $signature = 'statamic:eloquent:export-taxonomies {--force : Force the export to run, with all prompts answered "yes"}'; + protected $signature = 'statamic:eloquent:export-taxonomies + {--force : Force the export to run, with all prompts answered "yes"} + {--only-taxonomies : Only export taxonomies} + {--only-terms : Only export terms}'; /** * The console command description. @@ -69,7 +72,7 @@ private function usingDefaultRepositories(Closure $callback) private function exportTaxonomies() { - if (! $this->option('force') && ! $this->confirm('Do you want to export taxonomies?')) { + if (!$this->shouldExportTaxonomies()) { return; } @@ -90,7 +93,7 @@ private function exportTaxonomies() private function exportTerms() { - if (! $this->option('force') && ! $this->confirm('Do you want to export terms?')) { + if (!$this->shouldExportTerms()) { return; } @@ -130,4 +133,18 @@ private function exportTerms() $this->newLine(); $this->info('Terms exported'); } + + private function shouldExportTaxonomies(): bool + { + return $this->option('only-taxonomies') + || !$this->option('only-terms') + && ($this->option('force') || $this->confirm('Do you want to export taxonomies?')); + } + + private function shouldExportTerms(): bool + { + return $this->option('only-terms') + || !$this->option('only-taxonomies') + && ($this->option('force') || $this->confirm('Do you want to export terms?')); + } } From b00036fa38c7b3f3b04b4b494be3e4521736e560 Mon Sep 17 00:00:00 2001 From: Adam Patterson Date: Sat, 4 Jan 2025 13:20:21 -0700 Subject: [PATCH 2/9] Fixes linted --- src/Commands/ExportTaxonomies.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Commands/ExportTaxonomies.php b/src/Commands/ExportTaxonomies.php index 8ef9e179..7385b2a8 100644 --- a/src/Commands/ExportTaxonomies.php +++ b/src/Commands/ExportTaxonomies.php @@ -72,7 +72,7 @@ private function usingDefaultRepositories(Closure $callback) private function exportTaxonomies() { - if (!$this->shouldExportTaxonomies()) { + if (! $this->shouldExportTaxonomies()) { return; } @@ -93,7 +93,7 @@ private function exportTaxonomies() private function exportTerms() { - if (!$this->shouldExportTerms()) { + if (! $this->shouldExportTerms()) { return; } @@ -137,14 +137,14 @@ private function exportTerms() private function shouldExportTaxonomies(): bool { return $this->option('only-taxonomies') - || !$this->option('only-terms') + || ! $this->option('only-terms') && ($this->option('force') || $this->confirm('Do you want to export taxonomies?')); } private function shouldExportTerms(): bool { return $this->option('only-terms') - || !$this->option('only-taxonomies') + || ! $this->option('only-taxonomies') && ($this->option('force') || $this->confirm('Do you want to export terms?')); } } From 6f0ece81578f2a966121a19fea9c7a5bfdffd327 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 25 Feb 2025 10:59:12 +0000 Subject: [PATCH 3/9] [5.x] Drop Laravel 10 Support (#399) * Remove Laravel 10 & PHP 8.1 from testing matrix * Remove Testbench 8 version constraint * Require minimum of PHP 8.2 --- .github/workflows/tests.yml | 9 ++------- composer.json | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 05285188..2bd3d29e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,14 +12,9 @@ jobs: strategy: matrix: - php: [8.1, 8.2, 8.3, 8.4] - laravel: [10.*, 11.*] + php: [8.2, 8.3, 8.4] + laravel: [11.*] stability: [prefer-lowest, prefer-stable] - exclude: - - php: 8.1 - laravel: 11.* - - php: 8.4 - laravel: 10.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} diff --git a/composer.json b/composer.json index 7c6384b0..65716edd 100644 --- a/composer.json +++ b/composer.json @@ -24,13 +24,13 @@ } }, "require": { - "php": "^8.1", + "php": "^8.2", "statamic/cms": "^5.41" }, "require-dev": { "doctrine/dbal": "^3.8", "laravel/pint": "^1.0", - "orchestra/testbench": "^8.28 || ^9.6.1", + "orchestra/testbench": "^9.6.1", "phpunit/phpunit": "^10.5.35" }, "scripts": { From 0c355bbb3830864191044648b3c02ea4111757c0 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 25 Feb 2025 11:14:44 +0000 Subject: [PATCH 4/9] [5.x] Convert `$casts` property to method (#400) * Convert `$casts` property to method * Update example in the docs --- README.md | 21 ++++++++++++--------- src/Assets/AssetContainerModel.php | 9 ++++++--- src/Assets/AssetModel.php | 11 +++++++---- src/Collections/CollectionModel.php | 25 ++++++++++++++----------- src/Entries/EntryModel.php | 13 ++++++++----- src/Fields/BlueprintModel.php | 9 ++++++--- src/Fields/FieldsetModel.php | 9 ++++++--- src/Forms/FormModel.php | 9 ++++++--- src/Forms/SubmissionModel.php | 9 ++++++--- src/Globals/GlobalSetModel.php | 9 ++++++--- src/Globals/VariablesModel.php | 9 ++++++--- src/Revisions/RevisionModel.php | 9 ++++++--- src/Sites/SiteModel.php | 9 ++++++--- src/Structures/NavModel.php | 9 ++++++--- src/Structures/TreeModel.php | 11 +++++++---- src/Taxonomies/TaxonomyModel.php | 11 +++++++---- src/Taxonomies/TermModel.php | 9 ++++++--- src/Tokens/TokenModel.php | 11 +++++++---- 18 files changed, 128 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 57bbdbb9..4ea52a56 100644 --- a/README.md +++ b/README.md @@ -118,15 +118,18 @@ By default, the Eloquent Driver stores all data in a single `data` column. Howev class Entry extends \Statamic\Eloquent\Entries\EntryModel { - protected $casts = [ - // The casts from Statamic's base model... - 'date' => 'datetime', - 'data' => 'json', - 'published' => 'boolean', - - // Your custom casts... - 'featured_images' => 'json', - ]; + protected function casts(): array + { + return [ + // The casts from Statamic's base model... + 'date' => 'datetime', + 'data' => 'json', + 'published' => 'boolean', + + // Your custom casts... + 'featured_images' => 'json', + ]; + } } ``` diff --git a/src/Assets/AssetContainerModel.php b/src/Assets/AssetContainerModel.php index f93c3a7b..58a0dad2 100644 --- a/src/Assets/AssetContainerModel.php +++ b/src/Assets/AssetContainerModel.php @@ -11,9 +11,12 @@ class AssetContainerModel extends BaseModel protected $table = 'asset_containers'; - protected $casts = [ - 'settings' => 'json', - ]; + protected function casts(): array + { + return [ + 'settings' => 'json', + ]; + } public function getAttribute($key) { diff --git a/src/Assets/AssetModel.php b/src/Assets/AssetModel.php index 4316e38f..64d08aab 100644 --- a/src/Assets/AssetModel.php +++ b/src/Assets/AssetModel.php @@ -10,8 +10,11 @@ class AssetModel extends BaseModel protected $table = 'assets_meta'; - protected $casts = [ - 'data' => 'json', - 'meta' => 'json', - ]; + protected function casts(): array + { + return [ + 'data' => 'json', + 'meta' => 'json', + ]; + } } diff --git a/src/Collections/CollectionModel.php b/src/Collections/CollectionModel.php index ca341d73..f5a1e890 100644 --- a/src/Collections/CollectionModel.php +++ b/src/Collections/CollectionModel.php @@ -10,15 +10,18 @@ class CollectionModel extends BaseModel protected $table = 'collections'; - protected $casts = [ - 'settings' => 'json', - 'settings.routes' => 'array', - 'settings.inject' => 'array', - 'settings.taxonomies' => 'array', - 'settings.structure' => 'array', - 'settings.sites' => 'array', - 'settings.revisions' => 'boolean', - 'settings.dated' => 'boolean', - 'settings.default_publish_state' => 'boolean', - ]; + protected function casts(): array + { + return [ + 'settings' => 'json', + 'settings.routes' => 'array', + 'settings.inject' => 'array', + 'settings.taxonomies' => 'array', + 'settings.structure' => 'array', + 'settings.sites' => 'array', + 'settings.revisions' => 'boolean', + 'settings.dated' => 'boolean', + 'settings.default_publish_state' => 'boolean', + ]; + } } diff --git a/src/Entries/EntryModel.php b/src/Entries/EntryModel.php index 77e89122..6d8c0e7c 100644 --- a/src/Entries/EntryModel.php +++ b/src/Entries/EntryModel.php @@ -11,11 +11,14 @@ class EntryModel extends BaseModel protected $table = 'entries'; - protected $casts = [ - 'date' => 'datetime', - 'data' => 'json', - 'published' => 'boolean', - ]; + protected function casts(): array + { + return [ + 'date' => 'datetime', + 'data' => 'json', + 'published' => 'boolean', + ]; + } public function author() { diff --git a/src/Fields/BlueprintModel.php b/src/Fields/BlueprintModel.php index ce75588d..de7ca6b4 100644 --- a/src/Fields/BlueprintModel.php +++ b/src/Fields/BlueprintModel.php @@ -11,9 +11,12 @@ class BlueprintModel extends BaseModel protected $table = 'blueprints'; - protected $casts = [ - 'data' => 'json', - ]; + protected function casts(): array + { + return [ + 'data' => 'json', + ]; + } public function getAttribute($key) { diff --git a/src/Fields/FieldsetModel.php b/src/Fields/FieldsetModel.php index ad14288a..9de027ef 100644 --- a/src/Fields/FieldsetModel.php +++ b/src/Fields/FieldsetModel.php @@ -11,9 +11,12 @@ class FieldsetModel extends BaseModel protected $table = 'fieldsets'; - protected $casts = [ - 'data' => 'json', - ]; + protected function casts(): array + { + return [ + 'data' => 'json', + ]; + } public function getAttribute($key) { diff --git a/src/Forms/FormModel.php b/src/Forms/FormModel.php index baae002e..685b1a0d 100644 --- a/src/Forms/FormModel.php +++ b/src/Forms/FormModel.php @@ -10,7 +10,10 @@ class FormModel extends BaseModel protected $table = 'forms'; - protected $casts = [ - 'settings' => 'json', - ]; + protected function casts(): array + { + return [ + 'settings' => 'json', + ]; + } } diff --git a/src/Forms/SubmissionModel.php b/src/Forms/SubmissionModel.php index 89151e12..197e6274 100644 --- a/src/Forms/SubmissionModel.php +++ b/src/Forms/SubmissionModel.php @@ -12,9 +12,12 @@ class SubmissionModel extends BaseModel protected $table = 'form_submissions'; - protected $casts = [ - 'data' => 'json', - ]; + protected function casts(): array + { + return [ + 'data' => 'json', + ]; + } protected $dateFormat = 'Y-m-d H:i:s.u'; } diff --git a/src/Globals/GlobalSetModel.php b/src/Globals/GlobalSetModel.php index ffba9629..f538c8d6 100644 --- a/src/Globals/GlobalSetModel.php +++ b/src/Globals/GlobalSetModel.php @@ -11,9 +11,12 @@ class GlobalSetModel extends BaseModel protected $table = 'global_sets'; - protected $casts = [ - 'settings' => 'json', - ]; + protected function casts(): array + { + return [ + 'settings' => 'json', + ]; + } public function getAttribute($key) { diff --git a/src/Globals/VariablesModel.php b/src/Globals/VariablesModel.php index 4f4ec198..93ae0c67 100644 --- a/src/Globals/VariablesModel.php +++ b/src/Globals/VariablesModel.php @@ -11,9 +11,12 @@ class VariablesModel extends BaseModel protected $table = 'global_set_variables'; - protected $casts = [ - 'data' => 'array', - ]; + protected function casts(): array + { + return [ + 'data' => 'array', + ]; + } public function getAttribute($key) { diff --git a/src/Revisions/RevisionModel.php b/src/Revisions/RevisionModel.php index a7c22f14..bb62b1c8 100644 --- a/src/Revisions/RevisionModel.php +++ b/src/Revisions/RevisionModel.php @@ -10,7 +10,10 @@ class RevisionModel extends BaseModel protected $table = 'revisions'; - protected $casts = [ - 'attributes' => 'json', - ]; + protected function casts(): array + { + return [ + 'attributes' => 'json', + ]; + } } diff --git a/src/Sites/SiteModel.php b/src/Sites/SiteModel.php index d5a16065..2baf2dd2 100644 --- a/src/Sites/SiteModel.php +++ b/src/Sites/SiteModel.php @@ -11,9 +11,12 @@ class SiteModel extends BaseModel protected $table = 'sites'; - protected $casts = [ - 'attributes' => 'json', - ]; + protected function casts(): array + { + return [ + 'attributes' => 'json', + ]; + } public function getAttribute($key) { diff --git a/src/Structures/NavModel.php b/src/Structures/NavModel.php index c31513a3..42824236 100644 --- a/src/Structures/NavModel.php +++ b/src/Structures/NavModel.php @@ -10,7 +10,10 @@ class NavModel extends BaseModel protected $table = 'navigations'; - protected $casts = [ - 'settings' => 'json', - ]; + protected function casts(): array + { + return [ + 'settings' => 'json', + ]; + } } diff --git a/src/Structures/TreeModel.php b/src/Structures/TreeModel.php index b078bbcc..6e3cca2a 100644 --- a/src/Structures/TreeModel.php +++ b/src/Structures/TreeModel.php @@ -10,8 +10,11 @@ class TreeModel extends BaseModel protected $table = 'trees'; - protected $casts = [ - 'tree' => 'json', - 'settings' => 'json', - ]; + protected function casts(): array + { + return [ + 'tree' => 'json', + 'settings' => 'json', + ]; + } } diff --git a/src/Taxonomies/TaxonomyModel.php b/src/Taxonomies/TaxonomyModel.php index 13d78188..17ccc19e 100644 --- a/src/Taxonomies/TaxonomyModel.php +++ b/src/Taxonomies/TaxonomyModel.php @@ -11,10 +11,13 @@ class TaxonomyModel extends BaseModel protected $table = 'taxonomies'; - protected $casts = [ - 'settings' => 'json', - 'sites' => 'json', - ]; + protected function casts(): array + { + return [ + 'settings' => 'json', + 'sites' => 'json', + ]; + } public function getAttribute($key) { diff --git a/src/Taxonomies/TermModel.php b/src/Taxonomies/TermModel.php index aa5d326e..9291102e 100644 --- a/src/Taxonomies/TermModel.php +++ b/src/Taxonomies/TermModel.php @@ -11,9 +11,12 @@ class TermModel extends BaseModel protected $table = 'taxonomy_terms'; - protected $casts = [ - 'data' => 'json', - ]; + protected function casts(): array + { + return [ + 'data' => 'json', + ]; + } public function getAttribute($key) { diff --git a/src/Tokens/TokenModel.php b/src/Tokens/TokenModel.php index 82b70564..03fa7ec3 100644 --- a/src/Tokens/TokenModel.php +++ b/src/Tokens/TokenModel.php @@ -10,8 +10,11 @@ class TokenModel extends BaseModel protected $table = 'tokens'; - protected $casts = [ - 'data' => 'json', - 'expire_at' => 'datetime', - ]; + protected function casts(): array + { + return [ + 'data' => 'json', + 'expire_at' => 'datetime', + ]; + } } From 7a5449a9b19f6330417e5cab177915f719dc4ab3 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 25 Mar 2025 12:12:18 +0000 Subject: [PATCH 5/9] Use dev-master for the time being --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 56dbd2ad..2cac5a97 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ }, "require": { "php": "^8.2", - "statamic/cms": "^5.41" + "statamic/cms": "dev-master" }, "require-dev": { "doctrine/dbal": "^3.8", From c90bd2a43d93bf2631e7a9857c996853498a3175 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 4 Jul 2025 14:54:40 +0100 Subject: [PATCH 6/9] [5.x] Separate globals config and content (#411) * Drop `origin` column from `global_set_variables` column * Persist new `sites` option in global set options * Update existing tests There's still two failing tests locally, I'll get these fixed up tomorrow. * This test needs to be using multisite. * Not all tests require multisite. * Arr::get() is safer. * Update script * Fix styling * We shouldn't need to override this. * Sites and origins are now accessed using separate methods. * Update tests * Call `in` instead of `addLocalization` * Get rid of the GlobalFactory We're not using it anymore, and we've removed its equivelent in core. * Events are fired differently in v6. Remove test. --------- Co-authored-by: duncanmcclean --- ...7_100000_create_global_variables_table.php | 1 - ...op_origin_on_global_set_variables.php.stub | 21 +++++ src/Globals/GlobalSet.php | 8 +- src/Globals/Variables.php | 11 --- src/ServiceProvider.php | 2 + .../DropOriginOnGlobalSetVariables.php | 24 ++++++ src/Updates/UpdateGlobalVariables.php | 40 ++++++++++ tests/Commands/ImportGlobalsTest.php | 18 ++--- tests/Data/Globals/GlobalSetTest.php | 46 +---------- tests/Data/Globals/VariablesTest.php | 77 +++++++++++-------- tests/Factories/GlobalFactory.php | 55 ------------- tests/Globals/GlobalsetTest.php | 30 -------- tests/Repositories/GlobalRepositoryTest.php | 11 +-- .../Concerns/RunsUpdateScripts.php | 29 +++++++ .../UpdateGlobalVariablesTest.php | 62 +++++++++++++++ 15 files changed, 241 insertions(+), 194 deletions(-) create mode 100644 database/migrations/updates/drop_origin_on_global_set_variables.php.stub create mode 100644 src/Updates/DropOriginOnGlobalSetVariables.php create mode 100644 src/Updates/UpdateGlobalVariables.php delete mode 100644 tests/Factories/GlobalFactory.php delete mode 100644 tests/Globals/GlobalsetTest.php create mode 100644 tests/UpdateScripts/Concerns/RunsUpdateScripts.php create mode 100644 tests/UpdateScripts/UpdateGlobalVariablesTest.php diff --git a/database/migrations/2024_03_07_100000_create_global_variables_table.php b/database/migrations/2024_03_07_100000_create_global_variables_table.php index ae04f9f1..0025da09 100644 --- a/database/migrations/2024_03_07_100000_create_global_variables_table.php +++ b/database/migrations/2024_03_07_100000_create_global_variables_table.php @@ -12,7 +12,6 @@ public function up() $table->id(); $table->string('handle')->index(); $table->string('locale')->nullable(); - $table->string('origin')->nullable(); $table->jsonb('data'); $table->timestamps(); }); diff --git a/database/migrations/updates/drop_origin_on_global_set_variables.php.stub b/database/migrations/updates/drop_origin_on_global_set_variables.php.stub new file mode 100644 index 00000000..3fe54ffc --- /dev/null +++ b/database/migrations/updates/drop_origin_on_global_set_variables.php.stub @@ -0,0 +1,21 @@ +prefix('global_set_variables'), function (Blueprint $table) { + $table->dropColumn('origin'); + }); + } + + public function down() + { + Schema::table($this->prefix('global_set_variables'), function (Blueprint $table) { + $table->string('origin')->nullable(); + }); + } +}; diff --git a/src/Globals/GlobalSet.php b/src/Globals/GlobalSet.php index b4d8025e..9573b3a7 100644 --- a/src/Globals/GlobalSet.php +++ b/src/Globals/GlobalSet.php @@ -5,6 +5,7 @@ use Statamic\Contracts\Globals\GlobalSet as Contract; use Statamic\Eloquent\Globals\GlobalSetModel as Model; use Statamic\Globals\GlobalSet as FileEntry; +use Statamic\Support\Arr; class GlobalSet extends FileEntry { @@ -15,6 +16,7 @@ public static function fromModel(Model $model) $global = (new static) ->handle($model->handle) ->title($model->title) + ->sites(Arr::get($model->settings, 'sites')) ->model($model); return $global; @@ -31,7 +33,11 @@ public static function makeModelFromContract(Contract $source) return $class::firstOrNew(['handle' => $source->handle()])->fill([ 'title' => $source->title(), - 'settings' => [], // future proofing + 'settings' => [ + 'sites' => $source->sites() + ->mapWithKeys(fn ($site) => [$site => $source->origins()->get($site)]) + ->all(), + ], ]); } diff --git a/src/Globals/Variables.php b/src/Globals/Variables.php index 38e45273..5926e4bd 100644 --- a/src/Globals/Variables.php +++ b/src/Globals/Variables.php @@ -16,7 +16,6 @@ public static function fromModel(Model $model) ->globalSet($model->handle) ->locale($model->locale) ->data($model->data) - ->origin($model->origin ?? null) ->model($model); } @@ -31,24 +30,14 @@ public static function makeModelFromContract(Contract $source) $data = $source->data(); - if ($source->hasOrigin()) { - $data = $source->origin()->data()->merge($data); - } - return $class::firstOrNew([ 'handle' => $source->globalSet()->handle(), 'locale' => $source->locale, ])->fill([ 'data' => $data->filter(fn ($v) => $v !== null), - 'origin' => $source->hasOrigin() ? $source->origin()->locale() : null, ]); } - protected function getOriginByString($origin) - { - return $this->globalSet()->in($origin); - } - public function model($model = null) { if (func_num_args() === 0) { diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 0166dc8d..a3bc935b 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -59,6 +59,8 @@ class ServiceProvider extends AddonServiceProvider \Statamic\Eloquent\Updates\ChangeFormSubmissionsIdType::class, \Statamic\Eloquent\Updates\AddIndexToDateOnEntriesTable::class, \Statamic\Eloquent\Updates\AddOrderToSitesTable::class, + \Statamic\Eloquent\Updates\DropOriginOnGlobalSetVariables::class, + \Statamic\Eloquent\Updates\UpdateGlobalVariables::class, ]; public function boot() diff --git a/src/Updates/DropOriginOnGlobalSetVariables.php b/src/Updates/DropOriginOnGlobalSetVariables.php new file mode 100644 index 00000000..3b716b53 --- /dev/null +++ b/src/Updates/DropOriginOnGlobalSetVariables.php @@ -0,0 +1,24 @@ +isUpdatingTo('5.0.0'); + } + + public function update() + { + $source = __DIR__.'/../../database/migrations/updates/drop_origin_on_global_set_variables.php.stub'; + $dest = database_path('migrations/'.date('Y_m_d_His').'_drop_origin_on_global_set_variables.php'); + + $this->files->copy($source, $dest); + + $this->console()->info('Migrations created'); + $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); + } +} diff --git a/src/Updates/UpdateGlobalVariables.php b/src/Updates/UpdateGlobalVariables.php new file mode 100644 index 00000000..2b54001b --- /dev/null +++ b/src/Updates/UpdateGlobalVariables.php @@ -0,0 +1,40 @@ +isUpdatingTo('5.0.0'); + } + + public function update() + { + // This update script deals with reading and & writing from the database. There's an + // equivalent update script for Stache-driven sites that deals with reading & writing YAML files. + if (config('statamic.eloquent-driver.global_set_variables.driver') !== 'eloquent') { + return; + } + + // We don't need to do anything for single-site installs. + if (! Site::multiEnabled()) { + return; + } + + GlobalSet::all()->each(function ($globalSet) { + $variables = GlobalVariables::whereSet($globalSet->handle()); + + $sites = $variables->mapWithKeys(function ($variable) { + return [$variable->locale() => $variable->model()->origin]; + }); + + $globalSet->sites($sites)->save(); + }); + } +} diff --git a/tests/Commands/ImportGlobalsTest.php b/tests/Commands/ImportGlobalsTest.php index 88effc9c..87b57bfb 100644 --- a/tests/Commands/ImportGlobalsTest.php +++ b/tests/Commands/ImportGlobalsTest.php @@ -35,8 +35,7 @@ protected function setUp(): void public function it_imports_global_sets_and_variables() { $globalSet = tap(GlobalSet::make('footer')->title('Footer'))->save(); - $variables = $globalSet->makeLocalization('en')->data(['foo' => 'bar']); - $globalSet->addLocalization($variables)->save(); + $globalSet->in('en')->data(['foo' => 'bar'])->save(); $this->assertCount(0, GlobalSetModel::all()); $this->assertCount(0, VariablesModel::all()); @@ -58,8 +57,7 @@ public function it_imports_global_sets_and_variables() public function it_imports_global_sets_and_variables_with_force_argument() { $globalSet = tap(GlobalSet::make('footer')->title('Footer'))->save(); - $variables = $globalSet->makeLocalization('en')->data(['foo' => 'bar']); - $globalSet->addLocalization($variables)->save(); + $globalSet->in('en')->data(['foo' => 'bar'])->save(); $this->assertCount(0, GlobalSetModel::all()); $this->assertCount(0, VariablesModel::all()); @@ -79,8 +77,7 @@ public function it_imports_global_sets_and_variables_with_force_argument() public function it_imports_only_global_sets_with_console_question() { $globalSet = tap(GlobalSet::make('footer')->title('Footer'))->save(); - $variables = $globalSet->makeLocalization('en')->data(['foo' => 'bar']); - $globalSet->addLocalization($variables)->save(); + $globalSet->in('en')->data(['foo' => 'bar'])->save(); $this->assertCount(0, GlobalSetModel::all()); $this->assertCount(0, VariablesModel::all()); @@ -102,8 +99,7 @@ public function it_imports_only_global_sets_with_console_question() public function it_imports_only_global_sets_with_only_global_sets_argument() { $globalSet = tap(GlobalSet::make('footer')->title('Footer'))->save(); - $variables = $globalSet->makeLocalization('en')->data(['foo' => 'bar']); - $globalSet->addLocalization($variables)->save(); + $globalSet->in('en')->data(['foo' => 'bar'])->save(); $this->assertCount(0, GlobalSetModel::all()); $this->assertCount(0, VariablesModel::all()); @@ -123,8 +119,7 @@ public function it_imports_only_global_sets_with_only_global_sets_argument() public function it_imports_only_variables_with_console_question() { $globalSet = tap(GlobalSet::make('footer')->title('Footer'))->save(); - $variables = $globalSet->makeLocalization('en')->data(['foo' => 'bar']); - $globalSet->addLocalization($variables)->save(); + $globalSet->in('en')->data(['foo' => 'bar'])->save(); $this->assertCount(0, GlobalSetModel::all()); $this->assertCount(0, VariablesModel::all()); @@ -146,8 +141,7 @@ public function it_imports_only_variables_with_console_question() public function it_imports_only_variables_with_only_global_variables_argument() { $globalSet = tap(GlobalSet::make('footer')->title('Footer'))->save(); - $variables = $globalSet->makeLocalization('en')->data(['foo' => 'bar']); - $globalSet->addLocalization($variables)->save(); + $globalSet->in('en')->data(['foo' => 'bar'])->save(); $this->assertCount(0, GlobalSetModel::all()); $this->assertCount(0, VariablesModel::all()); diff --git a/tests/Data/Globals/GlobalSetTest.php b/tests/Data/Globals/GlobalSetTest.php index b27d983d..a7a87ecd 100644 --- a/tests/Data/Globals/GlobalSetTest.php +++ b/tests/Data/Globals/GlobalSetTest.php @@ -9,46 +9,8 @@ class GlobalSetTest extends TestCase { #[Test] - public function it_gets_file_contents_for_saving_with_a_single_site() + public function it_gets_file_contents_for_saving() { - config()->set('statamic.system.multisite', false); - - $this->setSites([ - 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], - ]); - - $set = (new GlobalSet)->title('The title'); - - $variables = $set->makeLocalization('en')->data([ - 'array' => ['first one', 'second one'], - 'string' => 'The string', - ]); - - $set->addLocalization($variables); - - $expected = <<<'EOT' -title: 'The title' -data: - array: - - 'first one' - - 'second one' - string: 'The string' - -EOT; - $this->assertEquals($expected, $set->fileContents()); - } - - #[Test] - public function it_gets_file_contents_for_saving_with_multiple_sites() - { - config()->set('statamic.system.multisite', true); - - $this->setSites([ - 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], - 'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'], - 'de' => ['name' => 'German', 'locale' => 'de_DE', 'url' => 'http://test.com/de/'], - ]); - $set = (new GlobalSet)->title('The title'); // We set the data but it's basically irrelevant since it won't get saved to this file. @@ -58,12 +20,6 @@ public function it_gets_file_contents_for_saving_with_multiple_sites() 'string' => 'The string', ]); }); - $set->in('fr', function ($loc) { - $loc->data([ - 'array' => ['le first one', 'le second one'], - 'string' => 'Le string', - ]); - }); $expected = <<<'EOT' title: 'The title' diff --git a/tests/Data/Globals/VariablesTest.php b/tests/Data/Globals/VariablesTest.php index c509f14e..d04b4377 100644 --- a/tests/Data/Globals/VariablesTest.php +++ b/tests/Data/Globals/VariablesTest.php @@ -22,7 +22,9 @@ class VariablesTest extends TestCase #[Test] public function it_gets_file_contents_for_saving() { - $entry = (new Variables)->data([ + $global = GlobalSet::make('test')->sites(['en']); + + $entry = (new Variables)->globalSet($global)->data([ 'array' => ['first one', 'second one'], 'string' => 'The string', 'null' => null, // this... @@ -42,33 +44,40 @@ public function it_gets_file_contents_for_saving() #[Test] public function it_gets_file_contents_for_saving_a_localized_set() { - $global = GlobalSet::make('test'); + $this->setSites([ + 'a' => ['url' => '/', 'locale' => 'en'], + 'b' => ['url' => '/b/', 'locale' => 'fr'], + 'c' => ['url' => '/b/', 'locale' => 'fr'], + 'd' => ['url' => '/d/', 'locale' => 'fr'], + ]); - $a = $global->makeLocalization('a')->data([ - 'array' => ['first one', 'second one'], + $global = GlobalSet::make('test')->sites([ + 'a' => null, + 'b' => 'a', + 'c' => null, + ])->save(); + + $a = $global->in('a')->data([ + 'array' => ['first one', 'second one'], 'string' => 'The string', - 'null' => null, // this... - 'empty' => [], // and this should get stripped out because there's no origin to fall back to. + 'null' => null, // this... + 'empty' => [], // and this should get stripped out because there's no origin to fall back to. ]); - $b = $global->makeLocalization('b')->origin($a)->data([ - 'array' => ['first one', 'second one'], + $b = $global->in('b')->data([ + 'array' => ['first one', 'second one'], 'string' => 'The string', - 'null' => null, // this... - 'empty' => [], // and this should not get stripped out, otherwise it would fall back to the origin. + 'null' => null, // this... + 'empty' => [], // and this should not get stripped out, otherwise it would fall back to the origin. ]); - $c = $global->makeLocalization('c')->data([ - 'array' => ['first one', 'second one'], + $c = $global->in('c')->data([ + 'array' => ['first one', 'second one'], 'string' => 'The string', - 'null' => null, // this... - 'empty' => [], // and this should get stripped out because there's no origin to fall back to. + 'null' => null, // this... + 'empty' => [], // and this should get stripped out because there's no origin to fall back to. ]); - $global->addLocalization($a); - $global->addLocalization($b); - $global->addLocalization($c); - $expected = <<<'EOT' array: - 'first one' @@ -85,7 +94,6 @@ public function it_gets_file_contents_for_saving_a_localized_set() string: 'The string' 'null': null empty: { } -origin: a EOT; $this->assertEquals($expected, $b->fileContents()); @@ -103,9 +111,22 @@ public function it_gets_file_contents_for_saving_a_localized_set() #[Test] public function if_the_value_is_explicitly_set_to_null_then_it_should_not_fall_back() { - $global = GlobalSet::make('test'); + $this->setSites([ + 'a' => ['url' => '/', 'locale' => 'en'], + 'b' => ['url' => '/b/', 'locale' => 'fr'], + 'c' => ['url' => '/b/', 'locale' => 'fr'], + 'd' => ['url' => '/d/', 'locale' => 'fr'], + ]); + + $global = GlobalSet::make('test')->sites([ + 'a' => null, + 'b' => 'a', + 'c' => 'b', + 'd' => null, + 'e' => 'd', + ])->save(); - $a = $global->makeLocalization('a')->data([ + $a = $global->in('a')->data([ 'one' => 'alfa', 'two' => 'bravo', 'three' => 'charlie', @@ -113,35 +134,29 @@ public function if_the_value_is_explicitly_set_to_null_then_it_should_not_fall_b ]); // originates from a - $b = $global->makeLocalization('b')->origin($a)->data([ + $b = $global->in('b')->data([ 'one' => 'echo', 'two' => null, ]); // originates from b, which originates from a - $c = $global->makeLocalization('c')->origin($b)->data([ + $c = $global->in('c')->data([ 'three' => 'foxtrot', ]); // does not originate from anything - $d = $global->makeLocalization('d')->data([ + $d = $global->in('d')->data([ 'one' => 'golf', 'two' => 'hotel', 'three' => 'india', ]); // originates from d. just to test that it doesn't unintentionally fall back to the default/first. - $e = $global->makeLocalization('e')->origin($d)->data([ + $e = $global->in('e')->data([ 'one' => 'juliett', 'two' => null, ]); - $global->addLocalization($a); - $global->addLocalization($b); - $global->addLocalization($c); - $global->addLocalization($d); - $global->addLocalization($e); - $this->assertEquals([ 'one' => 'alfa', 'two' => 'bravo', diff --git a/tests/Factories/GlobalFactory.php b/tests/Factories/GlobalFactory.php deleted file mode 100644 index e5498094..00000000 --- a/tests/Factories/GlobalFactory.php +++ /dev/null @@ -1,55 +0,0 @@ -id = $id; - - return $this; - } - - public function handle($handle) - { - $this->handle = $handle; - - return $this; - } - - public function data($data) - { - $this->data = $data; - - return $this; - } - - public function make() - { - $set = GlobalSet::make($this->handle); - - $set->addLocalization( - $set->makeLocalization('en')->data($this->data) - ); - - if ($this->id) { - $set->id($this->id); - } - - return $set; - } - - public function create() - { - return tap($this->make())->save(); - } -} diff --git a/tests/Globals/GlobalsetTest.php b/tests/Globals/GlobalsetTest.php deleted file mode 100644 index a7c9c853..00000000 --- a/tests/Globals/GlobalsetTest.php +++ /dev/null @@ -1,30 +0,0 @@ -addLocalization( - $global->makeLocalization('en')->data(['foo' => 'bar', 'baz' => 'qux']) - ); - - $global->save(); - - Event::assertDispatched(GlobalSetSaved::class); - Event::assertDispatched(GlobalVariablesSaved::class); - } -} diff --git a/tests/Repositories/GlobalRepositoryTest.php b/tests/Repositories/GlobalRepositoryTest.php index 197d4868..77c945b0 100644 --- a/tests/Repositories/GlobalRepositoryTest.php +++ b/tests/Repositories/GlobalRepositoryTest.php @@ -24,10 +24,10 @@ protected function setUp(): void $this->app->instance(Stache::class, $stache); $this->repo = new GlobalRepository($stache); - $globalOne = $this->repo->make('contact')->title('Contact Details')->save(); + $globalOne = $this->repo->make('contact')->title('Contact Details')->sites(['en'])->save(); (new Variables)->globalSet($globalOne)->data(['phone' => '555-1234'])->save(); - $globalTwo = $this->repo->make('global')->title('General')->save(); + $globalTwo = $this->repo->make('global')->title('General')->sites(['en'])->save(); (new Variables)->globalSet($globalTwo)->data(['foo' => 'Bar'])->save(); } @@ -89,17 +89,12 @@ public function it_gets_a_global_set_by_handle() #[Test] public function it_saves_a_global_to_the_database() { - $global = GlobalSetAPI::make('new'); - - $global->addLocalization( - $global->makeLocalization('en')->data(['foo' => 'bar', 'baz' => 'qux']) - ); + $global = GlobalSetAPI::make('new')->sites(['en']); $this->assertNull($this->repo->findByHandle('new')); $this->repo->save($global); $this->assertNotNull($item = $this->repo->find('new')); - $this->assertEquals(['foo' => 'bar', 'baz' => 'qux'], $item->in('en')->data()->all()); } } diff --git a/tests/UpdateScripts/Concerns/RunsUpdateScripts.php b/tests/UpdateScripts/Concerns/RunsUpdateScripts.php new file mode 100644 index 00000000..9e036079 --- /dev/null +++ b/tests/UpdateScripts/Concerns/RunsUpdateScripts.php @@ -0,0 +1,29 @@ +update(); + + return $script; + } + + protected function assertUpdateScriptRegistered($class) + { + $this->assertTrue( + app('statamic.update-scripts')->map->class->contains($class), + "Update script $class is not registered." + ); + } +} diff --git a/tests/UpdateScripts/UpdateGlobalVariablesTest.php b/tests/UpdateScripts/UpdateGlobalVariablesTest.php new file mode 100644 index 00000000..adbca2d6 --- /dev/null +++ b/tests/UpdateScripts/UpdateGlobalVariablesTest.php @@ -0,0 +1,62 @@ +string('origin')->nullable(); + }); + } + + #[Test] + public function it_is_registered() + { + $this->assertUpdateScriptRegistered(UpdateGlobalVariables::class); + } + + #[Test] + public function it_builds_the_sites_array_in_a_multisite_install() + { + $this->setSites([ + 'en' => ['url' => '/', 'locale' => 'en_US', 'name' => 'English'], + 'fr' => ['url' => '/', 'locale' => 'fr_FR', 'name' => 'French'], + 'de' => ['url' => '/', 'locale' => 'de_DE', 'name' => 'German'], + ]); + + GlobalSetModel::create(['handle' => 'test', 'title' => 'Test', 'settings' => []]); + VariablesModel::create(['handle' => 'test', 'locale' => 'en', 'origin' => null, 'data' => ['foo' => 'Bar', 'baz' => 'Qux']]); + VariablesModel::create(['handle' => 'test', 'locale' => 'fr', 'origin' => 'en', 'data' => ['foo' => 'Bar']]); + VariablesModel::create(['handle' => 'test', 'locale' => 'de', 'origin' => 'fr', 'data' => []]); + + $this->runUpdateScript(UpdateGlobalVariables::class); + + $this->assertDatabaseHas(GlobalSetModel::class, [ + 'handle' => 'test', + 'settings' => json_encode([ + 'sites' => [ + 'en' => null, + 'fr' => 'en', + 'de' => 'fr', + ], + ]), + ]); + } +} From bd89d3e23ce742f6aa260e085246a9e66b64768e Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Fri, 4 Jul 2025 18:44:40 +0100 Subject: [PATCH 7/9] [5.x] Allow globals and variables to be exported seperately (#431) * Allow globals and variables to be exported seperately * The origin is now defined on the global set * Change approach to export command * Update and add some tests thanks to Claude --------- Co-authored-by: Duncan McClean --- src/Commands/ExportGlobals.php | 82 ++++++--- tests/Commands/ExportGlobalsTest.php | 256 +++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 26 deletions(-) create mode 100644 tests/Commands/ExportGlobalsTest.php diff --git a/src/Commands/ExportGlobals.php b/src/Commands/ExportGlobals.php index 4dd9be28..25683dc9 100644 --- a/src/Commands/ExportGlobals.php +++ b/src/Commands/ExportGlobals.php @@ -2,7 +2,6 @@ namespace Statamic\Eloquent\Commands; -use Closure; use Illuminate\Console\Command; use Illuminate\Support\Facades\Facade; use Statamic\Console\RunsInPlease; @@ -28,7 +27,10 @@ class ExportGlobals extends Command * * @var string */ - protected $signature = 'statamic:eloquent:export-globals'; + protected $signature = 'statamic:eloquent:export-globals + {--force : Force the export to run, with all prompts answered "yes"} + {--only-globals : Only export global sets} + {--only-variables : Only export global variables}'; /** * The console command description. @@ -44,47 +46,75 @@ class ExportGlobals extends Command */ public function handle() { - $this->usingDefaultRepositories(function () { - $this->exportGlobals(); - }); - - return 0; - } - - private function usingDefaultRepositories(Closure $callback) - { + // ensure we are using stache globals, no matter what our config is Facade::clearResolvedInstance(GlobalRepositoryContract::class); - Facade::clearResolvedInstance(GlobalVariablesRepositoryContract::class); - Statamic::repository(GlobalRepositoryContract::class, GlobalRepository::class); - Statamic::repository(GlobalVariablesRepositoryContract::class, GlobalVariablesRepository::class); - app()->bind(GlobalSetContract::class, GlobalSet::class); + + // ensure we are using stache variables, no matter what our config is + Facade::clearResolvedInstance(GlobalVariablesRepositoryContract::class); + Statamic::repository(GlobalVariablesRepositoryContract::class, GlobalVariablesRepository::class); app()->bind(VariablesContract::class, Variables::class); - $callback(); + $this->exportGlobals(); + $this->exportGlobalVariables(); + + return 0; } private function exportGlobals() { + if (! $this->shouldExportGlobals()) { + return; + } + $sets = GlobalSetModel::all(); - $variables = VariablesModel::all(); - $this->withProgressBar($sets, function ($model) use ($variables) { - $global = GlobalSetFacade::make() + $this->withProgressBar($sets, function ($model) { + GlobalSetFacade::make() ->handle($model->handle) - ->title($model->title); + ->title($model->title) + ->sites($model->sites) + ->save(); + }); + + $this->newLine(); + $this->info('Globals exported'); + } + + private function exportGlobalVariables() + { + if (! $this->shouldExportVariables()) { + return; + } + + $variables = VariablesModel::all(); - foreach ($variables->where('handle', $model->handle) as $localization) { - $global->makeLocalization($localization->locale) - ->data($localization->data) - ->origin($localization->origin ?? null); + $this->withProgressBar($variables, function ($model) { + if (! $global = GlobalSetFacade::find($model->handle)) { + return; } - $global->save(); + $globalVariable = $global->makeLocalization($model->locale); + $globalVariable->data($model->data); + $globalVariable->save(); }); $this->newLine(); - $this->info('Globals exported'); + $this->info('Global variables exported'); + } + + private function shouldExportGlobals(): bool + { + return $this->option('only-globals') + || ! $this->option('only-variables') + && ($this->option('force') || $this->confirm('Do you want to export global sets?')); + } + + private function shouldExportVariables(): bool + { + return $this->option('only-variables') + || ! $this->option('only-globals') + && ($this->option('force') || $this->confirm('Do you want to export global variables?')); } } diff --git a/tests/Commands/ExportGlobalsTest.php b/tests/Commands/ExportGlobalsTest.php new file mode 100644 index 00000000..8a3494b9 --- /dev/null +++ b/tests/Commands/ExportGlobalsTest.php @@ -0,0 +1,256 @@ +createGlobalSet(); + $this->createGlobalVariables($globalSet->handle); + + // Run command with force flag + $this->artisan('statamic:eloquent:export-globals', ['--force' => true]) + ->assertExitCode(0) + ->expectsOutput('Globals exported') + ->expectsOutput('Global variables exported'); + + // Verify files were created + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/{$globalSet->handle}.yaml")); + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/en/{$globalSet->handle}.yaml")); + } + + #[Test] + public function it_can_export_only_globals_when_specified() + { + // Create test global set + $globalSet = $this->createGlobalSet(); + $this->createGlobalVariables($globalSet->handle); + + // Run command with only-globals flag + $this->artisan('statamic:eloquent:export-globals', ['--only-globals' => true, '--force' => true]) + ->assertExitCode(0) + ->expectsOutput('Globals exported') + ->doesntExpectOutput('Global variables exported'); + + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/{$globalSet->handle}.yaml")); + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/en/{$globalSet->handle}.yaml")); + } + + #[Test] + public function it_can_export_only_variables_when_specified() + { + // Create test global set and variables + $globalSet = $this->createGlobalSet(); + $this->createGlobalVariables($globalSet->handle); + + // Create the global set file first (needed for variables export) + GlobalSet::make()->handle($globalSet->handle)->title($globalSet->title)->save(); + + // Run command with only-variables flag + $this->artisan('statamic:eloquent:export-globals', ['--only-variables' => true, '--force' => true]) + ->assertExitCode(0) + ->expectsOutput('Global variables exported') + ->doesntExpectOutput('Globals exported'); + + // Verify variables file was created + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/en/{$globalSet->handle}.yaml")); + } + + #[Test] + public function it_prompts_for_confirmation_when_not_forced() + { + $this->createGlobalSet(); + + $this->artisan('statamic:eloquent:export-globals') + ->expectsConfirmation('Do you want to export global sets?', 'yes') + ->expectsConfirmation('Do you want to export global variables?', 'yes') + ->assertExitCode(0); + } + + #[Test] + public function it_skips_globals_when_user_declines() + { + $globalSet = $this->createGlobalSet(); + $this->createGlobalVariables($globalSet->handle); + + // Create the global set file first (needed for variables export) + GlobalSet::make()->handle($globalSet->handle)->title($globalSet->title)->save(); + + $this->artisan('statamic:eloquent:export-globals') + ->expectsConfirmation('Do you want to export global sets?', 'no') + ->expectsConfirmation('Do you want to export global variables?', 'yes') + ->assertExitCode(0) + ->doesntExpectOutput('Globals exported') + ->expectsOutput('Global variables exported'); + } + + #[Test] + public function it_skips_variables_when_user_declines() + { + $this->createGlobalSet(); + + $this->artisan('statamic:eloquent:export-globals') + ->expectsConfirmation('Do you want to export global sets?', 'yes') + ->expectsConfirmation('Do you want to export global variables?', 'no') + ->assertExitCode(0) + ->expectsOutput('Globals exported') + ->doesntExpectOutput('Global variables exported'); + } + + #[Test] + public function it_handles_empty_global_sets() + { + // Ensure there are no global sets + GlobalSetModel::query()->delete(); + + $this->artisan('statamic:eloquent:export-globals', ['--only-globals' => true, '--force' => true]) + ->assertExitCode(0) + ->expectsOutput('Globals exported'); + } + + #[Test] + public function it_handles_empty_variables() + { + // Ensure there are no variables + VariablesModel::query()->delete(); + + $this->artisan('statamic:eloquent:export-globals', ['--only-variables' => true, '--force' => true]) + ->assertExitCode(0) + ->expectsOutput('Global variables exported'); + } + + #[Test] + public function it_skips_variables_when_global_set_not_found() + { + // Create variables for a non-existent global set + $this->createGlobalVariables('non-existent-global'); + + $this->artisan('statamic:eloquent:export-globals', ['--only-variables' => true, '--force' => true]) + ->assertExitCode(0) + ->expectsOutput('Global variables exported'); + + // Verify no variable files were created + $this->assertFileDoesNotExist(Path::resolve('tests/__fixtures__/content/globals/en/non-existent-global.yaml')); + } + + #[Test] + public function it_exports_multiple_globals_and_their_variables() + { + // Create multiple global sets and variables + $global1 = $this->createGlobalSet('site-settings', 'Site Settings'); + $global2 = $this->createGlobalSet('social-media', 'Social Media'); + + $this->createGlobalVariables($global1->handle, ['site_name' => 'My Site', 'description' => 'A test site']); + $this->createGlobalVariables($global2->handle, ['twitter' => '@handle', 'facebook' => 'facebook.com/mypage']); + + $this->artisan('statamic:eloquent:export-globals', ['--force' => true]) + ->assertExitCode(0); + + // Verify all files were created + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/{$global1->handle}.yaml")); + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/en/{$global1->handle}.yaml")); + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/{$global2->handle}.yaml")); + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/en/{$global2->handle}.yaml")); + + // Verify content of variable files + $content1 = File::get(Path::resolve("tests/__fixtures__/content/globals/en/{$global1->handle}.yaml")); + $content2 = File::get(Path::resolve("tests/__fixtures__/content/globals/en/{$global2->handle}.yaml")); + + $this->assertStringContainsString('site_name: \'My Site\'', $content1); + $this->assertStringContainsString('description: \'A test site\'', $content1); + $this->assertStringContainsString('twitter: \'@handle\'', $content2); + $this->assertStringContainsString('facebook: facebook.com/mypage', $content2); + } + + #[Test] + public function it_exports_globals_with_multiple_sites() + { + // Configure app to support multiple sites + config(['statamic.sites.sites' => [ + 'default' => ['name' => 'English', 'locale' => 'en', 'url' => '/'], + 'fr' => ['name' => 'French', 'locale' => 'fr', 'url' => '/fr/'], + ]]); + + // Create global set with multiple sites + $globalSet = $this->createGlobalSet('site-info', 'Site Info', ['default', 'fr']); + + // Create variables for each site + $this->createGlobalVariables($globalSet->handle, ['title' => 'My Site'], 'default'); + $this->createGlobalVariables($globalSet->handle, ['title' => 'Mon Site'], 'fr'); + + $this->artisan('statamic:eloquent:export-globals', ['--force' => true]) + ->assertExitCode(0); + + // Verify files for both sites were created + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/{$globalSet->handle}.yaml")); + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/en/{$globalSet->handle}.yaml")); + $this->assertFileExists(Path::resolve("tests/__fixtures__/content/globals/fr/{$globalSet->handle}.yaml")); + + // Verify content of variable files + File::get(Path::resolve("tests/__fixtures__/content/globals/en/{$globalSet->handle}.yaml")); + File::get(Path::resolve("tests/__fixtures__/content/globals/fr/{$globalSet->handle}.yaml")); + } + + /** + * Create a test GlobalSetModel. + */ + private function createGlobalSet($handle = 'test-global', $title = 'Test Global', $sites = ['default']) + { + return GlobalSetModel::create([ + 'handle' => $handle, + 'title' => $title, + 'settings' => [ + 'sites' => collect($sites)->mapWithKeys(fn ($site) => [$site => null])->all(), + ], + ]); + } + + /** + * Create a test VariablesModel. + */ + private function createGlobalVariables($handle = 'test-global', $data = ['key' => 'value'], $locale = 'en') + { + return VariablesModel::create([ + 'handle' => $handle, + 'locale' => $locale, + 'data' => $data, + ]); + } +} From 3f6feb84183c691963de4a929eb20314cd32397a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 19 Aug 2025 15:56:58 +0100 Subject: [PATCH 8/9] [5.x] Addon Settings (#469) * Addon Settings * Fix styling * Require dev-addons for now * Fix export command test * wip * Allow referencing config options in settings * Avoid importing addons without settings * Some post-merge changes * Switch back to `master` --------- Co-authored-by: duncanmcclean --- README.md | 2 + config/eloquent-driver.php | 5 + ..._07_100000_create_addon_settings_table.php | 21 ++++ src/AddonSettings/AddonSettings.php | 44 +++++++ src/AddonSettings/AddonSettingsModel.php | 25 ++++ src/AddonSettings/AddonSettingsRepository.php | 37 ++++++ src/Commands/ExportAddonSettings.php | 49 ++++++++ src/Commands/ImportAddonSettings.php | 50 ++++++++ src/ServiceProvider.php | 22 ++++ tests/Commands/ExportAddonSettingsTest.php | 67 +++++++++++ tests/Commands/ImportAddonSettingsTest.php | 113 ++++++++++++++++++ .../AddonSettingsRepositoryTest.php | 95 +++++++++++++++ 12 files changed, 530 insertions(+) create mode 100644 database/migrations/2025_07_07_100000_create_addon_settings_table.php create mode 100644 src/AddonSettings/AddonSettings.php create mode 100644 src/AddonSettings/AddonSettingsModel.php create mode 100644 src/AddonSettings/AddonSettingsRepository.php create mode 100644 src/Commands/ExportAddonSettings.php create mode 100644 src/Commands/ImportAddonSettings.php create mode 100644 tests/Commands/ExportAddonSettingsTest.php create mode 100644 tests/Commands/ImportAddonSettingsTest.php create mode 100644 tests/Repositories/AddonSettingsRepositoryTest.php diff --git a/README.md b/README.md index 4ea52a56..2c65ee69 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The command will also give you the opportunity to indicate whether you'd like ex If you originally opt-out of importing existing content, then later change your mind, you can import existing content by running the relevant commands: +- Addon Settings: `php please eloquent:import-addon-settings` - Assets: `php please eloquent:import-assets` - Blueprints and Fieldsets: `php please eloquent:import-blueprints` - Collections: `php please eloquent:import-collections` @@ -47,6 +48,7 @@ If your assets are being driven by the Eloquent Driver and you're managing your If you wish to move back to flat-files, you may use the following commands to export your content out of the database: +- Addon Settings: `php please eloquent:export-addon-settings` - Assets: `php please eloquent:export-assets` - Blueprints and Fieldsets: `php please eloquent:export-blueprints` - Collections: `php please eloquent:export-collections` diff --git a/config/eloquent-driver.php b/config/eloquent-driver.php index 0ba4e51e..a59ea0c4 100644 --- a/config/eloquent-driver.php +++ b/config/eloquent-driver.php @@ -5,6 +5,11 @@ 'connection' => env('STATAMIC_ELOQUENT_CONNECTION', ''), 'table_prefix' => env('STATAMIC_ELOQUENT_PREFIX', ''), + 'addon_settings' => [ + 'driver' => 'file', + 'model' => \Statamic\Eloquent\AddonSettings\AddonSettingsModel::class, + ], + 'asset_containers' => [ 'driver' => 'file', 'model' => \Statamic\Eloquent\Assets\AssetContainerModel::class, diff --git a/database/migrations/2025_07_07_100000_create_addon_settings_table.php b/database/migrations/2025_07_07_100000_create_addon_settings_table.php new file mode 100644 index 00000000..7a2555bb --- /dev/null +++ b/database/migrations/2025_07_07_100000_create_addon_settings_table.php @@ -0,0 +1,21 @@ +prefix('addon_settings'), function (Blueprint $table) { + $table->string('addon')->index()->primary(); + $table->json('settings')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('addon_settings')); + } +}; diff --git a/src/AddonSettings/AddonSettings.php b/src/AddonSettings/AddonSettings.php new file mode 100644 index 00000000..c3fcf31b --- /dev/null +++ b/src/AddonSettings/AddonSettings.php @@ -0,0 +1,44 @@ +addon); + + return (new static($addon, $model->settings))->model($model); + } + + public function toModel() + { + return self::makeModelFromContract($this); + } + + public static function makeModelFromContract(AbstractSettings $settings) + { + $class = app('statamic.eloquent.addon_settings.model'); + + return $class::firstOrNew(['addon' => $settings->addon()->id()])->fill([ + 'settings' => array_filter($settings->raw()), + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + return $this; + } +} diff --git a/src/AddonSettings/AddonSettingsModel.php b/src/AddonSettings/AddonSettingsModel.php new file mode 100644 index 00000000..f0081dba --- /dev/null +++ b/src/AddonSettings/AddonSettingsModel.php @@ -0,0 +1,25 @@ + 'array', + ]; + } +} diff --git a/src/AddonSettings/AddonSettingsRepository.php b/src/AddonSettings/AddonSettingsRepository.php new file mode 100644 index 00000000..a645c76c --- /dev/null +++ b/src/AddonSettings/AddonSettingsRepository.php @@ -0,0 +1,37 @@ +toModel()->save(); + } + + public function delete(AddonSettingsContract $settings): bool + { + return $settings->toModel()->delete(); + } + + public static function bindings(): array + { + return [ + AddonSettingsContract::class => AddonSettings::class, + ]; + } +} diff --git a/src/Commands/ExportAddonSettings.php b/src/Commands/ExportAddonSettings.php new file mode 100644 index 00000000..3bd88be3 --- /dev/null +++ b/src/Commands/ExportAddonSettings.php @@ -0,0 +1,49 @@ +each(function ($model) { + Addon::get($model->addon)?->settings()->set($model->settings)->save(); + }); + + $this->newLine(); + $this->info('Addon settings exported'); + + return 0; + } +} diff --git a/src/Commands/ImportAddonSettings.php b/src/Commands/ImportAddonSettings.php new file mode 100644 index 00000000..72af0508 --- /dev/null +++ b/src/Commands/ImportAddonSettings.php @@ -0,0 +1,50 @@ +filter(fn ($addon) => collect($addon->settings()->raw())->filter()->isNotEmpty()) + ->each(function ($addon) { + app('statamic.eloquent.addon_settings.model')::updateOrCreate( + ['addon' => $addon->id()], + ['settings' => $addon->settings()->raw()] + ); + }); + + $this->components->info('Addon settings imported successfully.'); + + return 0; + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index a3bc935b..51f1c691 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Console\AboutCommand; use Statamic\Assets\AssetContainerContents; +use Statamic\Contracts\Addons\SettingsRepository as AddonSettingsRepositoryContract; use Statamic\Contracts\Assets\AssetContainerRepository as AssetContainerRepositoryContract; use Statamic\Contracts\Assets\AssetRepository as AssetRepositoryContract; use Statamic\Contracts\Entries\CollectionRepository as CollectionRepositoryContract; @@ -19,6 +20,7 @@ use Statamic\Contracts\Taxonomies\TaxonomyRepository as TaxonomyRepositoryContract; use Statamic\Contracts\Taxonomies\TermRepository as TermRepositoryContract; use Statamic\Contracts\Tokens\TokenRepository as TokenRepositoryContract; +use Statamic\Eloquent\AddonSettings\AddonSettingsRepository; use Statamic\Eloquent\Assets\AssetContainerContents as EloquentAssetContainerContents; use Statamic\Eloquent\Assets\AssetContainerRepository; use Statamic\Eloquent\Assets\AssetQueryBuilder; @@ -171,6 +173,10 @@ private function publishMigrations(): void __DIR__.'/../database/migrations/2024_07_16_100000_create_sites_table.php' => database_path('migrations/2024_07_16_100000_create_sites_table.php'), ], 'statamic-eloquent-site-migrations'); + $this->publishes($addonSettingMigrations = [ + __DIR__.'/../database/migrations/2025_07_07_100000_create_addon_settings_table.php' => database_path('migrations/2025_07_07_100000_create_addon_settings_table.php'), + ], 'statamic-eloquent-addon-setting-migrations'); + $this->publishes( array_merge( $taxonomyMigrations, @@ -189,6 +195,7 @@ private function publishMigrations(): void $revisionMigrations, $tokenMigrations, $siteMigrations, + $addonSettingMigrations ), 'migrations' ); @@ -204,6 +211,7 @@ private function publishMigrations(): void public function register() { + $this->registerAddonSettings(); $this->registerAssetContainers(); $this->registerAssets(); $this->registerBlueprints(); @@ -224,6 +232,19 @@ public function register() $this->registerSites(); } + private function registerAddonSettings() + { + if (config('statamic.eloquent-driver.addon_settings.driver', 'file') != 'eloquent') { + return; + } + + $this->app->bind('statamic.eloquent.addon_settings.model', function () { + return config('statamic.eloquent-driver.addon_settings.model'); + }); + + Statamic::repository(AddonSettingsRepositoryContract::class, AddonSettingsRepository::class); + } + private function registerAssetContainers() { // if we have this config key then we started on 2.1.0 or earlier when @@ -549,6 +570,7 @@ protected function addAboutCommandInfo() } AboutCommand::add('Statamic Eloquent Driver', collect([ + 'Addon Settings' => config('statamic.eloquent-driver.addon_settings.driver', 'file'), 'Asset Containers' => config('statamic.eloquent-driver.asset_containers.driver', 'file'), 'Assets' => config('statamic.eloquent-driver.assets.driver', 'file'), 'Blueprints' => config('statamic.eloquent-driver.blueprints.driver', 'file'), diff --git a/tests/Commands/ExportAddonSettingsTest.php b/tests/Commands/ExportAddonSettingsTest.php new file mode 100644 index 00000000..3ea99fb7 --- /dev/null +++ b/tests/Commands/ExportAddonSettingsTest.php @@ -0,0 +1,67 @@ +makeFromPackage(['id' => 'statamic/seo-pro', 'slug' => 'seo-pro']); + $importer = $this->makeFromPackage(['id' => 'statamic/importer', 'slug' => 'importer']); + + Facades\Addon::shouldReceive('all')->andReturn(collect([$seoPro, $importer])); + Facades\Addon::shouldReceive('get')->with('statamic/seo-pro')->andReturn($seoPro); + Facades\Addon::shouldReceive('get')->with('statamic/importer')->andReturn($importer); + + AddonSettingsModel::create(['addon' => 'statamic/seo-pro', 'settings' => ['title' => 'SEO Title', 'description' => 'SEO Description']]); + AddonSettingsModel::create(['addon' => 'statamic/importer', 'settings' => ['chunk_size' => 100]]); + + $this->artisan('statamic:eloquent:export-addon-settings') + ->expectsOutputToContain('Addon settings exported') + ->assertExitCode(0); + + $this->assertFileExists(resource_path('addons/seo-pro.yaml')); + $this->assertEquals(<<<'YAML' +title: 'SEO Title' +description: 'SEO Description' + +YAML + , File::get(resource_path('addons/seo-pro.yaml'))); + + $this->assertFileExists(resource_path('addons/importer.yaml')); + $this->assertEquals(<<<'YAML' +chunk_size: 100 + +YAML + , File::get(resource_path('addons/importer.yaml'))); + } + + private function makeFromPackage($attributes = []) + { + return Addon::makeFromPackage(array_merge([ + 'id' => 'vendor/test-addon', + 'name' => 'Test Addon', + 'description' => 'Test description', + 'namespace' => 'Vendor\\TestAddon', + 'provider' => TestAddonServiceProvider::class, + 'autoload' => '', + 'url' => 'http://test-url.com', + 'developer' => 'Test Developer LLC', + 'developerUrl' => 'http://test-developer.com', + 'version' => '1.0', + 'editions' => ['foo', 'bar'], + ], $attributes)); + } +} diff --git a/tests/Commands/ImportAddonSettingsTest.php b/tests/Commands/ImportAddonSettingsTest.php new file mode 100644 index 00000000..fa423e82 --- /dev/null +++ b/tests/Commands/ImportAddonSettingsTest.php @@ -0,0 +1,113 @@ +app->bind(SettingsContract::class, FileSettings::class); + $this->app->bind(SettingsRepositoryContract::class, FileSettingsRepository::class); + + $this->app->bind('statamic.eloquent.addon_settings.model', function () { + return AddonSettingsModel::class; + }); + + $this->app['files']->deleteDirectory(resource_path('addons')); + } + + #[Test] + public function it_imports_addon_settings() + { + $this->assertCount(0, AddonSettingsModel::all()); + + $seoPro = $this->makeFromPackage(['id' => 'statamic/seo-pro']); + Facades\Addon::shouldReceive('get')->with('statamic/seo-pro')->andReturn($seoPro); + app(SettingsRepositoryContract::class)->make($seoPro, ['title' => 'SEO Title', 'description' => 'SEO Description'])->save(); + + $importer = $this->makeFromPackage(['id' => 'statamic/importer']); + Facades\Addon::shouldReceive('get')->with('statamic/importer')->andReturn($importer); + app(SettingsRepositoryContract::class)->make($importer, ['chunk_size' => 100])->save(); + + Facades\Addon::shouldReceive('all')->andReturn(collect([$seoPro, $importer])); + + $this->artisan('statamic:eloquent:import-addon-settings') + ->expectsOutputToContain('Addon settings imported successfully.') + ->assertExitCode(0); + + $this->assertCount(2, AddonSettingsModel::all()); + + $this->assertDatabaseHas('addon_settings', [ + 'addon' => 'statamic/seo-pro', + 'settings' => json_encode(['title' => 'SEO Title', 'description' => 'SEO Description']), + ]); + + $this->assertDatabaseHas('addon_settings', [ + 'addon' => 'statamic/importer', + 'settings' => json_encode(['chunk_size' => 100]), + ]); + } + + #[Test] + public function it_doesnt_import_addons_without_settings() + { + $this->assertCount(0, AddonSettingsModel::all()); + + $seoPro = $this->makeFromPackage(['id' => 'statamic/seo-pro']); + Facades\Addon::shouldReceive('get')->with('statamic/seo-pro')->andReturn($seoPro); + app(SettingsRepositoryContract::class)->make($seoPro, ['title' => 'SEO Title', 'description' => 'SEO Description'])->save(); + + $importer = $this->makeFromPackage(['id' => 'statamic/importer']); + Facades\Addon::shouldReceive('get')->with('statamic/importer')->andReturn($importer); + + Facades\Addon::shouldReceive('all')->andReturn(collect([$seoPro, $importer])); + + $this->artisan('statamic:eloquent:import-addon-settings') + ->expectsOutputToContain('Addon settings imported successfully.') + ->assertExitCode(0); + + $this->assertCount(1, AddonSettingsModel::all()); + + $this->assertDatabaseHas('addon_settings', [ + 'addon' => 'statamic/seo-pro', + 'settings' => json_encode(['title' => 'SEO Title', 'description' => 'SEO Description']), + ]); + + $this->assertDatabaseMissing('addon_settings', [ + 'addon' => 'statamic/importer', + ]); + } + + private function makeFromPackage($attributes = []) + { + return Addon::makeFromPackage(array_merge([ + 'id' => 'vendor/test-addon', + 'name' => 'Test Addon', + 'description' => 'Test description', + 'namespace' => 'Vendor\\TestAddon', + 'provider' => TestAddonServiceProvider::class, + 'autoload' => '', + 'url' => 'http://test-url.com', + 'developer' => 'Test Developer LLC', + 'developerUrl' => 'http://test-developer.com', + 'version' => '1.0', + 'editions' => ['foo', 'bar'], + ], $attributes)); + } +} diff --git a/tests/Repositories/AddonSettingsRepositoryTest.php b/tests/Repositories/AddonSettingsRepositoryTest.php new file mode 100644 index 00000000..27996102 --- /dev/null +++ b/tests/Repositories/AddonSettingsRepositoryTest.php @@ -0,0 +1,95 @@ +repo = new AddonSettingsRepository; + } + + #[Test] + public function it_gets_addon_settings() + { + $addon = $this->makeFromPackage(); + + Facades\Addon::shouldReceive('all')->andReturn(collect([$addon])); + Facades\Addon::shouldReceive('get')->with('vendor/test-addon')->andReturn($addon); + + AddonSettingsModel::create(['addon' => 'vendor/test-addon', 'settings' => ['foo' => 'bar', 'baz' => 'qux']]); + + $settings = $this->repo->find('vendor/test-addon'); + + $this->assertInstanceOf(AddonSettings::class, $settings); + $this->assertEquals($addon, $settings->addon()); + $this->assertEquals(['foo' => 'bar', 'baz' => 'qux'], $settings->all()); + } + + #[Test] + public function it_saves_addon_settings() + { + $addon = $this->makeFromPackage(); + + $settings = $this->repo->make($addon, [ + 'foo' => 'bar', + 'baz' => 'qux', + 'quux' => null, // Should be filtered out. + ]); + + $settings->save(); + + $this->assertDatabaseHas('addon_settings', [ + 'addon' => 'vendor/test-addon', + 'settings' => json_encode(['foo' => 'bar', 'baz' => 'qux']), + ]); + } + + #[Test] + public function it_deletes_addon_settings() + { + $addon = $this->makeFromPackage(); + + Facades\Addon::shouldReceive('all')->andReturn(collect([$addon])); + Facades\Addon::shouldReceive('get')->with('vendor/test-addon')->andReturn($addon); + + AddonSettingsModel::create(['addon' => 'vendor/test-addon', 'settings' => ['foo' => 'bar', 'baz' => 'qux']]); + + $this->repo->find('vendor/test-addon')->delete(); + + $this->assertDatabaseMissing('addon_settings', [ + 'addon' => 'vendor/test-addon', + ]); + } + + private function makeFromPackage($attributes = []) + { + return Addon::makeFromPackage(array_merge([ + 'id' => 'vendor/test-addon', + 'name' => 'Test Addon', + 'description' => 'Test description', + 'namespace' => 'Vendor\\TestAddon', + 'provider' => TestAddonServiceProvider::class, + 'autoload' => '', + 'url' => 'http://test-url.com', + 'developer' => 'Test Developer LLC', + 'developerUrl' => 'http://test-developer.com', + 'version' => '1.0', + 'editions' => ['foo', 'bar'], + ], $attributes)); + } +} From ad9d58d80e0c53886efb8f27e52a68216634dc47 Mon Sep 17 00:00:00 2001 From: Adam Patterson Date: Tue, 23 Sep 2025 17:50:59 -0600 Subject: [PATCH 9/9] Updates migration stubs to match the package migrations (#502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migration stubs currently don’t match the default migration files. Removed the 30 character limit on the `form` column in `form_submissions`. The `forms` table doesn’t have a limit set on `handle` so it’s possible to create a handle that can’t be stored in `form_submissions`. --- .../updates/relate_form_submissions_by_handle.php.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/updates/relate_form_submissions_by_handle.php.stub b/database/migrations/updates/relate_form_submissions_by_handle.php.stub index 68b3552f..286beca9 100644 --- a/database/migrations/updates/relate_form_submissions_by_handle.php.stub +++ b/database/migrations/updates/relate_form_submissions_by_handle.php.stub @@ -14,7 +14,7 @@ return new class extends Migration { } Schema::table($this->prefix('form_submissions'), function (Blueprint $table) { - $table->string('form', 30)->nullable()->index()->after('id'); + $table->string('form')->nullable()->index()->after('id'); }); $forms = FormModel::all()->pluck('handle', 'id');