(feat): replace raw curl transport with utopia-php/client#137
Conversation
Adapter::request() now builds PSR-7 requests via utopia-php/psr7's Request\Factory and sends them through the client's cURL adapter. Adapter::requestMulti() fans out over Swoole coroutines with a bounded Utopia\Client\Pool instead of curl_multi + HTTP/2 multiplexing. Both keep the legacy result array shape so adapters and tests are unaffected. Also: PHP >= 8.5 and ext-swoole required, Mailgun attachments use multipart Part::file, unused Adapter::getCountryCode() removed along with the libphonenumber dependency, phpstan upgraded to 2.x, and the test image switched to appwrite/utopia-base (ships Swoole, no io_uring).
…rors APNs only accepts HTTP/2, which Swoole's Coroutine\Http\Client cannot negotiate (connection reset). The requestMulti() pool now uses the client's cURL adapter with CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0; Swoole's native-curl hook (enabled by Coroutine\run and by Appwrite's runtime) keeps sends concurrent per coroutine. APNS error extraction now falls back to the transport error or 'Unknown error' instead of passing null to Response::addResult().
Greptile SummaryThis PR replaces the library's raw
Confidence Score: 5/5Safe to merge; the transport swap is complete and correct, concurrency fallback is handled per-slot, and the breaking changes are clearly documented. The core logic changes are well-structured: every coroutine slot is guarded with a Throwable catch that populates the result array, the PSR-18 error path in request() also captures errors into the same array shape, and the buildRequest body-encoding logic faithfully mirrors the old curl behaviour. The two observations noted are non-blocking quality improvements rather than correctness issues. No files require special attention; the two style observations in Adapter.php are non-blocking. Important Files Changed
Reviews (4): Last reviewed commit: "(fix): catch Throwable in requestMulti c..." | Re-trigger Greptile |
Adapter now takes an optional Psr\Http\Client\ClientInterface (constructor param + setClient(), mirroring the telemetry pattern). Consumers can inject any PSR-18 implementation — pooled/retry-decorated utopia clients, or an in-memory mock for network-free tests. When none is injected, the previous defaults are built internally (HTTP/2 cURL adapter; pooled for requestMulti). The User-Agent moved from client config onto the request so injected clients send the same identity. Injected clients must negotiate HTTP/2 for APNs (documented on the constructor).
… docblock requestMulti() uses null-coalescing instead of the elvis operator so an explicit empty-array body is still sent; the result shape docblocks now declare error as string since null is never produced.
Adapter now takes (Closure(): ClientInterface)|null $clientFactory (constructor param + setClientFactory()) instead of a single ClientInterface. requestMulti() always pools internally, calling the factory once per pooled connection — so consumers describe how to build one client and the adapter owns concurrency, removing the inject-a-Pool-or-serialize caveat. The pool is consumed via Pools\Pool::use() directly since consumer factories only promise PSR-18, not the streaming interface Utopia\Client\Pool requires.
What does this PR do?
Replaces the raw
curl_*transport in the baseAdapterwith utopia-php/client (PSR-18). Nothing changes for existing usage — every adapter keeps its API and behavior:Batched sends (FCM, APNS) remain concurrent — now via Swoole coroutines over a bounded connection pool instead of
curl_multi.New: bring your own HTTP client
Adapters accept an optional client factory (
Closure(): ClientInterface), so any PSR-18 implementation can be used — with retries, a proxy, or an in-memory stub for tests. The adapter calls the factory whenever it needs a client and keeps handling pooling/concurrency itself:The parameter lives on the abstract
Adapterconstructor, so today it's reachable from adapter subclasses; exposing it on the concrete adapters is a follow-up. Once threaded through, usage looks like:If no factory is given, a suitable default is built internally. Factory-built clients must be able to negotiate HTTP/2 — APNs rejects HTTP/1.1 (see the constructor doc).
Breaking changes
>=8.1→>=8.5ext-swoolenow requiredAdapter::getCountryCode()removedlibphonenumberdependency. UseCallingCode::fromPhoneNumber()for routingSMSmetadata type is nowarray<string, mixed>Testing
main).composer lintandcomposer analyse(phpstan level 6) pass.Follow-ups
Retrydecorator by default in a future PR.