Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"require": {
"php": "^8.3",
"statamic/cms": "^6.10"
"statamic/cms": "dev-feature/hierarchical-taxonomies"
},
"require-dev": {
"doctrine/dbal": "^3.8",
Expand Down
8 changes: 8 additions & 0 deletions config/eloquent-driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,17 @@
'model' => \Statamic\Eloquent\Taxonomies\TaxonomyModel::class,
],

'taxonomy_trees' => [
'driver' => 'file',
'model' => \Statamic\Eloquent\Structures\TreeModel::class,
'tree' => \Statamic\Eloquent\Structures\TaxonomyTree::class,
],

'terms' => [
'driver' => 'file',
'model' => \Statamic\Eloquent\Taxonomies\TermModel::class,
'update_term_order_queue' => 'default',
'update_term_order_connection' => 'default',
],

'tokens' => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function up()
$table->string('uri')->nullable()->index();
$table->string('taxonomy')->index();
$table->jsonb('data');
$table->integer('order')->nullable()->index();
$table->timestamps();

$table->unique(['slug', 'taxonomy', 'site']);
Expand Down
21 changes: 21 additions & 0 deletions database/migrations/updates/add_order_to_terms_table.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Statamic\Eloquent\Database\BaseMigration as Migration;

return new class extends Migration {
public function up()
{
Schema::table($this->prefix('taxonomy_terms'), function (Blueprint $table) {
$table->integer('order')->after('data')->nullable();
});
}

public function down()
{
Schema::table($this->prefix('taxonomy_terms'), function (Blueprint $table) {
$table->dropColumn('order');
});
}
};
28 changes: 28 additions & 0 deletions src/Jobs/UpdateTaxonomyTermOrder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Statamic\Eloquent\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Statamic\Facades\Term;

class UpdateTaxonomyTermOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;

public $termId;

public function __construct($termId)
{
$this->termId = $termId;
}

public function handle()
{
if ($term = Term::find($this->termId)) {
$term->save();
}
}
}
18 changes: 18 additions & 0 deletions src/Listeners/UpdateTaxonomyTermOrder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Statamic\Eloquent\Listeners;

use Statamic\Contracts\Taxonomies\TermRepository as TermRepositoryContract;
use Statamic\Events\TaxonomyTreeSaved;

class UpdateTaxonomyTermOrder
{
public function handle(TaxonomyTreeSaved $event)
{
if (config('statamic.eloquent-driver.terms.driver', 'file') !== 'eloquent') {
return;
}

app(TermRepositoryContract::class)->updateOrders($event->tree->taxonomy());
}
}
23 changes: 23 additions & 0 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Statamic\Contracts\Structures\CollectionTreeRepository as CollectionTreeRepositoryContract;
use Statamic\Contracts\Structures\NavigationRepository as NavigationRepositoryContract;
use Statamic\Contracts\Structures\NavTreeRepository as NavTreeRepositoryContract;
use Statamic\Contracts\Structures\TaxonomyTreeRepository as TaxonomyTreeRepositoryContract;
use Statamic\Contracts\Taxonomies\TaxonomyRepository as TaxonomyRepositoryContract;
use Statamic\Contracts\Taxonomies\TermRepository as TermRepositoryContract;
use Statamic\Contracts\Tokens\TokenRepository as TokenRepositoryContract;
Expand All @@ -38,6 +39,7 @@
use Statamic\Eloquent\Structures\CollectionTreeRepository;
use Statamic\Eloquent\Structures\NavigationRepository;
use Statamic\Eloquent\Structures\NavTreeRepository;
use Statamic\Eloquent\Structures\TaxonomyTreeRepository;
use Statamic\Eloquent\Taxonomies\TaxonomyRepository;
use Statamic\Eloquent\Taxonomies\TermQueryBuilder;
use Statamic\Eloquent\Taxonomies\TermRepository;
Expand Down Expand Up @@ -65,6 +67,7 @@ class ServiceProvider extends AddonServiceProvider
\Statamic\Eloquent\Updates\AddOrderToSitesTable::class,
\Statamic\Eloquent\Updates\DropOriginOnGlobalSetVariables::class,
\Statamic\Eloquent\Updates\UpdateGlobalVariables::class,
\Statamic\Eloquent\Updates\AddOrderToTermsTable::class,
];

public function boot()
Expand Down Expand Up @@ -229,6 +232,7 @@ public function register()
$this->registerStructures();
$this->registerStructureTrees();
$this->registerTaxonomies();
$this->registerTaxonomyTrees();
$this->registerTerms();
$this->registerTokens();
$this->registerSites();
Expand Down Expand Up @@ -599,6 +603,25 @@ public function registerTaxonomies()
Stache::exclude('taxonomies');
}

private function registerTaxonomyTrees()
{
$this->app->bind('statamic.eloquent.taxonomies.tree', function () {
return config('statamic.eloquent-driver.taxonomy_trees.tree');
});

$this->app->bind('statamic.eloquent.taxonomies.tree_model', function () {
return config('statamic.eloquent-driver.taxonomy_trees.model');
});

if (config('statamic.eloquent-driver.taxonomy_trees.driver', 'file') != 'eloquent') {
return;
}

Statamic::repository(TaxonomyTreeRepositoryContract::class, TaxonomyTreeRepository::class);

Stache::exclude('taxonomy-trees');
}

public function registerTerms()
{
$this->app->bind('statamic.eloquent.terms.model', function () {
Expand Down
52 changes: 52 additions & 0 deletions src/Structures/TaxonomyTree.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Statamic\Eloquent\Structures;

use Illuminate\Database\Eloquent\Model;
use Statamic\Facades\Site;
use Statamic\Structures\TaxonomyTree as FileEntry;

class TaxonomyTree extends FileEntry
{
protected $model;

public static function fromModel(Model $model)
{
return (new static)
->tree($model->tree)
->handle($model->handle)
->locale($model->locale)
->syncOriginal()
->model($model);
}

public function toModel()
{
return self::makeModelFromContract($this);
}

public static function makeModelFromContract($source)
{
$class = app('statamic.eloquent.taxonomies.tree_model');

return $class::firstOrNew([
'handle' => $source->handle(),
'type' => 'taxonomy',
'locale' => Site::default()->handle(),
])->fill([
'tree' => $source->tree(),
'settings' => [],
]);
}

public function model($model = null)
{
if (func_num_args() === 0) {
return $this->model;
}

$this->model = $model;

return $this;
}
}
64 changes: 64 additions & 0 deletions src/Structures/TaxonomyTreeRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace Statamic\Eloquent\Structures;

use Statamic\Contracts\Structures\TaxonomyTree as TaxonomyTreeContract;
use Statamic\Contracts\Structures\Tree as TreeContract;
use Statamic\Facades\Blink;
use Statamic\Facades\Site;
use Statamic\Stache\Repositories\TaxonomyTreeRepository as StacheRepository;

class TaxonomyTreeRepository extends StacheRepository
{
public function find(string $handle): ?TreeContract
{
$site = Site::default()->handle();

return Blink::once("eloquent-taxonomy-tree-{$handle}-{$site}", function () use ($handle, $site) {
$model = app('statamic.eloquent.taxonomies.tree_model')::whereHandle($handle)
->where('locale', $site)
->whereType('taxonomy')
->first();

return $model ? app(app('statamic.eloquent.taxonomies.tree'))->fromModel($model) : null;
});
}

public function save($tree)
{
$model = $tree->toModel();
$model->save();

Blink::forget("eloquent-taxonomy-tree-{$model->handle}-{$model->locale}");

$tree->model($model->fresh());

return true;
}

public function delete($tree)
{
if (! $tree instanceof TaxonomyTree) {
return parent::delete($tree);
}

Blink::forget("eloquent-taxonomy-tree-{$tree->handle()}-{$tree->locale()}");

// TaxonomyStructure::in() caches the tree object under this key. Without
// forgetting it, code still holding the structure (e.g. Taxonomy::delete()
// deleting the tree's terms afterwards) would keep using the deleted,
// in-memory tree instance and re-insert it on save.
Blink::forget("taxonomy-structure-tree-{$tree->handle()}");

$tree->model()?->delete();

return true;
}

public static function bindings()
{
return [
TaxonomyTreeContract::class => TaxonomyTree::class,
];
}
}
4 changes: 4 additions & 0 deletions src/Taxonomies/Taxonomy.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public static function fromModel(Model $model)
->termTemplate($model->settings['term_template'] ?? null)
->template($model->settings['template'] ?? null)
->layout($model->settings['layout'] ?? null)
->routes($model->settings['routes'] ?? null)
->structureContents($model->settings['structure'] ?? null)
->model($model);
}

Expand All @@ -48,6 +50,8 @@ public static function makeModelFromContract(Contract $source)
'term_template' => $source->hasCustomTermTemplate() ? $source->termTemplate() : null,
'template' => $source->hasCustomTemplate() ? $source->template() : null,
'layout' => $source->layout,
'routes' => $source->routes,
'structure' => $source->structureContents(),
]);

return $model;
Expand Down
4 changes: 4 additions & 0 deletions src/Taxonomies/Term.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ public static function makeModelFromContract(Contract $source)
'uri' => $source->uri(),
'data' => collect($data)->filter(fn ($v) => $v !== null),
'updated_at' => $source->lastModified(),
// Calling ->order() through Term's __call proxy compares the return
// value against a LocalizedTerm instance, which throws for scalars.
// Go straight through inDefaultLocale() to avoid it.
'order' => $source->inDefaultLocale()->order(),
]);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Taxonomies/TermQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class TermQueryBuilder extends EloquentQueryBuilder
protected $taxonomies = [];

protected $columns = [
'id', 'data', 'site', 'slug', 'uri', 'taxonomy', 'created_at', 'updated_at',
'id', 'data', 'site', 'slug', 'uri', 'taxonomy', 'created_at', 'updated_at', 'order',
];

protected function transform($items, $columns = [])
Expand Down
30 changes: 29 additions & 1 deletion src/Taxonomies/TermRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Statamic\Eloquent\Taxonomies;

use Statamic\Contracts\Taxonomies\Term as TermContract;
use Statamic\Eloquent\Jobs\UpdateTaxonomyTermOrder;
use Statamic\Facades\Blink;
use Statamic\Facades\Collection;
use Statamic\Facades\Entry;
Expand Down Expand Up @@ -39,7 +40,10 @@ public function find($id): ?TermContract
return null;
}

return $term;
// A term saved earlier in the request is cached here in its raw, locale-less
// form (see save()). Resolve it the same way the freshly-queried path does,
// since some Term methods only behave correctly on a LocalizedTerm.
return $term instanceof LocalizedTerm ? $term : $term->inDefaultLocale();
}

public function findByUri(string $uri, ?string $site = null): ?TermContract
Expand Down Expand Up @@ -106,6 +110,13 @@ public function save($entry)

$entry->model($model->fresh());

// Building the model (e.g. resolving a hierarchical term's URI) may have
// queried and cached the taxonomy's existing term slugs before this term
// was persisted. Forget it so the next read reflects the saved term.
if (($taxonomy = $entry->taxonomy()) && $taxonomy->hasStructure()) {
Blink::forget('taxonomy-structure-term-slugs-'.$taxonomy->handle());
}

Blink::put("eloquent-term-{$entry->id()}", $entry);
Blink::put("eloquent-term-{$entry->uri()}", $entry);
}
Expand Down Expand Up @@ -157,4 +168,21 @@ public function entriesCount(TermContract $term, ?string $status = null): int

return $query->count();
}

public function updateOrders($taxonomy)
{
$taxonomy->queryTerms()
->get()
->each(function ($term) {
$dispatch = UpdateTaxonomyTermOrder::dispatch($term->id());

$connection = config('statamic.eloquent-driver.terms.update_term_order_connection', 'default');

if ($connection != 'default') {
$dispatch->onConnection($connection);
}

$dispatch->onQueue(config('statamic.eloquent-driver.terms.update_term_order_queue', 'default'));
});
}
}
Loading