====== PHP RFC: Object-oriented curl API v2 ======
* Version: 1.4
* Date: 2025-06-24
* Author: Eric Norris, erictnorris@gmail.com
* Status: Under Discussion
* Implementation: TBA
===== Introduction =====
As part of the [[https://github.com/php/php-tasks/issues/6|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:
* [[curl-oop|PHP RFC: Add OOP methods to Curl objects]]
* [[https://externals.io/message/117958|Discussion about new Curl URL API and ext/curl improvements]]
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?
- [[https://externals.io/message/122371#122408|kalle]]
I was also going to suggest to use enums for the options, and have them be grouped by what value type they need.
- [[https://externals.io/message/122371#122410|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.
- [[https://externals.io/message/122371#122413|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 [[extension_exceptions|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'':
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'':
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 =====
* Yes
* No
===== Patches and Tests =====
TBA, pending initial discussion.
===== References =====
* Previous RFC ([[curl-oop|PHP RFC: Add OOP methods to Curl objects]]) and [[https://externals.io/message/122371|discussion]]
* Previous discussion [[https://externals.io/message/117958|Discussion about new Curl URL API and ext/curl improvements]]
===== Rejected Features =====
===== Changelog =====
* 2025-07-01
* Updated exception hierarchy to conform with [[extension_exceptions|PHP RFC: Throwable Hierarchy Policy for Extensions]].
* Proposed monolithic option enumerations in lieu of smaller typed enumerations.
* Translated ''curl_exec'' to ''fetch'' and ''execute''.
* 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.