# PHP Curl Class: install and use the PHP cURL wrapper for API requests

> PHP Curl Class is a Composer package that wraps PHP's cURL extension in a Curl object with request methods, error state and a parallel MultiCurl queue. It fits PHP 8 applications that call HTTP APIs, and it is not a replacement for the cURL extension itself.

**php-curl-class/php-curl-class** — PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs

- Repository: https://github.com/php-curl-class/php-curl-class
- Website: https://www.phpcurlclass.com/
- Stars: 3,298 · Forks: 802
- Language: PHP
- License: Unlicense
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/php-curl-class-php-curl-class

## What PHP Curl Class solves, and who it is for

Raw ext-curl in PHP is a handle, a list of CURLOPT constants and a return value. You call curl_init, assemble an options array, call curl_exec, then check curl_errno and curl_error on the handle. For one request that is fine. For an application that talks to several APIs, the bookkeeping spreads across every call site, and the same five lines get rewritten with slightly different mistakes each time.

PHP Curl Class puts that bookkeeping into a Curl object. You construct it, call get, post, put, patch or delete, and read $curl->response, $curl->error, $curl->errorMessage and $curl->getHttpStatusCode(). The README describes the package as making it easy to send HTTP requests and integrate with web APIs. The audience is PHP developers writing integrations: a payment callback handler, a scraper, a CLI job that pulls a feed, a webhook sender. It is not aimed at developers who want a PSR-18 client, and it does not hide cURL. The README shows you can reach the underlying handle through $curl->curl and call curl_set_opt on it directly, which is the honest signal about what this package is: a convenience layer, not an abstraction that pretends the extension is not there.

The topics list on the repository includes api-client, http-client, web-scraper and restful, which matches the intended use. Nothing in the README positions it as a full HTTP stack, and the method list is dominated by cURL option plumbing rather than protocol features.

## How the wrapper, the response object and MultiCurl fit together

The mechanism is a thin object layer over ext-curl. The constructor takes an optional base URL and an options array: Curl::__construct($base_url = null, $options = []). A base URL means later get and post calls can pass a relative path, and the examples directory includes get_base_url_1.php and get_base_url_2.php showing that pattern. Options are applied to the underlying handle before the request runs.

Request data is passed as an array. For GET the array becomes query parameters; for POST it becomes the request body. The README example $curl->get('https://www.example.com/search', ['q' => 'keyword']) resolves to https://www.example.com/search?q=keyword. Headers, cookies, basic authentication, referrer and user agent each have a setter: setHeader, setCookie, setBasicAuthentication, setReferrer, setUserAgent. Redirects are off unless you call setFollowLocation, which matters because many API endpoints redirect to a canonical host and a silent 301 becomes an empty response body.

After a call, the object exposes both sides of the exchange. $curl->requestHeaders and $curl->responseHeaders are readable, and header lookup is case-insensitive: the README shows both $curl->responseHeaders['Content-Type'] and $curl->responseHeaders['CoNTeNT-TyPE'] returning image/png. That is more forgiving than the raw $_SERVER-style arrays PHP developers are used to, and it removes a class of bug where a header is present but spelled differently by the server. Response decoding depends on the content type, and getJsonDecoder() is exposed for inspection. Errors land on the object rather than in a return value, which is the single biggest design decision here: you must check $curl->error, and the README's own quick start does exactly that and calls $curl->diagnose() on failure.

MultiCurl is the second entry point. You create a MultiCurl instance, register success, error and complete callbacks, queue requests with addGet (and the equivalent add methods), then call start(), which the README says blocks until all queued items have been processed. Each callback receives the per-request instance, so $instance->url, $instance->response and $instance->errorCode identify which request fired. This is cURL's multi interface with a callback layer, not a promise API and not a coroutine runtime. The distinction matters when you are deciding whether it can replace an existing async stack.

## Install PHP Curl Class and send your first request

Installation is one Composer command. The README gives it directly and points to the Composer project for instructions on getting the composer binary itself.

```bash
composer require php-curl-class/php-curl-class
```

The README also documents an unreleased install from the default branch. That tracks master and can pull in breaking changes before a tagged release, so it belongs in throwaway experiments rather than a deployed service.

```bash
composer require php-curl-class/php-curl-class @dev
```

Requirements are stated plainly: PHP 8.5, 8.4, 8.3, 8.2, 8.1 and 8.0. There is no PHP 7 support in the current release line. Because the package wraps ext-curl, that extension has to be enabled in your runtime; a container built without it will fail at the first request rather than at install time, which is a worse place to find out.

The README's quick start is the shortest real example. It requires the Composer autoloader, constructs a Curl, issues a GET, and branches on the error flag.

```php
require __DIR__ . '/vendor/autoload.php';

use Curl\Curl;

$curl = new Curl();
$curl->get('https://www.example.com/');

if ($curl->error) {
    echo 'Error: ' . $curl->errorMessage . "\n";
    $curl->diagnose();
} else {
    echo 'Response:' . "\n";
    var_dump($curl->response);
}
```

On success you get the decoded response body in $curl->response. On failure, $curl->errorMessage carries the message and diagnose() prints request detail, which is the quickest way to see what actually went over the wire when an endpoint rejects you. The diagnose_request.php example in the examples directory is the standalone version of that call.

A POST with a body, authentication and headers follows the same shape. The README shows form fields passed as an array, with the credentials set on the object beforehand.

```php
$curl = new Curl();
$curl->setBasicAuthentication('username', 'password');
$curl->setHeader('X-Requested-With', 'XMLHttpRequest');
$curl->post('https://www.example.com/login/', [
    'username' => 'myusername',
    'password' => 'mypassword',
]);

var_dump($curl->requestHeaders);
var_dump($curl->responseHeaders);
```

For file uploads the README shows two forms: a path string prefixed with @, as in 'image' => '@path/to/file.jpg', and a CURLFile instance, as in 'image' => new CURLFile('path/to/file.jpg'). Prefer the CURLFile form; the @ syntax is the older convention and the README presents both without ranking them.

Downloads use a dedicated method. The README pairs it with an encoding option set to an empty string so that supported encodings are enabled first, and the object exposes getDownloadFileName() plus a download-complete callback.

```php
$curl = new Curl();
$curl->setOpt(CURLOPT_ENCODING , '');
$curl->download('https://www.example.com/file.bin', '/tmp/myfile.bin');
```

The method list also includes fastDownload($url, $filename, $connections = 4), which takes a connection count for range-based parallel fetching. The README does not state which servers support that, so test it against your target before relying on it for a large file.

Finally, cleanup. The README documents $curl->close() for manual release of the handle, which matters in long-running workers where many Curl objects are created and discarded.

```php
$curl->close();
```

## Where the object model gets in the way

The error model is the first real cost. Nothing throws by default. If you forget to check $curl->error after a call, a failed request looks exactly like a successful one with an empty response, and the failure surfaces later as a null pointer or a malformed payload somewhere else in the stack. Every call site needs the branch, or you need to wrap the object yourself, and at that point you have written the abstraction the package declined to provide.

There is no PSR-18 or PSR-7 compliance in the README or the method list. If your application is built on interfaces from php-fig, this package does not slot in, and swapping it for another client later means touching every call site rather than one injected dependency. That is a structural cost, not a stylistic preference.

Concurrency has a ceiling. MultiCurl is the cURL multi interface, so it is parallel I/O on one process, and start() blocks until the queue drains. There is no non-blocking mode, no event loop integration, and no way to interleave other work while requests are in flight. A service that needs thousands of concurrent outbound calls, or that already runs on ReactPHP or Swoole, will find this the wrong layer. The same applies to HTTP/2 multiplexing and connection reuse across requests: the README does not document a shared connection pool, and each Curl object is its own handle, so a loop that creates a new Curl per request pays the connection setup every time.

The README also does not document retry policy. attemptRetry() and getAttempts() appear in the method list and there is a before_send_retry.php example, but the README gives no backoff rules, no jitter and no statement about which failures are retried. Treat retry behaviour as something to read in the source and the examples before you depend on it in production, rather than assuming it matches whatever your team does elsewhere.

One more boundary: this is not a browser. It does not execute JavaScript, so pages that render client-side will return an empty shell. The topics list includes web-scraper and web-scraping, and the package is a reasonable HTTP layer for scraping static markup or JSON endpoints, but anything behind a JavaScript framework needs a headless browser instead. Using this package there wastes time on a problem it was never built to solve.

## How it differs from Guzzle and from calling ext-curl directly

Guzzle is the obvious alternative and the difference is architectural, not cosmetic. Guzzle implements PSR-18 and PSR-7: requests and responses are immutable message objects, the client is an interface you inject, and middleware lets you stack logging, retry and auth behaviour around the transport. It also supports handlers other than cURL, so the transport can be swapped. PHP Curl Class takes the opposite route. There is one mutable Curl object per request, configuration is set on that object with setters, and the result is read back from its properties. Nothing is injected and nothing is immutable.

That makes PHP Curl Class shorter for a script and weaker as an application seam. If you want to type-hint a client interface and swap implementations in tests, Guzzle fits. If you want to port an existing block of curl_init calls with minimal restructuring, this package is closer to a drop-in, because the mental model of a handle with options is preserved.

The other alternative is ext-curl with no wrapper at all. You keep full control and add no dependency, at the cost of writing option arrays and error checks by hand at every call site, and hand-rolling the parallel queue if you need one. The wrapper's value is concentrated in the parts you would otherwise write repeatedly: the error state, the header accessors, the download helpers and MultiCurl. If you only ever make one request per script, that value is close to zero and the dependency is hard to justify.

There is also the framework HTTP client, in Laravel or Symfony, which wraps Guzzle and adds testing fakes and framework-level configuration. If you are already inside one of those, adopting a second HTTP layer means two error conventions and two sets of retry behaviour in the same codebase, and reviewers have to know which one a given file uses.

## Maintenance, versions and what the Unlicense means for you

The repository is not archived, and the last push was on 2026-09-23, so the project is being worked on. Releases are tagged and semantic: 13.0.0 landed on 2026-04-20, following 12.0.4 on 2025-12-11 and 12.0.3 on 2025-11-24. A major version bump is the signal to read CHANGELOG.md before upgrading, since a 13.x release may change behaviour that 12.x code depends on. The README does not document a deprecation policy or a support window for older majors, so pinning a version in composer.json and upgrading deliberately is the safer path than tracking master.

Upgrade cost is tied to PHP versions, not to the package's internals. Supported runtimes are 8.0 through 8.5. Dropping 8.0 or 8.1 in a future major would force a runtime upgrade, which is usually the larger project. The repository also carries a Dependabot workflow, so dependency updates arrive as pull requests rather than silently, which is a review cost as well as a safety measure.

The licence is the Unlicense, which the repository ships as a LICENSE file at the root. It is a public-domain dedication, not a permissive licence with attribution conditions. For most teams that removes the compliance questions that MIT or Apache-2.0 raise, but it also means there is no warranty grant in the usual permissive-licence form. Whether your organisation accepts a public-domain dedication in its dependency policy is a question for your legal reviewers, not something to settle from a README. If your build pipeline runs automated licence scanning, check how the scanner classifies the Unlicense before you add it to an approved list, because scanners do not always agree on that identifier.

## What to check before you commit to it

Three things decide whether this package fits. First, confirm ext-curl is present in every environment you deploy to, including the CI image and the production container, because the failure appears at runtime rather than at install. Second, confirm your PHP version is in the 8.0 to 8.5 range, since the current release line does not support older runtimes. Third, decide whether the mutable-object error model is acceptable in your codebase. If your team already relies on exceptions for transport failures, you will be writing a wrapper around this wrapper, and at that point the PSR-18 route is the shorter path.

If all three pass, the package does what its README claims with very little ceremony, and the MultiCurl queue covers the common case of fanning out a few dozen API calls from a single PHP process. The examples directory is the real documentation: before_send_retry.php, curl_progress.php, download_files_with_callback.php and the diagnose_request.php example each show a behaviour the README only names.

## Diagnosing a failed request and reading back your options

The README's troubleshooting path is short and worth knowing before you need it. When $curl->error is true, $curl->errorMessage holds the message, and $curl->diagnose() prints request detail. The method list also exposes getCurlErrorCode(), getCurlErrorMessage(), getHttpStatusCode() and getHttpErrorMessage(), which separate transport failures from HTTP status failures. That split matters: a 404 is a successful cURL transfer with an unhappy status, and code that only checks $curl->error will treat it the same as a connection timeout, which is exactly the wrong response.

For inspecting what was actually sent, getOptions() and getOpt($option) read back the cURL options on the handle, and displayCurlOptionValue($option, $value = null) renders them. The examples directory includes curl_display_curl_option_value.php, curl_display_curl_option_values.php and curl_display_curl_option_values_user_set.php, which suggests the intended use is checking which options you set versus which cURL defaults applied. That is a useful debugging step when a request behaves differently in production than in a local script, because the difference is often an option someone set on the object earlier and forgot about.

If a request hangs, disableTimeout() exists, but the name is the warning: it removes the timeout rather than setting one. A worker that calls it on a slow endpoint can block indefinitely, and because the call is synchronous there is no other code in that process making progress while it waits.

## Security notes the repository carries

The repository has a SECURITY.md at the root, which is the documented channel for reporting vulnerabilities rather than the issue tracker. The README has a Security section, but the extracted content does not include its body, so the specific guidance it gives cannot be summarised here. Read SECURITY.md directly if you are evaluating the package for a regulated environment.

Two behaviours are visible from the README and worth flagging on their own. Redirects are not followed unless you call setFollowLocation, which is the safer default for API work but will surprise anyone porting code that relied on cURL following redirects by default. And the download helpers write to a path you supply, so the destination filename is your responsibility, as is validating the URL you pass in when any part of it comes from user input. The package does not sanitise that for you, and the README does not claim it does.

## Conclusion

Adopt PHP Curl Class if you write PHP 8 applications that call HTTP APIs and you want request state, error messages and a parallel queue without hand-building cURL handles. Do not adopt it if you need a PSR-18 client, an async runtime, or HTTP/2 multiplexing, because the package is a wrapper around ext-curl and nothing more. Before committing, check that your PHP version is in the supported range, confirm ext-curl is enabled in your runtime, and read the LICENSE file, since the Unlicense is a public-domain dedication rather than a permissive licence with attribution terms.

## FAQ

### What is the purpose of cURL in PHP, and what does PHP Curl Class add?

The cURL extension is what actually performs the HTTP transfer in PHP. PHP Curl Class wraps that extension in a Curl object with request methods, error state and header accessors, so you call $curl->get() and read $curl->response instead of managing a cURL handle and its options yourself.

### Is cURL the same as an HTTP request?

No. cURL is the client library that sends the request and receives the response; the HTTP request is the message that goes over the network. PHP Curl Class sits on the cURL side of that, building and sending the request and exposing the response.

### How is cURL different from Postman?

Postman is an interactive tool for building and inspecting requests by hand. PHP Curl Class is library code that runs inside a PHP application, so the same request is issued by your program at runtime rather than by a person clicking Send.

### What is meant by class in PHP, and why does PHP Curl Class use one?

A class is a PHP language construct that bundles data and the methods that operate on it. PHP Curl Class uses one so that a request, its options, its response and its error state live on a single object you construct with new Curl().

## Sources

- [License: Unlicense](https://github.com/php-curl-class/php-curl-class/blob/master/LICENSE)
- [php-curl-class/php-curl-class on GitHub](https://github.com/php-curl-class/php-curl-class)
- [Project website](https://www.phpcurlclass.com/)
- [README](https://github.com/php-curl-class/php-curl-class/blob/master/README.md)
- [Releases](https://github.com/php-curl-class/php-curl-class/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/php-curl-class-php-curl-class
