extensões aceitas. */ private const ALLOWED_TYPES = [ 'application/pdf' => 'pdf', 'image/png' => 'png', 'image/jpeg' => 'jpg', 'image/gif' => 'gif', 'application/msword' => 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.ms-excel' => 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', 'text/plain' => 'txt', ]; /** Tamanho máximo por arquivo: 20 MB */ private const MAX_SIZE = 20 * 1024 * 1024; /** * Raiz do diretório de uploads (fora do public/). */ private function getBaseUploadDir(): string { return dirname(__DIR__, 2) . '/storage/uploads/orders'; } /** * Pega o caminho do diretório de upload de uma ordem específica. */ private function getOrderUploadDir(int $orderId): string { return $this->getBaseUploadDir() . '/' . $orderId; } /** * Garante que o diretório de upload da ordem exista. */ private function ensureDirectory(string $path): void { if (!is_dir($path)) { mkdir($path, 0775, true); } } /** * Processa e armazena os arquivos do campo de upload. * Retorna a quantidade de arquivos salvos com sucesso. * * @param int $orderId * @param array $filesInput Entry do $_FILES (já normalizado como array de arquivos) * @return int * @throws \Exception em caso de erro crítico */ public function storeFiles(int $orderId, array $filesInput): int { $uploadDir = $this->getOrderUploadDir($orderId); $this->ensureDirectory($uploadDir); $model = new OrderAttachment(); $saved = 0; // Normaliza o $_FILES para iterar arquivo a arquivo $files = $this->normalizeFilesArray($filesInput); foreach ($files as $file) { // Ignora entradas vazias (campo não preenchido) if ($file['error'] === UPLOAD_ERR_NO_FILE) { continue; } if ($file['error'] !== UPLOAD_ERR_OK) { throw new \Exception("Erro no upload do arquivo '{$file['name']}': código {$file['error']}"); } if ($file['size'] > self::MAX_SIZE) { throw new \Exception("O arquivo '{$file['name']}' excede o tamanho máximo permitido de 20 MB."); } // Detecta o tipo MIME real do arquivo (não confiar só no header HTTP) $finfo = new \finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->file($file['tmp_name']); if (!array_key_exists($mimeType, self::ALLOWED_TYPES)) { throw new \Exception("Tipo de arquivo não permitido: '{$file['name']}' ({$mimeType})."); } $extension = self::ALLOWED_TYPES[$mimeType]; $storedName = uniqid('attach_', true) . '.' . $extension; $destination = $uploadDir . '/' . $storedName; if (!move_uploaded_file($file['tmp_name'], $destination)) { throw new \Exception("Falha ao mover o arquivo '{$file['name']}' para o servidor."); } $model->create([ 'order_id' => $orderId, 'original_name' => $file['name'], 'stored_name' => $storedName, 'mime_type' => $mimeType, 'size' => $file['size'], ]); $saved++; } return $saved; } /** * Retorna o caminho absoluto do arquivo no disco. */ public function getFilePath(int $orderId, string $storedName): string { return $this->getOrderUploadDir($orderId) . '/' . $storedName; } /** * Normaliza o array $_FILES para uma lista linear de arquivos, * independente de ser upload único ou múltiplo (name[]). */ private function normalizeFilesArray(array $files): array { // Se é um único arquivo, encapsula em array if (!is_array($files['name'])) { return [$files]; } $normalized = []; $count = count($files['name']); for ($i = 0; $i < $count; $i++) { $normalized[] = [ 'name' => $files['name'][$i], 'type' => $files['type'][$i], 'tmp_name' => $files['tmp_name'][$i], 'error' => $files['error'][$i], 'size' => $files['size'][$i], ]; } return $normalized; } }