leaseweb/api-caller-bundle
Adds a configurable cURL API caller to Symfony2 applications, supporting GET, POST, PUT and DELETE requests with JSON or HTML responses. It offers full debugging capabilities and simple service-based usage from your controllers.
Decide first whether you can actually install it. This is a Symfony 2 bundle, not a Laravel package. LswApiCallerBundle extends Symfony\Component\HttpKernel\Bundle\Bundle and registers a DataCollector against symfony/web-profiler-bundle. In a Laravel app there is nothing to register it with. So there are only two valid entry points:
composer.json. Read the source (ApiCaller, the Call/* classes) and port the pattern to Laravel's Http facade + Telescope's HTTP watcher. The portable part is roughly 200 lines.composer require leaseweb/api-caller-bundle
php -m | grep curl # must show "curl"
Register in app/AppKernel.php:
new Lsw\ApiCallerBundle\LswApiCallerBundle(),
Then look at two things before writing code: DependencyInjection/Configuration.php (to see what api_caller.options keys actually exist in the version you installed) and one call class, e.g. Call/HttpPostJsonBody.php, to confirm constructor argument order — the README shows (url, body, assoc, parameters) but the parameter's meaning (query vs. headers vs. cURL opts) is only visible in the source.
First use case: pick exactly one existing outbound call — a flaky partner endpoint you already have ad-hoc cURL for — and rewrite it as:
$output = $this->get('api_caller')->call(
new HttpGetJson('https://partner.example.com/v1/status', ['id' => $id])
);
Then open the Web Debug Toolbar. If you see the wall-socket panel with the request, response, and timing, the bundle is delivering its one real value. If not, stop — you have the wrapper without the debugging, and the wrapper alone isn't worth the Symfony2 coupling.
The equivalent day-one setup is a single named client:
// config/services.php or a dedicated client class
Http::baseUrl('https://partner.example.com')
->withToken(config('services.partner.token'))
->timeout(10)
->withHeaders(['User-Agent' => 'App API Caller']);
Then verify php artisan telescope:install is enabled and the Requests watcher is on — that is your replacement for the toolbar panel. Confirm one real call appears there before building further.
Never call api_caller from a controller or model. Inject it into one class per external API. This is the single highest-value discipline, because it's the pattern the bundle is trying to teach:
class PartnerClient
{
public function __construct(private ApiCallerInterface $apiCaller) {}
public function getStatus(string $id): StatusDto
{
$raw = $this->apiCaller->call(new HttpGetJson($this->base.'/status', ['id' => $id]));
return StatusDto::fromArray($raw);
}
}
Every call site then goes through a named method with a typed return, and the debug panel entries become attributable to a feature rather than anonymous cURL noise.
Pick the call type by the wire format, not by the HTTP verb.
| Need | Call class |
|---|---|
| GET, JSON response | HttpGetJson |
| POST with query/params only | HttpPostJson |
| POST with a structured body | HttpPostJsonBody (json_encodes arg 2) |
| PUT / DELETE | HttpPutJson / HttpDeleteJson |
| GET returning a page/HTML | HttpGetHtml |
The common mistake is reaching for HttpPostJson when the API expects a JSON body — that's what HttpPostJsonBody is for, and its third argument controls assoc decoding.
Centralize config, override per environment. Set api_caller.options in parameters.yml / config.yml and let parameters_dev.yml carry a longer timeout. Because these are container parameters, a timeout: 10 set globally means one slow partner blocks a request for ten full seconds — set per-client timeouts by overriding the parameter in a compiler pass or by passing options per call where the class supports it.
Wrap it for failure. call() is a thin cURL wrapper with no retry, backoff, or exception policy. Put that in the client class:
for ($i = 0; $i < 3; $i++) {
$raw = $this->apiCaller->call(new HttpGetJson($url, $params));
if ($raw !== null && $raw !== false) break;
usleep(100_000 * 2 ** $i);
}
In Laravel, mirror this exactly with a PartnerClient that wraps Http:: and does ->retry(3, 100, throw: false). The architectural lesson — one client class per API, typed returns, central config, observability at the boundary — transfers one-for-one. Only the transport changes.
Testing. In Symfony, stub the api_caller service with a fake ApiCallerInterface in the test container. In Laravel, Http::fake([...]) gives you the same seam with zero extra code — another reason the port wins.
The SSL defaults changed and the README is stale. The README's sample config shows ssl_verifypeer: false, but release v1.2.8 explicitly enabled peer verification to stop servers rejecting insecure connections. If you copy the README's YAML you are re-disabling TLS verification. Check DependencyInjection/Configuration.php in your installed version and keep verification on.
sslversion: 3 is a red flag, not a default to preserve. It exists to dodge old cURL auto-negotiation bugs. On any modern stack it forces a weaker protocol. Override it.
timeout is transport plus execution. There is no separate connect timeout, so a hanging DNS lookup or TLS handshake consumes the same budget as a slow response. If you need distinct connect/read budgets, this wrapper can't express it — set timeout low and retry.
No retry, no backoff, no circuit breaking. Treat every call() as a single attempt that may return null/false. Always null-check the return; the bundle does not throw on HTTP 4xx/5xx, so a 404 and an empty successful response can look identical unless you inspect the payload.
followlocation: true with no redirect cap. Cross-host redirects can leak Authorization headers and silently double your latency. For authenticated partner APIs, consider followlocation: false and handling redirects explicitly.
The toolbar is dev-only — do not build incident response on it. The panel exists because the profiler is enabled; in production it shows nothing. And note what it does capture: full request and response bodies, in the profiler's request store. If those bodies contain tokens or PII, you've just written credentials to the dev cache. Scrub at the client layer or accept it explicitly.
Large response bodies bloat the profiler. A multi-megabyte JSON download goes straight into the data collector. Cap what you fetch through this path.
Extension points, if you must extend. Subclass one of the Call/* classes to add headers or signing, or decorate the api_caller service to add logging/metrics around call(). Both are supported by the DI config and both are small. Do not fork the bundle to add retry — it belongs in your client class.
Expect deprecation noise on PHP 7.4+. The v1.2.11 release reinstated PHP 5.3 support for BC reasons, so the codebase carries PHP 5.3-era idioms. On anything newer you'll see notices; on PHP 8 you may see real breakage. Budget for it, or don't install.
64 stars, 0 dependents, last release 2018. There is no security-response path. If you fork or port, assign an owner and a sunset date now, before it's load-bearing.
How can I help you explore Laravel packages today?