/-/ and returns its public URL + metadata. */ class FileUploadService { public function __construct( private readonly FileValidatorService $fileValidator, private readonly string $projectDir, ) {} /** * @return array{url: string, filename: string, filemime: string, size: int} * @throws \RuntimeException on an invalid/oversized file */ public function storeFromRequest(Request $request, string $subDir): array { $content = $request->getContent(); preg_match('/filename=["\']?([^"\';\s]+)["\']?/i', $request->headers->get('Content-Disposition', ''), $m); $filename = $m[1] ?? 'file'; $tmpPath = sys_get_temp_dir() . '/' . uniqid('upload_', true); file_put_contents($tmpPath, $content); try { $safeFilename = $this->fileValidator->sanitizeFilename($filename); $mime = $this->fileValidator->detectMimeType($tmpPath); $year = date('Y'); $month = date('m'); $dir = $this->projectDir . '/public/uploads/' . $subDir . '/' . $year . '-' . $month; if (!is_dir($dir)) { mkdir($dir, 0755, true); } $storedName = uniqid('', true) . '_' . $safeFilename; rename($tmpPath, $dir . '/' . $storedName); return [ 'url' => '/uploads/' . $subDir . '/' . $year . '-' . $month . '/' . $storedName, 'filename' => $safeFilename, 'filemime' => $mime, 'size' => strlen($content), ]; } catch (\Throwable $e) { if (file_exists($tmpPath)) { unlink($tmpPath); } throw new \RuntimeException($e->getMessage(), 0, $e); } } }