refactor: drop RenderedSlide DTO and content_language null fallbacks

- TemplateImageGenerator->render() now returns a typed array shape
  {path: string, source_meta: array} instead of a one-off RenderedSlide
  DTO. Single internal callsite, no need for a dedicated class.
- Drop the `?? 'en'` fallback on $workspace->content_language in 6
  callsites: the column has a NOT NULL default of 'en' at the DB level,
  so the null coalesce was dead code.
- WorkspaceFactory now seeds content_language, brand_tone, brand_font
  and image_style explicitly so make() (no DB persist) produces a
  complete model — DB defaults aren't applied until create().
This commit is contained in:
Paulo Castellano 2026-05-08 19:32:51 -03:00
parent 4f3d9ff015
commit d97822c7e8
10 changed files with 29 additions and 41 deletions

View file

@ -62,7 +62,7 @@ public function instructions(): string
'brand_description' => $this->workspace->brand_description ?? '',
'brand_tone' => $this->workspace->brand_tone ?? '',
'brand_voice_notes' => $this->workspace->brand_voice_notes ?? '',
'content_language' => $this->workspace->content_language ?? 'en',
'content_language' => $this->workspace->content_language,
'current_content' => $this->currentContent,
'format' => $this->format,
'slide_count' => $this->slideCount,

View file

@ -34,7 +34,7 @@ public function instructions(): string
'brand_name' => $this->workspace->name ?? '',
'brand_tone' => $this->workspace->brand_tone ?? '',
'brand_voice_notes' => $this->workspace->brand_voice_notes ?? '',
'content_language' => $this->workspace->content_language ?? 'en',
'content_language' => $this->workspace->content_language,
'format' => $this->format,
])->render();
}

View file

@ -27,7 +27,7 @@ public function instructions(): string
'brand_name' => $this->workspace->name ?? '',
'brand_tone' => $this->workspace->brand_tone ?? '',
'brand_voice_notes' => $this->workspace->brand_voice_notes ?? '',
'content_language' => $this->workspace->content_language ?? 'en',
'content_language' => $this->workspace->content_language,
])->render();
}

View file

@ -35,7 +35,7 @@ public function instructions(): string
'brand_description' => $this->workspace->brand_description ?? '',
'brand_tone' => $this->workspace->brand_tone ?? '',
'brand_voice_notes' => $this->workspace->brand_voice_notes ?? '',
'content_language' => $this->workspace->content_language ?? 'en',
'content_language' => $this->workspace->content_language,
'current_content' => $this->currentContent,
'format' => 'single',
'slide_count' => 1,

View file

@ -79,7 +79,7 @@ public function apply(ApplyPostTemplateRequest $request, string $slug, TemplateI
);
if ($rendered) {
$mediaItem = $this->createMediaItem($workspace, $rendered->path);
$mediaItem = $this->createMediaItem($workspace, $rendered['path']);
$media[] = $mediaItem;
}
}

View file

@ -21,7 +21,6 @@
use App\Services\Ai\AiImageClient;
use App\Services\Ai\RecordAiUsage;
use App\Services\Image\BrandColorMapper;
use App\Services\Image\RenderedSlide;
use App\Services\Image\TemplateImageGenerator;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -313,15 +312,13 @@ private function notifyReady(Workspace $workspace, Post $post): void
$user = User::findOrFail($this->userId);
$locale = $workspace->content_language ?? 'en';
SendNotification::dispatch(
user: $user,
workspaceId: $workspace->id,
type: NotificationType::PostReady,
channel: NotificationChannel::InApp,
title: trans('notifications.post_ready.title', [], $locale),
body: trans('notifications.post_ready.body', [], $locale),
title: trans('notifications.post_ready.title', [], $workspace->content_language),
body: trans('notifications.post_ready.body', [], $workspace->content_language),
data: ['post_id' => $post->id],
);
}
@ -340,17 +337,18 @@ private function aspectRatioFor(ContentType $type): ?string
}
/**
* @param array{path: string, source_meta: array<string, mixed>} $rendered
* @return array<string, mixed>
*/
private function buildAiMediaItem(Workspace $workspace, RenderedSlide $rendered): array
private function buildAiMediaItem(Workspace $workspace, array $rendered): array
{
$media = $workspace->media()->create([
'collection' => 'ai-generated',
'type' => MediaType::Image,
'path' => $rendered->path,
'original_filename' => basename($rendered->path),
'path' => $rendered['path'],
'original_filename' => basename($rendered['path']),
'mime_type' => 'image/webp',
'size' => Storage::size($rendered->path),
'size' => Storage::size($rendered['path']),
'order' => 0,
]);
@ -361,7 +359,7 @@ private function buildAiMediaItem(Workspace $workspace, RenderedSlide $rendered)
'type' => 'image',
'mime_type' => 'image/webp',
'source' => Source::Ai->value,
'source_meta' => $rendered->sourceMeta,
'source_meta' => $rendered['source_meta'],
];
}
}

View file

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services\Image;
class RenderedSlide
{
/**
* @param array<string, mixed> $sourceMeta
*/
public function __construct(
public readonly string $path,
public readonly array $sourceMeta,
) {}
}

View file

@ -35,10 +35,12 @@ public function __construct(
) {}
/**
* Render a slide using Template A (full bleed) or Template B (photo card).
* Render a slide and return the storage path plus the source meta needed
* to regenerate it later. Returns null on failure (e.g. the AI client
* could not produce a usable image).
*
* @param array<int, string> $imageKeywords
* @return RenderedSlide|null The storage path + source meta, or null on failure.
* @return array{path: string, source_meta: array<string, mixed>}|null
*/
public function render(
Workspace $workspace,
@ -48,7 +50,7 @@ public function render(
array $imageKeywords,
int $width = self::DEFAULT_WIDTH,
int $height = self::DEFAULT_HEIGHT,
): ?RenderedSlide {
): ?array {
$this->width = $width;
$this->height = $height;
@ -59,7 +61,7 @@ public function render(
is_string($rawStyle) => ImageStyle::tryFrom($rawStyle) ?? ImageStyle::DEFAULT,
default => ImageStyle::DEFAULT,
};
$language = $workspace->content_language ?? 'en';
$language = $workspace->content_language;
$imageData = $this->aiImage->generate(
keywords: $imageKeywords,
@ -101,9 +103,9 @@ public function render(
],
);
return new RenderedSlide(
path: $filename,
sourceMeta: [
return [
'path' => $filename,
'source_meta' => [
'keywords' => array_values($imageKeywords),
'style' => $imageStyle->value,
'language' => $language,
@ -113,7 +115,7 @@ public function render(
'width' => $this->width,
'height' => $this->height,
],
);
];
}
/**

View file

@ -24,6 +24,10 @@ public function definition(): array
return [
'user_id' => User::factory(),
'name' => fake()->company(),
'content_language' => 'en',
'brand_tone' => 'professional',
'brand_font' => 'Inter',
'image_style' => 'cinematic',
];
}

View file

@ -73,8 +73,8 @@
);
if ($result !== null) {
expect($result->path)->toStartWith('ai-images/')->toEndWith('.webp');
expect($result->sourceMeta)
expect($result['path'])->toStartWith('ai-images/')->toEndWith('.webp');
expect($result['source_meta'])
->toHaveKey('keywords')
->toHaveKey('style', 'illustration')
->toHaveKey('model', 'gpt-image-2')