Files
SkinbaseNova/app/Services/NovaCards/NovaCardDraftService.php
2026-03-28 19:15:39 +01:00

284 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\NovaCards;
use App\Events\NovaCards\NovaCardAutosaved;
use App\Events\NovaCards\NovaCardCreated;
use App\Events\NovaCards\NovaCardTemplateSelected;
use App\Models\NovaCard;
use App\Models\NovaCardCategory;
use App\Models\NovaCardTemplate;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class NovaCardDraftService
{
public function __construct(
private readonly NovaCardTagService $tagService,
private readonly NovaCardProjectNormalizer $normalizer,
private readonly NovaCardVersionService $versions,
) {
}
public function createDraft(User $user, array $attributes = []): NovaCard
{
$template = $this->resolveTemplate(Arr::get($attributes, 'template_id'));
$category = $this->resolveCategory(Arr::get($attributes, 'category_id'));
$title = trim((string) Arr::get($attributes, 'title', 'Untitled card'));
$quote = trim((string) Arr::get($attributes, 'quote_text', 'Your next quote starts here.'));
$project = $this->normalizer->upgradeToV2(null, $template, $attributes);
$topLevel = $this->normalizer->syncTopLevelAttributes($project);
$originalCardId = Arr::get($attributes, 'original_card_id');
$rootCardId = Arr::get($attributes, 'root_card_id', $originalCardId);
$card = NovaCard::query()->create([
'user_id' => $user->id,
'category_id' => $category?->id,
'template_id' => $template?->id,
'title' => Str::limit($topLevel['title'] ?: $title, (int) config('nova_cards.validation.title_max', 120), ''),
'slug' => $this->generateUniqueSlug($title),
'quote_text' => Str::limit($topLevel['quote_text'] ?: $quote, (int) config('nova_cards.validation.quote_max', 420), ''),
'quote_author' => $topLevel['quote_author'],
'quote_source' => $topLevel['quote_source'],
'description' => Arr::get($attributes, 'description'),
'format' => $this->resolveFormat((string) Arr::get($attributes, 'format', NovaCard::FORMAT_SQUARE)),
'project_json' => $project,
'schema_version' => (int) $topLevel['schema_version'],
'visibility' => NovaCard::VISIBILITY_PRIVATE,
'status' => NovaCard::STATUS_DRAFT,
'moderation_status' => NovaCard::MOD_PENDING,
'background_type' => $topLevel['background_type'],
'background_image_id' => $topLevel['background_image_id'],
'allow_download' => true,
'allow_remix' => (bool) Arr::get($attributes, 'allow_remix', true),
'allow_background_reuse' => (bool) Arr::get($attributes, 'allow_background_reuse', false),
'allow_export' => (bool) Arr::get($attributes, 'allow_export', true),
'style_family' => Arr::get($attributes, 'style_family'),
'palette_family' => Arr::get($attributes, 'palette_family'),
'original_card_id' => $originalCardId,
'root_card_id' => $rootCardId,
]);
$this->tagService->syncTags($card, Arr::wrap(Arr::get($attributes, 'tags', [])));
$this->versions->snapshot($card->fresh(['template']), $user, $originalCardId ? 'Remix draft created' : 'Initial draft', true);
event(new NovaCardCreated($card->fresh()->load(['category', 'template', 'tags'])));
if ($card->template_id !== null) {
event(new NovaCardTemplateSelected($card->fresh()->load(['category', 'template', 'tags']), null, (int) $card->template_id));
}
return $card->load(['category', 'template', 'tags']);
}
public function autosave(NovaCard $card, array $payload): NovaCard
{
$currentProject = $this->normalizer->upgradeToV2(is_array($card->project_json) ? $card->project_json : [], $card->template, [], $card);
$template = $this->resolveTemplateId($payload, $card)
? NovaCardTemplate::query()->find($this->resolveTemplateId($payload, $card))
: $card->template;
$projectPatch = is_array(Arr::get($payload, 'project_json')) ? Arr::get($payload, 'project_json') : [];
$normalizedProject = $this->normalizer->upgradeToV2(
array_replace_recursive($currentProject, $projectPatch),
$template,
array_merge($payload, [
'title' => Arr::get($payload, 'title', $card->title),
'quote_text' => Arr::get($payload, 'quote_text', $card->quote_text),
'quote_author' => Arr::get($payload, 'quote_author', $card->quote_author),
'quote_source' => Arr::get($payload, 'quote_source', $card->quote_source),
'background_type' => Arr::get($payload, 'background_type', $card->background_type),
'background_image_id' => Arr::get($payload, 'background_image_id', $card->background_image_id),
'original_card_id' => $card->original_card_id,
'root_card_id' => $card->root_card_id,
]),
$card,
);
$topLevel = $this->normalizer->syncTopLevelAttributes($normalizedProject);
if (array_key_exists('title', $payload)) {
$topLevel['title'] = trim((string) $payload['title']);
}
if (array_key_exists('quote_text', $payload)) {
$topLevel['quote_text'] = trim((string) $payload['quote_text']);
}
if (array_key_exists('quote_author', $payload)) {
$topLevel['quote_author'] = (($value = trim((string) $payload['quote_author'])) !== '') ? $value : null;
}
if (array_key_exists('quote_source', $payload)) {
$topLevel['quote_source'] = (($value = trim((string) $payload['quote_source'])) !== '') ? $value : null;
}
$previousTemplateId = $card->template_id ? (int) $card->template_id : null;
$card->fill([
'title' => Str::limit($topLevel['title'], (int) config('nova_cards.validation.title_max', 120), ''),
'quote_text' => Str::limit($topLevel['quote_text'], (int) config('nova_cards.validation.quote_max', 420), ''),
'quote_author' => $topLevel['quote_author'],
'quote_source' => $topLevel['quote_source'],
'description' => Arr::get($payload, 'description', $card->description),
'format' => $this->resolveFormat((string) Arr::get($payload, 'format', $card->format)),
'project_json' => $normalizedProject,
'schema_version' => (int) $topLevel['schema_version'],
'background_type' => $topLevel['background_type'],
'background_image_id' => $topLevel['background_image_id'],
'template_id' => $this->resolveTemplateId($payload, $card),
'category_id' => $this->resolveCategoryId($payload, $card),
'visibility' => Arr::get($payload, 'visibility', $card->visibility),
'allow_download' => (bool) Arr::get($payload, 'allow_download', $card->allow_download),
'allow_remix' => (bool) Arr::get($payload, 'allow_remix', $card->allow_remix),
'allow_background_reuse' => (bool) Arr::get($payload, 'allow_background_reuse', $card->allow_background_reuse ?? false),
'allow_export' => (bool) Arr::get($payload, 'allow_export', $card->allow_export ?? true),
'style_family' => Arr::get($payload, 'style_family', $card->style_family),
'palette_family' => Arr::get($payload, 'palette_family', $card->palette_family),
'editor_mode_last_used' => Arr::get($payload, 'editor_mode_last_used', $card->editor_mode_last_used),
]);
if ($card->isDirty('title')) {
$card->slug = $this->generateUniqueSlug($card->title, $card->id);
}
$card->save();
$changes = $card->getChanges();
if (array_key_exists('tags', $payload)) {
$this->tagService->syncTags($card, Arr::wrap(Arr::get($payload, 'tags', [])));
$changes['tags'] = true;
}
if ($changes !== []) {
$fresh = $card->fresh()->load(['category', 'template', 'tags']);
$this->versions->snapshot($fresh->loadMissing('template'), $fresh->user, Arr::get($payload, 'version_label'));
event(new NovaCardAutosaved($fresh, array_keys($changes)));
$currentTemplateId = $fresh->template_id ? (int) $fresh->template_id : null;
if ($currentTemplateId !== $previousTemplateId && $currentTemplateId !== null) {
event(new NovaCardTemplateSelected($fresh, $previousTemplateId, $currentTemplateId));
}
}
return $card->refresh()->load(['category', 'template', 'tags']);
}
public function createRemix(User $user, NovaCard $source, array $attributes = []): NovaCard
{
$baseProject = $this->normalizer->normalizeForCard($source->loadMissing('template'));
$payload = array_merge([
'title' => 'Remix of ' . $source->title,
'quote_text' => $source->quote_text,
'quote_author' => $source->quote_author,
'quote_source' => $source->quote_source,
'description' => $source->description,
'format' => $source->format,
'background_type' => $source->background_type,
'background_image_id' => $source->background_image_id,
'template_id' => $source->template_id,
'category_id' => $source->category_id,
'tags' => $source->tags->pluck('name')->all(),
'project_json' => $baseProject,
'original_card_id' => $source->id,
'root_card_id' => $source->root_card_id ?: $source->id,
], $attributes);
return $this->createDraft($user, $payload);
}
public function createDuplicate(User $user, NovaCard $source, array $attributes = []): NovaCard
{
abort_unless($source->isOwnedBy($user), 403);
$baseProject = $this->normalizer->normalizeForCard($source->loadMissing('template'));
$payload = array_merge([
'title' => 'Copy of ' . $source->title,
'quote_text' => $source->quote_text,
'quote_author' => $source->quote_author,
'quote_source' => $source->quote_source,
'description' => $source->description,
'format' => $source->format,
'background_type' => $source->background_type,
'background_image_id' => $source->background_image_id,
'template_id' => $source->template_id,
'category_id' => $source->category_id,
'tags' => $source->tags->pluck('name')->all(),
'project_json' => $baseProject,
'allow_remix' => $source->allow_remix,
], $attributes);
return $this->createDraft($user, $payload);
}
private function buildProjectPayload(?NovaCardTemplate $template, array $attributes): array
{
return $this->normalizer->normalize(null, $template, $attributes);
}
private function generateUniqueSlug(string $title, ?int $ignoreId = null): string
{
$base = Str::slug($title);
if ($base === '') {
$base = 'nova-card';
}
$slug = $base;
$suffix = 2;
while (NovaCard::query()
->when($ignoreId !== null, fn ($query) => $query->where('id', '!=', $ignoreId))
->where('slug', $slug)
->exists()) {
$slug = $base . '-' . $suffix;
$suffix++;
}
return $slug;
}
private function resolveTemplate(mixed $templateId): ?NovaCardTemplate
{
if ($templateId) {
return NovaCardTemplate::query()->find($templateId);
}
return NovaCardTemplate::query()->where('active', true)->orderBy('order_num')->first();
}
private function resolveCategory(mixed $categoryId): ?NovaCardCategory
{
if ($categoryId) {
return NovaCardCategory::query()->find($categoryId);
}
return NovaCardCategory::query()->where('active', true)->orderBy('order_num')->first();
}
private function resolveFormat(string $format): string
{
$formats = array_keys((array) config('nova_cards.formats', []));
return in_array($format, $formats, true) ? $format : NovaCard::FORMAT_SQUARE;
}
private function resolveTemplateId(array $payload, NovaCard $card): ?int
{
if (! array_key_exists('template_id', $payload)) {
return $card->template_id;
}
return $this->resolveTemplate(Arr::get($payload, 'template_id'))?->id;
}
private function resolveCategoryId(array $payload, NovaCard $card): ?int
{
if (! array_key_exists('category_id', $payload)) {
return $card->category_id;
}
return $this->resolveCategory(Arr::get($payload, 'category_id'))?->id;
}
}