synaq/curl-bundle
Synaq CurlBundle integrates cURL into Symfony applications, providing a configurable HTTP client service for making requests and handling responses. It simplifies setup and use of cURL-based calls within Symfony projects, making it easier to fetch URLs and communicate with external services.
This is a Symfony2-era bundle (last touched around the Symfony 2.1–2.3 timeframe, zero dependents on Packagist). Treat it as a thin, opinionated wrapper around PHP's ext-curl, not a modern HTTP client. Before anything else, confirm it will even install in your app.
Install and pin
composer require synaq/curl-bundle
If you're on a modern Symfony/PHP stack, add "config": { "platform": { "php": "5.6.40" } } or use composer require --ignore-platform-reqs only long enough to inspect the source. In practice you'll find it's easier to vendor the class than fight resolution.
Register the bundle in app/AppKernel.php (or config/bundles.php if you've backported):
new Synaq\CurlBundle\SynaqCurlBundle(),
Look here first, in this order:
DependencyInjection/Configuration.php — the only authoritative list of config keys (timeout, user_agent, proxy, verify_peer, etc.).Resources/config/services.xml — the real service id you'll inject. Names vary by release (synaq.curl, synaq_curl, synaq_curl.curl); confirm with php app/console debug:container | grep -i curl.Curl/Curl.php (or Curl/CurlRequest.php) — the public methods you actually call.README.md — usually a two-line example; don't trust it over the source.Minimal config (append to app/config/config.yml):
synaq_curl:
timeout: 10
user_agent: "AcmeApp/1.0"
verify_peer: true
First use case — the canonical one is "call a third-party JSON API from a service." Inject the service, do a GET, read the body:
class WeatherService
{
private $curl;
public function __construct($curl)
{
$this->curl = $curl;
}
public function current($city)
{
$this->curl->get('https://api.example.com/weather', ['city' => $city]);
if ($this->curl->getHttpStatus() !== 200) {
throw new \RuntimeException('Weather API failed: '.$this->curl->getError());
}
return json_decode($this->curl->getResponse(), true);
}
}
Wire it with arguments: ["@synaq.curl"] (adjust the id to whatever debug:container reported).
Wrap it, don't sprinkle it. The service holds mutable state (last response, last status, last error), so sharing one instance across a request is asking for crosstalk. The dominant pattern is a per-domain client that news up or clones the curl object per call:
class GithubClient
{
public function __construct(private $curlFactory) {}
private function request(string $method, string $path, array $opts = [])
{
$curl = clone $this->curlFactory; // avoid shared response state
$curl->{$method}('https://api.github.com'.$path, $opts);
$status = $curl->getHttpStatus();
if ($status >= 400) {
throw new ApiException($curl->getResponse(), $status);
}
return json_decode($curl->getResponse(), true);
}
}
Configuration-driven environments. Keep base URLs, timeouts, proxy, and CA bundle paths in parameters.yml and override per environment. If the bundle doesn't ship a base_url key (it usually doesn't), put it in your own client and leave the bundle responsible only for transport concerns.
Verb helpers as thin sugar. The bundle's value is really get/post/put/delete/download plus option merging. Compose them:
// JSON POST
$curl->post($url, json_encode($payload), [
'Content-Type: application/json',
'Accept: application/json',
]);
If the method signature doesn't accept headers, set them via curl_setopt on $curl->getHandle() (most versions expose the raw handle) — but wrap that in your own class so you can drop the bundle later.
Downloads and large payloads. Use the download-style method (or CURLOPT_FILE) and stream to a temp file, then move atomically. Never getResponse() a 200 MB artifact into a PHP string.
Retry and backoff at the client layer. The bundle has no retry, circuit breaker, or middleware. Build a tiny decorator around your client:
for ($try = 0; $try < 3; $try++) {
try { return $client->fetch($url); }
catch (TransientApiException $e) { usleep(100000 * 2 ** $try); }
}
Testing. Because the service is a concrete object with side effects, mock at your client interface, not at synaq.curl. In a Symfony2 functional test, swap the service id for a stub in config_test.yml. Don't try to mock curl_exec — you'll lose a day.
Migrating off it. The cleanest pattern is a seam from day one: define interface HttpTransport { public function request(...): Response; }, implement SynaqCurlTransport, and later swap in symfony/http-client. The bundle's own classes shouldn't leak past that boundary.
services.xml schema, ContainerAware usage) and with PHP 7.4+ in older releases (dynamic properties, each(), missing return types). Vendor the two or three classes you need rather than depending on the Composer package.debug:container. If you reference a wrong id you get a confusing ServiceNotFoundException at compile time, not a helpful message.getHttpStatus() after a different service ran, you'll read stale data. Clone per request or instantiate fresh.curl_exec returns false on transport failure while HTTP 4xx/5xx look like success. Always check both getError()/curl_errno and the HTTP status. A 500 with a JSON body is not an exception unless you make it one.CURLOPT_SSL_VERIFYPEER => false in some environments, or fail on missing CA bundles. Set verify_peer: true and point cacert at a real bundle in config.yml; on Debian/Ubuntu that's /etc/ssl/certs/ca-certificates.crt.CURLOPT_TIMEOUT / CURLOPT_CONNECTTIMEOUT on the handle inside your wrapper.CURLOPT_FOLLOWLOCATION is often off by default; enabling it without CURLOPT_MAXREDIRS turns a misconfigured endpoint into a redirect loop. Cookie handling is per-handle, so a cloned object starts with a fresh jar.get($url, $params) in older releases concatenates with http_build_query, which uses + for spaces. Some strict APIs reject that; pass an already-encoded query string if needed.curl.options config array merged into every request — use that for proxies, user agents, and global headers. For anything per-request, extend or decorate the curl class and rebind the service id to your subclass in services.yml.CURLOPT_VERBOSE => true and a CURLOPT_STDERR stream to a log file, or log curl_getinfo($handle) after each call (timings, effective URL, redirect count). A PSR-3 logger injected into your wrapper beats var_dump inside the bundle forever.CURLOPT_FILE; for uploads use CURLOPT_INFILE/CURLOPT_UPLOAD. Loading a body into a string is the most common production surprise with these wrappers.symfony/http-client or Guzzle will then be a one-file change, and you won't be rewriting every call site.How can I help you explore Laravel packages today?