Keep artwork_category as membership, store a deterministic primary_category_id, and let upload plus studio pick up to five additional same-type categories without changing canonical URLs.
140 lines
4.4 KiB
PHP
140 lines
4.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Artwork;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class BackfillArtworkPrimaryCategoryCommand extends Command
|
|
{
|
|
protected $signature = 'artworks:backfill-primary-category
|
|
{--dry-run : Report without writing (default unless --apply is passed)}
|
|
{--report : Print category-count distribution and conflicts}
|
|
{--apply : Write primary_category_id for artworks with exactly one category}
|
|
{--force : Overwrite existing primary_category_id values}';
|
|
|
|
protected $description = 'Backfill artworks.primary_category_id from unique artwork_category memberships';
|
|
|
|
public function handle(): int
|
|
{
|
|
$apply = (bool) $this->option('apply');
|
|
$force = (bool) $this->option('force');
|
|
$report = (bool) $this->option('report') || ! $apply;
|
|
|
|
if ($report) {
|
|
$this->printDistribution();
|
|
}
|
|
|
|
$conflicts = $this->conflictArtworkIds();
|
|
$zeros = $this->zeroCategoryArtworkIds();
|
|
$eligible = $this->eligibleArtworkIds($force);
|
|
|
|
$this->info('Zero-category artworks: '.$zeros->count());
|
|
$this->info('Single-category eligible for backfill: '.$eligible->count());
|
|
$this->info('Multi-category conflicts (not auto-resolved): '.$conflicts->count());
|
|
|
|
if ($conflicts->isNotEmpty()) {
|
|
$this->warn('Multi-category artwork IDs (first 50): '.$conflicts->take(50)->implode(', '));
|
|
}
|
|
|
|
if (! $apply) {
|
|
$this->comment('Dry run only. Re-run with --apply to write primary_category_id for single-category artworks.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$updated = 0;
|
|
foreach ($eligible->chunk(500) as $chunk) {
|
|
$rows = DB::table('artwork_category')
|
|
->select('artwork_id', 'category_id')
|
|
->whereIn('artwork_id', $chunk->all())
|
|
->get()
|
|
->groupBy('artwork_id');
|
|
|
|
foreach ($rows as $artworkId => $memberships) {
|
|
if ($memberships->count() !== 1) {
|
|
continue;
|
|
}
|
|
|
|
$query = Artwork::query()->where('id', (int) $artworkId);
|
|
if (! $force) {
|
|
$query->whereNull('primary_category_id');
|
|
}
|
|
|
|
$updated += $query->update([
|
|
'primary_category_id' => (int) $memberships->first()->category_id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$this->info("Updated {$updated} artwork(s).");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function printDistribution(): void
|
|
{
|
|
$distribution = DB::query()
|
|
->fromSub(function ($query): void {
|
|
$query->from('artwork_category')
|
|
->select('artwork_id', DB::raw('COUNT(*) as category_count'))
|
|
->groupBy('artwork_id');
|
|
}, 'x')
|
|
->select('category_count', DB::raw('COUNT(*) as artworks'))
|
|
->groupBy('category_count')
|
|
->orderBy('category_count')
|
|
->get();
|
|
|
|
$this->table(['category_count', 'artworks'], $distribution->map(fn ($row): array => [
|
|
(int) $row->category_count,
|
|
(int) $row->artworks,
|
|
])->all());
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, int>
|
|
*/
|
|
private function eligibleArtworkIds(bool $force)
|
|
{
|
|
$query = DB::table('artwork_category')
|
|
->select('artwork_id')
|
|
->groupBy('artwork_id')
|
|
->havingRaw('COUNT(*) = 1');
|
|
|
|
if (! $force) {
|
|
$query->whereIn('artwork_id', Artwork::query()->whereNull('primary_category_id')->select('id'));
|
|
}
|
|
|
|
return $query->pluck('artwork_id')->map(fn ($id): int => (int) $id);
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, int>
|
|
*/
|
|
private function conflictArtworkIds()
|
|
{
|
|
return DB::table('artwork_category')
|
|
->select('artwork_id')
|
|
->groupBy('artwork_id')
|
|
->havingRaw('COUNT(*) > 1')
|
|
->orderByRaw('COUNT(*) DESC')
|
|
->pluck('artwork_id')
|
|
->map(fn ($id): int => (int) $id);
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, int>
|
|
*/
|
|
private function zeroCategoryArtworkIds()
|
|
{
|
|
return Artwork::query()
|
|
->whereDoesntHave('categories')
|
|
->pluck('id')
|
|
->map(fn ($id): int => (int) $id);
|
|
}
|
|
}
|