166 lines
5.3 KiB
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(),
|
|
]);
|
|
}
|
|
}
|
|
}
|