diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6537ca46 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a92c411..594f727a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,8 +12,8 @@ jobs: strategy: matrix: - php: [7.4, 7.3] - laravel: [8.*] + php: [8.0, 8.1] + laravel: [8.*, 9.*] dependency-version: [prefer-lowest, prefer-stable] name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} @@ -23,12 +23,16 @@ jobs: uses: actions/checkout@v1 - name: Setup PHP - uses: shivammathur/setup-php@v1 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extension-csv: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick coverage: none + - name: Set PHP 8.1 Testbench + run: composer require "orchestra/testbench ^6.22.0" --no-interaction --no-update + if: matrix.laravel == '8.*' && matrix.php >= 8.1 + - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update diff --git a/.gitignore b/.gitignore index e96516be..2d046d18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ composer.lock vendor .phpunit.result.cache +.php_cs.cache diff --git a/README.md b/README.md index fdeb4db3..c706aab7 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ This package provides support for storing your Statamic data in a database rather than the filesystem. -This driver currently supports entries but not taxonomies, navigations, globals, or form submissions. We'll be working on those in the future. - ## Installation Install using Composer: @@ -26,6 +24,7 @@ If you're starting from scratch, we can use traditional incrementing integers fo - Delete `content/collections/pages/home.md` - Change the structure `tree` in `content/collections/pages.yaml` to `{}`. +- Run `php artisan vendor:publish --provider="Statamic\Eloquent\ServiceProvider" --tag=migrations"`. - Run `php artisan vendor:publish --tag="statamic-eloquent-entries-table"`. - Run `php artisan migrate`. @@ -33,11 +32,35 @@ If you're starting from scratch, we can use traditional incrementing integers fo If you're planning to use existing content, we can use the existing UUIDs. This will prevent you from needing to update any data or relationships. -- In the `config/statamic/eloquent-driver.php` file, change `model` to `UuidEntryModel`. +- In the `config/statamic/eloquent-driver.php` file, change `model` to `\Statamic\Eloquent\Entries\UuidEntryModel`. +- Run `php artisan vendor:publish --provider="Statamic\Eloquent\ServiceProvider" --tag=migrations"`. - Run `php artisan vendor:publish --tag="statamic-eloquent-entries-table-with-string-ids"`. - Run `php artisan migrate`. -- Import entries into database with `php please eloquent:import-entries`. + +## Configuration + +The configuration file (`statamic.eloquent-driver`) allows you to choose which repositories you want to be driven by eloquent. By default, all are selected, but if you want to opt out simply change `driver` from `eloquent` to `file` for that repository. + +You may also specify your own models for each repository, should you wish to use something different from the one provided. + +## Importing existing file based content + +We have provided imports from file based content for each repository, which can be run as follows: + +- Assets: `php please eloquent:import-assets` +- Blueprints and Fieldsets: `php please eloquent:import-blueprints` +- Collections: `php please eloquent:import-collections` +- Entries: `php please eloquent:import-entries` +- Forms: `php please eloquent:import-forms` +- Globals: `php please eloquent:import-globals` +- Navs: `php please eloquent:import-navs` +- Revisions: `php please eloquent:import-revisions` +- Taxonomies: `php please eloquent:import-taxonomies` ## Storing Users in a Database -Statamic has a[ built-in users eloquent driver](https://statamic.dev/tips/storing-users-in-a-database) if you'd like to cross that bridge too. +Statamic has a [built-in users eloquent driver](https://statamic.dev/tips/storing-users-in-a-database) if you'd like to cross that bridge too. + +## Mixed driver entries and collections + +This driver **does not** make it possible to have some collections/entries file driven and some eloquent driven. If that is your requirement you may want to look into using [Runway](https://statamic.com/addons/duncanmcclean/runway). diff --git a/composer.json b/composer.json index e3c61052..5a0b78b7 100644 --- a/composer.json +++ b/composer.json @@ -23,11 +23,17 @@ } }, "require": { - "statamic/cms": "^3.0" + "php": "^8.0", + "statamic/cms": "^3.3.31" }, "require-dev": { + "doctrine/dbal": "^3.3", + "orchestra/testbench": "^6.7.0 || ^7.0", "phpunit/phpunit": "^9.4" }, + "scripts": { + "test": "phpunit" + }, "config": { "allow-plugins": { "pixelfear/composer-dist-plugin": true diff --git a/config/eloquent-driver.php b/config/eloquent-driver.php index 98b04a26..9a600aa1 100644 --- a/config/eloquent-driver.php +++ b/config/eloquent-driver.php @@ -2,8 +2,65 @@ return [ + 'connection' => env('STATAMIC_ELOQUENT_CONNECTION', ''), + 'table_prefix' => env('STATAMIC_ELOQUENT_PREFIX', ''), + + 'assets' => [ + 'driver' => 'eloquent', + 'container_model' => \Statamic\Eloquent\Assets\AssetContainerModel::class, + 'model' => \Statamic\Eloquent\Assets\AssetModel::class, + ], + + 'blueprints' => [ + 'driver' => 'eloquent', + 'blueprint_model' => \Statamic\Eloquent\Fields\BlueprintModel::class, + 'fieldset_model' => \Statamic\Eloquent\Fields\FieldsetModel::class, + ], + + 'collections' => [ + 'driver' => 'eloquent', + 'model' => \Statamic\Eloquent\Collections\CollectionModel::class, + 'tree' => \Statamic\Eloquent\Structures\CollectionTree::class, + 'tree_model' => \Statamic\Eloquent\Structures\TreeModel::class, + ], + 'entries' => [ + 'driver' => 'eloquent', 'model' => \Statamic\Eloquent\Entries\EntryModel::class, + 'entry' => \Statamic\Eloquent\Entries\Entry::class, + ], + + 'forms' => [ + 'driver' => 'eloquent', + 'model' => \Statamic\Eloquent\Forms\FormModel::class, + 'submission_model' => \Statamic\Eloquent\Forms\SubmissionModel::class, ], + 'global_sets' => [ + 'driver' => 'eloquent', + 'model' => \Statamic\Eloquent\Globals\GlobalSetModel::class, + 'variables_model' => \Statamic\Eloquent\Globals\VariablesModel::class, + ], + + 'navigations' => [ + 'driver' => 'eloquent', + 'model' => \Statamic\Eloquent\Structures\NavModel::class, + 'tree' => \Statamic\Eloquent\Structures\NavTree::class, + 'tree_model' => \Statamic\Eloquent\Structures\TreeModel::class, + ], + + 'revisions' => [ + 'driver' => 'eloquent', + 'model' => \Statamic\Eloquent\Revisions\RevisionModel::class, + ], + + 'taxonomies' => [ + 'driver' => 'eloquent', + 'model' => \Statamic\Eloquent\Taxonomies\TaxonomyModel::class, + ], + + 'terms' => [ + 'driver' => 'eloquent', + 'model' => \Statamic\Eloquent\Taxonomies\TermModel::class, + ], ]; diff --git a/database/migrations/create_asset_containers_table.php.stub b/database/migrations/create_asset_containers_table.php.stub new file mode 100644 index 00000000..ba1de19e --- /dev/null +++ b/database/migrations/create_asset_containers_table.php.stub @@ -0,0 +1,24 @@ +prefix('asset_containers'), function (Blueprint $table) { + $table->id(); + $table->string('handle')->unique(); + $table->string('title'); + $table->string('disk'); + $table->json('settings')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('asset_containers')); + } +}; diff --git a/database/migrations/create_asset_table.php.stub b/database/migrations/create_asset_table.php.stub new file mode 100644 index 00000000..eaedcc94 --- /dev/null +++ b/database/migrations/create_asset_table.php.stub @@ -0,0 +1,23 @@ +prefix('assets_meta'), function (Blueprint $table) { + $table->id(); + $table->string('handle')->index(); + $table->json('data')->nullable(); + $table->timestamps(); + }); + } + + + public function down() + { + Schema::dropIfExists($this->prefix('assets_meta')); + } +}; diff --git a/database/migrations/create_blueprints_table.php.stub b/database/migrations/create_blueprints_table.php.stub new file mode 100644 index 00000000..c2bf0635 --- /dev/null +++ b/database/migrations/create_blueprints_table.php.stub @@ -0,0 +1,73 @@ +prefix('blueprints'), function (Blueprint $table) { + $table->id(); + $table->string('namespace')->nullable()->default(null)->index(); + $table->string('handle'); + $table->json('data'); + $table->timestamps(); + + $table->unique(['handle', 'namespace']); + }); + + $this->seedDefaultBlueprint(); + } + + public function down() + { + Schema::dropIfExists($this->prefix('blueprints')); + } + + public function seedDefaultBlueprint() + { + try { + $config = json_encode([ + 'fields' => [ + [ + 'field' => [ + 'type' => 'markdown', + 'display' => 'Content', + 'localizable' => true, + ], + 'handle' => 'content', + ], + [ + 'field' => [ + 'type' => 'users', + 'display' => 'Author', + 'default' => 'current', + 'localizable' => true, + 'max_items' => 1, + ], + 'handle' => 'author', + ], + [ + 'field' => [ + 'type' => 'template', + 'display' => 'Template', + 'localizable' => true, + ], + 'handle' => 'template', + ], + ], + ]); + } catch (\JsonException $e) { + $config = '[]'; + } + + DB::table($this->prefix('blueprints'))->insert([ + 'namespace' => null, + 'handle' => 'default', + 'data' => $config, + 'created_at' => Carbon::now(), + ]); + } +}; diff --git a/database/migrations/create_collections_table.php.stub b/database/migrations/create_collections_table.php.stub new file mode 100644 index 00000000..b39bf13b --- /dev/null +++ b/database/migrations/create_collections_table.php.stub @@ -0,0 +1,23 @@ +prefix('collections'), function (Blueprint $table) { + $table->id(); + $table->string('handle')->unique(); + $table->string('title'); + $table->json('settings')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('collections')); + } +}; diff --git a/database/migrations/create_entries_table.php b/database/migrations/create_entries_table.php deleted file mode 100644 index 451f0c96..00000000 --- a/database/migrations/create_entries_table.php +++ /dev/null @@ -1,40 +0,0 @@ -increments('id'); - $table->string('site'); - $table->unsignedInteger('origin_id')->nullable(); - $table->boolean('published')->default(true); - $table->string('status'); - $table->string('slug'); - $table->string('uri')->nullable(); - $table->string('date')->nullable(); - $table->string('collection'); - $table->json('data'); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('entries'); - } -} diff --git a/database/migrations/create_entries_table.php.stub b/database/migrations/create_entries_table.php.stub new file mode 100644 index 00000000..850ebb44 --- /dev/null +++ b/database/migrations/create_entries_table.php.stub @@ -0,0 +1,34 @@ +prefix('entries'), function (Blueprint $table) { + $table->id(); + $table->string('site')->index(); + $table->unsignedBigInteger('origin_id')->nullable(); + $table->boolean('published')->default(true); + $table->string('status'); + $table->string('slug')->nullable(); + $table->string('uri')->nullable()->index(); + $table->string('date')->nullable(); + $table->string('collection')->index(); + $table->json('data'); + $table->timestamps(); + + $table->foreign('origin_id') + ->references('id') + ->on($this->prefix('entries')) + ->onDelete('set null'); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('entries')); + } +}; diff --git a/database/migrations/create_entries_table_with_string_ids.php b/database/migrations/create_entries_table_with_string_ids.php deleted file mode 100644 index 57156fb2..00000000 --- a/database/migrations/create_entries_table_with_string_ids.php +++ /dev/null @@ -1,40 +0,0 @@ -string('id'); - $table->string('site'); - $table->string('origin_id')->nullable(); - $table->boolean('published')->default(true); - $table->string('status'); - $table->string('slug'); - $table->string('uri')->nullable(); - $table->string('date')->nullable(); - $table->string('collection'); - $table->json('data'); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('entries'); - } -} diff --git a/database/migrations/create_entries_table_with_string_ids.php.stub b/database/migrations/create_entries_table_with_string_ids.php.stub new file mode 100644 index 00000000..c14c990c --- /dev/null +++ b/database/migrations/create_entries_table_with_string_ids.php.stub @@ -0,0 +1,35 @@ +prefix('entries'), function (Blueprint $table) { + $table->uuid('id'); + $table->string('site')->index(); + $table->uuid('origin_id')->nullable(); + $table->boolean('published')->default(true); + $table->string('status'); + $table->string('slug')->nullable(); + $table->string('uri')->nullable()->index(); + $table->string('date')->nullable(); + $table->string('collection')->index(); + $table->json('data'); + $table->timestamps(); + + $table->primary('id'); + $table->foreign('origin_id') + ->references('id') + ->on($this->prefix('entries')) + ->onDelete('set null'); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('entries')); + } +}; diff --git a/database/migrations/create_fieldsets_table.php.stub b/database/migrations/create_fieldsets_table.php.stub new file mode 100644 index 00000000..48646398 --- /dev/null +++ b/database/migrations/create_fieldsets_table.php.stub @@ -0,0 +1,22 @@ +prefix('fieldsets'), function (Blueprint $table) { + $table->id(); + $table->string('handle')->unique(); + $table->json('data'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('fieldsets')); + } +}; diff --git a/database/migrations/create_form_submissions_table.php.stub b/database/migrations/create_form_submissions_table.php.stub new file mode 100644 index 00000000..e9b6ba55 --- /dev/null +++ b/database/migrations/create_form_submissions_table.php.stub @@ -0,0 +1,24 @@ +prefix('form_submissions'), function (Blueprint $table) { + $table->id(); + $table->foreignId('form_id')->constrained($this->prefix('forms'))->cascadeOnDelete(); + $table->json('data')->nullable(); + $table->timestamps(6); + + $table->unique(['form_id', 'created_at']); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('form_submissions')); + } +}; diff --git a/database/migrations/create_forms_table.php.stub b/database/migrations/create_forms_table.php.stub new file mode 100644 index 00000000..e58e1f38 --- /dev/null +++ b/database/migrations/create_forms_table.php.stub @@ -0,0 +1,23 @@ +prefix('forms'), function (Blueprint $table) { + $table->id(); + $table->string('handle')->unique(); + $table->string('title'); + $table->json('settings')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('forms')); + } +}; diff --git a/database/migrations/create_globals_table.php.stub b/database/migrations/create_globals_table.php.stub new file mode 100644 index 00000000..d31ba7cc --- /dev/null +++ b/database/migrations/create_globals_table.php.stub @@ -0,0 +1,23 @@ +prefix('global_sets'), function (Blueprint $table) { + $table->id(); + $table->string('handle')->unique(); + $table->string('title'); + $table->json('localizations'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('global_sets')); + } +}; diff --git a/database/migrations/create_navigation_trees_table.php.stub b/database/migrations/create_navigation_trees_table.php.stub new file mode 100644 index 00000000..5b954249 --- /dev/null +++ b/database/migrations/create_navigation_trees_table.php.stub @@ -0,0 +1,27 @@ +prefix('trees'), function (Blueprint $table) { + $table->id(); + $table->string('handle'); + $table->string('type')->index(); + $table->string('locale')->nullable()->index(); + $table->json('tree')->nullable(); + $table->json('settings')->nullable(); + $table->timestamps(); + + $table->unique(['handle', 'type', 'locale']); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('trees')); + } +}; diff --git a/database/migrations/create_navigations_table.php.stub b/database/migrations/create_navigations_table.php.stub new file mode 100644 index 00000000..8ca6b420 --- /dev/null +++ b/database/migrations/create_navigations_table.php.stub @@ -0,0 +1,23 @@ +prefix('navigations'), function (Blueprint $table) { + $table->id(); + $table->string('handle')->unique(); + $table->string('title'); + $table->json('settings')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('navigations')); + } +}; diff --git a/database/migrations/create_revisions_table.php.stub b/database/migrations/create_revisions_table.php.stub new file mode 100644 index 00000000..8dc84d54 --- /dev/null +++ b/database/migrations/create_revisions_table.php.stub @@ -0,0 +1,28 @@ +prefix('revisions'), function (Blueprint $table) { + $table->id(); + $table->string('key'); + $table->string('action')->index(); + $table->string('user')->nullable(); + $table->string('message')->nullable(); + $table->json('attributes')->nullable(); + $table->timestamps(); + + $table->unique(['key', 'created_at']); + }); + } + + + public function down() + { + Schema::dropIfExists($this->prefix('revisions')); + } +}; diff --git a/database/migrations/create_taxonomies_table.php.stub b/database/migrations/create_taxonomies_table.php.stub new file mode 100644 index 00000000..d19218e3 --- /dev/null +++ b/database/migrations/create_taxonomies_table.php.stub @@ -0,0 +1,29 @@ +prefix('taxonomies'), function (Blueprint $table) { + $table->id(); + $table->string('handle')->unique(); + $table->string('title'); + $table->json('sites')->nullable(); + $table->json('settings')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists($this->prefix('taxonomies')); + } +}; diff --git a/database/migrations/create_terms_table.php.stub b/database/migrations/create_terms_table.php.stub new file mode 100644 index 00000000..e53afe14 --- /dev/null +++ b/database/migrations/create_terms_table.php.stub @@ -0,0 +1,27 @@ +prefix('taxonomy_terms'), function (Blueprint $table) { + $table->id(); + $table->string('site')->index(); + $table->string('slug'); + $table->string('uri')->nullable()->index(); + $table->string('taxonomy')->index(); + $table->json('data'); + $table->timestamps(); + + $table->unique(['slug', 'taxonomy', 'site']); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix('taxonomy_terms')); + } +}; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c0d1eb07..e0cf4ba7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,7 @@ -./tests + + + + + + + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 00000000..a11aab98 --- /dev/null +++ b/pint.json @@ -0,0 +1,20 @@ +{ + "preset": "laravel", + "rules": { + "binary_operator_spaces": { + "default": "single_space", + "operators": { + "=>": null + } + }, + "class_attributes_separation": { + "elements": { + "method": "one" + } + }, + "class_definition": { + "multi_line_extends_each_single_line": true, + "single_item_single_line": true + } + } +} diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php new file mode 100644 index 00000000..b19d97fc --- /dev/null +++ b/src/Assets/Asset.php @@ -0,0 +1,82 @@ +metaValue($key); + } + + if (! config('statamic.assets.cache_meta')) { + return $this->generateMeta(); + } + + if ($this->meta) { + return array_merge($this->meta, ['data' => $this->data->all()]); + } + + return $this->meta = Cache::rememberForever($this->metaCacheKey(), function () { + $handle = $this->container()->handle().'::'.$this->metaPath(); + if ($model = app('statamic.eloquent.assets.model')::where('handle', $handle)->first()) { + return $model->data; + } + + $this->writeMeta($meta = $this->generateMeta()); + + return $meta; + }); + } + + public function exists() + { + $files = Blink::once($this->container()->handle().'::files', function () { + return $this->container()->files(); + }); + + if (! $path = $this->path()) { + return false; + } + + return $files->contains($path); + } + + private function metaValue($key) + { + $value = Arr::get($this->meta(), $key); + + if (! is_null($value)) { + return $value; + } + + Cache::forget($this->metaCacheKey()); + + $this->writeMeta($meta = $this->generateMeta()); + + return Arr::get($meta, $key); + } + + public function writeMeta($meta) + { + $meta['data'] = Arr::removeNullValues($meta['data']); + + $model = app('statamic.eloquent.assets.model')::firstOrNew([ + 'handle' => $this->container()->handle().'::'.$this->metaPath(), + ])->fill(['data' => $meta]); + + // Set initial timestamps. + if (empty($model->created_at) && isset($meta['last_modified'])) { + $model->created_at = $meta['last_modified']; + $model->updated_at = $meta['last_modified']; + } + + $model->save(); + } +} diff --git a/src/Assets/AssetContainer.php b/src/Assets/AssetContainer.php new file mode 100644 index 00000000..c4e9bfb6 --- /dev/null +++ b/src/Assets/AssetContainer.php @@ -0,0 +1,105 @@ +fillFromModel($model); + } + + public function fillFromModel(Model $model) + { + $this + ->title($model->title) + ->handle($model->handle) + ->disk($model->disk ?? config('filesystems.default')) + ->allowUploads($model->settings['allow_uploads'] ?? null) + ->allowDownloading($model->settings['allow_downloading'] ?? null) + ->allowMoving($model->settings['allow_moving'] ?? null) + ->allowRenaming($model->settings['allow_renaming'] ?? null) + ->createFolders($model->settings['create_folders'] ?? null) + ->searchIndex($model->settings['search_index'] ?? null) + ->model($model); + + return $this; + } + + public function toModel() + { + $class = app('statamic.eloquent.assets.container_model'); + + return $class::firstOrNew(['handle' => $this->handle()])->fill([ + 'title' => $this->title(), + 'disk' => $this->diskHandle() ?? config('filesystems.default'), + 'settings' => [ + 'allow_uploads' => $this->allowUploads(), + 'allow_downloading' => $this->allowDownloading(), + 'allow_moving' => $this->allowMoving(), + 'allow_renaming' => $this->allowRenaming(), + 'create_folders' => $this->createFolders(), + 'search_index' => $this->searchIndex(), + ], + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + return $this; + } + + public function save() + { + $model = $this->toModel(); + $model->save(); + + $this->fillFromModel($model->fresh()); + + AssetContainerSaved::dispatch($this); + + return $this; + } + + public function delete() + { + $this->model()->delete(); + + AssetContainerDeleted::dispatch($this); + + return true; + } +} diff --git a/src/Assets/AssetContainerModel.php b/src/Assets/AssetContainerModel.php new file mode 100644 index 00000000..f93c3a7b --- /dev/null +++ b/src/Assets/AssetContainerModel.php @@ -0,0 +1,22 @@ + 'json', + ]; + + public function getAttribute($key) + { + return Arr::get($this->getAttributeValue('settings'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Assets/AssetContainerRepository.php b/src/Assets/AssetContainerRepository.php new file mode 100644 index 00000000..8b272194 --- /dev/null +++ b/src/Assets/AssetContainerRepository.php @@ -0,0 +1,65 @@ +map(function ($model) { + return Blink::once("eloquent-assetcontainers-{$model->handle}", function () use ($model) { + return app(AssetContainerContract::class)->fromModel($model); + }); + }); + }); + } + + public function findByHandle(string $handle): ?AssetContainerContract + { + return Blink::once("eloquent-assetcontainers-{$handle}", function () use ($handle) { + $model = app('statamic.eloquent.assets.container_model')::whereHandle($handle)->first(); + + if (! $model) { + return null; + } + + return app(AssetContainerContract::class)->fromModel($model); + }); + } + + public function make(string $handle = null): AssetContainerContract + { + return app(AssetContainerContract::class)->handle($handle); + } + + public function save(AssetContainerContract $container) + { + $container->save(); + + Blink::forget("eloquent-assetcontainers-{$container->handle()}"); + } + + public function delete($container) + { + $container->delete(); + + Blink::forget("eloquent-assetcontainers-{$container->handle()}"); + Blink::forget('eloquent-assetcontainers-all'); + } + + public static function bindings(): array + { + return [ + AssetContainerContract::class => AssetContainer::class, + ]; + } +} diff --git a/src/Assets/AssetModel.php b/src/Assets/AssetModel.php new file mode 100644 index 00000000..8a73e982 --- /dev/null +++ b/src/Assets/AssetModel.php @@ -0,0 +1,16 @@ + 'json', + ]; +} diff --git a/src/Assets/AssetRepository.php b/src/Assets/AssetRepository.php new file mode 100644 index 00000000..d7cb155a --- /dev/null +++ b/src/Assets/AssetRepository.php @@ -0,0 +1,30 @@ +container()->contents()->forget($asset->path())->save(); + + $handle = $asset->containerHandle().'::'.$asset->metaPath(); + app('statamic.eloquent.assets.model')::where('handle', $handle)->first()->delete(); + + Stache::store('assets::'.$asset->containerHandle())->delete($asset); + } + + public static function bindings(): array + { + return [ + AssetContract::class => Asset::class, + QueryBuilderContract::class => QueryBuilder::class, + ]; + } + } diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php new file mode 100644 index 00000000..4f9ea33b --- /dev/null +++ b/src/Collections/Collection.php @@ -0,0 +1,103 @@ +title($model->title ?? null) + ->routes($model->settings['routes'] ?? null) + ->requiresSlugs($model->settings['slugs'] ?? true) + ->titleFormats($model->settings['title_formats'] ?? null) + ->mount($model->settings['mount'] ?? null) + ->dated($model->settings['dated'] ?? null) + ->ampable($model->settings['ampable'] ?? null) + ->sites($model->settings['sites'] ?? null) + ->template($model->settings['template'] ?? null) + ->layout($model->settings['layout'] ?? null) + ->cascade($model->settings['inject'] ?? []) + ->searchIndex($model->settings['search_index'] ?? null) + ->revisionsEnabled($model->settings['revisions'] ?? false) + ->defaultPublishState($model->settings['default_status'] ?? true) + ->structureContents($model->settings['structure'] ?? null) + ->sortField($model->settings['sort_field'] ?? null) + ->sortDirection($model->settings['sort_dir'] ?? null) + ->taxonomies($model->settings['taxonomies'] ?? null) + ->propagate($model->settings['propagate'] ?? null) + ->futureDateBehavior($model->settings['future_date_behavior'] ?? null) + ->pastDateBehavior($model->settings['past_date_behavior'] ?? null) + ->previewTargets($model->settings['preview_targets'] ?? []) + ->handle($model->handle) + ->model($model); + } + + public function toModel() + { + return self::makeModelFromContract($this); + } + + public static function makeModelFromContract(Contract $source) + { + $class = app('statamic.eloquent.collections.model'); + + return $class::firstOrNew(['handle' => $source->handle])->fill([ + 'title' => $source->title ?? $source->handle, + 'settings' => [ + 'routes' => $source->routes, + 'slugs' => $source->requiresSlugs(), + 'title_formats' => collect($source->titleFormats())->filter(), + 'mount' => $source->mount, + 'dated' => $source->dated, + 'ampable' => $source->ampable, + 'sites' => $source->sites, + 'template' => $source->template, + 'layout' => $source->layout, + 'inject' => $source->cascade, + 'search_index' => $source->searchIndex, + 'revisions' => $source->revisionsEnabled(), + 'default_status' => $source->defaultPublishState, + 'structure' => $source->structureContents(), + 'sort_dir' => $source->sortDirection(), + 'sort_field' => $source->sortField(), + 'taxonomies' => $source->taxonomies, + 'propagate' => $source->propagate(), + 'past_date_behavior' => $source->pastDateBehavior(), + 'future_date_behavior' => $source->futureDateBehavior(), + 'preview_targets' => $source->previewTargets(), + ], + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + if (! is_null($model)) { + $this->id($model->id); + } + + return $this; + } + + protected function makeStructureFromContents() + { + return (new CollectionStructure) + ->handle($this->handle()) + ->expectsRoot($this->structureContents['root'] ?? false) + ->showSlugs($this->structureContents['slugs'] ?? false) + ->maxDepth($this->structureContents['max_depth'] ?? null); + } +} diff --git a/src/Collections/CollectionModel.php b/src/Collections/CollectionModel.php new file mode 100644 index 00000000..8f21bfd7 --- /dev/null +++ b/src/Collections/CollectionModel.php @@ -0,0 +1,25 @@ + 'json', + 'settings.routes' => 'array', + 'settings.inject' => 'array', + 'settings.taxonomies' => 'array', + 'settings.structure' => 'array', + 'settings.sites' => 'array', + 'settings.revisions' => 'boolean', + 'settings.dated' => 'boolean', + 'settings.default_publish_state' => 'boolean', + 'settings.ampable' => 'boolean', + ]; +} diff --git a/src/Collections/CollectionRepository.php b/src/Collections/CollectionRepository.php new file mode 100644 index 00000000..25cb76d2 --- /dev/null +++ b/src/Collections/CollectionRepository.php @@ -0,0 +1,81 @@ +queryEntries(); + + if ($ids) { + $query->whereIn('id', $ids); + } + + $query->get()->each(function ($entry) { + app('statamic.eloquent.entries.model')::find($entry->id())->update(['uri' => $entry->uri()]); + }); + } + + public function all(): IlluminateCollection + { + return Blink::once('eloquent-collections-all', function () { + return $this->transform(app('statamic.eloquent.collections.model')::all()); + }); + } + + public function find($handle): ?CollectionContract + { + return Blink::once("eloquent-collection-{$handle}", function () use ($handle) { + $model = app('statamic.eloquent.collections.model')::whereHandle($handle)->first(); + + return $model ? app(CollectionContract::class)->fromModel($model) : null; + }); + } + + public function findByHandle($handle): ?CollectionContract + { + return $this->find($handle); + } + + public function save($entry) + { + $model = $entry->toModel(); + $model->save(); + + Blink::forget("eloquent-collection-{$model->handle}"); + Blink::forget('eloquent-collections-all'); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $model = $entry->model(); + $model->delete(); + + Blink::forget("eloquent-collection-{$model->handle}"); + Blink::forget('eloquent-collections-all'); + } + + protected function transform($items, $columns = []) + { + return IlluminateCollection::make($items)->map(function ($model) { + return Blink::once("eloquent-collection-{$model->handle}", function () use ($model) { + return app(CollectionContract::class)::fromModel($model); + }); + }); + } + + public static function bindings(): array + { + return [ + CollectionContract::class => Collection::class, + ]; + } +} diff --git a/src/Commands/ImportAssets.php b/src/Commands/ImportAssets.php new file mode 100644 index 00000000..06becffe --- /dev/null +++ b/src/Commands/ImportAssets.php @@ -0,0 +1,89 @@ +useDefaultRepositories(); + + $this->importAssetContainers(); + $this->importAssets(); + + return 0; + } + + private function useDefaultRepositories() + { + Statamic::repository(AssetContainerRepositoryContract::class, AssetContainerRepository::class); + Statamic::repository(AssetRepositoryContract::class, AssetRepository::class); + + app()->bind(AssetContainerContract::class, AssetContainer::class); + app()->bind(AssetContract::class, Asset::class); + } + + private function importAssetContainers() + { + $containers = AssetContainerFacade::all(); + + $this->withProgressBar($containers, function ($container) { + $lastModified = $container->fileLastModified(); + $container->toModel()->fill(['created_at' => $lastModified, 'updated_at' => $lastModified])->save(); + }); + + $this->line(''); + $this->info('Asset containers imported'); + } + + private function importAssets() + { + $assets = AssetFacade::all(); + + $this->withProgressBar($assets, function ($asset) { + if ($contents = $asset->disk()->get($path = $asset->metaPath())) { + $metadata = YAML::file($path)->parse($contents); + $asset->writeMeta($metadata); + } + }); + + $this->newLine(); + $this->info('Assets imported'); + } +} diff --git a/src/Commands/ImportBlueprints.php b/src/Commands/ImportBlueprints.php new file mode 100644 index 00000000..bd8d054f --- /dev/null +++ b/src/Commands/ImportBlueprints.php @@ -0,0 +1,152 @@ +useDefaultRepositories(); + + $this->importBlueprints(); + $this->importFieldsets(); + + return 0; + } + + private function useDefaultRepositories() + { + app()->singleton( + 'Statamic\Fields\BlueprintRepository', + 'Statamic\Fields\BlueprintRepository' + ); + + app()->singleton( + 'Statamic\Fields\FieldsetRepository', + 'Statamic\Fields\FieldsetRepository' + ); + } + + private function importBlueprints() + { + $directory = resource_path('blueprints'); + + $files = File::withAbsolutePaths() + ->getFilesByTypeRecursively($directory, 'yaml'); + + $this->withProgressBar($files, function ($path) use ($directory) { + [$namespace, $handle] = $this->getNamespaceAndHandle( + Str::after(Str::before($path, '.yaml'), $directory.'/') + ); + + $contents = YAML::file($path)->parse(); + // Ensure sections are ordered correctly. + if (isset($contents['sections']) && is_array($contents['sections'])) { + $count = 0; + $contents['sections'] = collect($contents['sections']) + ->map(function ($section) use (&$count) { + $section['__count'] = $count++; + + return $section; + }) + ->toArray(); + } + + $blueprint = Blueprint::make() + ->setHidden(Arr::pull($contents, 'hide')) + ->setOrder(Arr::pull($contents, 'order')) + ->setInitialPath($path) + ->setHandle($handle) + ->setNamespace($namespace ?? null) + ->setContents($contents); + $lastModified = Carbon::createFromTimestamp(File::lastModified($path)); + + $model = app('statamic.eloquent.blueprints.blueprint_model')::firstOrNew([ + 'handle' => $blueprint->handle(), + 'namespace' => $blueprint->namespace() ?? null, + ])->fill([ + 'data' => $blueprint->contents(), + 'created_at' => $lastModified, + 'updated_at' => $lastModified, + ]); + + $model->save(); + }); + + $this->newLine(); + $this->info('Blueprints imported'); + } + + private function importFieldsets() + { + $directory = resource_path('fieldsets'); + + $files = File::withAbsolutePaths() + ->getFilesByTypeRecursively($directory, 'yaml'); + + $this->withProgressBar($files, function ($path) use ($directory) { + $basename = str_after($path, str_finish($directory, '/')); + $handle = str_before($basename, '.yaml'); + $handle = str_replace('/', '.', $handle); + + $fieldset = Fieldset::make($handle) + ->setContents(YAML::file($path)->parse()); + $lastModified = Carbon::createFromTimestamp(File::lastModified($path)); + + $model = app('statamic.eloquent.blueprints.fieldset_model')::firstOrNew([ + 'handle' => $fieldset->handle(), + ])->fill([ + 'data' => $fieldset->contents(), + 'created_at' => $lastModified, + 'updated_at' => $lastModified, + ]); + + $model->save(); + }); + + $this->newLine(); + $this->info('Fieldsets imported'); + } + + private function getNamespaceAndHandle($blueprint) + { + $blueprint = str_replace('/', '.', $blueprint); + $parts = explode('.', $blueprint); + $handle = array_pop($parts); + $namespace = implode('.', $parts); + $namespace = empty($namespace) ? null : $namespace; + + return [$namespace, $handle]; + } +} diff --git a/src/Commands/ImportCollections.php b/src/Commands/ImportCollections.php new file mode 100644 index 00000000..aeea7d47 --- /dev/null +++ b/src/Commands/ImportCollections.php @@ -0,0 +1,81 @@ +useDefaultRepositories(); + + $this->importCollections(); + + return 0; + } + + private function useDefaultRepositories() + { + Statamic::repository(CollectionRepositoryContract::class, CollectionRepository::class); + Statamic::repository(CollectionTreeRepositoryContract::class, CollectionTreeRepository::class); + + app()->bind(CollectionContract::class, StacheCollection::class); + } + + private function importCollections() + { + $collections = CollectionFacade::all(); + + $this->withProgressBar($collections, function ($collection) { + $lastModified = $collection->fileLastModified(); + EloquentCollection::makeModelFromContract($collection) + ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) + ->save(); + + if ($structure = $collection->structure()) { + $structure->trees()->each(function ($tree) { + $lastModified = $tree->fileLastModified(); + EloquentCollectionTree::makeModelFromContract($tree) + ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) + ->save(); + }); + } + }); + + $this->newLine(); + $this->info('Collections imported'); + } +} diff --git a/src/Commands/ImportEntries.php b/src/Commands/ImportEntries.php index 9f6b8824..c6a5726b 100644 --- a/src/Commands/ImportEntries.php +++ b/src/Commands/ImportEntries.php @@ -5,9 +5,8 @@ use Illuminate\Console\Command; use Statamic\Console\RunsInPlease; use Statamic\Contracts\Entries\CollectionRepository as CollectionRepositoryContract; +use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Contracts\Entries\EntryRepository as EntryRepositoryContract; -use Statamic\Eloquent\Entries\EntryQueryBuilder; -use Statamic\Eloquent\Entries\UuidEntryModel; use Statamic\Facades\Entry; use Statamic\Stache\Repositories\CollectionRepository; use Statamic\Stache\Repositories\EntryRepository; @@ -49,38 +48,20 @@ private function useDefaultRepositories() { Statamic::repository(EntryRepositoryContract::class, EntryRepository::class); Statamic::repository(CollectionRepositoryContract::class, CollectionRepository::class); + + app()->bind(EntryContract::class, app('statamic.eloquent.entries.entry')); } private function importEntries() { $entries = Entry::all(); - $bar = $this->output->createProgressBar($entries->count()); - $entries->each(function ($entry) use ($bar) { - $this->toModel($entry)->save(); - $bar->advance(); + $this->withProgressBar($entries, function ($entry) { + $lastModified = $entry->fileLastModified(); + $entry->toModel()->fill(['created_at' => $lastModified, 'updated_at' => $lastModified])->save(); }); - $bar->finish(); - $this->line(''); + $this->newLine(); $this->info('Entries imported'); } - - private function toModel($entry) - { - return new UuidEntryModel([ - 'id' => $entry->id(), - 'origin_id' => optional($entry->origin())->id(), - 'site' => $entry->locale(), - 'slug' => $entry->slug(), - 'uri' => $entry->uri(), - 'date' => $entry->hasDate() ? $entry->date() : null, - 'collection' => $entry->collectionHandle(), - 'data' => $entry->data()->except(EntryQueryBuilder::COLUMNS), - 'published' => $entry->published(), - 'status' => $entry->status(), - 'created_at' => $entry->lastModified(), - 'updated_at' => $entry->lastModified(), - ]); - } } diff --git a/src/Commands/ImportForms.php b/src/Commands/ImportForms.php new file mode 100644 index 00000000..b46a3a39 --- /dev/null +++ b/src/Commands/ImportForms.php @@ -0,0 +1,80 @@ +useDefaultRepositories(); + + $this->importForms(); + + return 0; + } + + private function useDefaultRepositories() + { + app()->bind(FormContract::class, StacheForm::class); + app()->bind(SubmissionContract::class, StacheSubmission::class); + } + + private function importForms() + { + $forms = (new FormRepository)->all(); + + $this->withProgressBar($forms, function ($form) { + $lastModified = Carbon::createFromTimestamp(File::lastModified($form->path())); + $model = Form::makeModelFromContract($form)->fill([ + 'created_at' => $lastModified, + 'updated_at' => $lastModified, + ]); + $model->save(); + + $form->submissions()->each(function ($submission) use ($model) { + $timestamp = (new SubmissionModel)->fromDateTime($submission->date()); + + $model->submissions()->firstOrNew(['created_at' => $timestamp])->fill([ + 'data' => $submission->data(), + 'updated_at' => $timestamp, + ])->save(); + }); + }); + + $this->newLine(); + $this->info('Forms imported'); + } +} diff --git a/src/Commands/ImportGlobals.php b/src/Commands/ImportGlobals.php new file mode 100644 index 00000000..097e99a3 --- /dev/null +++ b/src/Commands/ImportGlobals.php @@ -0,0 +1,65 @@ +useDefaultRepositories(); + + $this->importGlobals(); + + return 0; + } + + private function useDefaultRepositories() + { + Statamic::repository(GlobalRepositoryContract::class, GlobalRepository::class); + + app()->bind(GlobalSetContract::class, GlobalSet::class); + } + + private function importGlobals() + { + $sets = GlobalSetFacade::all(); + + $this->withProgressBar($sets, function ($set) { + $lastModified = $set->fileLastModified(); + $set->toModel()->fill(['created_at' => $lastModified, 'updated_at' => $lastModified])->save(); + }); + + $this->newLine(); + $this->info('Globals imported'); + } +} diff --git a/src/Commands/ImportNavs.php b/src/Commands/ImportNavs.php new file mode 100644 index 00000000..e1126fd7 --- /dev/null +++ b/src/Commands/ImportNavs.php @@ -0,0 +1,81 @@ +useDefaultRepositories(); + + $this->importNavs(); + + return 0; + } + + private function useDefaultRepositories() + { + Statamic::repository(NavigationRepositoryContract::class, NavigationRepository::class); + Statamic::repository(NavTreeRepositoryContract::class, NavTreeRepository::class); + + app()->bind(NavContract::class, EloquentNav::class); + app()->bind(TreeContract::class, EloquentTree::class); + } + + private function importNavs() + { + $navs = NavFacade::all(); + + $this->withProgressBar($navs, function ($nav) { + $lastModified = $nav->fileLastModified(); + EloquentNav::makeModelFromContract($nav) + ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) + ->save(); + + $nav->trees()->each(function ($tree) { + $lastModified = $tree->fileLastModified(); + EloquentNavTree::makeModelFromContract($tree) + ->fill(['created_at' => $lastModified, 'updated_at' => $lastModified]) + ->save(); + }); + }); + + $this->newLine(); + $this->info('Navs imported'); + } +} diff --git a/src/Commands/ImportRevisions.php b/src/Commands/ImportRevisions.php new file mode 100644 index 00000000..08c27136 --- /dev/null +++ b/src/Commands/ImportRevisions.php @@ -0,0 +1,67 @@ +importRevisions(); + } + + return 0; + } + + private function importRevisions() + { + $files = File::allFiles(config('statamic.revisions.path')); + + $this->withProgressBar($files, function ($file) { + $yml = YAML::file($file->getPathname())->parse(); + $revision = (new Revision()) + ->key($file->getRelativePath()) + ->action($yml['action'] ?? false) + ->date(Carbon::parse($yml['date'])) + ->user($yml['user'] ?? false) + ->message($yml['message'] ?? '') + ->attributes($yml['attributes'] ?? []); + if ($file->getBasename('.yaml') === 'working') { + $revision->action('working'); + } + + $revision->toModel()->save(); + }); + + $this->newLine(); + $this->info('Revisions imported'); + } +} diff --git a/src/Commands/ImportTaxonomies.php b/src/Commands/ImportTaxonomies.php new file mode 100644 index 00000000..3bc4d3fb --- /dev/null +++ b/src/Commands/ImportTaxonomies.php @@ -0,0 +1,96 @@ +useDefaultRepositories(); + + $this->importTaxonomies(); + $this->importTerms(); + + return 0; + } + + private function useDefaultRepositories() + { + Statamic::repository(TaxonomyRepositoryContract::class, TaxonomyRepository::class); + Statamic::repository(TermRepositoryContract::class, TermRepository::class); + + app()->bind(TaxonomyContract::class, StacheTaxonomy::class); + app()->bind(TermContract::class, StacheTerm::class); + } + + private function importTaxonomies() + { + $taxonomies = TaxonomyFacade::all(); + + $this->withProgressBar($taxonomies, function ($taxonomy) { + $lastModified = $taxonomy->fileLastModified(); + EloquentTaxonomy::makeModelFromContract($taxonomy)->fill([ + 'created_at' => $lastModified, + 'updated_at' => $lastModified, + ])->save(); + }); + + $this->newLine(); + $this->info('Taxonomies imported'); + } + + private function importTerms() + { + $terms = TermFacade::all(); + // Grab unique parent terms. + $terms = $terms->map->term()->unique(); + + $this->withProgressBar($terms, function ($term) { + $lastModified = $term->fileLastModified(); + EloquentTerm::makeModelFromContract($term)->fill([ + 'created_at' => $lastModified, + 'updated_at' => $lastModified, + ])->save(); + }); + + $this->newLine(); + $this->info('Terms imported'); + } +} diff --git a/src/Database/BaseMigration.php b/src/Database/BaseMigration.php new file mode 100644 index 00000000..d259752f --- /dev/null +++ b/src/Database/BaseMigration.php @@ -0,0 +1,33 @@ +setTable(config('statamic.eloquent-driver.table_prefix', '').$this->getTable()); + + if ($connection = config('statamic.eloquent-driver.connection', false)) { + $this->setConnection($connection); + } + } +} diff --git a/src/Entries/CollectionRepository.php b/src/Entries/CollectionRepository.php deleted file mode 100644 index 58f06241..00000000 --- a/src/Entries/CollectionRepository.php +++ /dev/null @@ -1,21 +0,0 @@ -queryEntries(); - - if ($ids) { - $query->whereIn('id', $ids); - } - - $query->get()->each(function ($entry) { - EntryModel::find($entry->id())->update(['uri' => $entry->uri()]); - }); - } -} diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index cbe04e05..101d3a52 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -2,6 +2,8 @@ namespace Statamic\Eloquent\Entries; +use Illuminate\Support\Carbon; +use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Eloquent\Entries\EntryModel as Model; use Statamic\Entries\Entry as FileEntry; @@ -11,7 +13,8 @@ class Entry extends FileEntry public static function fromModel(Model $model) { - return (new static) + $entry = (new static) + ->origin($model->origin_id) ->locale($model->site) ->slug($model->slug) ->date($model->date) @@ -20,6 +23,12 @@ public static function fromModel(Model $model) ->blueprint($model->data['blueprint'] ?? null) ->published($model->published) ->model($model); + + if (config('statamic.system.track_last_update')) { + $entry->set('updated_at', $model->updated_at ?? $model->created_at); + } + + return $entry; } public function toModel() @@ -34,7 +43,7 @@ public function toModel() return $class::findOrNew($this->id())->fill([ 'id' => $this->id(), - 'origin_id' => $this->originId(), + 'origin_id' => $this->origin()?->id(), 'site' => $this->locale(), 'slug' => $this->slug(), 'uri' => $this->uri(), @@ -43,6 +52,7 @@ public function toModel() 'data' => $data->except(EntryQueryBuilder::COLUMNS), 'published' => $this->published(), 'status' => $this->status(), + 'updated_at' => $this->lastModified(), ]); } @@ -54,14 +64,16 @@ public function model($model = null) $this->model = $model; - $this->id($model->id); + if (! is_null($model)) { + $this->id($model->id); + } return $this; } - public function lastModified() + public function fileLastModified() { - return $this->model->updated_at; + return $this->model?->updated_at ?? Carbon::now(); } public function origin($origin = null) @@ -72,24 +84,26 @@ public function origin($origin = null) return $this; } + $class = app('statamic.eloquent.entries.model'); + if ($this->origin) { + if (! $this->origin instanceof EntryContract) { + if ($model = $class::find($this->origin)) { + $this->origin = self::fromModel($model); + } + } + return $this->origin; } - if (! $this->model->origin) { - return null; + if (! $this->model?->origin_id) { + return; } - return self::fromModel($this->model->origin); - } - - public function originId() - { - return optional($this->origin)->id() ?? optional($this->model)->origin_id; - } + if ($model = $class::find($this->model->origin_id)) { + $this->origin = self::fromModel($model); + } - public function hasOrigin() - { - return $this->originId() !== null; + return $this->origin ?? null; } } diff --git a/src/Entries/EntryModel.php b/src/Entries/EntryModel.php index 64fdfaf6..8d6de882 100644 --- a/src/Entries/EntryModel.php +++ b/src/Entries/EntryModel.php @@ -2,10 +2,10 @@ namespace Statamic\Eloquent\Entries; -use Illuminate\Database\Eloquent\Model as Eloquent; use Illuminate\Support\Arr; +use Statamic\Eloquent\Database\BaseModel; -class EntryModel extends Eloquent +class EntryModel extends BaseModel { protected $guarded = []; @@ -14,14 +14,24 @@ class EntryModel extends Eloquent protected $casts = [ 'date' => 'datetime', 'data' => 'json', - 'published' => 'bool', + 'published' => 'boolean', ]; + public function author() + { + return $this->belongsTo(\App\Models\User::class, 'data->author'); + } + public function origin() { return $this->belongsTo(static::class); } + public function parent() + { + return $this->belongsTo(static::class, 'data->parent'); + } + public function getAttribute($key) { // Because the import script was importing `updated_at` into the diff --git a/src/Entries/EntryQueryBuilder.php b/src/Entries/EntryQueryBuilder.php index 4020890e..8e94f2b0 100644 --- a/src/Entries/EntryQueryBuilder.php +++ b/src/Entries/EntryQueryBuilder.php @@ -2,8 +2,10 @@ namespace Statamic\Eloquent\Entries; +use Illuminate\Support\Str; use Statamic\Contracts\Entries\QueryBuilder; use Statamic\Entries\EntryCollection; +use Statamic\Facades\Entry; use Statamic\Query\EloquentQueryBuilder; use Statamic\Stache\Query\QueriesTaxonomizedEntries; @@ -18,24 +20,42 @@ class EntryQueryBuilder extends EloquentQueryBuilder implements QueryBuilder protected function transform($items, $columns = []) { - return EntryCollection::make($items)->map(function ($model) { - return Entry::fromModel($model); + $items = EntryCollection::make($items)->map(function ($model) { + return app('statamic.eloquent.entries.entry')::fromModel($model); }); + + return Entry::applySubstitutions($items); } protected function column($column) { + if (! is_string($column)) { + return $column; + } + if ($column == 'origin') { $column = 'origin_id'; } if (! in_array($column, self::COLUMNS)) { - $column = 'data->'.$column; + if (! Str::startsWith($column, 'data->')) { + $column = 'data->'.$column; + } } return $column; } + public function find($id, $columns = ['*']) + { + $model = parent::find($id, $columns); + + if ($model) { + return app('statamic.eloquent.entries.entry')::fromModel($model) + ->selectedQueryColumns($columns); + } + } + public function get($columns = ['*']) { $this->addTaxonomyWheres(); @@ -47,7 +67,7 @@ public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', { $this->addTaxonomyWheres(); - return parent::paginate($perPage, $columns); + return parent::paginate($perPage, $columns, $pageName = 'page', $page = null); } public function count() diff --git a/src/Entries/EntryRepository.php b/src/Entries/EntryRepository.php index 37c2533e..114e776e 100644 --- a/src/Entries/EntryRepository.php +++ b/src/Entries/EntryRepository.php @@ -4,6 +4,7 @@ use Statamic\Contracts\Entries\Entry as EntryContract; use Statamic\Contracts\Entries\QueryBuilder; +use Statamic\Facades\Blink; use Statamic\Stache\Repositories\EntryRepository as StacheRepository; class EntryRepository extends StacheRepository @@ -11,15 +12,44 @@ class EntryRepository extends StacheRepository public static function bindings(): array { return [ - EntryContract::class => Entry::class, + EntryContract::class => app('statamic.eloquent.entries.entry'), QueryBuilder::class => EntryQueryBuilder::class, ]; } + public function find($id): ?EntryContract + { + $blinkKey = "eloquent-entry-{$id}"; + $item = Blink::once($blinkKey, function () use ($id) { + return $this->query()->where('id', $id)->first(); + }); + + if (! $item) { + Blink::forget($blinkKey); + return null; + } + + return $this->substitutionsById[$item->id()] ?? $item; + } + + public function findByUri(string $uri, string $site = null): ?EntryContract + { + $blinkKey = "eloquent-entry-{$uri}".($site ? '-'.$site : ''); + $item = Blink::once($blinkKey, function () use ($uri, $site) { + return parent::findByUri($uri, $site); + }); + + if (! $item) { + Blink::forget($blinkKey); + return null; + } + + return $this->substitutionsById[$item->id()] ?? $item; + } + public function save($entry) { $model = $entry->toModel(); - $model->save(); $entry->model($model->fresh()); diff --git a/src/Entries/UuidEntryModel.php b/src/Entries/UuidEntryModel.php index 74d3d2fa..bfcd731e 100644 --- a/src/Entries/UuidEntryModel.php +++ b/src/Entries/UuidEntryModel.php @@ -7,6 +7,7 @@ class UuidEntryModel extends EntryModel { public $incrementing = false; + protected $keyType = 'string'; protected static function boot() diff --git a/src/Fields/BlueprintModel.php b/src/Fields/BlueprintModel.php new file mode 100644 index 00000000..ce75588d --- /dev/null +++ b/src/Fields/BlueprintModel.php @@ -0,0 +1,22 @@ + 'json', + ]; + + public function getAttribute($key) + { + return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Fields/BlueprintRepository.php b/src/Fields/BlueprintRepository.php new file mode 100644 index 00000000..79032c82 --- /dev/null +++ b/src/Fields/BlueprintRepository.php @@ -0,0 +1,168 @@ +once($blueprint, function () use ($blueprint) { + [$namespace, $handle] = $this->getNamespaceAndHandle($blueprint); + if (! $blueprint) { + return null; + } + + $blueprintModel = ($namespace ? $this->filesIn($namespace) : BlueprintModel::whereNull('namespace')) + ->where('handle', $handle) + ->first(); + + if (! $blueprintModel) { + throw_if( + $namespace === null && $handle === 'default', + Exception::class, + 'Default Blueprint is required but not found. ' + ); + + return null; + } + + return $this->makeBlueprintFromModel($blueprintModel) ?? $this->findFallback($blueprint); + }); + } + + public function save(Blueprint $blueprint) + { + $this->clearBlinkCaches(); + + $this->updateModel($blueprint); + } + + public function delete(Blueprint $blueprint) + { + $this->clearBlinkCaches(); + + $this->deleteModel($blueprint); + } + + private function clearBlinkCaches() + { + Blink::store(self::BLINK_FOUND)->flush(); + Blink::store(self::BLINK_FROM_FILE)->flush(); + Blink::store(self::BLINK_NAMESPACE_PATHS)->flush(); + } + + public function in(string $namespace) + { + return $this + ->filesIn($namespace) + ->map(function ($file) { + return $this->makeBlueprintFromModel($file); + }) + ->sort(function ($a, $b) { + $orderA = $a->order() ?? 99999; + $orderB = $b->order() ?? 99999; + + return $orderA === $orderB + ? $a->title() <=> $b->title() + : $orderA <=> $orderB; + }) + ->keyBy->handle(); + } + + private function filesIn($namespace) + { + return Blink::store(self::BLINK_NAMESPACE_PATHS)->once($namespace ?? 'none', function () use ($namespace) { + $namespace = str_replace('/', '.', $namespace); + + if (count(($blueprintModels = BlueprintModel::where('namespace', $namespace)->get())) == 0) { + return collect(); + } + + return $blueprintModels; + }); + } + + private function makeBlueprintFromModel($model) + { + return Blink::store(self::BLINK_FROM_FILE)->once('database:blueprints:'.$model->id, function () use ($model) { + return Blueprint::make() + ->setHidden(Arr::get($model->data, 'hide')) + ->setOrder(Arr::get($model->data, 'order')) + ->setHandle($model->handle) + ->setNamespace($model->namespace) + ->setContents($this->updateOrderFromBlueprintSections($model->data)); + }); + } + + private function getNamespaceAndHandle($blueprint) + { + $blueprint = str_replace('/', '.', $blueprint); + $parts = explode('.', $blueprint); + $handle = array_pop($parts); + $namespace = implode('.', $parts); + $namespace = empty($namespace) ? null : $namespace; + + return [$namespace, $handle]; + } + + public function updateModel($blueprint) + { + $model = app('statamic.eloquent.blueprints.blueprint_model')::firstOrNew([ + 'handle' => $blueprint->handle(), + 'namespace' => $blueprint->namespace() ?? null, + ]); + + $model->data = $this->addOrderToBlueprintSections($blueprint->contents()); + $model->save(); + } + + public function deleteModel($blueprint) + { + $model = app('statamic.eloquent.blueprints.blueprint_model')::where('namespace', $blueprint->namespace() ?? null) + ->where('handle', $blueprint->handle()) + ->first(); + + if ($model) { + $model->delete(); + } + } + + private function addOrderToBlueprintSections($contents) + { + $count = 0; + $contents['sections'] = collect($contents['sections'] ?? []) + ->map(function ($section) use (&$count) { + $section['__count'] = $count++; + + return $section; + }) + ->toArray(); + + return $contents; + } + + private function updateOrderFromBlueprintSections($contents) + { + $contents['sections'] = collect($contents['sections'] ?? []) + ->sortBy('__count') + ->map(function ($section) { + unset($section['__count']); + + return $section; + }) + ->toArray(); + + return $contents; + } +} diff --git a/src/Fields/FieldsetModel.php b/src/Fields/FieldsetModel.php new file mode 100644 index 00000000..ad14288a --- /dev/null +++ b/src/Fields/FieldsetModel.php @@ -0,0 +1,22 @@ + 'json', + ]; + + public function getAttribute($key) + { + return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Fields/FieldsetRepository.php b/src/Fields/FieldsetRepository.php new file mode 100644 index 00000000..780b8ab0 --- /dev/null +++ b/src/Fields/FieldsetRepository.php @@ -0,0 +1,71 @@ +map(function ($model) { + return Blink::once("eloquent-fieldset-{$model->handle}", function () use ($model) { + return (new Fieldset) + ->setHandle($model->handle) + ->setContents($model->data); + }); + }); + }); + } + + public function find($handle): ?Fieldset + { + $handle = str_replace('/', '.', $handle); + + return $this->all()->filter(function ($fieldset) use ($handle) { + return $fieldset->handle() == $handle; + })->first(); + } + + public function save(Fieldset $fieldset) + { + $this->updateModel($fieldset); + } + + public function delete(Fieldset $fieldset) + { + $this->deleteModel($fieldset); + } + + public function updateModel($fieldset) + { + $model = app('statamic.eloquent.blueprints.fieldset_model')::firstOrNew([ + 'handle' => $fieldset->handle(), + ]); + + $model->data = $fieldset->contents(); + $model->save(); + + Blink::forget("eloquent-fieldset-{$model->handle}"); + } + + public function deleteModel($fieldset) + { + $model = app('statamic.eloquent.blueprints.fieldset_model')::where('handle', $fieldset->handle())->first(); + + if ($model) { + $model->delete(); + } + + Blink::forget("eloquent-fieldset-{$model->handle}"); + Blink::forget('eloquent-fieldsets-all'); + } +} diff --git a/src/Forms/Form.php b/src/Forms/Form.php new file mode 100644 index 00000000..b7d1d253 --- /dev/null +++ b/src/Forms/Form.php @@ -0,0 +1,94 @@ +title($model->title) + ->handle($model->handle) + ->store($model->settings['store'] ?? null) + ->email($model->settings['email'] ?? null) + ->honeypot($model->settings['honeypot'] ?? null) + ->model($model); + } + + public function toModel() + { + return self::makeModelFromContract($this); + } + + public static function makeModelFromContract(Contract $source) + { + $class = app('statamic.eloquent.forms.model'); + + return $class::firstOrNew(['handle' => $source->handle()])->fill([ + 'title' => $source->title(), + 'settings' => [ + 'store' => $source->store(), + 'email' => $source->email(), + 'honeypot' => $source->honeypot(), + ], + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + return $this; + } + + public function save() + { + $model = $this->toModel(); + $model->save(); + + $this->model($model->fresh()); + + FormSaved::dispatch($this); + } + + public function delete() + { + $this->submissions()->each->delete(); + $this->model()->delete(); + + FormDeleted::dispatch($this); + } + + public function submissions() + { + return $this->model()->submissions()->get()->map(function ($model) { + $submission = $this->makeSubmission() + ->id($model->id) + ->data($model->data); + + $submission + ->date($model->created_at); + + return $submission; + }); + } + + public function submission($id) + { + return $this->submissions()->filter(function ($submission) use ($id) { + return $submission->id() == $id; + })->first(); + } +} diff --git a/src/Forms/FormModel.php b/src/Forms/FormModel.php new file mode 100644 index 00000000..d98f74a3 --- /dev/null +++ b/src/Forms/FormModel.php @@ -0,0 +1,21 @@ + 'json', + ]; + + public function submissions() + { + return $this->hasMany(SubmissionModel::class, 'form_id'); + } +} diff --git a/src/Forms/FormRepository.php b/src/Forms/FormRepository.php new file mode 100644 index 00000000..56c5c443 --- /dev/null +++ b/src/Forms/FormRepository.php @@ -0,0 +1,48 @@ +first(); + + if (! $model) { + return; + } + + return app(FormContract::class)->fromModel($model); + } + + public function all() + { + return FormModel::all() + ->map(function ($form) { + return app(FormContract::class)::fromModel($form); + }); + } + + public function make($handle = null) + { + $form = app(FormContract::class); + + if ($handle) { + $form->handle($handle); + } + + return $form; + } + + public static function bindings(): array + { + return [ + FormContract::class => Form::class, + SubmissionContract::class => Submission::class, + ]; + } +} diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php new file mode 100644 index 00000000..f19974da --- /dev/null +++ b/src/Forms/Submission.php @@ -0,0 +1,79 @@ +id($model->id) + ->date($model->created_at) + ->data($model->data) + ->model($model); + } + + public function toModel() + { + $class = app('statamic.eloquent.forms.submission_model'); + $timestamp = (new $class)->fromDateTime($this->date()); + + return $class::firstOrNew([ + 'form_id' => $this->form->model()->id, + 'created_at' => $timestamp, + ])->fill([ + 'data' => $this->data, + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + return $this; + } + + public function date($date = null) + { + if (! is_null($date)) { + $this->date = $date; + } + + return $this->date; + } + + public function save() + { + $model = $this->toModel(); + $model->save(); + + $this->model($model->fresh()); + + SubmissionSaved::dispatch($this); + } + + public function delete() + { + if (! $this->model) { + $class = app('statamic.eloquent.forms.submission_model'); + $this->model = $class::findOrNew($this->id); + } + + $this->model->delete(); + + SubmissionDeleted::dispatch($this); + } +} diff --git a/src/Forms/SubmissionModel.php b/src/Forms/SubmissionModel.php new file mode 100644 index 00000000..d462b277 --- /dev/null +++ b/src/Forms/SubmissionModel.php @@ -0,0 +1,23 @@ + 'json', + ]; + + protected $dateFormat = 'Y-m-d H:i:s.u'; + + public function form() + { + return $this->belongsTo(FormModel::class, 'id'); + } +} diff --git a/src/Globals/GlobalRepository.php b/src/Globals/GlobalRepository.php new file mode 100644 index 00000000..9ebd0e9a --- /dev/null +++ b/src/Globals/GlobalRepository.php @@ -0,0 +1,71 @@ +map(function ($model) { + return Blink::once("eloquent-globalsets-{$model->handle}", function () use ($model) { + return app(GlobalSetContract::class)::fromModel($model); + }); + }); + } + + public function find($handle): ?GlobalSetContract + { + return Blink::once("eloquent-globalsets-{$handle}", function () use ($handle) { + $model = app('statamic.eloquent.global_sets.model')::whereHandle($handle)->first(); + if (! $model) { + return; + } + + return app(GlobalSetContract::class)->fromModel($model); + }); + } + + public function findByHandle($handle): ?GlobalSetContract + { + return $this->find($handle); + } + + public function all(): GlobalCollection + { + return Blink::once('eloquent-globalsets-all', function () { + return $this->transform(app('statamic.eloquent.global_sets.model')::all()); + }); + } + + public function save($entry) + { + $model = $entry->toModel(); + $model->save(); + + $entry->model($model->fresh()); + + Blink::forget("eloquent-globalsets-{$model->handle}"); + } + + public function delete($entry) + { + $entry->model()->delete(); + + Blink::forget("eloquent-globalsets-{$entry->handle()}"); + Blink::forget('eloquent-globalsets-all'); + } + + public static function bindings(): array + { + return [ + GlobalSetContract::class => GlobalSet::class, + VariablesContract::class => Variables::class, + ]; + } +} diff --git a/src/Globals/GlobalSet.php b/src/Globals/GlobalSet.php new file mode 100644 index 00000000..e4744cc1 --- /dev/null +++ b/src/Globals/GlobalSet.php @@ -0,0 +1,62 @@ +handle($model->handle) + ->title($model->title) + ->model($model); + + $variablesModel = app('statamic.eloquent.global_sets.variables_model'); + + foreach ($model->localizations as $localization) { + $global->addLocalization(app(VariablesContract::class)::fromModel($variablesModel::make($localization))); + } + + return $global; + } + + public function toModel() + { + $class = app('statamic.eloquent.global_sets.model'); + + $localizations = $this->localizations()->map(fn ($value) => $value->toModel()->toArray()); + + return $class::firstOrNew(['handle' => $this->handle()])->fill([ + 'title' => $this->title(), + 'localizations' => $localizations, + ]); + } + + public function makeLocalization($site) + { + return app(VariablesContract::class) + ->globalSet($this) + ->locale($site); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + if (! is_null($model)) { + $this->id($model->id); + } + + return $this; + } +} diff --git a/src/Globals/GlobalSetModel.php b/src/Globals/GlobalSetModel.php new file mode 100644 index 00000000..311a9259 --- /dev/null +++ b/src/Globals/GlobalSetModel.php @@ -0,0 +1,22 @@ + 'json', + ]; + + public function getAttribute($key) + { + return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Globals/Variables.php b/src/Globals/Variables.php new file mode 100644 index 00000000..74b88fa3 --- /dev/null +++ b/src/Globals/Variables.php @@ -0,0 +1,35 @@ +locale($model->locale) + ->data($model->data) + ->origin($model->origin ?? null); + } + + public function toModel() + { + $class = app('statamic.eloquent.global_sets.variables_model'); + + $data = $this->data(); + + return $class::make([ + 'locale' => $this->locale, + 'data' => $data, + 'origin' => $this->origin ?? null, + ]); + } + + protected function getOriginByString($origin) + { + return $this->globalSet()->in($origin); + } +} diff --git a/src/Globals/VariablesModel.php b/src/Globals/VariablesModel.php new file mode 100644 index 00000000..8772436f --- /dev/null +++ b/src/Globals/VariablesModel.php @@ -0,0 +1,20 @@ +getAttributeValue('data'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php new file mode 100644 index 00000000..da6f1057 --- /dev/null +++ b/src/Revisions/Revision.php @@ -0,0 +1,92 @@ +key($model->key) + ->action($model->action ?? false) + ->id($model->created_at->timestamp) + ->date($model->created_at) + ->user($model->user ?? false) + ->message($model->message ?? '') + ->attributes($model->attributes ?? []) + ->model($model); + } + + public function toModel() + { + $class = app('statamic.eloquent.revisions.model'); + + return $class::firstOrNew(['key' => $this->key(), 'created_at' => $this->date()])->fill([ + 'action' => $this->action(), + 'user' => $this->user()?->id(), + 'message' => with($this->message(), fn ($msg) => $msg == '0' ? '' : $msg), + 'attributes' => collect($this->attributes())->except('id'), + 'updated_at' => $this->date(), + ]); + } + + public function fromRevisionOrWorkingCopy($item) + { + return (new static) + ->key($item->key()) + ->action($item instanceof WorkingCopy ? 'working' : $item->action()) + ->date($item->date()) + ->user($item->user()?->id() ?? false) + ->message($item->message() ?? '') + ->attributes($item->attributes() ?? []); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + return $this; + } + + public function save() + { + $this->model->save(); + + RevisionSaved::dispatch($this); + } + + public function delete() + { + $this->model->delete(); + + RevisionDeleted::dispatch($this); + } +} diff --git a/src/Revisions/RevisionModel.php b/src/Revisions/RevisionModel.php new file mode 100644 index 00000000..a7c22f14 --- /dev/null +++ b/src/Revisions/RevisionModel.php @@ -0,0 +1,16 @@ + 'json', + ]; +} diff --git a/src/Revisions/RevisionRepository.php b/src/Revisions/RevisionRepository.php new file mode 100644 index 00000000..edcd563f --- /dev/null +++ b/src/Revisions/RevisionRepository.php @@ -0,0 +1,74 @@ +get() + ->map(function ($revision) use ($key) { + return $this->makeRevisionFromFile($key, $revision); + })->keyBy(function ($revision) { + return $revision->date()->timestamp; + }); + } + + public function findWorkingCopyByKey($key) + { + $class = app('statamic.eloquent.revisions.model'); + if (! $revision = $class::where(['key' => $key, 'action' => 'working'])->first()) { + return null; + } + + return $this->makeRevisionFromFile($key, $revision); + } + + public function save(RevisionContract $copy) + { + if ($copy instanceof WorkingCopy) { + app('statamic.eloquent.revisions.model')::where([ + 'key' => $copy->key(), + 'action' => 'working', + ])->delete(); + } + + $revision = (new Revision()) + ->fromRevisionOrWorkingCopy($copy) + ->toModel() + ->save(); + } + + public function delete(RevisionContract $revision) + { + if ($revision instanceof WorkingCopy) { + $this->findWorkingCopyByKey($revision->key())?->delete(); + + return; + } + + $revision->model?->delete(); + } + + protected function makeRevisionFromFile($key, $model) + { + return (new Revision)->fromModel($model); + } + + public static function bindings(): array + { + return [ + RevisionContract::class => Revision::class, + ]; + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 07a6730b..2ae11b68 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -2,13 +2,32 @@ namespace Statamic\Eloquent; +use Statamic\Contracts\Assets\AssetContainerRepository as AssetContainerRepositoryContract; +use Statamic\Contracts\Assets\AssetRepository as AssetRepositoryContract; use Statamic\Contracts\Entries\CollectionRepository as CollectionRepositoryContract; use Statamic\Contracts\Entries\EntryRepository as EntryRepositoryContract; -use Statamic\Eloquent\Commands\ImportEntries; -use Statamic\Eloquent\Entries\CollectionRepository; -use Statamic\Eloquent\Entries\EntryModel; +use Statamic\Contracts\Forms\FormRepository as FormRepositoryContract; +use Statamic\Contracts\Globals\GlobalRepository as GlobalRepositoryContract; +use Statamic\Contracts\Revisions\RevisionRepository as RevisionRepositoryContract; +use Statamic\Contracts\Structures\CollectionTreeRepository as CollectionTreeRepositoryContract; +use Statamic\Contracts\Structures\NavigationRepository as NavigationRepositoryContract; +use Statamic\Contracts\Structures\NavTreeRepository as NavTreeRepositoryContract; +use Statamic\Contracts\Taxonomies\TaxonomyRepository as TaxonomyRepositoryContract; +use Statamic\Contracts\Taxonomies\TermRepository as TermRepositoryContract; +use Statamic\Eloquent\Assets\AssetContainerRepository; +use Statamic\Eloquent\Assets\AssetRepository; +use Statamic\Eloquent\Collections\CollectionRepository; use Statamic\Eloquent\Entries\EntryQueryBuilder; use Statamic\Eloquent\Entries\EntryRepository; +use Statamic\Eloquent\Forms\FormRepository; +use Statamic\Eloquent\Globals\GlobalRepository; +use Statamic\Eloquent\Revisions\RevisionRepository; +use Statamic\Eloquent\Structures\CollectionTreeRepository; +use Statamic\Eloquent\Structures\NavigationRepository; +use Statamic\Eloquent\Structures\NavTreeRepository; +use Statamic\Eloquent\Taxonomies\TaxonomyRepository; +use Statamic\Eloquent\Taxonomies\TermQueryBuilder; +use Statamic\Eloquent\Taxonomies\TermRepository; use Statamic\Providers\AddonServiceProvider; use Statamic\Statamic; @@ -16,55 +35,266 @@ class ServiceProvider extends AddonServiceProvider { protected $config = false; - protected $updateScripts = [ - \Statamic\Eloquent\Updates\MoveConfig::class, - ]; + protected $migrationCount = 0; public function boot() { parent::boot(); - $this->mergeConfigFrom($config = __DIR__.'/../config/eloquent-driver.php', 'statamic.eloquent-driver'); + $this->mergeConfigFrom($config = __DIR__.'/../config/eloquent-driver.php', 'statamic-eloquent-driver'); + + $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); if (! $this->app->runningInConsole()) { return; } + $this->publishes([$config => config_path('statamic/eloquent-driver.php')], 'statamic-eloquent-config'); + $this->publishes([ - $config => config_path('statamic/eloquent-driver.php'), - ], 'statamic-eloquent-config'); + __DIR__.'/../database/migrations/create_taxonomies_table.php.stub' => $this->migrationsPath('create_taxonomies_table.php'), + __DIR__.'/../database/migrations/create_terms_table.php.stub' => $this->migrationsPath('create_terms_table.php'), + __DIR__.'/../database/migrations/create_globals_table.php.stub' => $this->migrationsPath('create_globals_table.php'), + __DIR__.'/../database/migrations/create_navigations_table.php.stub' => $this->migrationsPath('create_navigations_table.php'), + __DIR__.'/../database/migrations/create_navigation_trees_table.php.stub' => $this->migrationsPath('create_navigation_trees_table.php'), + __DIR__.'/../database/migrations/create_collections_table.php.stub' => $this->migrationsPath('create_collections_table.php'), + __DIR__.'/../database/migrations/create_blueprints_table.php.stub' => $this->migrationsPath('create_blueprints_table.php'), + __DIR__.'/../database/migrations/create_fieldsets_table.php.stub' => $this->migrationsPath('create_fieldsets_table.php'), + __DIR__.'/../database/migrations/create_forms_table.php.stub' => $this->migrationsPath('create_forms_table.php'), + __DIR__.'/../database/migrations/create_form_submissions_table.php.stub' => $this->migrationsPath('create_form_submissions_table.php'), + __DIR__.'/../database/migrations/create_asset_containers_table.php.stub' => $this->migrationsPath('create_asset_containers_table.php'), + __DIR__.'/../database/migrations/create_asset_table.php.stub' => $this->migrationsPath('create_asset_table.php'), + __DIR__.'/../database/migrations/create_revisions_table.php.stub' => $this->migrationsPath('create_revisions_table.php'), + ], 'migrations'); $this->publishes([ - __DIR__.'/../database/migrations/create_entries_table.php' => $this->migrationsPath('create_entries_table'), + __DIR__.'/../database/migrations/create_entries_table.php.stub' => $this->migrationsPath('create_entries_table'), ], 'statamic-eloquent-entries-table'); $this->publishes([ - __DIR__.'/../database/migrations/create_entries_table_with_string_ids.php' => $this->migrationsPath('create_entries_table_with_string_ids'), + __DIR__.'/../database/migrations/create_entries_table_with_string_ids.php.stub' => $this->migrationsPath('create_entries_table_with_string_ids'), ], 'statamic-eloquent-entries-table-with-string-ids'); - $this->commands([ImportEntries::class]); + $this->commands([ + Commands\ImportAssets::class, + Commands\ImportBlueprints::class, + Commands\ImportCollections::class, + Commands\ImportEntries::class, + Commands\ImportForms::class, + Commands\ImportGlobals::class, + Commands\ImportNavs::class, + Commands\ImportRevisions::class, + Commands\ImportTaxonomies::class, + ]); } public function register() { - Statamic::repository(EntryRepositoryContract::class, EntryRepository::class); + $this->registerAssets(); + $this->registerBlueprints(); + $this->registerCollections(); + $this->registerEntries(); + $this->registerForms(); + $this->registerGlobals(); + $this->registerRevisions(); + $this->registerStructures(); + $this->registerTaxonomies(); + $this->registerTerms(); + } + + private function registerAssets() + { + if (config('statamic.eloquent-driver.assets.driver', 'file') != 'eloquent') { + return; + } + + Statamic::repository(AssetContainerRepositoryContract::class, AssetContainerRepository::class); + Statamic::repository(AssetRepositoryContract::class, AssetRepository::class); + + $this->app->bind('statamic.eloquent.assets.container_model', function () { + return config('statamic.eloquent-driver.assets.container_model'); + }); + + $this->app->bind('statamic.eloquent.assets.model', function () { + return config('statamic.eloquent-driver.assets.model'); + }); + } + + private function registerBlueprints() + { + if (config('statamic.eloquent-driver.blueprints.driver', 'file') != 'eloquent') { + return; + } + + $this->app->singleton( + 'Statamic\Fields\BlueprintRepository', + 'Statamic\Eloquent\Fields\BlueprintRepository' + ); + + $this->app->singleton( + 'Statamic\Fields\FieldsetRepository', + 'Statamic\Eloquent\Fields\FieldsetRepository' + ); + + $this->app->bind('statamic.eloquent.blueprints.blueprint_model', function () { + return config('statamic.eloquent-driver.blueprints.blueprint_model'); + }); + + $this->app->bind('statamic.eloquent.blueprints.fieldset_model', function () { + return config('statamic.eloquent-driver.blueprints.fieldset_model'); + }); + } + + private function registerCollections() + { + if (config('statamic.eloquent-driver.collections.driver', 'file') != 'eloquent') { + return; + } + Statamic::repository(CollectionRepositoryContract::class, CollectionRepository::class); + $this->app->bind('statamic.eloquent.collections.model', function () { + return config('statamic.eloquent-driver.collections.model'); + }); + + Statamic::repository(CollectionTreeRepositoryContract::class, CollectionTreeRepository::class); + + $this->app->bind('statamic.eloquent.collections.tree', function () { + return config('statamic.eloquent-driver.collections.tree'); + }); + + $this->app->bind('statamic.eloquent.collections.tree_model', function () { + return config('statamic.eloquent-driver.collections.tree_model'); + }); + } + + private function registerEntries() + { + if (config('statamic.eloquent-driver.entries.driver', 'file') != 'eloquent') { + return; + } + + $this->app->bind('statamic.eloquent.entries.entry', function () { + return config('statamic.eloquent-driver.entries.entry'); + }); + + $this->app->bind('statamic.eloquent.entries.model', function () { + return config('statamic.eloquent-driver.entries.model'); + }); + + Statamic::repository(EntryRepositoryContract::class, EntryRepository::class); + $this->app->bind(EntryQueryBuilder::class, function ($app) { return new EntryQueryBuilder( $app['statamic.eloquent.entries.model']::query() ); }); + } - $this->app->bind('statamic.eloquent.entries.model', function () { - return config('statamic.eloquent-driver.entries.model'); + private function registerForms() + { + if (config('statamic.eloquent-driver.forms.driver', 'file') != 'eloquent') { + return; + } + + Statamic::repository(FormRepositoryContract::class, FormRepository::class); + + $this->app->bind('statamic.eloquent.forms.model', function () { + return config('statamic.eloquent-driver.forms.model'); + }); + + $this->app->bind('statamic.eloquent.forms.submission_model', function () { + return config('statamic.eloquent-driver.forms.submission_model'); }); } - protected function migrationsPath($filename) + private function registerGlobals() + { + if (config('statamic.eloquent-driver.global_sets.driver', 'file') != 'eloquent') { + return; + } + + Statamic::repository(GlobalRepositoryContract::class, GlobalRepository::class); + + $this->app->bind('statamic.eloquent.global_sets.model', function () { + return config('statamic.eloquent-driver.global_sets.model'); + }); + + $this->app->bind('statamic.eloquent.global_sets.variables_model', function () { + return config('statamic.eloquent-driver.global_sets.variables_model'); + }); + } + + private function registerRevisions() + { + if (config('statamic.eloquent-driver.revisions.driver', 'file') != 'eloquent') { + return; + } + + Statamic::repository(RevisionRepositoryContract::class, RevisionRepository::class); + + $this->app->bind('statamic.eloquent.revisions.model', function () { + return config('statamic.eloquent-driver.revisions.model'); + }); + } + + private function registerStructures() { - $date = date('Y_m_d_His'); + if (config('statamic.eloquent-driver.navigations.driver', 'file') != 'eloquent') { + return; + } + + Statamic::repository(NavigationRepositoryContract::class, NavigationRepository::class); + + $this->app->bind('statamic.eloquent.navigations.model', function () { + return config('statamic.eloquent-driver.navigations.model'); + }); - return database_path("migrations/{$date}_{$filename}.php"); + Statamic::repository(NavTreeRepositoryContract::class, NavTreeRepository::class); + + $this->app->bind('statamic.eloquent.navigations.tree', function () { + return config('statamic.eloquent-driver.navigations.tree'); + }); + + $this->app->bind('statamic.eloquent.navigations.tree_model', function () { + return config('statamic.eloquent-driver.navigations.tree_model'); + }); + } + + public function registerTaxonomies() + { + if (config('statamic.eloquent-driver.taxonomies.driver', 'file') != 'eloquent') { + return; + } + + Statamic::repository(TaxonomyRepositoryContract::class, TaxonomyRepository::class); + + $this->app->bind('statamic.eloquent.taxonomies.model', function () { + return config('statamic.eloquent-driver.taxonomies.model'); + }); + } + + public function registerTerms() + { + if (config('statamic.eloquent-driver.terms.driver', 'file') != 'eloquent') { + return; + } + + Statamic::repository(TermRepositoryContract::class, TermRepository::class); + + $this->app->bind('statamic.eloquent.terms.model', function () { + return config('statamic.eloquent-driver.terms.model'); + }); + + $this->app->bind(TermQueryBuilder::class, function ($app) { + return new TermQueryBuilder( + $app['statamic.eloquent.terms.model']::query() + ); + }); + } + + protected function migrationsPath($filename) + { + return database_path('migrations/'.date('Y_m_d_His', time() + (++$this->migrationCount + 60))."_{$filename}.php"); } } diff --git a/src/Structures/CollectionStructure.php b/src/Structures/CollectionStructure.php new file mode 100644 index 00000000..53a82ee9 --- /dev/null +++ b/src/Structures/CollectionStructure.php @@ -0,0 +1,13 @@ +tree($model->tree) + ->handle($model->handle) + ->locale($model->locale) + ->initialPath($model->settings['initial_path'] ?? null) + ->syncOriginal() + ->model($model); + } + + public function toModel() + { + return self::makeModelFromContract($this); + } + + public static function makeModelFromContract($source) + { + $class = app('statamic.eloquent.collections.tree_model'); + + return $class::firstOrNew([ + 'handle' => $source->handle(), + 'type' => 'collection', + 'locale' => $source->locale(), + ])->fill([ + 'tree' => $source->tree(), + 'settings' => [ + 'initial_path' => $source->initialPath(), + ], + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + return $this; + } +} diff --git a/src/Structures/CollectionTreeRepository.php b/src/Structures/CollectionTreeRepository.php new file mode 100644 index 00000000..f2f52b1b --- /dev/null +++ b/src/Structures/CollectionTreeRepository.php @@ -0,0 +1,32 @@ +where('locale', $site) + ->whereType('collection') + ->first(); + + return $model ? app(app('statamic.eloquent.collections.tree'))->fromModel($model) : null; + }); + } + + public function save($entry) + { + $model = $entry->toModel(); + $model->save(); + + Blink::forget("eloquent-collection-tree-{$model->handle}-{$model->locale}"); + + $entry->model($model->fresh()); + } +} diff --git a/src/Structures/Nav.php b/src/Structures/Nav.php new file mode 100644 index 00000000..044932dc --- /dev/null +++ b/src/Structures/Nav.php @@ -0,0 +1,62 @@ +handle($model->handle) + ->title($model->title) + ->collections($model->settings['collections'] ?? null) + ->maxDepth($model->settings['max_depth'] ?? null) + ->expectsRoot($model->settings['expects_root'] ?? false) + ->initialPath($model->settings['initial_path'] ?? null) + ->model($model); + } + + public function newTreeInstance() + { + return app(app('statamic.eloquent.navigations.tree')); + } + + public function toModel() + { + return self::makeModelFromContract($this); + } + + public static function makeModelFromContract(Contract $source) + { + $class = app('statamic.eloquent.navigations.model'); + + return $class::firstOrNew(['handle' => $source->handle()])->fill([ + 'title' => $source->title(), + 'settings' => [ + 'collections' => $source->collections()->map->handle(), + 'max_depth' => $source->maxDepth(), + 'expects_root' => $source->expectsRoot(), + 'initial_path' => $source->initialPath(), + ], + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + $this->id($model->id); + + return $this; + } +} diff --git a/src/Structures/NavModel.php b/src/Structures/NavModel.php new file mode 100644 index 00000000..c31513a3 --- /dev/null +++ b/src/Structures/NavModel.php @@ -0,0 +1,16 @@ + 'json', + ]; +} diff --git a/src/Structures/NavTree.php b/src/Structures/NavTree.php new file mode 100644 index 00000000..14d8c98a --- /dev/null +++ b/src/Structures/NavTree.php @@ -0,0 +1,56 @@ +tree($model->tree) + ->handle($model->handle) + ->locale($model->locale) + ->initialPath($model->settings['initial_path'] ?? null) + ->syncOriginal() + ->model($model); + } + + public function toModel() + { + return self::makeModelFromContract($this); + } + + public static function makeModelFromContract($source) + { + $class = app('statamic.eloquent.navigations.tree_model'); + + $isFileEntry = get_class($source) == FileEntry::class; + + return $class::firstOrNew([ + 'handle' => $source->handle(), + 'type' => 'navigation', + 'locale' => $source->locale(), + ])->fill([ + 'tree' => ($isFileEntry || $source->model) ? $source->tree() : [], + 'settings' => [ + 'initial_path' => $source->initialPath(), + ], + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + return $this; + } +} diff --git a/src/Structures/NavTreeRepository.php b/src/Structures/NavTreeRepository.php new file mode 100644 index 00000000..d7000082 --- /dev/null +++ b/src/Structures/NavTreeRepository.php @@ -0,0 +1,37 @@ +whereType('navigation') + ->where('locale', $site) + ->first(); + + return $model ? app(app('statamic.eloquent.navigations.tree'))->fromModel($model) : null; + }); + } + + public function save($entry) + { + $model = $entry->toModel(); + $model->save(); + + Blink::forget("eloquent-nav-tree-{$model->handle}-{$model->locale}"); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $entry->model()->delete(); + } +} diff --git a/src/Structures/NavigationRepository.php b/src/Structures/NavigationRepository.php new file mode 100644 index 00000000..60a096f7 --- /dev/null +++ b/src/Structures/NavigationRepository.php @@ -0,0 +1,54 @@ +map(function ($model) { + return Nav::fromModel($model); + }); + } + + public static function bindings(): array + { + return [ + NavContract::class => Nav::class, + ]; + } + + public function all(): Collection + { + return $this->transform(NavModel::all()); + } + + public function findByHandle($handle): ?NavContract + { + return Blink::once("eloquent-nav-{$handle}", function () use ($handle) { + $model = NavModel::whereHandle($handle)->first(); + + return $model ? app(NavContract::class)->fromModel($model) : null; + }); + } + + public function save($entry) + { + $model = $entry->toModel(); + $model->save(); + + Blink::forget("eloquent-nav-{$model->handle}"); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $entry->model()->delete(); + } +} diff --git a/src/Structures/TreeModel.php b/src/Structures/TreeModel.php new file mode 100644 index 00000000..b0c92ee3 --- /dev/null +++ b/src/Structures/TreeModel.php @@ -0,0 +1,17 @@ + 'json', + 'settings' => 'json', + ]; +} diff --git a/src/Taxonomies/Taxonomy.php b/src/Taxonomies/Taxonomy.php new file mode 100644 index 00000000..a6a6c764 --- /dev/null +++ b/src/Taxonomies/Taxonomy.php @@ -0,0 +1,55 @@ +handle($model->handle) + ->title($model->title) + ->sites($model->sites) + ->revisionsEnabled($model->settings['revisions'] ?? false) + ->model($model); + } + + public function toModel() + { + return self::makeModelFromContract($this); + } + + public static function makeModelFromContract(Contract $source) + { + $class = app('statamic.eloquent.taxonomies.model'); + + return $class::firstOrNew(['handle' => $source->handle()])->fill([ + 'title' => $source->title(), + 'sites' => $source->sites(), + 'settings' => [ + 'revisions' => $source->revisionsEnabled(), + ], + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + if (! is_null($model)) { + $this->id($model->id); + } + + return $this; + } +} diff --git a/src/Taxonomies/TaxonomyModel.php b/src/Taxonomies/TaxonomyModel.php new file mode 100644 index 00000000..1983c758 --- /dev/null +++ b/src/Taxonomies/TaxonomyModel.php @@ -0,0 +1,23 @@ + 'json', + 'sites' => 'json', + ]; + + public function getAttribute($key) + { + return Arr::get($this->getAttributeValue('settings'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Taxonomies/TaxonomyRepository.php b/src/Taxonomies/TaxonomyRepository.php new file mode 100644 index 00000000..2a36e2d3 --- /dev/null +++ b/src/Taxonomies/TaxonomyRepository.php @@ -0,0 +1,97 @@ +map(function ($model) { + return app(TaxonomyContract::class)::fromModel($model); + }); + } + + public static function bindings(): array + { + return [ + TaxonomyContract::class => Taxonomy::class, + ]; + } + + public function all(): Collection + { + $models = Blink::once('eloquent-taxonomies-all', function () { + return app('statamic.eloquent.taxonomies.model')::all(); + }) + ->each(function ($model) { + Blink::put("eloquent-taxonomies-{$model->handle}", $model); + }); + + return $this->transform($models); + } + + public function findByHandle($handle): ?TaxonomyContract + { + $taxonomyModel = Blink::once("eloquent-taxonomies-{$handle}", function () use ($handle) { + return app('statamic.eloquent.taxonomies.model')::whereHandle($handle)->first(); + }); + + return $taxonomyModel ? app(TaxonomyContract::class)->fromModel($taxonomyModel) : null; + } + + public function findByUri(string $uri, string $site = null): ?Taxonomy + { + $collection = Facades\Collection::all() + ->first(function ($collection) use ($uri, $site) { + if (Str::startsWith($uri, $collection->uri($site))) { + return true; + } + + return Str::startsWith($uri, '/'.$collection->handle()); + }); + + if ($collection) { + $uri = Str::after($uri, $collection->uri($site) ?? $collection->handle()); + } + + // If the collection is mounted to the home page, the uri would have + // the slash trimmed off at this point. We'll make sure it's there, + // then look for whats after it to get our handle. + $uri = Str::after(Str::ensureLeft($uri, '/'), '/'); + + return ($taxonomy = $this->findByHandle($uri)) ? $taxonomy->collection($collection) : null; + } + + public function handles(): Collection + { + return $this->all()->map->handle(); + } + + public function save($entry) + { + $model = $entry->toModel(); + $model->save(); + + $fresh = $model->fresh(); + + $entry->model($fresh); + + Blink::put("eloquent-taxonomies-{$fresh->handle}", $fresh); + } + + public function delete($entry) + { + $model = $entry->model(); + $model->delete(); + + Blink::forget("eloquent-taxonomies-{$model->handle}"); + Blink::forget('eloquent-taxonomies-all'); + } +} diff --git a/src/Taxonomies/Term.php b/src/Taxonomies/Term.php new file mode 100644 index 00000000..4e83f81d --- /dev/null +++ b/src/Taxonomies/Term.php @@ -0,0 +1,107 @@ +data; + + /** @var Term $term */ + $term = (new static) + ->slug($model->slug) + ->taxonomy($model->taxonomy) + ->model($model) + ->blueprint($model->data['blueprint'] ?? null); + + collect($data['localizations'] ?? []) + ->except($term->defaultLocale()) + ->each(function ($localeData, $locale) use ($term) { + $term->dataForLocale($locale, $localeData); + }); + + unset($data['localizations']); + + if (isset($data['collection'])) { + $term->collection($data['collection']); + unset($data['collection']); + } + + $term->syncOriginal(); + $term->data($data); + + if (config('statamic.system.track_last_update')) { + $term->set('updated_at', $model->updated_at ?? $model->created_at); + } + + return $term; + } + + public function toModel() + { + return self::makeModelFromContract($this); + } + + public static function makeModelFromContract(Contract $source) + { + $class = app('statamic.eloquent.terms.model'); + + $data = $source->data(); + + if (! isset($data['template'])) { + unset($data['template']); + } + + if ($source->blueprint && $source->taxonomy()->termBlueprints()->count() > 1) { + $data['blueprint'] = $source->blueprint; + } + + $source->localizations()->keys()->reduce(function ($data, $locale) use ($source) { + $data[$locale] = $source->dataForLocale($locale)->toArray(); + + return $data; + }, []); + + if ($collection = $source->collection()) { + $data['collection'] = $collection; + } + + return $class::firstOrNew([ + 'slug' => $source->slug(), + 'taxonomy' => $source->taxonomy(), + 'site' => $source->locale(), + ])->fill([ + 'uri' => $source->uri(), + 'data' => $data, + 'updated_at' => $source->lastModified(), + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + if (! is_null($model)) { + $this->id($model->id); + } + + return $this; + } + + public function fileLastModified() + { + return $this->model?->updated_at ?? Carbon::now(); + } +} diff --git a/src/Taxonomies/TermModel.php b/src/Taxonomies/TermModel.php new file mode 100644 index 00000000..aa5d326e --- /dev/null +++ b/src/Taxonomies/TermModel.php @@ -0,0 +1,22 @@ + 'json', + ]; + + public function getAttribute($key) + { + return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Taxonomies/TermQueryBuilder.php b/src/Taxonomies/TermQueryBuilder.php new file mode 100644 index 00000000..e8f95be2 --- /dev/null +++ b/src/Taxonomies/TermQueryBuilder.php @@ -0,0 +1,263 @@ +site; + if (! $site) { + $site = Site::default()->handle(); + } + + return TermCollection::make($items)->map(function ($model) use ($site) { + return app(TermContract::class)::fromModel($model)->in($site); + }); + } + + protected function column($column) + { + if (! is_string($column)) { + return $column; + } + + if (! in_array($column, $this->columns)) { + if (! Str::startsWith($column, 'data->')) { + $column = 'data->'.$column; + } + } + + return $column; + } + + public function where($column, $operator = null, $value = null, $boolean = 'and') + { + if ($column === 'site') { + $this->site = $operator; + + return $this; + } + + if (func_num_args() === 2) { + [$value, $operator] = [$operator, '=']; + } + + if (in_array($column, ['taxonomy', 'taxonomies'])) { + if (! $value) { + return $this; + } + + if (! is_array($value)) { + $value = [$value]; + } + + $this->taxonomies = array_merge($this->taxonomies, $value); + + return $this; + } + + if (in_array($column, ['collection', 'collections'])) { + if (! $value) { + return $this; + } + + if (! is_array($value)) { + $value = [$value]; + } + + $this->collections = array_merge($this->collections, $value); + + return $this; + } + + if (in_array($column, ['id', 'slug'])) { + $column = 'slug'; + + if (str_contains($value, '::')) { + + $taxonomy = Str::before($value.'', '::'); + + if ($taxonomy) { + $this->taxonomies[] = $taxonomy; + } + + $value = Str::after($value, '::'); + + } + } + + parent::where($column, $operator, $value, $boolean); + + return $this; + } + + public function whereIn($column, $values, $boolean = 'and') + { + if (in_array($column, ['taxonomy', 'taxonomies'])) { + if (! $values) { + return $this; + } + + $this->taxonomies = array_merge($this->taxonomies, collect($values)->all()); + + return $this; + } + + if (in_array($column, ['collection', 'collections'])) { + if (! $values) { + return $this; + } + + $this->collections = array_merge($this->collections, collect($values)->all()); + + return $this; + } + + if (in_array($column, ['id', 'slug'])) { + $column = 'slug'; + $values = collect($values) + ->map(function ($value) { + return Str::after($value, '::'); + }) + ->all(); + } + + parent::whereIn($column, $values, $boolean); + + return $this; + } + + public function find($id, $columns = ['*']) + { + $model = parent::find($id, $columns); + + if ($model) { + $site = $this->site; + if (! $site) { + $site = Site::default()->handle(); + } + + return app(TermContract::class)::fromModel($model) + ->in($site) + ->selectedQueryColumns($columns); + } + } + + public function get($columns = ['*']) + { + $this->applyCollectionAndTaxonomyWheres(); + + $items = parent::get($columns); + + // If a single collection has been queried, we'll supply it to the terms so + // things like URLs will be scoped to the collection. We can't do it when + // multiple collections are queried because it would be ambiguous. + if ($this->collections && count($this->collections) == 1) { + $items->each->collection(Collection::findByHandle($this->collections[0])); + } + + $items = Term::applySubstitutions($items); + + return $items->map(function ($term) { + + if ($this->site) { + return $term->in($this->site); + } + + return $term->inDefaultLocale(); + }); + } + + public function count() + { + $this->applyCollectionAndTaxonomyWheres(); + + return parent::count(); + } + + public function paginate($perPage = null, $columns = [], $pageName = 'page', $page = null) + { + $this->applyCollectionAndTaxonomyWheres(); + + return parent::paginate($perPage = null, $columns = [], $pageName = 'page', $page = null); + } + + private function applyCollectionAndTaxonomyWheres() + { + if (! empty($this->collections)) { + $this->builder->where(function ($query) { + + $taxonomies = empty($this->taxonomies) + ? Taxonomy::handles()->all() + : $this->taxonomies; + + // get entries in each collection that have a value for the taxonomies we are querying + // or the ones associated with the collection + // what we ultimately want is a subquery for terms in the form: + // where('taxonomy', $taxonomy)->whereIn('slug', $slugArray) + Entry::whereInCollection($this->collections) + ->flatMap(function ($entry) use ($taxonomies) { + $slugs = []; + foreach ($entry->collection()->taxonomies()->map->handle() as $taxonomy) { + if (in_array($taxonomy, $taxonomies)) { + foreach ($entry->get($taxonomy, []) as $term) { + $slugs[] = $taxonomy.'::'.$term; + } + } + } + + return $slugs; + }) + ->unique() + ->map(function ($term) { + return [ + 'taxonomy' => Str::before($term, '::'), + 'term' => Str::after($term, '::'), + ]; + }) + ->mapToGroups(function ($item) { + return [$item['taxonomy'] => $item['term']]; + }) + ->each(function ($terms, $taxonomy) use ($query) { + $query->orWhere(function ($query) use ($terms, $taxonomy) { + $query->where('taxonomy', $taxonomy) + ->whereIn('slug', $terms); + }); + }); + + }); + + } + + if (! empty($this->taxonomies)) { + $queryTaxonomies = collect($this->taxonomies) + ->filter() + ->unique(); + + if ($queryTaxonomies->count() > 0) { + $this->builder->whereIn('taxonomy', $queryTaxonomies->all()); + } + } + } +} diff --git a/src/Taxonomies/TermRepository.php b/src/Taxonomies/TermRepository.php new file mode 100644 index 00000000..b43ab4e3 --- /dev/null +++ b/src/Taxonomies/TermRepository.php @@ -0,0 +1,104 @@ +ensureAssociations(); + + return app(TermQueryBuilder::class); + } + + public function find($id): ?LocalizedTerm + { + [$handle, $slug] = explode('::', $id); + + $term = $this->query() + ->where('taxonomy', $handle) + ->where('slug', $slug) + ->get(); + + return $term ? $term->first() : null; + } + + public function findByUri(string $uri, string $site = null): ?TermContract + { + $site = $site ?? $this->stache->sites()->first(); + + if ($substitute = $this->substitutionsByUri[$site.'@'.$uri] ?? null) { + return $substitute; + } + + $collection = Collection::all() + ->first(function ($collection) use ($uri, $site) { + if (Str::startsWith($uri, $collection->uri($site))) { + return true; + } + + return Str::startsWith($uri, '/'.$collection->handle()); + }); + + if ($collection) { + $uri = Str::after($uri, $collection->uri($site) ?? $collection->handle()); + } + + $uri = Str::removeLeft($uri, '/'); + + [$taxonomy, $slug] = array_pad(explode('/', $uri), 2, null); + + if (! $slug) { + return null; + } + + if (! $taxonomy = $this->findTaxonomyHandleByUri($taxonomy)) { + return null; + } + + $term = $this->query() + ->where('slug', $slug) + ->where('taxonomy', $taxonomy) + ->first(); + + if (! $term) { + return null; + } + + return $term->in($site)?->collection($collection); + } + + private function findTaxonomyHandleByUri($uri) + { + return Taxonomy::findByHandle($uri)?->handle(); + } + + public function save($entry) + { + $model = $entry->toModel(); + $model->save(); + + //var_dump($entry->data()); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $entry->model()->delete(); + } + + public static function bindings(): array + { + return [ + TermContract::class => Term::class, + ]; + } +} diff --git a/tests/ConsoleKernel.php b/tests/ConsoleKernel.php new file mode 100644 index 00000000..17129e26 --- /dev/null +++ b/tests/ConsoleKernel.php @@ -0,0 +1,42 @@ +command('inspire') + // ->hourly(); + } + + /** + * Register the commands for the application. + * + * @return void + */ + protected function commands() + { + // $this->load(__DIR__.'/Commands'); + + // require base_path('routes/console.php'); + } +} diff --git a/tests/Data/Assets/AssetQueryBuilderTest.php b/tests/Data/Assets/AssetQueryBuilderTest.php new file mode 100644 index 00000000..1d052c14 --- /dev/null +++ b/tests/Data/Assets/AssetQueryBuilderTest.php @@ -0,0 +1,524 @@ + '/assets']); + + Storage::disk('test')->put('a.jpg', ''); + Storage::disk('test')->put('b.txt', ''); + Storage::disk('test')->put('c.txt', ''); + Storage::disk('test')->put('d.jpg', ''); + Storage::disk('test')->put('e.jpg', ''); + Storage::disk('test')->put('f.jpg', ''); + $this->container = tap(AssetContainer::make('test')->disk('test'))->save(); + } + + /** @test */ + public function it_can_get_assets() + { + $assets = $this->container->queryAssets()->get(); + + $this->assertCount(6, $assets); + $this->assertEquals(['a', 'b', 'c', 'd', 'e', 'f'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where() + { + $assets = $this->container->queryAssets()->where('filename', 'a')->orWhere('filename', 'c')->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['a', 'c'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where_in() + { + $assets = $this->container->queryAssets() + ->whereIn('filename', ['a', 'b']) + ->orWhereIn('filename', ['a', 'd']) + ->orWhereIn('extension', ['jpg']) + ->get(); + + $this->assertCount(5, $assets); + $this->assertEquals(['a', 'b', 'd', 'e', 'f'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where_not_in() + { + $assets = $this->container->queryAssets() + ->whereNotIn('filename', ['a', 'b']) + ->orWhereNotIn('filename', ['a', 'f']) + ->orWhereNotIn('extension', ['txt']) + ->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['d', 'e'], $assets->map->filename()->all()); + } + + private function createWhereDateTestAssets() + { + $blueprint = Blueprint::makeFromFields(['test_date' => ['type' => 'date', 'time_enabled' => true]]); + Blueprint::shouldReceive('find')->with('assets/test')->andReturn($blueprint); + + Asset::find('test::a.jpg')->data(['test_date' => '2021-11-15 20:31:04'])->save(); + Asset::find('test::b.txt')->data(['test_date' => '2021-11-14 09:00:00'])->save(); + Asset::find('test::c.txt')->data(['test_date' => '2021-11-15 00:00:00'])->save(); + Asset::find('test::d.jpg')->data(['test_date' => '2020-09-13 14:44:24'])->save(); + Asset::find('test::e.jpg')->data(['test_date' => null])->save(); + } + + /** @test **/ + public function assets_are_found_using_where_date() + { + $this->createWhereDateTestAssets(); + + $assets = $this->container->queryAssets()->whereDate('test_date', '2021-11-15')->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['a', 'c'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->whereDate('test_date', 1637000264)->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['a', 'c'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->whereDate('test_date', '>=', '2021-11-15')->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['a', 'c'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_month() + { + $this->createWhereDateTestAssets(); + + $assets = $this->container->queryAssets()->whereMonth('test_date', 11)->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['a', 'b', 'c'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->whereMonth('test_date', '<', 11)->get(); + + $this->assertCount(1, $assets); + $this->assertEquals(['d'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_day() + { + $this->createWhereDateTestAssets(); + + $assets = $this->container->queryAssets()->whereDay('test_date', 15)->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['a', 'c'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->whereDay('test_date', '<', 15)->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['b', 'd'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_year() + { + $this->createWhereDateTestAssets(); + + $assets = $this->container->queryAssets()->whereYear('test_date', 2021)->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['a', 'b', 'c'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->whereYear('test_date', '<', 2021)->get(); + + $this->assertCount(1, $assets); + $this->assertEquals(['d'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_time() + { + $this->createWhereDateTestAssets(); + + $assets = $this->container->queryAssets()->whereTime('test_date', '09:00')->get(); + + $this->assertCount(1, $assets); + $this->assertEquals(['b'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->whereTime('test_date', '>', '09:00')->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['a', 'd'], $assets->map->filename()->all()); + } + + public function assets_are_found_using_where_null() + { + Asset::find('test::a.jpg')->data(['text' => 'Text 1'])->save(); + Asset::find('test::b.txt')->data(['text' => 'Text 2'])->save(); + Asset::find('test::c.txt')->data([])->save(); + Asset::find('test::d.jpg')->data(['text' => 'Text 4'])->save(); + Asset::find('test::e.jpg')->data([])->save(); + Asset::find('test::f.jpg')->data([])->save(); + + $assets = $this->container->queryAssets()->whereNull('text')->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['c', 'e', 'f'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_not_null() + { + Asset::find('test::a.jpg')->data(['text' => 'Text 1'])->save(); + Asset::find('test::b.txt')->data(['text' => 'Text 2'])->save(); + Asset::find('test::c.txt')->data([])->save(); + Asset::find('test::d.jpg')->data(['text' => 'Text 4'])->save(); + Asset::find('test::e.jpg')->data([])->save(); + Asset::find('test::f.jpg')->data([])->save(); + + $assets = $this->container->queryAssets()->whereNotNull('text')->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['a', 'b', 'd'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where_null() + { + Asset::find('test::a.jpg')->data(['text' => 'Text 1', 'content' => 'Content 1'])->save(); + Asset::find('test::b.txt')->data(['text' => 'Text 2'])->save(); + Asset::find('test::c.txt')->data(['content' => 'Content 1'])->save(); + Asset::find('test::d.jpg')->data(['text' => 'Text 4'])->save(); + Asset::find('test::e.jpg')->data([])->save(); + Asset::find('test::f.jpg')->data([])->save(); + + $assets = $this->container->queryAssets()->whereNull('text')->orWhereNull('content')->get(); + + $this->assertCount(5, $assets); + $this->assertEquals(['c', 'e', 'f', 'b', 'd'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where_not_null() + { + Asset::find('test::a.jpg')->data(['text' => 'Text 1', 'content' => 'Content 1'])->save(); + Asset::find('test::b.txt')->data(['text' => 'Text 2'])->save(); + Asset::find('test::c.txt')->data(['content' => 'Content 1'])->save(); + Asset::find('test::d.jpg')->data(['text' => 'Text 4'])->save(); + Asset::find('test::e.jpg')->data([])->save(); + Asset::find('test::f.jpg')->data([])->save(); + + $assets = $this->container->queryAssets()->whereNotNull('content')->orWhereNotNull('text')->get(); + + $this->assertCount(4, $assets); + $this->assertEquals(['a', 'c', 'b', 'd'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_nested_where() + { + $assets = $this->container->queryAssets() + ->where(function ($query) { + $query->where('filename', 'a'); + }) + ->orWhere(function ($query) { + $query->where('filename', 'c')->orWhere('filename', 'd'); + }) + ->orWhere('filename', 'f') + ->get(); + + $this->assertCount(4, $assets); + $this->assertEquals(['a', 'c', 'd', 'f'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_nested_where_in() + { + $assets = $this->container->queryAssets() + ->where(function ($query) { + $query->whereIn('filename', ['a', 'b']); + }) + ->orWhere(function ($query) { + $query->whereIn('filename', ['a', 'd']) + ->orWhereIn('extension', ['txt']); + }) + ->orWhereIn('filename', ['f']) + ->get(); + + $this->assertCount(5, $assets); + $this->assertEquals(['a', 'b', 'd', 'c', 'f'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_between() + { + Asset::find('test::a.jpg')->data(['number_field' => 8])->save(); + Asset::find('test::b.txt')->data(['number_field' => 9])->save(); + Asset::find('test::c.txt')->data(['number_field' => 10])->save(); + Asset::find('test::d.jpg')->data(['number_field' => 11])->save(); + Asset::find('test::e.jpg')->data(['number_field' => 12])->save(); + Asset::find('test::f.jpg')->data([])->save(); + + $assets = $this->container->queryAssets()->whereBetween('number_field', [9, 11])->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['b', 'c', 'd'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_not_between() + { + Asset::find('test::a.jpg')->data(['number_field' => 8])->save(); + Asset::find('test::b.txt')->data(['number_field' => 9])->save(); + Asset::find('test::c.txt')->data(['number_field' => 10])->save(); + Asset::find('test::d.jpg')->data(['number_field' => 11])->save(); + Asset::find('test::e.jpg')->data(['number_field' => 12])->save(); + Asset::find('test::f.jpg')->data([])->save(); + + $assets = $this->container->queryAssets()->whereNotBetween('number_field', [9, 11])->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['a', 'e', 'f'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where_between() + { + Asset::find('test::a.jpg')->data(['number_field' => 8])->save(); + Asset::find('test::b.txt')->data(['number_field' => 9])->save(); + Asset::find('test::c.txt')->data(['number_field' => 10])->save(); + Asset::find('test::d.jpg')->data(['number_field' => 11])->save(); + Asset::find('test::e.jpg')->data(['number_field' => 12])->save(); + Asset::find('test::f.jpg')->data([])->save(); + + $assets = $this->container->queryAssets()->whereBetween('number_field', [9, 10])->orWhereBetween('number_field', [11, 12])->get(); + + $this->assertCount(4, $assets); + $this->assertEquals(['b', 'c', 'd', 'e'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where_not_between() + { + Asset::find('test::a.jpg')->data(['text' => 'a', 'number_field' => 8])->save(); + Asset::find('test::b.txt')->data(['text' => 'b', 'number_field' => 9])->save(); + Asset::find('test::c.txt')->data(['text' => 'c', 'number_field' => 10])->save(); + Asset::find('test::d.jpg')->data(['text' => 'd', 'number_field' => 11])->save(); + Asset::find('test::e.jpg')->data(['text' => 'e', 'number_field' => 12])->save(); + Asset::find('test::f.jpg')->data([])->save(); + + $assets = $this->container->queryAssets()->where('text', 'e')->orWhereNotBetween('number_field', [10, 12])->get(); + + $this->assertCount(4, $assets); + $this->assertEquals(['e', 'a', 'b', 'f'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_json_contains() + { + Asset::find('test::a.jpg')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Asset::find('test::b.txt')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Asset::find('test::c.txt')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Asset::find('test::d.jpg')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Asset::find('test::e.jpg')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $assets = $this->container->queryAssets()->whereJsonContains('test_taxonomy', ['taxonomy-1', 'taxonomy-5'])->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['a', 'c', 'e'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->whereJsonContains('test_taxonomy', 'taxonomy-1')->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['a', 'c'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_json_doesnt_contain() + { + Asset::find('test::a.jpg')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Asset::find('test::b.txt')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Asset::find('test::c.txt')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Asset::find('test::d.jpg')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Asset::find('test::e.jpg')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + Asset::find('test::f.jpg')->data(['test_taxonomy' => ['taxonomy-1']])->save(); + + $assets = $this->container->queryAssets()->whereJsonDoesntContain('test_taxonomy', ['taxonomy-1'])->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['b', 'd', 'e'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->whereJsonDoesntContain('test_taxonomy', 'taxonomy-1')->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['b', 'd', 'e'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where_json_contains() + { + Asset::find('test::a.jpg')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Asset::find('test::b.txt')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Asset::find('test::c.txt')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Asset::find('test::d.jpg')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Asset::find('test::e.jpg')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $assets = $this->container->queryAssets()->whereJsonContains('test_taxonomy', ['taxonomy-1'])->orWhereJsonContains('test_taxonomy', ['taxonomy-5'])->get(); + + $this->assertCount(3, $assets); + $this->assertEquals(['a', 'c', 'e'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_or_where_json_doesnt_contain() + { + Asset::find('test::a.jpg')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Asset::find('test::b.txt')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Asset::find('test::c.txt')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Asset::find('test::d.jpg')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Asset::find('test::e.jpg')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + Asset::find('test::f.jpg')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $assets = $this->container->queryAssets()->whereJsonContains('test_taxonomy', ['taxonomy-1'])->orWhereJsonDoesntContain('test_taxonomy', ['taxonomy-5'])->get(); + + $this->assertCount(4, $assets); + $this->assertEquals(['a', 'c', 'b', 'd'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_json_length() + { + Asset::find('test::a.jpg')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Asset::find('test::b.txt')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Asset::find('test::c.txt')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Asset::find('test::d.jpg')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Asset::find('test::e.jpg')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $assets = $this->container->queryAssets()->whereJsonLength('test_taxonomy', 1)->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['b', 'e'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_array_of_wheres() + { + $assets = $this->container->queryAssets() + ->where([ + 'filename' => 'a', + ['extension', 'jpg'], + ]) + ->get(); + + $this->assertCount(1, $assets); + $this->assertEquals(['a'], $assets->map->filename()->all()); + } + + /** @test **/ + public function results_are_found_using_where_with_json_value() + { + Asset::find('test::a.jpg')->data(['text' => 'Text 1', 'content' => ['value' => 1]])->save(); + Asset::find('test::b.txt')->data(['text' => 'Text 2', 'content' => ['value' => 2]])->save(); + Asset::find('test::c.txt')->data(['content' => ['value' => 1]])->save(); + Asset::find('test::d.jpg')->data(['text' => 'Text 4'])->save(); + // the following two assets use scalars for the content field to test that they get successfully ignored. + Asset::find('test::e.jpg')->data(['content' => 'string'])->save(); + Asset::find('test::f.jpg')->data(['content' => 123])->save(); + + $assets = $this->container->queryAssets()->where('content->value', 1)->get(); + + $this->assertCount(2, $assets); + $this->assertEquals(['a', 'c'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->where('content->value', '!=', 1)->get(); + + $this->assertCount(4, $assets); + $this->assertEquals(['b', 'd', 'e', 'f'], $assets->map->filename()->all()); + } + + /** @test **/ + public function assets_are_found_using_where_column() + { + Asset::find('test::a.jpg')->data(['foo' => 'Post 1', 'other_foo' => 'Not Post 1'])->save(); + Asset::find('test::b.txt')->data(['foo' => 'Post 2', 'other_foo' => 'Not Post 2'])->save(); + Asset::find('test::c.txt')->data(['foo' => 'Post 3', 'other_foo' => 'Post 3'])->save(); + Asset::find('test::d.jpg')->data(['foo' => 'Post 4', 'other_foo' => 'Post 4'])->save(); + Asset::find('test::e.jpg')->data(['foo' => 'Post 5', 'other_foo' => 'Not Post 5'])->save(); + Asset::find('test::f.jpg')->data(['foo' => 'Post 6', 'other_foo' => 'Not Post 6'])->save(); + + $entries = $this->container->queryAssets()->whereColumn('foo', 'other_foo')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 3', 'Post 4'], $entries->map->foo->all()); + + $entries = $this->container->queryAssets()->whereColumn('foo', '!=', 'other_foo')->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 5', 'Post 6'], $entries->map->foo->all()); + } + + /** @test */ + public function it_can_get_assets_using_when() + { + $assets = $this->container->queryAssets()->when(true, function ($query) { + $query->where('filename', 'a'); + })->get(); + + $this->assertCount(1, $assets); + $this->assertEquals(['a'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->when(false, function ($query) { + $query->where('filename', 'a'); + })->get(); + + $this->assertCount(6, $assets); + $this->assertEquals(['a', 'b', 'c', 'd', 'e', 'f'], $assets->map->filename()->all()); + } + + /** @test */ + public function it_can_get_assets_using_unless() + { + $assets = $this->container->queryAssets()->unless(true, function ($query) { + $query->where('filename', 'a'); + })->get(); + + $this->assertCount(6, $assets); + $this->assertEquals(['a', 'b', 'c', 'd', 'e', 'f'], $assets->map->filename()->all()); + + $assets = $this->container->queryAssets()->unless(false, function ($query) { + $query->where('filename', 'a'); + })->get(); + + $this->assertCount(1, $assets); + $this->assertEquals(['a'], $assets->map->filename()->all()); + } + + /** @test */ + public function it_can_get_assets_using_tap() + { + $assets = $this->container->queryAssets()->tap(function ($query) { + $query->where('filename', 'a'); + })->get(); + + $this->assertCount(1, $assets); + $this->assertEquals(['a'], $assets->map->filename()->all()); + } +} diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php new file mode 100644 index 00000000..b47c2f37 --- /dev/null +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -0,0 +1,691 @@ +save(); + + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'author' => 'John Doe'])->create(); + $entry = EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'author' => 'John Doe'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'author' => 'John Doe'])->create(); + + return $entry; + } + + /** @test **/ + public function entry_is_found_within_all_created_entries_using_entry_facade_with_find_method() + { + $searchedEntry = $this->createDummyCollectionAndEntries(); + $retrievedEntry = Entry::query()->find($searchedEntry->id()); + + // models wont be the same, so null them as we know that + $retrievedEntry->model(null); + $searchedEntry->model(null); + + $this->assertSame(json_encode($searchedEntry), json_encode($retrievedEntry)); + } + + /** @test **/ + public function entry_is_found_within_all_created_entries_and_select_query_columns_are_set_using_entry_facade_with_find_method_with_columns_param() + { + $searchedEntry = $this->createDummyCollectionAndEntries(); + $columns = ['title']; + $retrievedEntry = Entry::query()->find($searchedEntry->id(), $columns); + + $retrievedEntry->model(null); + $searchedEntry->model(null); + + $this->assertSame(json_encode($searchedEntry), json_encode($retrievedEntry)); + $this->assertSame($retrievedEntry->selectedQueryColumns(), $columns); + } + + /** @test **/ + public function entries_are_found_using_or_where() + { + $this->createDummyCollectionAndEntries(); + + $entries = Entry::query()->where('title', 'Post 1')->orWhere('title', 'Post 3')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_or_where_in() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5'])->create(); + + $entries = Entry::query()->whereIn('title', ['Post 1', 'Post 2'])->orWhereIn('title', ['Post 1', 'Post 4', 'Post 5'])->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 4', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_or_where_not_in() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5'])->create(); + + $entries = Entry::query()->whereNotIn('title', ['Post 1', 'Post 2'])->orWhereNotIn('title', ['Post 1', 'Post 5'])->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['Post 2', 'Post 3', 'Post 4', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_date() + { + $this->createWhereDateTestEntries(); + + $entries = Entry::query()->whereDate('test_date', '2021-11-15')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + + $entries = Entry::query()->whereDate('test_date', 1637000264)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + + $entries = Entry::query()->whereDate('test_date', '>=', '2021-11-15')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_month() + { + $this->createWhereDateTestEntries(); + + $entries = Entry::query()->whereMonth('test_date', 11)->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->all()); + + $entries = Entry::query()->whereMonth('test_date', '<', 11)->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_day() + { + $this->createWhereDateTestEntries(); + + $entries = Entry::query()->whereDay('test_date', 15)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + + $entries = Entry::query()->whereDay('test_date', '<', 15)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 2', 'Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_year() + { + $this->createWhereDateTestEntries(); + + $entries = Entry::query()->whereYear('test_date', 2021)->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->all()); + + $entries = Entry::query()->whereYear('test_date', '<', 2021)->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_time() + { + $this->createWhereDateTestEntries(); + + $entries = Entry::query()->whereTime('test_date', '09:00:00')->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 2'], $entries->map->title->all()); + + $entries = Entry::query()->whereTime('test_date', '>', '09:00:00')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 4'], $entries->map->title->all()); + } + + private function createWhereDateTestEntries() + { + $blueprint = Blueprint::makeFromFields(['test_date' => ['type' => 'date', 'time_enabled' => true]]); + Blueprint::shouldReceive('in')->with('collections/posts')->andReturn(collect(['posts' => $blueprint])); + + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'test_date' => '2021-11-15 20:31:04'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'test_date' => '2021-11-14 09:00:00'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'test_date' => '2021-11-15 00:00:00'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'test_date' => '2020-09-13 14:44:24'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'test_date' => null])->create(); + } + + public function entries_are_found_using_where_null() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'text' => 'Text 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'text' => 'Text 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'text' => 'Text 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5'])->create(); + + $entries = Entry::query()->whereNull('text')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 3', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_not_null() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'text' => 'Text 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'text' => 'Text 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'text' => 'Text 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5'])->create(); + + $entries = Entry::query()->whereNotNull('text')->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_or_where_null() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'text' => 'Text 1', 'content' => 'Content 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'text' => 'Text 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'content' => 'Content 1'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'text' => 'Text 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5'])->create(); + + $entries = Entry::query()->whereNull('text')->orWhereNull('content')->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['Post 2', 'Post 3', 'Post 4', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_or_where_not_null() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'text' => 'Text 1', 'content' => 'Content 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'text' => 'Text 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'content' => 'Content 1'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'text' => 'Text 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5'])->create(); + + $entries = Entry::query()->whereNotNull('content')->orWhereNotNull('text')->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3', 'Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_column() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'other_title' => 'Not Post 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'other_title' => 'Not Post 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'other_title' => 'Post 3'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'other_title' => 'Post 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'other_title' => 'Not Post 5'])->create(); + + $entries = Entry::query()->whereColumn('title', 'other_title')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 3', 'Post 4'], $entries->map->title->all()); + + $entries = Entry::query()->whereColumn('title', '!=', 'other_title')->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_nested_where() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5'])->create(); + EntryFactory::id('6')->slug('post-6')->collection('posts')->data(['title' => 'Post 6'])->create(); + + $entries = Entry::query() + ->where(function ($query) { + $query->where('title', 'Post 1'); + }) + ->orWhere(function ($query) { + $query->where('title', 'Post 3')->orWhere('title', 'Post 4'); + }) + ->orWhere('title', 'Post 6') + ->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['1', '3', '4', '6'], $entries->map->id()->all()); + } + + /** @test **/ + public function entries_are_found_using_nested_where_in() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5'])->create(); + EntryFactory::id('6')->slug('post-6')->collection('posts')->data(['title' => 'Post 6'])->create(); + EntryFactory::id('7')->slug('post-7')->collection('posts')->data(['title' => 'Post 7'])->create(); + EntryFactory::id('8')->slug('post-8')->collection('posts')->data(['title' => 'Post 8'])->create(); + EntryFactory::id('9')->slug('post-9')->collection('posts')->data(['title' => 'Post 9'])->create(); + + $entries = Entry::query() + ->where(function ($query) { + $query->whereIn('title', ['Post 1', 'Post 2']); + }) + ->orWhere(function ($query) { + $query->where('title', 'Post 4')->orWhereIn('title', ['Post 6', 'Post 7']); + }) + ->orWhere('title', 'Post 9') + ->get(); + + $this->assertCount(6, $entries); + $this->assertEquals(['1', '2', '4', '6', '7', '9'], $entries->map->id()->all()); + } + + /** @test **/ + public function entries_are_found_using_where_between() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'number_field' => 8])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'number_field' => 9])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'number_field' => 10])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'number_field' => 11])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'number_field' => 12])->create(); + + $entries = Entry::query()->whereBetween('number_field', [9, 11])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 2', 'Post 3', 'Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_not_between() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'number_field' => 8])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'number_field' => 9])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'number_field' => 10])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'number_field' => 11])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'number_field' => 12])->create(); + + $entries = Entry::query()->whereNotBetween('number_field', [9, 11])->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_or_where_between() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'number_field' => 8])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'number_field' => 9])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'number_field' => 10])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'number_field' => 11])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'number_field' => 12])->create(); + + $entries = Entry::query()->whereBetween('number_field', [9, 10])->orWhereBetween('number_field', [11, 12])->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['Post 2', 'Post 3', 'Post 4', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_or_where_not_between() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'number_field' => 8])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'number_field' => 9])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'number_field' => 10])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'number_field' => 11])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'number_field' => 12])->create(); + + $entries = Entry::query()->where('slug', 'post-5')->orWhereNotBetween('number_field', [10, 12])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_json_contains() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'test_taxonomy' => ['taxonomy-3']])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'test_taxonomy' => ['taxonomy-5']])->create(); + + $entries = Entry::query()->whereJsonContains('test_taxonomy', ['taxonomy-1', 'taxonomy-5'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 3', 'Post 5'], $entries->map->title->all()); + + $entries = Entry::query()->whereJsonContains('test_taxonomy', 'taxonomy-1')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_json_doesnt_contain() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'test_taxonomy' => ['taxonomy-3']])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'test_taxonomy' => ['taxonomy-5']])->create(); + + $entries = Entry::query()->whereJsonDoesntContain('test_taxonomy', ['taxonomy-1'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 2', 'Post 4', 'Post 5'], $entries->map->title->all()); + + $entries = Entry::query()->whereJsonDoesntContain('test_taxonomy', 'taxonomy-1')->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 2', 'Post 4', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_or_where_json_contains() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'test_taxonomy' => ['taxonomy-3']])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'test_taxonomy' => ['taxonomy-5']])->create(); + + $entries = Entry::query()->whereJsonContains('test_taxonomy', ['taxonomy-1'])->orWhereJsonContains('test_taxonomy', ['taxonomy-5'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 3', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_or_where_json_doesnt_contain() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'test_taxonomy' => ['taxonomy-3']])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'test_taxonomy' => ['taxonomy-5']])->create(); + + $entries = Entry::query()->whereJsonContains('test_taxonomy', ['taxonomy-1'])->orWhereJsonDoesntContain('test_taxonomy', ['taxonomy-5'])->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['Post 1', 'Post 3', 'Post 2', 'Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_json_length() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'test_taxonomy' => ['taxonomy-3']])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'test_taxonomy' => ['taxonomy-5']])->create(); + + $entries = Entry::query()->whereJsonLength('test_taxonomy', 1)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 2', 'Post 5'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_array_of_wheres() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'content' => 'Test'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'content' => 'Test two'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'content' => 'Test'])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'content' => 'Test two'])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'content' => 'Test'])->create(); + EntryFactory::id('6')->slug('post-6')->collection('posts')->data(['title' => 'Post 6', 'content' => 'Test two'])->create(); + + $entries = Entry::query() + ->where([ + 'content' => 'Test', + ['title', '<>', 'Post 1'], + ]) + ->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['3', '5'], $entries->map->id()->all()); + } + + /** @test **/ + public function entries_are_found_using_where_with_json_value() + { + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'content' => ['value' => 1]])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'content' => ['value' => 2]])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'content' => ['value' => 3]])->create(); + EntryFactory::id('4')->slug('post-4')->collection('posts')->data(['title' => 'Post 4', 'content' => ['value' => 2]])->create(); + EntryFactory::id('5')->slug('post-5')->collection('posts')->data(['title' => 'Post 5', 'content' => ['value' => 1]])->create(); + // the following two entries use scalars for the content field to test that they get successfully ignored. + EntryFactory::id('6')->slug('post-6')->collection('posts')->data(['title' => 'Post 6', 'content' => 'string'])->create(); + EntryFactory::id('7')->slug('post-7')->collection('posts')->data(['title' => 'Post 7', 'content' => 123])->create(); + + $entries = Entry::query()->where('content->value', 1)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 5'], $entries->map->title->all()); + + $entries = Entry::query()->where('content->value', '<>', 1)->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 2', 'Post 3', 'Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_when() + { + $this->createDummyCollectionAndEntries(); + + $entries = Entry::query()->when(true, function ($query) { + $query->where('title', 'Post 1'); + })->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 1'], $entries->map->title->all()); + + $entries = Entry::query()->when(false, function ($query) { + $query->where('title', 'Post 1'); + })->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_unless() + { + $this->createDummyCollectionAndEntries(); + + $entries = Entry::query()->unless(true, function ($query) { + $query->where('title', 'Post 1'); + })->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->all()); + + $entries = Entry::query()->unless(false, function ($query) { + $query->where('title', 'Post 1'); + })->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 1'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_tap() + { + $this->createDummyCollectionAndEntries(); + + $entries = Entry::query()->tap(function ($query) { + $query->where('title', 'Post 1'); + })->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 1'], $entries->map->title->all()); + } + + /** @test */ + public function it_substitutes_entries_by_id() + { + Collection::make('posts')->routes('/posts/{slug}')->save(); + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->create(); + + $substitute = EntryFactory::id('2')->slug('replaced-post-2')->collection('posts')->data(['title' => 'Replaced Post 2'])->make(); + + $found = Entry::query()->where('id', 2)->first(); + $this->assertNotNull($found); + $this->assertNotSame($found, $substitute); + + Entry::substitute($substitute); + + $found = Entry::query()->where('id', 2)->first(); + $this->assertNotNull($found); + $this->assertSame($found, $substitute); + } + + /** @test */ + public function it_substitutes_entries_by_uri() + { + Collection::make('posts')->routes('/posts/{slug}')->save(); + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->create(); + + $substitute = EntryFactory::id('2')->slug('replaced-post-2')->collection('posts')->data(['title' => 'Replaced Post 2'])->make(); + + $found = Entry::findByUri('/posts/post-2'); + $this->assertNotNull($found); + $this->assertNotSame($found, $substitute); + + $this->assertNull(Entry::findByUri('/posts/replaced-post-2')); + + Entry::substitute($substitute); + + $found = Entry::findByUri('/posts/replaced-post-2'); + $this->assertNotNull($found); + $this->assertSame($found, $substitute); + } + + /** @test */ + public function it_substitutes_entries_by_uri_and_site() + { + Site::setConfig(['sites' => [ + 'en' => ['url' => 'http://localhost/', 'locale' => 'en'], + 'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'], + ]]); + + Collection::make('posts')->routes('/posts/{slug}')->sites(['en', 'fr'])->save(); + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->locale('en')->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->locale('en')->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->locale('en')->create(); + EntryFactory::id('4')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->locale('fr')->create(); + EntryFactory::id('5')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->locale('fr')->create(); + EntryFactory::id('6')->slug('post-3')->collection('posts')->data(['title' => 'Post 3'])->locale('fr')->create(); + + $substituteEn = EntryFactory::id('7')->slug('replaced-post-2')->collection('posts')->data(['title' => 'Replaced Post 2'])->locale('en')->make(); + $substituteFr = EntryFactory::id('8')->slug('replaced-post-2')->collection('posts')->data(['title' => 'Replaced Post 2'])->locale('fr')->make(); + + $found = Entry::findByUri('/posts/post-2'); + $this->assertNotNull($found); + $this->assertNotSame($found, $substituteEn); + + $found = Entry::findByUri('/posts/post-2', 'en'); + $this->assertNotNull($found); + $this->assertNotSame($found, $substituteEn); + + $found = Entry::findByUri('/posts/post-2', 'fr'); + $this->assertNotNull($found); + $this->assertNotSame($found, $substituteFr); + + $this->assertNull(Entry::findByUri('/posts/replaced-post-2')); + $this->assertNull(Entry::findByUri('/posts/replaced-post-2', 'en')); + $this->assertNull(Entry::findByUri('/posts/replaced-post-2', 'fr')); + + Entry::substitute($substituteEn); + Entry::substitute($substituteFr); + + $found = Entry::findByUri('/posts/replaced-post-2'); + $this->assertNotNull($found); + $this->assertSame($found, $substituteEn); + + $found = Entry::findByUri('/posts/replaced-post-2', 'en'); + $this->assertNotNull($found); + $this->assertSame($found, $substituteEn); + + $found = Entry::findByUri('/posts/replaced-post-2', 'fr'); + $this->assertNotNull($found); + $this->assertSame($found, $substituteFr); + } + + /** @test */ + public function entries_are_found_using_offset() + { + $this->createDummyCollectionAndEntries(); + + $entries = Entry::query()->get(); + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->all()); + + $entries = Entry::query()->limit(10)->offset(1)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 2', 'Post 3'], $entries->map->title->all()); + } +} diff --git a/tests/Data/Globals/GlobalSetTest.php b/tests/Data/Globals/GlobalSetTest.php new file mode 100644 index 00000000..28664597 --- /dev/null +++ b/tests/Data/Globals/GlobalSetTest.php @@ -0,0 +1,76 @@ + 'en', + 'sites' => [ + 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], + ], + ]); + + $set = (new GlobalSet)->title('The title'); + + $variables = $set->makeLocalization('en')->data([ + 'array' => ['first one', 'second one'], + 'string' => 'The string', + ]); + + $set->addLocalization($variables); + + $expected = <<<'EOT' +title: 'The title' +data: + array: + - 'first one' + - 'second one' + string: 'The string' + +EOT; + $this->assertEquals($expected, $set->fileContents()); + } + + /** @test */ + public function it_gets_file_contents_for_saving_with_multiple_sites() + { + Site::setConfig([ + 'default' => 'en', + 'sites' => [ + 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://test.com/'], + 'fr' => ['name' => 'French', 'locale' => 'fr_FR', 'url' => 'http://fr.test.com/'], + 'de' => ['name' => 'German', 'locale' => 'de_DE', 'url' => 'http://test.com/de/'], + ], + ]); + + $set = (new GlobalSet)->title('The title'); + + // We set the data but it's basically irrelevant since it won't get saved to this file. + $set->in('en', function ($loc) { + $loc->data([ + 'array' => ['first one', 'second one'], + 'string' => 'The string', + ]); + }); + $set->in('fr', function ($loc) { + $loc->data([ + 'array' => ['le first one', 'le second one'], + 'string' => 'Le string', + ]); + }); + + $expected = <<<'EOT' +title: 'The title' + +EOT; + $this->assertEquals($expected, $set->fileContents()); + } +} diff --git a/tests/Data/Globals/VariablesTest.php b/tests/Data/Globals/VariablesTest.php new file mode 100644 index 00000000..969305cb --- /dev/null +++ b/tests/Data/Globals/VariablesTest.php @@ -0,0 +1,364 @@ +data([ + 'array' => ['first one', 'second one'], + 'string' => 'The string', + 'null' => null, // this... + 'empty' => [], // and this should get stripped out because it's the root. there's no origin to fall back to. + ]); + + $expected = <<<'EOT' +array: + - 'first one' + - 'second one' +string: 'The string' + +EOT; + $this->assertEquals($expected, $entry->fileContents()); + } + + /** @test */ + public function it_gets_file_contents_for_saving_a_localized_set() + { + $global = GlobalSet::make('test'); + + $a = $global->makeLocalization('a')->data([ + 'array' => ['first one', 'second one'], + 'string' => 'The string', + 'null' => null, // this... + 'empty' => [], // and this should get stripped out because there's no origin to fall back to. + ]); + + $b = $global->makeLocalization('b')->origin($a)->data([ + 'array' => ['first one', 'second one'], + 'string' => 'The string', + 'null' => null, // this... + 'empty' => [], // and this should not get stripped out, otherwise it would fall back to the origin. + ]); + + $c = $global->makeLocalization('c')->data([ + 'array' => ['first one', 'second one'], + 'string' => 'The string', + 'null' => null, // this... + 'empty' => [], // and this should get stripped out because there's no origin to fall back to. + ]); + + $expected = <<<'EOT' +array: + - 'first one' + - 'second one' +string: 'The string' + +EOT; + $this->assertEquals($expected, $a->fileContents()); + + $expected = <<<'EOT' +array: + - 'first one' + - 'second one' +string: 'The string' +'null': null +empty: { } +origin: a + +EOT; + $this->assertEquals($expected, $b->fileContents()); + + $expected = <<<'EOT' +array: + - 'first one' + - 'second one' +string: 'The string' + +EOT; + $this->assertEquals($expected, $c->fileContents()); + } + + /** @test */ + public function if_the_value_is_explicitly_set_to_null_then_it_should_not_fall_back() + { + $global = GlobalSet::make('test'); + + $a = $global->makeLocalization('a')->data([ + 'one' => 'alfa', + 'two' => 'bravo', + 'three' => 'charlie', + 'four' => 'delta', + ]); + + // originates from a + $b = $global->makeLocalization('b')->origin($a)->data([ + 'one' => 'echo', + 'two' => null, + ]); + + // originates from b, which originates from a + $c = $global->makeLocalization('c')->origin($b)->data([ + 'three' => 'foxtrot', + ]); + + // does not originate from anything + $d = $global->makeLocalization('d')->data([ + 'one' => 'golf', + 'two' => 'hotel', + 'three' => 'india', + ]); + + // originates from d. just to test that it doesn't unintentionally fall back to the default/first. + $e = $global->makeLocalization('e')->origin($d)->data([ + 'one' => 'juliett', + 'two' => null, + ]); + + $this->assertEquals([ + 'one' => 'alfa', + 'two' => 'bravo', + 'three' => 'charlie', + 'four' => 'delta', + ], $a->values()->all()); + $this->assertEquals('alfa', $a->value('one')); + $this->assertEquals('bravo', $a->value('two')); + $this->assertEquals('charlie', $a->value('three')); + $this->assertEquals('delta', $a->value('four')); + + $this->assertEquals([ + 'one' => 'echo', + 'two' => null, + 'three' => 'charlie', + 'four' => 'delta', + ], $b->values()->all()); + $this->assertEquals('echo', $b->value('one')); + $this->assertEquals(null, $b->value('two')); + $this->assertEquals('charlie', $b->value('three')); + $this->assertEquals('delta', $b->value('four')); + + $this->assertEquals([ + 'one' => 'echo', + 'two' => null, + 'three' => 'foxtrot', + 'four' => 'delta', + ], $c->values()->all()); + $this->assertEquals('echo', $c->value('one')); + $this->assertEquals(null, $c->value('two')); + $this->assertEquals('foxtrot', $c->value('three')); + $this->assertEquals('delta', $c->value('four')); + + $this->assertEquals([ + 'one' => 'golf', + 'two' => 'hotel', + 'three' => 'india', + ], $d->values()->all()); + $this->assertEquals('golf', $d->value('one')); + $this->assertEquals('hotel', $d->value('two')); + $this->assertEquals('india', $d->value('three')); + $this->assertEquals(null, $d->value('four')); + + $this->assertEquals([ + 'one' => 'juliett', + 'two' => null, + 'three' => 'india', + ], $e->values()->all()); + $this->assertEquals('juliett', $e->value('one')); + $this->assertEquals(null, $e->value('two')); + $this->assertEquals('india', $e->value('three')); + $this->assertEquals(null, $e->value('four')); + } + + /** @test */ + public function it_sets_data_values_using_magic_properties() + { + $variables = new Variables; + $this->assertNull($variables->get('foo')); + + $variables->foo = 'bar'; + + $this->assertTrue($variables->has('foo')); + $this->assertEquals('bar', $variables->get('foo')); + } + + /** @test */ + public function it_gets_evaluated_augmented_value_using_magic_property() + { + (new class extends Fieldtype + { + protected static $handle = 'test'; + + public function augment($value) + { + return $value.' (augmented)'; + } + })::register(); + + $blueprint = Facades\Blueprint::makeFromFields(['charlie' => ['type' => 'test']]); + BlueprintRepository::shouldReceive('find')->with('globals.settings')->andReturn($blueprint); + $global = GlobalSet::make('settings'); + $variables = $global->makeLocalization('en'); + $variables->set('alfa', 'bravo'); + $variables->set('charlie', 'delta'); + + $this->assertEquals('bravo', $variables->alfa); + $this->assertEquals('bravo', $variables['alfa']); + $this->assertEquals('delta (augmented)', $variables->charlie); + $this->assertEquals('delta (augmented)', $variables['charlie']); + } + + /** + * @test + * @dataProvider queryBuilderProvider + **/ + public function it_has_magic_property_and_methods_for_fields_that_augment_to_query_builders($builder) + { + $builder->shouldReceive('get')->times(2)->andReturn('query builder results'); + app()->instance('mocked-builder', $builder); + + (new class extends Fieldtype + { + protected static $handle = 'test'; + + public function augment($value) + { + return app('mocked-builder'); + } + })::register(); + + $blueprint = Facades\Blueprint::makeFromFields(['foo' => ['type' => 'test']]); + BlueprintRepository::shouldReceive('find')->with('globals.settings')->andReturn($blueprint); + $global = GlobalSet::make('settings'); + $variables = $global->makeLocalization('en'); + $variables->set('foo', 'delta'); + + $this->assertEquals('query builder results', $variables->foo); + $this->assertEquals('query builder results', $variables['foo']); + $this->assertSame($builder, $variables->foo()); + } + + public function queryBuilderProvider() + { + return [ + 'statamic' => [Mockery::mock(\Statamic\Query\Builder::class)], + 'database' => [Mockery::mock(\Illuminate\Database\Query\Builder::class)], + 'eloquent' => [Mockery::mock(\Illuminate\Database\Eloquent\Builder::class)], + ]; + } + + /** @test */ + public function calling_unknown_method_throws_exception() + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Call to undefined method Statamic\Eloquent\Globals\Variables::thisFieldDoesntExist()'); + + GlobalSet::make('settings')->makeLocalization('en')->thisFieldDoesntExist(); + } + + /** @test */ + public function it_converts_to_an_array() + { + $fieldtype = new class extends Fieldtype + { + protected static $handle = 'test'; + + public function augment($value) + { + return [ + new Value('alfa'), + new Value([ + new Value('bravo'), + new Value('charlie'), + 'delta', + ]), + ]; + } + }; + $fieldtype::register(); + + $blueprint = Blueprint::makeFromFields([ + 'baz' => [ + 'type' => 'test', + ], + ]); + BlueprintRepository::shouldReceive('find')->with('globals.settings')->andReturn($blueprint); + $global = GlobalSet::make('settings'); + $variables = $global->makeLocalization('en'); + $variables->set('foo', 'bar'); + $variables->set('baz', 'qux'); + + $this->assertInstanceOf(Arrayable::class, $variables); + + $array = $variables->toArray(); + $this->assertEquals($variables->augmented()->keys(), array_keys($array)); + $this->assertEquals([ + 'alfa', + [ + 'bravo', + 'charlie', + 'delta', + ], + ], $array['baz'], 'Value objects are not resolved recursively'); + } + + /** @test */ + public function only_requested_relationship_fields_are_included_in_to_array() + { + $regularFieldtype = new class extends Fieldtype + { + protected static $handle = 'regular'; + + public function augment($value) + { + return 'augmented '.$value; + } + }; + $regularFieldtype::register(); + + $relationshipFieldtype = new class extends Fieldtype + { + protected static $handle = 'relationship'; + + protected $relationship = true; + + public function augment($values) + { + return collect($values)->map(fn ($value) => 'augmented '.$value)->all(); + } + }; + $relationshipFieldtype::register(); + + $blueprint = Blueprint::makeFromFields([ + 'alfa' => ['type' => 'regular'], + 'bravo' => ['type' => 'relationship'], + 'charlie' => ['type' => 'relationship'], + ]); + BlueprintRepository::shouldReceive('find')->with('globals.settings')->andReturn($blueprint); + $global = GlobalSet::make('settings'); + $variables = $global->makeLocalization('en'); + $variables->set('alfa', 'one'); + $variables->set('bravo', ['a', 'b']); + $variables->set('charlie', ['c', 'd']); + + $this->assertEquals([ + 'alfa' => 'augmented one', + 'bravo' => ['a', 'b'], + 'charlie' => ['augmented c', 'augmented d'], + ], Arr::only($variables->selectedQueryRelations(['charlie'])->toArray(), ['alfa', 'bravo', 'charlie'])); + } +} diff --git a/tests/Data/Taxonomies/TermQueryBuilderTest.php b/tests/Data/Taxonomies/TermQueryBuilderTest.php new file mode 100644 index 00000000..67f8f358 --- /dev/null +++ b/tests/Data/Taxonomies/TermQueryBuilderTest.php @@ -0,0 +1,580 @@ +save(); + Term::make('a')->taxonomy('tags')->data([])->save(); + Term::make('b')->taxonomy('tags')->data([])->save(); + Term::make('c')->taxonomy('tags')->data([])->save(); + + $terms = Term::query()->get(); + $this->assertInstanceOf(TermCollection::class, $terms); + $this->assertEveryItemIsInstanceOf(LocalizedTerm::class, $terms); + } + + /** @test */ + public function it_filters_using_wheres() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('b')->taxonomy('tags')->data(['test' => 'bar'])->save(); + Term::make('c')->taxonomy('tags')->data(['test' => 'foo'])->save(); + + $terms = Term::query()->where('test', 'foo')->get(); + $this->assertEquals(['a', 'c'], $terms->map->slug()->sort()->values()->all()); + } + + /** @test */ + public function it_filters_using_or_wheres() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('b')->taxonomy('tags')->data(['test' => 'bar'])->save(); + Term::make('c')->taxonomy('tags')->data(['test' => 'baz'])->save(); + Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save(); + + $terms = Term::query()->where('test', 'foo')->orWhere('test', 'bar')->get(); + $this->assertEquals(['a', 'b', 'd'], $terms->map->slug()->sort()->values()->all()); + } + + /** @test */ + public function it_filters_using_or_where_ins() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('b')->taxonomy('tags')->data(['test' => 'bar'])->save(); + Term::make('c')->taxonomy('tags')->data(['test' => 'baz'])->save(); + Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save(); + + $terms = Term::query()->whereIn('test', ['foo', 'bar'])->orWhereIn('test', ['foo', 'raz'])->get(); + + $this->assertEquals(['a', 'b', 'd', 'e'], $terms->map->slug()->values()->all()); + } + + /** @test **/ + public function it_filters_using_or_where_not_ins() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('b')->taxonomy('tags')->data(['test' => 'bar'])->save(); + Term::make('c')->taxonomy('tags')->data(['test' => 'baz'])->save(); + Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save(); + Term::make('f')->taxonomy('tags')->data(['test' => 'taz'])->save(); + + $terms = Term::query()->whereNotIn('test', ['foo', 'bar'])->whereNotIn('test', ['foo', 'raz'])->get(); + + $this->assertEquals(['c', 'f'], $terms->map->slug()->values()->all()); + } + + /** @test */ + public function it_filters_using_nested_wheres() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('b')->taxonomy('tags')->data(['test' => 'bar'])->save(); + Term::make('c')->taxonomy('tags')->data(['test' => 'baz'])->save(); + Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save(); + + $terms = Term::query() + ->where(function ($query) { + $query->where('test', 'foo'); + }) + ->orWhere(function ($query) { + $query->where('test', 'baz'); + }) + ->orWhere('test', 'raz') + ->get(); + + $this->assertCount(4, $terms); + $this->assertEquals(['a', 'c', 'd', 'e'], $terms->map->slug()->sort()->values()->all()); + } + + /** @test */ + public function it_filters_using_nested_where_ins() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('b')->taxonomy('tags')->data(['test' => 'bar'])->save(); + Term::make('c')->taxonomy('tags')->data(['test' => 'baz'])->save(); + Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save(); + Term::make('f')->taxonomy('tags')->data(['test' => 'chaz'])->save(); + + $terms = Term::query() + ->where(function ($query) { + $query->where('test', 'foo'); + }) + ->orWhere(function ($query) { + $query->whereIn('test', ['baz', 'raz']); + }) + ->orWhere('test', 'chaz') + ->get(); + + $this->assertCount(5, $terms); + $this->assertEquals(['a', 'c', 'd', 'e', 'f'], $terms->map->slug()->sort()->values()->all()); + } + + /** @test */ + public function it_filters_using_nested_where_not_ins() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('b')->taxonomy('tags')->data(['test' => 'bar'])->save(); + Term::make('c')->taxonomy('tags')->data(['test' => 'baz'])->save(); + Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save(); + Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save(); + + $terms = Term::query() + ->where('test', 'foo') + ->orWhere(function ($query) { + $query->whereNotIn('test', ['baz', 'raz']); + }) + ->get(); + + $this->assertCount(3, $terms); + $this->assertEquals(['a', 'b', 'd'], $terms->map->slug()->sort()->values()->all()); + } + + /** @test */ + public function it_filters_by_taxonomy() + { + Taxonomy::make('tags')->save(); + Taxonomy::make('categories')->save(); + Taxonomy::make('colors')->save(); + Term::make('a')->taxonomy('tags')->data([])->save(); + Term::make('b')->taxonomy('categories')->data([])->save(); + Term::make('c')->taxonomy('colors')->data([])->save(); + Term::make('d')->taxonomy('tags')->data([])->save(); + + $terms = Term::query()->where('taxonomy', 'tags')->get(); + $this->assertEquals(['a', 'd'], $terms->map->slug()->sort()->values()->all()); + + $terms = Term::query()->whereIn('taxonomy', ['tags', 'categories'])->get(); + $this->assertEquals(['a', 'b', 'd'], $terms->map->slug()->sort()->values()->all()); + } + + /** @test */ + public function it_sorts() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['test' => 4])->save(); + Term::make('b')->taxonomy('tags')->data(['test' => 2])->save(); + Term::make('c')->taxonomy('tags')->data(['test' => 1])->save(); + Term::make('d')->taxonomy('tags')->data(['test' => 5])->save(); + Term::make('e')->taxonomy('tags')->data(['test' => 3])->save(); + + $terms = Term::query()->orderBy('test')->get(); + $this->assertEquals(['c', 'b', 'e', 'a', 'd'], $terms->map->slug()->all()); + } + + /** @test **/ + public function terms_are_found_using_where_column() + { + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['title' => 'Post 1', 'other_title' => 'Not Post 1'])->save(); + Term::make('b')->taxonomy('tags')->data(['title' => 'Post 2', 'other_title' => 'Not Post 2'])->save(); + Term::make('c')->taxonomy('tags')->data(['title' => 'Post 3', 'other_title' => 'Post 3'])->save(); + Term::make('d')->taxonomy('tags')->data(['title' => 'Post 4', 'other_title' => 'Post 4'])->save(); + Term::make('e')->taxonomy('tags')->data(['title' => 'Post 5', 'other_title' => 'Not Post 5'])->save(); + + $terms = Term::query()->whereColumn('title', 'other_title')->get(); + + $this->assertCount(2, $terms); + $this->assertEquals(['c', 'd'], $terms->map->slug()->all()); + + $terms = Term::query()->whereColumn('title', '!=', 'other_title')->get(); + + $this->assertCount(3, $terms); + $this->assertEquals(['a', 'b', 'e'], $terms->map->slug()->all()); + } + + /** @test */ + public function it_filters_usage_in_collections() + { + Taxonomy::make('tags')->save(); + Taxonomy::make('cats')->save(); + + Collection::make('blog')->taxonomies(['tags', 'cats'])->save(); + Collection::make('news')->taxonomies(['tags', 'cats'])->save(); + + EntryFactory::collection('blog')->data(['tags' => ['a'], 'cats' => ['f']])->create(); + EntryFactory::collection('blog')->data(['tags' => ['c'], 'cats' => ['g']])->create(); + EntryFactory::collection('news')->data(['tags' => ['a'], 'cats' => ['f']])->create(); + EntryFactory::collection('news')->data(['tags' => ['b'], 'cats' => ['h']])->create(); + + Term::make('a')->taxonomy('tags')->data([])->save(); + Term::make('b')->taxonomy('tags')->data([])->save(); + Term::make('c')->taxonomy('tags')->data([])->save(); + Term::make('d')->taxonomy('tags')->data([])->save(); + Term::make('e')->taxonomy('cats')->data([])->save(); + Term::make('f')->taxonomy('cats')->data([])->save(); + Term::make('g')->taxonomy('cats')->data([])->save(); + Term::make('h')->taxonomy('cats')->data([])->save(); + + $this->assertEquals(['cats::f', 'cats::g', 'tags::a', 'tags::c'], + Term::query() + ->where('collection', 'blog') + ->get()->map->id()->sort()->values()->all() + ); + + $this->assertEquals(['tags::a', 'tags::c'], + Term::query() + ->where('collection', 'blog') + ->where('taxonomy', 'tags') + ->get()->map->id()->sort()->values()->all() + ); + + $this->assertEquals(['cats::f', 'cats::h', 'tags::a', 'tags::b'], + Term::query() + ->where('collection', 'news') + ->get()->map->id()->sort()->values()->all() + ); + + $this->assertEquals(['tags::a', 'tags::b'], + Term::query() + ->where('collection', 'news') + ->where('taxonomy', 'tags') + ->get()->map->id()->sort()->values()->all() + ); + + $this->assertEquals(['cats::f', 'cats::g', 'cats::h', 'tags::a', 'tags::b', 'tags::c'], + Term::query() + ->whereIn('collection', ['blog', 'news']) + ->get()->map->id()->sort()->values()->all() + ); + + $this->assertEquals(['tags::a', 'tags::b', 'tags::c'], + Term::query() + ->whereIn('collection', ['blog', 'news']) + ->where('taxonomy', 'tags') + ->get()->map->id()->sort()->values()->all() + ); + } + + /** @test */ + public function it_substitutes_terms_by_id() + { + Taxonomy::make('tags')->save(); + Term::make('tag-1')->dataForLocale('en', [])->taxonomy('tags')->save(); + Term::make('tag-2')->dataForLocale('en', [])->taxonomy('tags')->save(); + Term::make('tag-3')->dataForLocale('en', [])->taxonomy('tags')->save(); + + $substitute = Term::make('tag-2')->taxonomy('tags')->dataForLocale('en', ['title' => 'Replaced'])->in('en'); + $substitute->save(); + + $found = Term::query()->where('id', 'tags::tag-2')->first(); + + $this->assertNotNull($found); + $this->assertNotEquals($found, $substitute); + + Term::substitute($substitute); + + $found = Term::query()->where('id', 'tags::tag-2')->first(); + + $this->assertNotNull($found); + $this->assertEquals(collect($found->toArray())->except(['updated_at'])->all(), collect($substitute->toArray())->except(['updated_at'])->all()); + } + + /** @test */ + public function it_substitutes_terms_by_uri() + { + Taxonomy::make('tags')->save(); + Term::make('tag-1')->taxonomy('tags')->dataForLocale('en', [])->save(); + Term::make('tag-2')->taxonomy('tags')->dataForLocale('en', [])->save(); + Term::make('tag-3')->taxonomy('tags')->dataForLocale('en', [])->save(); + + $substitute = Term::make('tag-2')->slug('replaced-tag-2')->taxonomy('tags')->dataForLocale('en', []); + + $found = Term::findByUri('/tags/tag-2'); + $this->assertNotNull($found); + $this->assertNotEquals($found, $substitute); + + $this->assertNull(Term::findByUri('/tags/replaced-tag-2')); + + Term::substitute($substitute); + + $found = Term::findByUri('/tags/replaced-tag-2'); + $this->assertNotNull($found); + $this->assertEquals($found, $substitute); + } + + /** @test */ + public function it_substitutes_terms_by_uri_and_site() + { + Site::setConfig(['sites' => [ + 'en' => ['url' => 'http://localhost/', 'locale' => 'en'], + 'fr' => ['url' => 'http://localhost/fr/', 'locale' => 'fr'], + ]]); + + Taxonomy::make('tags')->sites(['en', 'fr'])->save(); + Term::make('tag-1')->slug('tag-1')->taxonomy('tags') + ->dataForlocale('en', []) + ->dataForlocale('fr', []) + ->save(); + Term::make('tag-2')->slug('tag-2')->taxonomy('tags') + ->dataForlocale('en', []) + ->dataForlocale('fr', []) + ->save(); + Term::make('tag-3')->slug('tag-3')->taxonomy('tags') + ->dataForlocale('en', []) + ->dataForlocale('fr', []) + ->save(); + + $substitute = Term::make('tag-2') + ->slug('replaced-tag-2') + ->taxonomy('tags') + ->dataForLocale('en', []) + ->dataForLocale('fr', []); + $substituteEn = $substitute->in('en'); + $substituteFr = $substitute->in('fr'); + + $found = Term::findByUri('/tags/tag-2'); + $this->assertNotNull($found); + $this->assertNotSame($found, $substituteEn); + + $found = Term::findByUri('/tags/tag-2', 'en'); + $this->assertNotNull($found); + $this->assertNotSame($found, $substituteEn); + + $found = Term::findByUri('/tags/tag-2', 'fr'); + $this->assertNotNull($found); + $this->assertNotSame($found, $substituteFr); + + $this->assertNull(Term::findByUri('/tags/replaced-tag-2')); + $this->assertNull(Term::findByUri('/tags/replaced-tag-2', 'en')); + $this->assertNull(Term::findByUri('/tags/replaced-tag-2', 'fr')); + + Term::substitute($substituteEn); + Term::substitute($substituteFr); + + $found = Term::findByUri('/tags/replaced-tag-2'); + $this->assertNotNull($found); + $this->assertSame($found, $substituteEn); + + $found = Term::findByUri('/tags/replaced-tag-2', 'en'); + $this->assertNotNull($found); + $this->assertSame($found, $substituteEn); + + $found = Term::findByUri('/tags/replaced-tag-2', 'fr'); + $this->assertNotNull($found); + $this->assertSame($found, $substituteFr); + } + + /** @test **/ + public function entries_are_found_using_where_date() + { + $this->createWhereDateTestTerms(); + + $entries = Term::query()->whereDate('test_date', '2021-11-15')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + + $entries = Term::query()->whereDate('test_date', '>=', '2021-11-15')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_month() + { + $this->createWhereDateTestTerms(); + + $entries = Term::query()->whereMonth('test_date', 11)->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->all()); + + $entries = Term::query()->whereMonth('test_date', '<', 11)->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_day() + { + $this->createWhereDateTestTerms(); + + $entries = Term::query()->whereDay('test_date', 15)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 3'], $entries->map->title->all()); + + $entries = Term::query()->whereDay('test_date', '<', 15)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 2', 'Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_year() + { + $this->createWhereDateTestTerms(); + + $entries = Term::query()->whereYear('test_date', 2021)->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 1', 'Post 2', 'Post 3'], $entries->map->title->all()); + + $entries = Term::query()->whereYear('test_date', '<', 2021)->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 4'], $entries->map->title->all()); + } + + /** @test **/ + public function entries_are_found_using_where_time() + { + $this->createWhereDateTestTerms(); + + $entries = Term::query()->whereTime('test_date', '09:00:00')->get(); + + $this->assertCount(1, $entries); + $this->assertEquals(['Post 2'], $entries->map->title->all()); + + $entries = Term::query()->whereTime('test_date', '>', '09:00:00')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['Post 1', 'Post 4'], $entries->map->title->all()); + } + + private function createWhereDateTestTerms() + { + $blueprint = Blueprint::makeFromFields(['test_date' => ['type' => 'date', 'time_enabled' => true]]); + Blueprint::shouldReceive('in')->with('taxonomies/tags')->andReturn(collect(['tags' => $blueprint])); + + Taxonomy::make('tags')->save(); + Term::make('a')->taxonomy('tags')->data(['title' => 'Post 1', 'test_date' => '2021-11-15 20:31:04'])->save(); + Term::make('b')->taxonomy('tags')->data(['title' => 'Post 2', 'test_date' => '2021-11-14 09:00:00'])->save(); + Term::make('c')->taxonomy('tags')->data(['title' => 'Post 3', 'test_date' => '2021-11-15 00:00:00'])->save(); + Term::make('d')->taxonomy('tags')->data(['title' => 'Post 4', 'test_date' => '2020-09-13 14:44:24'])->save(); + Term::make('e')->taxonomy('tags')->data(['title' => 'Post 5', 'test_date' => null])->save(); + } + + /** @test **/ + public function terms_are_found_using_where_json_contains() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + Taxonomy::make('tags')->save(); + Term::make('1')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Term::make('2')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Term::make('3')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Term::make('4')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Term::make('5')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = Term::query()->whereJsonContains('test_taxonomy', ['taxonomy-1', 'taxonomy-5'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['1', '3', '5'], $entries->map->slug()->all()); + + $entries = Term::query()->whereJsonContains('test_taxonomy', 'taxonomy-1')->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['1', '3'], $entries->map->slug()->all()); + } + + /** @test **/ + public function terms_are_found_using_where_json_doesnt_contain() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + Taxonomy::make('tags')->save(); + Term::make('1')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Term::make('2')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Term::make('3')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Term::make('4')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Term::make('5')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = Term::query()->whereJsonDoesntContain('test_taxonomy', ['taxonomy-1'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['2', '4', '5'], $entries->map->slug()->all()); + + $entries = Term::query()->whereJsonDoesntContain('test_taxonomy', 'taxonomy-1')->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['2', '4', '5'], $entries->map->slug()->all()); + } + + /** @test **/ + public function terms_are_found_using_or_where_json_contains() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + Taxonomy::make('tags')->save(); + Term::make('1')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Term::make('2')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Term::make('3')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Term::make('4')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Term::make('5')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = Term::query()->whereJsonContains('test_taxonomy', ['taxonomy-1'])->orWhereJsonContains('test_taxonomy', ['taxonomy-5'])->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['1', '3', '5'], $entries->map->slug()->all()); + } + + /** @test **/ + public function terms_are_found_using_or_where_json_doesnt_contain() + { + if (config('database.default') === 'sqlite') { + $this->markTestSkipped('SQLite doesn\'t support JSON contains queries'); + } + + Taxonomy::make('tags')->save(); + Term::make('1')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Term::make('2')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Term::make('3')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Term::make('4')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Term::make('5')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = Term::query()->whereJsonContains('test_taxonomy', ['taxonomy-1'])->orWhereJsonDoesntContain('test_taxonomy', ['taxonomy-5'])->get(); + + $this->assertCount(4, $entries); + $this->assertEquals(['1', '2', '3', '4'], $entries->map->slug()->all()); + } + + /** @test **/ + public function terms_are_found_using_where_json_length() + { + Taxonomy::make('tags')->save(); + Term::make('1')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-2']])->save(); + Term::make('2')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3']])->save(); + Term::make('3')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-1', 'taxonomy-3']])->save(); + Term::make('4')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-3', 'taxonomy-4']])->save(); + Term::make('5')->taxonomy('tags')->data(['test_taxonomy' => ['taxonomy-5']])->save(); + + $entries = Term::query()->whereJsonLength('test_taxonomy', 1)->get(); + + $this->assertCount(2, $entries); + $this->assertEquals(['2', '5'], $entries->map->slug()->all()); + } +} diff --git a/tests/Entries/EntryModelTest.php b/tests/Entries/EntryModelTest.php index 7d8addc6..a77e536e 100644 --- a/tests/Entries/EntryModelTest.php +++ b/tests/Entries/EntryModelTest.php @@ -2,8 +2,8 @@ namespace Tests\Entries; -use PHPUnit\Framework\TestCase; use Statamic\Eloquent\Entries\EntryModel; +use Tests\TestCase; class EntryModelTest extends TestCase { @@ -13,8 +13,8 @@ public function it_gets_attributes_from_json_column() $model = new EntryModel([ 'slug' => 'the-slug', 'data' => [ - 'foo' => 'bar' - ] + 'foo' => 'bar', + ], ]); $this->assertEquals('the-slug', $model->slug); diff --git a/tests/Entries/EntryTest.php b/tests/Entries/EntryTest.php new file mode 100644 index 00000000..73a653a7 --- /dev/null +++ b/tests/Entries/EntryTest.php @@ -0,0 +1,58 @@ + 'the-slug', + 'data' => [ + 'foo' => 'bar', + ], + ]); + + $entry = (new Entry)->fromModel($model); + + $this->assertEquals('the-slug', $entry->slug()); + $this->assertEquals('bar', $entry->data()->get('foo')); + $this->assertEquals(['foo' => 'bar'], $entry->data()->except(['updated_at'])->toArray()); + } + + /** @test */ + public function it_saves_to_entry_model() + { + $model = new EntryModel([ + 'slug' => 'the-slug', + 'data' => [ + 'foo' => 'bar', + ], + 'site' => 'en', + 'uri' => '/blog/the-slug', + 'date' => null, + 'collection' => 'blog', + 'published' => false, + 'status' => 'draft', + 'origin_id' => null, + 'id' => null, + ]); + + $collection = Collection::make('blog')->title('blog')->routes([ + 'en' => '/blog/{slug}', + ])->save(); + + $entry = (new Entry)->fromModel($model)->collection($collection); + + $this->assertEquals(collect($model->toArray())->except(['updated_at'])->all(), collect($entry->toModel()->toArray())->except('updated_at')->all()); + } +} diff --git a/tests/Factories/EntryFactory.php b/tests/Factories/EntryFactory.php new file mode 100644 index 00000000..44d5f51c --- /dev/null +++ b/tests/Factories/EntryFactory.php @@ -0,0 +1,139 @@ +reset(); + } + + public function id($id) + { + $this->id = $id; + + return $this; + } + + public function slug($slug) + { + $this->slug = $slug; + + return $this; + } + + public function collection($collection) + { + $this->collection = $collection; + + return $this; + } + + public function data($data) + { + $this->data = $data; + + return $this; + } + + public function date($date) + { + $this->date = $date; + + return $this; + } + + public function published($published) + { + $this->published = $published; + + return $this; + } + + public function locale($locale) + { + $this->locale = $locale; + + return $this; + } + + public function origin($origin) + { + $this->origin = $origin; + + return $this; + } + + public function make() + { + $entry = Entry::make() + ->locale($this->locale) + ->collection($this->createCollection()) + ->slug($this->slug) + ->data($this->data) + ->date($this->date) + ->origin($this->origin) + ->published($this->published); + + if ($this->id) { + $entry->id($this->id); + } + + $this->reset(); + + return $entry; + } + + public function create() + { + return tap($this->make())->save(); + } + + protected function createCollection() + { + if ($this->collection instanceof StatamicCollection) { + return $this->collection; + } + + return Collection::findByHandle($this->collection) + ?? Collection::make($this->collection) + ->sites(['en']) + ->save(); + } + + private function reset() + { + $this->id = null; + $this->slug = null; + $this->data = []; + $this->date = null; + $this->published = true; + $this->order = null; + $this->locale = 'en'; + $this->origin = null; + $this->collection = null; + } +} diff --git a/tests/Factories/FieldsetFactory.php b/tests/Factories/FieldsetFactory.php new file mode 100644 index 00000000..cb06320a --- /dev/null +++ b/tests/Factories/FieldsetFactory.php @@ -0,0 +1,54 @@ +contents = $contents; + + return $this; + } + + public function withSections($sections) + { + if (! $this->contents) { + $this->contents = []; + } + + $this->contents['sections'] = $sections; + + return $this; + } + + public function withFieldtypes($fieldtypes) + { + foreach ($fieldtypes as $name => $fieldtype) { + $this->fieldtypes[$name] = $fieldtype; + } + + return $this; + } + + public function withFields($fields) + { + if (! $this->contents) { + $this->contents = []; + } + + $this->contents['fields'] = $fields; + + return $this; + } + + public function create() + { + return tap(new Fieldset) + ->contents($this->contents); + } +} diff --git a/tests/Factories/GlobalFactory.php b/tests/Factories/GlobalFactory.php new file mode 100644 index 00000000..e5498094 --- /dev/null +++ b/tests/Factories/GlobalFactory.php @@ -0,0 +1,55 @@ +id = $id; + + return $this; + } + + public function handle($handle) + { + $this->handle = $handle; + + return $this; + } + + public function data($data) + { + $this->data = $data; + + return $this; + } + + public function make() + { + $set = GlobalSet::make($this->handle); + + $set->addLocalization( + $set->makeLocalization('en')->data($this->data) + ); + + if ($this->id) { + $set->id($this->id); + } + + return $set; + } + + public function create() + { + return tap($this->make())->save(); + } +} diff --git a/tests/Repositories/AssetContainerRepositoryTest.php b/tests/Repositories/AssetContainerRepositoryTest.php new file mode 100644 index 00000000..e1ed68ef --- /dev/null +++ b/tests/Repositories/AssetContainerRepositoryTest.php @@ -0,0 +1,75 @@ +sites(['en', 'fr']); + $this->directory = __DIR__.'/../__fixtures__/content/assets'; + $this->repo = new AssetContainerRepository($stache); + + $this->repo->make('another')->title('Another Asset Container')->disk('local')->save(); + $this->repo->make('main')->title('Main Assets')->disk('local')->save(); + } + + /** @test */ + public function it_gets_all_asset_containers() + { + $containers = $this->repo->all(); + + $this->assertInstanceOf(IlluminateCollection::class, $containers); + $this->assertCount(2, $containers); + $this->assertEveryItemIsInstanceOf(AssetContainer::class, $containers); + + $ordered = $containers->sortBy->handle()->values(); + $this->assertEquals(['another', 'main'], $ordered->map->id()->all()); + $this->assertEquals(['another', 'main'], $ordered->map->handle()->all()); + $this->assertEquals(['Another Asset Container', 'Main Assets'], $ordered->map->title()->all()); + } + + /** @test */ + public function it_gets_an_asset_container_by_handle() + { + tap($this->repo->findByHandle('main'), function ($container) { + $this->assertInstanceOf(AssetContainer::class, $container); + $this->assertEquals('main', $container->id()); + $this->assertEquals('main', $container->handle()); + $this->assertEquals('Main Assets', $container->title()); + }); + + tap($this->repo->findByHandle('another'), function ($container) { + $this->assertInstanceOf(AssetContainer::class, $container); + $this->assertEquals('another', $container->id()); + $this->assertEquals('another', $container->handle()); + $this->assertEquals('Another Asset Container', $container->title()); + }); + + $this->assertNull($this->repo->findByHandle('unknown')); + } + + /** @test */ + public function it_saves_a_container_to_the_database() + { + $container = Facades\AssetContainer::make('new'); + $this->assertNull($this->repo->findByHandle('new')); + + $this->repo->save($container); + + $this->assertNotNull($item = $this->repo->findByHandle('new')); + $this->assertEquals($container, $item); + } +} diff --git a/tests/Repositories/CollectionRepositoryTest.php b/tests/Repositories/CollectionRepositoryTest.php new file mode 100644 index 00000000..aea05723 --- /dev/null +++ b/tests/Repositories/CollectionRepositoryTest.php @@ -0,0 +1,87 @@ +sites(['en', 'fr']); + $this->app->instance(Stache::class, $stache); + $this->repo = new CollectionRepository($stache); + + $this->repo->make('alphabetical')->title('Alphabetical')->routes('alphabetical/{slug}')->save(); + $this->repo->make('blog')->title('Blog')->dated(true)->taxonomies(['tags'])->save(); + $this->repo->make('numeric')->title('Numeric')->routes('numeric/{slug}')->save(); + $this->repo->make('pages')->title('Pages')->routes('{parent_uri}/{slug}')->structureContents(['root' => true])->save(); + } + + /** @test */ + public function it_gets_all_collections() + { + $collections = $this->repo->all(); + + $this->assertInstanceOf(IlluminateCollection::class, $collections); + $this->assertCount(4, $collections); + $this->assertEveryItemIsInstanceOf(Collection::class, $collections); + + $ordered = $collections->sortBy->handle()->values(); + $this->assertEquals(['alphabetical', 'blog', 'numeric', 'pages'], $ordered->map->handle()->all()); + $this->assertEquals(['Alphabetical', 'Blog', 'Numeric', 'Pages'], $ordered->map->title()->all()); + } + + /** @test */ + public function it_gets_a_collection_by_handle() + { + tap($this->repo->findByHandle('alphabetical'), function ($collection) { + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals('alphabetical', $collection->handle()); + $this->assertEquals('Alphabetical', $collection->title()); + }); + + tap($this->repo->findByHandle('blog'), function ($collection) { + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals('blog', $collection->handle()); + $this->assertEquals('Blog', $collection->title()); + }); + + tap($this->repo->findByHandle('numeric'), function ($collection) { + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals('numeric', $collection->handle()); + $this->assertEquals('Numeric', $collection->title()); + }); + + tap($this->repo->findByHandle('pages'), function ($collection) { + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals('pages', $collection->handle()); + $this->assertEquals('Pages', $collection->title()); + }); + + $this->assertNull($this->repo->findByHandle('unknown')); + } + + /** @test */ + public function it_saves_a_collection_to_the_database() + { + $collection = CollectionAPI::make('new'); + $collection->cascade(['foo' => 'bar']); + $this->assertNull($this->repo->findByHandle('new')); + + $this->repo->save($collection); + + $this->assertNotNull($item = $this->repo->findByHandle('new')); + $this->assertEquals(['foo' => 'bar'], $item->cascade()->all()); + } +} diff --git a/tests/Repositories/GlobalRepositoryTest.php b/tests/Repositories/GlobalRepositoryTest.php new file mode 100644 index 00000000..fa069df4 --- /dev/null +++ b/tests/Repositories/GlobalRepositoryTest.php @@ -0,0 +1,104 @@ +sites(['en', 'fr']); + $this->app->instance(Stache::class, $stache); + $this->repo = new GlobalRepository($stache); + + $globalOne = $this->repo->make('contact')->title('Contact Details')->save(); + (new Variables)->globalSet($globalOne)->data(['phone' => '555-1234'])->save(); + + $globalTwo = $this->repo->make('global')->title('General')->save(); + (new Variables)->globalSet($globalTwo)->data(['foo' => 'Bar'])->save(); + } + + /** @test */ + public function it_gets_all_global_sets() + { + $sets = $this->repo->all(); + + $this->assertInstanceOf(GlobalCollection::class, $sets); + $this->assertCount(2, $sets); + $this->assertEveryItemIsInstanceOf(GlobalSet::class, $sets); + + $ordered = $sets->sortBy->handle()->values(); + $this->assertEquals(['contact', 'global'], $ordered->map->id()->all()); + $this->assertEquals(['contact', 'global'], $ordered->map->handle()->all()); + $this->assertEquals(['Contact Details', 'General'], $ordered->map->title()->all()); + } + + /** @test */ + public function it_gets_a_global_set_by_id() + { + tap($this->repo->find('global'), function ($set) { + $this->assertInstanceOf(GlobalSet::class, $set); + $this->assertEquals('global', $set->id()); + $this->assertEquals('global', $set->handle()); + $this->assertEquals('General', $set->title()); + }); + + tap($this->repo->find('contact'), function ($set) { + $this->assertInstanceOf(GlobalSet::class, $set); + $this->assertEquals('contact', $set->id()); + $this->assertEquals('contact', $set->handle()); + $this->assertEquals('Contact Details', $set->title()); + }); + + $this->assertNull($this->repo->find('unknown')); + } + + /** @test */ + public function it_gets_a_global_set_by_handle() + { + tap($this->repo->findByHandle('global'), function ($set) { + $this->assertInstanceOf(GlobalSet::class, $set); + $this->assertEquals('global', $set->id()); + $this->assertEquals('global', $set->handle()); + $this->assertEquals('General', $set->title()); + }); + + tap($this->repo->findByHandle('contact'), function ($set) { + $this->assertInstanceOf(GlobalSet::class, $set); + $this->assertEquals('contact', $set->id()); + $this->assertEquals('contact', $set->handle()); + $this->assertEquals('Contact Details', $set->title()); + }); + + $this->assertNull($this->repo->findByHandle('unknown')); + } + + /** @test */ + public function it_saves_a_global_to_the_database() + { + $global = GlobalSetAPI::make('new'); + + $global->addLocalization( + $global->makeLocalization('en')->data(['foo' => 'bar', 'baz' => 'qux']) + ); + + $this->assertNull($this->repo->findByHandle('new')); + + $this->repo->save($global); + + $this->assertNotNull($item = $this->repo->find('new')); + $this->assertEquals(['foo' => 'bar', 'baz' => 'qux'], $item->in('en')->data()->all()); + } +} diff --git a/tests/Repositories/NavigationRepositoryTest.php b/tests/Repositories/NavigationRepositoryTest.php new file mode 100644 index 00000000..b537b2ba --- /dev/null +++ b/tests/Repositories/NavigationRepositoryTest.php @@ -0,0 +1,74 @@ +sites(['en']); + $this->app->instance(Stache::class, $stache); + + $this->repo = new NavigationRepository($stache); + + $this->repo->make('footer')->title('Footer')->expectsRoot(true)->save(); + $sidebar = tap($this->repo->make('sidebar')->title('Sidebar'))->save(); + $sidebar->makeTree('en', [['entry' => 'pages-contact'], ['entry' => 'pages-contact']])->save(); + } + + /** @test */ + public function it_gets_all_navs() + { + $navs = $this->repo->all(); + + $this->assertInstanceOf(Collection::class, $navs); + $this->assertCount(2, $navs); + $this->assertEveryItemIsInstanceOf(Structure::class, $navs); + + $ordered = $navs->sortBy->handle()->values(); + $this->assertEquals(['footer', 'sidebar'], $ordered->map->handle()->all()); + $this->assertEquals(['Footer', 'Sidebar'], $ordered->map->title()->all()); + } + + /** @test */ + public function it_gets_a_nav_by_handle() + { + tap($this->repo->findByHandle('sidebar'), function ($nav) { + $this->assertInstanceOf(Structure::class, $nav); + $this->assertEquals('sidebar', $nav->handle()); + $this->assertEquals('Sidebar', $nav->title()); + }); + + tap($this->repo->findByHandle('footer'), function ($nav) { + $this->assertInstanceOf(Structure::class, $nav); + $this->assertEquals('footer', $nav->handle()); + $this->assertEquals('Footer', $nav->title()); + }); + + $this->assertNull($this->repo->findByHandle('unknown')); + } + + /** @test */ + public function it_saves_a_nav_to_the_database() + { + $structure = (new Nav)->handle('new'); + + $this->assertNull($this->repo->findByHandle('new')); + + $this->repo->save($structure); + + $this->assertNotNull($this->repo->findByHandle('new')); + } +} diff --git a/tests/Repositories/TaxonomyRepositoryTest.php b/tests/Repositories/TaxonomyRepositoryTest.php new file mode 100644 index 00000000..29424e06 --- /dev/null +++ b/tests/Repositories/TaxonomyRepositoryTest.php @@ -0,0 +1,104 @@ +sites(['en', 'fr']); + $this->app->instance(Stache::class, $stache); + $this->directory = __DIR__.'/../__fixtures__/content/taxonomies'; + + $collectionRepo = new CollectionRepository($stache); + $collectionRepo->make('alphabetical')->title('Alphabetical')->routes('alphabetical/{slug}')->save(); + $collectionRepo->make('blog')->title('Blog')->dated(true)->taxonomies(['tags'])->save(); + $collectionRepo->make('numeric')->title('Numeric')->routes('numeric/{slug}')->save(); + $collectionRepo->make('pages')->title('Pages')->routes('{parent_uri}/{slug}')->structureContents(['root' => true])->save(); + + $this->repo = new TaxonomyRepository($stache); + $this->repo->make('categories')->title('Categories')->save(); + $this->repo->make('tags')->title('Tags')->save(); + } + + /** @test */ +// public function it_gets_all_taxonomies() +// { +// $taxonomies = $this->repo->all(); +// +// $this->assertInstanceOf(IlluminateCollection::class, $taxonomies); +// $this->assertCount(2, $taxonomies); +// $this->assertEveryItemIsInstanceOf(Taxonomy::class, $taxonomies); +// +// $ordered = $taxonomies->sortBy->handle()->values(); +// $this->assertEquals(['categories', 'tags'], $ordered->map->handle()->all()); +// $this->assertEquals(['Categories', 'Tags'], $ordered->map->title()->all()); +// } +// +// /** @test */ +// public function it_gets_a_taxonomy_by_handle() +// { +// tap($this->repo->findByHandle('categories'), function ($taxonomy) { +// $this->assertInstanceOf(Taxonomy::class, $taxonomy); +// $this->assertEquals('categories', $taxonomy->handle()); +// $this->assertEquals('Categories', $taxonomy->title()); +// }); +// +// tap($this->repo->findByHandle('tags'), function ($taxonomy) { +// $this->assertInstanceOf(Taxonomy::class, $taxonomy); +// $this->assertEquals('tags', $taxonomy->handle()); +// $this->assertEquals('Tags', $taxonomy->title()); +// }); +// +// $this->assertNull($this->repo->findByHandle('unknown')); +// } + + /** @test */ + public function it_gets_a_taxonomy_by_uri() + { + tap($this->repo->findByUri('/categories'), function ($taxonomy) { + $this->assertInstanceOf(Taxonomy::class, $taxonomy); + $this->assertEquals('categories', $taxonomy->handle()); + $this->assertEquals('Categories', $taxonomy->title()); + $this->assertNull($taxonomy->collection()); + }); + } + + /** @test */ + public function it_gets_a_taxonomy_by_uri_with_collection() + { + tap($this->repo->findByUri('/blog/categories'), function ($taxonomy) { + $this->assertInstanceOf(Taxonomy::class, $taxonomy); + $this->assertEquals('categories', $taxonomy->handle()); + $this->assertEquals('Categories', $taxonomy->title()); + $this->assertEquals(Collection::findByHandle('blog'), $taxonomy->collection()); + }); + } + + /** @test */ +// public function it_saves_a_taxonomy_to_the_stache_and_to_a_file() +// { +// $taxonomy = TaxonomyAPI::make('new'); +// $taxonomy->cascade(['foo' => 'bar']); +// $this->assertNull($this->repo->findByHandle('new')); +// +// $this->repo->save($taxonomy); +// +// $this->assertNotNull($item = $this->repo->findByHandle('new')); +// $this->assertEquals(['foo' => 'bar'], $item->cascade()->all()); +// } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..4d4ed7bd --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,214 @@ +shouldFakeVersion) { + \Facades\Statamic\Version::shouldReceive('get')->andReturn('3.0.0-testing'); + $this->addToAssertionCount(-1); // Dont want to assert this + } + + if ($this->shouldUseStringEntryIds) { + $this->runMigrationsForUUIDEntries(); + } else { + $this->runMigrationsForIncrementingEntries(); + } + } + + public function tearDown(): void + { + parent::tearDown(); + } + + protected function getPackageProviders($app) + { + return [ + \Statamic\Providers\StatamicServiceProvider::class, + \Statamic\Eloquent\ServiceProvider::class, + \Wilderborn\Partyline\ServiceProvider::class, + ]; + } + + protected function getPackageAliases($app) + { + return ['Statamic' => 'Statamic\Statamic']; + } + + protected function resolveApplicationConfiguration($app) + { + parent::resolveApplicationConfiguration($app); + + $configs = [ + 'eloquent-driver', + ]; + + foreach ($configs as $config) { + $app['config']->set("statamic.$config", require(__DIR__."/../config/{$config}.php")); + } + } + + protected function getEnvironmentSetUp($app) + { + // We changed the default sites setup but the tests assume defaults like the following. + $app['config']->set('statamic.sites', [ + 'default' => 'en', + 'sites' => [ + 'en' => ['name' => 'English', 'locale' => 'en_US', 'url' => 'http://localhost/'], + ], + ]); + $app['config']->set('auth.providers.users.driver', 'statamic'); + $app['config']->set('statamic.stache.watcher', false); + $app['config']->set('statamic.users.repository', 'file'); + $app['config']->set('statamic.stache.stores.users', [ + 'class' => \Statamic\Stache\Stores\UsersStore::class, + 'directory' => __DIR__.'/__fixtures__/users', + ]); + + $app['config']->set('statamic.editions.pro', true); + + $app['config']->set('cache.stores.outpost', [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/outpost-data'), + ]); + } + + protected function assertEveryItem($items, $callback) + { + if ($items instanceof \Illuminate\Support\Collection) { + $items = $items->all(); + } + + $passes = 0; + + foreach ($items as $item) { + if ($callback($item)) { + $passes++; + } + } + + $this->assertEquals(count($items), $passes, 'Failed asserting that every item passes.'); + } + + protected function assertEveryItemIsInstanceOf($class, $items) + { + if ($items instanceof \Illuminate\Support\Collection) { + $items = $items->all(); + } + + $matches = 0; + + foreach ($items as $item) { + if ($item instanceof $class) { + $matches++; + } + } + + $this->assertEquals(count($items), $matches, 'Failed asserting that every item is an instance of '.$class); + } + + protected function assertContainsHtml($string) + { + preg_match('/<[^<]+>/', $string, $matches); + + $this->assertNotEmpty($matches, 'Failed asserting that string contains HTML.'); + } + + public static function assertArraySubset($subset, $array, bool $checkForObjectIdentity = false, string $message = ''): void + { + $class = version_compare(app()->version(), 7, '>=') ? \Illuminate\Testing\Assert::class : \Illuminate\Foundation\Testing\Assert::class; + $class::assertArraySubset($subset, $array, $checkForObjectIdentity, $message); + } + + // This method is unavailable on earlier versions of Laravel. + public function partialMock($abstract, \Closure $mock = null) + { + $mock = \Mockery::mock(...array_filter(func_get_args()))->makePartial(); + $this->app->instance($abstract, $mock); + + return $mock; + } + + /** + * @deprecated + */ + public static function assertFileNotExists(string $filename, string $message = ''): void + { + method_exists(static::class, 'assertFileDoesNotExist') + ? static::assertFileDoesNotExist($filename, $message) + : parent::assertFileNotExists($filename, $message); + } + + /** + * @deprecated + */ + public static function assertDirectoryNotExists(string $filename, string $message = ''): void + { + method_exists(static::class, 'assertDirectoryDoesNotExist') + ? static::assertDirectoryDoesNotExist($filename, $message) + : parent::assertDirectoryNotExists($filename, $message); + } + + public static function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void + { + method_exists(\PHPUnit\Framework\Assert::class, 'assertMatchesRegularExpression') + ? parent::assertMatchesRegularExpression($pattern, $string, $message) + : parent::assertRegExp($pattern, $string, $message); + } + + public function runBaseMigrations() + { + foreach ($this->baseMigrations as $migration) { + $migration = require $migration; + $migration->up(); + } + } + + public function runMigrationsForIncrementingEntries() + { + $this->runBaseMigrations(); + + $migration = require __DIR__.'/../database/migrations/create_entries_table.php.stub'; + $migration->up(); + } + + public function runMigrationsForUUIDEntries() + { + $this->runBaseMigrations(); + + $migration = require __DIR__.'/../database/migrations/create_entries_table_with_string_ids.php.stub'; + $migration->up(); + } +}