|null $secondaryCategoryIds null keeps existing secondaries that remain compatible */ public function sync( Artwork $artwork, ?int $primaryCategoryId, ?array $secondaryCategoryIds = [], ?string $source = null, bool $reindex = true, ): void { $primaryCategoryId = $primaryCategoryId !== null && $primaryCategoryId > 0 ? $primaryCategoryId : null; $secondaryCategoryIds = $secondaryCategoryIds === null ? $this->existingSecondaryIds($artwork, $primaryCategoryId) : $this->normalizeIds($secondaryCategoryIds); if ($primaryCategoryId === null) { if ($secondaryCategoryIds !== []) { throw ValidationException::withMessages([ 'primary_category_id' => ['A primary category is required when additional categories are selected.'], ]); } $this->persist($artwork, null, [], $source, $reindex); return; } $secondaryCategoryIds = array_values(array_filter( $secondaryCategoryIds, static fn (int $id): bool => $id !== $primaryCategoryId, )); if (count($secondaryCategoryIds) > $this->maxSecondary()) { throw ValidationException::withMessages([ 'secondary_category_ids' => [sprintf('You can add at most %d additional categories.', $this->maxSecondary())], ]); } $requestedIds = array_values(array_unique([$primaryCategoryId, ...$secondaryCategoryIds])); $categories = Category::query() ->whereIn('id', $requestedIds) ->get() ->keyBy('id'); if ($categories->count() !== count($requestedIds)) { throw ValidationException::withMessages([ 'primary_category_id' => ['One or more selected categories do not exist.'], ]); } $primary = $categories->get($primaryCategoryId); if (! $primary) { throw ValidationException::withMessages([ 'primary_category_id' => ['Choose a valid primary category.'], ]); } if (! $primary->is_active) { throw ValidationException::withMessages([ 'primary_category_id' => ['The primary category is not available.'], ]); } $contentTypeId = (int) $primary->content_type_id; foreach ($secondaryCategoryIds as $secondaryId) { $category = $categories->get($secondaryId); if (! $category || ! $category->is_active) { throw ValidationException::withMessages([ 'secondary_category_ids' => ['One or more additional categories are not available.'], ]); } if ((int) $category->content_type_id !== $contentTypeId) { throw ValidationException::withMessages([ 'secondary_category_ids' => ['Additional categories must belong to the same content type as the primary category.'], ]); } } $this->persist($artwork, $primaryCategoryId, $secondaryCategoryIds, $source, $reindex); } /** * @return array */ public function presentCategory(Category $category, bool $isPrimary): array { return [ 'id' => (int) $category->id, 'name' => html_entity_decode((string) $category->name, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'slug' => (string) $category->slug, 'content_type_slug' => (string) ($category->contentType?->slug ?? ''), 'url' => $category->contentType ? $category->url : null, 'is_primary' => $isPrimary, 'parent' => $category->parent ? [ 'id' => (int) $category->parent->id, 'slug' => (string) $category->parent->slug, 'name' => html_entity_decode((string) $category->parent->name, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'content_type_slug' => (string) ($category->parent->contentType?->slug ?? ''), 'url' => $category->parent->contentType ? $category->parent->url : null, ] : null, ]; } /** * @param list $ids * @return list */ private function normalizeIds(array $ids): array { $normalized = []; foreach ($ids as $id) { $value = (int) $id; if ($value > 0 && ! in_array($value, $normalized, true)) { $normalized[] = $value; } } return $normalized; } /** * @return list */ private function existingSecondaryIds(Artwork $artwork, ?int $primaryCategoryId): array { return $artwork->categories() ->pluck('categories.id') ->map(static fn ($id): int => (int) $id) ->reject(static fn (int $id): bool => $primaryCategoryId !== null && $id === $primaryCategoryId) ->values() ->all(); } /** * @param list $secondaryCategoryIds */ private function persist( Artwork $artwork, ?int $primaryCategoryId, array $secondaryCategoryIds, ?string $source, bool $reindex, ): void { $membershipIds = $primaryCategoryId === null ? [] : array_values(array_unique([$primaryCategoryId, ...$secondaryCategoryIds])); DB::transaction(function () use ($artwork, $primaryCategoryId, $membershipIds, $source): void { $artwork->categories()->sync($membershipIds); $artwork->primary_category_id = $primaryCategoryId; if ($source !== null) { $artwork->category_source = $source; } $artwork->save(); }); $artwork->unsetRelation('categories'); $artwork->unsetRelation('primaryCategory'); if (! $reindex) { return; } DB::afterCommit(function () use ($artwork): void { $this->artworkService->clearArtworkCache($artwork->fresh() ?? $artwork); Cache::forget('categories.directory.v1'); $this->searchIndexer->update($artwork); }); } }