From 6ae48f014f2361aabf16d08efceee55664536264 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 4 May 2026 14:14:14 -0300 Subject: [PATCH] fix: persist morph map alias on AI-generated Media rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both PostAiCreateController::createMediaItem and PostTemplateController::createMediaItem were doing: $media = new Media([...]); $media->mediable_type = Workspace::class; // ← FQCN literal $media->mediable_id = $workspace->id; $media->save(); Assigning Workspace::class directly bypasses the morphMap configured in AppServiceProvider, so rows ended up with mediable_type = 'App\\Models\\Workspace' instead of the alias 'workspace'. Other queries that pivot through the morphMap (e.g. $workspace->media) lose those records on hosts where the FQCN doesn't match the alias. Switch both to the relationship form: $media = $workspace->media()->create([...]); Laravel fills mediable_type via the morphMap, producing 'workspace'. This is the same pattern AssetController and the HasMedia trait already use; these two AI controllers were the only outliers (`grep -rn 'mediable_type =' app/` confirms). Added tests/Unit/MediaPolymorphTest as a regression — asserts the created row's mediable_type is the alias and that the relationship resolves back to the workspace. --- .../App/PostAiCreateController.php | 9 ++---- .../App/PostTemplateController.php | 9 ++---- tests/Unit/MediaPolymorphTest.php | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 tests/Unit/MediaPolymorphTest.php diff --git a/app/Http/Controllers/App/PostAiCreateController.php b/app/Http/Controllers/App/PostAiCreateController.php index 84dcbd20..cac14ec8 100644 --- a/app/Http/Controllers/App/PostAiCreateController.php +++ b/app/Http/Controllers/App/PostAiCreateController.php @@ -9,7 +9,6 @@ use App\Enums\PostPlatform\ContentType; use App\Http\Requests\App\Ai\StartPostCreationRequest; use App\Jobs\Ai\StreamPostCreation; -use App\Models\Media; use App\Models\SocialAccount; use App\Models\Workspace; use Illuminate\Http\JsonResponse; @@ -198,7 +197,9 @@ private function buildMediaArray(Workspace $workspace, array $state): array */ private function createMediaItem(Workspace $workspace, string $path, array $meta = []): array { - $media = new Media([ + // Use the relationship (not `mediable_type = Workspace::class`) so + // the morph map alias `'workspace'` is persisted instead of the FQCN. + $media = $workspace->media()->create([ 'collection' => 'ai-generated', 'type' => MediaType::Image, 'path' => $path, @@ -208,10 +209,6 @@ private function createMediaItem(Workspace $workspace, string $path, array $meta 'order' => 0, ]); - $media->mediable_type = Workspace::class; - $media->mediable_id = $workspace->id; - $media->save(); - return [ 'id' => $media->id, 'path' => $media->path, diff --git a/app/Http/Controllers/App/PostTemplateController.php b/app/Http/Controllers/App/PostTemplateController.php index 86a80fca..a6de5061 100644 --- a/app/Http/Controllers/App/PostTemplateController.php +++ b/app/Http/Controllers/App/PostTemplateController.php @@ -7,7 +7,6 @@ use App\Actions\Post\CreatePost; use App\Enums\Media\Type as MediaType; use App\Http\Resources\App\PostTemplateResource; -use App\Models\Media; use App\Models\SocialAccount; use App\Models\Workspace; use App\Services\Image\TemplateImageGenerator; @@ -122,7 +121,9 @@ private function interpolate(string $text, Workspace $workspace): string */ private function createMediaItem(Workspace $workspace, string $path): array { - $media = new Media([ + // Use the relationship (not `mediable_type = Workspace::class`) so + // the morph map alias `'workspace'` is persisted instead of the FQCN. + $media = $workspace->media()->create([ 'collection' => 'ai-generated', 'type' => MediaType::Image, 'path' => $path, @@ -132,10 +133,6 @@ private function createMediaItem(Workspace $workspace, string $path): array 'order' => 0, ]); - $media->mediable_type = Workspace::class; - $media->mediable_id = $workspace->id; - $media->save(); - return [ 'id' => $media->id, 'path' => $media->path, diff --git a/tests/Unit/MediaPolymorphTest.php b/tests/Unit/MediaPolymorphTest.php new file mode 100644 index 00000000..f34ee3f3 --- /dev/null +++ b/tests/Unit/MediaPolymorphTest.php @@ -0,0 +1,32 @@ +mediable_type = Workspace::class;` + * which bypasses the morph map. Using the relationship + * (`$workspace->media()->create([...])` or + * `$media->mediable()->associate($workspace)`) is the correct path. + */ +test('Workspace::media()->create persists the morphMap alias, not the FQCN', function () { + $workspace = Workspace::factory()->create(); + + $media = $workspace->media()->create([ + 'collection' => 'ai-generated', + 'type' => 'image', + 'path' => 'test/dummy.webp', + 'original_filename' => 'dummy.webp', + 'mime_type' => 'image/webp', + 'size' => 100, + 'order' => 0, + ]); + + expect($media->mediable_type)->toBe('workspace'); + expect($media->mediable_type)->not->toBe(Workspace::class); + expect($media->mediable->is($workspace))->toBeTrue(); +});