PHP quickstart
Describes PHP SDK 0.1.0-dev. Requires PHP
7.4.33 or later (8.x supported), with ext-dom,
ext-libxml, ext-json and ext-curl.
Install
The package is not yet on Packagist, so there is no
composer require legibl/legibl yet. Install it from a checkout by
pointing Composer at the SDK directory:
# from your application's composer.json
composer config repositories.legibl path /path/to/legibl/sdk/php
composer require legibl/legibl:@dev
Composer's files autoloader loads src/functions.php,
so the Legibl\curl_* functions exist as soon as
vendor/autoload.php is required. There is no bootstrap call.
Configure
One environment variable is the whole setup:
export LEGIBL_API_KEY=your-key
With no key set the SDK runs in stub mode: it returns a synthetic token and spends nothing. That is deliberate — an unconfigured install cannot bill you — but it also means a missing key looks like a solver that returns wrong answers. See Troubleshooting if that is what you are seeing.
Solve a captcha
five lines, including the import
use function Legibl\{curl_init, curl_setopt, curl_exec, curl_close};
$ch = curl_init('https://example.test/protected');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch); // a challenge here is solved and the request resubmitted
curl_close($ch);
That is the entire integration. $body is either the original
response, or — if the page carried a reCAPTCHA v2, invisible v2 or Turnstile
challenge that could be reached — the response to the resubmitted request with
the solution attached.
With it off, libcurl writes the body straight to output and returns a
bool. There is nothing for the SDK to inspect, so it reports
unavailable and passes the request through unchanged. This is the
single most common integration failure —
the full explanation is here.
What adoption actually costs
This is stated precisely because it is easy to state imprecisely, and the difference decides how large a rollout is.
The use function Legibl\{...} import is required once
in every file that calls curl — not once per project, and not once
per class hierarchy. PHP's namespace fallback does not route calls
through the SDK on its own.
Why fallback is not enough
PHP resolves an unqualified function call inside a namespace by looking in
the current namespace first, then falling back to the global namespace. Inside
namespace App\Scraper;, a bare curl_exec() resolves to
App\Scraper\curl_exec and then to \curl_exec. It never
reaches Legibl\curl_exec, because Legibl is not in
that chain.
What does the work is the use function import, which aliases
those names within that file only. That is still a one-line
change — it is just one line per file.
sdk/php/tests/AdoptionMechanismTest.php deliberately omits the
import and asserts that the SDK's transport is never consulted and no outcome
is recorded — the calls go straight to libcurl. If this page is ever
“corrected” to claim that fallback alone is sufficient, that test
is the counter-evidence.
Code that is not namespaced
Global-namespace code has no import to add: call the functions by their qualified names. This is also the right choice anywhere you want the routing to be visible at the call site.
$ch = \Legibl\curl_init('https://example.test/protected');
\Legibl\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = \Legibl\curl_exec($ch);
\Legibl\curl_close($ch);
Planning a rollout
Count the files that call curl_exec, not the projects:
grep -rl 'curl_exec' --include='*.php' src/ | wc -l
Each of those files needs one import line. Files that only pass a handle around, or that never call curl themselves, need nothing.
Partial adoption is reported, not guessed at
Resubmitting a blocked request means reconstructing it, and libcurl does not
let you read most options back — so the SDK records them as you set them. If
you import curl_exec but configure the handle through the global
curl_setopt, the SDK cannot rebuild the request. Rather than
resubmitting a guess, it records:
outcome: unavailable
reason: the request cannot be reconstructed for resubmission; the handle was
configured through the global curl_setopt rather than Legibl\curl_setopt
Import all four functions together, or none of them.
What the drop-ins will not do
| Situation | Behaviour |
|---|---|
CURLOPT_RETURNTRANSFER off |
unavailable; request passed through. The body went to
output and there is nothing to inspect. |
CURLOPT_WRITEFUNCTION or CURLOPT_FILE set |
unavailable, for the same reason. |
Handle configured through the global curl_setopt |
unavailable; the request cannot be reconstructed. |
Legibl\curl_multi_exec() |
Throws Legibl\Exception\LegiblException.
The multi-handle API is not wrapped, and forwarding it silently would
leave the SDK looking installed while every challenge on those requests
passed unhandled. |
Confirm it is actually working
The SDK is deliberately quiet, which means a broken integration and a clean run look identical. Wire up an observer once, at bootstrap, before you believe anything:
use Legibl\Interceptor;
use Legibl\Observer\CallbackObserver;
use Legibl\Outcome;
$observer = new CallbackObserver(function (Outcome $o, ?string $domain, float $secs) {
error_log(sprintf('legibl %s %s %.2fs %s', $o->status, $domain, $secs, $o->reason ?? ''));
});
Interceptor::setInstance(Interceptor::fromConfig(null, $observer));
And log the resolved configuration — it is built to be safe to log, reporting
api_key_set as a boolean and proxy domains only, never
credentials:
error_log(json_encode(\Legibl\Config::fromEnvironment()->describe()));
// {"mode":"live","service_url":"https://api.legibl.io","api_key_set":true, ... ,"impersonation":"inactive"}
Next
- Configuration & modes — every environment variable, the four modes, proxies and observability.
- Errors & outcomes — each typed exception and its corrective action.
- Transport impersonation — browser-shaped
TLS via
LD_PRELOAD, for targets that refuse PHP's curl before any captcha appears.