644 lines
24 KiB
PHP
644 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Artwork;
|
|
use App\Models\ArtworkRelation;
|
|
use App\Models\User;
|
|
use App\Services\Maturity\ArtworkMaturityService;
|
|
use App\Services\Vision\VectorService;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
final class ArtworkEvolutionService
|
|
{
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public static function relationTypes(): array
|
|
{
|
|
return [
|
|
ArtworkRelation::TYPE_REMAKE_OF,
|
|
ArtworkRelation::TYPE_REMASTER_OF,
|
|
ArtworkRelation::TYPE_REVISION_OF,
|
|
ArtworkRelation::TYPE_INSPIRED_BY,
|
|
ArtworkRelation::TYPE_VARIATION_OF,
|
|
];
|
|
}
|
|
|
|
public function __construct(
|
|
private readonly ArtworkMaturityService $maturity,
|
|
private readonly GroupService $groups,
|
|
private readonly VectorService $vectors,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, string>>
|
|
*/
|
|
public function relationTypeOptions(): array
|
|
{
|
|
return array_map(fn (string $type): array => [
|
|
'value' => $type,
|
|
'label' => $this->relationTypeLabel($type),
|
|
'short_label' => $this->relationTypeShortLabel($type),
|
|
], self::relationTypes());
|
|
}
|
|
|
|
/**
|
|
* @param array{target_artwork_id?: int|null, relation_type?: string|null, note?: string|null} $payload
|
|
*/
|
|
public function syncPrimaryRelation(Artwork $sourceArtwork, User $actor, array $payload): ?ArtworkRelation
|
|
{
|
|
$this->ensureManageable($actor, $sourceArtwork, 'You can only update evolution links for artworks you manage.');
|
|
|
|
$targetArtworkId = (int) ($payload['target_artwork_id'] ?? 0);
|
|
$relationType = $this->normalizeRelationType((string) ($payload['relation_type'] ?? ArtworkRelation::TYPE_REMAKE_OF));
|
|
$note = $this->normalizeNote($payload['note'] ?? null);
|
|
|
|
if ($targetArtworkId <= 0) {
|
|
ArtworkRelation::query()->where('source_artwork_id', (int) $sourceArtwork->id)->delete();
|
|
|
|
return null;
|
|
}
|
|
|
|
if ($targetArtworkId === (int) $sourceArtwork->id) {
|
|
throw ValidationException::withMessages([
|
|
'evolution_target_artwork_id' => 'Choose an older artwork, not the artwork you are editing right now.',
|
|
]);
|
|
}
|
|
|
|
$targetArtwork = Artwork::query()
|
|
->with(['group.members'])
|
|
->find($targetArtworkId);
|
|
|
|
if (! $targetArtwork) {
|
|
throw ValidationException::withMessages([
|
|
'evolution_target_artwork_id' => 'Choose a valid artwork to link as the original version.',
|
|
]);
|
|
}
|
|
|
|
$this->ensureManageable($actor, $targetArtwork, 'You can only link artworks that you are allowed to manage.');
|
|
|
|
if (! $this->isPubliclyVisible($targetArtwork)) {
|
|
throw ValidationException::withMessages([
|
|
'evolution_target_artwork_id' => 'Choose a published public artwork for the original version.',
|
|
]);
|
|
}
|
|
|
|
if (! $this->isOlderVersionCandidate($sourceArtwork, $targetArtwork)) {
|
|
throw ValidationException::withMessages([
|
|
'evolution_target_artwork_id' => 'Choose an older artwork as the original version for this Then & Now story.',
|
|
]);
|
|
}
|
|
|
|
return DB::transaction(function () use ($sourceArtwork, $targetArtwork, $actor, $relationType, $note): ArtworkRelation {
|
|
ArtworkRelation::query()
|
|
->where('source_artwork_id', (int) $sourceArtwork->id)
|
|
->delete();
|
|
|
|
return ArtworkRelation::query()->create([
|
|
'source_artwork_id' => (int) $sourceArtwork->id,
|
|
'target_artwork_id' => (int) $targetArtwork->id,
|
|
'relation_type' => $relationType,
|
|
'note' => $note,
|
|
'sort_order' => 0,
|
|
'created_by_user_id' => (int) $actor->id,
|
|
])->load([
|
|
'targetArtwork.user.profile',
|
|
'targetArtwork.group',
|
|
'targetArtwork.categories.contentType',
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function editorRelation(Artwork $artwork, User $actor): ?array
|
|
{
|
|
$relation = ArtworkRelation::query()
|
|
->with(['targetArtwork.user.profile', 'targetArtwork.group', 'targetArtwork.categories.contentType'])
|
|
->where('source_artwork_id', (int) $artwork->id)
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->first();
|
|
|
|
if (! $relation || ! $relation->targetArtwork) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => (int) $relation->id,
|
|
'relation_type' => (string) $relation->relation_type,
|
|
'relation_label' => $this->relationTypeLabel((string) $relation->relation_type),
|
|
'short_label' => $this->relationTypeShortLabel((string) $relation->relation_type),
|
|
'note' => $relation->note,
|
|
'target_artwork' => $this->mapStudioOption($relation->targetArtwork, $actor),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function manageableSearchOptions(Artwork $sourceArtwork, User $actor, string $search = '', int $limit = 18): array
|
|
{
|
|
$this->ensureManageable($actor, $sourceArtwork, 'You can only search evolution links for artworks you manage.');
|
|
|
|
$manageableGroupIds = collect($this->groups->studioOptionsForUser($actor))
|
|
->filter(fn (array $group): bool => (bool) data_get($group, 'permissions.can_publish_artworks', false))
|
|
->pluck('id')
|
|
->map(static fn ($id): int => (int) $id)
|
|
->filter()
|
|
->values();
|
|
|
|
$term = trim($search);
|
|
$safeLimit = max(1, min($limit, 36));
|
|
$rankedOptions = [];
|
|
$rankedIds = [];
|
|
|
|
if ($this->vectors->isConfigured()) {
|
|
$rankedOptions = $this->similarityRankedOptions($sourceArtwork, $actor, $manageableGroupIds->all(), $term, $safeLimit);
|
|
$rankedIds = array_map(static fn (array $option): int => (int) ($option['id'] ?? 0), $rankedOptions);
|
|
$rankedIds = array_values(array_filter($rankedIds));
|
|
}
|
|
|
|
if (count($rankedOptions) >= $safeLimit) {
|
|
return array_slice($rankedOptions, 0, $safeLimit);
|
|
}
|
|
|
|
$fallbackOptions = $this->fallbackSearchOptions(
|
|
$sourceArtwork,
|
|
$actor,
|
|
$manageableGroupIds->all(),
|
|
$term,
|
|
$safeLimit,
|
|
$rankedIds,
|
|
);
|
|
|
|
return collect(array_merge($rankedOptions, $fallbackOptions))
|
|
->unique('id')
|
|
->take($safeLimit)
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function publicPayload(Artwork $artwork, ?User $viewer = null): ?array
|
|
{
|
|
$primaryRelation = ArtworkRelation::query()
|
|
->with([
|
|
'sourceArtwork.user.profile',
|
|
'sourceArtwork.group',
|
|
'sourceArtwork.categories.contentType',
|
|
'targetArtwork.user.profile',
|
|
'targetArtwork.group',
|
|
'targetArtwork.categories.contentType',
|
|
])
|
|
->where('source_artwork_id', (int) $artwork->id)
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->first();
|
|
|
|
$incomingRelations = ArtworkRelation::query()
|
|
->with([
|
|
'sourceArtwork.user.profile',
|
|
'sourceArtwork.group',
|
|
'sourceArtwork.categories.contentType',
|
|
'targetArtwork.user.profile',
|
|
'targetArtwork.group',
|
|
'targetArtwork.categories.contentType',
|
|
])
|
|
->where('target_artwork_id', (int) $artwork->id)
|
|
->orderByDesc('updated_at')
|
|
->orderByDesc('id')
|
|
->limit(4)
|
|
->get();
|
|
|
|
$primary = $primaryRelation ? $this->mapPrimaryPanel($primaryRelation, $viewer) : null;
|
|
$updates = $incomingRelations
|
|
->map(fn (ArtworkRelation $relation): ?array => $this->mapIncomingUpdate($relation, $viewer))
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
if ($primary === null && $updates === []) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'eyebrow' => 'Artwork Evolution',
|
|
'primary' => $primary,
|
|
'updates' => $updates,
|
|
];
|
|
}
|
|
|
|
private function ensureManageable(User $actor, Artwork $artwork, string $message): void
|
|
{
|
|
if (! Gate::forUser($actor)->allows('update', $artwork)) {
|
|
throw ValidationException::withMessages([
|
|
'evolution_target_artwork_id' => $message,
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function isPubliclyVisible(Artwork $artwork): bool
|
|
{
|
|
return ! $artwork->trashed()
|
|
&& (bool) $artwork->is_public
|
|
&& (bool) $artwork->is_approved
|
|
&& $artwork->published_at !== null
|
|
&& $artwork->published_at->lte(now());
|
|
}
|
|
|
|
private function isOlderVersionCandidate(Artwork $sourceArtwork, Artwork $targetArtwork): bool
|
|
{
|
|
$sourceTimestamp = $this->comparisonTimestamp($sourceArtwork);
|
|
$targetTimestamp = $this->comparisonTimestamp($targetArtwork);
|
|
|
|
if ($sourceTimestamp === null || $targetTimestamp === null) {
|
|
return true;
|
|
}
|
|
|
|
return $targetTimestamp->lt($sourceTimestamp);
|
|
}
|
|
|
|
private function comparisonTimestamp(Artwork $artwork): ?Carbon
|
|
{
|
|
$value = $artwork->published_at ?: $artwork->created_at;
|
|
|
|
return $value instanceof Carbon ? $value : ($value ? Carbon::parse($value) : null);
|
|
}
|
|
|
|
private function normalizeRelationType(string $type): string
|
|
{
|
|
$normalized = Str::lower(trim($type));
|
|
|
|
return in_array($normalized, self::relationTypes(), true)
|
|
? $normalized
|
|
: ArtworkRelation::TYPE_REMAKE_OF;
|
|
}
|
|
|
|
private function normalizeNote(mixed $note): ?string
|
|
{
|
|
$resolved = trim((string) $note);
|
|
|
|
return $resolved !== '' ? $resolved : null;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function mapStudioOption(Artwork $artwork, User $actor, array $context = []): array
|
|
{
|
|
$category = $artwork->categories->sortBy('sort_order')->first();
|
|
$publishedAt = $artwork->published_at;
|
|
$year = $publishedAt?->year ?: $artwork->created_at?->year;
|
|
$similarityScore = array_key_exists('similarity_score', $context) && is_numeric($context['similarity_score'])
|
|
? round((float) $context['similarity_score'], 5)
|
|
: null;
|
|
|
|
return [
|
|
'id' => (int) $artwork->id,
|
|
'title' => html_entity_decode((string) $artwork->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
|
'publisher' => $artwork->group?->name ?: $artwork->user?->name ?: $artwork->user?->username ?: 'Artist',
|
|
'year' => $year,
|
|
'published_at' => optional($publishedAt)->toIsoString(),
|
|
'thumbnail' => ThumbnailPresenter::present($artwork, 'md')['url'] ?? null,
|
|
'url' => route('art.show', [
|
|
'id' => (int) $artwork->id,
|
|
'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: (string) $artwork->id,
|
|
]),
|
|
'studio_edit_url' => route('studio.artworks.edit', ['id' => (int) $artwork->id]),
|
|
'content_type' => $category?->contentType?->name,
|
|
'category' => $category?->name,
|
|
'is_manageable' => Gate::forUser($actor)->allows('update', $artwork),
|
|
'similarity_score' => $similarityScore,
|
|
'sort_source' => (string) ($context['sort_source'] ?? 'fallback'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param list<int> $manageableGroupIds
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function similarityRankedOptions(Artwork $sourceArtwork, User $actor, array $manageableGroupIds, string $term, int $limit): array
|
|
{
|
|
try {
|
|
$matches = $this->vectors->similarToArtwork($sourceArtwork, min(120, max($limit * 4, 48)));
|
|
} catch (\Throwable) {
|
|
return [];
|
|
}
|
|
|
|
$orderedIds = [];
|
|
$scores = [];
|
|
|
|
foreach ($matches as $match) {
|
|
$candidateId = (int) ($match['id'] ?? 0);
|
|
if ($candidateId <= 0 || isset($scores[$candidateId])) {
|
|
continue;
|
|
}
|
|
|
|
$orderedIds[] = $candidateId;
|
|
$scores[$candidateId] = (float) ($match['score'] ?? 0.0);
|
|
}
|
|
|
|
if ($orderedIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$candidates = $this->manageableCandidatesQuery($sourceArtwork, $actor, $manageableGroupIds, $term)
|
|
->whereIn('id', $orderedIds)
|
|
->get()
|
|
->keyBy('id');
|
|
|
|
$options = [];
|
|
foreach ($orderedIds as $candidateId) {
|
|
/** @var Artwork|null $candidate */
|
|
$candidate = $candidates->get($candidateId);
|
|
if (! $candidate) {
|
|
continue;
|
|
}
|
|
|
|
$options[] = $this->mapStudioOption($candidate, $actor, [
|
|
'similarity_score' => $scores[$candidateId] ?? null,
|
|
'sort_source' => 'vector_similarity',
|
|
]);
|
|
|
|
if (count($options) >= $limit) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* @param list<int> $manageableGroupIds
|
|
* @param list<int> $excludeIds
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function fallbackSearchOptions(Artwork $sourceArtwork, User $actor, array $manageableGroupIds, string $term, int $limit, array $excludeIds = []): array
|
|
{
|
|
$query = $this->manageableCandidatesQuery($sourceArtwork, $actor, $manageableGroupIds, $term);
|
|
|
|
if ($excludeIds !== []) {
|
|
$query->whereNotIn('id', $excludeIds);
|
|
}
|
|
|
|
return $query
|
|
->orderByRaw('CASE WHEN user_id = ? THEN 0 ELSE 1 END', [(int) $actor->id])
|
|
->orderByRaw('CASE WHEN published_at IS NULL THEN 1 ELSE 0 END')
|
|
->orderByDesc('published_at')
|
|
->limit(max($limit * 2, 36))
|
|
->get()
|
|
->map(fn (Artwork $candidate): array => $this->mapStudioOption($candidate, $actor))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param list<int> $manageableGroupIds
|
|
*/
|
|
private function manageableCandidatesQuery(Artwork $sourceArtwork, User $actor, array $manageableGroupIds, string $term): Builder
|
|
{
|
|
$query = Artwork::query()
|
|
->with(['user.profile', 'group', 'categories.contentType'])
|
|
->whereKeyNot((int) $sourceArtwork->id)
|
|
->whereNull('deleted_at')
|
|
->where('is_public', true)
|
|
->where('is_approved', true)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '<=', now())
|
|
->where(function ($builder) use ($actor, $manageableGroupIds): void {
|
|
$builder->where('user_id', (int) $actor->id);
|
|
|
|
if ($manageableGroupIds !== []) {
|
|
$builder->orWhereIn('group_id', $manageableGroupIds);
|
|
}
|
|
});
|
|
|
|
$referenceTimestamp = $this->comparisonTimestamp($sourceArtwork);
|
|
if ($referenceTimestamp !== null) {
|
|
$query->where('published_at', '<=', $referenceTimestamp);
|
|
}
|
|
|
|
if ($term !== '') {
|
|
$like = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $term) . '%';
|
|
|
|
$query->where(function ($builder) use ($like): void {
|
|
$builder->where('title', 'like', $like)
|
|
->orWhere('slug', 'like', $like)
|
|
->orWhereHas('group', fn ($groupQuery) => $groupQuery->where('name', 'like', $like))
|
|
->orWhereHas('user', fn ($userQuery) => $userQuery
|
|
->where('name', 'like', $like)
|
|
->orWhere('username', 'like', $like));
|
|
});
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function mapPrimaryPanel(ArtworkRelation $relation, ?User $viewer): ?array
|
|
{
|
|
$beforeArtwork = $relation->targetArtwork;
|
|
$afterArtwork = $relation->sourceArtwork;
|
|
|
|
if (! $beforeArtwork || ! $afterArtwork) {
|
|
return null;
|
|
}
|
|
|
|
if (! $this->isPubliclyVisible($beforeArtwork) || ! $this->isPubliclyVisible($afterArtwork)) {
|
|
return null;
|
|
}
|
|
|
|
$before = $this->mapPublicCard($beforeArtwork, $viewer, 'Original');
|
|
$after = $this->mapPublicCard($afterArtwork, $viewer, $this->relationTypeShortLabel((string) $relation->relation_type));
|
|
|
|
if ($this->shouldOmitForViewer($before) || $this->shouldOmitForViewer($after)) {
|
|
return null;
|
|
}
|
|
|
|
$beforeYear = $before['year'] ?? null;
|
|
$afterYear = $after['year'] ?? null;
|
|
$yearsApart = $this->yearsApart($beforeYear, $afterYear);
|
|
|
|
return [
|
|
'id' => (int) $relation->id,
|
|
'relation_type' => (string) $relation->relation_type,
|
|
'relation_label' => $this->relationTypeLabel((string) $relation->relation_type),
|
|
'heading' => 'Then & Now',
|
|
'summary' => $this->primarySummary($beforeYear, $yearsApart),
|
|
'years_apart' => $yearsApart,
|
|
'years_apart_label' => $yearsApart !== null && $yearsApart > 0 ? $yearsApart . ' years later' : null,
|
|
'note' => $relation->note,
|
|
'before' => $before,
|
|
'after' => $after,
|
|
'compare' => [
|
|
'available' => $this->compareAvailable($before, $after),
|
|
'title' => 'Then & Now comparison',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function mapIncomingUpdate(ArtworkRelation $relation, ?User $viewer): ?array
|
|
{
|
|
$beforeArtwork = $relation->targetArtwork;
|
|
$afterArtwork = $relation->sourceArtwork;
|
|
|
|
if (! $beforeArtwork || ! $afterArtwork) {
|
|
return null;
|
|
}
|
|
|
|
if (! $this->isPubliclyVisible($beforeArtwork) || ! $this->isPubliclyVisible($afterArtwork)) {
|
|
return null;
|
|
}
|
|
|
|
$before = $this->mapPublicCard($beforeArtwork, $viewer, 'Original');
|
|
$after = $this->mapPublicCard($afterArtwork, $viewer, $this->relationTypeShortLabel((string) $relation->relation_type));
|
|
|
|
if ($this->shouldOmitForViewer($before) || $this->shouldOmitForViewer($after)) {
|
|
return null;
|
|
}
|
|
|
|
$yearsApart = $this->yearsApart($before['year'] ?? null, $after['year'] ?? null);
|
|
|
|
return [
|
|
'id' => (int) $relation->id,
|
|
'relation_type' => (string) $relation->relation_type,
|
|
'relation_label' => $this->relationTypeLabel((string) $relation->relation_type),
|
|
'heading' => 'Updated Version',
|
|
'summary' => $this->incomingSummary($after['year'] ?? null, $yearsApart),
|
|
'years_apart' => $yearsApart,
|
|
'years_apart_label' => $yearsApart !== null && $yearsApart > 0 ? $yearsApart . ' years later' : null,
|
|
'note' => $relation->note,
|
|
'before' => $before,
|
|
'after' => $after,
|
|
'compare' => [
|
|
'available' => $this->compareAvailable($before, $after),
|
|
'title' => 'Compare versions',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function mapPublicCard(Artwork $artwork, ?User $viewer, string $roleLabel): array
|
|
{
|
|
$category = $artwork->categories->sortBy('sort_order')->first();
|
|
$md = ThumbnailPresenter::present($artwork, 'md');
|
|
$lg = ThumbnailPresenter::present($artwork, 'lg');
|
|
$xl = ThumbnailPresenter::present($artwork, 'xl');
|
|
$publishedAt = $artwork->published_at;
|
|
|
|
return $this->maturity->decoratePayload([
|
|
'id' => (int) $artwork->id,
|
|
'title' => html_entity_decode((string) $artwork->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
|
'url' => route('art.show', [
|
|
'id' => (int) $artwork->id,
|
|
'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: (string) $artwork->id,
|
|
]),
|
|
'publisher' => $artwork->group?->name ?: $artwork->user?->name ?: $artwork->user?->username ?: 'Artist',
|
|
'published_at' => optional($publishedAt)->toIsoString(),
|
|
'year' => $publishedAt?->year ?: $artwork->created_at?->year,
|
|
'role_label' => $roleLabel,
|
|
'thumbnail' => $md['url'] ?? null,
|
|
'image_md' => $md['url'] ?? null,
|
|
'image_lg' => $lg['url'] ?? null,
|
|
'image_xl' => $xl['url'] ?? null,
|
|
'width' => (int) ($artwork->width ?? 0),
|
|
'height' => (int) ($artwork->height ?? 0),
|
|
'content_type' => $category?->contentType?->name,
|
|
'category' => $category?->name,
|
|
], $artwork, $viewer);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $card
|
|
*/
|
|
private function shouldOmitForViewer(array $card): bool
|
|
{
|
|
return (bool) data_get($card, 'maturity.should_hide', false);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $before
|
|
* @param array<string, mixed> $after
|
|
*/
|
|
private function compareAvailable(array $before, array $after): bool
|
|
{
|
|
return ! empty($before['image_lg']) && ! empty($after['image_lg']);
|
|
}
|
|
|
|
private function yearsApart(mixed $beforeYear, mixed $afterYear): ?int
|
|
{
|
|
if (! is_numeric($beforeYear) || ! is_numeric($afterYear)) {
|
|
return null;
|
|
}
|
|
|
|
return max(0, (int) $afterYear - (int) $beforeYear);
|
|
}
|
|
|
|
private function primarySummary(mixed $beforeYear, ?int $yearsApart): string
|
|
{
|
|
if (is_numeric($beforeYear) && $yearsApart !== null && $yearsApart > 0) {
|
|
return sprintf('This artwork revisits an earlier version from %d, %d years later.', (int) $beforeYear, $yearsApart);
|
|
}
|
|
|
|
if (is_numeric($beforeYear)) {
|
|
return sprintf('This artwork revisits an earlier version from %d.', (int) $beforeYear);
|
|
}
|
|
|
|
return 'This artwork revisits an earlier version from the creator archive.';
|
|
}
|
|
|
|
private function incomingSummary(mixed $afterYear, ?int $yearsApart): string
|
|
{
|
|
if (is_numeric($afterYear) && $yearsApart !== null && $yearsApart > 0) {
|
|
return sprintf('This artwork was later revisited in %d, %d years later.', (int) $afterYear, $yearsApart);
|
|
}
|
|
|
|
if (is_numeric($afterYear)) {
|
|
return sprintf('This artwork was later revisited in %d.', (int) $afterYear);
|
|
}
|
|
|
|
return 'This artwork later received an updated version from the same creator.';
|
|
}
|
|
|
|
private function relationTypeLabel(string $type): string
|
|
{
|
|
return match ($type) {
|
|
ArtworkRelation::TYPE_REMASTER_OF => 'Remaster of',
|
|
ArtworkRelation::TYPE_REVISION_OF => 'Revision of',
|
|
ArtworkRelation::TYPE_INSPIRED_BY => 'Inspired by',
|
|
ArtworkRelation::TYPE_VARIATION_OF => 'Variation of',
|
|
default => 'Remake of',
|
|
};
|
|
}
|
|
|
|
private function relationTypeShortLabel(string $type): string
|
|
{
|
|
return match ($type) {
|
|
ArtworkRelation::TYPE_REMASTER_OF => 'Remaster',
|
|
ArtworkRelation::TYPE_REVISION_OF => 'Update',
|
|
ArtworkRelation::TYPE_INSPIRED_BY => 'Inspired take',
|
|
ArtworkRelation::TYPE_VARIATION_OF => 'Variation',
|
|
default => 'Remake',
|
|
};
|
|
}
|
|
} |