refactor(api): extract inline-media hosting into a HostInlineMedia action

The download+host+422 orchestration was a private controller method doing IO and
throwing — that's an operation, not a controller concern. Move it to
App\Actions\Post\HostInlineMedia::execute() (alongside CreatePost/UpdatePost) so
the controller stays thin and the logic is reusable/testable.
This commit is contained in:
Paulo Castellano 2026-06-28 21:15:01 -03:00
parent 41cb9b02a3
commit a6bb39cc1a
2 changed files with 45 additions and 29 deletions

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Actions\Post;
use App\Enums\Media\Type as MediaType;
use App\Models\Workspace;
use App\Services\Post\MediaAttacher;
use Illuminate\Validation\ValidationException;
class HostInlineMedia
{
/**
* Resolve a post's inline media into hosted media — external URLs are
* downloaded and stored; items already on our disk pass through. Rejects
* (422) when any URL can't be fetched, so a post is never persisted with
* broken media.
*
* @param array<MediaType> $allowedTypes
* @param array<int, array<string, mixed>> $media
* @return array<int, array<string, mixed>>
*
* @throws ValidationException
*/
public static function execute(Workspace $workspace, array $allowedTypes, array $media): array
{
if ($media === []) {
return $media;
}
$result = app(MediaAttacher::class)->resolveInlineMedia($workspace, $allowedTypes, $media);
if ($result['failed'] !== []) {
throw ValidationException::withMessages([
'media' => ['Could not fetch media from URL: '.implode(', ', $result['failed'])],
]);
}
return $result['media'];
}
}

View file

@ -6,6 +6,7 @@
use App\Actions\Post\CreatePost;
use App\Actions\Post\DeletePost;
use App\Actions\Post\HostInlineMedia;
use App\Actions\Post\UpdatePost;
use App\Enums\Media\Type as MediaType;
use App\Enums\Post\Action as PostAction;
@ -18,7 +19,6 @@
use App\Http\Resources\Api\PostPreviewResource;
use App\Http\Resources\Api\PostResource;
use App\Models\Post;
use App\Models\Workspace;
use App\Services\Post\MediaAttacher;
use App\Support\PostStatusRules;
use Illuminate\Http\JsonResponse;
@ -54,7 +54,7 @@ public function store(StorePostRequest $request): JsonResponse
$data = $request->validated();
if (array_key_exists('media', $data)) {
$data['media'] = $this->hostInlineMedia(
$data['media'] = HostInlineMedia::execute(
$workspace,
Post::allowedMediaTypesFor($request->selectedPlatforms()),
$data['media'],
@ -77,7 +77,7 @@ public function update(UpdatePostRequest $request, Post $post): PostResource|Jso
$data = $request->validated();
if (array_key_exists('media', $data)) {
$data['media'] = $this->hostInlineMedia(
$data['media'] = HostInlineMedia::execute(
$request->user()->currentWorkspace,
$post->allowedMediaTypes(),
$data['media'],
@ -99,32 +99,6 @@ public function update(UpdatePostRequest $request, Post $post): PostResource|Jso
return new PostResource($updated);
}
/**
* Download and host external media URLs; reject (422) if one can't be fetched.
*
* @param array<MediaType> $allowedTypes
* @param array<int, array<string, mixed>> $media
* @return array<int, array<string, mixed>>
*
* @throws ValidationException
*/
private function hostInlineMedia(Workspace $workspace, array $allowedTypes, array $media): array
{
if ($media === []) {
return $media;
}
$result = app(MediaAttacher::class)->resolveInlineMedia($workspace, $allowedTypes, $media);
if ($result['failed'] !== []) {
throw ValidationException::withMessages([
'media' => ['Could not fetch media from URL: '.implode(', ', $result['failed'])],
]);
}
return $result['media'];
}
public function destroy(Request $request, Post $post): JsonResponse
{
$this->authorize('delete', $post);