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..ab9249ee 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,10 @@ 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: + ``` composer require statamic/eloquent-driver ``` @@ -19,7 +18,6 @@ php artisan vendor:publish --tag="statamic-eloquent-config" Since Statamic uses UUIDs within content files by default, we provide two solutions depending on whether you need to use existing content. - ### Fresh install of [statamic/statamic](https://github.com/statamic/statamic) (using incrementing ids) If you're starting from scratch, we can use traditional incrementing integers for IDs. diff --git a/config/eloquent-driver.php b/config/eloquent-driver.php index 98b04a26..23d104ef 100644 --- a/config/eloquent-driver.php +++ b/config/eloquent-driver.php @@ -4,6 +4,50 @@ 'entries' => [ 'model' => \Statamic\Eloquent\Entries\EntryModel::class, + 'entry' => \Statamic\Eloquent\Entries\Entry::class, + ], + + 'collections' => [ + 'model' => \Statamic\Eloquent\Collections\CollectionModel::class, + 'entry' => \Statamic\Eloquent\Collections\Collection::class, + ], + + 'trees' => [ + 'model' => \Statamic\Eloquent\Structures\TreeModel::class, + ], + + 'taxonomies' => [ + 'model' => \Statamic\Eloquent\Taxonomies\TaxonomyModel::class, + ], + + 'terms' => [ + 'model' => \Statamic\Eloquent\Taxonomies\TermModel::class, + ], + + 'global-sets' => [ + 'model' => \Statamic\Eloquent\Globals\GlobalSetModel::class, + ], + + 'variables' => [ + 'model' => \Statamic\Eloquent\Globals\VariablesModel::class, + ], + + 'navigations' => [ + 'model' => \Statamic\Eloquent\Structures\NavModel::class, + ], + + 'nav-trees' => [ + 'model' => \Statamic\Eloquent\Structures\NavTreeModel::class, + ], + + 'roles' => [ + 'model' => \Statamic\Eloquent\Auth\RoleModel::class, + 'entry' => \Statamic\Eloquent\Auth\Role::class, + ], + + 'groups' => [ + 'model' => \Statamic\Eloquent\Auth\UserGroupModel::class, + 'entry' => \Statamic\Eloquent\Auth\UserGroup::class, ], ]; diff --git a/database/migrations/2021_05_18_160811_create_taxonomies_table.php b/database/migrations/2021_05_18_160811_create_taxonomies_table.php new file mode 100644 index 00000000..5e4e371a --- /dev/null +++ b/database/migrations/2021_05_18_160811_create_taxonomies_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->string('handle'); + $table->string('title'); + $table->json('sites')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('taxonomies'); + } +} diff --git a/database/migrations/2021_05_19_082853_create_terms_table.php b/database/migrations/2021_05_19_082853_create_terms_table.php new file mode 100644 index 00000000..d9800956 --- /dev/null +++ b/database/migrations/2021_05_19_082853_create_terms_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('site'); + $table->string('slug'); + $table->string('uri')->nullable(); + $table->string('taxonomy'); + $table->json('data'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('taxonomy_terms'); + } +} diff --git a/database/migrations/2021_05_19_122354_create_globals_table.php b/database/migrations/2021_05_19_122354_create_globals_table.php new file mode 100644 index 00000000..c9740310 --- /dev/null +++ b/database/migrations/2021_05_19_122354_create_globals_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('handle'); + $table->string('title'); + $table->json('localizations'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('global_sets'); + } +} diff --git a/database/migrations/2021_05_19_143631_create_navigations_table.php b/database/migrations/2021_05_19_143631_create_navigations_table.php new file mode 100644 index 00000000..a07b1fb2 --- /dev/null +++ b/database/migrations/2021_05_19_143631_create_navigations_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('handle'); + $table->string('title'); + $table->json('collections')->nullable(); + $table->integer('maxDepth')->nullable(); + $table->boolean('expectsRoot')->default(false); + $table->string('initialPath')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('navigations'); + } +} diff --git a/database/migrations/2021_05_27_082335_create_navigation_trees_table.php b/database/migrations/2021_05_27_082335_create_navigation_trees_table.php new file mode 100644 index 00000000..961bb046 --- /dev/null +++ b/database/migrations/2021_05_27_082335_create_navigation_trees_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('handle'); + $table->string('type'); + $table->string('initialPath')->nullable(); + $table->string('locale')->nullable(); + $table->json('tree')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('trees'); + } +} diff --git a/database/migrations/2021_05_28_114212_create_collections_table.php b/database/migrations/2021_05_28_114212_create_collections_table.php new file mode 100644 index 00000000..85e25ff7 --- /dev/null +++ b/database/migrations/2021_05_28_114212_create_collections_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('handle'); + $table->string('title'); + $table->json('routes')->nullable(); + $table->boolean('dated')->default(false); + $table->string('past_date_behavior')->nullable(); + $table->string('future_date_behavior')->nullable(); + $table->boolean('default_publish_state')->default(true); + $table->boolean('ampable')->default(false); + $table->json('sites')->nullable(); + $table->string('template')->nullable(); + $table->string('layout')->nullable(); + $table->string('sort_dir')->nullable(); + $table->string('sort_field')->nullable(); + $table->string('mount')->nullable(); + $table->json('taxonomies')->nullable(); + $table->boolean('revisions')->default(false); + $table->json('inject')->nullable(); + $table->json('structure')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('collections'); + } +} diff --git a/database/migrations/2021_09_26_213111_create_blueprints_table.php b/database/migrations/2021_09_26_213111_create_blueprints_table.php new file mode 100644 index 00000000..82258451 --- /dev/null +++ b/database/migrations/2021_09_26_213111_create_blueprints_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('namespace')->nullable()->default(null); + $table->string('handle'); + $table->json('data'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('blueprints'); + } +} diff --git a/database/migrations/2021_09_26_214356_create_fieldsets_table.php b/database/migrations/2021_09_26_214356_create_fieldsets_table.php new file mode 100644 index 00000000..2336ce19 --- /dev/null +++ b/database/migrations/2021_09_26_214356_create_fieldsets_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('handle'); + $table->json('data'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('fieldsets'); + } +} diff --git a/database/migrations/2021_09_29_155001_create_assets_meta_assets_container_table.php b/database/migrations/2021_09_29_155001_create_assets_meta_assets_container_table.php new file mode 100644 index 00000000..c381e498 --- /dev/null +++ b/database/migrations/2021_09_29_155001_create_assets_meta_assets_container_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('handle'); + $table->json('data'); + $table->timestamps(); + }); + + Schema::create('asset_containers', function (Blueprint $table) { + $table->id(); + $table->string('handle'); + $table->json('data'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('asset_meta'); + Schema::dropIfExists('asset_containers'); + } +} diff --git a/database/migrations/2021_10_06_193801_create_roles_table.php b/database/migrations/2021_10_06_193801_create_roles_table.php new file mode 100644 index 00000000..c5f343c4 --- /dev/null +++ b/database/migrations/2021_10_06_193801_create_roles_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('handle'); + $table->string('title'); + $table->json('permissions')->nullable(); + $table->json('preferences')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('roles'); + } +} diff --git a/database/migrations/2021_10_06_213822_create_groups_table.php b/database/migrations/2021_10_06_213822_create_groups_table.php new file mode 100644 index 00000000..59cd2b32 --- /dev/null +++ b/database/migrations/2021_10_06_213822_create_groups_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('handle'); + $table->string('title'); + $table->json('roles')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('groups'); + } +} diff --git a/database/seeders/DefaultBlueprintSeeder.php b/database/seeders/DefaultBlueprintSeeder.php new file mode 100644 index 00000000..6972904a --- /dev/null +++ b/database/seeders/DefaultBlueprintSeeder.php @@ -0,0 +1,71 @@ + [ + [ + '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' + ], + ] + ]; + + /** + * Run the database seeds. + * + * @return void + */ + public function run() + { + DB::table('blueprints')->updateOrInsert( + [ + 'handle' => 'default' + ], + [ + 'namespace' => null, + 'data' => $this->configJson(), + 'created_at' => Carbon::now() + ] + ); + } + + public function configJson() + { + try { + $config = json_encode(self::DEFAULT_BLUEPRINT_CONFIG, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + $config = '[]'; + } + + return $config; + } +} diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php new file mode 100644 index 00000000..970bcb78 --- /dev/null +++ b/src/Assets/Asset.php @@ -0,0 +1,77 @@ +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 () { + if ($model = AssetModel::where('handle', $this->container()->handle() . '::' . $this->metaPath())->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 = AssetModel::firstOrNew([ + 'handle' => $this->container()->handle() . '::' . $this->metaPath() + ]); + + $model->data = $meta; + + $model->save(); + } +} diff --git a/src/Assets/AssetContainer.php b/src/Assets/AssetContainer.php new file mode 100644 index 00000000..6e82ba76 --- /dev/null +++ b/src/Assets/AssetContainer.php @@ -0,0 +1,56 @@ +data; + + return AssetContainerFacade::make($model->handle) + ->disk(array_get($data, 'disk')) + ->title(array_get($data, 'title')) + ->allowDownloading(array_get($data, 'allow_downloading')) + ->allowMoving(array_get($data, 'allow_moving')) + ->allowRenaming(array_get($data, 'allow_renaming')) + ->allowUploads(array_get($data, 'allow_uploads')) + ->createFolders(array_get($data, 'create_folders')) + ->searchIndex(array_get($data, 'search_index')); + } + + public function toModel() + { + $model = AssetContainerModel::firstOrNew([ + 'handle' => $this->id(), + ]); + + $model->data = $this->fileData(); + $model->save(); + + return $model; + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + $this->id($model->handle); + + return $this; + } + + public function lastModified() + { + return $this->model->updated_at; + } +} diff --git a/src/Assets/AssetContainerModel.php b/src/Assets/AssetContainerModel.php new file mode 100644 index 00000000..c7a55c90 --- /dev/null +++ b/src/Assets/AssetContainerModel.php @@ -0,0 +1,18 @@ + 'json', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; +} diff --git a/src/Assets/AssetContainerRepository.php b/src/Assets/AssetContainerRepository.php new file mode 100644 index 00000000..e2a0721d --- /dev/null +++ b/src/Assets/AssetContainerRepository.php @@ -0,0 +1,61 @@ + AssetContainer::class, + ]; + } + + public function all(): Collection + { + return Blink::once('asset-containers', function () { + $keys = AssetContainerModel::get()->map(function ($model) { + return AssetContainer::fromModel($model); + }); + + return Collection::make($keys); + }); + } + + public function find($handle): ?AssetContainer + { + return Blink::once('asset-containers::' . $handle, function () use ($handle) { + if (($model = AssetContainerModel::where('handle', $handle)->first()) == null) { + return null; + } + + $assetContainer = AssetContainer::fromModel($model); + + return $assetContainer; + }); + } + + public function findByHandle($handle): ?AssetContainer + { + return $this->find($handle); + } + + public function save($assetContainer) + { + $model = $assetContainer->toModel(); + + $model->save(); + + $assetContainer->model($model->fresh()); + } + + public function delete($assetContainer) + { + $assetContainer->toModel()->delete(); + } +} diff --git a/src/Assets/AssetModel.php b/src/Assets/AssetModel.php new file mode 100644 index 00000000..159d4c72 --- /dev/null +++ b/src/Assets/AssetModel.php @@ -0,0 +1,18 @@ + 'json', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; +} diff --git a/src/Assets/AssetRepository.php b/src/Assets/AssetRepository.php new file mode 100644 index 00000000..edc4db69 --- /dev/null +++ b/src/Assets/AssetRepository.php @@ -0,0 +1,28 @@ +container()->contents()->forget($asset->path())->save(); + + AssetModel::where('handle', $asset->containerHandle() . '::' . $asset->metaPath())->first()->delete(); + + Stache::store('assets::' . $asset->containerHandle())->delete($asset); + } + + public static function bindings(): array + { + return [ + AssetContract::class => Asset::class, + QueryBuilder::class => \Statamic\Assets\QueryBuilder::class, + ]; + } +} diff --git a/src/Auth/Role.php b/src/Auth/Role.php new file mode 100644 index 00000000..5089271c --- /dev/null +++ b/src/Auth/Role.php @@ -0,0 +1,45 @@ +title($model->title) + ->handle($model->handle) + ->permissions($model->permissions) + ->preferences($model->preferences) + ->model($model); + } + + public function toModel() + { + $class = app('statamic.eloquent.roles.model'); + + return $class::findOrNew($this->model?->id)->fill([ + 'title' => $this->title, + 'handle' => $this->handle, + 'permissions' => $this->permissions, + 'preferences' => $this->preferences, + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + $this->id($model->id); + + return $this; + } +} \ No newline at end of file diff --git a/src/Auth/RoleModel.php b/src/Auth/RoleModel.php new file mode 100644 index 00000000..0091ae58 --- /dev/null +++ b/src/Auth/RoleModel.php @@ -0,0 +1,19 @@ + 'json', + 'preferences' => 'json', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; +} diff --git a/src/Auth/RoleRepository.php b/src/Auth/RoleRepository.php new file mode 100644 index 00000000..f9549bc0 --- /dev/null +++ b/src/Auth/RoleRepository.php @@ -0,0 +1,68 @@ +handle($handle); + } + + public function save($role) + { + $model = $role->toModel(); + + $model->save(); + + $role->model($model->fresh()); + } + + public function delete($role) + { + $role->model()->delete(); + } + + public static function bindings(): array + { + return [ + RoleContract::class => app('statamic.eloquent.roles.entry'), + ]; + } + + protected function transform($items, $columns = []) + { + return IlluminateCollection::make($items)->map(function ($model) { + return app(RoleContract::class)::fromModel($model); + }); + } + public function all(): IlluminateCollection + { + $class = app('statamic.eloquent.roles.model'); + return $this->transform($class::all()); + } + + public function find($handle): ?RoleContract + { + $class = app('statamic.eloquent.roles.model'); + $model = $class::whereHandle($handle)->first(); + + return $model + ? app(RoleContract::class)->fromModel($model) + : null; + } + + public function findByHandle($handle): ?RoleContract + { + $class = app('statamic.eloquent.roles.model'); + $model = $class::whereHandle($handle)->first(); + + return $model + ? app(RoleContract::class)->fromModel($model) + : null; + } +} diff --git a/src/Auth/UserGroup.php b/src/Auth/UserGroup.php new file mode 100644 index 00000000..b699b639 --- /dev/null +++ b/src/Auth/UserGroup.php @@ -0,0 +1,43 @@ +title($model->title) + ->handle($model->handle) + ->roles($model->roles) + ->model($model); + } + + public function toModel() + { + $class = app('statamic.eloquent.groups.model'); + + return $class::findOrNew($this->model?->id)->fill([ + 'title' => $this->title, + 'handle' => $this->handle, + 'roles' => $this->roles->keys(), + ]); + } + + 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/Auth/UserGroupModel.php b/src/Auth/UserGroupModel.php new file mode 100644 index 00000000..b6eb0658 --- /dev/null +++ b/src/Auth/UserGroupModel.php @@ -0,0 +1,18 @@ + 'json', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; +} diff --git a/src/Auth/UserGroupRepository.php b/src/Auth/UserGroupRepository.php new file mode 100644 index 00000000..69874ffb --- /dev/null +++ b/src/Auth/UserGroupRepository.php @@ -0,0 +1,58 @@ +toModel(); + + $model->save(); + + $userGroup->model($model->fresh()); + } + + public function delete($userGroup) + { + $userGroup->model()->delete(); + } + + public static function bindings(): array + { + return [ + UserGroupContract::class => app('statamic.eloquent.groups.entry'), + ]; + } + + protected function transform($items, $columns = []) + { + return IlluminateCollection::make($items)->map(function ($model) { + return app(UserGroupContract::class)::fromModel($model); + }); + } + public function all(): IlluminateCollection + { + $class = app('statamic.eloquent.groups.model'); + return $this->transform($class::all()); + } + + public function find($id): ?UserGroupContract + { + $class = app('statamic.eloquent.groups.model'); + $model = $class::whereHandle($id)->first(); + + return $model + ? app(UserGroupContract::class)->fromModel($model) + : null; + } +} diff --git a/src/Blueprints/BlueprintModel.php b/src/Blueprints/BlueprintModel.php new file mode 100644 index 00000000..a518f4f4 --- /dev/null +++ b/src/Blueprints/BlueprintModel.php @@ -0,0 +1,24 @@ + 'json', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function getAttribute($key) + { + return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Blueprints/BlueprintRepository.php b/src/Blueprints/BlueprintRepository.php new file mode 100644 index 00000000..88ff7a43 --- /dev/null +++ b/src/Blueprints/BlueprintRepository.php @@ -0,0 +1,129 @@ +once($blueprint, function () use ($blueprint) { + [$namespace, $handle] = $this->getNamespaceAndHandle($blueprint); + if (!$blueprint) { + return null; + } + + if (($blueprintModel = BlueprintModel::where('namespace', $namespace)->where('handle', $handle)->first()) === null) { + 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, 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 (new Blueprint) + ->setHidden(Arr::get($model->data, 'hide')) + ->setOrder(Arr::get($model->data, 'order')) + ->setHandle($model->handle) + ->setNamespace($model->namespace) + ->setContents($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 = BlueprintModel::firstOrNew([ + 'handle' => $blueprint->handle(), + 'namespace' => $blueprint->namespace() ?? null, + ]); + $model->data = $blueprint->contents(); + $model->save(); + } + + public function deleteModel($blueprint) + { + $model = BlueprintModel::where('namespace', $blueprint->namespace() ?? null)->where('handle', $blueprint->handle())->first(); + $model->delete(); + } +} diff --git a/src/Collections/Collection.php b/src/Collections/Collection.php new file mode 100644 index 00000000..331fdebc --- /dev/null +++ b/src/Collections/Collection.php @@ -0,0 +1,80 @@ +structureContents($model->structure) + ->sortDirection($model->sort_dir) + ->sortField($model->sort_field) + ->layout($model->layout) + ->template($model->template) + ->sites($model->sites) + ->futureDateBehavior($model->future_date_behavior) + ->pastDateBehavior($model->past_date_behavior) + ->ampable($model->ampable) + ->dated($model->dated) + ->title($model->title) + ->handle($model->handle) + ->routes($model->routes) + ->taxonomies($model->taxonomies) + ->mount($model->mount) + ->model($model); + } + + public function toModel() + { + $class = app('statamic.eloquent.collections.model'); + + return $class::findOrNew($this->model?->id)->fill([ + 'title' => $this->title, + 'handle' => $this->handle, + 'routes' => $this->routes, + 'dated' => $this->dated, + 'past_date_behavior' => $this->pastDateBehavior(), + 'future_date_behavior' => $this->futureDateBehavior(), + 'default_publish_state' => $this->defaultPublishState, + 'ampable' => $this->ampable, + 'sites' => $this->sites, + 'template' => $this->template, + 'layout' => $this->layout, + 'sort_dir' => $this->sortDirection(), + 'sort_field' => $this->sortField(), + 'mount' => $this->mount, + 'taxonomies' => $this->taxonomies, + 'revisions' => $this->revisions, + 'inject' => $this->cascade, + 'structure' => $this->hasStructure() ? $this->structureContents() : null, + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + $this->id($model->id); + + return $this; + } + + protected function makeStructureFromContents() + { + return (new CollectionStructure) + ->handle($this->handle()) + ->expectsRoot($this->structureContents['root'] ?? 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..c1f76b3b --- /dev/null +++ b/src/Collections/CollectionModel.php @@ -0,0 +1,24 @@ + 'json', + 'inject' => 'json', + 'taxonomies' => 'json', + 'structure' => 'json', + 'sites' => 'json', + 'revisions' => 'bool', + 'dated' => 'bool', + 'default_publish_state' => 'bool', + 'ampable' => 'bool', + ]; +} diff --git a/src/Collections/CollectionRepository.php b/src/Collections/CollectionRepository.php new file mode 100644 index 00000000..9b115ca2 --- /dev/null +++ b/src/Collections/CollectionRepository.php @@ -0,0 +1,75 @@ +queryEntries(); + + if ($ids) { + $query->whereIn('id', $ids); + } + + $query->get()->each(function ($entry) { + EntryModel::where('id', $entry->id())->update(['uri' => $entry->uri()]); + }); + } + + public function all(): IlluminateCollection + { + return $this->transform(CollectionModel::all()); + } + + public function find($handle): ?CollectionContract + { + $model = CollectionModel::whereHandle($handle)->first(); + + return $model + ? app(CollectionContract::class)->fromModel($model) + : null; + } + + public function findByHandle($handle): ?CollectionContract + { + $model = CollectionModel::whereHandle($handle)->first(); + + return $model + ? app(CollectionContract::class)->fromModel($model) + : null; + } + + public function save($entry) + { + $model = $entry->toModel(); + + $model->save(); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $entry->model()->delete(); + } + + protected function transform($items, $columns = []) + { + return IlluminateCollection::make($items)->map(function ($model) { + return Collection::fromModel($model); + }); + } + + public static function bindings(): array + { + return [ + CollectionContract::class => app('statamic.eloquent.collections.entry'), + ]; + } +} diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index cbe04e05..358a3a7d 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -2,6 +2,7 @@ namespace Statamic\Eloquent\Entries; +use Illuminate\Support\Carbon; use Statamic\Eloquent\Entries\EntryModel as Model; use Statamic\Entries\Entry as FileEntry; @@ -59,6 +60,28 @@ public function model($model = null) return $this; } + /** + * This overwrite is needed to prevent Statamic to save updated_at also into the data. We track updated_at already in the database. + * + * @param null $user + * @return $this|Entry|FileEntry|\Statamic\Taxonomies\LocalizedTerm + */ + public function updateLastModified($user = null) + { + if (! config('statamic.system.track_last_update')) { + return $this; + } + + $user + ? $this->set('updated_by', $user->id()) + : $this->remove('updated_by'); + + // ensure 'updated_at' does not exists in the data of the entry. + $this->remove('updated_at'); + + return $this; + } + public function lastModified() { return $this->model->updated_at; @@ -77,7 +100,7 @@ public function origin($origin = null) } if (! $this->model->origin) { - return null; + return; } return self::fromModel($this->model->origin); diff --git a/src/Entries/EntryQueryBuilder.php b/src/Entries/EntryQueryBuilder.php index f02a62b1..08e40905 100644 --- a/src/Entries/EntryQueryBuilder.php +++ b/src/Entries/EntryQueryBuilder.php @@ -19,7 +19,7 @@ class EntryQueryBuilder extends EloquentQueryBuilder implements QueryBuilder protected function transform($items, $columns = []) { return EntryCollection::make($items)->map(function ($model) { - return Entry::fromModel($model); + return app('statamic.eloquent.entries.entry')::fromModel($model); }); } diff --git a/src/Entries/EntryRepository.php b/src/Entries/EntryRepository.php index 37c2533e..fd57ec55 100644 --- a/src/Entries/EntryRepository.php +++ b/src/Entries/EntryRepository.php @@ -11,7 +11,7 @@ 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, ]; } diff --git a/src/Fieldsets/FieldsetModel.php b/src/Fieldsets/FieldsetModel.php new file mode 100644 index 00000000..aaa11b75 --- /dev/null +++ b/src/Fieldsets/FieldsetModel.php @@ -0,0 +1,24 @@ + 'json', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function getAttribute($key) + { + return Arr::get($this->getAttributeValue('data'), $key, parent::getAttribute($key)); + } +} diff --git a/src/Fieldsets/FieldsetRepository.php b/src/Fieldsets/FieldsetRepository.php new file mode 100644 index 00000000..2acf4d21 --- /dev/null +++ b/src/Fieldsets/FieldsetRepository.php @@ -0,0 +1,73 @@ +map(function ($model) { + return (new Fieldset) + ->setHandle($model->handle) + ->setContents($model->data); + }); + }); + } + + public function find($handle): ?Fieldset + { + if ($cached = array_get($this->fieldsets, $handle)) { + return $cached; + } + + $handle = str_replace('/', '.', $handle); + + if (($model = FieldsetModel::where('handle', $handle)->first()) === null) { + return null; + } + + $fieldset = (new Fieldset) + ->setHandle($handle) + ->setContents($model->data); + + $this->fieldsets[$handle] = $fieldset; + + return $fieldset; + } + + public function save(Fieldset $fieldset) + { + $this->updateModel($fieldset); + } + + public function delete(Fieldset $fieldset) + { + $this->deleteModel($fieldset); + } + + public function updateModel($fieldset) + { + $model = FieldsetModel::firstOrNew([ + 'handle' => $fieldset->handle(), + ]); + $model->data = $fieldset->contents(); + $model->save(); + } + + public function deleteModel($fieldset) + { + $model = FieldsetModel::where('handle', $fieldset->handle())->first(); + $model->delete(); + } +} diff --git a/src/Globals/GlobalRepository.php b/src/Globals/GlobalRepository.php new file mode 100644 index 00000000..7a72d26a --- /dev/null +++ b/src/Globals/GlobalRepository.php @@ -0,0 +1,53 @@ +map(function ($model) { + return GlobalSet::fromModel($model); + }); + } + + public function find($handle): ?GlobalSetContract + { + return app(GlobalSetContract::class)->fromModel(GlobalSetModel::whereHandle($handle)->firstOrFail()); + } + + public function findByHandle($handle): ?GlobalSetContract + { + return app(GlobalSetContract::class)->fromModel(GlobalSetModel::whereHandle($handle)->firstOrFail()); + } + + public function all(): GlobalCollection + { + return $this->transform(GlobalSetModel::all()); + } + + public function save($entry) + { + $model = $entry->toModel(); + + $model->save(); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $entry->model()->delete(); + } + + public static function bindings(): array + { + return [ + GlobalSetContract::class => GlobalSet::class, + ]; + } +} diff --git a/src/Globals/GlobalSet.php b/src/Globals/GlobalSet.php new file mode 100644 index 00000000..a3d28718 --- /dev/null +++ b/src/Globals/GlobalSet.php @@ -0,0 +1,60 @@ +handle($model->handle) + ->title($model->title) + ->model($model); + + foreach ($model->localizations as $localization) { + $global->addLocalization(Variables::fromModel(VariablesModel::make($localization))); + } + + return $global; + } + + public function toModel() + { + $class = app('statamic.eloquent.global-sets.model'); + + $localizations = $this->localizations()->map(function ($value, $key) { + return $value->toModel()->toArray(); + }); + + return $class::findOrNew($this->model?->id)->fill([ + 'handle' => $this->handle(), + 'title' => $this->title(), + 'localizations' => $localizations, + ]); + } + + public function makeLocalization($site) + { + return (new Variables) + ->globalSet($this) + ->locale($site); + } + + 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/Globals/GlobalSetModel.php b/src/Globals/GlobalSetModel.php new file mode 100644 index 00000000..44d2db6c --- /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..628d2947 --- /dev/null +++ b/src/Globals/Variables.php @@ -0,0 +1,28 @@ +locale($model->locale) + ->data($model->data); + } + + public function toModel() + { + $class = app('statamic.eloquent.variables.model'); + + $data = $this->data(); + + return $class::make([ + 'locale' => $this->locale, + 'data' => $data, + ]); + } +} diff --git a/src/Globals/VariablesModel.php b/src/Globals/VariablesModel.php new file mode 100644 index 00000000..fbfca951 --- /dev/null +++ b/src/Globals/VariablesModel.php @@ -0,0 +1,22 @@ +getAttributeValue('data'), $key, parent::getAttribute($key)); + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 07a6730b..8b1adf3d 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,11 +4,31 @@ use Statamic\Contracts\Entries\CollectionRepository as CollectionRepositoryContract; use Statamic\Contracts\Entries\EntryRepository as EntryRepositoryContract; +use Statamic\Contracts\Globals\GlobalRepository as GlobalRepositoryContract; +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\Contracts\Assets\AssetContainerRepository as AssetContainerRepositoryContract; +use Statamic\Contracts\Assets\AssetRepository as AssetRepositoryContract; +use Statamic\Contracts\Auth\RoleRepository as RoleRepositoryContract; +use Statamic\Contracts\Auth\UserGroupRepository as UserGroupRepositoryContract; +use Statamic\Eloquent\Collections\CollectionRepository; use Statamic\Eloquent\Commands\ImportEntries; -use Statamic\Eloquent\Entries\CollectionRepository; -use Statamic\Eloquent\Entries\EntryModel; use Statamic\Eloquent\Entries\EntryQueryBuilder; use Statamic\Eloquent\Entries\EntryRepository; +use Statamic\Eloquent\Globals\GlobalRepository; +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\Eloquent\Assets\AssetRepository; +use Statamic\Eloquent\Assets\AssetContainerRepository; +use Statamic\Eloquent\Auth\RoleRepository; +use Statamic\Eloquent\Auth\UserGroupRepository; use Statamic\Providers\AddonServiceProvider; use Statamic\Statamic; @@ -24,9 +44,9 @@ 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'); - if (! $this->app->runningInConsole()) { + if (!$this->app->runningInConsole()) { return; } @@ -35,11 +55,11 @@ public function boot() ], 'statamic-eloquent-config'); $this->publishes([ - __DIR__.'/../database/migrations/create_entries_table.php' => $this->migrationsPath('create_entries_table'), + __DIR__ . '/../database/migrations/create_entries_table.php' => $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' => $this->migrationsPath('create_entries_table_with_string_ids'), ], 'statamic-eloquent-entries-table-with-string-ids'); $this->commands([ImportEntries::class]); @@ -47,8 +67,15 @@ public function boot() public function register() { + $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); - Statamic::repository(CollectionRepositoryContract::class, CollectionRepository::class); $this->app->bind(EntryQueryBuilder::class, function ($app) { return new EntryQueryBuilder( @@ -56,9 +83,91 @@ public function register() ); }); - $this->app->bind('statamic.eloquent.entries.model', function () { - return config('statamic.eloquent-driver.entries.model'); + Statamic::repository(AssetRepositoryContract::class, AssetRepository::class); + Statamic::repository(AssetContainerRepositoryContract::class, AssetContainerRepository::class); + + $this->app->singleton( + 'Statamic\Fields\BlueprintRepository', + 'Statamic\Eloquent\Blueprints\BlueprintRepository' + ); + + $this->app->singleton( + 'Statamic\Fields\FieldsetRepository', + 'Statamic\Eloquent\Fieldsets\FieldsetRepository' + ); + + $this->app->bind('statamic.eloquent.collections.model', function () { + return config('statamic-eloquent-driver.collections.model'); + }); + + $this->app->bind('statamic.eloquent.collections.entry', function () { + return config('statamic-eloquent-driver.collections.entry'); + }); + + $this->app->bind('statamic.eloquent.trees.model', function () { + return config('statamic-eloquent-driver.trees.model'); + }); + + Statamic::repository(CollectionRepositoryContract::class, CollectionRepository::class); + Statamic::repository(CollectionTreeRepositoryContract::class, CollectionTreeRepository::class); + + Statamic::repository(TaxonomyRepositoryContract::class, TaxonomyRepository::class); + Statamic::repository(TermRepositoryContract::class, TermRepository::class); + + $this->app->bind(TermQueryBuilder::class, function ($app) { + return new TermQueryBuilder( + $app['statamic.eloquent.terms.model']::query() + ); + }); + + $this->app->bind('statamic.eloquent.terms.model', function () { + return config('statamic-eloquent-driver.terms.model'); }); + + $this->app->bind('statamic.eloquent.taxonomies.model', function () { + return config('statamic-eloquent-driver.taxonomies.model'); + }); + + 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.variables.model', function () { + return config('statamic-eloquent-driver.variables.model'); + }); + + Statamic::repository(NavigationRepositoryContract::class, NavigationRepository::class); + Statamic::repository(NavTreeRepositoryContract::class, NavTreeRepository::class); + + $this->app->bind('statamic.eloquent.navigations.model', function () { + return config('statamic-eloquent-driver.navigations.model'); + }); + + $this->app->bind('statamic.eloquent.trees.model', function () { + return config('statamic-eloquent-driver.trees.model'); + }); + + $this->app->bind('statamic.eloquent.roles.entry', function () { + return config('statamic-eloquent-driver.roles.entry'); + }); + + $this->app->bind('statamic.eloquent.roles.model', function () { + return config('statamic-eloquent-driver.roles.model'); + }); + + Statamic::repository(RoleRepositoryContract::class, RoleRepository::class); + + $this->app->bind('statamic.eloquent.groups.entry', function () { + return config('statamic-eloquent-driver.groups.entry'); + }); + + $this->app->bind('statamic.eloquent.groups.model', function () { + return config('statamic-eloquent-driver.groups.model'); + }); + + Statamic::repository(UserGroupRepositoryContract::class, UserGroupRepository::class); } protected function migrationsPath($filename) 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->initialPath) + ->syncOriginal() + ->model($model); + } + + public function toModel() + { + $class = app('statamic.eloquent.trees.model'); + + return $class::findOrNew($this->model?->id)->fill([ + 'handle' => $this->handle(), + 'initialPath' => $this->initialPath(), + 'locale' => $this->locale(), + 'tree' => $this->tree, + 'type' => 'collection', + ]); + } + + 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..05c71bf2 --- /dev/null +++ b/src/Structures/CollectionTreeRepository.php @@ -0,0 +1,30 @@ +where('locale', $site) + ->whereType('collection') + ->first(); + + return $model + ? app(CollectionTree::class)->fromModel($model) + : null; + } + + public function save($entry) + { + $model = $entry->toModel(); + + $model->save(); + + $entry->model($model->fresh()); + } +} diff --git a/src/Structures/Nav.php b/src/Structures/Nav.php new file mode 100644 index 00000000..c7252782 --- /dev/null +++ b/src/Structures/Nav.php @@ -0,0 +1,55 @@ +handle($model->handle) + ->title($model->title) + ->collections($model->collections) + ->maxDepth($model->maxDepth) + ->expectsRoot($model->expectsRoot) + ->initialPath($model->initialPath) + ->model($model); + } + + public function newTreeInstance() + { + return new NavTree; + } + + public function toModel() + { + $class = app('statamic.eloquent.navigations.model'); + + return $class::findOrNew($this->model?->id)->fill([ + 'handle' => $this->handle(), + 'title' => $this->title(), + 'collections' => $this->collections()->map->handle(), + 'maxDepth' => $this->maxDepth(), + 'expectsRoot' => $this->expectsRoot(), + 'initialPath' => $this->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..be34352c --- /dev/null +++ b/src/Structures/NavModel.php @@ -0,0 +1,18 @@ + 'json', + 'expectsRoot' => 'boolean', + 'maxDepth' => 'integer', + ]; +} diff --git a/src/Structures/NavTree.php b/src/Structures/NavTree.php new file mode 100644 index 00000000..48d22dc5 --- /dev/null +++ b/src/Structures/NavTree.php @@ -0,0 +1,45 @@ +tree($model->tree) + ->handle($model->handle) + ->locale($model->locale) + ->initialPath($model->initialPath) + ->model($model); + } + + public function toModel() + { + $class = app('statamic.eloquent.trees.model'); + + return $class::findOrNew($this->model?->id)->fill([ + 'handle' => $this->handle(), + 'initialPath' => $this->initialPath(), + 'locale' => $this->locale(), + 'tree' => $this->tree, + 'type' => 'navigation', + ]); + } + + 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..c24d3d73 --- /dev/null +++ b/src/Structures/NavTreeRepository.php @@ -0,0 +1,42 @@ +whereType('navigation') + ->where('locale', $site) + ->first(); + + return $model + ? app(TreeContract::class)->fromModel($model) + : null; + } + + public function save($entry) + { + $model = $entry->toModel(); + + $model->save(); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $entry->model()->delete(); + } + + public static function bindings() + { + return [ + TreeContract::class => NavTree::class, + ]; + } +} diff --git a/src/Structures/NavigationRepository.php b/src/Structures/NavigationRepository.php new file mode 100644 index 00000000..fd5fd02c --- /dev/null +++ b/src/Structures/NavigationRepository.php @@ -0,0 +1,52 @@ +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 + { + $model = NavModel::whereHandle($handle)->first(); + + return $model + ? app(NavContract::class)->fromModel($model) + : null; + } + + public function save($entry) + { + $model = $entry->toModel(); + + $model->save(); + + $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..9afc4f81 --- /dev/null +++ b/src/Structures/TreeModel.php @@ -0,0 +1,16 @@ + 'json', + ]; +} diff --git a/src/Taxonomies/Taxonomy.php b/src/Taxonomies/Taxonomy.php new file mode 100644 index 00000000..50bc0a8d --- /dev/null +++ b/src/Taxonomies/Taxonomy.php @@ -0,0 +1,44 @@ +handle($model->handle) + ->title($model->title) + ->sites($model->sites) + ->model($model); + } + + public function toModel() + { + $class = app('statamic.eloquent.taxonomies.model'); + + return $class::findOrNew($this->model?->id)->fill([ + 'handle' => $this->handle(), + 'title' => $this->title(), + 'sites' => $this->sites(), + ]); + } + + 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/Taxonomies/TaxonomyModel.php b/src/Taxonomies/TaxonomyModel.php new file mode 100644 index 00000000..a33ec055 --- /dev/null +++ b/src/Taxonomies/TaxonomyModel.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/TaxonomyRepository.php b/src/Taxonomies/TaxonomyRepository.php new file mode 100644 index 00000000..57497501 --- /dev/null +++ b/src/Taxonomies/TaxonomyRepository.php @@ -0,0 +1,52 @@ +map(function ($model) { + return Taxonomy::fromModel($model); + }); + } + + public static function bindings(): array + { + return [ + TaxonomyContract::class => Taxonomy::class, + ]; + } + + public function all(): Collection + { + return $this->transform(TaxonomyModel::all()); + } + + public function findByHandle($handle): ?TaxonomyContract + { + $taxonomyModel = TaxonomyModel::whereHandle($handle)->first(); + + return $taxonomyModel + ? app(TaxonomyContract::class)->fromModel($taxonomyModel) + : null; + } + + public function save($entry) + { + $model = $entry->toModel(); + + $model->save(); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $entry->model()->delete(); + } +} diff --git a/src/Taxonomies/Term.php b/src/Taxonomies/Term.php new file mode 100644 index 00000000..0faa2f40 --- /dev/null +++ b/src/Taxonomies/Term.php @@ -0,0 +1,71 @@ +slug($model->slug); + $term = $term->taxonomy($model->taxonomy); + $term = $term->data($model->data); + $term = $term->model($model); + $term = $term->blueprint($model->data['blueprint'] ?? null); + + collect($model->data['localizations'] ?? [])->each(function ($data, $locale) use ($term) { + $term->dataForLocale($locale, $data); + }); + + return $term; + } + + public function toModel() + { + $class = app('statamic.eloquent.terms.model'); + + $data = $this->data(); + + if ($this->blueprint && $this->taxonomy()->termBlueprints()->count() > 1) { + $data['blueprint'] = $this->blueprint; + } + + $data['localizations'] = $this->localizations()->keys()->reduce(function ($localizations, $locale) { + $localizations[$locale] = $this->dataForLocale($locale)->toArray(); + + return $localizations; + }, []); + + return $class::findOrNew($this->model?->id)->fill([ + 'site' => $this->locale(), + 'slug' => $this->slug(), + 'uri' => $this->uri(), + 'taxonomy' => $this->taxonomy(), + 'data' => $data, + ]); + } + + public function model($model = null) + { + if (func_num_args() === 0) { + return $this->model; + } + + $this->model = $model; + + $this->id($model->id); + + return $this; + } + + public function lastModified() + { + return $this->model->updated_at; + } +} diff --git a/src/Taxonomies/TermModel.php b/src/Taxonomies/TermModel.php new file mode 100644 index 00000000..35f17548 --- /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..a7050d4e --- /dev/null +++ b/src/Taxonomies/TermQueryBuilder.php @@ -0,0 +1,48 @@ +site; + if(!$site) { + $site = Site::default()->handle(); + } + + return TermCollection::make($items)->map(function ($model) use($site) { + return Term::fromModel($model)->in($site); + }); + } + + protected function column($column) + { + if (! in_array($column, $this->columns)) { + $column = 'data->'.$column; + } + + return $column; + } + + public function where($column, $operator = null, $value = null) + { + if ($column === 'site') { + $this->site = $operator; + + return $this; + } + + return parent::where($column, $operator, $value); + } +} diff --git a/src/Taxonomies/TermRepository.php b/src/Taxonomies/TermRepository.php new file mode 100644 index 00000000..7d2dfc32 --- /dev/null +++ b/src/Taxonomies/TermRepository.php @@ -0,0 +1,50 @@ +ensureAssociations(); + + return app(TermQueryBuilder::class); + } + + public function find($id): ?LocalizedTerm + { + [$handle, $slug] = explode('::', $id); + + $term = $this->query() + ->where('taxonomy', $handle) + ->where('slug', $slug); + $term = $term->first(); + + return $term; + } + + public function save($entry) + { + $model = $entry->toModel(); + + $model->save(); + + $entry->model($model->fresh()); + } + + public function delete($entry) + { + $entry->model()->delete(); + } + + public static function bindings(): array + { + return [ + TermContract::class => Term::class, + ]; + } +}