====== 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.