Files
SkinbaseNova/app/Services/Studio/StudioBulkActionService.php

166 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\Artwork;
use App\Models\Tag;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Handles bulk operations on artworks for the Studio module.
*/
final class StudioBulkActionService
{
/**
* Execute a bulk action on the given artwork IDs, enforcing ownership.
*
* @param int $userId The authenticated user ID
* @param string $action publish|unpublish|archive|unarchive|delete|change_category|add_tags|remove_tags
* @param array $artworkIds Array of artwork IDs
* @param array $params Extra params (category_id, tag_ids)
* @return array{success: int, failed: int, errors: array}
*/
public function execute(int $userId, string $action, array $artworkIds, array $params = []): array
{
$result = ['success' => 0, 'failed' => 0, 'errors' => []];
// Validate ownership — fetch only artworks belonging to this user
$query = Artwork::where('user_id', $userId);
if ($action === 'unarchive') {
$query->onlyTrashed();
}
$artworks = $query->whereIn('id', $artworkIds)->get();
$foundIds = $artworks->pluck('id')->all();
$missingIds = array_diff($artworkIds, $foundIds);
foreach ($missingIds as $id) {
$result['failed']++;
$result['errors'][] = "Artwork #{$id}: not found or not owned by you";
}
if ($artworks->isEmpty()) {
return $result;
}
DB::beginTransaction();
try {
foreach ($artworks as $artwork) {
$this->applyAction($artwork, $action, $params);
$result['success']++;
}
DB::commit();
// Reindex affected artworks in Meilisearch
$this->reindexArtworks($artworks);
Log::info('Studio bulk action completed', [
'user_id' => $userId,
'action' => $action,
'count' => $result['success'],
'ids' => $foundIds,
]);
} catch (\Throwable $e) {
DB::rollBack();
$result['failed'] += $result['success'];
$result['success'] = 0;
$result['errors'][] = 'Transaction failed: ' . $e->getMessage();
Log::error('Studio bulk action failed', [
'user_id' => $userId,
'action' => $action,
'error' => $e->getMessage(),
]);
}
return $result;
}
private function applyAction(Artwork $artwork, string $action, array $params): void
{
match ($action) {
'publish' => $this->publish($artwork),
'unpublish' => $this->unpublish($artwork),
'archive' => $artwork->delete(), // Soft delete
'unarchive' => $artwork->restore(),
'delete' => $artwork->forceDelete(),
'change_category' => $this->changeCategory($artwork, $params),
'add_tags' => $this->addTags($artwork, $params),
'remove_tags' => $this->removeTags($artwork, $params),
default => throw new \InvalidArgumentException("Unknown action: {$action}"),
};
}
private function publish(Artwork $artwork): void
{
$artwork->update([
'is_public' => true,
'published_at' => $artwork->published_at ?? now(),
]);
}
private function unpublish(Artwork $artwork): void
{
$artwork->update(['is_public' => false]);
}
private function changeCategory(Artwork $artwork, array $params): void
{
if (empty($params['category_id'])) {
throw new \InvalidArgumentException('category_id required for change_category');
}
$artwork->categories()->sync([(int) $params['category_id']]);
}
private function addTags(Artwork $artwork, array $params): void
{
if (empty($params['tag_ids'])) {
throw new \InvalidArgumentException('tag_ids required for add_tags');
}
$pivotData = [];
foreach ((array) $params['tag_ids'] as $tagId) {
$pivotData[(int) $tagId] = ['source' => 'studio_bulk', 'confidence' => 1.0];
}
$artwork->tags()->syncWithoutDetaching($pivotData);
// Increment usage counts
Tag::whereIn('id', array_keys($pivotData))
->increment('usage_count');
}
private function removeTags(Artwork $artwork, array $params): void
{
if (empty($params['tag_ids'])) {
throw new \InvalidArgumentException('tag_ids required for remove_tags');
}
$tagIds = array_map('intval', (array) $params['tag_ids']);
$artwork->tags()->detach($tagIds);
Tag::whereIn('id', $tagIds)
->where('usage_count', '>', 0)
->decrement('usage_count');
}
/**
* Trigger Meilisearch reindex for the given artworks.
*/
private function reindexArtworks(\Illuminate\Database\Eloquent\Collection $artworks): void
{
try {
$artworks->each->searchable();
} catch (\Throwable $e) {
Log::warning('Studio: Failed to reindex artworks after bulk action', [
'error' => $e->getMessage(),
]);
}
}
}