rfc:curl_oop_v2

PHP RFC: Object-oriented curl API v2

  • Version: 1.4
  • Date: 2025-06-24
  • Author: Eric Norris, [email protected]
  • Status: Under Discussion
  • Implementation: TBA

Introduction

As part of the Resource to object conversion project, PHP internals developers converted curl resource types to objects. These objects do not have any methods, and do not currently provide an object-oriented API for developers to use.

There have been at least two prior attempts at suggesting an object-oriented API for curl, but they never made it to a voting phase:

Creating an object-oriented API for curl still seems worth doing, however. We can leverage PHP features (e.g. namespace, enumerations, and asymmetric visibility) to provide a clean and safe interface for developers.

Proposal

New 'Curl' Namespace

We'll move all existing Curl* classes to the Curl namespace, e.g. CurlHandle will become Curl\Handle. We'll maintain non-namespaced aliases for backwards compatibility.

New enumerations

Multiple developers suggested using enumerations in response to the prior object-oriented curl RFC:

What about making the CURL options an enumeration?

- kalle

I was also going to suggest to use enums for the options, and have them be grouped by what value type they need.

- girgias

The suggestion to use an Enum (or several) here is a good one and would help a lot with that, so I'm +1 there.

- crell

Using enumerations for curl options will improve usability and discoverability, as today the curl_setopt and curl_multi_setopt only specify an int type for the option; this solely relies on documentation and convention to inform users that they should use CURLOPT_* and CURLMOPT_* constants.

This RFC proposes enumerations for any constants that are in fact enumerations, e.g. the aforementioned CURLOPT_* and CURLMOPT_* constants, as well as the CURLINFO_* and CURLPAUSE_* constants, etc. For brevity, I will not list all of the implementations.

Curl\HandleOption

namespace Curl;
 
enum HandleOption: int {
    case AbstractUnixSocket = CURLOPT_ABSTRACT_UNIX_SOCKET;
    case AcceptEncoding     = CURLOPT_ACCEPT_ENCODING;
    case AcceptTimeoutMs    = CURLOPT_ACCEPTTIMEOUT_MS;
    // ...other options elided...
}

Note: some options will not be a part of the enumeration list when applicable. For example, CURLOPT_RETURNTRANSFER, CURLOPT_FILE, and CURLOPT_WRITEFUNCTION are no longer valid options, as the Curl\Handle class has the more straightforward fetch(): string and execute(resource|callable $out): void methods.

Curl\MultiHandleOption

namespace Curl;
 
enum MultiHandleOption: int {
    case ChunkLengthPenaltySize   = CURLMOPT_CHUNK_LENGTH_PENALTY_SIZE;
    case ContentLengthPenaltySize = CURLMOPT_CONTENT_LENGTH_PENALTY_SIZE;
    case MaxConnects              = CURLMOPT_MAXCONNECTS;
    // ...other options elided...
}

Curl\Info

namespace Curl;
 
enum Info: int {
    case AppConnectTime = CURLINFO_APPCONNECT_TIME;
    // ...other values elided...
}

Curl\Pause

namespace Curl;
 
enum Pause: int {
    case All = CURLPAUSE_ALL;
    // ...other values elided...
}

New Curl exception hierarchy

Like the previous RFC, this RFC proposes that we add a new exception class to represent curl errors. Unlike the previous RFC, this will follow the newly ratified PHP RFC: Throwable Hierarchy Policy for Extensions:

namespace Curl;
 
// "At the lowest level of the hierarchy there MUST be a base ``Exception`` and
// base ``Error`` defined within the top-level of the extension's namespace."
 
class CurlException extends \Exception {}
class CurlError extends \Error {}
 
// The HandleException class represents an exception generated by a Handle or MultiHandle.
class HandleException extends CurlException
{
    /** Equivalent to curl_error() or the non-existent curl_multi_error(). */
    protected string $message;
 
    /** Equivalent to curl_errno() or curl_multi_errno(). */
    protected int $code;
}

Curl\Handle class implementation

The Curl\Handle class is a relatively straightforward translation of the non-object-oriented APIs, albeit with a few differences:

  • Instead of returning false, errors are treated as exceptions.
  • curl_exec has two equivalents: fetch, for cases where CURLOPT_RETURNTRANSFER was previously used, and execute, for CURLOPT_FILE.
namespace Curl;
 
class Handle
{
    /** Equivalent to curl_errno(). */
    public private(set) int $errorNumber;
 
    /** Equivalent to curl_error(). */
    public private(set) string $errorMessage;
 
    /**
     * Equivalent to curl_init().
     */
    public function __construct(?string $uri = null);
 
    /**
     * Equivalent to curl_exec(), except it (a) throws exceptions on failure, and (b)
     * always returns the content as a string.
     * 
     * @throws \Curl\HandleException
     */    
    public function fetch(): string;
 
    /**
     * Equivalent to curl_exec(), except it (a) throws exceptions on failure, and (b)
     * will always write to the specified stream resource or write callback.
     * 
     * A callable must have the following signature:
     * 
     * callback(resource $curlHandle, string $data): int
     * 
     * curlHandle
     *   The cURL handle.
     * 
     * data
     *   The data to be written.
     * 
     * The data must be saved by the callback and the callback must return the exact
     * number of bytes written or the transfer will be aborted with an error.
     * 
     * @throws \Curl\HandleException
     */    
    public function execute(resource|callable $out): void
 
    /**
     * Equivalent to curl_getinfo().
     */    
    public function getInfo(?\Curl\Info $option): mixed;
 
    /**
     * Equivalent to curl_pause().
     */    
    public function pause(\Curl\Pause $flag): mixed;
 
    /**
     * Equivalent to curl_reset().
     */    
    public function reset(): void;
 
    /**
     * Equivalent to curl_setopt().
     * 
     * @throws \Curl\HandleException
     */    
    public function setOption(\Curl\HandleOption $opt, mixed $value): \Curl\Handle;
 
    /**
     * Equivalent to curl_escape().
     * 
     * @throws \Curl\HandleException
     */    
    public function escapeUrl(string $string): string;
 
    /**
     * Equivalent to curl_unescape().
     * 
     * @throws \Curl\HandleException
     */    
    public function unescapeUrl(string $string): string;
 
    /**
     * Equivalent to curl_upkeep().
     * 
     * @throws \Curl\HandleException
     */    
    public function upkeep(): void;
}

Curl\MultiHandle class implementation

The Curl\MultiHandle class is again a relatively straightforward translation of the non-object-oriented APIs with the same differences as the Cur\Handle translation.

A few additional notable differences:

  • curl_multi_exec will not return CURLM_CALL_MULTI_PERFORM, and will instead emulate newer curl libraries by internally calling curl until it no longer returns CURLM_CALL_MULTI_PERFORM.
  • Since CURLOPT_RETURNTRANSFER etc. are no longer valid options, addHandle takes a parameter to indicate where PHP should direct the output of the Curl\Handle. A null parameter value retains the CURLOPT_RETURNTRANSFER behavior, where a user must then call getContent.
  • curl_multi_select is called poll.
  • curl_multi_info_read is called getMessages.
namespace Curl;
 
class MultiHandle
{
    /** Equivalent to curl_multi_errno(). */
    public private(set) int $errorNumber;
 
    /** Equivalent to curl_multi_error(), if it existed. */
    public private(set) string $errorMessage;
 
    /**
     * Equivalent to curl_multi_init().
     */
    public function __construct();
 
    /**
     * Equivalent to curl_multi_add_handle().
     * 
     * $out, if specified, will write the output of the Curl\Handle to the stream or callback
     * when the Curl\MultiHandle is executed. If not specified, you can retrieve the content of
     * the handle via the getContent() method.
     * 
     * @throws \Curl\HandleException
     */    
    public function addHandle(\Curl\Handle $handle, resource|callable|null $out = null): void;
 
    /**
     * Equivalent to curl_multi_remove_handle().
     * 
     * @throws \Curl\HandleException
     */    
    public function removeHandle(\Curl\Handle $handle): void;
 
    /**
     * Equivalent to curl_multi_exec(). Note that it will never return CURLM_CALL_MULTI_PERFORM, and will
     * internally emulate newer curl libraries. Returns true if there are still active connections.
     * 
     * @throws \Curl\HandleException
     */    
    public function execute(): bool;
 
    /**
     * Equivalent to curl_multi_getcontent().
     */    
    public function getContent(\Curl\Handle $handle): ?string;
 
    /**
     * Equivalent to curl_multi_info_read().
     */    
    public function getMessages(int &$queued_messages = null): array|false;
 
    /**
     * Equivalent to curl_multi_select().
     */    
    public function poll(float $timeout = 1.0): int
 
    /**
     * Equivalent to curl_multi_setopt().
     * 
     * @throws \Curl\HandleException
     */    
    public function setOption(\Curl\MultiHandleOption $opt, mixed $value): \Curl\MultiHandle;
}

Examples

Curl\Handle:

<?php
 
$ch = new Curl\Handle("https://example.com")
    ->setOption(Curl\HandleOption::ConnectTimeout, 30)
    ->setOption(Curl\HandleOption::FollowLocation, true);
 
try {
    echo $ch->fetch() . "\n";
} catch (\Curl\HandleException $ex) {
    echo "curl error ($ex->code): $ex->message\n";
}

Curl\MultiHandle:

<?php
 
$ch = new Curl\Handle("https://example.com")
    ->setOption(Curl\HandleOption::ConnectTimeout, 30)
    ->setOption(Curl\HandleOption::FollowLocation, true);
 
$mh = new Curl\MultiHandle();
$mh->addHandle($ch);
 
while ($mh->execute()) {
    echo "multi handle is still active...\n";
    $mh->poll(1.0);
}
 
echo $mh->getContent($ch) . "\n";

Backward Incompatible Changes

The Curl namespace will no longer be safe to use, and the aforementioned class names will be reserved, for example:

  • Curl\Handle
  • Curl\MultiHandle
  • Curl\CurlException
  • Curl\HandleOption
  • ...

CurlHandle and other existing classes will still work, as this RFC proposes to alias them to their new namespaced counterparts.

Proposed PHP Version(s)

Next PHP 8.x (currently 8.5, potentially 8.6).

RFC Impact

To the Ecosystem

None expected.

To Existing Extensions

curl will gain new classes, and existing curl classes will gain new methods.

To SAPIs

None expected.

Open Issues

  • Should we organize the curl option enumerations by value type? Or have a single enumeration for all curl_setopt options and all curl_multi_setopt options? Responses from the mailing list were skeptical of the value of typed-based enumerations.
  • Should we provide methods to improve the experience of using curl for HTTP-based transfers? While responses were positive, I would like to leave this for future scope.

Future Scope

There is a strong appetite from the community to simplify basic HTTP operations (GET, POST w/ a form body, POST w/ a JSON body). A future RFC could propose this, with or without a standard Request / Response interface in PHP's core.

Voting Choices

Implement object-oriented curl API as outlined in the RFC?
Real name Yes No
Final result: 0 0
This poll has been closed.

Patches and Tests

TBA, pending initial discussion.

References

Rejected Features

Changelog

  • 2025-07-01
  • 2025-07-07
    • Removed open question around proposing a simple HTTP client, and moved it to the Future Scope section.
    • Updated the addHandle method in the Curl\MultiHandle class to account for the lack of CURLOPT_RETURNTRANSFER, etc. options.
rfc/curl_oop_v2.txt · Last modified: by enorris