├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── pint-fix.yml │ ├── pint-lint.yml │ └── tests.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── eloquent-driver.php ├── database └── migrations │ ├── 2024_03_07_100000_create_asset_containers_table.php │ ├── 2024_03_07_100000_create_asset_table.php │ ├── 2024_03_07_100000_create_blueprints_table.php │ ├── 2024_03_07_100000_create_collections_table.php │ ├── 2024_03_07_100000_create_fieldsets_table.php │ ├── 2024_03_07_100000_create_form_submissions_table.php │ ├── 2024_03_07_100000_create_forms_table.php │ ├── 2024_03_07_100000_create_global_variables_table.php │ ├── 2024_03_07_100000_create_globals_table.php │ ├── 2024_03_07_100000_create_navigation_trees_table.php │ ├── 2024_03_07_100000_create_navigations_table.php │ ├── 2024_03_07_100000_create_revisions_table.php │ ├── 2024_03_07_100000_create_taxonomies_table.php │ ├── 2024_03_07_100000_create_terms_table.php │ ├── 2024_03_07_100000_create_tokens_table.php │ ├── 2024_05_15_100000_modify_form_submissions_id.php │ ├── 2024_07_16_100000_create_sites_table.php │ ├── entries │ ├── 2024_03_07_100000_create_entries_table.php │ └── 2024_03_07_100000_create_entries_table_with_string_ids.php │ └── updates │ ├── add_blueprint_to_entries_table.php.stub │ ├── add_id_to_attributes_in_revisions_table.php.stub │ ├── add_index_to_date_on_entries_table.php.stub │ ├── add_order_to_entries_table.php.stub │ ├── drop_foreign_keys_on_entries.php.stub │ ├── drop_foreign_keys_on_forms.php.stub │ ├── drop_status_on_entries.php.stub │ ├── relate_form_submissions_by_handle.php.stub │ ├── update_assets_table.php.stub │ └── update_globals_table.php.stub ├── phpunit.xml.dist ├── pint.json ├── src ├── Assets │ ├── Asset.php │ ├── AssetContainer.php │ ├── AssetContainerContents.php │ ├── AssetContainerModel.php │ ├── AssetContainerRepository.php │ ├── AssetModel.php │ ├── AssetQueryBuilder.php │ └── AssetRepository.php ├── Collections │ ├── Collection.php │ ├── CollectionModel.php │ └── CollectionRepository.php ├── Commands │ ├── ExportAssets.php │ ├── ExportBlueprints.php │ ├── ExportCollections.php │ ├── ExportEntries.php │ ├── ExportForms.php │ ├── ExportGlobals.php │ ├── ExportNavs.php │ ├── ExportRevisions.php │ ├── ExportSites.php │ ├── ExportTaxonomies.php │ ├── ImportAssets.php │ ├── ImportBlueprints.php │ ├── ImportCollections.php │ ├── ImportEntries.php │ ├── ImportForms.php │ ├── ImportGlobals.php │ ├── ImportNavs.php │ ├── ImportRevisions.php │ ├── ImportSites.php │ ├── ImportTaxonomies.php │ └── SyncAssets.php ├── Database │ ├── BaseMigration.php │ └── BaseModel.php ├── Entries │ ├── Entry.php │ ├── EntryModel.php │ ├── EntryQueryBuilder.php │ ├── EntryRepository.php │ └── UuidEntryModel.php ├── Fields │ ├── BlueprintModel.php │ ├── BlueprintRepository.php │ ├── FieldsetModel.php │ └── FieldsetRepository.php ├── Forms │ ├── Form.php │ ├── FormModel.php │ ├── FormRepository.php │ ├── Submission.php │ ├── SubmissionModel.php │ ├── SubmissionQueryBuilder.php │ └── SubmissionRepository.php ├── Globals │ ├── GlobalRepository.php │ ├── GlobalSet.php │ ├── GlobalSetModel.php │ ├── GlobalVariablesRepository.php │ ├── Variables.php │ └── VariablesModel.php ├── Jobs │ ├── UpdateCollectionEntryOrder.php │ └── UpdateCollectionEntryParent.php ├── Revisions │ ├── Revision.php │ ├── RevisionModel.php │ └── RevisionRepository.php ├── ServiceProvider.php ├── Sites │ ├── SiteModel.php │ └── Sites.php ├── Structures │ ├── CollectionStructure.php │ ├── CollectionTree.php │ ├── CollectionTreeRepository.php │ ├── Nav.php │ ├── NavModel.php │ ├── NavTree.php │ ├── NavTreeRepository.php │ ├── NavigationRepository.php │ └── TreeModel.php ├── Taxonomies │ ├── Taxonomy.php │ ├── TaxonomyModel.php │ ├── TaxonomyRepository.php │ ├── Term.php │ ├── TermModel.php │ ├── TermQueryBuilder.php │ └── TermRepository.php ├── Tokens │ ├── Token.php │ ├── TokenModel.php │ └── TokenRepository.php └── Updates │ ├── AddBlueprintToEntriesTable.php │ ├── AddIdToAttributesInRevisionsTable.php │ ├── AddIndexToDateOnEntriesTable.php │ ├── AddMetaAndIndexesToAssetsTable.php │ ├── AddOrderToEntriesTable.php │ ├── ChangeDefaultBlueprint.php │ ├── ChangeFormSubmissionsIdType.php │ ├── DropForeignKeysOnEntriesAndForms.php │ ├── DropStatusOnEntries.php │ ├── MoveConfig.php │ ├── RelateFormSubmissionsByHandle.php │ └── SplitGlobalsFromVariables.php └── tests ├── Assets ├── AssetContainerContentsTest.php └── AssetTest.php ├── Collections └── CollectionTest.php ├── Commands ├── ExportSitesTest.php ├── ImportAssetsTest.php ├── ImportBlueprintsTest.php ├── ImportCollectionsTest.php ├── ImportEntriesTest.php ├── ImportFormsTest.php ├── ImportGlobalsTest.php ├── ImportNavsTest.php ├── ImportRevisionsTest.php ├── ImportSitesTest.php └── ImportTaxonomiesTest.php ├── Data ├── Assets │ └── AssetQueryBuilderTest.php ├── Entries │ └── EntryQueryBuilderTest.php ├── Fields │ ├── BlueprintTest.php │ └── FieldsetTest.php ├── Globals │ ├── GlobalSetTest.php │ └── VariablesTest.php └── Taxonomies │ └── TermQueryBuilderTest.php ├── Entries ├── EntryModelTest.php ├── EntryRepositoryTest.php └── EntryTest.php ├── Factories ├── EntryFactory.php ├── FieldsetFactory.php └── GlobalFactory.php ├── Forms ├── FormSubmissionTest.php └── FormTest.php ├── Globals └── GlobalsetTest.php ├── Repositories ├── AssetContainerRepositoryTest.php ├── CollectionRepositoryTest.php ├── GlobalRepositoryTest.php ├── NavigationRepositoryTest.php ├── TaxonomyRepositoryTest.php └── TokenRepositoryTest.php ├── Sites └── SitesTest.php ├── Terms └── TermTest.php ├── TestCase.php └── __fixtures__ ├── dev-null └── .gitkeep └── resources └── fieldsets └── seo.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Fill out a bug report to help us improve the Statamic Eloquent Driver. 3 | labels: [bug] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Bug description 8 | description: What happened? What did you expect to happen? Feel free to drop any screenshots in here. 9 | placeholder: I did this thing over here, and saw this error... 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: How to reproduce 15 | description: List the steps so we're able to recreate this bug. Bonus points if you can provide an example GitHub repo with this bug reproduced. 16 | placeholder: Go here, Type this, Click that, Look over there. 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: Logs 22 | description: You can paste any relevant logs here, they'll be automatically rendered in code blocks. You can find your logs in `storage/logs`. 23 | render: shell 24 | - type: textarea 25 | attributes: 26 | label: Environment 27 | description: | 28 | Please paste the *full* output of the `php please support:details` command. It gives us some context about your project. 29 | render: yaml # the format of the command is close to yaml and gets highlighted nicely 30 | validations: 31 | required: true 32 | - type: textarea 33 | attributes: 34 | label: Additional details 35 | description: Fancy setup? Anything else you need to share? 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Statamic Core 5 | url: https://github.com/statamic/cms/issues/new/choose 6 | about: This repository is for the Eloquent Driver. If you're having issues with Statamic itself, please open an issue on the cms repository. 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Got an idea for a new feature or improvement in the Eloquent Driver? We'd love to hear it! 3 | labels: [feature request] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Description 8 | description: What feature or improvement would you like to see? Feel free to provide as much detail as you can. 9 | validations: 10 | required: true 11 | -------------------------------------------------------------------------------- /.github/workflows/pint-fix.yml: -------------------------------------------------------------------------------- 1 | name: Fix PHP code style issues 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.php' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | fix-php-code-styling: 13 | runs-on: ubuntu-latest 14 | if: github.repository_owner == 'statamic' 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | with: 20 | ref: ${{ github.head_ref }} 21 | token: ${{ secrets.PINT }} 22 | 23 | - name: Fix PHP code style issues 24 | uses: aglipanci/laravel-pint-action@1.0.0 25 | 26 | - name: Commit changes 27 | uses: stefanzweifel/git-auto-commit-action@v4 28 | with: 29 | commit_message: Fix styling 30 | -------------------------------------------------------------------------------- /.github/workflows/pint-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint PHP code style issues 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.php' 7 | 8 | jobs: 9 | lint-php-code-styling: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Check PHP code style issues 17 | uses: aglipanci/laravel-pint-action@1.0.0 18 | with: 19 | testMode: true 20 | verboseMode: true 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | php-tests: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | php: [8.1, 8.2, 8.3, 8.4] 16 | laravel: [10.*, 11.*, 12.*] 17 | stability: [prefer-lowest, prefer-stable] 18 | exclude: 19 | - php: 8.1 20 | laravel: 11.* 21 | - php: 8.1 22 | laravel: 12.* 23 | - php: 8.4 24 | laravel: 10.* 25 | 26 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} 27 | 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v1 31 | 32 | - name: Setup PHP 33 | uses: shivammathur/setup-php@v2 34 | with: 35 | php-version: ${{ matrix.php }} 36 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 37 | coverage: none 38 | 39 | - name: Install dependencies 40 | run: | 41 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 42 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-suggest 43 | 44 | - name: Execute tests 45 | run: vendor/bin/phpunit 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | .phpunit.result.cache 4 | .php_cs.cache 5 | .nova/* 6 | .phpunit.cache 7 | .idea 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Statamic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statamic/eloquent-driver", 3 | "description": "Allows you to store Statamic data in a database.", 4 | "license": "MIT", 5 | "autoload": { 6 | "psr-4": { 7 | "Statamic\\Eloquent\\": "src" 8 | } 9 | }, 10 | "autoload-dev": { 11 | "psr-4": { 12 | "Tests\\": "tests" 13 | } 14 | }, 15 | "extra": { 16 | "statamic": { 17 | "name": "Eloquent Driver", 18 | "description": "Allows you to store Statamic data in a database." 19 | }, 20 | "laravel": { 21 | "providers": [ 22 | "Statamic\\Eloquent\\ServiceProvider" 23 | ] 24 | } 25 | }, 26 | "require": { 27 | "php": "^8.1", 28 | "statamic/cms": "^5.41" 29 | }, 30 | "require-dev": { 31 | "doctrine/dbal": "^3.8", 32 | "laravel/pint": "^1.0", 33 | "orchestra/testbench": "^8.28 || ^9.6.1 || ^10.0", 34 | "phpunit/phpunit": "^10.5.35 || ^11.0" 35 | }, 36 | "scripts": { 37 | "test": "phpunit" 38 | }, 39 | "config": { 40 | "allow-plugins": { 41 | "pixelfear/composer-dist-plugin": true 42 | } 43 | }, 44 | "minimum-stability": "dev", 45 | "prefer-stable": true 46 | } 47 | -------------------------------------------------------------------------------- /config/eloquent-driver.php: -------------------------------------------------------------------------------- 1 | env('STATAMIC_ELOQUENT_CONNECTION', ''), 6 | 'table_prefix' => env('STATAMIC_ELOQUENT_PREFIX', ''), 7 | 8 | 'asset_containers' => [ 9 | 'driver' => 'file', 10 | 'model' => \Statamic\Eloquent\Assets\AssetContainerModel::class, 11 | ], 12 | 13 | 'assets' => [ 14 | 'driver' => 'file', 15 | 'model' => \Statamic\Eloquent\Assets\AssetModel::class, 16 | 'asset' => \Statamic\Eloquent\Assets\Asset::class, 17 | ], 18 | 19 | 'blueprints' => [ 20 | 'driver' => 'file', 21 | 'model' => \Statamic\Eloquent\Fields\BlueprintModel::class, 22 | 'namespaces' => 'all', 23 | ], 24 | 25 | 'collections' => [ 26 | 'driver' => 'file', 27 | 'model' => \Statamic\Eloquent\Collections\CollectionModel::class, 28 | 'update_entry_order_queue' => 'default', 29 | 'update_entry_order_connection' => 'default', 30 | ], 31 | 32 | 'collection_trees' => [ 33 | 'driver' => 'file', 34 | 'model' => \Statamic\Eloquent\Structures\TreeModel::class, 35 | 'tree' => \Statamic\Eloquent\Structures\CollectionTree::class, 36 | ], 37 | 38 | 'entries' => [ 39 | 'driver' => 'file', 40 | 'model' => \Statamic\Eloquent\Entries\EntryModel::class, 41 | 'entry' => \Statamic\Eloquent\Entries\Entry::class, 42 | 'map_data_to_columns' => false, 43 | ], 44 | 45 | 'fieldsets' => [ 46 | 'driver' => 'file', 47 | 'model' => \Statamic\Eloquent\Fields\FieldsetModel::class, 48 | ], 49 | 50 | 'forms' => [ 51 | 'driver' => 'file', 52 | 'model' => \Statamic\Eloquent\Forms\FormModel::class, 53 | ], 54 | 55 | 'form_submissions' => [ 56 | 'driver' => 'file', 57 | 'model' => \Statamic\Eloquent\Forms\SubmissionModel::class, 58 | ], 59 | 60 | 'global_sets' => [ 61 | 'driver' => 'file', 62 | 'model' => \Statamic\Eloquent\Globals\GlobalSetModel::class, 63 | ], 64 | 65 | 'global_set_variables' => [ 66 | 'driver' => 'file', 67 | 'model' => \Statamic\Eloquent\Globals\VariablesModel::class, 68 | ], 69 | 70 | 'navigations' => [ 71 | 'driver' => 'file', 72 | 'model' => \Statamic\Eloquent\Structures\NavModel::class, 73 | ], 74 | 75 | 'navigation_trees' => [ 76 | 'driver' => 'file', 77 | 'model' => \Statamic\Eloquent\Structures\TreeModel::class, 78 | 'tree' => \Statamic\Eloquent\Structures\NavTree::class, 79 | ], 80 | 81 | 'revisions' => [ 82 | 'driver' => 'file', 83 | 'model' => \Statamic\Eloquent\Revisions\RevisionModel::class, 84 | ], 85 | 86 | 'taxonomies' => [ 87 | 'driver' => 'file', 88 | 'model' => \Statamic\Eloquent\Taxonomies\TaxonomyModel::class, 89 | ], 90 | 91 | 'terms' => [ 92 | 'driver' => 'file', 93 | 'model' => \Statamic\Eloquent\Taxonomies\TermModel::class, 94 | ], 95 | 96 | 'tokens' => [ 97 | 'driver' => 'file', 98 | 'model' => \Statamic\Eloquent\Tokens\TokenModel::class, 99 | ], 100 | 101 | 'sites' => [ 102 | 'driver' => 'file', 103 | 'model' => \Statamic\Eloquent\Sites\SiteModel::class, 104 | ], 105 | ]; 106 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_asset_containers_table.php: -------------------------------------------------------------------------------- 1 | prefix('asset_containers'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->unique(); 14 | $table->string('title'); 15 | $table->string('disk'); 16 | $table->jsonb('settings')->nullable(); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists($this->prefix('asset_containers')); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_asset_table.php: -------------------------------------------------------------------------------- 1 | prefix('assets_meta'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('container')->index(); 14 | $table->string('folder')->index(); 15 | $table->string('basename')->index(); 16 | $table->string('filename')->index(); 17 | $table->char('extension', 10)->index(); 18 | $table->string('path')->index(); 19 | $table->jsonb('meta')->nullable(); 20 | $table->timestamps(); 21 | 22 | $table->unique(['container', 'folder', 'basename']); 23 | }); 24 | } 25 | 26 | public function down() 27 | { 28 | Schema::dropIfExists($this->prefix('assets_meta')); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_blueprints_table.php: -------------------------------------------------------------------------------- 1 | prefix('blueprints'), function (Blueprint $table) { 13 | $table->id(); 14 | $table->string('namespace')->nullable()->default(null)->index(); 15 | $table->string('handle'); 16 | $table->jsonb('data'); 17 | $table->timestamps(); 18 | 19 | $table->unique(['handle', 'namespace']); 20 | }); 21 | 22 | $this->seedDefaultBlueprint(); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists($this->prefix('blueprints')); 28 | } 29 | 30 | public function seedDefaultBlueprint() 31 | { 32 | try { 33 | $config = json_encode([ 34 | 'fields' => [ 35 | [ 36 | 'field' => [ 37 | 'type' => 'markdown', 38 | 'display' => 'Content', 39 | 'localizable' => true, 40 | ], 41 | 'handle' => 'content', 42 | ], 43 | ], 44 | ]); 45 | } catch (\JsonException $e) { 46 | $config = '[]'; 47 | } 48 | 49 | DB::table($this->prefix('blueprints'))->insert([ 50 | 'namespace' => null, 51 | 'handle' => 'default', 52 | 'data' => $config, 53 | 'created_at' => Carbon::now(), 54 | ]); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_collections_table.php: -------------------------------------------------------------------------------- 1 | prefix('collections'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->unique(); 14 | $table->string('title'); 15 | $table->jsonb('settings')->nullable(); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists($this->prefix('collections')); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_fieldsets_table.php: -------------------------------------------------------------------------------- 1 | prefix('fieldsets'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->unique(); 14 | $table->jsonb('data'); 15 | $table->timestamps(); 16 | }); 17 | } 18 | 19 | public function down() 20 | { 21 | Schema::dropIfExists($this->prefix('fieldsets')); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_form_submissions_table.php: -------------------------------------------------------------------------------- 1 | prefix('form_submissions'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('form')->nullable()->index(); 14 | $table->jsonb('data')->nullable(); 15 | $table->timestamps(6); 16 | 17 | $table->unique(['form', 'created_at']); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists($this->prefix('form_submissions')); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_forms_table.php: -------------------------------------------------------------------------------- 1 | prefix('forms'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->unique(); 14 | $table->string('title'); 15 | $table->jsonb('settings')->nullable(); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists($this->prefix('forms')); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_global_variables_table.php: -------------------------------------------------------------------------------- 1 | prefix('global_set_variables'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->index(); 14 | $table->string('locale')->nullable(); 15 | $table->string('origin')->nullable(); 16 | $table->jsonb('data'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists($this->prefix('global_set_variables')); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_globals_table.php: -------------------------------------------------------------------------------- 1 | prefix('global_sets'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->unique(); 14 | $table->string('title'); 15 | $table->jsonb('settings'); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists($this->prefix('global_sets')); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_navigation_trees_table.php: -------------------------------------------------------------------------------- 1 | prefix('trees'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle'); 14 | $table->string('type')->index(); 15 | $table->string('locale')->nullable()->index(); 16 | $table->jsonb('tree')->nullable(); 17 | $table->jsonb('settings')->nullable(); 18 | $table->timestamps(); 19 | 20 | $table->unique(['handle', 'type', 'locale']); 21 | }); 22 | } 23 | 24 | public function down() 25 | { 26 | Schema::dropIfExists($this->prefix('trees')); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_navigations_table.php: -------------------------------------------------------------------------------- 1 | prefix('navigations'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->unique(); 14 | $table->string('title'); 15 | $table->jsonb('settings')->nullable(); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists($this->prefix('navigations')); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_revisions_table.php: -------------------------------------------------------------------------------- 1 | prefix('revisions'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('key'); 14 | $table->string('action')->index(); 15 | $table->string('user')->nullable(); 16 | $table->string('message')->nullable(); 17 | $table->jsonb('attributes')->nullable(); 18 | $table->timestamps(); 19 | 20 | $table->unique(['key', 'created_at']); 21 | }); 22 | } 23 | 24 | public function down() 25 | { 26 | Schema::dropIfExists($this->prefix('revisions')); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_taxonomies_table.php: -------------------------------------------------------------------------------- 1 | prefix('taxonomies'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->unique(); 14 | $table->string('title'); 15 | $table->jsonb('sites')->nullable(); 16 | $table->jsonb('settings')->nullable(); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::dropIfExists($this->prefix('taxonomies')); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_terms_table.php: -------------------------------------------------------------------------------- 1 | prefix('taxonomy_terms'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('site')->index(); 14 | $table->string('slug'); 15 | $table->string('uri')->nullable()->index(); 16 | $table->string('taxonomy')->index(); 17 | $table->jsonb('data'); 18 | $table->timestamps(); 19 | 20 | $table->unique(['slug', 'taxonomy', 'site']); 21 | }); 22 | } 23 | 24 | public function down() 25 | { 26 | Schema::dropIfExists($this->prefix('taxonomy_terms')); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_03_07_100000_create_tokens_table.php: -------------------------------------------------------------------------------- 1 | prefix('tokens'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('token')->unique(); 14 | $table->string('handler'); 15 | $table->jsonb('data'); 16 | $table->timestamp('expire_at'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists($this->prefix('tokens')); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/2024_05_15_100000_modify_form_submissions_id.php: -------------------------------------------------------------------------------- 1 | prefix('form_submissions'), function (Blueprint $table) { 12 | $table->decimal('id', 14, 4)->index()->unique()->change(); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table($this->prefix('form_submissions'), function (Blueprint $table) { 19 | $table->dropUnique('form_submissions_id_unique'); 20 | $table->id()->change(); 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /database/migrations/2024_07_16_100000_create_sites_table.php: -------------------------------------------------------------------------------- 1 | prefix('sites'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('handle')->unique(); 14 | $table->string('name'); 15 | $table->string('url'); 16 | $table->string('locale'); 17 | $table->string('lang'); 18 | $table->jsonb('attributes'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists($this->prefix('sites')); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/entries/2024_03_07_100000_create_entries_table.php: -------------------------------------------------------------------------------- 1 | prefix('entries'), function (Blueprint $table) { 12 | $table->id(); 13 | $table->string('site')->index(); 14 | $table->unsignedBigInteger('origin_id')->nullable()->index(); 15 | $table->boolean('published')->default(true); 16 | $table->string('slug')->nullable(); 17 | $table->string('uri')->nullable()->index(); 18 | $table->string('date')->nullable()->index(); 19 | $table->integer('order')->nullable()->index(); 20 | $table->string('collection')->index(); 21 | $table->string('blueprint', 30)->nullable()->index(); 22 | $table->jsonb('data'); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | public function down() 28 | { 29 | Schema::dropIfExists($this->prefix('entries')); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/entries/2024_03_07_100000_create_entries_table_with_string_ids.php: -------------------------------------------------------------------------------- 1 | prefix('entries'), function (Blueprint $table) { 12 | $table->uuid('id'); 13 | $table->string('site')->index(); 14 | $table->uuid('origin_id')->nullable()->index(); 15 | $table->boolean('published')->default(true); 16 | $table->string('slug')->nullable(); 17 | $table->string('uri')->nullable()->index(); 18 | $table->string('date')->nullable()->index(); 19 | $table->integer('order')->nullable()->index(); 20 | $table->string('collection')->index(); 21 | $table->string('blueprint', 30)->nullable()->index(); 22 | $table->jsonb('data'); 23 | $table->timestamps(); 24 | 25 | $table->primary('id'); 26 | }); 27 | } 28 | 29 | public function down() 30 | { 31 | Schema::dropIfExists($this->prefix('entries')); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/updates/add_blueprint_to_entries_table.php.stub: -------------------------------------------------------------------------------- 1 | prefix('entries'), function (Blueprint $table) { 12 | $table->string('blueprint', 30)->nullable()->index()->after('collection'); 13 | }); 14 | 15 | Entry::all()->each->saveQuietly(); 16 | } 17 | 18 | public function down() 19 | { 20 | Schema::table($this->prefix('entries'), function (Blueprint $table) { 21 | $table->dropColumn('blueprint'); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/updates/add_id_to_attributes_in_revisions_table.php.stub: -------------------------------------------------------------------------------- 1 | whereJsonDoesntContainKey('attributes->id') 12 | ->chunkById(200, function ($revisions) { 13 | foreach ($revisions as $revision) { 14 | $id = str($revision->key) 15 | ->afterLast('/') 16 | ->toString(); 17 | 18 | $attributes = $revision->attributes; 19 | $attributes['id'] = $id; 20 | 21 | $revision->attributes = $attributes; 22 | $revision->save(); 23 | } 24 | }); 25 | } 26 | 27 | public function down() 28 | { 29 | // Reverse the above 30 | RevisionModel::query() 31 | ->whereJsonContainsKey('attributes->id') 32 | ->chunkById(200, function ($revisions) { 33 | foreach ($revisions as $revision) { 34 | $attributes = $revision->attributes; 35 | unset($attributes['id']); 36 | 37 | $revision->attributes = $attributes; 38 | $revision->save(); 39 | } 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /database/migrations/updates/add_index_to_date_on_entries_table.php.stub: -------------------------------------------------------------------------------- 1 | prefix('entries'), 'date')) { 11 | return; 12 | } 13 | 14 | Schema::table($this->prefix('entries'), function (Blueprint $table) { 15 | $table->index(['date']); 16 | }); 17 | } 18 | 19 | public function down() 20 | { 21 | if (! Schema::hasIndex($this->prefix('entries'), 'date')) { 22 | return; 23 | } 24 | 25 | Schema::table($this->prefix('entries'), function (Blueprint $table) { 26 | $table->dropIndex(['date']); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/updates/add_order_to_entries_table.php.stub: -------------------------------------------------------------------------------- 1 | prefix('entries'), function (Blueprint $table) { 11 | $table->integer('order')->after('collection')->nullable(); 12 | }); 13 | } 14 | 15 | public function down() 16 | { 17 | Schema::table($this->prefix('entries'), function (Blueprint $table) { 18 | $table->dropColumn('order'); 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /database/migrations/updates/drop_foreign_keys_on_entries.php.stub: -------------------------------------------------------------------------------- 1 | prefix('entries'), function (Blueprint $table) { 11 | $table->dropForeign(['origin_id']); 12 | $table->index('order'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table($this->prefix('entries'), function (Blueprint $table) { 19 | $table->foreign('origin_id') 20 | ->references('id') 21 | ->on($this->prefix('entries')) 22 | ->onDelete('set null'); 23 | $table->dropIndex(['order']); 24 | }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /database/migrations/updates/drop_foreign_keys_on_forms.php.stub: -------------------------------------------------------------------------------- 1 | prefix('form_submissions'), function (Blueprint $table) { 11 | $table->dropForeign(['form_id']); 12 | }); 13 | } 14 | 15 | public function down() 16 | { 17 | Schema::table($this->prefix('form_submissions'), function (Blueprint $table) { 18 | $table->foreign('form_id') 19 | ->references('id') 20 | ->on($this->prefix('forms')) 21 | ->onDelete('cascade'); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/updates/drop_status_on_entries.php.stub: -------------------------------------------------------------------------------- 1 | prefix('entries'), function (Blueprint $table) { 11 | $table->dropColumn('status'); 12 | }); 13 | } 14 | 15 | public function down() 16 | { 17 | Schema::table($this->prefix('entries'), function (Blueprint $table) { 18 | $table->string('status')->nullable(); 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /database/migrations/updates/relate_form_submissions_by_handle.php.stub: -------------------------------------------------------------------------------- 1 | prefix('form_submissions'), 'form')) { 13 | return; 14 | } 15 | 16 | Schema::table($this->prefix('form_submissions'), function (Blueprint $table) { 17 | $table->string('form', 30)->nullable()->index()->after('id'); 18 | }); 19 | 20 | $forms = FormModel::all()->pluck('handle', 'id'); 21 | 22 | SubmissionModel::all() 23 | ->each(function ($submission) use ($forms) { 24 | if ($form = $forms->get($submission->form_id)) { 25 | $submission->form = $form; 26 | $submission->save(); 27 | } 28 | }); 29 | 30 | Schema::table($this->prefix('form_submissions'), function (Blueprint $table) { 31 | collect(Schema::getForeignKeys($this->prefix('form_submissions')))->each(function ($fk) use ($table) { 32 | if (count($fk['columns']) == 1 && in_array('form_id', $fk['columns'])) { 33 | $table->dropForeign(['form_id']); 34 | } 35 | }); 36 | 37 | $table->dropUnique(['form_id', 'created_at']); 38 | $table->dropColumn('form_id'); 39 | }); 40 | } 41 | 42 | public function down() 43 | { 44 | Schema::table($this->prefix('form_submissions'), function (Blueprint $table) { 45 | $table->unsignedBigInteger('form_id')->index(); 46 | }); 47 | 48 | $forms = FormModel::all()->pluck('handle', 'id'); 49 | 50 | SubmissionModel::all() 51 | ->each(function ($submission) use ($forms) { 52 | if ($form = $forms->get($submission->form)) { 53 | $submission->form_id = $form; 54 | $submission->save(); 55 | } 56 | }); 57 | 58 | Schema::table($this->prefix('form_submissions'), function (Blueprint $table) { 59 | $table->dropColumn('form'); 60 | }); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /database/migrations/updates/update_assets_table.php.stub: -------------------------------------------------------------------------------- 1 | prefix('assets_meta'), function (Blueprint $table) { 13 | $table->string('container')->after('handle')->index(); 14 | $table->string('folder')->after('container')->index(); 15 | $table->string('basename')->after('folder')->index(); 16 | $table->string('filename')->after('basename')->index(); 17 | $table->char('extension', 10)->after('filename')->index(); 18 | $table->string('path')->after('extension')->index(); 19 | $table->jsonb('meta')->after('path')->nullable(); 20 | 21 | $table->index(['container', 'folder', 'basename']); 22 | }); 23 | 24 | AssetModel::all() 25 | ->each(function ($model) { 26 | $path = Str::of($model->handle)->after('::')->replace('.meta/', '')->beforeLast('.yaml'); 27 | 28 | if ($path->startsWith('./')) { 29 | $path = $path->replaceFirst('./', ''); 30 | } 31 | 32 | $model->container = Str::before($model->handle, '::'); 33 | $model->path = $path; 34 | $model->folder = $path->contains('/') ? $path->beforeLast('/') : '/'; 35 | $model->basename = $path->afterLast('/'); 36 | $model->extension = Str::of($model->basename)->afterLast('.'); 37 | $model->filename = Str::of($model->basename)->beforeLast('.'); 38 | $model->meta = $model->data; 39 | $model->save(); 40 | }); 41 | 42 | Schema::table($this->prefix('assets_meta'), function (Blueprint $table) { 43 | $table->dropIndex(['handle']); 44 | $table->dropColumn('handle'); 45 | }); 46 | } 47 | 48 | public function down() 49 | { 50 | Schema::table($this->prefix('assets_meta'), function (Blueprint $table) { 51 | $table->string('handle')->index(); 52 | }); 53 | 54 | AssetModel::all() 55 | ->each(function ($model) { 56 | $model->handle = $model->container.'::'.$model->folder.'/.meta/'.$model->basename.'.yaml'; 57 | $model->data = $model->meta; 58 | $model->saveQuietly(); 59 | }); 60 | 61 | Schema::table($this->prefix('assets_meta'), function (Blueprint $table) { 62 | $table->dropIndex(['container', 'folder', 'basename']); 63 | 64 | $table->dropColumn(['meta', 'path', 'basename', 'filename', 'extension', 'folder', 'container']); 65 | }); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /database/migrations/updates/update_globals_table.php.stub: -------------------------------------------------------------------------------- 1 | prefix('global_sets'), function (Blueprint $table) { 13 | $table->jsonb('settings')->after('title')->nullable(); 14 | }); 15 | 16 | GlobalSetModel::all() 17 | ->each(function ($model) { 18 | $localizations = json_decode($model->localizations); 19 | 20 | foreach ($localizations as $localization) { 21 | VariablesModel::create([ 22 | 'handle' => $model->handle, 23 | 'locale' => $localization->locale, 24 | 'data' => $localization->data, 25 | 'origin' => $localization->origin, 26 | ]); 27 | } 28 | }); 29 | 30 | Schema::table($this->prefix('global_sets'), function (Blueprint $table) { 31 | $table->dropColumn('localizations'); 32 | }); 33 | } 34 | 35 | public function down() 36 | { 37 | Schema::table($this->prefix('global_sets'), function (Blueprint $table) { 38 | $table->dropColumn('settings'); 39 | $table->jsonb('localizations')->after('title')->nullable(); 40 | }); 41 | 42 | GlobalSetModel::all() 43 | ->each(function ($model) { 44 | $model->localizations = VariablesModel::where('handle', $model->handle) 45 | ->get() 46 | ->mapWithKeys(function ($variable, $key) { 47 | return [ 48 | $variable->locale = [ 49 | 'locale' => $variable->locale, 50 | 'data' => $variable->data, 51 | 'origin' => $variable->origin, 52 | ], 53 | ]; 54 | }); 55 | 56 | $model->save(); 57 | }); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "binary_operator_spaces": { 5 | "default": "single_space", 6 | "operators": { 7 | "=>": null 8 | } 9 | }, 10 | "class_attributes_separation": { 11 | "elements": { 12 | "method": "one" 13 | } 14 | }, 15 | "class_definition": { 16 | "multi_line_extends_each_single_line": true, 17 | "single_item_single_line": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Assets/AssetContainerModel.php: -------------------------------------------------------------------------------- 1 | 'json', 16 | ]; 17 | 18 | public function getAttribute($key) 19 | { 20 | return Arr::get($this->getAttributeValue('settings'), $key, parent::getAttribute($key)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Assets/AssetContainerRepository.php: -------------------------------------------------------------------------------- 1 | map(function ($model) { 19 | return Blink::once("eloquent-assetcontainer-{$model->handle}", function () use ($model) { 20 | return app(AssetContainerContract::class)->fromModel($model); 21 | }); 22 | }); 23 | }); 24 | } 25 | 26 | public function findByHandle(string $handle): ?AssetContainerContract 27 | { 28 | return Blink::once("eloquent-assetcontainer-{$handle}", function () use ($handle) { 29 | $model = app('statamic.eloquent.assets.container_model')::whereHandle($handle)->first(); 30 | 31 | if (! $model) { 32 | return null; 33 | } 34 | 35 | return app(AssetContainerContract::class)->fromModel($model); 36 | }); 37 | } 38 | 39 | public function make(?string $handle = null): AssetContainerContract 40 | { 41 | return app(AssetContainerContract::class)->handle($handle); 42 | } 43 | 44 | public function save(AssetContainerContract $container) 45 | { 46 | $container->save(); 47 | 48 | Blink::forget("eloquent-assetcontainer-{$container->handle()}"); 49 | } 50 | 51 | public function delete($container) 52 | { 53 | $container->delete(); 54 | 55 | Blink::forget("eloquent-assetcontainer-{$container->handle()}"); 56 | Blink::forget('eloquent-assetcontainers'); 57 | } 58 | 59 | public static function bindings(): array 60 | { 61 | return [ 62 | AssetContainerContract::class => AssetContainer::class, 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Assets/AssetModel.php: -------------------------------------------------------------------------------- 1 | 'json', 15 | 'meta' => 'json', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Assets/AssetQueryBuilder.php: -------------------------------------------------------------------------------- 1 | '.$column; 23 | } elseif (! in_array($column, self::COLUMNS)) { 24 | $column = 'meta->data->'.$column; 25 | } 26 | 27 | return $column; 28 | } 29 | 30 | protected function transform($items, $columns = []) 31 | { 32 | return AssetCollection::make($items)->map(function ($model) use ($columns) { 33 | return app('statamic.eloquent.assets.asset')::fromModel($model) 34 | ->selectedQueryColumns($this->selectedQueryColumns ?? $columns); 35 | }); 36 | } 37 | 38 | public function with($relations, $callback = null) 39 | { 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Assets/AssetRepository.php: -------------------------------------------------------------------------------- 1 | query() 24 | ->where('container', $container) 25 | ->where('folder', $folder) 26 | ->where('basename', $filename) 27 | ->first(); 28 | }); 29 | 30 | if (! $item) { 31 | Blink::forget($blinkKey); 32 | 33 | return null; 34 | } 35 | 36 | return $item; 37 | } 38 | 39 | public function findByUrl(string $url) 40 | { 41 | if (! $container = $this->resolveContainerFromUrl($url)) { 42 | return null; 43 | } 44 | 45 | $siteUrl = rtrim(Site::current()->absoluteUrl(), '/'); 46 | $containerUrl = $container->url(); 47 | 48 | if (Str::startsWith($containerUrl, '/')) { 49 | $containerUrl = $siteUrl.$containerUrl; 50 | } 51 | 52 | if (Str::startsWith($containerUrl, $siteUrl)) { 53 | $url = $siteUrl.$url; 54 | } 55 | 56 | $path = Str::after($url, $containerUrl); 57 | 58 | if (Str::startsWith($path, '/')) { 59 | $path = substr($path, 1); 60 | } 61 | 62 | return $this->findById("{$container}::{$path}"); 63 | } 64 | 65 | public function delete($asset) 66 | { 67 | $this->query() 68 | ->where([ 69 | 'container' => $asset->container(), 70 | 'folder' => $asset->folder(), 71 | 'basename' => $asset->basename(), 72 | ]) 73 | ->delete(); 74 | } 75 | 76 | public function save($asset) 77 | { 78 | $asset->writeMeta($asset->generateMeta()); 79 | } 80 | 81 | public static function bindings(): array 82 | { 83 | return [ 84 | AssetContract::class => app('statamic.eloquent.assets.asset'), 85 | QueryBuilderContract::class => AssetQueryBuilder::class, 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Collections/CollectionModel.php: -------------------------------------------------------------------------------- 1 | 'json', 15 | 'settings.routes' => 'array', 16 | 'settings.inject' => 'array', 17 | 'settings.taxonomies' => 'array', 18 | 'settings.structure' => 'array', 19 | 'settings.sites' => 'array', 20 | 'settings.revisions' => 'boolean', 21 | 'settings.dated' => 'boolean', 22 | 'settings.default_publish_state' => 'boolean', 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /src/Collections/CollectionRepository.php: -------------------------------------------------------------------------------- 1 | transform(app('statamic.eloquent.collections.model')::all()); 16 | }); 17 | } 18 | 19 | public function find($handle): ?CollectionContract 20 | { 21 | return Blink::once("eloquent-collection-{$handle}", function () use ($handle) { 22 | $model = app('statamic.eloquent.collections.model')::whereHandle($handle)->first(); 23 | 24 | return $model ? app(CollectionContract::class)->fromModel($model) : null; 25 | }); 26 | } 27 | 28 | public function findByHandle($handle): ?CollectionContract 29 | { 30 | return $this->find($handle); 31 | } 32 | 33 | public function save($entry) 34 | { 35 | $model = $entry->toModel(); 36 | $model->save(); 37 | 38 | Blink::forget("eloquent-collection-{$model->handle}"); 39 | Blink::forget('eloquent-collections'); 40 | 41 | $entry->model($model->fresh()); 42 | } 43 | 44 | public function delete($entry) 45 | { 46 | $model = $entry->model(); 47 | $model->delete(); 48 | 49 | Blink::forget("eloquent-collection-{$model->handle}"); 50 | Blink::forget('eloquent-collections'); 51 | } 52 | 53 | protected function transform($items, $columns = []) 54 | { 55 | return IlluminateCollection::make($items)->map(function ($model) { 56 | return Blink::once("eloquent-collection-{$model->handle}", function () use ($model) { 57 | return app(CollectionContract::class)::fromModel($model); 58 | }); 59 | }); 60 | } 61 | 62 | public static function bindings(): array 63 | { 64 | return [ 65 | CollectionContract::class => Collection::class, 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Commands/ExportGlobals.php: -------------------------------------------------------------------------------- 1 | usingDefaultRepositories(function () { 48 | $this->exportGlobals(); 49 | }); 50 | 51 | return 0; 52 | } 53 | 54 | private function usingDefaultRepositories(Closure $callback) 55 | { 56 | Facade::clearResolvedInstance(GlobalRepositoryContract::class); 57 | Facade::clearResolvedInstance(GlobalVariablesRepositoryContract::class); 58 | 59 | Statamic::repository(GlobalRepositoryContract::class, GlobalRepository::class); 60 | Statamic::repository(GlobalVariablesRepositoryContract::class, GlobalVariablesRepository::class); 61 | 62 | app()->bind(GlobalSetContract::class, GlobalSet::class); 63 | app()->bind(VariablesContract::class, Variables::class); 64 | 65 | $callback(); 66 | } 67 | 68 | private function exportGlobals() 69 | { 70 | $sets = GlobalSetModel::all(); 71 | $variables = VariablesModel::all(); 72 | 73 | $this->withProgressBar($sets, function ($model) use ($variables) { 74 | $global = GlobalSetFacade::make() 75 | ->handle($model->handle) 76 | ->title($model->title); 77 | 78 | foreach ($variables->where('handle', $model->handle) as $localization) { 79 | $global->makeLocalization($localization->locale) 80 | ->data($localization->data) 81 | ->origin($localization->origin ?? null); 82 | } 83 | 84 | $global->save(); 85 | }); 86 | 87 | $this->newLine(); 88 | $this->info('Globals exported'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Commands/ExportNavs.php: -------------------------------------------------------------------------------- 1 | usingDefaultRepositories(function () { 48 | $this->exportNavs(); 49 | $this->exportNavTrees(); 50 | }); 51 | 52 | return 0; 53 | } 54 | 55 | private function usingDefaultRepositories(Closure $callback) 56 | { 57 | Facade::clearResolvedInstance(NavigationRepositoryContract::class); 58 | Facade::clearResolvedInstance(NavTreeRepositoryContract::class); 59 | 60 | Statamic::repository(NavigationRepositoryContract::class, NavigationRepository::class); 61 | Statamic::repository(NavTreeRepositoryContract::class, NavTreeRepository::class); 62 | 63 | app()->bind(NavContract::class, Nav::class); 64 | app()->bind(TreeContract::class, Tree::class); 65 | 66 | $callback(); 67 | } 68 | 69 | private function exportNavs() 70 | { 71 | if (! $this->option('force') && ! $this->confirm('Do you want to export navs?')) { 72 | return; 73 | } 74 | 75 | $navs = NavModel::all(); 76 | 77 | $this->withProgressBar($navs, function ($model) { 78 | $nav = NavFacade::make() 79 | ->handle($model->handle) 80 | ->title($model->title) 81 | ->collections($model->settings['collections'] ?? null) 82 | ->maxDepth($model->settings['max_depth'] ?? null) 83 | ->expectsRoot($model->settings['expects_root'] ?? false) 84 | ->save(); 85 | }); 86 | 87 | $this->newLine(); 88 | $this->info('Navs exported'); 89 | } 90 | 91 | private function exportNavTrees() 92 | { 93 | if (! $this->option('force') && ! $this->confirm('Do you want to export nav trees?')) { 94 | return; 95 | } 96 | 97 | $navs = NavFacade::all(); 98 | 99 | $this->withProgressBar($navs, function ($nav) { 100 | TreeModel::where('handle', $nav->handle()) 101 | ->where('type', 'navigation') 102 | ->get() 103 | ->each(function ($treeModel) use ($nav) { 104 | $nav->newTreeInstance() 105 | ->tree($treeModel->tree) 106 | ->handle($treeModel->handle) 107 | ->locale($treeModel->locale) 108 | ->syncOriginal() 109 | ->save(); 110 | }); 111 | }); 112 | 113 | $this->newLine(); 114 | $this->info('Nav trees exported'); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Commands/ExportRevisions.php: -------------------------------------------------------------------------------- 1 | exportRevisions(); 38 | } 39 | 40 | return 0; 41 | } 42 | 43 | private function exportRevisions() 44 | { 45 | $files = RevisionModel::all(); 46 | 47 | $this->withProgressBar($files, function ($model) { 48 | $class = $model->action != 'working' ? Revision::class : WorkingCopy::class; 49 | 50 | $revision = (new $class) 51 | ->key($model->key) 52 | ->action($model->action ?? false) 53 | ->id($model->created_at->timestamp) 54 | ->date($model->created_at) 55 | ->user($model->user ?? false) 56 | ->message($model->message ?? '') 57 | ->attributes($model->attributes ?? []) 58 | ->save(); 59 | }); 60 | 61 | $this->newLine(); 62 | $this->info('Revisions exported'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Commands/ExportSites.php: -------------------------------------------------------------------------------- 1 | mapWithKeys(function ($model) { 37 | return [ 38 | $model->handle => collect([ 39 | 'name' => $model->name, 40 | 'lang' => $model->lang ?? null, 41 | 'locale' => $model->locale, 42 | 'url' => $model->url, 43 | 'attributes' => $model->attributes ?? [], 44 | ])->filter()->all(), 45 | ]; 46 | }); 47 | 48 | (new Sites)->setSites($sites)->save(); 49 | 50 | $this->newLine(); 51 | $this->info('Sites exported'); 52 | 53 | return 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Commands/ImportAssets.php: -------------------------------------------------------------------------------- 1 | useDefaultRepositories(); 47 | 48 | $this->importAssetContainers(); 49 | $this->importAssets(); 50 | 51 | return 0; 52 | } 53 | 54 | private function useDefaultRepositories(): void 55 | { 56 | Facade::clearResolvedInstance(AssetContainerRepositoryContract::class); 57 | Facade::clearResolvedInstance(AssetRepositoryContract::class); 58 | 59 | Statamic::repository(AssetContainerRepositoryContract::class, AssetContainerRepository::class); 60 | Statamic::repository(AssetRepositoryContract::class, AssetRepository::class); 61 | 62 | app()->bind(AssetContainerContract::class, AssetContainer::class); 63 | app()->bind(AssetContainerContents::class, fn ($app) => new AssetContainerContents); 64 | } 65 | 66 | private function importAssetContainers(): void 67 | { 68 | if (! $this->shouldImportAssetContainers()) { 69 | return; 70 | } 71 | 72 | $this->withProgressBar(AssetContainerFacade::all(), function ($container) { 73 | AssetContainer::makeModelFromContract($container)?->save(); 74 | }); 75 | 76 | $this->components->info('Assets containers imported sucessfully'); 77 | } 78 | 79 | private function importAssets(): void 80 | { 81 | if (! $this->shouldImportAssets()) { 82 | return; 83 | } 84 | 85 | $this->withProgressBar(AssetFacade::all(), function ($asset) { 86 | EloquentAsset::makeModelFromContract($asset)?->save(); 87 | }); 88 | 89 | $this->components->info('Assets imported sucessfully'); 90 | } 91 | 92 | private function shouldImportAssetContainers(): bool 93 | { 94 | return $this->option('only-asset-containers') 95 | || ! $this->option('only-assets') 96 | && ($this->option('force') || $this->confirm('Do you want to import asset containers?')); 97 | } 98 | 99 | private function shouldImportAssets(): bool 100 | { 101 | return $this->option('only-assets') 102 | || ! $this->option('only-asset-containers') 103 | && ($this->option('force') || $this->confirm('Do you want to import assets?')); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Commands/ImportEntries.php: -------------------------------------------------------------------------------- 1 | useDefaultRepositories(); 41 | 42 | $this->importEntries(); 43 | 44 | return 0; 45 | } 46 | 47 | private function useDefaultRepositories(): void 48 | { 49 | Facade::clearResolvedInstance(EntryRepositoryContract::class); 50 | Facade::clearResolvedInstance(CollectionRepositoryContract::class); 51 | 52 | Statamic::repository(EntryRepositoryContract::class, EntryRepository::class); 53 | Statamic::repository(CollectionRepositoryContract::class, CollectionRepository::class); 54 | 55 | app()->bind(EntryContract::class, app('statamic.eloquent.entries.entry')); 56 | } 57 | 58 | private function importEntries(): void 59 | { 60 | $entries = Entry::all(); 61 | 62 | $entriesWithOrigin = $entries->filter->hasOrigin(); 63 | $entriesWithoutOrigin = $entries->filter(function ($entry) { 64 | return ! $entry->hasOrigin(); 65 | }); 66 | 67 | if ($entriesWithOrigin->count() > 0) { 68 | $this->components->info('Importing origin entries...'); 69 | } 70 | 71 | $this->withProgressBar($entriesWithoutOrigin, function ($entry) { 72 | $lastModified = $entry->fileLastModified(); 73 | 74 | $entry = EloquentEntry::makeModelFromContract($entry) 75 | ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) 76 | ->save(); 77 | }); 78 | 79 | if ($entriesWithOrigin->count() > 0) { 80 | $this->components->info('Importing localized entries...'); 81 | 82 | $this->withProgressBar($entriesWithOrigin, function ($entry) { 83 | $lastModified = $entry->fileLastModified(); 84 | 85 | EloquentEntry::makeModelFromContract($entry) 86 | ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) 87 | ->save(); 88 | }); 89 | } 90 | 91 | $this->components->info('Entries imported successfully.'); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Commands/ImportGlobals.php: -------------------------------------------------------------------------------- 1 | useDefaultRepositories(); 45 | 46 | $this->importGlobals(); 47 | 48 | return 0; 49 | } 50 | 51 | private function useDefaultRepositories(): void 52 | { 53 | Facade::clearResolvedInstance(GlobalRepositoryContract::class); 54 | 55 | Statamic::repository(GlobalRepositoryContract::class, GlobalRepository::class); 56 | Statamic::repository(GlobalVariablesRepositoryContract::class, GlobalVariablesRepository::class); 57 | 58 | app()->bind(GlobalSetContract::class, GlobalSet::class); 59 | } 60 | 61 | private function importGlobals(): void 62 | { 63 | $importGlobals = $this->shouldImportGlobalSets(); 64 | $importVariables = $this->shouldImportGlobalVariables(); 65 | 66 | $this->withProgressBar(GlobalSetFacade::all(), function ($set) use ($importGlobals, $importVariables) { 67 | if ($importGlobals) { 68 | $lastModified = $set->fileLastModified(); 69 | 70 | GlobalSet::makeModelFromContract($set) 71 | ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) 72 | ->save(); 73 | } 74 | 75 | if ($importVariables) { 76 | $set->localizations()->each(function ($locale) { 77 | Variables::makeModelFromContract($locale)->save(); 78 | }); 79 | } 80 | }); 81 | 82 | $this->components->info('Globals imported successfully.'); 83 | } 84 | 85 | private function shouldImportGlobalSets(): bool 86 | { 87 | return $this->option('only-global-sets') 88 | || ! $this->option('only-global-variables') 89 | && ($this->option('force') || $this->confirm('Do you want to import global sets?')); 90 | } 91 | 92 | private function shouldImportGlobalVariables(): bool 93 | { 94 | return $this->option('only-global-variables') 95 | || ! $this->option('only-global-sets') 96 | && ($this->option('force') || $this->confirm('Do you want to import global variables?')); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Commands/ImportNavs.php: -------------------------------------------------------------------------------- 1 | useDefaultRepositories(); 45 | 46 | $this->importNavs(); 47 | 48 | return 0; 49 | } 50 | 51 | private function useDefaultRepositories(): void 52 | { 53 | Facade::clearResolvedInstance(NavigationRepositoryContract::class); 54 | Facade::clearResolvedInstance(NavTreeRepositoryContract::class); 55 | 56 | Statamic::repository(NavigationRepositoryContract::class, NavigationRepository::class); 57 | Statamic::repository(NavTreeRepositoryContract::class, NavTreeRepository::class); 58 | 59 | app()->bind(NavContract::class, EloquentNav::class); 60 | } 61 | 62 | private function importNavs(): void 63 | { 64 | $importNavs = $this->shouldImportNavigations(); 65 | $importTrees = $this->shouldImportNavigationTrees(); 66 | 67 | $this->withProgressBar(NavFacade::all(), function ($nav) use ($importNavs, $importTrees) { 68 | if ($importNavs) { 69 | $lastModified = $nav->fileLastModified(); 70 | 71 | EloquentNav::makeModelFromContract($nav) 72 | ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) 73 | ->save(); 74 | } 75 | 76 | if ($importTrees) { 77 | $nav->trees()->each(function ($tree) { 78 | $lastModified = $tree->fileLastModified(); 79 | 80 | EloquentNavTree::makeModelFromContract($tree) 81 | ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) 82 | ->save(); 83 | }); 84 | } 85 | }); 86 | 87 | $this->components->info('Navs imported successfully.'); 88 | } 89 | 90 | private function shouldImportNavigations(): bool 91 | { 92 | return $this->option('only-navs') 93 | || ! $this->option('only-nav-trees') 94 | && ($this->option('force') || $this->confirm('Do you want to import navs?')); 95 | } 96 | 97 | private function shouldImportNavigationTrees(): bool 98 | { 99 | return $this->option('only-nav-trees') 100 | || ! $this->option('only-navs') 101 | && ($this->option('force') || $this->confirm('Do you want to import nav trees?')); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Commands/ImportRevisions.php: -------------------------------------------------------------------------------- 1 | components->error('This import can only be run when revisions are enabled.'); 37 | 38 | return 1; 39 | } 40 | 41 | $this->importRevisions(); 42 | 43 | return 0; 44 | } 45 | 46 | private function importRevisions(): void 47 | { 48 | $this->withProgressBar(File::allFiles(config('statamic.revisions.path')), function ($file) { 49 | $yaml = YAML::file($file->getPathname())->parse(); 50 | 51 | $revision = (new Revision) 52 | ->key($file->getRelativePath()) 53 | ->action($yaml['action'] ?? false) 54 | ->date(Carbon::parse($yaml['date'])) 55 | ->user($yaml['user'] ?? false) 56 | ->message($yaml['message'] ?? '') 57 | ->attributes($yaml['attributes'] ?? []); 58 | 59 | if ($file->getBasename('.yaml') === 'working') { 60 | $revision->action('working'); 61 | } 62 | 63 | $revision->toModel()->save(); 64 | }); 65 | 66 | $this->components->info('Revisions imported successfully.'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Commands/ImportSites.php: -------------------------------------------------------------------------------- 1 | config(); 34 | 35 | (new EloquentSites)->setSites($sites)->save(); 36 | 37 | $this->components->info('Sites imported successfully.'); 38 | 39 | return 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Commands/SyncAssets.php: -------------------------------------------------------------------------------- 1 | reject(fn ($container) => $this->option('container') != 'all' && $this->option('container') != $container->handle()) 37 | ->each(fn ($container) => $this->processContainer($container)); 38 | 39 | $this->info('Complete'); 40 | } 41 | 42 | private function processContainer(AssetContainer $container) 43 | { 44 | $this->info("Container: {$container->handle()}"); 45 | 46 | $this->processFolder($container); 47 | } 48 | 49 | private function processFolder(AssetContainer $container, $folder = '/') 50 | { 51 | $this->line("Processing folder: {$folder}"); 52 | 53 | // get raw listing of this folder, avoiding any of statamic's asset container caching 54 | $contents = collect($container->disk()->filesystem()->listContents($folder)); 55 | 56 | $files = $contents 57 | ->reject(fn ($item) => $item['type'] != 'file') 58 | ->pluck('path'); 59 | 60 | // ensure we have an asset for any paths 61 | $files->each(function ($file) use ($container) { 62 | if (Str::of($file)->afterLast('/')->startsWith('.')) { 63 | return; 64 | } 65 | 66 | $this->info($file); 67 | 68 | if (! Facades\Asset::find($container->handle().'::'.$file)) { 69 | Facades\Asset::make() 70 | ->container($container->handle()) 71 | ->path($file) 72 | ->saveQuietly(); 73 | } 74 | }); 75 | 76 | // delete any assets we have a db entry for that no longer exist 77 | AssetModel::query() 78 | ->where('container', $container->handle()) 79 | ->where('folder', $folder) 80 | ->chunk(100, function ($assets) use ($files) { 81 | $assets->each(function ($asset) use ($files) { 82 | if (! $files->contains($asset->path)) { 83 | $this->error("Deleting {$asset->path}"); 84 | 85 | $asset->delete(); 86 | } 87 | }); 88 | }); 89 | 90 | // process any sub-folders of this folder 91 | $contents 92 | ->reject(fn ($item) => $item['type'] != 'dir') 93 | ->pluck('path') 94 | ->each(function ($subfolder) use ($container, $folder) { 95 | if (str_contains($subfolder.'/', '.meta/')) { 96 | return; 97 | } 98 | 99 | $subfolder = Str::ensureLeft($subfolder, '/'); 100 | 101 | if ($folder != $subfolder && (strlen($subfolder) > strlen($folder))) { 102 | $this->processFolder($container, $subfolder); 103 | } 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Database/BaseMigration.php: -------------------------------------------------------------------------------- 1 | setTable(config('statamic.eloquent-driver.table_prefix', '').$this->getTable()); 14 | 15 | if ($connection = config('statamic.eloquent-driver.connection', false)) { 16 | $this->setConnection($connection); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Entries/EntryModel.php: -------------------------------------------------------------------------------- 1 | 'datetime', 16 | 'data' => 'json', 17 | 'published' => 'boolean', 18 | ]; 19 | 20 | public function author() 21 | { 22 | return $this->belongsTo(\App\Models\User::class, 'data->author'); 23 | } 24 | 25 | public function origin() 26 | { 27 | return $this->belongsTo(static::class); 28 | } 29 | 30 | public function parent() 31 | { 32 | return $this->belongsTo(static::class, 'data->parent'); 33 | } 34 | 35 | public function getAttribute($key) 36 | { 37 | // Because the import script was importing `updated_at` into the 38 | // json data column, we will explicitly reference other SQL 39 | // columns first to prevent errors with that bad data. 40 | if (in_array($key, EntryQueryBuilder::COLUMNS)) { 41 | return parent::getAttribute($key); 42 | } 43 | 44 | return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Entries/UuidEntryModel.php: -------------------------------------------------------------------------------- 1 | {$entry->getKeyName()})) { 19 | $entry->{$entry->getKeyName()} = (string) Str::uuid(); 20 | } 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Fields/BlueprintModel.php: -------------------------------------------------------------------------------- 1 | 'json', 16 | ]; 17 | 18 | public function getAttribute($key) 19 | { 20 | return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Fields/FieldsetModel.php: -------------------------------------------------------------------------------- 1 | 'json', 16 | ]; 17 | 18 | public function getAttribute($key) 19 | { 20 | return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Fields/FieldsetRepository.php: -------------------------------------------------------------------------------- 1 | map(function ($model) { 21 | return Blink::once("eloquent-fieldset-{$model->handle}", function () use ($model) { 22 | $handle = $model->handle; 23 | 24 | if (Str::startsWith($handle, 'vendor.')) { 25 | $handle = Str::of($handle)->after('vendor.')->replaceFirst('.', '::'); 26 | } 27 | 28 | return (new Fieldset) 29 | ->setHandle($handle) 30 | ->setContents($model->data); 31 | }); 32 | }); 33 | }) 34 | ->merge($this->getNamespacedFieldsets()); 35 | } 36 | 37 | public function find($handle): ?Fieldset 38 | { 39 | $handle = str_replace('/', '.', $handle); 40 | 41 | if (Str::startsWith($handle, 'vendor.')) { 42 | $handle = Str::of($handle)->after('vendor.')->replaceFirst('.', '::'); 43 | } 44 | 45 | return $this->all()->filter(function ($fieldset) use ($handle) { 46 | return $fieldset->handle() == $handle; 47 | })->first(); 48 | } 49 | 50 | public function save(Fieldset $fieldset) 51 | { 52 | $this->updateModel($fieldset); 53 | } 54 | 55 | public function delete(Fieldset $fieldset) 56 | { 57 | $this->deleteModel($fieldset); 58 | } 59 | 60 | public function updateModel($fieldset) 61 | { 62 | $model = app('statamic.eloquent.fieldsets.model')::firstOrNew([ 63 | 'handle' => $fieldset->handle(), 64 | ]); 65 | 66 | $model->data = $fieldset->contents(); 67 | $model->save(); 68 | 69 | Blink::forget("eloquent-fieldset-{$model->handle}"); 70 | } 71 | 72 | public function deleteModel($fieldset) 73 | { 74 | $model = app('statamic.eloquent.fieldsets.model')::where('handle', $fieldset->handle())->first(); 75 | 76 | if ($model) { 77 | $model->delete(); 78 | } 79 | 80 | Blink::forget("eloquent-fieldset-{$model->handle}"); 81 | Blink::forget('eloquent-fieldsets'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Forms/Form.php: -------------------------------------------------------------------------------- 1 | title($model->title) 20 | ->handle($model->handle) 21 | ->store($model->settings['store'] ?? null) 22 | ->email($model->settings['email'] ?? null) 23 | ->honeypot($model->settings['honeypot'] ?? null) 24 | ->data($model->settings['data'] ?? []) 25 | ->model($model); 26 | } 27 | 28 | public function toModel() 29 | { 30 | return self::makeModelFromContract($this); 31 | } 32 | 33 | public static function makeModelFromContract(Contract $source) 34 | { 35 | $class = app('statamic.eloquent.forms.model'); 36 | 37 | return $class::firstOrNew(['handle' => $source->handle()])->fill([ 38 | 'title' => $source->title(), 39 | 'settings' => [ 40 | 'store' => $source->store(), 41 | 'email' => $source->email(), 42 | 'honeypot' => $source->honeypot(), 43 | 'data' => $source->data()->filter(fn ($v) => $v !== null), 44 | ], 45 | ]); 46 | } 47 | 48 | public function model($model = null) 49 | { 50 | if (func_num_args() === 0) { 51 | return $this->model; 52 | } 53 | 54 | $this->model = $model; 55 | 56 | return $this; 57 | } 58 | 59 | public function save() 60 | { 61 | $model = $this->toModel(); 62 | $model->save(); 63 | 64 | $this->model($model->fresh()); 65 | 66 | Blink::forget("eloquent-forms-{$this->handle()}"); 67 | Blink::forget('eloquent-forms'); 68 | 69 | FormSaved::dispatch($this); 70 | } 71 | 72 | public function delete() 73 | { 74 | $this->submissions()->each->delete(); 75 | $this->model()->delete(); 76 | 77 | Blink::forget("eloquent-forms-{$this->handle()}"); 78 | Blink::forget('eloquent-forms'); 79 | 80 | FormDeleted::dispatch($this); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Forms/FormModel.php: -------------------------------------------------------------------------------- 1 | 'json', 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /src/Forms/FormRepository.php: -------------------------------------------------------------------------------- 1 | first(); 15 | 16 | if (! $model) { 17 | return; 18 | } 19 | 20 | return app(FormContract::class)->fromModel($model); 21 | }); 22 | } 23 | 24 | public function all() 25 | { 26 | return Blink::once('eloquent-forms', function () { 27 | return app('statamic.eloquent.forms.model')::all() 28 | ->map(function ($form) { 29 | return app(FormContract::class)::fromModel($form); 30 | }); 31 | }); 32 | } 33 | 34 | public function make($handle = null) 35 | { 36 | $form = app(FormContract::class); 37 | 38 | if ($handle) { 39 | $form->handle($handle); 40 | } 41 | 42 | return $form; 43 | } 44 | 45 | public static function bindings(): array 46 | { 47 | return [ 48 | FormContract::class => Form::class, 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Forms/Submission.php: -------------------------------------------------------------------------------- 1 | id($model->id) 25 | ->date($model->created_at ?? Carbon::now()) 26 | ->data(Arr::except($model->data, 'date')) 27 | ->model($model); 28 | } 29 | 30 | public function toModel() 31 | { 32 | $class = app('statamic.eloquent.form_submissions.model'); 33 | $timestamp = (new $class)->fromDateTime($this->date()); 34 | 35 | $model = $class::findOrNew($this->id()); 36 | 37 | return (! empty($model->id)) ? $model->fill([ 38 | 'data' => $this->data->filter(fn ($v) => $v !== null), 39 | ]) : $model->fill([ 40 | 'id' => $this->id(), 41 | 'data' => $this->data, 42 | 'form' => $this->form->handle(), 43 | 'created_at' => $timestamp, 44 | ]); 45 | } 46 | 47 | public function model($model = null) 48 | { 49 | if (func_num_args() === 0) { 50 | return $this->model; 51 | } 52 | 53 | $this->model = $model; 54 | 55 | return $this; 56 | } 57 | 58 | public function date($date = null) 59 | { 60 | if (! is_null($date)) { 61 | if (is_string($date)) { 62 | $date = Carbon::parse($date); 63 | } 64 | 65 | $this->date = $date; 66 | 67 | return $this; 68 | } 69 | 70 | return $this->date ?? ($this->date = Carbon::now()); 71 | } 72 | 73 | public function save() 74 | { 75 | if (SubmissionSaving::dispatch($this) === false) { 76 | return false; 77 | } 78 | 79 | $withEvents = $this->withEvents; 80 | $this->withEvents = true; 81 | 82 | $afterSaveCallbacks = $this->afterSaveCallbacks; 83 | $this->afterSaveCallbacks = []; 84 | 85 | $model = $this->toModel(); 86 | $model->save(); 87 | 88 | if ($isNew = $model->wasRecentlyCreated) { 89 | $this->id = $model->getKey(); 90 | } 91 | 92 | $this->model($model->fresh()); 93 | 94 | foreach ($afterSaveCallbacks as $callback) { 95 | $callback($this); 96 | } 97 | 98 | if ($withEvents) { 99 | if ($isNew) { 100 | SubmissionCreated::dispatch($this); 101 | } 102 | 103 | SubmissionSaved::dispatch($this); 104 | } 105 | } 106 | 107 | public function delete() 108 | { 109 | if (! $this->model) { 110 | $class = app('statamic.eloquent.form_submissions.model'); 111 | $this->model = $class::findOrNew($this->id); 112 | } 113 | 114 | $withEvents = $this->withEvents; 115 | $this->withEvents = true; 116 | 117 | $this->model->delete(); 118 | 119 | if ($withEvents) { 120 | SubmissionDeleted::dispatch($this); 121 | } 122 | 123 | return true; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Forms/SubmissionModel.php: -------------------------------------------------------------------------------- 1 | 'json', 17 | ]; 18 | 19 | protected $dateFormat = 'Y-m-d H:i:s.u'; 20 | } 21 | -------------------------------------------------------------------------------- /src/Forms/SubmissionQueryBuilder.php: -------------------------------------------------------------------------------- 1 | ')) { 28 | $column = 'data->'.$column; 29 | } 30 | } 31 | 32 | return $column; 33 | } 34 | 35 | protected function transform($items, $columns = []) 36 | { 37 | return $items->map(function ($model) { 38 | return Submission::fromModel($model) 39 | ->form(Form::find($model->form)); 40 | }); 41 | } 42 | 43 | public function with($relations, $callback = null) 44 | { 45 | return $this; 46 | } 47 | 48 | public function first() 49 | { 50 | return $this->get()->first(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Forms/SubmissionRepository.php: -------------------------------------------------------------------------------- 1 | toModel(); 14 | $model->save(); 15 | 16 | $submission->model($submission->fresh()); 17 | } 18 | 19 | public function delete($submission) 20 | { 21 | $submission->model()->delete(); 22 | } 23 | 24 | public static function bindings(): array 25 | { 26 | return [ 27 | SubmissionContract::class => Submission::class, 28 | SubmissionQueryBuilderContract::class => SubmissionQueryBuilder::class, 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Globals/GlobalRepository.php: -------------------------------------------------------------------------------- 1 | map(function ($model) { 15 | return Blink::once("eloquent-globalsets-{$model->handle}", function () use ($model) { 16 | return app(GlobalSetContract::class)::fromModel($model); 17 | }); 18 | }); 19 | } 20 | 21 | public function find($handle): ?GlobalSetContract 22 | { 23 | return Blink::once("eloquent-globalsets-{$handle}", function () use ($handle) { 24 | $model = app('statamic.eloquent.global_sets.model')::whereHandle($handle)->first(); 25 | if (! $model) { 26 | return; 27 | } 28 | 29 | return app(GlobalSetContract::class)->fromModel($model); 30 | }); 31 | } 32 | 33 | public function findByHandle($handle): ?GlobalSetContract 34 | { 35 | return $this->find($handle); 36 | } 37 | 38 | public function all(): GlobalCollection 39 | { 40 | return Blink::once('eloquent-globalsets', function () { 41 | return $this->transform(app('statamic.eloquent.global_sets.model')::all()); 42 | }); 43 | } 44 | 45 | public function save($entry) 46 | { 47 | $model = $entry->toModel(); 48 | $model->save(); 49 | 50 | $entry->localizations()->each(function ($locale) { 51 | $locale->save(); 52 | }); 53 | 54 | $entry->model($model->fresh()); 55 | 56 | Blink::forget("eloquent-globalsets-{$model->handle}"); 57 | } 58 | 59 | public function delete($entry) 60 | { 61 | $entry->model()->delete(); 62 | 63 | Blink::forget("eloquent-globalsets-{$entry->handle()}"); 64 | Blink::forget('eloquent-globalsets'); 65 | } 66 | 67 | public static function bindings(): array 68 | { 69 | return [ 70 | GlobalSetContract::class => GlobalSet::class, 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Globals/GlobalSet.php: -------------------------------------------------------------------------------- 1 | handle($model->handle) 17 | ->title($model->title) 18 | ->model($model); 19 | 20 | return $global; 21 | } 22 | 23 | public function toModel() 24 | { 25 | return self::makeModelFromContract($this); 26 | } 27 | 28 | public static function makeModelFromContract(Contract $source) 29 | { 30 | $class = app('statamic.eloquent.global_sets.model'); 31 | 32 | return $class::firstOrNew(['handle' => $source->handle()])->fill([ 33 | 'title' => $source->title(), 34 | 'settings' => [], // future proofing 35 | ]); 36 | } 37 | 38 | public function model($model = null) 39 | { 40 | if (func_num_args() === 0) { 41 | return $this->model; 42 | } 43 | 44 | $this->model = $model; 45 | 46 | if (! is_null($model)) { 47 | $this->id($model->id); 48 | } 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Globals/GlobalSetModel.php: -------------------------------------------------------------------------------- 1 | 'json', 16 | ]; 17 | 18 | public function getAttribute($key) 19 | { 20 | return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Globals/GlobalVariablesRepository.php: -------------------------------------------------------------------------------- 1 | each(function ($model) { 17 | return app(Variables::class)::fromModel($model); 18 | }) 19 | ); 20 | } 21 | 22 | public function find($id): ?Variables 23 | { 24 | $id = Str::split($id, '::'); 25 | 26 | $model = app('statamic.eloquent.global_set_variables.model')::query() 27 | ->where('handle', $id[0]) 28 | ->when(count($id) > 1, function ($query) use ($id) { 29 | $query->where('locale', $id[1]); 30 | }) 31 | ->first(); 32 | 33 | if (! $model) { 34 | return null; 35 | } 36 | 37 | return app(Variables::class)::fromModel($model); 38 | } 39 | 40 | public function whereSet($handle): VariablesCollection 41 | { 42 | return VariablesCollection::make( 43 | app('statamic.eloquent.global_set_variables.model')::query() 44 | ->where('handle', $handle) 45 | ->get() 46 | ->map(function ($model) { 47 | return app(Variables::class)::fromModel($model); 48 | }) 49 | ); 50 | } 51 | 52 | public function save($variable) 53 | { 54 | $model = $variable->toModel(); 55 | $model->save(); 56 | 57 | $variable->model($model->fresh()); 58 | } 59 | 60 | public function delete($variable) 61 | { 62 | $variable->model()->delete(); 63 | } 64 | 65 | public static function bindings(): array 66 | { 67 | return [ 68 | Variables::class => \Statamic\Eloquent\Globals\Variables::class, 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Globals/Variables.php: -------------------------------------------------------------------------------- 1 | globalSet($model->handle) 17 | ->locale($model->locale) 18 | ->data($model->data) 19 | ->origin($model->origin ?? null) 20 | ->model($model); 21 | } 22 | 23 | public function toModel() 24 | { 25 | return self::makeModelFromContract($this); 26 | } 27 | 28 | public static function makeModelFromContract(Contract $source) 29 | { 30 | $class = app('statamic.eloquent.global_set_variables.model'); 31 | 32 | $data = $source->data(); 33 | 34 | if ($source->hasOrigin()) { 35 | $data = $source->origin()->data()->merge($data); 36 | } 37 | 38 | return $class::firstOrNew([ 39 | 'handle' => $source->globalSet()->handle(), 40 | 'locale' => $source->locale, 41 | ])->fill([ 42 | 'data' => $data->filter(fn ($v) => $v !== null), 43 | 'origin' => $source->hasOrigin() ? $source->origin()->locale() : null, 44 | ]); 45 | } 46 | 47 | protected function getOriginByString($origin) 48 | { 49 | return $this->globalSet()->in($origin); 50 | } 51 | 52 | public function model($model = null) 53 | { 54 | if (func_num_args() === 0) { 55 | return $this->model; 56 | } 57 | 58 | $this->model = $model; 59 | 60 | return $this; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Globals/VariablesModel.php: -------------------------------------------------------------------------------- 1 | 'array', 16 | ]; 17 | 18 | public function getAttribute($key) 19 | { 20 | return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Jobs/UpdateCollectionEntryOrder.php: -------------------------------------------------------------------------------- 1 | entryId = $entryId; 20 | } 21 | 22 | public function handle() 23 | { 24 | if ($entry = Entry::find($this->entryId)) { 25 | $entry->save(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Jobs/UpdateCollectionEntryParent.php: -------------------------------------------------------------------------------- 1 | entryId = $entryId; 20 | } 21 | 22 | public function handle() 23 | { 24 | if ($entry = Entry::find($this->entryId)) { 25 | $entry->save(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Revisions/Revision.php: -------------------------------------------------------------------------------- 1 | key($model->key) 36 | ->action($model->action ?? false) 37 | ->id($model->created_at->timestamp) 38 | ->date($model->created_at) 39 | ->user($model->user ?? false) 40 | ->message($model->message ?? '') 41 | ->attributes($model->attributes ?? []) 42 | ->model($model); 43 | } 44 | 45 | public function toModel() 46 | { 47 | $class = app('statamic.eloquent.revisions.model'); 48 | 49 | return $class::firstOrNew(['key' => $this->key(), 'created_at' => $this->date()])->fill([ 50 | 'action' => $this->action(), 51 | 'user' => $this->user()?->id(), 52 | 'message' => with($this->message(), fn ($msg) => $msg == '0' ? '' : $msg), 53 | 'attributes' => $this->attributes(), 54 | 'updated_at' => $this->date(), 55 | ]); 56 | } 57 | 58 | public function fromRevisionOrWorkingCopy($item) 59 | { 60 | return (new static) 61 | ->key($item->key()) 62 | ->action($item instanceof WorkingCopy ? 'working' : $item->action()) 63 | ->date($item->date()) 64 | ->user($item->user()?->id() ?? false) 65 | ->message($item->message() ?? '') 66 | ->attributes($item->attributes() ?? []); 67 | } 68 | 69 | public function model($model = null) 70 | { 71 | if (func_num_args() === 0) { 72 | return $this->model; 73 | } 74 | 75 | $this->model = $model; 76 | 77 | return $this; 78 | } 79 | 80 | public function save() 81 | { 82 | if (RevisionSaving::dispatch($this) === false) { 83 | return false; 84 | } 85 | 86 | $this->model->save(); 87 | 88 | RevisionSaved::dispatch($this); 89 | } 90 | 91 | public function delete() 92 | { 93 | $this->model->delete(); 94 | 95 | RevisionDeleted::dispatch($this); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Revisions/RevisionModel.php: -------------------------------------------------------------------------------- 1 | 'json', 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /src/Revisions/RevisionRepository.php: -------------------------------------------------------------------------------- 1 | orderBy('created_at') 20 | ->get() 21 | ->map(function ($revision) use ($key) { 22 | return $this->makeRevisionFromFile($key, $revision); 23 | })->keyBy(function ($revision) { 24 | return $revision->date()->timestamp; 25 | }); 26 | } 27 | 28 | public function findWorkingCopyByKey($key) 29 | { 30 | $class = app('statamic.eloquent.revisions.model'); 31 | if (! $revision = $class::where(['key' => $key, 'action' => 'working'])->first()) { 32 | return null; 33 | } 34 | 35 | return $this->makeRevisionFromFile($key, $revision); 36 | } 37 | 38 | public function save(RevisionContract $copy) 39 | { 40 | if ($copy instanceof WorkingCopy) { 41 | app('statamic.eloquent.revisions.model')::where([ 42 | 'key' => $copy->key(), 43 | 'action' => 'working', 44 | ])->delete(); 45 | } 46 | 47 | $revision = (new Revision) 48 | ->fromRevisionOrWorkingCopy($copy) 49 | ->toModel() 50 | ->save(); 51 | } 52 | 53 | public function delete(RevisionContract $revision) 54 | { 55 | if ($revision instanceof WorkingCopy) { 56 | $this->findWorkingCopyByKey($revision->key())?->delete(); 57 | 58 | return; 59 | } 60 | 61 | $revision->model?->delete(); 62 | } 63 | 64 | protected function makeRevisionFromFile($key, $model) 65 | { 66 | return (new Revision)->fromModel($model); 67 | } 68 | 69 | public static function bindings(): array 70 | { 71 | return [ 72 | RevisionContract::class => Revision::class, 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Sites/SiteModel.php: -------------------------------------------------------------------------------- 1 | 'json', 16 | ]; 17 | 18 | public function getAttribute($key) 19 | { 20 | return Arr::get($this->getAttributeValue('attributes'), $key, parent::getAttribute($key)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Sites/Sites.php: -------------------------------------------------------------------------------- 1 | getTable())) { 15 | return $this->getFallbackConfig(); 16 | } 17 | 18 | $sites = app('statamic.eloquent.sites.model')::all(); 19 | 20 | return $sites->isEmpty() ? $this->getFallbackConfig() : $sites->mapWithKeys(function ($model) { 21 | return [ 22 | $model->handle => [ 23 | 'name' => $model->name, 24 | 'lang' => $model->lang, 25 | 'locale' => $model->locale, 26 | 'url' => $model->url, 27 | 'attributes' => $model->attributes ?? [], 28 | ], 29 | ]; 30 | }); 31 | } 32 | 33 | protected function saveToStore() 34 | { 35 | foreach ($this->config() as $handle => $config) { 36 | $lang = $config['lang'] ?? Str::before($config['locale'] ?? '', '_') ?? 'en'; 37 | 38 | app('statamic.eloquent.sites.model')::firstOrNew(['handle' => $handle]) 39 | ->fill([ 40 | 'name' => $config['name'] ?? '', 41 | 'lang' => $lang, 42 | 'locale' => $config['locale'] ?? '', 43 | 'url' => $config['url'] ?? '', 44 | 'attributes' => $config['attributes'] ?? [], 45 | ]) 46 | ->save(); 47 | } 48 | 49 | app('statamic.eloquent.sites.model')::whereNotIn('handle', array_keys($this->config()))->get()->each->delete(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Structures/CollectionStructure.php: -------------------------------------------------------------------------------- 1 | tree($model->tree) 16 | ->handle($model->handle) 17 | ->locale($model->locale) 18 | ->syncOriginal() 19 | ->model($model); 20 | } 21 | 22 | public function toModel() 23 | { 24 | return self::makeModelFromContract($this); 25 | } 26 | 27 | public static function makeModelFromContract($source) 28 | { 29 | $class = app('statamic.eloquent.collections.tree_model'); 30 | 31 | return $class::firstOrNew([ 32 | 'handle' => $source->handle(), 33 | 'type' => 'collection', 34 | 'locale' => $source->locale(), 35 | ])->fill([ 36 | 'tree' => $source->tree(), 37 | 'settings' => [], 38 | ]); 39 | } 40 | 41 | public function model($model = null) 42 | { 43 | if (func_num_args() === 0) { 44 | return $this->model; 45 | } 46 | 47 | $this->model = $model; 48 | 49 | return $this; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Structures/CollectionTreeRepository.php: -------------------------------------------------------------------------------- 1 | where('locale', $site) 17 | ->whereType('collection') 18 | ->first(); 19 | 20 | return $model ? app(app('statamic.eloquent.collections.tree'))->fromModel($model) : null; 21 | }); 22 | } 23 | 24 | public function save($entry) 25 | { 26 | $model = $entry->toModel(); 27 | $model->save(); 28 | 29 | Blink::forget("eloquent-collection-tree-{$model->handle}-{$model->locale}"); 30 | 31 | $entry->model($model->fresh()); 32 | } 33 | 34 | public static function bindings() 35 | { 36 | return [ 37 | CollectionTreeContract::class => CollectionTree::class, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Structures/Nav.php: -------------------------------------------------------------------------------- 1 | handle($model->handle) 17 | ->title($model->title) 18 | ->collections($model->settings['collections'] ?? null) 19 | ->maxDepth($model->settings['max_depth'] ?? null) 20 | ->expectsRoot($model->settings['expects_root'] ?? false) 21 | ->canSelectAcrossSites($model->settings['select_across_sites'] ?? false) 22 | ->model($model); 23 | } 24 | 25 | public function newTreeInstance() 26 | { 27 | return app(app('statamic.eloquent.navigations.tree')); 28 | } 29 | 30 | public function toModel() 31 | { 32 | return self::makeModelFromContract($this); 33 | } 34 | 35 | public static function makeModelFromContract(Contract $source) 36 | { 37 | $class = app('statamic.eloquent.navigations.model'); 38 | 39 | $model = $class::firstOrNew(['handle' => $source->handle()])->fill([ 40 | 'title' => $source->title(), 41 | 'settings' => [], 42 | ]); 43 | 44 | $model->settings = array_merge($model->settings ?? [], [ 45 | 'collections' => $source->collections()->map->handle(), 46 | 'select_across_sites' => $source->canSelectAcrossSites(), 47 | 'max_depth' => $source->maxDepth(), 48 | 'expects_root' => $source->expectsRoot(), 49 | ]); 50 | 51 | return $model; 52 | } 53 | 54 | public function model($model = null) 55 | { 56 | if (func_num_args() === 0) { 57 | return $this->model; 58 | } 59 | 60 | $this->model = $model; 61 | 62 | $this->id($model->id); 63 | 64 | return $this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Structures/NavModel.php: -------------------------------------------------------------------------------- 1 | 'json', 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /src/Structures/NavTree.php: -------------------------------------------------------------------------------- 1 | tree($model->tree) 16 | ->handle($model->handle) 17 | ->locale($model->locale) 18 | ->syncOriginal() 19 | ->model($model); 20 | } 21 | 22 | public function toModel() 23 | { 24 | return self::makeModelFromContract($this); 25 | } 26 | 27 | public static function makeModelFromContract($source) 28 | { 29 | $class = app('statamic.eloquent.navigations.tree_model'); 30 | 31 | $isFileEntry = get_class($source) == FileEntry::class; 32 | 33 | return $class::firstOrNew([ 34 | 'handle' => $source->handle(), 35 | 'type' => 'navigation', 36 | 'locale' => $source->locale(), 37 | ])->fill([ 38 | 'tree' => ($isFileEntry || $source->model) ? $source->tree() : [], 39 | 'settings' => [], 40 | ]); 41 | } 42 | 43 | public function model($model = null) 44 | { 45 | if (func_num_args() === 0) { 46 | return $this->model; 47 | } 48 | 49 | $this->model = $model; 50 | 51 | return $this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Structures/NavTreeRepository.php: -------------------------------------------------------------------------------- 1 | whereType('navigation') 17 | ->where('locale', $site) 18 | ->first(); 19 | 20 | return $model ? app(app('statamic.eloquent.navigations.tree'))->fromModel($model) : null; 21 | }); 22 | } 23 | 24 | public function save($entry) 25 | { 26 | // if we are using flat files for the config, but eloquent for the data 27 | if (! $entry instanceof NavTree) { 28 | return parent::save($entry); 29 | } 30 | 31 | $model = $entry->toModel(); 32 | $model->save(); 33 | 34 | Blink::forget("eloquent-nav-tree-{$model->handle}-{$model->locale}"); 35 | 36 | $entry->model($model->fresh()); 37 | } 38 | 39 | public function delete($entry) 40 | { 41 | // if we are using flat files for the config, but eloquent for the data 42 | if (! $entry instanceof NavTree) { 43 | return parent::save($entry); 44 | } 45 | 46 | $entry->model()->delete(); 47 | } 48 | 49 | public static function bindings() 50 | { 51 | return [ 52 | NavTreeContract::class => NavTree::class, 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Structures/NavigationRepository.php: -------------------------------------------------------------------------------- 1 | map(function ($model) { 15 | return Nav::fromModel($model); 16 | }); 17 | } 18 | 19 | public static function bindings(): array 20 | { 21 | return [ 22 | NavContract::class => Nav::class, 23 | ]; 24 | } 25 | 26 | public function all(): Collection 27 | { 28 | return $this->transform(app('statamic.eloquent.navigations.model')::all()); 29 | } 30 | 31 | public function findByHandle($handle): ?NavContract 32 | { 33 | return Blink::once("eloquent-nav-{$handle}", function () use ($handle) { 34 | $model = app('statamic.eloquent.navigations.model')::whereHandle($handle)->first(); 35 | 36 | return $model ? app(NavContract::class)->fromModel($model) : null; 37 | }); 38 | } 39 | 40 | public function save($entry) 41 | { 42 | $model = $entry->toModel(); 43 | $model->save(); 44 | 45 | Blink::forget("eloquent-nav-{$model->handle}"); 46 | 47 | $entry->model($model->fresh()); 48 | } 49 | 50 | public function delete($entry) 51 | { 52 | $entry->model()->delete(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Structures/TreeModel.php: -------------------------------------------------------------------------------- 1 | 'json', 15 | 'settings' => 'json', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Taxonomies/Taxonomy.php: -------------------------------------------------------------------------------- 1 | handle($model->handle) 17 | ->title($model->title) 18 | ->sites($model->sites) 19 | ->revisionsEnabled($model->settings['revisions'] ?? false) 20 | ->previewTargets($model->settings['preview_targets'] ?? []) 21 | ->searchIndex($model->settings['search_index'] ?? '') 22 | ->termTemplate($model->settings['term_template'] ?? null) 23 | ->template($model->settings['template'] ?? null) 24 | ->layout($model->settings['layout'] ?? null) 25 | ->model($model); 26 | } 27 | 28 | public function toModel() 29 | { 30 | return self::makeModelFromContract($this); 31 | } 32 | 33 | public static function makeModelFromContract(Contract $source) 34 | { 35 | $class = app('statamic.eloquent.taxonomies.model'); 36 | 37 | $model = $class::firstOrNew(['handle' => $source->handle()])->fill([ 38 | 'title' => $source->title(), 39 | 'sites' => $source->sites(), 40 | 'settings' => [], 41 | ]); 42 | 43 | $model->settings = array_merge($model->settings ?? [], [ 44 | 'revisions' => $source->revisionsEnabled(), 45 | 'preview_targets' => $source->previewTargets(), 46 | 'term_template' => $source->hasCustomTermTemplate() ? $source->termTemplate() : null, 47 | 'template' => $source->hasCustomTemplate() ? $source->template() : null, 48 | 'layout' => $source->layout, 49 | ]); 50 | 51 | return $model; 52 | } 53 | 54 | public function model($model = null) 55 | { 56 | if (func_num_args() === 0) { 57 | return $this->model; 58 | } 59 | 60 | $this->model = $model; 61 | 62 | if (! is_null($model)) { 63 | $this->id($model->id); 64 | } 65 | 66 | return $this; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Taxonomies/TaxonomyModel.php: -------------------------------------------------------------------------------- 1 | 'json', 16 | 'sites' => 'json', 17 | ]; 18 | 19 | public function getAttribute($key) 20 | { 21 | return Arr::get($this->getAttributeValue('settings'), $key, parent::getAttribute($key)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Taxonomies/TaxonomyRepository.php: -------------------------------------------------------------------------------- 1 | map(function ($model) { 18 | return app(TaxonomyContract::class)::fromModel($model); 19 | }); 20 | } 21 | 22 | public static function bindings(): array 23 | { 24 | return [ 25 | TaxonomyContract::class => Taxonomy::class, 26 | ]; 27 | } 28 | 29 | public function all(): Collection 30 | { 31 | $models = Blink::once('eloquent-taxonomies', function () { 32 | return app('statamic.eloquent.taxonomies.model')::all(); 33 | }) 34 | ->each(function ($model) { 35 | Blink::put("eloquent-taxonomies-{$model->handle}", $model); 36 | }); 37 | 38 | return $this->transform($models); 39 | } 40 | 41 | public function findByHandle($handle): ?TaxonomyContract 42 | { 43 | $taxonomyModel = Blink::once("eloquent-taxonomies-{$handle}", function () use ($handle) { 44 | return app('statamic.eloquent.taxonomies.model')::whereHandle($handle)->first(); 45 | }); 46 | 47 | return $taxonomyModel instanceof Model ? app(TaxonomyContract::class)->fromModel($taxonomyModel) : null; 48 | } 49 | 50 | public function findByUri(string $uri, ?string $site = null): ?Taxonomy 51 | { 52 | $collection = Facades\Collection::all() 53 | ->first(function ($collection) use ($uri, $site) { 54 | if (Str::startsWith($uri, $collection->uri($site))) { 55 | return true; 56 | } 57 | 58 | return Str::startsWith($uri, '/'.$collection->handle()); 59 | }); 60 | 61 | if ($collection) { 62 | $uri = Str::after($uri, $collection->uri($site) ?? $collection->handle()); 63 | } 64 | 65 | // If the collection is mounted to the home page, the uri would have 66 | // the slash trimmed off at this point. We'll make sure it's there, 67 | // then look for whats after it to get our handle. 68 | $uri = Str::after(Str::ensureLeft($uri, '/'), '/'); 69 | 70 | return ($taxonomy = $this->findByHandle($uri)) ? $taxonomy->collection($collection) : null; 71 | } 72 | 73 | public function handles(): Collection 74 | { 75 | return $this->all()->map->handle(); 76 | } 77 | 78 | public function save($entry) 79 | { 80 | $model = $entry->toModel(); 81 | $model->save(); 82 | 83 | $fresh = $model->fresh(); 84 | 85 | $entry->model($fresh); 86 | 87 | Blink::put("eloquent-taxonomies-{$fresh->handle}", $fresh); 88 | } 89 | 90 | public function delete($entry) 91 | { 92 | $model = $entry->model(); 93 | $model->delete(); 94 | 95 | Blink::forget("eloquent-taxonomies-{$model->handle}"); 96 | Blink::forget('eloquent-taxonomies'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Taxonomies/Term.php: -------------------------------------------------------------------------------- 1 | data; 17 | 18 | /** @var Term $term */ 19 | $term = (new static) 20 | ->slug($model->slug) 21 | ->taxonomy($model->taxonomy) 22 | ->model($model) 23 | ->blueprint($model->data['blueprint'] ?? null); 24 | 25 | collect($data['localizations'] ?? []) 26 | ->except($term->defaultLocale()) 27 | ->each(function ($localeData, $locale) use ($term) { 28 | $term->dataForLocale($locale, $localeData); 29 | }); 30 | 31 | unset($data['localizations']); 32 | 33 | if (isset($data['collection'])) { 34 | $term->collection($data['collection']); 35 | unset($data['collection']); 36 | } 37 | 38 | $term->data($data); 39 | 40 | if (config('statamic.system.track_last_update')) { 41 | $updatedAt = ($model->updated_at ?? $model->created_at); 42 | 43 | $term->set('updated_at', $updatedAt instanceof Carbon ? $updatedAt->timestamp : $updatedAt); 44 | } 45 | 46 | return $term->syncOriginal(); 47 | } 48 | 49 | public function toModel() 50 | { 51 | return self::makeModelFromContract($this); 52 | } 53 | 54 | public static function makeModelFromContract(Contract $source) 55 | { 56 | $class = app('statamic.eloquent.terms.model'); 57 | 58 | $data = $source->fileData(); 59 | 60 | if (! isset($data['template'])) { 61 | unset($data['template']); 62 | } 63 | 64 | if ($source->blueprint && $source->taxonomy()->termBlueprints()->count() > 1) { 65 | $data['blueprint'] = $source->blueprint; 66 | } 67 | 68 | $source->localizations()->keys()->reduce(function ($data, $locale) use ($source) { 69 | $data[$locale] = $source->dataForLocale($locale)->toArray(); 70 | 71 | return $data; 72 | }, []); 73 | 74 | if ($collection = $source->collection()) { 75 | $data['collection'] = $collection; 76 | } 77 | 78 | return $class::firstOrNew([ 79 | 'slug' => $source->getOriginal('slug', $source->slug()), 80 | 'taxonomy' => $source->taxonomy(), 81 | 'site' => $source->locale(), 82 | ])->fill([ 83 | 'slug' => $source->slug(), 84 | 'uri' => $source->uri(), 85 | 'data' => collect($data)->filter(fn ($v) => $v !== null), 86 | 'updated_at' => $source->lastModified(), 87 | ]); 88 | } 89 | 90 | public function model($model = null) 91 | { 92 | if (func_num_args() === 0) { 93 | return $this->model; 94 | } 95 | 96 | $this->model = $model; 97 | 98 | if (! is_null($model)) { 99 | $this->id($model->id); 100 | } 101 | 102 | return $this; 103 | } 104 | 105 | public function fileLastModified() 106 | { 107 | return $this->model?->updated_at ?? Carbon::now(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Taxonomies/TermModel.php: -------------------------------------------------------------------------------- 1 | 'json', 16 | ]; 17 | 18 | public function getAttribute($key) 19 | { 20 | return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Taxonomies/TermRepository.php: -------------------------------------------------------------------------------- 1 | ensureAssociations(); 17 | 18 | return app(TermQueryBuilder::class); 19 | } 20 | 21 | public function find($id): ?TermContract 22 | { 23 | [$handle, $slug] = explode('::', $id); 24 | 25 | $blinkKey = "eloquent-term-{$id}"; 26 | $term = Blink::once($blinkKey, function () use ($handle, $slug) { 27 | return $this->query() 28 | ->where('taxonomy', $handle) 29 | ->where('slug', $slug) 30 | ->get() 31 | ->first(); 32 | }); 33 | 34 | if (! $term) { 35 | Blink::forget($blinkKey); 36 | 37 | return null; 38 | } 39 | 40 | return $term; 41 | } 42 | 43 | public function findByUri(string $uri, ?string $site = null): ?TermContract 44 | { 45 | $site = $site ?? $this->stache->sites()->first(); 46 | 47 | if ($substitute = $this->substitutionsByUri[$site.'@'.$uri] ?? null) { 48 | return $substitute; 49 | } 50 | 51 | $collection = Collection::all() 52 | ->first(function ($collection) use ($uri, $site) { 53 | if (Str::startsWith($uri, $collection->uri($site))) { 54 | return true; 55 | } 56 | 57 | return Str::startsWith($uri, '/'.$collection->handle()); 58 | }); 59 | 60 | if ($collection) { 61 | $uri = Str::after($uri, $collection->uri($site) ?? $collection->handle()); 62 | } 63 | 64 | $uri = Str::removeLeft($uri, '/'); 65 | 66 | [$taxonomy, $slug] = array_pad(explode('/', $uri), 2, null); 67 | 68 | if (! $slug) { 69 | return null; 70 | } 71 | 72 | if (! $taxonomy = $this->findTaxonomyHandleByUri(Str::ensureLeft($taxonomy, '/'))) { 73 | return null; 74 | } 75 | 76 | $blinkKey = 'eloquent-term-'.md5(urlencode($uri)).($site ? '-'.$site : ''); 77 | $term = Blink::once($blinkKey, function () use ($slug, $taxonomy) { 78 | return $this->query() 79 | ->where('slug', $slug) 80 | ->where('taxonomy', $taxonomy) 81 | ->first(); 82 | }); 83 | 84 | if (! $term) { 85 | Blink::forget($blinkKey); 86 | 87 | return null; 88 | } 89 | 90 | return $term->in($site)?->collection($collection); 91 | } 92 | 93 | private function findTaxonomyHandleByUri($uri) 94 | { 95 | return Taxonomy::all()->first(function ($taxonomy) use ($uri) { 96 | return $taxonomy->uri() == $uri; 97 | })?->handle(); 98 | } 99 | 100 | public function save($entry) 101 | { 102 | $model = $entry->toModel(); 103 | $model->save(); 104 | 105 | $entry->model($model->fresh()); 106 | 107 | Blink::put("eloquent-term-{$entry->id()}", $entry); 108 | Blink::put("eloquent-entry-{$entry->uri()}", $entry); 109 | } 110 | 111 | public function delete($entry) 112 | { 113 | $entry->model()->delete(); 114 | 115 | Blink::forget("eloquent-term-{$entry->id()}"); 116 | Blink::forget("eloquent-entry-{$entry->uri()}"); 117 | } 118 | 119 | public static function bindings(): array 120 | { 121 | return [ 122 | TermContract::class => Term::class, 123 | ]; 124 | } 125 | 126 | protected function ensureAssociations() 127 | { 128 | if (config('statamic.eloquent-driver.taxonomies.driver', 'file') === 'eloquent') { 129 | return; 130 | } 131 | 132 | parent::ensureAssociations(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Tokens/Token.php: -------------------------------------------------------------------------------- 1 | token, $model->handler, $model->data)) 16 | ->expireAt($model->expire_at) 17 | ->model($model); 18 | } 19 | 20 | public function toModel() 21 | { 22 | return self::makeModelFromContract($this); 23 | } 24 | 25 | public static function makeModelFromContract(Contract $source) 26 | { 27 | $class = app('statamic.eloquent.tokens.model'); 28 | 29 | return $class::firstOrNew(['token' => $source->token()])->fill([ 30 | 'handler' => $source->handler(), 31 | 'data' => $source->data(), 32 | 'expire_at' => $source->expiry(), 33 | ]); 34 | } 35 | 36 | public function model($model = null) 37 | { 38 | if (func_num_args() === 0) { 39 | return $this->model; 40 | } 41 | 42 | $this->model = $model; 43 | 44 | return $this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Tokens/TokenModel.php: -------------------------------------------------------------------------------- 1 | 'json', 15 | 'expire_at' => 'datetime', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Tokens/TokenRepository.php: -------------------------------------------------------------------------------- 1 | first(); 13 | 14 | if (! $model) { 15 | return null; 16 | } 17 | 18 | return Token::fromModel($model); 19 | } 20 | 21 | public function save(TokenContract $token): bool 22 | { 23 | return $token->toModel()->save(); 24 | } 25 | 26 | public function delete(TokenContract $token): bool 27 | { 28 | return $token->toModel()->delete(); 29 | } 30 | 31 | public function collectGarbage(): void 32 | { 33 | app('statamic.eloquent.tokens.model')::where('expire_at', '<', now())->delete(); 34 | } 35 | 36 | public static function bindings(): array 37 | { 38 | return [ 39 | TokenContract::class => Token::class, 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Updates/AddBlueprintToEntriesTable.php: -------------------------------------------------------------------------------- 1 | files->copy($source, $dest); 23 | 24 | $this->console()->info('Migration created'); 25 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Updates/AddIdToAttributesInRevisionsTable.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('3.0.2'); 12 | } 13 | 14 | public function update() 15 | { 16 | $source = __DIR__.'/../../database/migrations/updates/add_id_to_attributes_in_revisions_table.php.stub'; 17 | $dest = database_path('migrations/'.date('Y_m_d_His').'_add_id_to_attributes_in_revisions_table.php'); 18 | 19 | $this->files->copy($source, $dest); 20 | 21 | $this->console()->info('Migration created'); 22 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Updates/AddIndexToDateOnEntriesTable.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('4.22.0') 13 | && Schema::hasTable(config('statamic.eloquent-driver.table_prefix', '').'entries') 14 | && ! Schema::hasIndex(config('statamic.eloquent-driver.table_prefix', '').'entries', 'date'); 15 | } 16 | 17 | public function update() 18 | { 19 | $source = __DIR__.'/../../database/migrations/updates/add_index_to_date_on_entries_table.php.stub'; 20 | $dest = database_path('migrations/'.date('Y_m_d_His').'_add_index_to_date_on_entries_table.php'); 21 | 22 | $this->files->copy($source, $dest); 23 | 24 | $this->console()->info('Migration created'); 25 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Updates/AddMetaAndIndexesToAssetsTable.php: -------------------------------------------------------------------------------- 1 | files->copy($source, $dest); 23 | 24 | $this->console()->info('Migration created'); 25 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Updates/AddOrderToEntriesTable.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('1.0.3') 13 | && ! Schema::hasColumn(config('statamic.eloquent-driver.table_prefix', '').'entries', 'order'); 14 | } 15 | 16 | public function update() 17 | { 18 | $source = __DIR__.'/../../database/migrations/updates/add_order_to_entries_table.php.stub'; 19 | $dest = database_path('migrations/'.date('Y_m_d_His').'_add_order_to_entries_table.php'); 20 | 21 | $this->files->copy($source, $dest); 22 | 23 | $this->console()->info('Migration created'); 24 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Updates/ChangeDefaultBlueprint.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('2.3.0'); 13 | } 14 | 15 | public function update() 16 | { 17 | $model = BlueprintModel::where('handle', 'default')->first(); 18 | 19 | if ($model) { 20 | $model->data = [ 21 | 'fields' => [ 22 | [ 23 | 'field' => [ 24 | 'type' => 'markdown', 25 | 'display' => 'Content', 26 | 'localizable' => true, 27 | ], 28 | 'handle' => 'content', 29 | ], 30 | ], 31 | ]; 32 | 33 | $model->save(); 34 | 35 | $this->console()->info('Successfully updated the default blueprint'); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Updates/ChangeFormSubmissionsIdType.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('4.1.0'); 12 | } 13 | 14 | public function update() 15 | { 16 | $source = __DIR__.'/../../database/migrations/2024_05_15_100000_modify_form_submissions_id.php'; 17 | $dest = database_path('migrations/2024_05_15_100000_modify_form_submissions_id.php'); 18 | 19 | $this->files->copy($source, $dest); 20 | 21 | $this->console()->info('Migration created'); 22 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Updates/DropForeignKeysOnEntriesAndForms.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('2.3.0'); 12 | } 13 | 14 | public function update() 15 | { 16 | $source = __DIR__.'/../../database/migrations/updates/drop_foreign_keys_on_entries.php.stub'; 17 | $dest = database_path('migrations/'.date('Y_m_d_His').'_drop_foreign_keys_on_entries.php'); 18 | 19 | $this->files->copy($source, $dest); 20 | 21 | $source = __DIR__.'/../../database/migrations/updates/drop_foreign_keys_on_forms.php.stub'; 22 | $dest = database_path('migrations/'.date('Y_m_d_His').'_drop_foreign_keys_on_forms.php'); 23 | 24 | $this->files->copy($source, $dest); 25 | 26 | $this->console()->info('Migrations created'); 27 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Updates/DropStatusOnEntries.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('4.0.0'); 12 | } 13 | 14 | public function update() 15 | { 16 | $source = __DIR__.'/../../database/migrations/updates/drop_status_on_entries.php.stub'; 17 | $dest = database_path('migrations/'.date('Y_m_d_His').'_drop_status_on_entries.php'); 18 | 19 | $this->files->copy($source, $dest); 20 | 21 | $this->console()->info('Migrations created'); 22 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Updates/MoveConfig.php: -------------------------------------------------------------------------------- 1 | isUpdatingTo('0.2.0'); 12 | } 13 | 14 | public function update() 15 | { 16 | $oldPath = config_path('statamic-eloquent-driver.php'); 17 | $newPath = config_path('statamic/eloquent-driver.php'); 18 | 19 | if ($this->files->exists($newPath) || ! $this->files->exists($oldPath)) { 20 | return; 21 | } 22 | 23 | $this->files->move($oldPath, $newPath); 24 | 25 | $this->console()->info('Eloquent driver config successfully moved to [config/statamic/eloquent-driver.php]!'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Updates/RelateFormSubmissionsByHandle.php: -------------------------------------------------------------------------------- 1 | files->copy($source, $dest); 21 | 22 | $this->console()->info('Migration created'); 23 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Updates/SplitGlobalsFromVariables.php: -------------------------------------------------------------------------------- 1 | files->copy($source, $dest); 23 | 24 | $source = __DIR__.'/../../database/migrations/updates/update_globals_table.php.stub'; 25 | $dest = database_path('migrations/'.date('Y_m_d_His').'_update_globals_table.php'); 26 | 27 | $this->files->copy($source, $dest); 28 | 29 | $this->console()->info('Migration created'); 30 | $this->console()->comment('Remember to run `php artisan migrate` to apply it to your database.'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Assets/AssetContainerContentsTest.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'driver' => 'local', 21 | 'root' => __DIR__.'/tmp', 22 | ]]); 23 | } 24 | 25 | protected function tearDown(): void 26 | { 27 | app('files')->deleteDirectory(__DIR__.'/tmp'); 28 | 29 | parent::tearDown(); 30 | } 31 | 32 | #[Test] 33 | public function it_gets_a_folder_listing() 34 | { 35 | $container = tap(AssetContainer::make('test')->disk('test'))->save(); 36 | $container->makeAsset('one/one.txt')->upload(UploadedFile::fake()->create('one.txt')); 37 | $container->makeAsset('two/two.txt')->upload(UploadedFile::fake()->create('two.txt')); 38 | 39 | $this->assertSame([ 40 | [ 41 | 'path' => 'one', 42 | 'type' => 'dir', 43 | ], 44 | [ 45 | 'path' => 'two', 46 | 'type' => 'dir', 47 | ], 48 | ], $container->contents()->directories()->all()); 49 | } 50 | 51 | #[Test] 52 | public function it_adds_to_a_folder_listing() 53 | { 54 | $container = tap(AssetContainer::make('test')->disk('test'))->save(); 55 | $container->makeAsset('one/one.txt')->upload(UploadedFile::fake()->create('one.txt')); 56 | $container->makeAsset('two/two.txt')->upload(UploadedFile::fake()->create('two.txt')); 57 | 58 | $this->assertCount(2, $container->contents()->directories()->all()); 59 | 60 | $container->contents()->add('three'); 61 | 62 | $this->assertCount(3, $container->contents()->directories()->all()); 63 | } 64 | 65 | #[Test] 66 | public function it_forgets_a_folder_listing() 67 | { 68 | $container = tap(AssetContainer::make('test')->disk('test'))->save(); 69 | $container->makeAsset('one/one.txt')->upload(UploadedFile::fake()->create('one.txt')); 70 | $container->makeAsset('two/two.txt')->upload(UploadedFile::fake()->create('two.txt')); 71 | 72 | $this->assertCount(2, $container->contents()->directories()->all()); 73 | 74 | $container->contents()->forget('one'); 75 | 76 | $this->assertCount(1, $container->contents()->directories()->all()); 77 | } 78 | 79 | #[Test] 80 | public function it_creates_parent_folders_where_they_dont_exist() 81 | { 82 | $container = tap(AssetContainer::make('test')->disk('test'))->save(); 83 | $container->makeAsset('one/two/three/file.txt')->upload(UploadedFile::fake()->create('one.txt')); 84 | 85 | $this->assertCount(3, $container->contents()->filteredDirectoriesIn('', true)); 86 | } 87 | 88 | #[Test] 89 | public function it_doesnt_nest_folders_that_start_with_the_same_name() 90 | { 91 | $container = tap(AssetContainer::make('test')->disk('test'))->save(); 92 | $container->makeAsset('one/file.txt')->upload(UploadedFile::fake()->create('one.txt')); 93 | $container->makeAsset('one-two/file.txt')->upload(UploadedFile::fake()->create('one.txt')); 94 | $container->makeAsset('one/two/file.txt')->upload(UploadedFile::fake()->create('one.txt')); 95 | 96 | $filtered = $container->contents()->filteredDirectoriesIn('one/', true); 97 | 98 | $this->assertCount(1, $filtered); 99 | $this->assertSame($filtered->keys()->all(), ['one/two']); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/Assets/AssetTest.php: -------------------------------------------------------------------------------- 1 | '/assets']); 22 | 23 | $this->container = tap(Facades\AssetContainer::make('test')->disk('test'))->save(); 24 | 25 | Storage::disk('test')->put('a.jpg', ''); 26 | Facades\Asset::make()->container('test')->path('a.jpg')->save(); 27 | 28 | Storage::disk('test')->put('b.txt', ''); 29 | Facades\Asset::make()->container('test')->path('b.txt')->save(); 30 | 31 | Storage::disk('test')->put('c.txt', ''); 32 | Facades\Asset::make()->container('test')->path('c.txt')->save(); 33 | 34 | Storage::disk('test')->put('d.jpg', ''); 35 | Facades\Asset::make()->container('test')->path('d.jpg')->save(); 36 | 37 | Storage::disk('test')->put('e.jpg', ''); 38 | Facades\Asset::make()->container('test')->path('e.jpg')->save(); 39 | 40 | Storage::disk('test')->put('f.jpg', ''); 41 | Facades\Asset::make()->container('test')->path('f.jpg')->save(); 42 | } 43 | 44 | #[Test] 45 | public function saving_an_asset_clears_the_eloquent_blink_cache() 46 | { 47 | $asset = Facades\Asset::find('test::f.jpg'); 48 | 49 | $this->assertTrue(Facades\Blink::has("eloquent-asset-{$asset->id()}")); 50 | 51 | $asset->save(); 52 | 53 | $this->assertFalse(Facades\Blink::has("eloquent-asset-{$asset->id()}")); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Collections/CollectionTest.php: -------------------------------------------------------------------------------- 1 | 'Blog', 21 | 'handle' => 'blog', 22 | 'settings' => [ 23 | 'routes' => '/blog/{slug}', 24 | 'slugs' => true, 25 | 'dated' => true, 26 | 'template' => 'blog', 27 | 'default_status' => false, 28 | ], 29 | ]); 30 | 31 | $find = CollectionFacade::find('blog'); 32 | 33 | $this->assertTrue($model->is($find->model())); 34 | $this->assertEquals('blog', $find->handle()); 35 | $this->assertEquals('Blog', $find->title()); 36 | $this->assertEquals('/blog/{slug}', $find->route('en')); 37 | $this->assertTrue($find->requiresSlugs()); 38 | $this->assertTrue($find->dated()); 39 | $this->assertEquals('blog', $find->template()); 40 | $this->assertFalse($find->defaultPublishState()); 41 | } 42 | 43 | #[Test] 44 | public function it_saves_to_collection_model() 45 | { 46 | $collection = (new Collection)->handle('test'); 47 | 48 | $this->assertDatabaseMissing('collections', ['handle' => 'test']); 49 | 50 | $collection->save(); 51 | 52 | $this->assertDatabaseHas('collections', ['handle' => 'test']); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Commands/ExportSitesTest.php: -------------------------------------------------------------------------------- 1 | 'en', 'name' => 'English', 'locale' => 'en_US', 'lang' => '', 'url' => 'http://test.com/', 'attributes' => []]); 19 | SiteModel::create(['handle' => 'fr', 'name' => 'French', 'locale' => 'fr_FR', 'lang' => '', 'url' => 'http://fr.test.com/', 'attributes' => []]); 20 | SiteModel::create(['handle' => 'es', 'name' => 'Spanish', 'locale' => 'es_ES', 'lang' => '', 'url' => 'http://test.com/es/', 'attributes' => ['foo' => 'bar']]); 21 | SiteModel::create(['handle' => 'de', 'name' => 'German', 'locale' => 'de_DE', 'lang' => 'de', 'url' => 'http://test.com/de/', 'attributes' => []]); 22 | 23 | $this->artisan('statamic:eloquent:export-sites') 24 | ->expectsOutputToContain('Sites exported') 25 | ->assertExitCode(0); 26 | 27 | $this->assertEquals([ 28 | 'en' => [ 29 | 'name' => 'English', 30 | 'locale' => 'en_US', 31 | 'url' => 'http://test.com/', 32 | ], 33 | 'fr' => [ 34 | 'name' => 'French', 35 | 'locale' => 'fr_FR', 36 | 'url' => 'http://fr.test.com/', 37 | ], 38 | 'es' => [ 39 | 'name' => 'Spanish', 40 | 'locale' => 'es_ES', 41 | 'url' => 'http://test.com/es/', 42 | 'attributes' => ['foo' => 'bar'], 43 | ], 44 | 'de' => [ 45 | 'name' => 'German', 46 | 'lang' => 'de', 47 | 'locale' => 'de_DE', 48 | 'url' => 'http://test.com/de/', 49 | ], 50 | ], (new Sites)->config()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Commands/ImportRevisionsTest.php: -------------------------------------------------------------------------------- 1 | set('statamic.revisions', [ 24 | 'enabled' => true, 25 | 'path' => __DIR__.'/tmp', 26 | ]); 27 | 28 | mkdir(__DIR__.'/tmp'); 29 | 30 | Facade::clearResolvedInstance(RevisionRepositoryContract::class); 31 | 32 | app()->bind(RevisionRepositoryContract::class, \Statamic\Revisions\RevisionRepository::class); 33 | app()->bind(RevisionContract::class, \Statamic\Revisions\Revision::class); 34 | } 35 | 36 | protected function tearDown(): void 37 | { 38 | app('files')->deleteDirectory(__DIR__.'/tmp'); 39 | 40 | parent::tearDown(); 41 | } 42 | 43 | #[Test] 44 | public function it_cannot_import_revisions_when_feature_is_disabled() 45 | { 46 | config(['statamic.revisions.enabled' => false]); 47 | 48 | $this->artisan('statamic:eloquent:import-revisions') 49 | ->expectsOutputToContain('This import can only be run when revisions are enabled.'); 50 | } 51 | 52 | #[Test] 53 | public function it_imports_revisions() 54 | { 55 | Revision::make() 56 | ->key('collections/pages/en/foo') 57 | ->action('revision') 58 | ->date(Carbon::now()) 59 | ->message('Initial revision') 60 | ->attributes(['foo' => 'bar']) 61 | ->save(); 62 | 63 | $this->assertCount(0, RevisionModel::all()); 64 | 65 | $this->artisan('statamic:eloquent:import-revisions') 66 | ->expectsOutputToContain('Revisions imported successfully.') 67 | ->assertExitCode(0); 68 | 69 | $this->assertCount(1, RevisionModel::all()); 70 | 71 | $this->assertDatabaseHas('revisions', [ 72 | 'key' => 'collections/pages/en/foo', 73 | 'action' => 'revision', 74 | 'message' => 'Initial revision', 75 | 'attributes' => '{"foo":"bar"}', 76 | ]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Commands/ImportSitesTest.php: -------------------------------------------------------------------------------- 1 | app->bind('statamic.eloquent.sites.model', function () { 19 | return SiteModel::class; 20 | }); 21 | } 22 | 23 | #[Test] 24 | public function it_imports_sites() 25 | { 26 | $this->assertCount(0, SiteModel::all()); 27 | 28 | $this->setSites([ 29 | 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], 30 | 'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'], 31 | 'es' => ['name' => 'Spanish', 'locale' => 'es_ES', 'url' => 'http://test.com/es/'], 32 | 'de' => ['name' => 'German', 'locale' => 'de_DE', 'url' => 'http://test.com/de/'], 33 | ]); 34 | 35 | \Statamic\Facades\Site::save(); 36 | 37 | $this->artisan('statamic:eloquent:import-sites') 38 | ->expectsOutputToContain('Sites imported successfully.') 39 | ->assertExitCode(0); 40 | 41 | $this->assertCount(4, SiteModel::all()); 42 | 43 | $this->assertDatabaseHas('sites', [ 44 | 'handle' => 'en', 45 | 'name' => 'English', 46 | ]); 47 | 48 | $this->assertDatabaseHas('sites', [ 49 | 'handle' => 'fr', 50 | 'name' => 'French', 51 | ]); 52 | 53 | $this->assertDatabaseHas('sites', [ 54 | 'handle' => 'de', 55 | 'name' => 'German', 56 | ]); 57 | 58 | $this->assertDatabaseHas('sites', [ 59 | 'handle' => 'es', 60 | 'name' => 'Spanish', 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Data/Fields/BlueprintTest.php: -------------------------------------------------------------------------------- 1 | app->singleton(\Statamic\Fields\BlueprintRepository::class, function () { 20 | return (new \Statamic\Eloquent\Fields\BlueprintRepository) 21 | ->setDirectory(resource_path('blueprints')); 22 | }); 23 | 24 | $this->app->singleton( 25 | 'Statamic\Fields\FieldsetRepository', 26 | 'Statamic\Eloquent\Fields\FieldsetRepository' 27 | ); 28 | 29 | $this->app->bind('statamic.eloquent.blueprints.blueprint_model', function () { 30 | return \Statamic\Eloquent\Fields\BlueprintModel::class; 31 | }); 32 | 33 | $this->app->bind('statamic.eloquent.blueprints.fieldset_model', function () { 34 | return \Statamic\Eloquent\Fields\FieldsetModel::class; 35 | }); 36 | } 37 | 38 | #[Test] 39 | public function it_saves_and_removes_hidden_on_model() 40 | { 41 | $blueprint = Blueprint::make() 42 | ->setHandle('test') 43 | ->setHidden(true) 44 | ->save(); 45 | 46 | $model = Blueprint::getModel($blueprint); 47 | 48 | $this->assertTrue($model->data['hide']); 49 | 50 | $blueprint->setHidden(false)->save(); 51 | 52 | $model = Blueprint::getModel($blueprint); 53 | 54 | $this->assertFalse(isset($model->data['hide'])); 55 | } 56 | 57 | #[Test] 58 | public function it_deletes_the_model_when_the_blueprint_is_deleted() 59 | { 60 | $blueprint = Blueprint::make() 61 | ->setHandle('test') 62 | ->setHidden(true) 63 | ->save(); 64 | 65 | $model = Blueprint::getModel($blueprint); 66 | 67 | $this->assertNotNull($model); 68 | 69 | $blueprint->delete(); 70 | 71 | $model = Blueprint::getModel($blueprint); 72 | 73 | $this->assertNull($model); 74 | } 75 | 76 | #[Test] 77 | public function it_uses_file_based_namespaces() 78 | { 79 | config()->set('statamic.eloquent-driver.blueprints.namespaces', ['collections']); 80 | 81 | $this->assertCount(1, BlueprintModel::all()); 82 | 83 | $blueprint = Blueprint::make() 84 | ->setNamespace('forms') 85 | ->setHandle('test') 86 | ->setHidden(true) 87 | ->save(); 88 | 89 | $this->assertCount(1, BlueprintModel::all()); // we check theres no new database entries, ie its been handled by files 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Data/Fields/FieldsetTest.php: -------------------------------------------------------------------------------- 1 | app->singleton( 19 | 'Statamic\Fields\FieldsetRepository', 20 | 'Statamic\Eloquent\Fields\FieldsetRepository' 21 | ); 22 | 23 | $this->app->bind('statamic.eloquent.fieldsets.model', function () { 24 | return \Statamic\Eloquent\Fields\FieldsetModel::class; 25 | }); 26 | } 27 | 28 | #[Test] 29 | public function it_handles_fieldsets_registered_by_addons() 30 | { 31 | $this->assertCount(0, Fieldset::all()); 32 | 33 | Fieldset::addNamespace( 34 | 'my-addon', 35 | directory: __DIR__.'/../../__fixtures__/resources/fieldsets' 36 | ); 37 | 38 | $this->assertCount(1, Fieldset::all()); 39 | $this->assertSame('my-addon::seo', Fieldset::all()->first()->handle()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Data/Globals/GlobalSetTest.php: -------------------------------------------------------------------------------- 1 | set('statamic.system.multisite', false); 15 | 16 | $this->setSites([ 17 | 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], 18 | ]); 19 | 20 | $set = (new GlobalSet)->title('The title'); 21 | 22 | $variables = $set->makeLocalization('en')->data([ 23 | 'array' => ['first one', 'second one'], 24 | 'string' => 'The string', 25 | ]); 26 | 27 | $set->addLocalization($variables); 28 | 29 | $expected = <<<'EOT' 30 | title: 'The title' 31 | data: 32 | array: 33 | - 'first one' 34 | - 'second one' 35 | string: 'The string' 36 | 37 | EOT; 38 | $this->assertEquals($expected, $set->fileContents()); 39 | } 40 | 41 | #[Test] 42 | public function it_gets_file_contents_for_saving_with_multiple_sites() 43 | { 44 | config()->set('statamic.system.multisite', true); 45 | 46 | $this->setSites([ 47 | 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], 48 | 'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'], 49 | 'de' => ['name' => 'German', 'locale' => 'de_DE', 'url' => 'http://test.com/de/'], 50 | ]); 51 | 52 | $set = (new GlobalSet)->title('The title'); 53 | 54 | // We set the data but it's basically irrelevant since it won't get saved to this file. 55 | $set->in('en', function ($loc) { 56 | $loc->data([ 57 | 'array' => ['first one', 'second one'], 58 | 'string' => 'The string', 59 | ]); 60 | }); 61 | $set->in('fr', function ($loc) { 62 | $loc->data([ 63 | 'array' => ['le first one', 'le second one'], 64 | 'string' => 'Le string', 65 | ]); 66 | }); 67 | 68 | $expected = <<<'EOT' 69 | title: 'The title' 70 | 71 | EOT; 72 | $this->assertEquals($expected, $set->fileContents()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Entries/EntryModelTest.php: -------------------------------------------------------------------------------- 1 | 'the-slug', 16 | 'data' => [ 17 | 'foo' => 'bar', 18 | ], 19 | ]); 20 | 21 | $this->assertEquals('the-slug', $model->slug); 22 | $this->assertEquals('bar', $model->foo); 23 | $this->assertEquals(['foo' => 'bar'], $model->data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Factories/EntryFactory.php: -------------------------------------------------------------------------------- 1 | reset(); 32 | } 33 | 34 | public function id($id) 35 | { 36 | $this->id = $id; 37 | 38 | return $this; 39 | } 40 | 41 | public function slug($slug) 42 | { 43 | $this->slug = $slug; 44 | 45 | return $this; 46 | } 47 | 48 | public function collection($collection) 49 | { 50 | $this->collection = $collection; 51 | 52 | return $this; 53 | } 54 | 55 | public function data($data) 56 | { 57 | $this->data = $data; 58 | 59 | return $this; 60 | } 61 | 62 | public function date($date) 63 | { 64 | $this->date = $date; 65 | 66 | return $this; 67 | } 68 | 69 | public function published($published) 70 | { 71 | $this->published = $published; 72 | 73 | return $this; 74 | } 75 | 76 | public function locale($locale) 77 | { 78 | $this->locale = $locale; 79 | 80 | return $this; 81 | } 82 | 83 | public function origin($origin) 84 | { 85 | $this->origin = $origin; 86 | 87 | return $this; 88 | } 89 | 90 | public function make() 91 | { 92 | $collection = $this->createCollection(); 93 | 94 | $entry = Entry::make() 95 | ->locale($this->locale) 96 | ->collection($collection) 97 | ->blueprint($collection->entryBlueprint()) 98 | ->slug($this->slug) 99 | ->data($this->data) 100 | ->origin($this->origin) 101 | ->published($this->published); 102 | 103 | if ($this->date) { 104 | $entry->date($this->date); 105 | } 106 | 107 | if ($this->id) { 108 | $entry->id($this->id); 109 | } 110 | 111 | $this->reset(); 112 | 113 | return $entry; 114 | } 115 | 116 | public function create() 117 | { 118 | return tap($this->make())->save(); 119 | } 120 | 121 | protected function createCollection() 122 | { 123 | if ($this->collection instanceof StatamicCollection) { 124 | return $this->collection; 125 | } 126 | 127 | return Collection::findByHandle($this->collection) 128 | ?? Collection::make($this->collection) 129 | ->sites(['en']) 130 | ->save(); 131 | } 132 | 133 | private function reset() 134 | { 135 | $this->id = null; 136 | $this->slug = null; 137 | $this->data = []; 138 | $this->date = null; 139 | $this->published = true; 140 | $this->order = null; 141 | $this->locale = 'en'; 142 | $this->origin = null; 143 | $this->collection = null; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tests/Factories/FieldsetFactory.php: -------------------------------------------------------------------------------- 1 | contents = $contents; 14 | 15 | return $this; 16 | } 17 | 18 | public function withSections($sections) 19 | { 20 | if (! $this->contents) { 21 | $this->contents = []; 22 | } 23 | 24 | $this->contents['sections'] = $sections; 25 | 26 | return $this; 27 | } 28 | 29 | public function withFieldtypes($fieldtypes) 30 | { 31 | foreach ($fieldtypes as $name => $fieldtype) { 32 | $this->fieldtypes[$name] = $fieldtype; 33 | } 34 | 35 | return $this; 36 | } 37 | 38 | public function withFields($fields) 39 | { 40 | if (! $this->contents) { 41 | $this->contents = []; 42 | } 43 | 44 | $this->contents['fields'] = $fields; 45 | 46 | return $this; 47 | } 48 | 49 | public function create() 50 | { 51 | return tap(new Fieldset) 52 | ->contents($this->contents); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Factories/GlobalFactory.php: -------------------------------------------------------------------------------- 1 | id = $id; 18 | 19 | return $this; 20 | } 21 | 22 | public function handle($handle) 23 | { 24 | $this->handle = $handle; 25 | 26 | return $this; 27 | } 28 | 29 | public function data($data) 30 | { 31 | $this->data = $data; 32 | 33 | return $this; 34 | } 35 | 36 | public function make() 37 | { 38 | $set = GlobalSet::make($this->handle); 39 | 40 | $set->addLocalization( 41 | $set->makeLocalization('en')->data($this->data) 42 | ); 43 | 44 | if ($this->id) { 45 | $set->id($this->id); 46 | } 47 | 48 | return $set; 49 | } 50 | 51 | public function create() 52 | { 53 | return tap($this->make())->save(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Forms/FormTest.php: -------------------------------------------------------------------------------- 1 | title('Test form')->save(); 16 | 17 | $form = Facades\Form::find('test'); 18 | 19 | $this->assertSame(Facades\Blink::get('eloquent-forms-test'), $form); 20 | } 21 | 22 | #[Test] 23 | public function getting_all_forms_sets_the_blink_cache() 24 | { 25 | $form = tap(Facades\Form::make('test')->title('Test form'))->save(); 26 | 27 | Facades\Form::all(); 28 | 29 | $this->assertCount(1, Facades\Blink::get('eloquent-forms')); 30 | $this->assertSame($form->handle(), Facades\Blink::get('eloquent-forms')->first()->handle()); 31 | } 32 | 33 | #[Test] 34 | public function saving_a_form_removes_the_blink_cache() 35 | { 36 | Facades\Form::make('test')->title('Test form')->save(); 37 | 38 | $form = Facades\Form::find('test'); 39 | Facades\Form::all(); // to set up eloquent-forms blink 40 | 41 | $this->assertSame(Facades\Blink::get('eloquent-forms-test'), $form); 42 | $this->assertCount(1, Facades\Blink::get('eloquent-forms')); 43 | 44 | $form->save(); 45 | 46 | $this->assertNull(Facades\Blink::get('eloquent-forms-test')); 47 | $this->assertNull(Facades\Blink::get('eloquent-forms')); 48 | } 49 | 50 | #[Test] 51 | public function deleting_a_form_removes_the_blink_cache() 52 | { 53 | Facades\Form::make('test')->title('Test form')->save(); 54 | 55 | $form = Facades\Form::find('test'); 56 | Facades\Form::all(); // to set up eloquent-forms blink 57 | 58 | $this->assertSame(Facades\Blink::get('eloquent-forms-test'), $form); 59 | $this->assertCount(1, Facades\Blink::get('eloquent-forms')); 60 | 61 | $form->delete(); 62 | 63 | $this->assertNull(Facades\Blink::get('eloquent-forms-test')); 64 | $this->assertNull(Facades\Blink::get('eloquent-forms')); 65 | } 66 | 67 | #[Test] 68 | public function it_stores_form_data() 69 | { 70 | $form = tap(Facades\Form::make('test')->title('Test form')->data(['some' => 'data']))->save(); 71 | 72 | $this->assertSame(['some' => 'data'], Arr::get($form->model(), 'settings.data')); 73 | } 74 | 75 | #[Test] 76 | public function null_values_are_removed_from_data() 77 | { 78 | $form = tap(Facades\Form::make('test')->title('Test form')->data(['some' => 'data', 'null_value' => null]))->save(); 79 | 80 | $this->assertSame(['some' => 'data'], Arr::get($form->model(), 'settings.data')); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Globals/GlobalsetTest.php: -------------------------------------------------------------------------------- 1 | addLocalization( 22 | $global->makeLocalization('en')->data(['foo' => 'bar', 'baz' => 'qux']) 23 | ); 24 | 25 | $global->save(); 26 | 27 | Event::assertDispatched(GlobalSetSaved::class); 28 | Event::assertDispatched(GlobalVariablesSaved::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Repositories/AssetContainerRepositoryTest.php: -------------------------------------------------------------------------------- 1 | sites(['en', 'fr']); 23 | $this->directory = __DIR__.'/../__fixtures__/content/assets'; 24 | $this->repo = new AssetContainerRepository($stache); 25 | 26 | $this->repo->make('another')->title('Another Asset Container')->disk('local')->save(); 27 | $this->repo->make('main')->title('Main Assets')->disk('local')->save(); 28 | } 29 | 30 | #[Test] 31 | public function it_gets_all_asset_containers() 32 | { 33 | $containers = $this->repo->all(); 34 | 35 | $this->assertInstanceOf(IlluminateCollection::class, $containers); 36 | $this->assertCount(2, $containers); 37 | $this->assertEveryItemIsInstanceOf(AssetContainer::class, $containers); 38 | 39 | $ordered = $containers->sortBy->handle()->values(); 40 | $this->assertEquals(['another', 'main'], $ordered->map->id()->all()); 41 | $this->assertEquals(['another', 'main'], $ordered->map->handle()->all()); 42 | $this->assertEquals(['Another Asset Container', 'Main Assets'], $ordered->map->title()->all()); 43 | } 44 | 45 | #[Test] 46 | public function it_gets_an_asset_container_by_handle() 47 | { 48 | tap($this->repo->findByHandle('main'), function ($container) { 49 | $this->assertInstanceOf(AssetContainer::class, $container); 50 | $this->assertEquals('main', $container->id()); 51 | $this->assertEquals('main', $container->handle()); 52 | $this->assertEquals('Main Assets', $container->title()); 53 | }); 54 | 55 | tap($this->repo->findByHandle('another'), function ($container) { 56 | $this->assertInstanceOf(AssetContainer::class, $container); 57 | $this->assertEquals('another', $container->id()); 58 | $this->assertEquals('another', $container->handle()); 59 | $this->assertEquals('Another Asset Container', $container->title()); 60 | }); 61 | 62 | $this->assertNull($this->repo->findByHandle('unknown')); 63 | } 64 | 65 | #[Test] 66 | public function it_saves_a_container_to_the_database() 67 | { 68 | $container = Facades\AssetContainer::make('new'); 69 | $this->assertNull($this->repo->findByHandle('new')); 70 | 71 | $this->repo->save($container); 72 | 73 | $this->assertNotNull($item = $this->repo->findByHandle('new')); 74 | $this->assertEquals($container, $item); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Repositories/CollectionRepositoryTest.php: -------------------------------------------------------------------------------- 1 | sites(['en', 'fr']); 23 | $this->app->instance(Stache::class, $stache); 24 | $this->repo = new CollectionRepository($stache); 25 | 26 | $this->repo->make('alphabetical')->title('Alphabetical')->routes('alphabetical/{slug}')->save(); 27 | $this->repo->make('blog')->title('Blog')->dated(true)->taxonomies(['tags'])->save(); 28 | $this->repo->make('numeric')->title('Numeric')->routes('numeric/{slug}')->save(); 29 | $this->repo->make('pages')->title('Pages')->routes('{parent_uri}/{slug}')->structureContents(['root' => true])->save(); 30 | } 31 | 32 | #[Test] 33 | public function it_gets_all_collections() 34 | { 35 | $collections = $this->repo->all(); 36 | 37 | $this->assertInstanceOf(IlluminateCollection::class, $collections); 38 | $this->assertCount(4, $collections); 39 | $this->assertEveryItemIsInstanceOf(Collection::class, $collections); 40 | 41 | $ordered = $collections->sortBy->handle()->values(); 42 | $this->assertEquals(['alphabetical', 'blog', 'numeric', 'pages'], $ordered->map->handle()->all()); 43 | $this->assertEquals(['Alphabetical', 'Blog', 'Numeric', 'Pages'], $ordered->map->title()->all()); 44 | } 45 | 46 | #[Test] 47 | public function it_gets_a_collection_by_handle() 48 | { 49 | tap($this->repo->findByHandle('alphabetical'), function ($collection) { 50 | $this->assertInstanceOf(Collection::class, $collection); 51 | $this->assertEquals('alphabetical', $collection->handle()); 52 | $this->assertEquals('Alphabetical', $collection->title()); 53 | }); 54 | 55 | tap($this->repo->findByHandle('blog'), function ($collection) { 56 | $this->assertInstanceOf(Collection::class, $collection); 57 | $this->assertEquals('blog', $collection->handle()); 58 | $this->assertEquals('Blog', $collection->title()); 59 | }); 60 | 61 | tap($this->repo->findByHandle('numeric'), function ($collection) { 62 | $this->assertInstanceOf(Collection::class, $collection); 63 | $this->assertEquals('numeric', $collection->handle()); 64 | $this->assertEquals('Numeric', $collection->title()); 65 | }); 66 | 67 | tap($this->repo->findByHandle('pages'), function ($collection) { 68 | $this->assertInstanceOf(Collection::class, $collection); 69 | $this->assertEquals('pages', $collection->handle()); 70 | $this->assertEquals('Pages', $collection->title()); 71 | }); 72 | 73 | $this->assertNull($this->repo->findByHandle('unknown')); 74 | } 75 | 76 | #[Test] 77 | public function it_saves_a_collection_to_the_database() 78 | { 79 | $collection = CollectionAPI::make('new'); 80 | $collection->cascade(['foo' => 'bar']); 81 | $this->assertNull($this->repo->findByHandle('new')); 82 | 83 | $this->repo->save($collection); 84 | 85 | $this->assertNotNull($item = $this->repo->findByHandle('new')); 86 | $this->assertEquals(['foo' => 'bar'], $item->cascade()->all()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Repositories/NavigationRepositoryTest.php: -------------------------------------------------------------------------------- 1 | sites(['en']); 23 | $this->app->instance(Stache::class, $stache); 24 | 25 | $this->repo = new NavigationRepository($stache); 26 | 27 | $this->repo->make('footer')->title('Footer')->expectsRoot(true)->save(); 28 | $sidebar = tap($this->repo->make('sidebar')->title('Sidebar'))->save(); 29 | $sidebar->makeTree('en', [['entry' => 'pages-contact'], ['entry' => 'pages-contact']])->save(); 30 | } 31 | 32 | #[Test] 33 | public function it_gets_all_navs() 34 | { 35 | $navs = $this->repo->all(); 36 | 37 | $this->assertInstanceOf(Collection::class, $navs); 38 | $this->assertCount(2, $navs); 39 | $this->assertEveryItemIsInstanceOf(Structure::class, $navs); 40 | 41 | $ordered = $navs->sortBy->handle()->values(); 42 | $this->assertEquals(['footer', 'sidebar'], $ordered->map->handle()->all()); 43 | $this->assertEquals(['Footer', 'Sidebar'], $ordered->map->title()->all()); 44 | } 45 | 46 | #[Test] 47 | public function it_gets_a_nav_by_handle() 48 | { 49 | tap($this->repo->findByHandle('sidebar'), function ($nav) { 50 | $this->assertInstanceOf(Structure::class, $nav); 51 | $this->assertEquals('sidebar', $nav->handle()); 52 | $this->assertEquals('Sidebar', $nav->title()); 53 | }); 54 | 55 | tap($this->repo->findByHandle('footer'), function ($nav) { 56 | $this->assertInstanceOf(Structure::class, $nav); 57 | $this->assertEquals('footer', $nav->handle()); 58 | $this->assertEquals('Footer', $nav->title()); 59 | }); 60 | 61 | $this->assertNull($this->repo->findByHandle('unknown')); 62 | } 63 | 64 | #[Test] 65 | public function it_saves_a_nav_to_the_database() 66 | { 67 | $structure = (new Nav)->handle('new'); 68 | 69 | $this->assertNull($this->repo->findByHandle('new')); 70 | 71 | $this->repo->save($structure); 72 | 73 | $this->assertNotNull($this->repo->findByHandle('new')); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Repositories/TokenRepositoryTest.php: -------------------------------------------------------------------------------- 1 | repo = new TokenRepository; 21 | 22 | $this->repo->make('abc', 'ExampleHandler', ['foo' => 'bar'])->save(); 23 | } 24 | 25 | #[Test] 26 | public function it_gets_a_token() 27 | { 28 | tap($this->repo->find('abc'), function ($token) { 29 | $this->assertInstanceOf(Token::class, $token); 30 | $this->assertEquals('abc', $token->token()); 31 | $this->assertEquals('ExampleHandler', $token->handler()); 32 | $this->assertEquals(['foo' => 'bar'], $token->data()->all()); 33 | $this->assertInstanceOf(Carbon::class, $token->expiry()); 34 | }); 35 | 36 | $this->assertNull($this->repo->find('unknown')); 37 | } 38 | 39 | #[Test] 40 | public function it_saves_a_token_to_the_database() 41 | { 42 | $token = $this->repo->make('new', 'ExampleHandler', ['foo' => 'bar']); 43 | 44 | $this->assertNull($this->repo->find('new')); 45 | 46 | $this->repo->save($token); 47 | 48 | $this->assertNotNull($item = $this->repo->find('new')); 49 | $this->assertEquals(['foo' => 'bar'], $item->data()->all()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Sites/SitesTest.php: -------------------------------------------------------------------------------- 1 | app->bind('statamic.eloquent.sites.model', function () { 21 | return SiteModel::class; 22 | }); 23 | 24 | $this->app->singleton( 25 | 'Statamic\Sites\Sites', 26 | 'Statamic\Eloquent\Sites\Sites' 27 | ); 28 | 29 | Facade::clearResolvedInstance(\Statamic\Sites\Sites::class); 30 | } 31 | 32 | #[Test] 33 | public function it_saves_sites() 34 | { 35 | $this->assertCount(0, SiteModel::all()); 36 | 37 | $this->setSites([ 38 | 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], 39 | 'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'], 40 | 'es' => ['name' => 'Spanish', 'locale' => 'es_ES', 'url' => 'http://test.com/es/'], 41 | 'de' => ['name' => 'German', 'locale' => 'de_DE', 'url' => 'http://test.com/de/'], 42 | ]); 43 | 44 | Site::save(); 45 | 46 | $this->assertCount(4, Site::all()); 47 | $this->assertCount(4, SiteModel::all()); 48 | } 49 | 50 | #[Test] 51 | public function it_deletes_sites() 52 | { 53 | $this->assertCount(0, SiteModel::all()); 54 | 55 | $this->setSites([ 56 | 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], 57 | 'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'], 58 | 'es' => ['name' => 'Spanish', 'locale' => 'es_ES', 'url' => 'http://test.com/es/'], 59 | 'de' => ['name' => 'German', 'locale' => 'de_DE', 'url' => 'http://test.com/de/'], 60 | ]); 61 | 62 | Site::save(); 63 | 64 | $this->setSites([ 65 | 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], 66 | 'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'], 67 | 'es' => ['name' => 'Spanish', 'locale' => 'es_ES', 'url' => 'http://test.com/es/'], 68 | ]); 69 | 70 | Site::save(); 71 | 72 | $this->assertCount(3, Site::all()); 73 | $this->assertCount(3, SiteModel::all()); 74 | $this->assertSame(['en', 'fr', 'es'], SiteModel::all()->pluck('handle')->all()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('statamic.eloquent-driver', require (__DIR__.'/../config/eloquent-driver.php')); 23 | 24 | collect(config('statamic.eloquent-driver')) 25 | ->filter(fn ($config) => isset($config['driver'])) 26 | ->reject(fn ($config, $key) => $key === 'sites') 27 | ->each(fn ($config, $key) => $app['config']->set("statamic.eloquent-driver.{$key}.driver", 'eloquent')); 28 | } 29 | 30 | protected function getEnvironmentSetUp($app) 31 | { 32 | parent::getEnvironmentSetUp($app); 33 | 34 | // We changed the default sites setup but the tests assume defaults like the following. 35 | Site::setSites([ 36 | 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://localhost/'], 37 | ]); 38 | 39 | $app['config']->set('auth.providers.users.driver', 'statamic'); 40 | $app['config']->set('statamic.stache.watcher', false); 41 | $app['config']->set('statamic.stache.stores.users', [ 42 | 'class' => \Statamic\Stache\Stores\UsersStore::class, 43 | 'directory' => __DIR__.'/__fixtures__/users', 44 | ]); 45 | 46 | $app['config']->set('statamic.editions.pro', true); 47 | 48 | $app['config']->set('cache.stores.outpost', [ 49 | 'driver' => 'file', 50 | 'path' => storage_path('framework/cache/outpost-data'), 51 | ]); 52 | } 53 | 54 | protected function assertEveryItem($items, $callback) 55 | { 56 | if ($items instanceof \Illuminate\Support\Collection) { 57 | $items = $items->all(); 58 | } 59 | 60 | $passes = 0; 61 | 62 | foreach ($items as $item) { 63 | if ($callback($item)) { 64 | $passes++; 65 | } 66 | } 67 | 68 | $this->assertEquals(count($items), $passes, 'Failed asserting that every item passes.'); 69 | } 70 | 71 | protected function assertEveryItemIsInstanceOf($class, $items) 72 | { 73 | if ($items instanceof \Illuminate\Support\Collection) { 74 | $items = $items->all(); 75 | } 76 | 77 | $matches = 0; 78 | 79 | foreach ($items as $item) { 80 | if ($item instanceof $class) { 81 | $matches++; 82 | } 83 | } 84 | 85 | $this->assertEquals(count($items), $matches, 'Failed asserting that every item is an instance of '.$class); 86 | } 87 | 88 | protected function isUsingSqlite() 89 | { 90 | $connection = config('database.default'); 91 | 92 | return config("database.connections.{$connection}.driver") === 'sqlite'; 93 | } 94 | 95 | /** 96 | * Define database migrations. 97 | * 98 | * @return void 99 | */ 100 | protected function defineDatabaseMigrations() 101 | { 102 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 103 | 104 | $this->shouldUseStringEntryIds 105 | ? $this->loadMigrationsFrom(__DIR__.'/../database/migrations/entries/2024_03_07_100000_create_entries_table_with_string_ids.php') 106 | : $this->loadMigrationsFrom(__DIR__.'/../database/migrations/entries/2024_03_07_100000_create_entries_table.php'); 107 | } 108 | 109 | protected function setSites($sites) 110 | { 111 | Site::setSites($sites); 112 | 113 | config()->set('statamic.system.multisite', Site::hasMultiple()); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/__fixtures__/dev-null/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/statamic/eloquent-driver/b64654ecaf369efd2300ac94ef1444fd4d78ca68/tests/__fixtures__/dev-null/.gitkeep -------------------------------------------------------------------------------- /tests/__fixtures__/resources/fieldsets/seo.yaml: -------------------------------------------------------------------------------- 1 | title: 'Advanced SEO (Sitemap)' 2 | fields: 3 | - 4 | handle: seo_section_sitemap 5 | field: 6 | type: advanced_seo 7 | display: 'Sitemap' 8 | field: seo_section_sitemap 9 | visibility: hidden 10 | - 11 | handle: seo_sitemap_enabled 12 | field: 13 | type: advanced_seo 14 | display: 'Enabled' 15 | field: seo_sitemap_enabled 16 | visibility: hidden 17 | - 18 | handle: seo_sitemap_priority 19 | field: 20 | type: advanced_seo 21 | display: 'Priority' 22 | field: seo_sitemap_priority 23 | visibility: hidden 24 | - 25 | handle: seo_sitemap_change_frequency 26 | field: 27 | type: advanced_seo 28 | display: 'Change Frequency' 29 | field: seo_sitemap_change_frequency 30 | visibility: hidden 31 | --------------------------------------------------------------------------------