Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
8 / 8 |
|
100.00% |
2 / 2 |
CRAP | |
100.00% |
1 / 1 |
| SignedImageUrl | |
100.00% |
8 / 8 |
|
100.00% |
2 / 2 |
3 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| for | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace App\Shared\Infrastructure\Storage; |
| 6 | |
| 7 | use DateInterval; |
| 8 | use Symfony\Component\HttpFoundation\UriSigner; |
| 9 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
| 10 | |
| 11 | /** |
| 12 | * Mints the short-lived URL a browser uses to fetch a stored image. The JWT lives |
| 13 | * in memory only, so an <img src> cannot send Authorization: the capability has to |
| 14 | * travel in the URL itself, signed with APP_SECRET and expiring in an hour. |
| 15 | * |
| 16 | * The URL is signed absolute (that is what UriSigner::checkRequest rebuilds from |
| 17 | * the incoming request) but returned as a path + query, so the caller prefixes it |
| 18 | * with whatever API base it uses — `/api` in prod, `http://localhost:8080` in dev. |
| 19 | */ |
| 20 | final readonly class SignedImageUrl |
| 21 | { |
| 22 | private const TTL = 'PT1H'; |
| 23 | |
| 24 | public function __construct( |
| 25 | private UriSigner $signer, |
| 26 | private UrlGeneratorInterface $urls, |
| 27 | ) { |
| 28 | } |
| 29 | |
| 30 | public function for(?string $name): ?string |
| 31 | { |
| 32 | if ($name === null) { |
| 33 | return null; |
| 34 | } |
| 35 | |
| 36 | $absolute = $this->signer->sign( |
| 37 | $this->urls->generate('files_show', ['name' => $name], UrlGeneratorInterface::ABSOLUTE_URL), |
| 38 | new DateInterval(self::TTL), |
| 39 | ); |
| 40 | |
| 41 | return (string) preg_replace('#^\w+://[^/]+#', '', $absolute); |
| 42 | } |
| 43 | } |