Client HTTP ringan dengan chaining API, tanpa dependensi eksternal. Kini mendukung upload file (multipart/form-data) via \CURLFile, lengkap dengan helper dan contoh.
Ringkas:
- GET/POST/PUT/PATCH/DELETE
- JSON & x-www-form-urlencoded
- Multipart (file upload) otomatis saat payload berisi
\CURLFile- Helper
Isfirequest::file()untuk membuat\CURLFilepostMultipart()untuk pemakaian eksplisit- Timeout, headers, base URL join aman, SSL verify on
- Akses respons mudah:
ok(),status(),headers(),body(),object(),json(),error(),errno(),info()
Simpan kode berikut sebagai
Isfirequest.phpbila ingin dipisah, atau pakai langsung dari blok ini.
<?php
class Isfirequest
{
/** @var string */
protected $_base_url = "";
/** @var int seconds */
protected $_timeout = 30;
/** @var string[] final header lines "Key: Value" */
protected $_headers = [];
/** @var string */
protected $_method = "GET";
/** @var array structured response */
protected $response = [
'status' => null,
'headers' => [],
'body' => null,
'error' => null,
'errno' => 0,
'info' => []
];
/** chain alias, tetap disediakan agar kompatibel */
public $isfireq;
/* =========================
* Konfigurasi & Utilities
* ========================= */
public function config(array $options = [])
{
$this->buildRequest($options);
$this->isfireq = $this;
return $this;
}
protected function buildRequest(array $opt = [])
{
if (!empty($opt['base_url'])) {
$this->setBaseUrl($opt['base_url']);
}
if (!empty($opt['method'])) {
$this->setMethod($opt['method']);
}
if (!empty($opt['timeout'])) {
$this->setTimeout((int)$opt['timeout']);
}
if (!empty($opt['headers']) && is_array($opt['headers'])) {
$this->setHeaders($opt['headers'], true);
}
return $this;
}
public function setBaseUrl($baseUrl)
{
$this->_base_url = rtrim((string)$baseUrl, "/");
return $this;
}
public function setMethod($method = "GET")
{
$this->_method = strtoupper((string)$method);
return $this;
}
public function setTimeout($seconds)
{
$this->_timeout = max(0, (int)$seconds);
return $this;
}
/** ganti semua headers (replace=true) atau menambah (replace=false) */
public function setHeaders(array $headers, $replace = true)
{
$lines = [];
foreach ($headers as $k => $v) {
if (is_int($k)) {
$line = trim((string)$v);
if ($line !== '') $lines[] = $line;
} else {
$lines[] = trim($k) . ': ' . trim((string)$v);
}
}
$this->_headers = $replace ? $lines : array_values(array_unique(array_merge($this->_headers, $lines)));
return $this;
}
/** tambah headers (alias kompatibel) */
public function addHeaders(array $headers) { return $this->setHeaders($headers, false); }
public function setResponse(array $data = [])
{
$this->response = array_merge([
'status' => null,
'headers' => [],
'body' => null,
'error' => null,
'errno' => 0,
'info' => []
], $data);
return $this;
}
/** Helper membuat \CURLFile (untuk multipart). */
public static function file(string $path, ?string $mime = null, ?string $filename = null): \CURLFile
{
if ($mime === null && function_exists('mime_content_type')) {
$detected = @mime_content_type($path);
if (is_string($detected) && $detected !== '') {
$mime = $detected;
}
}
if ($filename === null) {
$filename = basename($path);
}
return new \CURLFile($path, $mime ?: null, $filename);
}
/* =========================
* HTTP Methods
* ========================= */
public function get($url, array $query = [])
{
$this->setMethod('GET');
$finalUrl = $this->buildUrl($url, $query);
return $this->send($finalUrl, null);
}
public function post($url, $data = [])
{
$this->setMethod('POST');
$finalUrl = $this->buildUrl($url, []);
$payload = $this->preparePayload($data);
return $this->send($finalUrl, $payload);
}
/** PUT/PATCH/DELETE umum */
public function sendRaw($method, $url, $data = [])
{
$this->setMethod($method);
$finalUrl = $this->buildUrl($url, []);
$payload = $this->preparePayload($data);
return $this->send($finalUrl, $payload);
}
/** Eksplisit multipart upload. */
public function postMultipart($url, array $fields)
{
$this->setMethod('POST');
$finalUrl = $this->buildUrl($url, []);
// pastikan payload tetap array (jangan di-encode)
return $this->send($finalUrl, $fields);
}
/* =========================
* Response Helpers
* ========================= */
public function body() { return $this->response['body']; }
public function status() { return $this->response['status']; }
public function headers(){ return $this->response['headers']; }
public function info() { return $this->response['info']; }
public function ok() { return $this->status() !== null && $this->status() >= 200 && $this->status() < 300; }
public function error() { return $this->response['error']; }
public function errno() { return $this->response['errno']; }
public function object()
{
$body = $this->response['body'];
if ($this->isJsonString($body)) {
return json_decode($body);
}
return $body;
}
public function json()
{
$body = $this->response['body'];
$out = $this->isJsonString($body) ? $body : json_encode($body);
header("Content-Type: application/json");
echo $out;
}
/* =========================
* Internal: Request Runner
* ========================= */
protected function send($url, $payload = null)
{
$curl = curl_init();
// deteksi multipart: payload array yang berisi \CURLFile (di level mana pun)
$isMultipart = $this->hasCurlFile($payload);
// normalisasi header; jika multipart, hapus Content-Type (cURL akan set boundary)
$headers = $this->normalizeHeaders($this->_headers);
$headerLines = $headers['lines'];
if ($isMultipart) {
$headerLines = $this->filterOutHeader($headerLines, 'content-type');
}
$opts = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => $this->_timeout,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $this->_method,
CURLOPT_HTTPHEADER => $headerLines,
CURLOPT_HEADER => true, // ambil header respons
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
];
// body hanya untuk metode yang ber-body
$methodHasBody = in_array($this->_method, ['POST','PUT','PATCH','DELETE'], true);
if ($methodHasBody && $payload !== null) {
$opts[CURLOPT_POSTFIELDS] = $payload;
}
curl_setopt_array($curl, $opts);
$raw = curl_exec($curl);
$errno = curl_errno($curl);
$err = $errno ? curl_error($curl) : null;
$info = curl_getinfo($curl);
// parsing header + body
$respHeaders = [];
$respBody = null;
if ($raw !== false && isset($info['header_size'])) {
$headerSize = $info['header_size'];
$rawHeaders = substr($raw, 0, $headerSize);
$respBody = substr($raw, $headerSize);
$respHeaders = $this->parseResponseHeaders($rawHeaders);
}
$status = isset($info['http_code']) ? (int)$info['http_code'] : null;
curl_close($curl);
$this->response = [
'status' => $status,
'headers' => $respHeaders,
'body' => $respBody,
'error' => $err,
'errno' => $errno,
'info' => $info
];
return $this;
}
protected function buildUrl($url, array $query = [])
{
$base = $this->_base_url !== "" ? $this->_base_url : "";
if ($base !== "") {
$url = ltrim($url, "/");
$base = rtrim($base, "/");
$url = $base . "/" . $url;
}
if (!empty($query)) {
$qs = http_build_query($query);
$url .= (strpos($url, '?') === false ? '?' : '&') . $qs;
}
return $url;
}
protected function preparePayload($data)
{
// jika ada \CURLFile → wajib multipart → jangan encode
if ($this->hasCurlFile($data)) {
return $data; // biarkan array sebagaimana adanya
}
// jika header JSON aktif → json_encode
$hasJsonHeader = $this->headerExists('Content-Type', 'application/json', $this->_headers);
if ($hasJsonHeader) {
if (is_string($data) && $this->isJsonString($data)) {
return $data; // sudah JSON
}
return json_encode($data, JSON_UNESCAPED_UNICODE);
}
// default: x-www-form-urlencoded
if (is_array($data)) {
return http_build_query($data);
}
return $data; // string mentah
}
protected function hasCurlFile($data): bool
{
if ($data instanceof \CURLFile) return true;
if (!is_array($data)) return false;
$iter = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($data));
foreach ($iter as $v) {
if ($v instanceof \CURLFile) return true;
}
return false;
}
protected function headerExists($key, $valueNeedle, array $headerLines)
{
$key = strtolower($key);
$valueNeedle = strtolower($valueNeedle);
foreach ($headerLines as $line) {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
if (strtolower(trim($parts[0])) === $key &&
strpos(strtolower(trim($parts[1])), $valueNeedle) !== false) {
return true;
}
}
}
return false;
}
protected function normalizeHeaders(array $lines)
{
// Hilangkan duplikat berdasarkan key (case-insensitive), keep-last
$map = [];
foreach ($lines as $line) {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$k = strtolower(trim($parts[0]));
$map[$k] = trim($parts[0]) . ': ' . trim($parts[1]);
}
}
return ['lines' => array_values($map)];
}
protected function filterOutHeader(array $lines, string $keyLower): array
{
$out = [];
foreach ($lines as $line) {
$parts = explode(':', $line, 2);
if (count($parts) === 2 && strtolower(trim($parts[0])) === $keyLower) {
continue; // drop
}
$out[] = $line;
}
return $out;
}
protected function parseResponseHeaders($rawHeaders)
{
$headers = [];
$lines = preg_split("/
|
|
/", trim($rawHeaders));
foreach ($lines as $line) {
if (stripos($line, 'HTTP/') === 0 || trim($line) === '') {
continue;
}
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$k = trim($parts[0]);
$v = trim($parts[1]);
if (!isset($headers[$k])) {
$headers[$k] = $v;
} else {
if (is_array($headers[$k])) {
$headers[$k][] = $v;
} else {
$headers[$k] = [$headers[$k], $v];
}
}
}
}
return $headers;
}
protected function isJsonString($data)
{
if (!is_string($data) || $data === '') return false;
json_decode($data, true);
return (json_last_error() === JSON_ERROR_NONE);
}
}- Deteksi otomatis multipart saat payload mengandung
\CURLFile(di level mana pun).
→ Library akan menghapus headerContent-Typeagar cURL yang menetapkanmultipart/form-data; boundary=...dengan benar. - Helper:
Isfirequest::file($path, $mime = null, $filename = null) - Metode eksplisit:
postMultipart($url, array $fields)(opsional;post()biasa juga otomatis multipart bila ada\CURLFile)
require __DIR__ . '/Isfirequest.php';
$http = (new Isfirequest())->config([
'base_url' => 'https://api.example.com/v1',
'timeout' => 15,
'headers' => [
'User-Agent' => 'Isfirequest/1.1',
'Accept' => 'application/json',
],
]);$res = $http->get('/users', ['page' => 2, 'limit' => 20]);
if ($res->ok()) {
var_dump($res->object());
}$http->setHeaders(['Content-Type' => 'application/json'], false);
$res = $http->post('/users', [
'name' => 'Ahmad',
'email' => 'ahmad@example.com'
]);$res = $http->post('/login', [
'username' => 'demo',
'password' => 'secret'
]);Saat value payload berisi
\CURLFile, library otomatis mengirim multipart/form-data.
Jangan setContent-Typesendiri; biarkan cURL yang menambahkan header + boundary.
$file = Isfirequest::file(__DIR__.'/avatar.png', 'image/png', 'avatar.png');
$res = $http->post('/profile/avatar', [
'user_id' => 123,
'photo' => $file, // nama field sesuai yang diminta server
]);$files = [
Isfirequest::file(__DIR__.'/a.jpg', 'image/jpeg', 'a.jpg'),
Isfirequest::file(__DIR__.'/b.jpg', 'image/jpeg', 'b.jpg'),
];
$res = $http->post('/gallery/upload', [
'album_id' => 55,
'files[]' => $files, // gunakan nama field array yang diterima API
]);$res = $http->postMultipart('/docs/submit', [
'title' => 'Proposal',
'doc' => Isfirequest::file(__DIR__.'/proposal.pdf', 'application/pdf', 'proposal.pdf'),
]);Catatan: Anda boleh mencampur field teks + file di payload array yang sama.
$http->config([
'base_url' => 'https://api.example.com',
'timeout' => 30,
'headers' => ['Accept' => 'application/json'],
]);
$http->setBaseUrl('https://api.example.com');
$http->setTimeout(20);
$http->setMethod('GET'); // otomatis oleh get()/post()/sendRaw()
// replace semua header
$http->setHeaders([
'Accept' => 'application/json',
'User-Agent' => 'Isfirequest/1.1',
]);
// tambahkan tanpa replace
$http->setHeaders(['X-Trace-Id' => 'abcd-1234'], false);
$http->addHeaders(['Authorization' => 'Bearer <token>']);$http->get($path, $query = []);
$http->post($path, $data = []); // otomatis: JSON/x-www-form-urlencoded/multipart
$http->sendRaw('PUT'|'PATCH'|'DELETE', $path, $data = []);
$http->postMultipart($path, array $fields); // eksplisit multipart$res->ok(); // bool (2xx)
$res->status(); // int|null
$res->headers(); // array header respons
$res->body(); // string body mentah
$res->object(); // auto decode JSON → stdClass|mixed
$res->json(); // echo JSON + header Content-Type
$res->info(); // info cURL (timing, url final, dll)
$res->error(); // pesan error cURL (jika ada)
$res->errno(); // kode error cURL (jika ada)$http->addHeaders(['Authorization' => 'Bearer '.$token]);
$res = $http->get('/me');- Timeout: set sesuai SLA API (mis. 10–30 detik).
- SSL: verifikasi aktif; jangan dimatikan di produksi.
- Token: simpan di
.env/ secret manager. - Jangan set
Content-Typesaat multipart (biarkan cURL menambahkan boundary). - Validasi input sebelum kirim ke upstream.
function callWithRetry(Isfirequest $http, $path, $payload = [], $maxRetry = 2) {
$attempt = 0;
do {
$attempt++;
$res = $http->post($path, $payload);
if ($res->ok()) return $res;
if ($res->errno() !== 0 && $attempt <= $maxRetry) { // error jaringan
usleep(200_000);
continue;
}
if ($res->status() >= 500 && $attempt <= $maxRetry) { // 5xx
usleep(200_000);
continue;
}
break;
} while ($attempt <= $maxRetry);
return $res;
}// AppServiceProvider
use App\Http\Isfirequest;
$this->app->singleton(Isfirequest::class, function () {
return (new Isfirequest())->config([
'base_url' => config('services.myapi.base_url'),
'timeout' => 20,
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'MyApp/1.1',
],
]);
});// Controller
public function upload(Isfirequest $http)
{
$res = $http->post('/upload', [
'title' => 'File ku',
'file' => Isfirequest::file(storage_path('app/public/a.pdf'), 'application/pdf', 'a.pdf'),
]);
abort_if(!$res->ok(), 502, "Upstream: {$res->status()}");
return response()->json($res->object());
}$res->errno() !== 0→ error jaringan/SSL/DNS. Cek$res->error()&$res->info().- 400/422 saat upload → pastikan nama field dan format array (mis.
files[]) sesuai kontrak API. Content-Typebentrok saat upload → jangan setContent-Typemanual; library sudah menghapusnya pada multipart.object()null → body bukan JSON. Cek$res->body().
- Interceptor before/after request
- Opsi proxy
- Streaming download ke file
- Circuit breaker ringan
Bebas dipakai untuk proyek Anda. Kredit balik sangat diapresiasi. 🙌
— Selamat ngoding!