Files
SkinbaseNova/app/Services/Artworks/ArtworkPublicationService.php
2026-04-18 17:02:56 +02:00

162 lines
4.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Artworks;
use App\Models\ActivityEvent;
use App\Models\Artwork;
use App\Services\Activity\UserActivityService;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ArtworkPublicationService
{
public function publishNow(Artwork $artwork, ?Carbon $publishedAt = null): Artwork
{
$publishedAt ??= now()->utc();
$artwork->forceFill([
'artwork_status' => 'published',
'publish_at' => null,
'artwork_timezone' => null,
'published_at' => $publishedAt,
'is_public' => $artwork->visibility !== Artwork::VISIBILITY_PRIVATE,
])->save();
$this->syncSearch($artwork);
$this->recordActivity($artwork);
return $artwork;
}
public function publishIfDue(Artwork $artwork, ?Carbon $now = null): Artwork
{
$now ??= now()->utc();
if (! $this->isDue($artwork, $now)) {
return $artwork;
}
DB::transaction(function () use (&$artwork, $now): void {
$locked = Artwork::query()
->lockForUpdate()
->find($artwork->id);
if (! $locked || ! $this->isDue($locked, $now)) {
if ($locked) {
$artwork = $locked;
}
return;
}
$artwork = $this->publishNow($locked, $now);
});
return $artwork->fresh() ?? $artwork;
}
public function publishDueScheduled(int $limit = 100, ?Carbon $now = null): array
{
$now ??= now()->utc();
$candidates = Artwork::query()
->where('artwork_status', 'scheduled')
->where('publish_at', '<=', $now)
->where('is_approved', true)
->orderBy('publish_at')
->limit($limit)
->get(['id', 'user_id', 'title', 'publish_at', 'artwork_status']);
$published = collect();
foreach ($candidates as $candidate) {
$result = null;
DB::transaction(function () use ($candidate, $now, &$result): void {
$locked = Artwork::query()
->lockForUpdate()
->where('id', $candidate->id)
->where('artwork_status', 'scheduled')
->first();
if (! $locked || ! $this->isDue($locked, $now)) {
return;
}
$result = $this->publishNow($locked, $now);
});
if ($result instanceof Artwork) {
$published->push($result);
}
}
return [
'candidates' => $candidates,
'published' => $published,
];
}
public function publishDueScheduledForUser(int $userId, int $limit = 100, ?Carbon $now = null): void
{
$now ??= now()->utc();
$candidateIds = Artwork::query()
->where('user_id', $userId)
->where('artwork_status', 'scheduled')
->where('publish_at', '<=', $now)
->where('is_approved', true)
->orderBy('publish_at')
->limit($limit)
->pluck('id');
foreach ($candidateIds as $candidateId) {
$artwork = Artwork::query()->find((int) $candidateId);
if ($artwork) {
$this->publishIfDue($artwork, $now);
}
}
}
private function isDue(Artwork $artwork, Carbon $now): bool
{
return $artwork->artwork_status === 'scheduled'
&& $artwork->is_approved
&& $artwork->publish_at !== null
&& $artwork->publish_at->lte($now);
}
private function syncSearch(Artwork $artwork): void
{
if (! method_exists($artwork, 'searchable')) {
return;
}
try {
$artwork->searchable();
} catch (\Throwable $e) {
Log::warning("PublishScheduled: scout reindex failed for #{$artwork->id}: {$e->getMessage()}");
}
}
private function recordActivity(Artwork $artwork): void
{
try {
ActivityEvent::record(
actorId: (int) $artwork->user_id,
type: ActivityEvent::TYPE_UPLOAD,
targetType: ActivityEvent::TARGET_ARTWORK,
targetId: (int) $artwork->id,
);
} catch (\Throwable) {
}
try {
app(UserActivityService::class)->logUpload((int) $artwork->user_id, (int) $artwork->id);
} catch (\Throwable) {
}
}
}