Upload File dan Bikin API di PHP: Dua Fitur yang Sering Salah Dikerjakan
Dari sekian banyak fitur web yang pernah saya kerjakan, upload file dan pembuatan API adalah dua yang paling sering saya temukan implementasinya kurang tepat β baik dari sisi keamanan maupun konsistensi. Bukan berarti yang mengerjakannya tidak bisa coding, tapi lebih ke kebiasaan yang terbawa dari tutorial cepat yang tidak selalu memperhatikan best practice.
Artikel ini menggabungkan keduanya karena di banyak kasus, fitur upload memang jadi bagian dari endpoint API.
Upload File: Validasi Berlapis adalah Wajib
Masalah terbesar dengan upload file yang tidak aman adalah kemungkinan seseorang mengupload file PHP atau script berbahaya yang bisa dieksekusi di server. Untuk mencegah ini, validasi harus dilakukan dari beberapa sisi sekaligus.
<?php
class FileUploader {
private array $allowedMimes = [
"image/jpeg", "image/png", "image/gif", "image/webp"
];
private array $allowedExtensions = ["jpg", "jpeg", "png", "gif", "webp"];
private int $maxSize = 2097152; // 2MB dalam bytes
private string $uploadDir;
public function __construct(string $uploadDir) {
$this->uploadDir = rtrim($uploadDir, "/") . "/";
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function upload(array $file): array {
// Cek error dari PHP
if ($file["error"] !== UPLOAD_ERR_OK) {
return $this->error("Upload gagal (kode: {$file["error"]})");
}
// Validasi ukuran
if ($file["size"] > $this->maxSize) {
return $this->error("Ukuran file melebihi " . ($this->maxSize / 1048576) . "MB");
}
// Validasi MIME type dari isi file (bukan dari nama)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file["tmp_name"]);
finfo_close($finfo);
if (!in_array($mimeType, $this->allowedMimes)) {
return $this->error("Tipe file tidak diizinkan: $mimeType");
}
// Validasi ekstensi
$ext = strtolower(pathinfo($file["name"], PATHINFO_EXTENSION));
if (!in_array($ext, $this->allowedExtensions)) {
return $this->error("Ekstensi .$ext tidak diizinkan");
}
// Verifikasi ini benar-benar uploaded file
if (!is_uploaded_file($file["tmp_name"])) {
return $this->error("File tidak valid");
}
// Generate nama file unik
$namaFile = bin2hex(random_bytes(16)) . "." . $ext;
$pathTujuan = $this->uploadDir . $namaFile;
if (!move_uploaded_file($file["tmp_name"], $pathTujuan)) {
return $this->error("Gagal menyimpan file");
}
return ["sukses" => true, "nama_file" => $namaFile, "path" => $pathTujuan];
}
private function error(string $pesan): array {
return ["sukses" => false, "pesan" => $pesan];
}
}
// Pemakaian
$uploader = new FileUploader("/var/www/uploads/");
$hasil = $uploader->upload($_FILES["foto"]);
if ($hasil["sukses"]) {
echo "File tersimpan: " . $hasil["nama_file"];
} else {
echo "Error: " . $hasil["pesan"];
}
Satu catatan penting: direktori upload sebaiknya di luar document root, atau dikonfigurasi agar Nginx/Apache tidak mengeksekusi file di dalamnya sebagai script.
API Response yang Konsisten
Masalah umum di API PHP buatan sendiri adalah format response yang tidak konsisten β kadang return array data mentah, kadang ada wrapper, kadang tidak ada status code yang tepat. Ini menyulitkan siapapun yang mengkonsumsi API tersebut.
Solusinya: buat satu helper function yang mengurus semua response:
<?php
function apiResponse(int $httpCode, bool $sukses, string $pesan, $data = null, array $meta = []): void {
http_response_code($httpCode);
header("Content-Type: application/json; charset=utf-8");
// CORS header kalau diperlukan
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");
if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") {
exit(0);
}
$response = [
"sukses" => $sukses,
"pesan" => $pesan,
"data" => $data,
"waktu" => date("c"),
];
if (!empty($meta)) {
$response["meta"] = $meta;
}
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
// Contoh endpoint GET /api/posts
$method = $_SERVER["REQUEST_METHOD"];
$uri = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
if ($method === "GET" && $uri === "/api/posts") {
$halaman = max(1, (int)($_GET["hal"] ?? 1));
$perHalaman = 15;
$offset = ($halaman - 1) * $perHalaman;
$db = getDB();
$total = $db->query("SELECT COUNT(*) FROM posts WHERE status = 'published'")->fetchColumn();
$stmt = $db->prepare("SELECT id, title, slug, created_at FROM posts WHERE status = 'published' ORDER BY created_at DESC LIMIT ? OFFSET ?");
$stmt->execute([$perHalaman, $offset]);
$posts = $stmt->fetchAll();
apiResponse(200, true, "Data berhasil diambil", $posts, [
"total" => (int)$total,
"halaman" => $halaman,
"per_halaman" => $perHalaman,
"total_halaman" => (int)ceil($total / $perHalaman),
]);
}
// 404 kalau endpoint tidak ditemukan
apiResponse(404, false, "Endpoint tidak ditemukan");
HTTP Status Code yang Perlu Dipakai dengan Benar
Ini yang sering diabaikan β banyak API yang selalu return 200 meski terjadi error. Padahal HTTP status code itu bagian dari protokol dan klien menggunakannya untuk tahu bagaimana memproses response.
- 200 β OK, request berhasil diproses
- 201 β Created, resource baru berhasil dibuat
- 400 β Bad Request, request tidak valid atau parameter kurang
- 401 β Unauthorized, perlu autentikasi dulu
- 403 β Forbidden, sudah autentikasi tapi tidak punya izin
- 404 β Not Found, resource tidak ditemukan
- 422 β Unprocessable Entity, validasi gagal
- 429 β Too Many Requests, rate limit
- 500 β Internal Server Error, ada yang salah di server
Rate Limiting Sederhana
Tanpa rate limiting, API bisa dibanjiri request secara sengaja atau tidak sengaja. Implementasi sederhana menggunakan APCu:
<?php
function checkRateLimit(string $identifier, int $maxRequest = 60, int $perDetik = 60): bool {
if (!extension_loaded("apcu")) return true; // skip kalau APCu tidak ada
$key = "rate_limit:" . $identifier;
$count = apcu_fetch($key, $success);
if (!$success) {
apcu_store($key, 1, $perDetik);
return true;
}
if ($count >= $maxRequest) {
return false; // limit terlampaui
}
apcu_inc($key);
return true;
}
// Pakai IP sebagai identifier
$ip = $_SERVER["HTTP_X_FORWARDED_FOR"] ?? $_SERVER["REMOTE_ADDR"];
if (!checkRateLimit($ip)) {
apiResponse(429, false, "Terlalu banyak request. Coba lagi sebentar.");
}
Penutup
Upload file dan API adalah dua fitur yang hampir selalu ada di aplikasi web modern. Mengerjakan keduanya dengan benar dari awal jauh lebih mudah daripada harus menambal celah keamanan atau memperbaiki inkonsistensi di kemudian hari setelah aplikasi sudah berjalan di production. π
Belum ada komentar. Jadilah yang pertama menulis.
Tulis Komentar