Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

Isfirequest — Minimal HTTP Client berbasis cURL (PHP) + Multipart Upload

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 \CURLFile
  • postMultipart() untuk pemakaian eksplisit
  • Timeout, headers, base URL join aman, SSL verify on
  • Akses respons mudah: ok(), status(), headers(), body(), object(), json(), error(), errno(), info()

📦 Sumber Kode (drop-in)

Simpan kode berikut sebagai Isfirequest.php bila 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);
    }
}

✨ Apa yang Baru (Multipart)

  • Deteksi otomatis multipart saat payload mengandung \CURLFile (di level mana pun).
    → Library akan menghapus header Content-Type agar cURL yang menetapkan multipart/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)

🚀 Pemakaian Cepat

Instalasi Manual

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',
  ],
]);

GET dengan Query

$res = $http->get('/users', ['page' => 2, 'limit' => 20]);
if ($res->ok()) {
    var_dump($res->object());
}

POST JSON

$http->setHeaders(['Content-Type' => 'application/json'], false);

$res = $http->post('/users', [
  'name'  => 'Ahmad',
  'email' => 'ahmad@example.com'
]);

POST Form-Encoded (default)

$res = $http->post('/login', [
  'username' => 'demo',
  'password' => 'secret'
]);

📤 Upload File (Multipart)

Saat value payload berisi \CURLFile, library otomatis mengirim multipart/form-data.
Jangan set Content-Type sendiri; biarkan cURL yang menambahkan header + boundary.

1) Upload Satu File

$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
]);

2) Beberapa File (array)

$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
]);

3) Eksplisit: postMultipart()

$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.


🧰 API Permukaan

Konfigurasi & Headers

$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 Methods

$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

Response Helpers

$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)

🔐 Autentikasi

$http->addHeaders(['Authorization' => 'Bearer '.$token]);
$res = $http->get('/me');

🛡️ Keamanan & Praktik Baik

  • 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-Type saat multipart (biarkan cURL menambahkan boundary).
  • Validasi input sebelum kirim ke upstream.

🧪 Retry Ringan (opsional)

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;
}

🧩 Integrasi Singkat di Laravel (opsional)

// 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());
}

🧰 Troubleshooting

  • $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-Type bentrok saat upload → jangan set Content-Type manual; library sudah menghapusnya pada multipart.
  • object() null → body bukan JSON. Cek $res->body().

🗺️ Roadmap

  • Interceptor before/after request
  • Opsi proxy
  • Streaming download ke file
  • Circuit breaker ringan

📄 Lisensi

Bebas dipakai untuk proyek Anda. Kredit balik sangat diapresiasi. 🙌

— Selamat ngoding!

About

Isfirequest — Minimal HTTP Client berbasis cURL (PHP) + Multipart Upload

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages