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

236 lines
9.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\NovaCards;
use App\Models\NovaCard;
use App\Models\NovaCardCreatorPreset;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class NovaCardCreatorPresetService
{
/** Maximum presets per user across all types. */
public const MAX_TOTAL = 30;
/** Maximum presets per type per user. */
public const MAX_PER_TYPE = 8;
public function listForUser(User $user, ?string $presetType = null): Collection
{
return NovaCardCreatorPreset::query()
->where('user_id', $user->id)
->when($presetType !== null, fn ($q) => $q->where('preset_type', $presetType))
->orderByDesc('is_default')
->orderBy('name')
->get();
}
public function create(User $user, array $data): NovaCardCreatorPreset
{
$type = (string) Arr::get($data, 'preset_type', NovaCardCreatorPreset::TYPE_STYLE);
// Enforce per-type limit.
$typeCount = NovaCardCreatorPreset::query()
->where('user_id', $user->id)
->where('preset_type', $type)
->count();
if ($typeCount >= self::MAX_PER_TYPE) {
abort(422, 'Maximum number of ' . $type . ' presets reached (' . self::MAX_PER_TYPE . ').');
}
// Enforce total limit.
$totalCount = NovaCardCreatorPreset::query()->where('user_id', $user->id)->count();
if ($totalCount >= self::MAX_TOTAL) {
abort(422, 'Maximum total presets reached (' . self::MAX_TOTAL . ').');
}
$preset = NovaCardCreatorPreset::query()->create([
'user_id' => $user->id,
'name' => (string) Arr::get($data, 'name', 'My preset'),
'preset_type' => $type,
'config_json' => $this->sanitizeConfig((array) Arr::get($data, 'config_json', []), $type),
'is_default' => false,
]);
if ((bool) Arr::get($data, 'is_default', false)) {
$this->setDefault($user, $preset);
}
return $preset->refresh();
}
public function update(User $user, NovaCardCreatorPreset $preset, array $data): NovaCardCreatorPreset
{
$this->authorizeOwner($user, $preset);
$preset->fill([
'name' => (string) Arr::get($data, 'name', $preset->name),
'config_json' => $this->sanitizeConfig(
(array) Arr::get($data, 'config_json', $preset->config_json ?? []),
$preset->preset_type,
),
]);
$preset->save();
if (array_key_exists('is_default', $data) && (bool) $data['is_default']) {
$this->setDefault($user, $preset);
}
return $preset->refresh();
}
public function delete(User $user, NovaCardCreatorPreset $preset): void
{
$this->authorizeOwner($user, $preset);
$preset->delete();
}
public function setDefault(User $user, NovaCardCreatorPreset|int $preset): void
{
if (is_int($preset)) {
$preset = NovaCardCreatorPreset::query()->findOrFail($preset);
}
$this->authorizeOwner($user, $preset);
// Clear existing defaults of the same type before setting new one.
NovaCardCreatorPreset::query()
->where('user_id', $user->id)
->where('preset_type', $preset->preset_type)
->where('id', '!=', $preset->id)
->update(['is_default' => false]);
$preset->update(['is_default' => true]);
}
/**
* Capture a preset from a published or saved card project JSON.
* Extracts only the fields relevant to the given preset type.
*/
public function captureFromCard(User $user, NovaCard $card, string $presetName, string $presetType): NovaCardCreatorPreset
{
$project = is_array($card->project_json) ? $card->project_json : [];
$config = $this->extractFromProject($project, $presetType);
return $this->create($user, [
'name' => $presetName,
'preset_type' => $presetType,
'config_json' => $config,
]);
}
/**
* Apply a preset config on top of the given project patch array.
* Returns the merged patch to be passed into the draft autosave flow.
*/
public function applyToProjectPatch(NovaCardCreatorPreset $preset, array|NovaCard $currentProject): array
{
if ($currentProject instanceof NovaCard) {
$currentProject = is_array($currentProject->project_json) ? $currentProject->project_json : [];
}
$config = $preset->config_json ?? [];
return match ($preset->preset_type) {
NovaCardCreatorPreset::TYPE_TYPOGRAPHY => [
'typography' => array_merge(
(array) Arr::get($currentProject, 'typography', []),
(array) (Arr::get($config, 'typography', $config)),
),
],
NovaCardCreatorPreset::TYPE_LAYOUT => [
'layout' => array_merge(
(array) Arr::get($currentProject, 'layout', []),
(array) (Arr::get($config, 'layout', $config)),
),
],
NovaCardCreatorPreset::TYPE_BACKGROUND => [
'background' => array_merge(
(array) Arr::get($currentProject, 'background', []),
(array) (Arr::get($config, 'background', $config)),
),
],
NovaCardCreatorPreset::TYPE_STYLE => array_filter([
'typography' => isset($config['typography'])
? array_merge((array) Arr::get($currentProject, 'typography', []), (array) $config['typography'])
: null,
'background' => isset($config['background'])
? array_merge((array) Arr::get($currentProject, 'background', []), (array) $config['background'])
: null,
'effects' => isset($config['effects'])
? array_merge((array) Arr::get($currentProject, 'effects', []), (array) $config['effects'])
: null,
]),
NovaCardCreatorPreset::TYPE_STARTER => $config,
default => [],
};
}
private function extractFromProject(array $project, string $presetType): array
{
return match ($presetType) {
NovaCardCreatorPreset::TYPE_TYPOGRAPHY => array_intersect_key(
(array) Arr::get($project, 'typography', []),
array_flip(['font_preset', 'text_color', 'accent_color', 'quote_size', 'author_size', 'letter_spacing', 'line_height', 'shadow_preset', 'quote_mark_preset', 'text_panel_style', 'text_glow', 'text_stroke']),
),
NovaCardCreatorPreset::TYPE_LAYOUT => array_intersect_key(
(array) Arr::get($project, 'layout', []),
array_flip(['layout', 'position', 'alignment', 'padding', 'max_width']),
),
NovaCardCreatorPreset::TYPE_BACKGROUND => array_intersect_key(
(array) Arr::get($project, 'background', []),
array_flip(['type', 'gradient_preset', 'gradient_colors', 'solid_color', 'overlay_style', 'blur_level', 'opacity', 'brightness', 'contrast', 'texture_overlay', 'gradient_direction']),
),
NovaCardCreatorPreset::TYPE_STYLE => [
'typography' => array_intersect_key(
(array) Arr::get($project, 'typography', []),
array_flip(['font_preset', 'text_color', 'accent_color', 'shadow_preset', 'quote_mark_preset', 'text_panel_style']),
),
'background' => array_intersect_key(
(array) Arr::get($project, 'background', []),
array_flip(['gradient_preset', 'gradient_colors', 'overlay_style']),
),
'effects' => (array) Arr::get($project, 'effects', []),
],
NovaCardCreatorPreset::TYPE_STARTER => array_intersect_key($project, array_flip([
'template', 'layout', 'typography', 'background', 'decorations', 'effects', 'frame',
])),
default => [],
};
}
private function sanitizeConfig(array $config, string $type): array
{
// Strip deeply nested unknowns and limit total size.
$encoded = json_encode($config);
if ($encoded === false || mb_strlen($encoded) > 32_768) {
abort(422, 'Preset config is too large.');
}
return $config;
}
private function authorizeOwner(User $user, NovaCardCreatorPreset $preset): void
{
if ((int) $preset->user_id !== (int) $user->id) {
abort(403);
}
}
public function toArray(NovaCardCreatorPreset $preset): array
{
return [
'id' => (int) $preset->id,
'name' => (string) $preset->name,
'preset_type' => (string) $preset->preset_type,
'config_json' => $preset->config_json ?? [],
'is_default' => (bool) $preset->is_default,
'created_at' => optional($preset->created_at)?->toISOString(),
];
}
}