'jpg', 'image/png' => 'png', 'image/webp' => 'webp', ]; public function __construct( #[Autowire('%app.cover_originals_dir%')] private readonly string $originalsDir, #[Autowire('%app.cover_thumbnails_dir%')] private readonly string $thumbnailsDir, private readonly CoverThumbnailer $thumbnailer, private readonly Filesystem $filesystem, ) { } /** * @param string $contents the image, kept byte for byte as the original * * @return string the name of the stored cover * * @throws CoverProcessingException */ public function store(string $contents): string { // The extension comes from the detected content, never from the client's file name. $extension = self::EXTENSIONS[@getimagesizefromstring($contents)['mime'] ?? ''] ?? null; if ($extension === null) { throw new CoverProcessingException('Unsupported image type.'); } $name = bin2hex(random_bytes(16)) . '.' . $extension; $this->filesystem->mkdir([$this->originalsDir, $this->thumbnailsDir]); try { $this->thumbnailer->createThumbnails( $contents, fn(string $size): string => $this->thumbnailPath($name, $size), ); $this->filesystem->dumpFile($this->originalPath($name), $contents); } catch (\Throwable $exception) { $this->delete($name); throw $exception; } return $name; } public function delete(string $name): void { $this->filesystem->remove([ $this->originalPath($name), ...array_map( fn(string $size): string => $this->thumbnailPath($name, $size), array_keys(CoverThumbnailer::SIZES), ), ]); } public function originalPath(string $name): string { return $this->originalsDir . '/' . $name; } public function thumbnailPath(string $name, string $size): string { return $this->thumbnailsDir . '/' . $name . '.' . $size . '.jpg'; } }