yakeing/php_curl
A simple PHP cURL wrapper for making HTTP requests without low-level cURL boilerplate. Supports GET/POST, custom headers, cookies, proxies, and file uploads, helping you fetch remote content or call APIs quickly from any PHP project.
Reality check first. yakeing/php_curl is a small, single-purpose cURL wrapper: 36 stars, last tagged release August 2019, and the repository is archived. MPL-2.0 licensed. There is no service provider, no facade, no config file, and no Laravel integration of any kind — it is a plain PHP class you instantiate yourself. Treat it as a "thin convenience layer over ext-curl", not as an HTTP client you build a codebase around.
Step 1 — read the source before installing. The package is small enough to audit in ten minutes. Open src/ on GitHub and note three things:
Yakeing\Curl\Curl),false/empty-string return, a thrown \Exception, or a curl_error() string. This single detail determines every wrapper you write around it.Step 2 — install and confirm it loads.
composer require yakeing/php_curl
php -r 'require "vendor/autoload.php"; var_dump(class_exists("Yakeing\\Curl\\Curl"));'
If Composer resolves nothing, check the exact Packagist name against the name field in the repo's composer.json — small packages like this are frequently renamed or unpublished, and a 2019 package may declare constraints (php: >=5.6) that are merely stale rather than broken.
Step 3 — first use case. The one thing this package is genuinely convenient for is a stateful cookie-driven flow against a legacy endpoint: log in, keep the session, submit a form, upload a file. That is the demo to run first:
$curl = new \Yakeing\Curl\Curl();
$curl->setCookieFile(storage_path('app/cookies/session.txt'));
$curl->post('https://legacy.example.com/login', [
'user' => config('services.legacy.user'),
'pass' => config('services.legacy.pass'),
]);
$html = $curl->get('https://legacy.example.com/report');
Step 4 — make the build-vs-buy call now, not later. For anything greenfield, Illuminate\Support\Facades\Http (Guzzle under the hood) already covers cookies (->withCookies()), multipart (->attach()), basic/digest auth (->withBasicAuth(), ->withDigestAuth()), retries, timeouts, and Http::fake() testing. Adopt this package only if (a) you are maintaining an existing integration that already depends on it, or (b) you specifically want its cookie-jar or form-building ergonomics on a PHP project with no Laravel HTTP layer. Score 0.15 and an archived repo are a fair summary of the risk.
Never call it directly from controllers, jobs, or commands. Isolate it behind one interface so the eventual migration off an archived dependency is a one-file change.
namespace App\Support\Http;
interface LegacyClient
{
public function get(string $path, array $query = []): string;
public function post(string $path, array $data = []): string;
public function upload(string $path, string $field, string $localFile): string;
}
namespace App\Support\Http;
use Yakeing\Curl\Curl;
class YakeingLegacyClient implements LegacyClient
{
public function __construct(private Curl $curl, private string $baseUrl) {}
public function get(string $path, array $query = []): string
{
return $this->guard(fn () => $this->curl->get($this->url($path, $query)));
}
public function post(string $path, array $data = []): string
{
return $this->guard(fn () => $this->curl->post($this->url($path), $data));
}
public function upload(string $path, string $field, string $localFile): string
{
return $this->guard(fn () => $this->curl->post(
$this->url($path),
[$field => new \CURLFile($localFile)]
));
}
private function url(string $path, array $query = []): string
{
$url = rtrim($this->baseUrl, '/') . '/' . ltrim($path, '/');
return $query ? $url . '?' . http_build_query($query) : $url;
}
private function guard(callable $call): string
{
try {
$body = $call();
} catch (\Throwable $e) {
throw new LegacyTransportException($e->getMessage(), 0, $e);
}
if (! is_string($body) || $body === '') {
throw new LegacyTransportException('Empty or non-string response body.');
}
return $body;
}
}
Bind it once, in a dedicated provider.
// app/Providers/LegacyHttpServiceProvider.php
public function register(): void
{
$this->app->singleton(LegacyClient::class, fn () => new YakeingLegacyClient(
new \Yakeing\Curl\Curl(),
config('services.legacy.base_url'),
));
}
Keep base_url, credentials, and the cookie-jar path in config/services.php / .env; never inline them. Note the singleton caveat below.
Always set timeouts and verify TLS. If the wrapper exposes no timeout setter, reach for the underlying handle if it exposes one, or wrap the call in a job that enforces duration. Do not accept library defaults:
// If the class exposes the handle or option setters, use them:
$curl->setTimeout(10); // CURLOPT_TIMEOUT
$curl->setConnectTimeout(3); // CURLOPT_CONNECTTIMEOUT
// And never ship a production config that disables CURLOPT_SSL_VERIFYPEER.
Run slow calls in queues, with the job timeout above the cURL timeout. A blocking cURL request inside a web request is how you get 30-second TTFB and exhausted PHP-FPM workers:
class PushOrderToLegacy extends ShouldQueue
{
public $timeout = 30;
public $tries = 3;
public $backoff = [5, 30, 120];
public function handle(LegacyClient $client): void
{
$client->post('/orders', $this->order->toLegacyPayload());
}
}
Test at the interface boundary. Http::fake() will not intercept this package — it bypasses Laravel's HTTP layer entirely. Bind a fake LegacyClient in tests instead, and keep a single, small integration test that hits a local stub server for the real wrapper:
$this->app->instance(LegacyClient::class, new FakeLegacyClient(['/report' => '<html/>']));
Migration path. Because everything goes through LegacyClient, swapping in a Guzzle/Http-backed implementation is a new class plus one binding change. Write the interface with the future client in mind (status codes, headers, exceptions) and do not leak the package's return conventions into your domain code.
composer.json, and treat any security issue as yours to fix. If you fork it, MPL-2.0 is file-level copyleft: modifications to its files must be published, but your application code that merely calls it can stay closed. Keep any patched copy in a separate, clearly-identified namespace.retry(), no Laravel Http::pool() concurrency, no Http::fake(). Retries, backoff, and circuit-breaking must be hand-rolled in your wrapper or handled by the queue.curl_errno() / curl_error() alongside the URL.storage_path('app/cookies/'.Str::uuid().'.txt')) and clean it up in a finally.storage_path() is safe; sys_get_temp_dir() is not always present or writable in containerized/read-only filesystems. Create the directory, and remember Laravel's storage:link does not cover app/ subdirectories automatically.CURLFile. The old '@/path/to/file' string syntax was removed in PHP 5.5+. If the package's form builder still concatenates @ prefixes, uploads will silently send a literal string as the field value — one of the most common silent failures in legacy cURL wrappers.Log::debug() in your guard() covering URL, duration, byte length, and error is usually enough. Never log credentials or full cookie contents.composer.json constraints. Old packages sometimes require php: >=5.6 with no upper bound (fine) or, worse, a pin like ~7.0 that breaks installs on PHP 8. Check before you commit; --ignore-platform-reqs is not a fix, it is a postponement.Http:: / Guzzle gives you the same cookie, form, auth, and upload features with active maintenance, first-class testing support, and no MPL considerations.How can I help you explore Laravel packages today?