fix(posts): cache the link card as an array, not the DTO
The link-card cache stored a LinkCardMetadata object, which does not round-trip through the Redis cache driver — a cache hit came back as __PHP_Incomplete_Class and 500'd the preview endpoint. Cache the plain array (toArray) and rebuild the DTO via a new fromArray(). Primitives round-trip cleanly through every driver. The cache test now asserts a primitive is stored and a hit reconstructs the DTO; the array cache driver used in tests hid the bug because it never serializes.
This commit is contained in:
parent
925fd85b95
commit
8866e4ed46
3 changed files with 35 additions and 4 deletions
|
|
@ -32,11 +32,16 @@ public function fetch(string $text): ?LinkCardMetadata
|
|||
return null;
|
||||
}
|
||||
|
||||
return Cache::remember(
|
||||
// Cache a plain array, never the DTO: a rich object does not round-trip
|
||||
// cleanly through every cache driver (it comes back as an incomplete
|
||||
// class), whereas primitives always do.
|
||||
$data = Cache::remember(
|
||||
'link_card:'.sha1($url),
|
||||
now()->addMinutes(self::CACHE_MINUTES),
|
||||
fn (): ?LinkCardMetadata => $this->build($url),
|
||||
fn (): ?array => $this->build($url)?->toArray(),
|
||||
);
|
||||
|
||||
return $data === null ? null : LinkCardMetadata::fromArray($data);
|
||||
}
|
||||
|
||||
private function build(string $url): ?LinkCardMetadata
|
||||
|
|
|
|||
|
|
@ -16,6 +16,22 @@ public function __construct(
|
|||
public ?string $imageUrl,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Rebuild the DTO from its cached array form (the inverse of toArray, minus
|
||||
* the derived `domain`, which toArray recomputes).
|
||||
*
|
||||
* @param array{uri?: string, title?: string, description?: string, image?: ?string} $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
uri: (string) data_get($data, 'uri', ''),
|
||||
title: (string) data_get($data, 'title', ''),
|
||||
description: (string) data_get($data, 'description', ''),
|
||||
imageUrl: data_get($data, 'image'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{uri: string, domain: string, title: string, description: string, image: ?string}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use App\Services\Social\LinkCard\LinkCardFetcher;
|
||||
use App\Services\Social\LinkCard\LinkCardMetadata;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
// example.com resolves to a public IP so the SafeHttpFetcher SSRF guard passes;
|
||||
|
|
@ -50,8 +52,16 @@
|
|||
]);
|
||||
|
||||
$fetcher = app(LinkCardFetcher::class);
|
||||
$fetcher->fetch('https://example.com');
|
||||
$fetcher->fetch('https://example.com');
|
||||
$first = $fetcher->fetch('https://example.com');
|
||||
$second = $fetcher->fetch('https://example.com');
|
||||
|
||||
Http::assertSentCount(1);
|
||||
|
||||
// The cache must hold a primitive array, not the DTO — a rich object does
|
||||
// not round-trip through every cache driver (it comes back as an incomplete
|
||||
// class). A cache hit still reconstructs a proper DTO.
|
||||
expect(Cache::get('link_card:'.sha1('https://example.com')))->toBeArray();
|
||||
expect($second)->toBeInstanceOf(LinkCardMetadata::class)
|
||||
->and($second->uri)->toBe($first->uri)
|
||||
->and($second->title)->toBe('Cached');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue