Legibl docs PHP 0.1.0-dev · Python 0.1.0

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.

CURLOPT_RETURNTRANSFER is not optional

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.

One import per file that calls curl

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.

There is a test that pins this

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

SituationBehaviour
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