teknoo/curl-request
A lightweight PHP wrapper around cURL for building and sending HTTP requests. It offers a simple object-oriented API for handling URLs, headers, methods, and responses, making it easy to integrate clean cURL-based calls into any PHP project.
Start by deciding whether you should start at all. This package is archived and marked deprecated, with no dependents and no maintainer. composer require teknoo/curl-request still resolves from Packagist, but nothing will ever be fixed in it. Adopt it only to service an existing dependency — for anything new, reach for Guzzle or symfony/http-client, both of which give you PSR-18, retries, and connection pooling for free.
If you are inheriting it, the fastest path to competence:
src/, not the README. The README is a handful of examples and constitutes the entire documentation. The class count is small (request, response, and the cURL transport/executor) and each file is short — fifteen minutes of reading beats guessing at accessor names.composer.json for the PHP floor and the ext-curl requirement. This code predates PHP 8; verify it runs on your fleet's version before you build on top of it.The shape of the API is a request object you configure, plus a transport you execute it through:
use Teknoo\Curl\Request;
use Teknoo\Curl\Transport\CurlTransport;
$transport = new CurlTransport();
$request = new Request('https://api.internal/orders/42');
$request->setMethod('GET');
$request->addHeader('Accept: application/json');
$request->addHeader('Authorization: Bearer ' . $token);
$response = $transport->send($request);
$status = $response->getStatusCode();
$body = $response->getContent();
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
Class and method names follow the
Teknoo\Curlnamespace, but the package has no API reference beyond the README and its examples — confirm every accessor (getContent,getStatusCode, header iteration) againstsrc/before you paste. The shape is reliable; the spellings are not.
Your first real use case is almost always the same: replacing a bare curl_init() block or a file_get_contents() call in a service class with something that has headers, a timeout, and a testable seam.
Never call the transport directly from business logic. The single highest-value thing you can do with an unmaintained dependency is put a one-method interface in front of it, so that the day you migrate to Guzzle the diff is confined to one class:
interface HttpClient
{
public function send(string $method, string $url, array $headers = [], ?string $body = null): HttpResponse;
}
Implement CurlHttpClient against teknoo/curl-request today; implement GuzzleHttpClient tomorrow. Everything downstream is untouched.
Build a request factory for your recurring headers. Auth tokens, User-Agent, Accept, and a correlation ID appear on every call. Centralize them in one place so you can rotate a token or add tracing without touching fifty call sites. This package does no serialization — you encode JSON yourself and set Content-Type: application/json manually.
Reuse the transport instance. In a worker or a long-running process, construct the transport once in the container and pass it around. Constructing a fresh one per request throws away the cURL handle and any keep-alive benefit, and on a hot loop that is measurable.
Handle two distinct failure modes. cURL-level failures (DNS, connect refused, TLS, timeout) surface as exceptions from the transport. HTTP 4xx/5xx do not — those come back as a normal response object with a status code. Your wrapper should normalize both:
try {
$response = $transport->send($request);
} catch (\Throwable $e) {
throw new UpstreamUnavailable($url, previous: $e);
}
if ($response->getStatusCode() >= 400) {
throw new UpstreamError($url, $response->getStatusCode(), $response->getContent());
}
Put retries in a decorator, not in the caller. Retry only idempotent verbs and only on connection errors and 502/503/504, with exponential backoff and a cap. Retrying a POST on a connect timeout is how you get duplicate orders.
Testing strategy: stub your HttpClient interface for unit tests — do not let a test hit the network. For integration coverage of the transport itself, spin up php -S locally or use a fixture server; that also exercises the TLS/timeout paths you actually care about.
Concurrency: there is none. This is a synchronous, one-request-at-a-time wrapper. If you need to fan out to ten endpoints, either reach for curl_multi yourself or — usually better — push each call onto a queue worker so ten processes each make one blocking call. Do not loop over a batch of API calls in a web request path.
Instrument it. Log method, URL (query string redacted for tokens), status, duration, and cURL error code. Because the library is silent, this logging is your only visibility when something goes wrong at 3am.
CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST are left at secure defaults. Older cURL wrappers in this lineage had a habit of disabling them "to make it work" on a dev box; grep the source before shipping to production.CURLOPT_FOLLOWLOCATION off, so a 301/302 hands you an empty body and a confusing json_decode failure. Set it explicitly, and cap CURLOPT_MAXREDIRS.curl or use a proper client for that case.\r\n.\Exception broadly will swallow your own bugs alongside cURL failures. Enumerate the exception types from src/.CURLOPT_HTTP_VERSION, proxy settings, custom CA bundle) or to add timing/metrics around send(). That subclass is also your escape hatch — you can reimplement its single method on top of PSR-18 and keep every caller working.require pins a PHP line you have since upgraded past, Composer will refuse to install. --ignore-platform-reqs is not a fix; verify the code actually runs on your runtime, or migrate.How can I help you explore Laravel packages today?