ptcong/php-http-class
A lightweight PHP HTTP client class for sending GET and POST requests, managing cookies, custom headers, redirects, and file uploads through a simple API. Drop-in and dependency-free, it fits into any PHP project without extra setup.
Install it like any other Composer dependency:
composer require ptcong/php-http-class
Two things to verify before you write a line of code:
ptcong/php-http-client but the Packagist name is ptcong/php-http-class. Check vendor/ptcong/php-http-class/composer.json for the psr-4 map — that is the source of truth for your use statements, not the README. Old, low-traffic packages like this frequently have a namespace that differs from what the README shows.ext-curl is enabled in your container/CI image; there is no stream/Guzzle fallback.Where to look first: src/ for the four or five classes that matter (client, request, response, cookie, and whatever "browser"/session object wraps them). Read those before the README — for a package this size the class signatures are more reliable documentation than the readme, and the method names are self-describing.
The first use case worth wiring up is the one this library is actually good at: a stateful, browser-like request sequence — hit a login form, keep the cookies, then fetch a page behind auth. That's the scenario where its built-in cookie jar and redirect handling save you more code than Guzzle would.
// Pseudocode shape — verify exact setters against src/
$client = new HttpClient();
$request = new HttpRequest('https://example.com/login');
$request->setMethod('POST')
->setHeaders(['User-Agent' => 'Mozilla/5.0 ...', 'Referer' => 'https://example.com/'])
->setData(['user' => $u, 'pass' => $p]);
$response = $client->send($request);
echo $response->getStatusCode(); // 200
echo $response->getBody(); // raw HTML/JSON
Wrap it, don't sprinkle it. The classes are not PSR-18 and share no interface with Laravel's HTTP client, so every direct new HttpClient() is a future refactor. Put it behind one class:
namespace App\Services;
use App\Contracts\Fetcher;
class LegacyHttpFetcher implements Fetcher
{
public function get(string $url, array $headers = []): array
{
$request = new HttpRequest($url);
$request->setHeaders($headers);
$response = (new HttpClient())->send($request);
return [
'status' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'body' => $response->getBody(),
];
}
}
Bind Fetcher to the implementation in a service provider. Now tests can swap in a fake, and if you later migrate to Guzzle/Laravel Http, only one class changes.
Reuse one client object per unit of work. A fresh client per request defeats the cookie jar and can re-do DNS/TLS handshakes. Hold a single instance for the whole sequence:
class ScrapeLoginPage
{
public function __construct(private HttpClient $client) {}
public function handle(): string
{
$this->client->send($this->loginRequest());
$this->client->send($this->csrfWarmup()); // cookies now carried
return $this->client->send($this->targetRequest())->getBody();
}
}
If the class hierarchy supports a shared cookie jar/session object separate from the client, pass that explicitly instead — it survives client re-instantiation and is easier to inspect in a debug dump.
Laravel conventions that pay off:
scoped binding (not singleton) so an Octane/queue worker doesn't leak cookies between jobs.User-Agent into config/services.php, and apply them in one place. Hardcoded timeouts are the most common cause of a hung queue worker.RateLimited middleware or an explicit usleep — a raw cURL client will hammer a host with no backoff.getBody() straight into symfony/dom-crawler or Dom\HTMLDocument; don't scrape with regex.Response handling: treat non-2xx as data, not an exception you rely on. Normalize immediately (status → bool, JSON decode with JSON_THROW_ON_ERROR, trim BOM) inside the wrapper so callers never touch the library's response object.
It duplicates a dependency you already have. Laravel ships Guzzle by default and Illuminate\Support\Facades\Http wraps it with fakes, retries, and ->throw(). The honest everyday calculus: reach for this package only when (a) you're maintaining an existing codebase that already uses it, or (b) you genuinely want a zero-dependency, ~5-class client and don't need middleware. Don't adopt it greenfield just for cookie persistence — Guzzle's CookieJar does that too. Note also that 0 stars and 0 dependents means no ecosystem, no security advisories, and no one to ask when it breaks.
No PSR-7/PSR-18/PSR-17. Consequences: Http::fake() won't intercept it, Guzzle middleware (retry, logging, OAuth subs) won't apply, and you can't type-hint ClientInterface. Everything — retries, logging, auth refresh — has to be hand-rolled in your wrapper.
PHP version drift. This is pre-PHP-7.4-era code in all likelihood. On PHP 8.1+ you'll hit dynamic-property deprecations and possibly implicit-nullable-parameter deprecations. Check composer show ptcong/php-http-class for the php constraint; if it's >=5.x with no upper bound, treat a PHP 8.2/8.3 upgrade as a mini-project. Suppress noise rather than ignore it — deprecations become errors in 9.0.
Error signalling is ambiguous. Old cURL wrappers typically return false or a response object with status 0 on transport failure rather than throwing. Always check for both: a falsy return and a zero/negative status. Wrap in your own exception:
if (! $response || $response->getStatusCode() === 0) {
throw new HttpTransportException($request->getUrl()); // include cURL error if exposed
}
Timeouts and SSL. Defaults in libraries this age are frequently "no timeout" and SSL verification inherited from php.ini. In containers with missing CA bundles you'll get silent failures; set the CA path explicitly rather than flipping verification off. Always set a connect timeout and a total timeout — a connect timeout alone won't save you from a slow-drip response.
Cookie behaviour is the whole point and the easiest thing to get wrong. The jar is usually in-memory and tied to the client instance, so:
var_dump the cookie collection) when a session "randomly" drops.Redirects hide POST semantics. If the library auto-follows redirects, confirm whether the method is downgraded to GET (correct) or the body is resent (rare, but wrong). Login flows silently failing usually trace here.
Debugging. Set CURLOPT_VERBOSE/CURLINFO_HEADER_OUT — if the wrapper exposes the underlying handle or a debug flag, turn it on and log the outgoing request line plus headers. If it doesn't expose it, subclass the client and override the single method that performs curl_exec (that's the most useful extension point this package offers). Log getStatusCode(), getHeaders(), and the first 500 chars of the body together; correlating those three is what actually solves scrapers.
Extension points are thin. No events, no middleware, no interceptors. The realistic options are: subclass and override, decorate via your own wrapper class, or fork. Given the size of the library, forking a single class into app/Support/ is often more honest than depending on an unmaintained package — at least then you own the PHP 8 compatibility.
How can I help you explore Laravel packages today?